diff --git a/.github/update.log b/.github/update.log index ff8b2a0176..940dbe0b0f 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1141,3 +1141,4 @@ Update On Wed Oct 1 20:41:09 CEST 2025 Update On Thu Oct 2 20:40:58 CEST 2025 Update On Fri Oct 3 20:39:08 CEST 2025 Update On Sat Oct 4 20:34:01 CEST 2025 +Update On Sun Oct 5 20:34:00 CEST 2025 diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json index f98f6bd0a5..dc818daf4d 100644 --- a/clash-nyanpasu/manifest/version.json +++ b/clash-nyanpasu/manifest/version.json @@ -2,7 +2,7 @@ "manifest_version": 1, "latest": { "mihomo": "v1.19.14", - "mihomo_alpha": "alpha-40e0813", + "mihomo_alpha": "alpha-d225625", "clash_rs": "v0.9.1", "clash_premium": "2023-09-05-gdcc8d87", "clash_rs_alpha": "0.9.1-alpha+sha.f613a53" @@ -69,5 +69,5 @@ "linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf" } }, - "updated_at": "2025-09-30T22:20:51.860Z" + "updated_at": "2025-10-04T22:20:46.044Z" } diff --git a/clash-verge-rev/.devcontainer/devcontainer.json b/clash-verge-rev/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..3c883b6c94 --- /dev/null +++ b/clash-verge-rev/.devcontainer/devcontainer.json @@ -0,0 +1,101 @@ +{ + "name": "Clash Verge Rev Development Environment", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04", + + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "20" + }, + "ghcr.io/devcontainers/features/rust:1": { + "version": "latest", + "profile": "default" + }, + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer", + "tauri-apps.tauri-vscode", + "ms-vscode.vscode-typescript-next", + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss", + "ms-vscode.vscode-json", + "redhat.vscode-yaml", + "formulahendry.auto-rename-tag", + "ms-vscode.hexeditor", + "christian-kohler.path-intellisense", + "yzhang.markdown-all-in-one", + "streetsidesoftware.code-spell-checker", + "ms-vscode.vscode-eslint" + ], + "settings": { + "rust-analyzer.cargo.features": ["verge-dev"], + "rust-analyzer.check.command": "clippy", + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } + } + } + }, + + "forwardPorts": [1420, 3000, 8080, 9090, 7890, 7891], + + "portsAttributes": { + "1420": { + "label": "Tauri Dev Server", + "onAutoForward": "notify" + }, + "3000": { + "label": "Vite Dev Server", + "onAutoForward": "notify" + }, + "7890": { + "label": "Clash HTTP Proxy", + "onAutoForward": "silent" + }, + "7891": { + "label": "Clash SOCKS Proxy", + "onAutoForward": "silent" + }, + "9090": { + "label": "Clash API", + "onAutoForward": "silent" + } + }, + + "postCreateCommand": "bash .devcontainer/post-create.sh", + + "mounts": [ + "source=clash-verge-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume", + "source=clash-verge-cargo-registry,target=/usr/local/cargo/registry,type=volume", + "source=clash-verge-cargo-git,target=/usr/local/cargo/git,type=volume" + ], + + "containerEnv": { + "RUST_BACKTRACE": "1", + "NODE_OPTIONS": "--max-old-space-size=4096", + "TAURI_DEV_WATCHER_IGNORE_FILE": ".taurignore" + }, + + "remoteUser": "vscode", + "workspaceFolder": "/workspaces/clash-verge-rev", + "shutdownAction": "stopContainer" +} diff --git a/clash-verge-rev/.github/ISSUE_TEMPLATE/config.yml b/clash-verge-rev/.github/ISSUE_TEMPLATE/config.yml index e49cbe6479..b640f5e662 100644 --- a/clash-verge-rev/.github/ISSUE_TEMPLATE/config.yml +++ b/clash-verge-rev/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,4 @@ +blank_issues_enabled: false contact_links: - name: 讨论交流 / Communication url: https://t.me/clash_verge_rev diff --git a/clash-verge-rev/.github/workflows/alpha.yml b/clash-verge-rev/.github/workflows/alpha.yml index 6c21bc2289..41dcec4d84 100644 --- a/clash-verge-rev/.github/workflows/alpha.yml +++ b/clash-verge-rev/.github/workflows/alpha.yml @@ -294,6 +294,15 @@ jobs: sudo apt-get update sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf + - name: Install x86 OpenSSL (macOS only) + if: matrix.target == 'x86_64-apple-darwin' + run: | + arch -x86_64 brew install openssl@3 + echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV + echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV + echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV + - name: Install Node uses: actions/setup-node@v4 with: diff --git a/clash-verge-rev/.github/workflows/autobuild-check-test.yml b/clash-verge-rev/.github/workflows/autobuild-check-test.yml new file mode 100644 index 0000000000..ea827db229 --- /dev/null +++ b/clash-verge-rev/.github/workflows/autobuild-check-test.yml @@ -0,0 +1,133 @@ +name: Autobuild Check Logic Test + +on: + workflow_dispatch: + +jobs: + check_autobuild_logic: + name: Check Autobuild Should Run Logic + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Check if version or source changed, or assets already exist + id: check + run: | + # # 仅用于测试逻辑,手动触发自动跳过 + # if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # echo "should_run=skip" >> $GITHUB_OUTPUT + # echo "🟡 手动触发,跳过 should_run 检查" + # exit 0 + # fi + + # 确保有 HEAD~1 + if ! git rev-parse HEAD~1 > /dev/null 2>&1; then + echo "should_run=true" >> $GITHUB_OUTPUT + echo "🟢 没有前一个提交,默认需要构建" + exit 0 + fi + + # 版本号变更判断 + CURRENT_VERSION=$(jq -r '.version' package.json) + PREVIOUS_VERSION=$(git show HEAD~1:package.json | jq -r '.version' 2>/dev/null || echo "") + + if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then + echo "should_run=true" >> $GITHUB_OUTPUT + echo "🟢 版本号变更: $PREVIOUS_VERSION → $CURRENT_VERSION" + exit 0 + fi + + # 检查 src 变更(排除常见产物与缓存) + SRC_DIFF=$(git diff --name-only HEAD~1 HEAD -- src/ | grep -Ev '^src/(dist|build|node_modules|\.next|\.cache)' || true) + TAURI_DIFF=$(git diff --name-only HEAD~1 HEAD -- src-tauri/ | grep -Ev '^src-tauri/(target|node_modules|dist|\.cache)' || true) + + if [ -n "$SRC_DIFF" ] || [ -n "$TAURI_DIFF" ]; then + echo "should_run=true" >> $GITHUB_OUTPUT + echo "🟢 源码变更 detected" + exit 0 + fi + + # 找到最后一个修改 Tauri 相关文件的 commit + echo "🔍 查找最后一个 Tauri 相关变更的 commit..." + + LAST_TAURI_COMMIT="" + for commit in $(git rev-list HEAD --max-count=50); do + # 检查此 commit 是否修改了 Tauri 相关文件 + CHANGED_FILES=$(git show --name-only --pretty=format: $commit | tr '\n' ' ') + HAS_TAURI_CHANGES=false + + # 检查各个模式 + if echo "$CHANGED_FILES" | grep -q "src/" && echo "$CHANGED_FILES" | grep -qvE "src/(dist|build|node_modules|\.next|\.cache)"; then + HAS_TAURI_CHANGES=true + elif echo "$CHANGED_FILES" | grep -qE "src-tauri/(src|Cargo\.(toml|lock)|tauri\..*\.conf\.json|build\.rs|capabilities)"; then + HAS_TAURI_CHANGES=true + fi + + if [ "$HAS_TAURI_CHANGES" = true ]; then + LAST_TAURI_COMMIT=$(git rev-parse --short $commit) + break + fi + done + + if [ -z "$LAST_TAURI_COMMIT" ]; then + echo "⚠️ 最近的 commits 中未找到 Tauri 相关变更,使用当前 commit" + LAST_TAURI_COMMIT=$(git rev-parse --short HEAD) + fi + + CURRENT_COMMIT=$(git rev-parse --short HEAD) + echo "📝 最后 Tauri 相关 commit: $LAST_TAURI_COMMIT" + echo "📝 当前 commit: $CURRENT_COMMIT" + + # 检查 autobuild release 是否存在 + AUTOBUILD_RELEASE_EXISTS=$(gh release view "autobuild" --json id -q '.id' 2>/dev/null || echo "") + + if [ -z "$AUTOBUILD_RELEASE_EXISTS" ]; then + echo "should_run=true" >> $GITHUB_OUTPUT + echo "🟢 没有 autobuild release,需构建" + else + # 检查 latest.json 是否存在 + LATEST_JSON_EXISTS=$(gh release view "autobuild" --json assets -q '.assets[] | select(.name == "latest.json") | .name' 2>/dev/null || echo "") + + if [ -z "$LATEST_JSON_EXISTS" ]; then + echo "should_run=true" >> $GITHUB_OUTPUT + echo "🟢 没有 latest.json,需构建" + else + # 下载并解析 latest.json 检查版本和 commit hash + echo "📥 下载 latest.json 检查版本..." + LATEST_JSON_URL=$(gh release view "autobuild" --json assets -q '.assets[] | select(.name == "latest.json") | .browser_download_url' 2>/dev/null) + + if [ -n "$LATEST_JSON_URL" ]; then + LATEST_JSON_CONTENT=$(curl -s "$LATEST_JSON_URL" 2>/dev/null || echo "") + + if [ -n "$LATEST_JSON_CONTENT" ]; then + LATEST_VERSION=$(echo "$LATEST_JSON_CONTENT" | jq -r '.version' 2>/dev/null || echo "") + echo "📦 最新 autobuild 版本: $LATEST_VERSION" + + # 从版本字符串中提取 commit hash (格式: X.Y.Z+autobuild.MMDD.commit) + LATEST_COMMIT=$(echo "$LATEST_VERSION" | sed -n 's/.*+autobuild\.[0-9]\{4\}\.\([a-f0-9]*\)$/\1/p' || echo "") + echo "📝 最新 autobuild commit: $LATEST_COMMIT" + + if [ "$LAST_TAURI_COMMIT" != "$LATEST_COMMIT" ]; then + echo "should_run=true" >> $GITHUB_OUTPUT + echo "🟢 Tauri commit hash 不匹配 ($LAST_TAURI_COMMIT != $LATEST_COMMIT),需构建" + else + echo "should_run=false" >> $GITHUB_OUTPUT + echo "🔴 相同 Tauri commit hash ($LAST_TAURI_COMMIT),不需构建" + fi + else + echo "should_run=true" >> $GITHUB_OUTPUT + echo "⚠️ 无法下载或解析 latest.json,需构建" + fi + else + echo "should_run=true" >> $GITHUB_OUTPUT + echo "⚠️ 无法获取 latest.json 下载 URL,需构建" + fi + fi + fi + + - name: Output should_run result + run: | + echo "Result: ${{ steps.check.outputs.should_run }}" diff --git a/clash-verge-rev/.github/workflows/autobuild.yml b/clash-verge-rev/.github/workflows/autobuild.yml index c3796f4911..f35eda2f09 100644 --- a/clash-verge-rev/.github/workflows/autobuild.yml +++ b/clash-verge-rev/.github/workflows/autobuild.yml @@ -3,8 +3,8 @@ name: Auto Build on: workflow_dispatch: schedule: - # UTC+8 0,6,12,18 - - cron: "0 16,22,4,10 * * *" + # UTC+8 12:00, 18:00 -> UTC 4:00, 10:00 + - cron: "0 4,10 * * *" permissions: write-all env: TAG_NAME: autobuild @@ -18,55 +18,10 @@ concurrency: jobs: check_commit: name: Check Commit Needs Build - runs-on: ubuntu-latest - outputs: - should_run: ${{ steps.check.outputs.should_run }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 2 - - - name: Check if version changed or src changed - id: check - run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "should_run=true" >> $GITHUB_OUTPUT - exit 0 - fi - - CURRENT_VERSION=$(cat package.json | jq -r '.version') - echo "Current version: $CURRENT_VERSION" - - git checkout HEAD~1 package.json - PREVIOUS_VERSION=$(cat package.json | jq -r '.version') - echo "Previous version: $PREVIOUS_VERSION" - - git checkout HEAD package.json - - if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then - echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION" - echo "should_run=true" >> $GITHUB_OUTPUT - exit 0 - fi - - CURRENT_SRC_HASH=$(git rev-parse HEAD:src) - PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "") - CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "") - PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "") - - echo "Current src hash: $CURRENT_SRC_HASH" - echo "Previous src hash: $PREVIOUS_SRC_HASH" - echo "Current tauri hash: $CURRENT_TAURI_HASH" - echo "Previous tauri hash: $PREVIOUS_TAURI_HASH" - - if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then - echo "Source directories changed" - echo "should_run=true" >> $GITHUB_OUTPUT - else - echo "Version and source directories unchanged" - echo "should_run=false" >> $GITHUB_OUTPUT - fi + uses: clash-verge-rev/clash-verge-rev/.github/workflows/check-commit-needs-build.yml@dev + with: + tag_name: autobuild + force_build: ${{ github.event_name == 'workflow_dispatch' }} update_tag: name: Update tag @@ -95,9 +50,28 @@ jobs: fi shell: bash + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Release AutoBuild Version + run: pnpm release-version autobuild-latest + - name: Set Env run: | echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV + VERSION=$(jq -r .version package.json) + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV shell: bash - run: | @@ -111,25 +85,24 @@ jobs: cat > release.txt << EOF $UPDATE_LOGS - ## 我应该下载哪个版本? - - ### MacOS - - MacOS intel芯片: x64.dmg - - MacOS apple M芯片: aarch64.dmg - - ### Linux - - Linux 64位: amd64.deb/amd64.rpm - - Linux arm64 architecture: arm64.deb/aarch64.rpm - - Linux armv7架构: armhf.deb/armhfp.rpm + ## 下载地址 ### Windows (不再支持Win7) #### 正常版本(推荐) - - 64位: x64-setup.exe - - arm64架构: arm64-setup.exe - #### 便携版问题很多不再提供 + - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) + #### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) - - 64位: x64_fixed_webview2-setup.exe - - arm64架构: arm64_fixed_webview2-setup.exe + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe) + + ### macOS + - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg) + + ### Linux + #### DEB包(Debian系) 使用 apt ./路径 安装 + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) + + #### RPM包(Redhat系) 使用 dnf ./路径 安装 + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm) ### FAQ - [常见问题](https://clash-verge-rev.github.io/faq/windows.html) @@ -152,29 +125,18 @@ jobs: clean_old_assets: name: Clean Old Release Assets - runs-on: ubuntu-latest - needs: update_tag - if: ${{ needs.update_tag.result == 'success' }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Remove old assets from release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG_NAME: ${{ env.TAG_NAME }} - run: | - VERSION=$(cat package.json | jq -r '.version') - assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true) - for asset in $assets; do - if [[ "$asset" != *"$VERSION"* ]]; then - echo "Deleting old asset: $asset" - gh release delete-asset "$TAG_NAME" "$asset" -y - fi - done + needs: [check_commit, update_tag] + if: ${{ needs.check_commit.outputs.should_run == 'true' && needs.update_tag.result == 'success' }} + + uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev + with: + tag_name: autobuild + dry_run: false autobuild-x86-windows-macos-linux: name: Autobuild x86 Windows, MacOS and Linux - needs: update_tag + needs: [check_commit, update_tag] + if: ${{ needs.check_commit.outputs.should_run == 'true' }} strategy: fail-fast: false matrix: @@ -214,6 +176,15 @@ jobs: sudo apt-get update sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf + - name: Install x86 OpenSSL (macOS only) + if: matrix.target == 'x86_64-apple-darwin' + run: | + arch -x86_64 brew install openssl@3 + echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV + echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV + echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV + - uses: pnpm/action-setup@v4 name: Install pnpm with: @@ -231,9 +202,9 @@ jobs: pnpm run prebuild ${{ matrix.target }} - name: Release ${{ env.TAG_CHANNEL }} Version - run: pnpm release-version ${{ env.TAG_NAME }} + run: pnpm release-version autobuild-latest - - name: Tauri build + - name: Tauri build for Windows-macOS-Linux uses: tauri-apps/tauri-action@v0 env: NODE_OPTIONS: "--max_old_space_size=4096" @@ -254,10 +225,12 @@ jobs: prerelease: true tauriScript: pnpm args: --target ${{ matrix.target }} + # includeUpdaterJson: true autobuild-arm-linux: name: Autobuild ARM Linux - needs: update_tag + needs: [check_commit, update_tag] + if: ${{ needs.check_commit.outputs.should_run == 'true' }} strategy: fail-fast: false matrix: @@ -304,7 +277,7 @@ jobs: pnpm run prebuild ${{ matrix.target }} - name: Release ${{ env.TAG_CHANNEL }} Version - run: pnpm release-version ${{ env.TAG_NAME }} + run: pnpm release-version autobuild-latest - name: Setup for linux run: | @@ -355,7 +328,7 @@ jobs: gcc-arm-linux-gnueabihf \ g++-arm-linux-gnueabihf - - name: Build for Linux + - name: Tauri Build for Linux run: | export PKG_CONFIG_ALLOW_CROSS=1 if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then @@ -391,7 +364,8 @@ jobs: autobuild-x86-arm-windows_webview2: name: Autobuild x86 and ARM Windows with WebView2 - needs: update_tag + needs: [check_commit, update_tag] + if: ${{ needs.check_commit.outputs.should_run == 'true' }} strategy: fail-fast: false matrix: @@ -435,7 +409,7 @@ jobs: pnpm run prebuild ${{ matrix.target }} - name: Release ${{ env.TAG_CHANNEL }} Version - run: pnpm release-version ${{ env.TAG_NAME }} + run: pnpm release-version autobuild-latest - name: Download WebView2 Runtime run: | @@ -444,7 +418,7 @@ jobs: Remove-Item .\src-tauri\tauri.windows.conf.json Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json - - name: Tauri build + - name: Tauri build for Windows id: build uses: tauri-apps/tauri-action@v0 env: @@ -455,6 +429,7 @@ jobs: with: tauriScript: pnpm args: --target ${{ matrix.target }} + # includeUpdaterJson: true - name: Rename run: | @@ -489,3 +464,107 @@ jobs: run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + notify-telegram: + name: Notify Telegram + runs-on: ubuntu-latest + needs: + [ + update_tag, + autobuild-x86-windows-macos-linux, + autobuild-arm-linux, + autobuild-x86-arm-windows_webview2, + ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Fetch UPDATE logs + id: fetch_update_logs + run: | + if [ -f "UPDATELOG.md" ]; then + UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) + if [ -n "$UPDATE_LOGS" ]; then + echo "Found update logs" + echo "UPDATE_LOGS<> $GITHUB_ENV + echo "$UPDATE_LOGS" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + else + echo "No update sections found in UPDATELOG.md" + fi + else + echo "UPDATELOG.md file not found" + fi + shell: bash + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "22" + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Release AutoBuild Version + run: pnpm release-version autobuild-latest + + - name: Get Version and Release Info + run: | + sudo apt-get update + sudo apt-get install jq + echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV + echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV + echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV + + - name: Generate release.txt + run: | + if [ -z "$UPDATE_LOGS" ]; then + echo "No update logs found, using default message" + UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon." + else + echo "Using found update logs" + fi + + cat > release.txt << EOF + $UPDATE_LOGS + + ## 下载地址 + + ### Windows (不再支持Win7) + #### 正常版本(推荐) + - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) + + #### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe) + + ### macOS + - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg) + + ### Linux + #### DEB包(Debian系) 使用 apt ./路径 安装 + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) + + #### RPM包(Redhat系) 使用 dnf ./路径 安装 + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm) + + ### FAQ + - [常见问题](https://clash-verge-rev.github.io/faq/windows.html) + + ### 稳定机场VPN推荐 + - [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6) + + Created at ${{ env.BUILDTIME }}. + EOF + + - name: Send Telegram Notification + run: node scripts/telegram.mjs + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + BUILD_TYPE: autobuild + VERSION: ${{ env.VERSION }} + DOWNLOAD_URL: ${{ env.DOWNLOAD_URL }} diff --git a/clash-verge-rev/.github/workflows/check-commit-needs-build.yml b/clash-verge-rev/.github/workflows/check-commit-needs-build.yml new file mode 100644 index 0000000000..e625319b61 --- /dev/null +++ b/clash-verge-rev/.github/workflows/check-commit-needs-build.yml @@ -0,0 +1,159 @@ +name: Check Commit Needs Build + +on: + workflow_dispatch: + inputs: + tag_name: + description: "Release tag name to check against (default: autobuild)" + required: false + default: "autobuild" + type: string + force_build: + description: "Force build regardless of checks" + required: false + default: false + type: boolean + workflow_call: + inputs: + tag_name: + description: "Release tag name to check against (default: autobuild)" + required: false + default: "autobuild" + type: string + force_build: + description: "Force build regardless of checks" + required: false + default: false + type: boolean + outputs: + should_run: + description: "Whether the build should run" + value: ${{ jobs.check_commit.outputs.should_run }} + last_tauri_commit: + description: "The last commit hash with Tauri-related changes" + value: ${{ jobs.check_commit.outputs.last_tauri_commit }} + autobuild_version: + description: "The generated autobuild version string" + value: ${{ jobs.check_commit.outputs.autobuild_version }} + +permissions: + contents: read + actions: read + +env: + TAG_NAME: ${{ inputs.tag_name || 'autobuild' }} + +jobs: + check_commit: + name: Check Commit Needs Build + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.check.outputs.should_run }} + last_tauri_commit: ${{ steps.check.outputs.last_tauri_commit }} + autobuild_version: ${{ steps.check.outputs.autobuild_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 50 + + - name: Check if version changed or src changed + id: check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Force build if requested + if [ "${{ inputs.force_build }}" == "true" ]; then + echo "🚀 Force build requested" + echo "should_run=true" >> $GITHUB_OUTPUT + exit 0 + fi + + CURRENT_VERSION=$(cat package.json | jq -r '.version') + echo "📦 Current version: $CURRENT_VERSION" + + git checkout HEAD~1 package.json + PREVIOUS_VERSION=$(cat package.json | jq -r '.version') + echo "📦 Previous version: $PREVIOUS_VERSION" + + git checkout HEAD package.json + + if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then + echo "✅ Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION" + echo "should_run=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Use get_latest_tauri_commit.bash to find the latest Tauri-related commit + echo "🔍 Finding last commit with Tauri-related changes using script..." + + # Make script executable + chmod +x scripts-workflow/get_latest_tauri_commit.bash + + # Get the latest Tauri-related commit hash (full hash) + LAST_TAURI_COMMIT_FULL=$(./scripts-workflow/get_latest_tauri_commit.bash) + if [[ $? -ne 0 ]] || [[ -z "$LAST_TAURI_COMMIT_FULL" ]]; then + echo "❌ Failed to get Tauri-related commit, using current commit" + LAST_TAURI_COMMIT_FULL=$(git rev-parse HEAD) + fi + + # Get short hash for display and version tagging + LAST_TAURI_COMMIT=$(git rev-parse --short "$LAST_TAURI_COMMIT_FULL") + + echo "📝 Last Tauri-related commit: $LAST_TAURI_COMMIT" + + # Generate autobuild version using autobuild-latest format + CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g') + MONTH=$(TZ=Asia/Shanghai date +%m) + DAY=$(TZ=Asia/Shanghai date +%d) + AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}" + + echo "🏷️ Autobuild version: $AUTOBUILD_VERSION" + echo "📝 Last Tauri commit: $LAST_TAURI_COMMIT" + + # Set outputs for other jobs to use + echo "last_tauri_commit=$LAST_TAURI_COMMIT" >> $GITHUB_OUTPUT + echo "autobuild_version=$AUTOBUILD_VERSION" >> $GITHUB_OUTPUT + + # Check if autobuild release exists + echo "🔍 Checking autobuild release and latest.json..." + AUTOBUILD_RELEASE_EXISTS=$(gh release view "${{ env.TAG_NAME }}" --json id -q '.id' 2>/dev/null || echo "") + + if [ -z "$AUTOBUILD_RELEASE_EXISTS" ]; then + echo "✅ No autobuild release exists, build needed" + echo "should_run=true" >> $GITHUB_OUTPUT + else + # Check if latest.json exists in the release + LATEST_JSON_EXISTS=$(gh release view "${{ env.TAG_NAME }}" --json assets -q '.assets[] | select(.name == "latest.json") | .name' 2>/dev/null || echo "") + + if [ -z "$LATEST_JSON_EXISTS" ]; then + echo "✅ No latest.json found in autobuild release, build needed" + echo "should_run=true" >> $GITHUB_OUTPUT + else + # Download and parse latest.json to check version and commit hash + echo "📥 Downloading latest.json to check version..." + LATEST_JSON_URL="https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild/latest.json" + + LATEST_JSON_CONTENT=$(curl -sL "$LATEST_JSON_URL" 2>/dev/null || echo "") + + if [ -n "$LATEST_JSON_CONTENT" ]; then + LATEST_VERSION=$(echo "$LATEST_JSON_CONTENT" | jq -r '.version' 2>/dev/null || echo "") + echo "📦 Latest autobuild version: $LATEST_VERSION" + + # Extract commit hash from version string (format: X.Y.Z+autobuild.MMDD.commit) + LATEST_COMMIT=$(echo "$LATEST_VERSION" | sed -n 's/.*+autobuild\.[0-9]\{4\}\.\([a-f0-9]*\)$/\1/p' || echo "") + echo "📝 Latest autobuild commit: $LATEST_COMMIT" + + if [ "$LAST_TAURI_COMMIT" != "$LATEST_COMMIT" ]; then + echo "✅ Tauri commit hash mismatch ($LAST_TAURI_COMMIT != $LATEST_COMMIT), build needed" + echo "should_run=true" >> $GITHUB_OUTPUT + else + echo "❌ Same Tauri commit hash ($LAST_TAURI_COMMIT), no build needed" + echo "should_run=false" >> $GITHUB_OUTPUT + fi + else + echo "⚠️ Failed to download or parse latest.json, build needed" + echo "should_run=true" >> $GITHUB_OUTPUT + fi + fi + fi diff --git a/clash-verge-rev/.github/workflows/clean-old-assets.yml b/clash-verge-rev/.github/workflows/clean-old-assets.yml new file mode 100644 index 0000000000..4c9ad9f029 --- /dev/null +++ b/clash-verge-rev/.github/workflows/clean-old-assets.yml @@ -0,0 +1,220 @@ +name: Clean Old Assets + +on: + workflow_dispatch: + inputs: + tag_name: + description: "Release tag name to clean (default: autobuild)" + required: false + default: "autobuild" + type: string + dry_run: + description: "Dry run mode (only show what would be deleted)" + required: false + default: false + type: boolean + workflow_call: + inputs: + tag_name: + description: "Release tag name to clean (default: autobuild)" + required: false + default: "autobuild" + type: string + dry_run: + description: "Dry run mode (only show what would be deleted)" + required: false + default: false + type: boolean + +permissions: write-all + +env: + TAG_NAME: ${{ inputs.tag_name || 'autobuild' }} + TAG_CHANNEL: AutoBuild + +jobs: + check_current_version: + name: Check Current Version and Commit + runs-on: ubuntu-latest + outputs: + current_version: ${{ steps.check.outputs.current_version }} + last_tauri_commit: ${{ steps.check.outputs.last_tauri_commit }} + autobuild_version: ${{ steps.check.outputs.autobuild_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 50 + + - name: Get current version and find last Tauri commit + id: check + run: | + CURRENT_VERSION=$(cat package.json | jq -r '.version') + echo "📦 Current version: $CURRENT_VERSION" + + # Find the last commit that changed Tauri-related files + echo "🔍 Finding last commit with Tauri-related changes..." + + # Define patterns for Tauri-related files + TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities" + + # Get the last commit that changed any of these patterns (excluding build artifacts) + LAST_TAURI_COMMIT="" + for commit in $(git rev-list HEAD --max-count=50); do + # Check if this commit changed any Tauri-related files + CHANGED_FILES=$(git show --name-only --pretty=format: $commit | tr '\n' ' ') + HAS_TAURI_CHANGES=false + + # Check each pattern + if echo "$CHANGED_FILES" | grep -q "src/" && echo "$CHANGED_FILES" | grep -qvE "src/(dist|build|node_modules|\.next|\.cache)"; then + HAS_TAURI_CHANGES=true + elif echo "$CHANGED_FILES" | grep -qE "src-tauri/(src|Cargo\.(toml|lock)|tauri\..*\.conf\.json|build\.rs|capabilities)"; then + HAS_TAURI_CHANGES=true + fi + + if [ "$HAS_TAURI_CHANGES" = true ]; then + LAST_TAURI_COMMIT=$(git rev-parse --short $commit) + break + fi + done + + if [ -z "$LAST_TAURI_COMMIT" ]; then + echo "⚠️ No Tauri-related changes found in recent commits, using current commit" + LAST_TAURI_COMMIT=$(git rev-parse --short HEAD) + fi + + echo "📝 Last Tauri-related commit: $LAST_TAURI_COMMIT" + echo "📝 Current commit: $(git rev-parse --short HEAD)" + + # Generate autobuild version for consistency + CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g') + MONTH=$(TZ=Asia/Shanghai date +%m) + DAY=$(TZ=Asia/Shanghai date +%d) + AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}" + + echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION" + + # Set outputs for other jobs to use + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "last_tauri_commit=$LAST_TAURI_COMMIT" >> $GITHUB_OUTPUT + echo "autobuild_version=$AUTOBUILD_VERSION" >> $GITHUB_OUTPUT + + clean_old_assets: + name: Clean Old Release Assets + runs-on: ubuntu-latest + needs: check_current_version + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Clean old assets from release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ env.TAG_NAME }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + # Use values from check_current_version job + CURRENT_AUTOBUILD_VERSION="${{ needs.check_current_version.outputs.autobuild_version }}" + LAST_TAURI_COMMIT="${{ needs.check_current_version.outputs.last_tauri_commit }}" + CURRENT_VERSION="${{ needs.check_current_version.outputs.current_version }}" + + echo "📦 Current version: $CURRENT_VERSION" + echo "📦 Current autobuild version: $CURRENT_AUTOBUILD_VERSION" + echo "📝 Last Tauri commit: $LAST_TAURI_COMMIT" + echo "🏷️ Target tag: $TAG_NAME" + echo "🔍 Dry run mode: $DRY_RUN" + + # Check if release exists + RELEASE_EXISTS=$(gh release view "$TAG_NAME" --json id -q '.id' 2>/dev/null || echo "") + + if [ -z "$RELEASE_EXISTS" ]; then + echo "❌ Release '$TAG_NAME' not found" + exit 1 + fi + + echo "✅ Found release '$TAG_NAME'" + + # Get all assets + echo "📋 Getting list of all assets..." + assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true) + + if [ -z "$assets" ]; then + echo "ℹ️ No assets found in release '$TAG_NAME'" + exit 0 + fi + + echo "📋 Found assets:" + echo "$assets" | sed 's/^/ - /' + + # Count assets to keep and delete + ASSETS_TO_KEEP="" + ASSETS_TO_DELETE="" + + for asset in $assets; do + # Keep assets that match current autobuild version or are non-versioned files (like latest.json) + if [[ "$asset" == *"$CURRENT_AUTOBUILD_VERSION"* ]] || [[ "$asset" == "latest.json" ]]; then + ASSETS_TO_KEEP="$ASSETS_TO_KEEP$asset\n" + else + ASSETS_TO_DELETE="$ASSETS_TO_DELETE$asset\n" + fi + done + + echo "" + echo "🔒 Assets to keep (current version: $CURRENT_AUTOBUILD_VERSION):" + if [ -n "$ASSETS_TO_KEEP" ]; then + echo -e "$ASSETS_TO_KEEP" | grep -v '^$' | sed 's/^/ - /' + else + echo " - None" + fi + + echo "" + echo "🗑️ Assets to delete:" + if [ -n "$ASSETS_TO_DELETE" ]; then + echo -e "$ASSETS_TO_DELETE" | grep -v '^$' | sed 's/^/ - /' + else + echo " - None" + echo "ℹ️ No old assets to clean" + exit 0 + fi + + if [ "$DRY_RUN" = "true" ]; then + echo "" + echo "🔍 DRY RUN MODE: No assets will actually be deleted" + echo " To actually delete these assets, run this workflow again with dry_run=false" + else + echo "" + echo "🗑️ Deleting old assets..." + + DELETED_COUNT=0 + FAILED_COUNT=0 + + for asset in $assets; do + # Skip assets that should be kept + if [[ "$asset" == *"$CURRENT_AUTOBUILD_VERSION"* ]] || [[ "$asset" == "latest.json" ]]; then + continue + fi + + echo " Deleting: $asset" + if gh release delete-asset "$TAG_NAME" "$asset" -y 2>/dev/null; then + DELETED_COUNT=$((DELETED_COUNT + 1)) + else + echo " ⚠️ Failed to delete $asset" + FAILED_COUNT=$((FAILED_COUNT + 1)) + fi + done + + echo "" + echo "📊 Cleanup summary:" + echo " - Deleted: $DELETED_COUNT assets" + if [ $FAILED_COUNT -gt 0 ]; then + echo " - Failed: $FAILED_COUNT assets" + fi + echo " - Kept: $(echo -e "$ASSETS_TO_KEEP" | grep -v '^$' | wc -l) assets" + + if [ $FAILED_COUNT -gt 0 ]; then + echo "⚠️ Some assets failed to delete. Please check the logs above." + exit 1 + else + echo "✅ Cleanup completed successfully!" + fi + fi diff --git a/clash-verge-rev/.github/workflows/dev.yml b/clash-verge-rev/.github/workflows/dev.yml index 573e80ba7b..b078aa0d52 100644 --- a/clash-verge-rev/.github/workflows/dev.yml +++ b/clash-verge-rev/.github/workflows/dev.yml @@ -13,8 +13,13 @@ on: required: false type: boolean default: true - run_macos_x86_64: - description: "运行 macOS x86_64" + run_windows_arm64: + description: "运行 Windows ARM64" + required: false + type: boolean + default: true + run_linux_amd64: + description: "运行 Linux amd64" required: false type: boolean default: true @@ -45,14 +50,23 @@ jobs: bundle: dmg id: macos-aarch64 input: run_macos_aarch64 - - os: macos-latest - target: x86_64-apple-darwin - bundle: dmg - id: macos-x86_64 - input: run_macos_x86_64 + - os: windows-latest + target: aarch64-pc-windows-msvc + bundle: nsis + id: windows-arm64 + input: run_windows_arm64 + - os: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + bundle: deb + id: linux-amd64 + input: run_linux_amd64 runs-on: ${{ matrix.os }} steps: + - name: Skip job if not selected + if: github.event.inputs[matrix.input] != 'true' + run: echo "Job ${{ matrix.id }} skipped as requested" + - name: Checkout Repository if: github.event.inputs[matrix.input] == 'true' uses: actions/checkout@v4 @@ -71,14 +85,14 @@ jobs: with: workspaces: src-tauri save-if: false - cache-all-crates: false + cache-all-crates: true shared-key: autobuild-shared - - name: Install Node - if: github.event.inputs[matrix.input] == 'true' - uses: actions/setup-node@v4 - with: - node-version: "20" + - name: Install dependencies (ubuntu only) + if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true' + run: | + sudo apt-get update + sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf - uses: pnpm/action-setup@v4 name: Install pnpm @@ -86,6 +100,13 @@ jobs: with: run_install: false + - name: Install Node + if: github.event.inputs[matrix.input] == 'true' + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + - name: Pnpm install and check if: github.event.inputs[matrix.input] == 'true' run: | @@ -93,6 +114,7 @@ jobs: pnpm run prebuild ${{ matrix.target }} - name: Release ${{ env.TAG_CHANNEL }} Version + if: github.event.inputs[matrix.input] == 'true' run: pnpm release-version ${{ env.TAG_NAME }} - name: Tauri build @@ -113,7 +135,7 @@ jobs: tauriScript: pnpm args: --target ${{ matrix.target }} -b ${{ matrix.bundle }} - - name: Upload Artifacts + - name: Upload Artifacts (macOS) if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true' uses: actions/upload-artifact@v4 with: @@ -121,10 +143,18 @@ jobs: path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg if-no-files-found: error - - name: Upload Artifacts + - name: Upload Artifacts (Windows) if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true' uses: actions/upload-artifact@v4 with: name: ${{ matrix.target }} path: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe if-no-files-found: error + + - name: Upload Artifacts (Linux) + if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true' + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.target }} + path: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb + if-no-files-found: error diff --git a/clash-verge-rev/.github/workflows/fmt.yml b/clash-verge-rev/.github/workflows/fmt.yml index 3320cba1a1..5ec2ca9ad4 100644 --- a/clash-verge-rev/.github/workflows/fmt.yml +++ b/clash-verge-rev/.github/workflows/fmt.yml @@ -10,28 +10,67 @@ on: jobs: rustfmt: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 + - name: Check Rust changes + id: check_rust + uses: dorny/paths-filter@v3 + with: + filters: | + rust: + - 'src-tauri/**' + - '**/*.rs' + + - name: Skip if no Rust changes + if: steps.check_rust.outputs.rust != 'true' + run: echo "No Rust changes, skipping rustfmt." + - name: install Rust stable and rustfmt + if: steps.check_rust.outputs.rust == 'true' uses: dtolnay/rust-toolchain@stable with: components: rustfmt - name: run cargo fmt + if: steps.check_rust.outputs.rust == 'true' run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check prettier: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + - name: Check Web changes + id: check_web + uses: dorny/paths-filter@v3 + with: + filters: | + web: + - 'src/**' + - '**/*.js' + - '**/*.ts' + - '**/*.tsx' + - '**/*.css' + - '**/*.scss' + - '**/*.json' + - '**/*.md' + - '**/*.json' + + - name: Skip if no Web changes + if: steps.check_web.outputs.web != 'true' + run: echo "No web changes, skipping prettier." + - uses: actions/setup-node@v4 + if: steps.check_web.outputs.web == 'true' with: node-version: "lts/*" - run: corepack enable + if: steps.check_web.outputs.web == 'true' - run: pnpm install --frozen-lockfile + if: steps.check_web.outputs.web == 'true' - run: pnpm format:check + if: steps.check_web.outputs.web == 'true' # taplo: # name: taplo (.toml files) diff --git a/clash-verge-rev/.github/workflows/clippy.yml b/clash-verge-rev/.github/workflows/lint-clippy.yml similarity index 65% rename from clash-verge-rev/.github/workflows/clippy.yml rename to clash-verge-rev/.github/workflows/lint-clippy.yml index 669b025888..6bc0bf338d 100644 --- a/clash-verge-rev/.github/workflows/clippy.yml +++ b/clash-verge-rev/.github/workflows/lint-clippy.yml @@ -2,6 +2,7 @@ name: Clippy Lint on: pull_request: + workflow_dispatch: jobs: clippy: @@ -18,11 +19,30 @@ jobs: runs-on: ${{ matrix.os }} steps: + - name: Check src-tauri changes + id: check_changes + uses: dorny/paths-filter@v3 + with: + filters: | + rust: + - 'src-tauri/**' + + - name: Skip if src-tauri not changed + if: steps.check_changes.outputs.rust != 'true' + run: echo "No src-tauri changes, skipping clippy lint." + + - name: Continue if src-tauri changed + if: steps.check_changes.outputs.rust == 'true' + run: echo "src-tauri changed, running clippy lint." + - name: Checkout Repository uses: actions/checkout@v4 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy - name: Add Rust Target run: rustup target add ${{ matrix.target }} @@ -41,21 +61,25 @@ jobs: sudo apt-get update sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version: "22" - - name: Install pnpm uses: pnpm/action-setup@v4 with: run_install: false + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "pnpm" + - name: Pnpm install and check run: | pnpm i pnpm run prebuild ${{ matrix.target }} + # This workflow runs linting using cargo clippy. + # Note: If the web build step is skipped, + # cargo clippy will fail to run due to missing web dist in the Tauri environment. - name: Build Web Assets run: pnpm run web:build env: diff --git a/clash-verge-rev/.github/workflows/release.yml b/clash-verge-rev/.github/workflows/release.yml index e101675cd5..da80b04de2 100644 --- a/clash-verge-rev/.github/workflows/release.yml +++ b/clash-verge-rev/.github/workflows/release.yml @@ -5,10 +5,6 @@ on: # ! 不再使用 workflow_dispatch 触发。 # workflow_dispatch: push: - # 应当限制在 main 分支上触发发布。 - branches: - - main - # 应当限制 v*.*.* 的 tag 触发发布。 tags: - "v*.*.*" permissions: write-all @@ -27,22 +23,132 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check if tag is from main branch + run: | + TAG_REF="${GITHUB_REF##*/}" + echo "Checking if tag $TAG_REF is from main branch..." + + TAG_COMMIT=$(git rev-list -n 1 $TAG_REF) + MAIN_COMMITS=$(git rev-list origin/main) + + if echo "$MAIN_COMMITS" | grep -q "$TAG_COMMIT"; then + echo "✅ Tag $TAG_REF is from main branch" + else + echo "❌ Tag $TAG_REF is not from main branch" + echo "This release workflow only accepts tags from main branch." + exit 1 + fi - name: Check tag and package.json version run: | - TAG_REF="${GITHUB_REF##*/}" + TAG_REF="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}" echo "Current tag: $TAG_REF" + PKG_VERSION=$(jq -r .version package.json) echo "package.json version: $PKG_VERSION" - if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then - echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)." + + EXPECTED_TAG="v$PKG_VERSION" + + if [[ "$TAG_REF" != "$EXPECTED_TAG" ]]; then + echo "❌ Version mismatch:" + echo " Git tag : $TAG_REF" + echo " package.json : $EXPECTED_TAG" exit 1 fi - echo "Tag and package.json version are consistent." + + echo "✅ Tag and package.json version are consistent." + + update_tag: + name: Update tag + runs-on: ubuntu-latest + needs: [release, release-for-linux-arm, release-for-fixed-webview2] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Fetch UPDATE logs + id: fetch_update_logs + run: | + if [ -f "UPDATELOG.md" ]; then + UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) + if [ -n "$UPDATE_LOGS" ]; then + echo "Found update logs" + echo "UPDATE_LOGS<> $GITHUB_ENV + echo "$UPDATE_LOGS" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + else + echo "No update sections found in UPDATELOG.md" + fi + else + echo "UPDATELOG.md file not found" + fi + shell: bash + + - name: Set Env + run: | + echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV + TAG_REF="${GITHUB_REF##*/}" + echo "TAG_NAME=$TAG_REF" >> $GITHUB_ENV + VERSION=$(echo "$TAG_REF" | sed 's/^v//') + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/$TAG_REF" >> $GITHUB_ENV + shell: bash + + - run: | + if [ -z "$UPDATE_LOGS" ]; then + echo "No update logs found, using default message" + UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon." + else + echo "Using found update logs" + fi + + cat > release.txt << EOF + $UPDATE_LOGS + + ## 下载地址 + + ### Windows (不再支持Win7) + #### 正常版本(推荐) + - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) + + #### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe) + + ### macOS + - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg) + + ### Linux + #### DEB包(Debian系) 使用 apt ./路径 安装 + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) + + #### RPM包(Redhat系) 使用 dnf ./路径 安装 + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm) + + ### FAQ + - [常见问题](https://clash-verge-rev.github.io/faq/windows.html) + + ### 稳定机场VPN推荐 + - [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6) + Created at ${{ env.BUILDTIME }}. + EOF + + - name: Upload Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.TAG_NAME }} + name: "Clash Verge Rev ${{ env.TAG_NAME }}" + body_path: release.txt + draft: false + prerelease: false + token: ${{ secrets.GITHUB_TOKEN }} + # generate_release_notes: true release: name: Release Build - needs: check_tag_version + needs: [check_tag_version] strategy: fail-fast: false matrix: @@ -81,6 +187,15 @@ jobs: sudo apt-get update sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf + - name: Install x86 OpenSSL (macOS only) + if: matrix.target == 'x86_64-apple-darwin' + run: | + arch -x86_64 brew install openssl@3 + echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV + echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV + echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV + - name: Install Node uses: actions/setup-node@v4 with: @@ -110,14 +225,18 @@ jobs: APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} with: - tagName: v__VERSION__ - releaseName: "Clash Verge Rev v__VERSION__" - releaseBody: "More new features are now supported." + tagName: ${{ github.ref_name }} + releaseName: "Clash Verge Rev ${{ github.ref_name }}" + releaseBody: "Draft release, will be updated later." + releaseDraft: true + prerelease: false tauriScript: pnpm args: --target ${{ matrix.target }} + includeUpdaterJson: true release-for-linux-arm: name: Release Build for Linux ARM + needs: [check_tag_version] strategy: fail-fast: false matrix: @@ -232,7 +351,7 @@ jobs: with: tag_name: v${{env.VERSION}} name: "Clash Verge Rev v${{env.VERSION}}" - body: "More new features are now supported." + body: "See release notes for detailed changelog." token: ${{ secrets.GITHUB_TOKEN }} files: | src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb @@ -240,6 +359,7 @@ jobs: release-for-fixed-webview2: name: Release Build for Fixed WebView2 + needs: [check_tag_version] strategy: fail-fast: false matrix: @@ -323,7 +443,7 @@ jobs: with: tag_name: v${{steps.build.outputs.appVersion}} name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}" - body: "More new features are now supported." + body: "See release notes for detailed changelog." token: ${{ secrets.GITHUB_TOKEN }} files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* @@ -335,7 +455,7 @@ jobs: release-update: name: Release Update runs-on: ubuntu-latest - needs: [release, release-for-linux-arm] + needs: [update_tag] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -360,7 +480,7 @@ jobs: release-update-for-fixed-webview2: runs-on: ubuntu-latest - needs: [release-for-fixed-webview2] + needs: [update_tag] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -386,7 +506,7 @@ jobs: submit-to-winget: name: Submit to Winget runs-on: ubuntu-latest - needs: [release-update] + needs: [update_tag, release-update] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -405,3 +525,103 @@ jobs: release-tag: v${{env.VERSION}} installers-regex: '_(arm64|x64|x86)-setup\.exe$' token: ${{ secrets.WINGET_TOKEN }} + + notify-telegram: + name: Notify Telegram + runs-on: ubuntu-latest + needs: + [ + update_tag, + release-update, + release-update-for-fixed-webview2, + submit-to-winget, + ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Fetch UPDATE logs + id: fetch_update_logs + run: | + if [ -f "UPDATELOG.md" ]; then + UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) + if [ -n "$UPDATE_LOGS" ]; then + echo "Found update logs" + echo "UPDATE_LOGS<> $GITHUB_ENV + echo "$UPDATE_LOGS" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + else + echo "No update sections found in UPDATELOG.md" + fi + else + echo "UPDATELOG.md file not found" + fi + shell: bash + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "22" + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Get Version and Release Info + run: | + sudo apt-get update + sudo apt-get install jq + echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV + echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV + echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV + + - name: Generate release.txt + run: | + if [ -z "$UPDATE_LOGS" ]; then + echo "No update logs found, using default message" + UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon." + else + echo "Using found update logs" + fi + + cat > release.txt << EOF + $UPDATE_LOGS + + ## 下载地址 + + ### Windows (不再支持Win7) + #### 正常版本(推荐) + - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) + + #### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe) + + ### macOS + - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg) + + ### Linux + #### DEB包(Debian系) 使用 apt ./路径 安装 + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) + + #### RPM包(Redhat系) 使用 dnf ./路径 安装 + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm) + + ### FAQ + - [常见问题](https://clash-verge-rev.github.io/faq/windows.html) + + ### 稳定机场VPN推荐 + - [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6) + Created at ${{ env.BUILDTIME }}. + EOF + + - name: Send Telegram Notification + run: node scripts/telegram.mjs + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + BUILD_TYPE: release + VERSION: ${{ env.VERSION }} + DOWNLOAD_URL: ${{ env.DOWNLOAD_URL }} diff --git a/clash-verge-rev/.gitignore b/clash-verge-rev/.gitignore index aa25c632f0..9658768939 100644 --- a/clash-verge-rev/.gitignore +++ b/clash-verge-rev/.gitignore @@ -10,3 +10,4 @@ scripts/_env.sh .tool-versions .idea .old +.eslintcache diff --git a/clash-verge-rev/CONTRIBUTING.md b/clash-verge-rev/CONTRIBUTING.md index 07c47b49ab..3b55dd9135 100644 --- a/clash-verge-rev/CONTRIBUTING.md +++ b/clash-verge-rev/CONTRIBUTING.md @@ -12,6 +12,11 @@ Before you start contributing to the project, you need to set up your developmen ### Setup for Windows Users +> [!NOTE] +> **If you are using a Windows ARM device, you additionally need to install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the environment variable.** +> +> Because the `ring` crate is compiled based on `clang` under Windows ARM. + If you're a Windows user, you may need to perform some additional steps: - Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary. @@ -51,11 +56,14 @@ apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev You have two options for downloading the clash binary: - Automatically download it via the provided script: + ```shell pnpm run prebuild - # Use '--force' to force update to the latest version - # pnpm run prebuild --force + # Use '--force' or '-f' to update both the Mihomo core version + # and the Clash Verge Rev service version to the latest available. + pnpm run prebuild --force ``` + - Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin). ### Run the Development Server @@ -66,6 +74,8 @@ To run the development server, use the following command: pnpm dev # If an app instance already exists, use a different command pnpm dev:diff +# To using tauri built-in dev tool +pnpm dev:tauri ``` ### Build the Project diff --git a/clash-verge-rev/README.md b/clash-verge-rev/README.md index 6aa3321b98..2db0ab1fa3 100644 --- a/clash-verge-rev/README.md +++ b/clash-verge-rev/README.md @@ -23,11 +23,11 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple). #### 我应当怎样选择发行版 -| 版本 | 特征 | 链接 | -| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- | -| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) | -| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) | -| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) | +| 版本 | 特征 | 链接 | +| :---------- | :--------------------------------------- | :------------------------------------------------------------------------------------- | +| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) | +| Alpha(废弃) | 测试发布流程。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) | +| AutoBuild | 滚动更新版,适合测试反馈,可能存在缺陷。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) | #### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看 @@ -88,7 +88,7 @@ To run the development server, execute the following commands after all prerequi ```shell pnpm i -pnpm run check +pnpm run prebuild pnpm dev ``` diff --git a/clash-verge-rev/UPDATELOG.md b/clash-verge-rev/UPDATELOG.md index 94c256afc5..b0a28fda57 100644 --- a/clash-verge-rev/UPDATELOG.md +++ b/clash-verge-rev/UPDATELOG.md @@ -1,55 +1,159 @@ -## v2.4.0 - -### 🏆 重大改进 - -- **核心架构升级**:与内核 `Mihomo` 采用 `IPC` 通信,不再依赖 `Restful API` 通信,提升性能和稳定性 -- **流量监控系统重构**:前端实现全新的增强流量监控系统,支持数据压缩、采样和智能缓存 -- **数据验证机制**:引入类型安全的数据验证器,确保 `API` 响应数据的一致性和可靠性 -- **配置缓存架构**:实现智能配置缓存系统,支持后端数据缓存和强制刷新机制 +## v2.4.3 ### ✨ 新增功能 -- 增加 `Verge Version` 复制按钮 -- 新增增强型流量监控 `Hook` 支持高级数据管理与采样 -- 支持原始/压缩流量数据处理与时间范围查询 -- 引用计数管理器智能收集数据 -- 新增流量监控诊断工具与错误边界组件 -- 多版本画布流量图表,丰富可视化选项 -- 新增强制刷新 `Clash` 配置/节点缓存功能,提升更新响应速度 -- 增加代理请求缓存机制,减少重复 `API` 调用 -- 添加首页卡片移动 (暂测) +- **Mihomo(Meta) 内核升级至 v1.19.14** +- Linux 打包为 `.deb` `.rpm` 提供 pkexec 依赖项 +- 支持前端修改日志(最大文件大小、最大保留数量) +- 新增链式代理图形化设置功能 -### 🚀 性能优化 +### 🚀 优化改进 -- `IPC` 通信机制显著提升数据传输效率 -- 智能数据采样和压缩减少内存占用 -- 引用计数机制避免不必要的数据收集,提升整体性能 -- 优化流量图表渲染性能,支持大数据量展示 -- 改进前端数据获取和缓存策略 -- 实现配置/节点缓存 `TTL` 机制,减少不必要的配置/节点请求 -- 改进 `Clash` 配置/节点刷新间隔,从5秒优化到60秒,减少系统资源消耗 -- 同步设置页面所有按钮 +- 重构并简化服务模式启动检测流程,消除重复检测 +- 重构并简化窗口创建流程 +- 重构日志系统,单个日志默认最大 10 MB +- 优化前端资源占用 +- 改进 macos 下系统代理设置的方法 +- 优化 TUN 模式可用性的判断 +- 移除流媒体检测的系统级提示(使用软件内通知) +- 优化后端 i18n 资源占用 ### 🐞 修复问题 +- 优化服务模式重装逻辑,避免不必要的重复检查 +- 修复轻量模式退出无响应的问题 +- 修复托盘轻量模式支持退出/进入 +- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程 +- macOS Tun/系统代理 模式下图标大小不统一 +- 托盘节点切换不再显示隐藏组 +- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商 +- 修复MacOS 下 Tun开启后 系统代理无法打开的问题 +- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题 + +## v2.4.2 + +### ✨ 新增功能 + +- 增加托盘节点选择 + +### 🚀 性能优化 + +- 优化前端首页加载速度 +- 优化前端未使用 i18n 文件缓存 +- 优化后端内存占用 +- 优化后端启动速度 + +### 🐞 修复问题 + +- 修复首页节点切换失效的问题 +- 修复和优化服务检查流程 +- 修复2.4.1引入的订阅地址重定向报错问题 +- 修复 rpm/deb 包名称问题 +- 修复托盘轻量模式状态检测异常 +- 修复通过 scheme 导入订阅崩溃 +- 修复单例检测实效 +- 修复启动阶段可能导致的无法连接内核 +- 修复导入订阅无法 Auth Basic + +### 👙 界面样式 + +- 简化和改进代理设置样式 + +## v2.4.1 + +### 🏆 重大改进 + +- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性 + +### ✨ 新增功能 + +- **Mihomo(Meta) 内核升级至 v1.19.13** + +### 🚀 性能优化 + +- 优化热键响应速度,提升快捷键操作体验 +- 改进服务管理响应性,减少系统服务操作等待时间 +- 提升文件和配置处理性能 +- 优化任务管理和日志记录效率 +- 优化异步内存管理,减少内存占用并提升多任务处理效率 +- 优化启动阶段初始化性能 + +### 🐞 修复问题 + +- 修复应用在某些操作中可能出现的响应延迟问题 +- 修复任务管理中的潜在并发问题 +- 修复通过托盘重启应用无法恢复 +- 修复订阅在某些情况下无法导入 +- 修复无法新建订阅时使用远程链接 +- 修复卸载服务后的 tun 开关状态问题 +- 修复页面快速切换订阅时导致崩溃 +- 修复丢失工作目录时无法恢复环境 +- 修复从轻量模式恢复导致崩溃 + +### 👙 界面样式 + +- 统一代理设置样式 + +### 🗑️ 移除内容 + +- 移除启动阶段自动清理过期订阅 + +## v2.4.0 + +**发行代号:融** +代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。 + +### 🏆 重大改进 + +- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性 +- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示 +- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度 + +### ✨ 新增功能 + +- **Mihomo(Meta) 内核升级至 v1.19.12** +- 新增版本信息复制按钮 +- 增强型流量监控,支持更详细的数据分析 +- 新增流量图表多种显示模式 +- 新增强制刷新配置和节点缓存功能 +- 首页流量统计支持查看刻度线详情 + +### 🚀 性能优化 + +- 全面提升数据传输和处理效率 +- 优化内存使用,减少系统资源消耗 +- 改进流量图表渲染性能 +- 优化配置和节点刷新策略,从5秒延长到60秒 +- 改进数据缓存机制,减少重复请求 +- 优化异步程序性能 + +### 🐞 修复问题 + +- 修复系统代理状态检测和显示不一致问题 - 修复系统主题窗口颜色不一致问题 -- 修复 `URL` 编码处理,正确处理特殊字符 -- 增强代理更新的错误处理机制 -- 修复 `JSON` 解析错误处理 -- 优化调试日志输出,减少噪音 -- 修复配置修改后前端缓存不同步问题 -- 改进核心启动/停止/重启后的状态刷新机制 -- 修复 `Windows` 安装器删除用户自启问题 -- 修复 `Windows` 安装器参数使用错误问题 -- 修复 `IPC` 迁移后节点测速功能异常 -- 修复 `IPC` 迁移后连接上下行速率计算功能异常 +- 修复特殊字符 URL 处理问题 +- 修复配置修改后缓存不同步问题 +- 修复 Windows 安装器自启设置问题 +- 修复 macOS 下 Dock 图标恢复窗口问题 +- 修复 linux 下 KDE/Plasma 异常标题栏按钮 +- 修复架构升级后节点测速功能异常 +- 修复架构升级后流量统计功能异常 +- 修复架构升级后日志功能异常 +- 修复外部控制器跨域配置保存问题 +- 修复首页端口显示不一致问题 +- 修复首页流量统计刻度线显示问题 +- 修复日志页面按钮功能混淆问题 +- 修复日志等级设置保存问题 +- 修复日志等级异常过滤 +- 修复清理日志天数功能异常 +- 修复偶发性启动卡死问题 +- 修复首页虚拟网卡开关在管理模式下的状态问题 ### 🔧 技术改进 -- 移除过时的 `Http` 控制 `Mihomo` 统一使用 `IPC` 控制 -- 添加外部控制器配置和 `UI` 支持 -- 改进 `IPC` 路径处理,支持 `Unix` 系统特定功能 -- 优化 `IPC` 目录安全检查和路径解析 +- 统一使用新的内核通信方式 +- 新增外部控制器配置界面 +- 改进跨平台兼容性支持 ## v2.3.2 diff --git a/clash-verge-rev/eslint.config.ts b/clash-verge-rev/eslint.config.ts new file mode 100644 index 0000000000..b8cf71f123 --- /dev/null +++ b/clash-verge-rev/eslint.config.ts @@ -0,0 +1,134 @@ +import eslintReact from "@eslint-react/eslint-plugin"; +import eslintJS from "@eslint/js"; +import configPrettier from "eslint-config-prettier"; +import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript"; +import pluginImportX from "eslint-plugin-import-x"; +import pluginPrettier from "eslint-plugin-prettier"; +import pluginReactHooks from "eslint-plugin-react-hooks"; +import pluginReactRefresh from "eslint-plugin-react-refresh"; +import pluginUnusedImports from "eslint-plugin-unused-imports"; +import { defineConfig } from "eslint/config"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default defineConfig([ + { + files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], + + plugins: { + js: eslintJS, + "react-hooks": pluginReactHooks, + // @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421 + "import-x": pluginImportX, + "react-refresh": pluginReactRefresh, + "unused-imports": pluginUnusedImports, + prettier: pluginPrettier, + }, + + extends: [ + eslintJS.configs.recommended, + tseslint.configs.recommended, + eslintReact.configs["recommended-typescript"], + configPrettier, + ], + + languageOptions: { + globals: globals.browser, + }, + + settings: { + react: { + version: "detect", + }, + "import-x/resolver-next": [ + createTypeScriptImportResolver({ + project: "./tsconfig.json", + }), + ], + }, + + rules: { + // React + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + + "@eslint-react/no-forward-ref": "off", + + // React performance and production quality rules + "@eslint-react/no-array-index-key": "warn", + "@eslint-react/no-children-count": "error", + "@eslint-react/no-children-for-each": "error", + "@eslint-react/no-children-map": "error", + "@eslint-react/no-children-only": "error", + "@eslint-react/no-children-prop": "error", + "@eslint-react/no-children-to-array": "error", + "@eslint-react/no-class-component": "error", + "@eslint-react/no-clone-element": "error", + "@eslint-react/no-create-ref": "error", + "@eslint-react/no-default-props": "error", + "@eslint-react/no-direct-mutation-state": "error", + "@eslint-react/no-implicit-key": "error", + "@eslint-react/no-prop-types": "error", + "@eslint-react/no-set-state-in-component-did-mount": "error", + "@eslint-react/no-set-state-in-component-did-update": "error", + "@eslint-react/no-set-state-in-component-will-update": "error", + "@eslint-react/no-string-refs": "error", + "@eslint-react/no-unstable-context-value": "warn", + "@eslint-react/no-unstable-default-props": "warn", + "@eslint-react/no-unused-class-component-members": "error", + "@eslint-react/no-unused-state": "error", + "@eslint-react/no-useless-fragment": "warn", + "@eslint-react/prefer-destructuring-assignment": "warn", + + // TypeScript + "@typescript-eslint/no-explicit-any": "off", + + // unused-imports 代替 no-unused-vars + "@typescript-eslint/no-unused-vars": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + vars: "all", + varsIgnorePattern: "^_+$", + args: "after-used", + argsIgnorePattern: "^_+$", + }, + ], + + // Import + "import-x/no-unresolved": "error", + "import-x/order": [ + "warn", + { + groups: [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + ], + "newlines-between": "always", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + + // 其他常见 + "prefer-const": "warn", + "no-case-declarations": "error", + "no-fallthrough": "error", + "no-empty": ["warn", { allowEmptyCatch: true }], + + // Prettier 格式化问题 + "prettier/prettier": "warn", + }, + }, +]); diff --git a/clash-verge-rev/package.json b/clash-verge-rev/package.json index d851488b17..52e7ac62c8 100644 --- a/clash-verge-rev/package.json +++ b/clash-verge-rev/package.json @@ -1,10 +1,12 @@ { "name": "clash-verge", - "version": "2.4.0", + "version": "2.4.3", "license": "GPL-3.0-only", "scripts": { - "dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", - "dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", + "dev": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev", + "dev:diff": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev", + "dev:trace": "cross-env RUST_BACKTRACE=full RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace", + "dev:tauri": "cross-env RUST_BACKTRACE=full tauri dev -f tauri-dev", "build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build", "build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release", "tauri": "tauri", @@ -23,6 +25,7 @@ "publish-version": "node scripts/publish-version.mjs", "fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml", "clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml", + "lint": "eslint -c eslint.config.ts --cache src", "format": "prettier --write .", "format:check": "prettier --check ." }, @@ -33,76 +36,81 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@juggle/resize-observer": "^3.4.0", - "@mui/icons-material": "^7.2.0", - "@mui/lab": "7.0.0-beta.14", - "@mui/material": "^7.2.0", - "@mui/x-data-grid": "^8.9.1", - "@tauri-apps/api": "2.7.0", + "@mui/icons-material": "^7.3.4", + "@mui/lab": "7.0.0-beta.17", + "@mui/material": "^7.3.4", + "@mui/x-data-grid": "^8.13.1", + "@tauri-apps/api": "2.8.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.0", - "@tauri-apps/plugin-dialog": "^2.3.1", - "@tauri-apps/plugin-fs": "^2.4.1", - "@tauri-apps/plugin-global-shortcut": "^2.3.0", - "@tauri-apps/plugin-notification": "^2.3.0", + "@tauri-apps/plugin-dialog": "^2.4.0", + "@tauri-apps/plugin-fs": "^2.4.2", + "@tauri-apps/plugin-http": "~2.5.2", "@tauri-apps/plugin-process": "^2.3.0", - "@tauri-apps/plugin-shell": "2.3.0", + "@tauri-apps/plugin-shell": "2.3.1", "@tauri-apps/plugin-updater": "2.9.0", - "@tauri-apps/plugin-window-state": "^2.4.0", "@types/json-schema": "^7.0.15", - "ahooks": "^3.9.0", - "axios": "^1.11.0", - "chart.js": "^4.5.0", - "cli-color": "^2.0.4", - "dayjs": "1.11.13", + "ahooks": "^3.9.5", + "axios": "^1.12.2", + "dayjs": "1.11.18", "foxact": "^0.2.49", - "glob": "^11.0.3", - "i18next": "^25.3.2", + "i18next": "^25.5.3", "js-yaml": "^4.1.0", "json-schema": "^0.4.0", "lodash-es": "^4.17.21", - "monaco-editor": "^0.52.2", + "monaco-editor": "^0.53.0", "monaco-yaml": "^5.4.0", - "nanoid": "^5.1.5", - "react": "19.1.0", - "react-beautiful-dnd": "^13.1.1", - "react-chartjs-2": "^5.3.0", - "react-dom": "19.1.0", + "nanoid": "^5.1.6", + "react": "19.2.0", + "react-dom": "19.2.0", "react-error-boundary": "6.0.0", - "react-hook-form": "^7.61.1", - "react-i18next": "15.6.1", + "react-hook-form": "^7.64.0", + "react-i18next": "16.0.0", "react-markdown": "10.1.0", "react-monaco-editor": "0.59.0", - "react-router-dom": "7.7.1", - "react-virtuoso": "^4.13.0", - "sockette": "^2.0.6", - "swr": "^2.3.4", - "tar": "^7.4.3", + "react-router-dom": "7.9.3", + "react-virtuoso": "^4.14.1", + "swr": "^2.3.6", "types-pac": "^1.0.3", - "zustand": "^5.0.6" + "zustand": "^5.0.8" }, "devDependencies": { "@actions/github": "^6.0.1", - "@tauri-apps/cli": "2.7.1", + "@eslint-react/eslint-plugin": "^2.0.6", + "@eslint/js": "^9.37.0", + "@tauri-apps/cli": "2.8.4", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", - "@types/react": "19.1.8", - "@types/react-beautiful-dnd": "^13.1.8", - "@types/react-dom": "19.1.6", - "@vitejs/plugin-legacy": "^7.1.0", - "@vitejs/plugin-react": "4.7.0", + "@types/react": "19.2.0", + "@types/react-dom": "19.2.0", + "@vitejs/plugin-legacy": "^7.2.1", + "@vitejs/plugin-react": "5.0.4", "adm-zip": "^0.5.16", - "commander": "^14.0.0", - "cross-env": "^10.0.0", + "cli-color": "^2.0.4", + "commander": "^14.0.1", + "cross-env": "^10.1.0", + "eslint": "^9.37.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import-x": "^4.16.1", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-react-hooks": "^6.1.1", + "eslint-plugin-react-refresh": "^0.4.23", + "eslint-plugin-unused-imports": "^4.2.0", + "glob": "^11.0.3", + "globals": "^16.4.0", "https-proxy-agent": "^7.0.6", - "meta-json-schema": "^1.19.11", + "jiti": "^2.6.1", + "meta-json-schema": "^1.19.14", "node-fetch": "^3.3.2", "prettier": "^3.6.2", - "prettier-plugin-organize-imports": "^4.2.0", - "sass": "^1.89.2", - "terser": "^5.43.1", - "typescript": "^5.8.3", - "vite": "^7.0.6", + "sass": "^1.93.2", + "tar": "^7.5.1", + "terser": "^5.44.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.9", "vite-plugin-monaco-editor": "^1.1.0", - "vite-plugin-svgr": "^4.3.0" + "vite-plugin-svgr": "^4.5.0" }, "type": "module", "packageManager": "pnpm@9.13.2" diff --git a/clash-verge-rev/pnpm-lock.yaml b/clash-verge-rev/pnpm-lock.yaml index 3306fec204..38ad0822e7 100644 --- a/clash-verge-rev/pnpm-lock.yaml +++ b/clash-verge-rev/pnpm-lock.yaml @@ -10,91 +10,76 @@ importers: dependencies: '@dnd-kit/core': specifier: ^6.3.1 - version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@dnd-kit/sortable': specifier: ^10.0.0 - version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) '@dnd-kit/utilities': specifier: ^3.2.2 - version: 3.2.2(react@19.1.0) + version: 3.2.2(react@19.2.0) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.1.8)(react@19.1.0) + version: 11.14.0(@types/react@19.2.0)(react@19.2.0) '@emotion/styled': specifier: ^11.14.1 - version: 11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0) '@juggle/resize-observer': specifier: ^3.4.0 version: 3.4.0 '@mui/icons-material': - specifier: ^7.2.0 - version: 7.2.0(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + specifier: ^7.3.4 + version: 7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.0)(react@19.2.0) '@mui/lab': - specifier: 7.0.0-beta.14 - version: 7.0.0-beta.14(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 7.0.0-beta.17 + version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/material': - specifier: ^7.2.0 - version: 7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^7.3.4 + version: 7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/x-data-grid': - specifier: ^8.9.1 - version: 8.9.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^8.13.1 + version: 8.13.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tauri-apps/api': - specifier: 2.7.0 - version: 2.7.0 + specifier: 2.8.0 + version: 2.8.0 '@tauri-apps/plugin-clipboard-manager': specifier: ^2.3.0 version: 2.3.0 '@tauri-apps/plugin-dialog': - specifier: ^2.3.1 - version: 2.3.1 + specifier: ^2.4.0 + version: 2.4.0 '@tauri-apps/plugin-fs': - specifier: ^2.4.1 - version: 2.4.1 - '@tauri-apps/plugin-global-shortcut': - specifier: ^2.3.0 - version: 2.3.0 - '@tauri-apps/plugin-notification': - specifier: ^2.3.0 - version: 2.3.0 + specifier: ^2.4.2 + version: 2.4.2 + '@tauri-apps/plugin-http': + specifier: ~2.5.2 + version: 2.5.2 '@tauri-apps/plugin-process': specifier: ^2.3.0 version: 2.3.0 '@tauri-apps/plugin-shell': - specifier: 2.3.0 - version: 2.3.0 + specifier: 2.3.1 + version: 2.3.1 '@tauri-apps/plugin-updater': specifier: 2.9.0 version: 2.9.0 - '@tauri-apps/plugin-window-state': - specifier: ^2.4.0 - version: 2.4.0 '@types/json-schema': specifier: ^7.0.15 version: 7.0.15 ahooks: - specifier: ^3.9.0 - version: 3.9.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^3.9.5 + version: 3.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) axios: - specifier: ^1.11.0 - version: 1.11.0 - chart.js: - specifier: ^4.5.0 - version: 4.5.0 - cli-color: - specifier: ^2.0.4 - version: 2.0.4 + specifier: ^1.12.2 + version: 1.12.2 dayjs: - specifier: 1.11.13 - version: 1.11.13 + specifier: 1.11.18 + version: 1.11.18 foxact: specifier: ^0.2.49 - version: 0.2.49(react@19.1.0) - glob: - specifier: ^11.0.3 - version: 11.0.3 + version: 0.2.49(react@19.2.0) i18next: - specifier: ^25.3.2 - version: 25.3.2(typescript@5.8.3) + specifier: ^25.5.3 + version: 25.5.3(typescript@5.9.3) js-yaml: specifier: ^4.1.0 version: 4.1.0 @@ -105,69 +90,63 @@ importers: specifier: ^4.17.21 version: 4.17.21 monaco-editor: - specifier: ^0.52.2 - version: 0.52.2 + specifier: ^0.53.0 + version: 0.53.0 monaco-yaml: specifier: ^5.4.0 - version: 5.4.0(monaco-editor@0.52.2) + version: 5.4.0(monaco-editor@0.53.0) nanoid: - specifier: ^5.1.5 - version: 5.1.5 + specifier: ^5.1.6 + version: 5.1.6 react: - specifier: 19.1.0 - version: 19.1.0 - react-beautiful-dnd: - specifier: ^13.1.1 - version: 13.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-chartjs-2: - specifier: ^5.3.0 - version: 5.3.0(chart.js@4.5.0)(react@19.1.0) + specifier: 19.2.0 + version: 19.2.0 react-dom: - specifier: 19.1.0 - version: 19.1.0(react@19.1.0) + specifier: 19.2.0 + version: 19.2.0(react@19.2.0) react-error-boundary: specifier: 6.0.0 - version: 6.0.0(react@19.1.0) + version: 6.0.0(react@19.2.0) react-hook-form: - specifier: ^7.61.1 - version: 7.61.1(react@19.1.0) + specifier: ^7.64.0 + version: 7.64.0(react@19.2.0) react-i18next: - specifier: 15.6.1 - version: 15.6.1(i18next@25.3.2(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + specifier: 16.0.0 + version: 16.0.0(i18next@25.5.3(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-markdown: specifier: 10.1.0 - version: 10.1.0(@types/react@19.1.8)(react@19.1.0) + version: 10.1.0(@types/react@19.2.0)(react@19.2.0) react-monaco-editor: specifier: 0.59.0 - version: 0.59.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.59.0(monaco-editor@0.53.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-router-dom: - specifier: 7.7.1 - version: 7.7.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 7.9.3 + version: 7.9.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-virtuoso: - specifier: ^4.13.0 - version: 4.13.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - sockette: - specifier: ^2.0.6 - version: 2.0.6 + specifier: ^4.14.1 + version: 4.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) swr: - specifier: ^2.3.4 - version: 2.3.4(react@19.1.0) - tar: - specifier: ^7.4.3 - version: 7.4.3 + specifier: ^2.3.6 + version: 2.3.6(react@19.2.0) types-pac: specifier: ^1.0.3 version: 1.0.3 zustand: - specifier: ^5.0.6 - version: 5.0.6(@types/react@19.1.8)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + specifier: ^5.0.8 + version: 5.0.8(@types/react@19.2.0)(react@19.2.0)(use-sync-external-store@1.5.0(react@19.2.0)) devDependencies: '@actions/github': specifier: ^6.0.1 version: 6.0.1 + '@eslint-react/eslint-plugin': + specifier: ^2.0.6 + version: 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint/js': + specifier: ^9.37.0 + version: 9.37.0 '@tauri-apps/cli': - specifier: 2.7.1 - version: 2.7.1 + specifier: 2.8.4 + version: 2.8.4 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -175,62 +154,98 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/react': - specifier: 19.1.8 - version: 19.1.8 - '@types/react-beautiful-dnd': - specifier: ^13.1.8 - version: 13.1.8 + specifier: 19.2.0 + version: 19.2.0 '@types/react-dom': - specifier: 19.1.6 - version: 19.1.6(@types/react@19.1.8) + specifier: 19.2.0 + version: 19.2.0(@types/react@19.2.0) '@vitejs/plugin-legacy': - specifier: ^7.1.0 - version: 7.1.0(terser@5.43.1)(vite@7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)) + specifier: ^7.2.1 + version: 7.2.1(terser@5.44.0)(vite@7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1)) '@vitejs/plugin-react': - specifier: 4.7.0 - version: 4.7.0(vite@7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)) + specifier: 5.0.4 + version: 5.0.4(vite@7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1)) adm-zip: specifier: ^0.5.16 version: 0.5.16 + cli-color: + specifier: ^2.0.4 + version: 2.0.4 commander: - specifier: ^14.0.0 - version: 14.0.0 + specifier: ^14.0.1 + version: 14.0.1 cross-env: - specifier: ^10.0.0 - version: 10.0.0 + specifier: ^10.1.0 + version: 10.1.0 + eslint: + specifier: ^9.37.0 + version: 9.37.0(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.37.0(jiti@2.6.1)) + eslint-import-resolver-typescript: + specifier: ^4.4.4 + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import-x: + specifier: ^4.16.1 + version: 4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.5.4 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1))(prettier@3.6.2) + eslint-plugin-react-hooks: + specifier: ^6.1.1 + version: 6.1.1(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: ^0.4.23 + version: 0.4.23(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-unused-imports: + specifier: ^4.2.0 + version: 4.2.0(@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)) + glob: + specifier: ^11.0.3 + version: 11.0.3 + globals: + specifier: ^16.4.0 + version: 16.4.0 https-proxy-agent: specifier: ^7.0.6 version: 7.0.6 + jiti: + specifier: ^2.6.1 + version: 2.6.1 meta-json-schema: - specifier: ^1.19.11 - version: 1.19.11 + specifier: ^1.19.14 + version: 1.19.14 node-fetch: specifier: ^3.3.2 version: 3.3.2 prettier: specifier: ^3.6.2 version: 3.6.2 - prettier-plugin-organize-imports: - specifier: ^4.2.0 - version: 4.2.0(prettier@3.6.2)(typescript@5.8.3) sass: - specifier: ^1.89.2 - version: 1.89.2 + specifier: ^1.93.2 + version: 1.93.2 + tar: + specifier: ^7.5.1 + version: 7.5.1 terser: - specifier: ^5.43.1 - version: 5.43.1 + specifier: ^5.44.0 + version: 5.44.0 typescript: - specifier: ^5.8.3 - version: 5.8.3 + specifier: ^5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.45.0 + version: 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) vite: - specifier: ^7.0.6 - version: 7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) + specifier: ^7.1.9 + version: 7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1) vite-plugin-monaco-editor: specifier: ^1.1.0 - version: 1.1.0(monaco-editor@0.52.2) + version: 1.1.0(monaco-editor@0.53.0) vite-plugin-svgr: - specifier: ^4.3.0 - version: 4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)) + specifier: ^4.5.0 + version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1)) packages: @@ -240,10 +255,6 @@ packages: '@actions/http-client@2.2.3': resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -252,12 +263,12 @@ packages: resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.0': - resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.0': - resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.27.3': @@ -297,8 +308,8 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.27.3': - resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -343,12 +354,12 @@ packages: resolution: {integrity: sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.0': - resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} engines: {node: '>=6.0.0'} hasBin: true @@ -735,20 +746,20 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.0': - resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.2': - resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} '@dnd-kit/accessibility@3.1.1': @@ -773,6 +784,15 @@ packages: peerDependencies: react: '>=16.8.0' + '@emnapi/core@1.5.0': + resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -980,10 +1000,99 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.8.0': + resolution: {integrity: sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint-react/ast@2.0.6': + resolution: {integrity: sha512-i72EzCOYQo+CKnxKRPuguRM0FPIpA/ojDhK0XZI++nO6fG67ggKMSf2DZTIbScus30GsluhaYLpN8R2rppeVJA==} + engines: {node: '>=20.19.0'} + + '@eslint-react/core@2.0.6': + resolution: {integrity: sha512-VbyrfHT3CK22WrYszjXR38tWudlkwDn1G72i/qArWIDppJCFCxfUfNvpM63TaPEUjY/fWWLfsDn9DWltpAvuYw==} + engines: {node: '>=20.19.0'} + + '@eslint-react/eff@2.0.6': + resolution: {integrity: sha512-hBXZg93uB8L8UxjAbgSMQA/27qKeNokXy3Dfkhr6I2gQ4A+IjdXBqbI6hHGlyxyGRmkUpk3RLDELZ/0uqqtysg==} + engines: {node: '>=20.19.0'} + + '@eslint-react/eslint-plugin@2.0.6': + resolution: {integrity: sha512-g+Wz8gr+J0rJVVr4y5DilXroPtZGart1phFWZtcuKPsdPEfINCEyHPzBrscOOzXFa26D5HKQOIxGb/IzYrjVSw==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.36.0 + typescript: ^5.9.3 + + '@eslint-react/kit@2.0.6': + resolution: {integrity: sha512-7Vz4GcKAsUVwpJMBBxEiHI/kXfUlTTTN71YGmvbgBsHpsXRQRQC1+yl3Q3FNsabd8909ww1eay2uIuigfVUkaQ==} + engines: {node: '>=20.19.0'} + + '@eslint-react/shared@2.0.6': + resolution: {integrity: sha512-yhRcipMwhzhYuJMWXHZVVnlAPntpMSUIvMtUYvplKeQvnEX+/awWobZSKqxWzB2QouBWSo5W3Sp2kb4vZonsig==} + engines: {node: '>=20.19.0'} + + '@eslint-react/var@2.0.6': + resolution: {integrity: sha512-fWx7HZ4vpm3woyoclxbtN/Swz7Qgm4LU3mYDs+pHj4ZSOvtqPCTOACMIFR/MwRwEWBAthUyMMZfxFpWz/8RJmA==} + engines: {node: '>=20.19.0'} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.0': + resolution: {integrity: sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.16.0': + resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.37.0': + resolution: {integrity: sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.0': + resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -1003,6 +1112,9 @@ packages: '@jridgewell/gen-mapping@0.3.12': resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1019,31 +1131,28 @@ packages: '@juggle/resize-observer@3.4.0': resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} - '@kurkle/color@0.3.4': - resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@mui/core-downloads-tracker@7.3.4': + resolution: {integrity: sha512-BIktMapG3r4iXwIhYNpvk97ZfYWTreBBQTWjQKbNbzI64+ULHfYavQEX2w99aSWHS58DvXESWIgbD9adKcUOBw==} - '@mui/core-downloads-tracker@7.2.0': - resolution: {integrity: sha512-d49s7kEgI5iX40xb2YPazANvo7Bx0BLg/MNRwv+7BVpZUzXj1DaVCKlQTDex3gy/0jsCb4w7AY2uH4t4AJvSog==} - - '@mui/icons-material@7.2.0': - resolution: {integrity: sha512-gRCspp3pfjHQyTmSOmYw7kUQTd9Udpdan4R8EnZvqPeoAtHnPzkvjBrBqzKaoAbbBp5bGF7BcD18zZJh4nwu0A==} + '@mui/icons-material@7.3.4': + resolution: {integrity: sha512-9n6Xcq7molXWYb680N2Qx+FRW8oT6j/LXF5PZFH3ph9X/Rct0B/BlLAsFI7iL9ySI6LVLuQIVtrLiPT82R7OZw==} engines: {node: '>=14.0.0'} peerDependencies: - '@mui/material': ^7.2.0 + '@mui/material': ^7.3.4 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - '@mui/lab@7.0.0-beta.14': - resolution: {integrity: sha512-pn+ZvylDcBKQOo17oa/PhtIA/UFQFq8RvpN+r/jHrztz/CjMDju2CWBne0txvQ5JIS8uTIGp2/IsTa7II1g5wg==} + '@mui/lab@7.0.0-beta.17': + resolution: {integrity: sha512-H8tSINm6Xgbi7o49MplAwks4tAEE6SpFNd9l7n4NURl0GSpOv0CZvgXKSJt4+6TmquDhE7pomHpHWJiVh/2aCg==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@mui/material': ^7.1.2 - '@mui/material-pigment-css': ^7.1.1 + '@mui/material': ^7.3.2 + '@mui/material-pigment-css': ^7.3.2 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1057,13 +1166,13 @@ packages: '@types/react': optional: true - '@mui/material@7.2.0': - resolution: {integrity: sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==} + '@mui/material@7.3.4': + resolution: {integrity: sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@mui/material-pigment-css': ^7.2.0 + '@mui/material-pigment-css': ^7.3.3 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1077,8 +1186,8 @@ packages: '@types/react': optional: true - '@mui/private-theming@7.2.0': - resolution: {integrity: sha512-y6N1Yt3T5RMxVFnCh6+zeSWBuQdNDm5/UlM0EAYZzZR/1u+XKJWYQmbpx4e+F+1EpkYi3Nk8KhPiQDi83M3zIw==} + '@mui/private-theming@7.3.3': + resolution: {integrity: sha512-OJM+9nj5JIyPUvsZ5ZjaeC9PfktmK+W5YaVLToLR8L0lB/DGmv1gcKE43ssNLSvpoW71Hct0necfade6+kW3zQ==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1087,8 +1196,8 @@ packages: '@types/react': optional: true - '@mui/styled-engine@7.2.0': - resolution: {integrity: sha512-yq08xynbrNYcB1nBcW9Fn8/h/iniM3ewRguGJXPIAbHvxEF7Pz95kbEEOAAhwzxMX4okhzvHmk0DFuC5ayvgIQ==} + '@mui/styled-engine@7.3.3': + resolution: {integrity: sha512-CmFxvRJIBCEaWdilhXMw/5wFJ1+FT9f3xt+m2pPXhHPeVIbBg9MnMvNSJjdALvnQJMPw8jLhrUtXmN7QAZV2fw==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -1100,8 +1209,8 @@ packages: '@emotion/styled': optional: true - '@mui/system@7.2.0': - resolution: {integrity: sha512-PG7cm/WluU6RAs+gNND2R9vDwNh+ERWxPkqTaiXQJGIFAyJ+VxhyKfzpdZNk0z0XdmBxxi9KhFOpgxjehf/O0A==} + '@mui/system@7.3.3': + resolution: {integrity: sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1116,16 +1225,16 @@ packages: '@types/react': optional: true - '@mui/types@7.4.4': - resolution: {integrity: sha512-p63yhbX52MO/ajXC7hDHJA5yjzJekvWD3q4YDLl1rSg+OXLczMYPvTuSuviPRCgRX8+E42RXz1D/dz9SxPSlWg==} + '@mui/types@7.4.7': + resolution: {integrity: sha512-8vVje9rdEr1rY8oIkYgP+Su5Kwl6ik7O3jQ0wl78JGSmiZhRHV+vkjooGdKD8pbtZbutXFVTWQYshu2b3sG9zw==} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - '@mui/utils@7.2.0': - resolution: {integrity: sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw==} + '@mui/utils@7.3.3': + resolution: {integrity: sha512-kwNAUh7bLZ7mRz9JZ+6qfRnnxbE4Zuc+RzXnhSpRSxjTlSTj7b4JxRLXpG+MVtPVtqks5k/XC8No1Vs3x4Z2gg==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1134,8 +1243,8 @@ packages: '@types/react': optional: true - '@mui/x-data-grid@8.9.1': - resolution: {integrity: sha512-N1R3bcxaIpiXqZl7E9nu2b3zp1DC9w5cyaeSJiGhkPpgjKcuogJlL5tMp7UMyUE2h+c0i+1ANMUzD+chHZ6lhg==} + '@mui/x-data-grid@8.13.1': + resolution: {integrity: sha512-64MlyukMoGEDLT3kqdm6tw+rocgMayChj+h+fdAwqD4+2NMQoD5wZElQE+xTNmU0/DPv710X4ENceBRt2hMuGw==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.9.0 @@ -1150,13 +1259,34 @@ packages: '@emotion/styled': optional: true - '@mui/x-internals@8.8.0': - resolution: {integrity: sha512-qTRK5oINkAjZ7sIHpSnESLNq1xtQUmmfmGscYUSEP0uHoYh6pKkNWH9+7yzggRHuTv+4011VBwN9s+efrk+xZg==} + '@mui/x-internals@8.13.1': + resolution: {integrity: sha512-OKQyCJ9uxtMpjBZCOEQGOR5MhgL1f9HjI4qZHuaLxxtDATK5rcBbVjBF67hI8FzXeF1wrcZP2wsjc4AgGpAo9g==} engines: {node: '>=14.0.0'} peerDependencies: - '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 + '@mui/x-virtualizer@0.2.2': + resolution: {integrity: sha512-+ZcGYh/9ykoEofzcAWcJ3n6TBXzCc2ETvytho30wRkYv1ez+8yps0ezns/QvC4JqXBge/3y+e+QatIYjkTltdw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@octokit/auth-token@4.0.0': resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} engines: {node: '>= 18'} @@ -1287,14 +1417,18 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@rolldown/pluginutils@1.0.0-beta.27': - resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-beta.38': + resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==} - '@rollup/pluginutils@5.1.4': - resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + '@rollup/pluginutils@5.2.0': + resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -1302,106 +1436,109 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.40.2': - resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==} + '@rollup/rollup-android-arm-eabi@4.46.2': + resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.40.2': - resolution: {integrity: sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==} + '@rollup/rollup-android-arm64@4.46.2': + resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.40.2': - resolution: {integrity: sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==} + '@rollup/rollup-darwin-arm64@4.46.2': + resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.40.2': - resolution: {integrity: sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==} + '@rollup/rollup-darwin-x64@4.46.2': + resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.40.2': - resolution: {integrity: sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==} + '@rollup/rollup-freebsd-arm64@4.46.2': + resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.40.2': - resolution: {integrity: sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==} + '@rollup/rollup-freebsd-x64@4.46.2': + resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.40.2': - resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==} + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.40.2': - resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==} + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.40.2': - resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==} + '@rollup/rollup-linux-arm64-gnu@4.46.2': + resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.40.2': - resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==} + '@rollup/rollup-linux-arm64-musl@4.46.2': + resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.40.2': - resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==} + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': - resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==} + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.40.2': - resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==} + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.40.2': - resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==} + '@rollup/rollup-linux-riscv64-musl@4.46.2': + resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.40.2': - resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==} + '@rollup/rollup-linux-s390x-gnu@4.46.2': + resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.40.2': - resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==} + '@rollup/rollup-linux-x64-gnu@4.46.2': + resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.40.2': - resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==} + '@rollup/rollup-linux-x64-musl@4.46.2': + resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.40.2': - resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==} + '@rollup/rollup-win32-arm64-msvc@4.46.2': + resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.40.2': - resolution: {integrity: sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==} + '@rollup/rollup-win32-ia32-msvc@4.46.2': + resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.40.2': - resolution: {integrity: sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==} + '@rollup/rollup-win32-x64-msvc@4.46.2': + resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} cpu: [x64] os: [win32] + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -1470,106 +1607,103 @@ packages: peerDependencies: '@svgr/core': '*' - '@tauri-apps/api@2.7.0': - resolution: {integrity: sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg==} + '@tauri-apps/api@2.8.0': + resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==} - '@tauri-apps/cli-darwin-arm64@2.7.1': - resolution: {integrity: sha512-j2NXQN6+08G03xYiyKDKqbCV2Txt+hUKg0a8hYr92AmoCU8fgCjHyva/p16lGFGUG3P2Yu0xiNe1hXL9ZuRMzA==} + '@tauri-apps/cli-darwin-arm64@2.8.4': + resolution: {integrity: sha512-BKu8HRkYV01SMTa7r4fLx+wjgtRK8Vep7lmBdHDioP6b8XH3q2KgsAyPWfEZaZIkZ2LY4SqqGARaE9oilNe0oA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.7.1': - resolution: {integrity: sha512-CdYAefeM35zKsc91qIyKzbaO7FhzTyWKsE8hj7tEJ1INYpoh1NeNNyL/NSEA3Nebi5ilugioJ5tRK8ZXG8y3gw==} + '@tauri-apps/cli-darwin-x64@2.8.4': + resolution: {integrity: sha512-imb9PfSd/7G6VAO7v1bQ2A3ZH4NOCbhGJFLchxzepGcXf9NKkfun157JH9mko29K6sqAwuJ88qtzbKCbWJTH9g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.7.1': - resolution: {integrity: sha512-dnvyJrTA1UJxJjQ8q1N/gWomjP8Twij1BUQu2fdcT3OPpqlrbOk5R1yT0oD/721xoKNjroB5BXCsmmlykllxNg==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.8.4': + resolution: {integrity: sha512-Ml215UnDdl7/fpOrF1CNovym/KjtUbCuPgrcZ4IhqUCnhZdXuphud/JT3E8X97Y03TZ40Sjz8raXYI2ET0exzw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.7.1': - resolution: {integrity: sha512-FtBW6LJPNRTws3qyUc294AqCWU91l/H0SsFKq6q4Q45MSS4x6wxLxou8zB53tLDGEPx3JSoPLcDaSfPlSbyujQ==} + '@tauri-apps/cli-linux-arm64-gnu@2.8.4': + resolution: {integrity: sha512-pbcgBpMyI90C83CxE5REZ9ODyIlmmAPkkJXtV398X3SgZEIYy5TACYqlyyv2z5yKgD8F8WH4/2fek7+jH+ZXAw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.7.1': - resolution: {integrity: sha512-/HXY0t4FHkpFzjeYS5c16mlA6z0kzn5uKLWptTLTdFSnYpr8FCnOP4Sdkvm2TDQPF2ERxXtNCd+WR/jQugbGnA==} + '@tauri-apps/cli-linux-arm64-musl@2.8.4': + resolution: {integrity: sha512-zumFeaU1Ws5Ay872FTyIm7z8kfzEHu8NcIn8M6TxbJs0a7GRV21KBdpW1zNj2qy7HynnpQCqjAYXTUUmm9JAOw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-riscv64-gnu@2.7.1': - resolution: {integrity: sha512-GeW5lVI2GhhnaYckiDzstG2j2Jwlud5d2XefRGwlOK+C/bVGLT1le8MNPYK8wgRlpeK8fG1WnJJYD6Ke7YQ8bg==} + '@tauri-apps/cli-linux-riscv64-gnu@2.8.4': + resolution: {integrity: sha512-qiqbB3Zz6IyO201f+1ojxLj65WYj8mixL5cOMo63nlg8CIzsP23cPYUrx1YaDPsCLszKZo7tVs14pc7BWf+/aQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.7.1': - resolution: {integrity: sha512-DprxKQkPxIPYwUgg+cscpv2lcIUhn2nxEPlk0UeaiV9vATxCXyytxr1gLcj3xgjGyNPlM0MlJyYaPy1JmRg1cA==} + '@tauri-apps/cli-linux-x64-gnu@2.8.4': + resolution: {integrity: sha512-TaqaDd9Oy6k45Hotx3pOf+pkbsxLaApv4rGd9mLuRM1k6YS/aw81YrsMryYPThrxrScEIUcmNIHaHsLiU4GMkw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.7.1': - resolution: {integrity: sha512-KLlq3kOK7OUyDR757c0zQjPULpGZpLhNB0lZmZpHXvoOUcqZoCXJHh4dT/mryWZJp5ilrem5l8o9ngrDo0X1AA==} + '@tauri-apps/cli-linux-x64-musl@2.8.4': + resolution: {integrity: sha512-ot9STAwyezN8w+bBHZ+bqSQIJ0qPZFlz/AyscpGqB/JnJQVDFQcRDmUPFEaAtt2UUHSWzN3GoTJ5ypqLBp2WQA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@2.7.1': - resolution: {integrity: sha512-dH7KUjKkSypCeWPiainHyXoES3obS+JIZVoSwSZfKq2gWgs48FY3oT0hQNYrWveE+VR4VoR3b/F3CPGbgFvksA==} + '@tauri-apps/cli-win32-arm64-msvc@2.8.4': + resolution: {integrity: sha512-+2aJ/g90dhLiOLFSD1PbElXX3SoMdpO7HFPAZB+xot3CWlAZD1tReUFy7xe0L5GAR16ZmrxpIDM9v9gn5xRy/w==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.7.1': - resolution: {integrity: sha512-1oeibfyWQPVcijOrTg709qhbXArjX3x1MPjrmA5anlygwrbByxLBcLXvotcOeULFcnH2FYUMMLLant8kgvwE5A==} + '@tauri-apps/cli-win32-ia32-msvc@2.8.4': + resolution: {integrity: sha512-yj7WDxkL1t9Uzr2gufQ1Hl7hrHuFKTNEOyascbc109EoiAqCp0tgZ2IykQqOZmZOHU884UAWI1pVMqBhS/BfhA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.7.1': - resolution: {integrity: sha512-D7Q9kDObutuirCNLxYQ7KAg2Xxg99AjcdYz/KuMw5HvyEPbkC9Q7JL0vOrQOrHEHxIQ2lYzFOZvKKoC2yyqXcg==} + '@tauri-apps/cli-win32-x64-msvc@2.8.4': + resolution: {integrity: sha512-XuvGB4ehBdd7QhMZ9qbj/8icGEatDuBNxyYHbLKsTYh90ggUlPa/AtaqcC1Fo69lGkTmq9BOKrs1aWSi7xDonA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.7.1': - resolution: {integrity: sha512-RcGWR4jOUEl92w3uvI0h61Llkfj9lwGD1iwvDRD2isMrDhOzjeeeVn9aGzeW1jubQ/kAbMYfydcA4BA0Cy733Q==} + '@tauri-apps/cli@2.8.4': + resolution: {integrity: sha512-ejUZBzuQRcjFV+v/gdj/DcbyX/6T4unZQjMSBZwLzP/CymEjKcc2+Fc8xTORThebHDUvqoXMdsCZt8r+hyN15g==} engines: {node: '>= 10'} hasBin: true '@tauri-apps/plugin-clipboard-manager@2.3.0': resolution: {integrity: sha512-81NOBA2P+OTY8RLkBwyl9ZR/0CeggLub4F6zxcxUIfFOAqtky7J61+K/MkH2SC1FMxNBxrX0swDuKvkjkHadlA==} - '@tauri-apps/plugin-dialog@2.3.1': - resolution: {integrity: sha512-B7jvyhycV8SI/WHzPjciwtYfdFM6/9EXuMjRgYWZwn8GPDmHxpT80aJdb/eDVN+NgoAFDh9bu4QPonYahoYnZQ==} + '@tauri-apps/plugin-dialog@2.4.0': + resolution: {integrity: sha512-OvXkrEBfWwtd8tzVCEXIvRfNEX87qs2jv6SqmVPiHcJjBhSF/GUvjqUNIDmKByb5N8nvDqVUM7+g1sXwdC/S9w==} - '@tauri-apps/plugin-fs@2.4.1': - resolution: {integrity: sha512-vJlKZVGF3UAFGoIEVT6Oq5L4HGDCD78WmA4uhzitToqYiBKWAvZR61M6zAyQzHqLs0ADemkE4RSy/5sCmZm6ZQ==} + '@tauri-apps/plugin-fs@2.4.2': + resolution: {integrity: sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig==} - '@tauri-apps/plugin-global-shortcut@2.3.0': - resolution: {integrity: sha512-WbAz0ElhpP+0kzQZRScdCC7UQ7OPH8PAn//fsBNu7+ywihsnVSVOg1L9YhieAtLNtAlnmFI69Yl5AGaA3ge5IQ==} - - '@tauri-apps/plugin-notification@2.3.0': - resolution: {integrity: sha512-QDwXo9VzAlH97c0veuf19TZI6cRBPfJDl2O6hNEDvI66j60lOO9z+PL6MJrj8A6Y+t55r7mGhe3rQWLmOre2HA==} + '@tauri-apps/plugin-http@2.5.2': + resolution: {integrity: sha512-x1mQKHSLDk4mS2S938OTeyk8L7QyLpCrKZCZcjkljGsvTvRMojCvI9SeJ1kaxc7t8xSilkC7WdId8xER9TIGLg==} '@tauri-apps/plugin-process@2.3.0': resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==} - '@tauri-apps/plugin-shell@2.3.0': - resolution: {integrity: sha512-6GIRxO2z64uxPX4CCTuhQzefvCC0ew7HjdBhMALiGw74vFBDY95VWueAHOHgNOMV4UOUAFupyidN9YulTe5xlA==} + '@tauri-apps/plugin-shell@2.3.1': + resolution: {integrity: sha512-jjs2WGDO/9z2pjNlydY/F5yYhNsscv99K5lCmU5uKjsVvQ3dRlDhhtVYoa4OLDmktLtQvgvbQjCFibMl6tgGfw==} '@tauri-apps/plugin-updater@2.9.0': resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==} - '@tauri-apps/plugin-window-state@2.4.0': - resolution: {integrity: sha512-hRSzPNi2NG0lPFthfVY0V5C1MyWN/gGaQtQYw7i9zZhLzrhZveHZ2omHG1rIiIsjfTGbO7fhjydSoeTTK9GqLw==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1589,16 +1723,14 @@ packages: '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/hoist-non-react-statics@3.3.7': - resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} - peerDependencies: - '@types/react': '*' + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -1606,6 +1738,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} @@ -1624,24 +1759,21 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/react-beautiful-dnd@13.1.8': - resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==} - - '@types/react-dom@19.1.6': - resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} + '@types/react-dom@19.2.0': + resolution: {integrity: sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==} peerDependencies: - '@types/react': ^19.0.0 - - '@types/react-redux@7.1.34': - resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==} + '@types/react': ^19.2.0 '@types/react-transition-group@4.4.12': resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} peerDependencies: '@types/react': '*' - '@types/react@19.1.8': - resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} + '@types/react@19.2.0': + resolution: {integrity: sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==} + + '@types/trusted-types@1.0.6': + resolution: {integrity: sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1649,24 +1781,183 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.45.0': + resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.45.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.45.0': + resolution: {integrity: sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.45.0': + resolution: {integrity: sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.45.0': + resolution: {integrity: sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.45.0': + resolution: {integrity: sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.45.0': + resolution: {integrity: sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.45.0': + resolution: {integrity: sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.45.0': + resolution: {integrity: sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.45.0': + resolution: {integrity: sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.45.0': + resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitejs/plugin-legacy@7.1.0': - resolution: {integrity: sha512-dzpcfN7gWWgzUwSxv7/ntUTEvoA8BYQWsdrawZFfwDSbitsEOXj8oSLn7I8QHJ8ijDukmCxsuUcib42EyTEVMw==} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@vitejs/plugin-legacy@7.2.1': + resolution: {integrity: sha512-CaXb/y0mlfu7jQRELEJJc2/5w2bX2m1JraARgFnvSB2yfvnCNJVWWlqAo6WjnKoepOwKx8gs0ugJThPLKCOXIg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: terser: ^5.16.0 vite: ^7.0.0 - '@vitejs/plugin-react@4.7.0': - resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} - engines: {node: ^14.18.0 || >=16.0.0} + '@vitejs/plugin-react@5.0.4': + resolution: {integrity: sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==} + engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true @@ -1678,13 +1969,16 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} - ahooks@3.9.0: - resolution: {integrity: sha512-r20/C38aFyU3Zqp3620gkdLnxmQhnmWORB3eGGTDlM4i/fOc0GUvM+f2oleMzEu7b3+pHXyzz+FB6ojxsUdYdw==} - engines: {node: '>=8.0.0'} + ahooks@3.9.5: + resolution: {integrity: sha512-TrjXie49Q8HuHKTa84Fm9A+famMDAG1+7a9S9Gq6RQ0h90Jgqmiq3CkObuRjWT/C4d6nRZCw35Y2k2fmybb5eA==} + engines: {node: '>=18'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1704,11 +1998,43 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.11.0: - resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} @@ -1732,9 +2058,21 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + birecord@0.1.1: + resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1758,6 +2096,14 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1772,6 +2118,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -1784,10 +2134,6 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - chart.js@4.5.0: - resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} - engines: {pnpm: '>=8'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1821,13 +2167,23 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - commander@14.0.0: - resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + commander@14.0.1: + resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} engines: {node: '>=20'} commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -1841,8 +2197,8 @@ packages: core-js-compat@3.44.0: resolution: {integrity: sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==} - core-js@3.44.0: - resolution: {integrity: sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==} + core-js@3.45.0: + resolution: {integrity: sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==} cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} @@ -1857,8 +2213,8 @@ packages: typescript: optional: true - cross-env@10.0.0: - resolution: {integrity: sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==} + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} engines: {node: '>=20'} hasBin: true @@ -1866,9 +2222,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-box-model@1.2.1: - resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} - csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1880,8 +2233,28 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + dayjs@1.11.18: + resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} @@ -1895,6 +2268,17 @@ packages: decode-named-character-reference@1.1.0: resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1914,6 +2298,10 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -1943,6 +2331,10 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1959,6 +2351,14 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + es5-ext@0.10.64: resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} engines: {node: '>=0.10'} @@ -1986,10 +2386,199 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@4.4.4: + resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + engines: {node: ^16.17.0 || >=18.6.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import-x@4.16.1: + resolution: {integrity: sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/utils': ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + eslint-import-resolver-node: '*' + peerDependenciesMeta: + '@typescript-eslint/utils': + optional: true + eslint-import-resolver-node: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-react-debug@2.0.6: + resolution: {integrity: sha512-uUqjSApa9nGQh9VEVktp7j4T/wK0WbJVQ5phHwcFQcSVOfEr9G6ISjpjFG/lJ70SIX3v3rAOrF7GvEZWA7MxcQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.36.0 + typescript: ^5.9.3 + + eslint-plugin-react-dom@2.0.6: + resolution: {integrity: sha512-PQzi+KksafJsoRN9gyM+a9JMJsvHxy6vCzr2c/eZvSluRDFxyWWwIRPCHijus0I8hlh0fyOOhR1S3DU2c05Zuw==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.36.0 + typescript: ^5.9.3 + + eslint-plugin-react-hooks-extra@2.0.6: + resolution: {integrity: sha512-Dg82EIM+4J+Ideb61Hc+vnwGS5icBVB+D2uqINqEngxmDCDhubM+zTsjYJd9qeuiAQ3e7qM8A1GPHkHyrGW5bQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + eslint: ^9.36.0 + typescript: ^5.9.3 + + eslint-plugin-react-hooks@6.1.1: + resolution: {integrity: sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-naming-convention@2.0.6: + resolution: {integrity: sha512-BWgXnegBQU0GbOIvQRVKjXak//hOQLfiNXGO7vJ7MBN2DVEOEL8UK52YfxWW4dQYa7q4NNTqy7UOkK9sgpVw6g==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.36.0 + typescript: ^5.9.3 + + eslint-plugin-react-refresh@0.4.23: + resolution: {integrity: sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==} + peerDependencies: + eslint: '>=8.40' + + eslint-plugin-react-web-api@2.0.6: + resolution: {integrity: sha512-NGH3mUJ/L3OD12g0RaNQP5ofripIFnVuGGbOrENw9ret6Q7XGNDALHjsLxYCmFJAcHXun0W4DKmH1mHm8hyrIQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.36.0 + typescript: ^5.9.3 + + eslint-plugin-react-x@2.0.6: + resolution: {integrity: sha512-x8i52RElVgIUxSneM4uC+nkQcBGEFkiXAD8/QgxBbrfm0VKZ1748IBuNrp7125Ubx3JDlqJfJz1iEPJCyhvdAA==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.36.0 + typescript: ^5.9.3 + + eslint-plugin-unused-imports@4.2.0: + resolution: {integrity: sha512-hLbJ2/wnjKq4kGA9AUaExVFIbNzyxYdVo49QZmKCnhk5pc9wcYRbfgLHvWJ8tnsdcseGhoUAddm9gn/lt+d74w==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.37.0: + resolution: {integrity: sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + esniff@2.0.1: resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} engines: {node: '>=0.10'} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} @@ -2009,8 +2598,28 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -2021,6 +2630,10 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2028,6 +2641,17 @@ packages: find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -2037,6 +2661,10 @@ packages: debug: optional: true + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -2065,6 +2693,13 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2077,15 +2712,60 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} hasBin: true + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -2117,14 +2797,22 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - i18next@25.3.2: - resolution: {integrity: sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA==} + i18next@25.5.3: + resolution: {integrity: sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==} peerDependencies: typescript: ^5 peerDependenciesMeta: typescript: optional: true + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + immutable@5.1.2: resolution: {integrity: sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==} @@ -2132,9 +2820,17 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + intersection-observer@0.12.2: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} @@ -2144,13 +2840,44 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -2158,10 +2885,18 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2169,6 +2904,24 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-immutable-type@5.0.1: + resolution: {integrity: sha512-LkHEOGVZZXxGl8vDs+10k3DvP++SEoYEAJLRk6buTFi6kD7QekThV7xHS0j6gpnUCQ0zpud/gMDGiV4dQneLTg==} + peerDependencies: + eslint: '*' + typescript: '>=4.7.4' + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2180,6 +2933,45 @@ packages: is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2187,6 +2979,10 @@ packages: resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + js-cookie@3.0.5: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} @@ -2208,12 +3004,25 @@ packages: engines: {node: '>=6'} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2222,15 +3031,29 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -2285,9 +3108,6 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - memoizee@0.4.17: resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} engines: {node: '>=0.12'} @@ -2296,8 +3116,12 @@ packages: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} - meta-json-schema@1.19.11: - resolution: {integrity: sha512-qIcE1GmsF2b2CNMl7/C1w9nLaGOI1tP8/DrAZm0bJ+tkUAoKL1G2ZsdRUUiKFAy2Z0mbmQ8GCQ/+IBhjMRxCmQ==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + meta-json-schema@1.19.14: + resolution: {integrity: sha512-A+NSHAfXWn2T225dawVuLXVXrSWhxjRNiG+nS+Cet1Zovslrq2lMqvkIrXhdaK6Gv+VYrEV8rAkYcqAz2pxKMw==} engines: {node: '>=18', pnpm: '>=9'} micromark-core-commonmark@2.0.3: @@ -2379,21 +3203,26 @@ packages: resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - - monaco-editor@0.52.2: - resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + monaco-editor@0.53.0: + resolution: {integrity: sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==} monaco-languageserver-types@0.4.0: resolution: {integrity: sha512-QQ3BZiU5LYkJElGncSNb5AKoJ/LCs6YBMCJMAz9EA7v+JaOdn3kx2cXpPTcZfKA5AEsR0vc97sAw+5mdNhVBmw==} @@ -2422,11 +3251,19 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.5: - resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} engines: {node: ^18 || >=20} hasBin: true + napi-postinstall@0.3.3: + resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -2452,9 +3289,49 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -2472,6 +3349,10 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2498,19 +3379,21 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - prettier-plugin-organize-imports@4.2.0: - resolution: {integrity: sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg==} - peerDependencies: - prettier: '>=2.0' - typescript: '>=2.9' - vue-tsc: ^2.1.0 || 3 - peerDependenciesMeta: - vue-tsc: - optional: true + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} prettier@3.6.2: resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} @@ -2526,26 +3409,17 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - raf-schd@4.0.3: - resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} - react-beautiful-dnd@13.1.1: - resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} - deprecated: 'react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672' - peerDependencies: - react: ^16.8.5 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-chartjs-2@5.3.0: - resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==} + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} peerDependencies: - chart.js: ^4.1.1 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - react-dom@19.1.0: - resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} - peerDependencies: - react: ^19.1.0 + react: ^19.2.0 react-error-boundary@6.0.0: resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==} @@ -2555,16 +3429,16 @@ packages: react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-hook-form@7.61.1: - resolution: {integrity: sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==} + react-hook-form@7.64.0: + resolution: {integrity: sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@15.6.1: - resolution: {integrity: sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==} + react-i18next@16.0.0: + resolution: {integrity: sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==} peerDependencies: - i18next: '>= 23.2.3' + i18next: '>= 25.5.2' react: '>= 16.8.0' react-dom: '*' react-native: '*' @@ -2580,11 +3454,8 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - react-is@19.1.0: - resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} + react-is@19.1.1: + resolution: {integrity: sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==} react-markdown@10.1.0: resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} @@ -2599,31 +3470,19 @@ packages: react: '>=16.8.0 <20.0.0' react-dom: '>=16.8.0 <20.0.0' - react-redux@7.2.9: - resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} - peerDependencies: - react: ^16.8.3 || ^17 || ^18 - react-dom: '*' - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} - react-router-dom@7.7.1: - resolution: {integrity: sha512-bavdk2BA5r3MYalGKZ01u8PGuDBloQmzpBZVhDLrOOv1N943Wq6dcM9GhB3x8b7AbqPMEezauv4PeGkAJfy7FQ==} + react-router-dom@7.9.3: + resolution: {integrity: sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.7.1: - resolution: {integrity: sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==} + react-router@7.9.3: + resolution: {integrity: sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -2638,22 +3497,23 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react-virtuoso@4.13.0: - resolution: {integrity: sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA==} + react-virtuoso@4.14.1: + resolution: {integrity: sha512-NRUF1ak8lY+Tvc6WN9cce59gU+lilzVtOozP+pm9J7iHshLGGjsiAB4rB2qlBPHjFbcXOQpT+7womNHGDUql8w==} peerDependencies: react: '>=16 || >=17 || >= 18 || >= 19' react-dom: '>=16 || >=17 || >= 18 || >=19' - react@19.1.0: - resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - redux@4.2.1: - resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} regenerate-unicode-properties@10.2.0: resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} @@ -2665,6 +3525,10 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + regexpu-core@6.2.0: resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} engines: {node: '>=4'} @@ -2692,23 +3556,45 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} hasBin: true - rollup@4.40.2: - resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.46.2: + resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - sass@1.89.2: - resolution: {integrity: sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + sass@1.93.2: + resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==} engines: {node: '>=14.0.0'} hasBin: true - scheduler@0.26.0: - resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} screenfull@5.2.0: resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} @@ -2718,12 +3604,29 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2732,6 +3635,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2739,9 +3658,6 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - sockette@2.0.6: - resolution: {integrity: sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2760,6 +3676,17 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-ts@2.2.1: + resolution: {integrity: sha512-Q2u0gko67PLLhbte5HmPfdOjNvUKbKQM+mCNQae6jE91DmoFHY6HH9GcdqCeNx87DZ2KKjiFxmA0R/42OneGWw==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2768,6 +3695,18 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -2779,6 +3718,14 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + style-to-js@1.1.16: resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} @@ -2788,6 +3735,10 @@ packages: stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -2795,20 +3746,24 @@ packages: svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} - swr@2.3.4: - resolution: {integrity: sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==} + swr@2.3.6: + resolution: {integrity: sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==} peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + systemjs@6.15.1: resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==} - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} engines: {node: '>=18'} - terser@5.43.1: - resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + terser@5.44.0: + resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} engines: {node: '>=10'} hasBin: true @@ -2816,11 +3771,8 @@ packages: resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} engines: {node: '>=0.12'} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} to-regex-range@5.0.1: @@ -2833,6 +3785,23 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-declaration-location@1.0.7: + resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} + peerDependencies: + typescript: '>=4.0.0' + + ts-pattern@5.8.0: + resolution: {integrity: sha512-kIjN2qmWiHnhgr5DAkAafF9fwb0T5OhMVSWrm8XEdTFnX6+wfXwYOFjeF86UZ54vduqiR7BfqScFmXSzSaH8oA==} + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2840,17 +3809,48 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + types-pac@1.0.3: resolution: {integrity: sha512-MF2UAZGvGMOM+vHi9Zj/LvQqdNN1m1xSB+PjAW9B/GvFqaB4GwR18YaIbGIGDRTW/J8iqFXQHLZd5eJVtho46w==} - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript-eslint@8.45.0: + resolution: {integrity: sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + undici@5.29.0: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} @@ -2892,16 +3892,17 @@ packages: universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - use-memo-one@1.1.3: - resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} use-sync-external-store@1.5.0: resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} @@ -2919,13 +3920,13 @@ packages: peerDependencies: monaco-editor: '>=0.33.0' - vite-plugin-svgr@4.3.0: - resolution: {integrity: sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==} + vite-plugin-svgr@4.5.0: + resolution: {integrity: sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==} peerDependencies: vite: '>=2.6.0' - vite@7.0.6: - resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==} + vite@7.1.9: + resolution: {integrity: sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2988,11 +3989,31 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3020,8 +4041,21 @@ packages: engines: {node: '>= 14'} hasBin: true - zustand@5.0.6: - resolution: {integrity: sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.1.11: + resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} + + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -3058,11 +4092,6 @@ snapshots: tunnel: 0.0.6 undici: 5.29.0 - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 - '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -3071,18 +4100,18 @@ snapshots: '@babel/compat-data@7.28.0': {} - '@babel/core@7.28.0': + '@babel/core@7.28.4': dependencies: - '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 + '@babel/generator': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.27.6 - '@babel/parser': 7.28.0 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -3091,17 +4120,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.0': + '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@jridgewell/gen-mapping': 0.3.12 '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -3111,29 +4140,29 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.4 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.0)': + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-annotate-as-pure': 7.27.3 regexpu-core: 6.2.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.0)': + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 debug: 4.4.1 @@ -3146,55 +4175,55 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.0)': + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-wrap-function': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -3207,555 +4236,571 @@ snapshots: '@babel/helper-wrap-function@7.27.1': dependencies: '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/helpers@7.27.6': + '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 - '@babel/parser@7.28.0': + '@babel/parser@7.28.4': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.4) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.0)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) - '@babel/traverse': 7.28.0 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-classes@7.28.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) - '@babel/traverse': 7.28.0 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 '@babel/template': 7.27.2 - '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) - '@babel/traverse': 7.28.0 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.0)': + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regenerator@7.28.1(@babel/core@7.28.0)': + '@babel/plugin-transform-regenerator@7.28.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/preset-env@7.28.0(@babel/core@7.28.0)': + '@babel/preset-env@7.28.0(@babel/core@7.28.4)': dependencies: '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0) - '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) - '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-regenerator': 7.28.1(@babel/core@7.28.0) - '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.0) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.0) - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.0) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.0) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0) + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.4) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.4) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-regenerator': 7.28.1(@babel/core@7.28.4) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.4) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.4) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.4) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.4) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.4) core-js-compat: 3.44.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.0)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 esutils: 2.0.3 - '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.0 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 - '@babel/traverse@7.28.0': + '@babel/traverse@7.28.4': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 + '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.0 + '@babel/parser': 7.28.4 '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 debug: 4.4.1 transitivePeerDependencies: - supports-color - '@babel/types@7.28.2': + '@babel/types@7.28.4': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@dnd-kit/accessibility@3.1.1(react@19.1.0)': + '@dnd-kit/accessibility@3.1.1(react@19.2.0)': dependencies: - react: 19.1.0 + react: 19.2.0 tslib: 2.8.1 - '@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@dnd-kit/core@6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@dnd-kit/accessibility': 3.1.1(react@19.1.0) - '@dnd-kit/utilities': 3.2.2(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@dnd-kit/accessibility': 3.1.1(react@19.2.0) + '@dnd-kit/utilities': 3.2.2(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) tslib: 2.8.1 - '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)': dependencies: - '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@dnd-kit/utilities': 3.2.2(react@19.1.0) - react: 19.1.0 + '@dnd-kit/core': 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@dnd-kit/utilities': 3.2.2(react@19.2.0) + react: 19.2.0 tslib: 2.8.1 - '@dnd-kit/utilities@3.2.2(react@19.1.0)': + '@dnd-kit/utilities@3.2.2(react@19.2.0)': dependencies: - react: 19.1.0 + react: 19.2.0 tslib: 2.8.1 + '@emnapi/core@1.5.0': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -3784,19 +4829,19 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0)': + '@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 - react: 19.1.0 + react: 19.2.0 optionalDependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.0 transitivePeerDependencies: - supports-color @@ -3810,26 +4855,26 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@19.1.8)(react@19.1.0) + '@emotion/react': 11.14.0(@types/react@19.2.0)(react@19.2.0) '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) '@emotion/utils': 1.4.2 - react: 19.1.0 + react: 19.2.0 optionalDependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.0 transitivePeerDependencies: - supports-color '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.0)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.0)': dependencies: - react: 19.1.0 + react: 19.2.0 '@emotion/utils@1.4.2': {} @@ -3912,8 +4957,153 @@ snapshots: '@esbuild/win32-x64@0.25.4': optional: true + '@eslint-community/eslint-utils@4.8.0(eslint@9.37.0(jiti@2.6.1))': + dependencies: + eslint: 9.37.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint-react/ast@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 2.0.6 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + string-ts: 2.2.1 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/core@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/ast': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.0.6 + '@eslint-react/kit': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + birecord: 0.1.1 + ts-pattern: 5.8.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/eff@2.0.6': {} + + '@eslint-react/eslint-plugin@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 2.0.6 + '@eslint-react/kit': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + eslint-plugin-react-debug: 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-dom: 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-hooks-extra: 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-naming-convention: 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-web-api: 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-x: 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@eslint-react/kit@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 2.0.6 + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/shared@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 2.0.6 + '@eslint-react/kit': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + ts-pattern: 5.8.0 + zod: 4.1.11 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/var@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/ast': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.0.6 + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + ts-pattern: 5.8.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.0': + dependencies: + '@eslint/core': 0.16.0 + + '@eslint/core@0.16.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.37.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.4.0': + dependencies: + '@eslint/core': 0.16.0 + levn: 0.4.1 + '@fastify/busboy@2.1.1': {} + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -3938,6 +5128,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/source-map@0.3.6': @@ -3954,139 +5149,167 @@ snapshots: '@juggle/resize-observer@3.4.0': {} - '@kurkle/color@0.3.4': {} + '@mui/core-downloads-tracker@7.3.4': {} - '@mui/core-downloads-tracker@7.2.0': {} - - '@mui/icons-material@7.2.0(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': + '@mui/icons-material@7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.0)(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 - '@mui/material': 7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 + '@babel/runtime': 7.28.4 + '@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 optionalDependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.0 - '@mui/lab@7.0.0-beta.14(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 - '@mui/material': 7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@mui/system': 7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) - '@mui/types': 7.4.4(@types/react@19.1.8) - '@mui/utils': 7.2.0(@types/react@19.1.8)(react@19.1.0) + '@babel/runtime': 7.28.4 + '@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/system': 7.3.3(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0) + '@mui/types': 7.4.7(@types/react@19.2.0) + '@mui/utils': 7.3.3(@types/react@19.2.0)(react@19.2.0) clsx: 2.1.1 prop-types: 15.8.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.1.8)(react@19.1.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) - '@types/react': 19.1.8 + '@emotion/react': 11.14.0(@types/react@19.2.0)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0) + '@types/react': 19.2.0 - '@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 - '@mui/core-downloads-tracker': 7.2.0 - '@mui/system': 7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) - '@mui/types': 7.4.4(@types/react@19.1.8) - '@mui/utils': 7.2.0(@types/react@19.1.8)(react@19.1.0) + '@babel/runtime': 7.28.4 + '@mui/core-downloads-tracker': 7.3.4 + '@mui/system': 7.3.3(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0) + '@mui/types': 7.4.7(@types/react@19.2.0) + '@mui/utils': 7.3.3(@types/react@19.2.0)(react@19.2.0) '@popperjs/core': 2.11.8 - '@types/react-transition-group': 4.4.12(@types/react@19.1.8) + '@types/react-transition-group': 4.4.12(@types/react@19.2.0) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-is: 19.1.0 - react-transition-group: 4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-is: 19.1.1 + react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.1.8)(react@19.1.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) - '@types/react': 19.1.8 + '@emotion/react': 11.14.0(@types/react@19.2.0)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0) + '@types/react': 19.2.0 - '@mui/private-theming@7.2.0(@types/react@19.1.8)(react@19.1.0)': + '@mui/private-theming@7.3.3(@types/react@19.2.0)(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 - '@mui/utils': 7.2.0(@types/react@19.1.8)(react@19.1.0) + '@babel/runtime': 7.28.4 + '@mui/utils': 7.3.3(@types/react@19.2.0)(react@19.2.0) prop-types: 15.8.1 - react: 19.1.0 + react: 19.2.0 optionalDependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.0 - '@mui/styled-engine@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': + '@mui/styled-engine@7.3.3(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 '@emotion/sheet': 1.4.0 csstype: 3.1.3 prop-types: 15.8.1 - react: 19.1.0 + react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.1.8)(react@19.1.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + '@emotion/react': 11.14.0(@types/react@19.2.0)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0) - '@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': + '@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 - '@mui/private-theming': 7.2.0(@types/react@19.1.8)(react@19.1.0) - '@mui/styled-engine': 7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) - '@mui/types': 7.4.4(@types/react@19.1.8) - '@mui/utils': 7.2.0(@types/react@19.1.8)(react@19.1.0) + '@babel/runtime': 7.28.4 + '@mui/private-theming': 7.3.3(@types/react@19.2.0)(react@19.2.0) + '@mui/styled-engine': 7.3.3(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(react@19.2.0) + '@mui/types': 7.4.7(@types/react@19.2.0) + '@mui/utils': 7.3.3(@types/react@19.2.0)(react@19.2.0) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 - react: 19.1.0 + react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.1.8)(react@19.1.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) - '@types/react': 19.1.8 + '@emotion/react': 11.14.0(@types/react@19.2.0)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0) + '@types/react': 19.2.0 - '@mui/types@7.4.4(@types/react@19.1.8)': + '@mui/types@7.4.7(@types/react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 optionalDependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.0 - '@mui/utils@7.2.0(@types/react@19.1.8)(react@19.1.0)': + '@mui/utils@7.3.3(@types/react@19.2.0)(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 - '@mui/types': 7.4.4(@types/react@19.1.8) + '@babel/runtime': 7.28.4 + '@mui/types': 7.4.7(@types/react@19.2.0) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 - react: 19.1.0 - react-is: 19.1.0 + react: 19.2.0 + react-is: 19.1.1 optionalDependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.0 - '@mui/x-data-grid@8.9.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/x-data-grid@8.13.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 - '@mui/material': 7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@mui/system': 7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) - '@mui/utils': 7.2.0(@types/react@19.1.8)(react@19.1.0) - '@mui/x-internals': 8.8.0(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + '@babel/runtime': 7.28.4 + '@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/system': 7.3.3(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0) + '@mui/utils': 7.3.3(@types/react@19.2.0)(react@19.2.0) + '@mui/x-internals': 8.13.1(@types/react@19.2.0)(react@19.2.0) + '@mui/x-virtualizer': 0.2.2(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) clsx: 2.1.1 prop-types: 15.8.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - use-sync-external-store: 1.5.0(react@19.1.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + use-sync-external-store: 1.5.0(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.1.8)(react@19.1.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + '@emotion/react': 11.14.0(@types/react@19.2.0)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.0)(react@19.2.0))(@types/react@19.2.0)(react@19.2.0) transitivePeerDependencies: - '@types/react' - '@mui/x-internals@8.8.0(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': + '@mui/x-internals@8.13.1(@types/react@19.2.0)(react@19.2.0)': dependencies: - '@babel/runtime': 7.27.6 - '@mui/system': 7.2.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) - '@mui/utils': 7.2.0(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 + '@babel/runtime': 7.28.4 + '@mui/utils': 7.3.3(@types/react@19.2.0)(react@19.2.0) + react: 19.2.0 reselect: 5.1.1 + use-sync-external-store: 1.5.0(react@19.2.0) transitivePeerDependencies: - '@types/react' + '@mui/x-virtualizer@0.2.2(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/utils': 7.3.3(@types/react@19.2.0)(react@19.2.0) + '@mui/x-internals': 8.13.1(@types/react@19.2.0)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + '@octokit/auth-token@4.0.0': {} '@octokit/core@5.2.1': @@ -4206,128 +5429,133 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true + '@pkgr/core@0.2.9': {} + '@popperjs/core@2.11.8': {} - '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.0-beta.38': {} - '@rollup/pluginutils@5.1.4(rollup@4.40.2)': + '@rollup/pluginutils@5.2.0(rollup@4.46.2)': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.40.2 + rollup: 4.46.2 - '@rollup/rollup-android-arm-eabi@4.40.2': + '@rollup/rollup-android-arm-eabi@4.46.2': optional: true - '@rollup/rollup-android-arm64@4.40.2': + '@rollup/rollup-android-arm64@4.46.2': optional: true - '@rollup/rollup-darwin-arm64@4.40.2': + '@rollup/rollup-darwin-arm64@4.46.2': optional: true - '@rollup/rollup-darwin-x64@4.40.2': + '@rollup/rollup-darwin-x64@4.46.2': optional: true - '@rollup/rollup-freebsd-arm64@4.40.2': + '@rollup/rollup-freebsd-arm64@4.46.2': optional: true - '@rollup/rollup-freebsd-x64@4.40.2': + '@rollup/rollup-freebsd-x64@4.46.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.40.2': + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.40.2': + '@rollup/rollup-linux-arm-musleabihf@4.46.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.40.2': + '@rollup/rollup-linux-arm64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.40.2': + '@rollup/rollup-linux-arm64-musl@4.46.2': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.40.2': + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': + '@rollup/rollup-linux-ppc64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.40.2': + '@rollup/rollup-linux-riscv64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.40.2': + '@rollup/rollup-linux-riscv64-musl@4.46.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.40.2': + '@rollup/rollup-linux-s390x-gnu@4.46.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.40.2': + '@rollup/rollup-linux-x64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-x64-musl@4.40.2': + '@rollup/rollup-linux-x64-musl@4.46.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.40.2': + '@rollup/rollup-win32-arm64-msvc@4.46.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.40.2': + '@rollup/rollup-win32-ia32-msvc@4.46.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.40.2': + '@rollup/rollup-win32-x64-msvc@4.46.2': optional: true - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.0)': + '@rtsao/scc@1.1.0': + optional: true + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.0)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.0)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.0)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.0)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.0)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.0)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.0)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.4 - '@svgr/babel-preset@8.1.0(@babel/core@7.28.0)': + '@svgr/babel-preset@8.1.0(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.28.0 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.0) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.0) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.0) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.0) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.0) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.0) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.0) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.4) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.4) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.4) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.4) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.4) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.4) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.4) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.4) - '@svgr/core@8.1.0(typescript@5.8.3)': + '@svgr/core@8.1.0(typescript@5.9.3)': dependencies: - '@babel/core': 7.28.0 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4) camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@5.8.3) + cosmiconfig: 8.3.6(typescript@5.9.3) snake-case: 3.0.4 transitivePeerDependencies: - supports-color @@ -4335,124 +5563,121 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.8.3))': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': dependencies: - '@babel/core': 7.28.0 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.0) - '@svgr/core': 8.1.0(typescript@5.8.3) + '@babel/core': 7.28.4 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4) + '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@tauri-apps/api@2.7.0': {} + '@tauri-apps/api@2.8.0': {} - '@tauri-apps/cli-darwin-arm64@2.7.1': + '@tauri-apps/cli-darwin-arm64@2.8.4': optional: true - '@tauri-apps/cli-darwin-x64@2.7.1': + '@tauri-apps/cli-darwin-x64@2.8.4': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.7.1': + '@tauri-apps/cli-linux-arm-gnueabihf@2.8.4': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.7.1': + '@tauri-apps/cli-linux-arm64-gnu@2.8.4': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.7.1': + '@tauri-apps/cli-linux-arm64-musl@2.8.4': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.7.1': + '@tauri-apps/cli-linux-riscv64-gnu@2.8.4': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.7.1': + '@tauri-apps/cli-linux-x64-gnu@2.8.4': optional: true - '@tauri-apps/cli-linux-x64-musl@2.7.1': + '@tauri-apps/cli-linux-x64-musl@2.8.4': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.7.1': + '@tauri-apps/cli-win32-arm64-msvc@2.8.4': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.7.1': + '@tauri-apps/cli-win32-ia32-msvc@2.8.4': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.7.1': + '@tauri-apps/cli-win32-x64-msvc@2.8.4': optional: true - '@tauri-apps/cli@2.7.1': + '@tauri-apps/cli@2.8.4': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.7.1 - '@tauri-apps/cli-darwin-x64': 2.7.1 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.7.1 - '@tauri-apps/cli-linux-arm64-gnu': 2.7.1 - '@tauri-apps/cli-linux-arm64-musl': 2.7.1 - '@tauri-apps/cli-linux-riscv64-gnu': 2.7.1 - '@tauri-apps/cli-linux-x64-gnu': 2.7.1 - '@tauri-apps/cli-linux-x64-musl': 2.7.1 - '@tauri-apps/cli-win32-arm64-msvc': 2.7.1 - '@tauri-apps/cli-win32-ia32-msvc': 2.7.1 - '@tauri-apps/cli-win32-x64-msvc': 2.7.1 + '@tauri-apps/cli-darwin-arm64': 2.8.4 + '@tauri-apps/cli-darwin-x64': 2.8.4 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.8.4 + '@tauri-apps/cli-linux-arm64-gnu': 2.8.4 + '@tauri-apps/cli-linux-arm64-musl': 2.8.4 + '@tauri-apps/cli-linux-riscv64-gnu': 2.8.4 + '@tauri-apps/cli-linux-x64-gnu': 2.8.4 + '@tauri-apps/cli-linux-x64-musl': 2.8.4 + '@tauri-apps/cli-win32-arm64-msvc': 2.8.4 + '@tauri-apps/cli-win32-ia32-msvc': 2.8.4 + '@tauri-apps/cli-win32-x64-msvc': 2.8.4 '@tauri-apps/plugin-clipboard-manager@2.3.0': dependencies: - '@tauri-apps/api': 2.7.0 + '@tauri-apps/api': 2.8.0 - '@tauri-apps/plugin-dialog@2.3.1': + '@tauri-apps/plugin-dialog@2.4.0': dependencies: - '@tauri-apps/api': 2.7.0 + '@tauri-apps/api': 2.8.0 - '@tauri-apps/plugin-fs@2.4.1': + '@tauri-apps/plugin-fs@2.4.2': dependencies: - '@tauri-apps/api': 2.7.0 + '@tauri-apps/api': 2.8.0 - '@tauri-apps/plugin-global-shortcut@2.3.0': + '@tauri-apps/plugin-http@2.5.2': dependencies: - '@tauri-apps/api': 2.7.0 - - '@tauri-apps/plugin-notification@2.3.0': - dependencies: - '@tauri-apps/api': 2.7.0 + '@tauri-apps/api': 2.8.0 '@tauri-apps/plugin-process@2.3.0': dependencies: - '@tauri-apps/api': 2.7.0 + '@tauri-apps/api': 2.8.0 - '@tauri-apps/plugin-shell@2.3.0': + '@tauri-apps/plugin-shell@2.3.1': dependencies: - '@tauri-apps/api': 2.7.0 + '@tauri-apps/api': 2.8.0 '@tauri-apps/plugin-updater@2.9.0': dependencies: - '@tauri-apps/api': 2.7.0 + '@tauri-apps/api': 2.8.0 - '@tauri-apps/plugin-window-state@2.4.0': + '@tybys/wasm-util@0.10.1': dependencies: - '@tauri-apps/api': 2.7.0 + tslib: 2.8.1 + optional: true '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.7 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@types/babel__traverse@7.20.7': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 '@types/debug@4.1.12': dependencies: @@ -4460,23 +5685,23 @@ snapshots: '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 - '@types/estree@1.0.7': {} + '@types/estree@1.0.8': {} '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 - '@types/hoist-non-react-statics@3.3.7(@types/react@19.1.8)': - dependencies: - '@types/react': 19.1.8 - hoist-non-react-statics: 3.3.2 + '@types/js-cookie@3.0.6': {} '@types/js-yaml@4.0.9': {} '@types/json-schema@7.0.15': {} + '@types/json5@0.0.29': + optional: true + '@types/lodash-es@4.17.12': dependencies: '@types/lodash': 4.17.16 @@ -4493,86 +5718,241 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-beautiful-dnd@13.1.8': + '@types/react-dom@19.2.0(@types/react@19.2.0)': dependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.0 - '@types/react-dom@19.1.6(@types/react@19.1.8)': + '@types/react-transition-group@4.4.12(@types/react@19.2.0)': dependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.0 - '@types/react-redux@7.1.34': - dependencies: - '@types/hoist-non-react-statics': 3.3.7(@types/react@19.1.8) - '@types/react': 19.1.8 - hoist-non-react-statics: 3.3.2 - redux: 4.2.1 - - '@types/react-transition-group@4.4.12(@types/react@19.1.8)': - dependencies: - '@types/react': 19.1.8 - - '@types/react@19.1.8': + '@types/react@19.2.0': dependencies: csstype: 3.1.3 + '@types/trusted-types@1.0.6': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} + '@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.45.0 + eslint: 9.37.0(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.45.0 + debug: 4.4.1 + eslint: 9.37.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.45.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + debug: 4.4.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.45.0': + dependencies: + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 + + '@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.1 + eslint: 9.37.0(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.45.0': {} + + '@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.45.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.8.0(eslint@9.37.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.45.0': + dependencies: + '@typescript-eslint/types': 8.45.0 + eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-legacy@7.1.0(terser@5.43.1)(vite@7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1))': + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.0) - '@babel/preset-env': 7.28.0(@babel/core@7.28.0) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.0) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0) + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1))': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.4) + '@babel/preset-env': 7.28.0(@babel/core@7.28.4) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.4) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.4) browserslist: 4.25.1 browserslist-to-esbuild: 2.1.1(browserslist@4.25.1) - core-js: 3.44.0 + core-js: 3.45.0 magic-string: 0.30.17 regenerator-runtime: 0.14.1 systemjs: 6.15.1 - terser: 5.43.1 - vite: 7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) + terser: 5.44.0 + vite: 7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1))': + '@vitejs/plugin-react@5.0.4(vite@7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1))': dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) - '@rolldown/pluginutils': 1.0.0-beta.27 + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.38 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) + vite: 7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1) transitivePeerDependencies: - supports-color - acorn@8.14.1: {} + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} adm-zip@0.5.16: {} agent-base@7.1.3: {} - ahooks@3.9.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + ahooks@3.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@babel/runtime': 7.27.6 - dayjs: 1.11.13 + '@babel/runtime': 7.28.4 + '@types/js-cookie': 3.0.6 + dayjs: 1.11.18 intersection-observer: 0.12.2 js-cookie: 3.0.5 lodash: 4.17.21 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) react-fast-compare: 3.2.2 resize-observer-polyfill: 1.5.1 screenfull: 5.2.0 tslib: 2.8.1 + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -4585,9 +5965,73 @@ snapshots: argparse@2.0.1: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + optional: true + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + optional: true + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + optional: true + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + optional: true + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + optional: true + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + optional: true + + async-function@1.0.0: + optional: true + asynckit@0.4.0: {} - axios@1.11.0: + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + optional: true + + axios@1.12.2: dependencies: follow-redirects: 1.15.9 form-data: 4.0.4 @@ -4597,42 +6041,54 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 cosmiconfig: 7.1.0 resolve: 1.22.10 - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.0): + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4): dependencies: '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.0): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.4): dependencies: - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) core-js-compat: 3.44.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.0): + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.4): dependencies: - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + '@babel/core': 7.28.4 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) transitivePeerDependencies: - supports-color bail@2.0.2: {} + balanced-match@1.0.2: {} + before-after-hook@2.2.3: {} + birecord@0.1.1: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 - optional: true browserslist-to-esbuild@2.1.1(browserslist@4.25.1): dependencies: @@ -4653,6 +6109,20 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + optional: true + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + optional: true + callsites@3.1.0: {} camelcase@6.3.0: {} @@ -4661,6 +6131,11 @@ snapshots: ccount@2.0.1: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -4669,10 +6144,6 @@ snapshots: character-reference-invalid@2.0.1: {} - chart.js@4.5.0: - dependencies: - '@kurkle/color': 0.3.4 - chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -4703,10 +6174,16 @@ snapshots: comma-separated-tokens@2.0.3: {} - commander@14.0.0: {} + commander@14.0.1: {} commander@2.20.3: {} + comment-parser@1.4.1: {} + + compare-versions@6.1.1: {} + + concat-map@0.0.1: {} + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} @@ -4717,7 +6194,7 @@ snapshots: dependencies: browserslist: 4.25.1 - core-js@3.44.0: {} + core-js@3.45.0: {} cosmiconfig@7.1.0: dependencies: @@ -4727,16 +6204,16 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@8.3.6(typescript@5.8.3): + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 - cross-env@10.0.0: + cross-env@10.1.0: dependencies: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 @@ -4747,10 +6224,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-box-model@1.2.1: - dependencies: - tiny-invariant: 1.3.3 - csstype@3.1.3: {} d@1.0.2: @@ -4760,7 +6233,33 @@ snapshots: data-uri-to-buffer@4.0.1: {} - dayjs@1.11.13: {} + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + optional: true + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + optional: true + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + optional: true + + dayjs@1.11.18: {} + + debug@3.2.7: + dependencies: + ms: 2.1.3 + optional: true debug@4.4.1: dependencies: @@ -4770,6 +6269,22 @@ snapshots: dependencies: character-entities: 2.0.2 + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + delayed-stream@1.0.0: {} deprecation@2.3.1: {} @@ -4783,9 +6298,14 @@ snapshots: dependencies: dequal: 2.0.3 + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + optional: true + dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 csstype: 3.1.3 dot-case@3.0.4: @@ -4813,6 +6333,64 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + optional: true + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -4828,6 +6406,18 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + optional: true + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + optional: true + es5-ext@0.10.64: dependencies: es6-iterator: 2.0.3 @@ -4885,6 +6475,298 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-config-prettier@10.1.8(eslint@9.37.0(jiti@2.6.1)): + dependencies: + eslint: 9.37.0(jiti@2.6.1) + + eslint-import-context@0.1.9(unrs-resolver@1.11.1): + dependencies: + get-tsconfig: 4.10.1 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.11.1 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + optional: true + + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)): + dependencies: + debug: 4.4.1 + eslint: 9.37.0(jiti@2.6.1) + eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + get-tsconfig: 4.10.1 + is-bun-module: 2.0.0 + stable-hash-x: 0.2.0 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + optional: true + + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)): + dependencies: + '@typescript-eslint/types': 8.45.0 + comment-parser: 1.4.1 + debug: 4.4.1 + eslint: 9.37.0(jiti@2.6.1) + eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + is-glob: 4.0.3 + minimatch: 10.0.3 + semver: 7.7.2 + stable-hash-x: 0.2.0 + unrs-resolver: 1.11.1 + optionalDependencies: + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.37.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + optional: true + + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1))(prettier@3.6.2): + dependencies: + eslint: 9.37.0(jiti@2.6.1) + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.37.0(jiti@2.6.1)) + + eslint-plugin-react-debug@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.0.6 + '@eslint-react/kit': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.8.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-dom@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.0.6 + '@eslint-react/kit': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + compare-versions: 6.1.1 + eslint: 9.37.0(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.8.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-hooks-extra@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.0.6 + '@eslint-react/kit': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.8.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-hooks@6.1.1(eslint@9.37.0(jiti@2.6.1)): + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + eslint: 9.37.0(jiti@2.6.1) + zod: 4.1.11 + zod-validation-error: 4.0.2(zod@4.1.11) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-naming-convention@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.0.6 + '@eslint-react/kit': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.8.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.4.23(eslint@9.37.0(jiti@2.6.1)): + dependencies: + eslint: 9.37.0(jiti@2.6.1) + + eslint-plugin-react-web-api@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.0.6 + '@eslint-react/kit': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.8.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-x@2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.0.6 + '@eslint-react/kit': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.0.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + compare-versions: 6.1.1 + eslint: 9.37.0(jiti@2.6.1) + is-immutable-type: 5.0.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + string-ts: 2.2.1 + ts-api-utils: 2.1.0(typescript@5.9.3) + ts-pattern: 5.8.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-unused-imports@4.2.0(@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)): + dependencies: + eslint: 9.37.0(jiti@2.6.1) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.37.0(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.8.0(eslint@9.37.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.4.0 + '@eslint/core': 0.16.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.37.0 + '@eslint/plugin-kit': 0.4.0 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + esniff@2.0.1: dependencies: d: 1.0.2 @@ -4892,6 +6774,22 @@ snapshots: event-emitter: 0.3.5 type: 2.7.3 + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} estree-walker@2.0.2: {} @@ -4909,7 +6807,27 @@ snapshots: extend@3.0.2: {} - fdir@6.4.6(picomatch@4.0.3): + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -4918,15 +6836,35 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - optional: true find-root@1.1.0: {} + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + follow-redirects@1.15.9: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + optional: true + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -4944,18 +6882,31 @@ snapshots: dependencies: fetch-blob: 3.2.0 - foxact@0.2.49(react@19.1.0): + foxact@0.2.49(react@19.2.0): dependencies: client-only: 0.0.1 server-only: 0.0.1 optionalDependencies: - react: 19.1.0 + react: 19.2.0 fsevents@2.3.3: optional: true function-bind@1.1.2: {} + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + optional: true + + functions-have-names@1.2.3: + optional: true + gensync@1.0.0-beta.2: {} get-intrinsic@1.3.0: @@ -4976,6 +6927,25 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + optional: true + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@11.0.3: dependencies: foreground-child: 3.3.1 @@ -4985,8 +6955,35 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.0 + globals@14.0.0: {} + + globals@16.4.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + gopd@1.2.0: {} + graphemer@1.4.0: {} + + has-bigints@1.1.0: + optional: true + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + optional: true + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -4999,7 +6996,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.6: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/hast': 3.0.4 '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 @@ -5038,11 +7035,15 @@ snapshots: transitivePeerDependencies: - supports-color - i18next@25.3.2(typescript@5.8.3): + i18next@25.5.3(typescript@5.9.3): dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 + + ignore@5.3.2: {} + + ignore@7.0.5: {} immutable@5.1.2: {} @@ -5051,8 +7052,17 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + imurmurhash@0.1.4: {} + inline-style-parser@0.2.4: {} + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + optional: true + intersection-observer@0.12.2: {} is-alphabetical@2.0.1: {} @@ -5062,39 +7072,171 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + optional: true + is-arrayish@0.2.1: {} + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + optional: true + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + optional: true + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + optional: true + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.2 + + is-callable@1.2.7: + optional: true + is-core-module@2.16.1: dependencies: hasown: 2.0.2 + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + optional: true + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + optional: true + is-decimal@2.0.1: {} - is-extglob@2.1.1: + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 optional: true is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + optional: true + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - optional: true is-hexadecimal@2.0.1: {} - is-number@7.0.0: + is-immutable-type@5.0.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/type-utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.9.3) + ts-declaration-location: 1.0.7(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + is-map@2.0.3: optional: true + is-negative-zero@2.0.3: + optional: true + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + optional: true + + is-number@7.0.0: {} + is-plain-obj@4.1.0: {} is-promise@2.2.2: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + optional: true + + is-set@2.0.3: + optional: true + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + optional: true + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + optional: true + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + optional: true + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + optional: true + + is-weakmap@2.0.2: + optional: true + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + optional: true + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + optional: true + + isarray@2.0.5: + optional: true + isexe@2.0.0: {} jackspeak@4.1.1: dependencies: '@isaacs/cliui': 8.0.2 + jiti@2.6.1: {} + js-cookie@3.0.5: {} js-tokens@4.0.0: {} @@ -5107,20 +7249,46 @@ snapshots: jsesc@3.1.0: {} + json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + optional: true + json5@2.2.3: {} jsonc-parser@3.3.1: {} + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lines-and-columns@1.2.4: {} + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash-es@4.17.21: {} lodash.debounce@4.0.8: {} + lodash.merge@4.6.2: {} + lodash@4.17.21: {} longest-streak@3.1.0: {} @@ -5238,8 +7406,6 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - memoize-one@5.2.1: {} - memoizee@0.4.17: dependencies: d: 1.0.2 @@ -5253,7 +7419,9 @@ snapshots: meow@13.2.0: {} - meta-json-schema@1.19.11: {} + merge2@1.4.1: {} + + meta-json-schema@1.19.14: {} micromark-core-commonmark@2.0.3: dependencies: @@ -5392,7 +7560,6 @@ snapshots: dependencies: braces: 3.0.3 picomatch: 2.3.1 - optional: true mime-db@1.52.0: {} @@ -5404,15 +7571,26 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: + optional: true + minipass@7.1.2: {} - minizlib@3.0.2: + minizlib@3.1.0: dependencies: minipass: 7.1.2 - mkdirp@3.0.1: {} - - monaco-editor@0.52.2: {} + monaco-editor@0.53.0: + dependencies: + '@types/trusted-types': 1.0.6 monaco-languageserver-types@0.4.0: dependencies: @@ -5426,18 +7604,18 @@ snapshots: monaco-types@0.1.0: {} - monaco-worker-manager@2.0.1(monaco-editor@0.52.2): + monaco-worker-manager@2.0.1(monaco-editor@0.53.0): dependencies: - monaco-editor: 0.52.2 + monaco-editor: 0.53.0 - monaco-yaml@5.4.0(monaco-editor@0.52.2): + monaco-yaml@5.4.0(monaco-editor@0.53.0): dependencies: jsonc-parser: 3.3.1 - monaco-editor: 0.52.2 + monaco-editor: 0.53.0 monaco-languageserver-types: 0.4.0 monaco-marker-data-provider: 1.2.4 monaco-types: 0.1.0 - monaco-worker-manager: 2.0.1(monaco-editor@0.52.2) + monaco-worker-manager: 2.0.1(monaco-editor@0.53.0) path-browserify: 1.0.1 prettier: 3.6.2 vscode-languageserver-textdocument: 1.0.12 @@ -5449,7 +7627,11 @@ snapshots: nanoid@3.3.11: {} - nanoid@5.1.5: {} + nanoid@5.1.6: {} + + napi-postinstall@0.3.3: {} + + natural-compare@1.4.0: {} next-tick@1.1.0: {} @@ -5473,10 +7655,73 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: + optional: true + + object-keys@1.1.1: + optional: true + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + optional: true + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + optional: true + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + optional: true + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + optional: true + once@1.4.0: dependencies: wrappy: 1.0.2 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + optional: true + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + package-json-from-dist@1.0.1: {} parent-module@1.0.1: @@ -5502,6 +7747,8 @@ snapshots: path-browserify@1.0.1: {} + path-exists@4.0.0: {} + path-key@3.1.1: {} path-parse@1.0.7: {} @@ -5515,21 +7762,24 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: - optional: true + picomatch@2.3.1: {} picomatch@4.0.3: {} + possible-typed-array-names@1.1.0: + optional: true + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.8.3): + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: dependencies: - prettier: 3.6.2 - typescript: 5.8.3 + fast-diff: 1.3.0 prettier@3.6.2: {} @@ -5543,69 +7793,50 @@ snapshots: proxy-from-env@1.1.0: {} - raf-schd@4.0.3: {} + punycode@2.3.1: {} - react-beautiful-dnd@13.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@babel/runtime': 7.27.6 - css-box-model: 1.2.1 - memoize-one: 5.2.1 - raf-schd: 4.0.3 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-redux: 7.2.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - redux: 4.2.1 - use-memo-one: 1.1.3(react@19.1.0) - transitivePeerDependencies: - - react-native + queue-microtask@1.2.3: {} - react-chartjs-2@5.3.0(chart.js@4.5.0)(react@19.1.0): + react-dom@19.2.0(react@19.2.0): dependencies: - chart.js: 4.5.0 - react: 19.1.0 + react: 19.2.0 + scheduler: 0.27.0 - react-dom@19.1.0(react@19.1.0): + react-error-boundary@6.0.0(react@19.2.0): dependencies: - react: 19.1.0 - scheduler: 0.26.0 - - react-error-boundary@6.0.0(react@19.1.0): - dependencies: - '@babel/runtime': 7.27.6 - react: 19.1.0 + '@babel/runtime': 7.28.4 + react: 19.2.0 react-fast-compare@3.2.2: {} - react-hook-form@7.61.1(react@19.1.0): + react-hook-form@7.64.0(react@19.2.0): dependencies: - react: 19.1.0 + react: 19.2.0 - react-i18next@15.6.1(i18next@25.3.2(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3): + react-i18next@16.0.0(i18next@25.5.3(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 - i18next: 25.3.2(typescript@5.8.3) - react: 19.1.0 + i18next: 25.5.3(typescript@5.9.3) + react: 19.2.0 optionalDependencies: - react-dom: 19.1.0(react@19.1.0) - typescript: 5.8.3 + react-dom: 19.2.0(react@19.2.0) + typescript: 5.9.3 react-is@16.13.1: {} - react-is@17.0.2: {} + react-is@19.1.1: {} - react-is@19.1.0: {} - - react-markdown@10.1.0(@types/react@19.1.8)(react@19.1.0): + react-markdown@10.1.0(@types/react@19.2.0)(react@19.2.0): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.1.8 + '@types/react': 19.2.0 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.0 - react: 19.1.0 + react: 19.2.0 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -5614,61 +7845,57 @@ snapshots: transitivePeerDependencies: - supports-color - react-monaco-editor@0.59.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + react-monaco-editor@0.59.0(monaco-editor@0.53.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - monaco-editor: 0.52.2 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - react-redux@7.2.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@babel/runtime': 7.27.6 - '@types/react-redux': 7.1.34 - hoist-non-react-statics: 3.3.2 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 19.1.0 - react-is: 17.0.2 - optionalDependencies: - react-dom: 19.1.0(react@19.1.0) + monaco-editor: 0.53.0 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) react-refresh@0.17.0: {} - react-router-dom@7.7.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + react-router-dom@7.9.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-router: 7.7.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-router: 7.9.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react-router@7.7.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + react-router@7.9.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: cookie: 1.0.2 - react: 19.1.0 + react: 19.2.0 set-cookie-parser: 2.7.1 optionalDependencies: - react-dom: 19.1.0(react@19.1.0) + react-dom: 19.2.0(react@19.2.0) - react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + react-transition-group@4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - react-virtuoso@4.13.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + react-virtuoso@4.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - react@19.1.0: {} + react@19.2.0: {} readdirp@4.1.2: {} - redux@4.2.1: + reflect.getprototypeof@1.0.10: dependencies: - '@babel/runtime': 7.27.6 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + optional: true regenerate-unicode-properties@10.2.0: dependencies: @@ -5678,6 +7905,16 @@ snapshots: regenerator-runtime@0.14.1: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + optional: true + regexpu-core@6.2.0: dependencies: regenerate: 1.4.2 @@ -5716,39 +7953,69 @@ snapshots: resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.10: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rollup@4.40.2: + reusify@1.1.0: {} + + rollup@4.46.2: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.40.2 - '@rollup/rollup-android-arm64': 4.40.2 - '@rollup/rollup-darwin-arm64': 4.40.2 - '@rollup/rollup-darwin-x64': 4.40.2 - '@rollup/rollup-freebsd-arm64': 4.40.2 - '@rollup/rollup-freebsd-x64': 4.40.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.40.2 - '@rollup/rollup-linux-arm-musleabihf': 4.40.2 - '@rollup/rollup-linux-arm64-gnu': 4.40.2 - '@rollup/rollup-linux-arm64-musl': 4.40.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.40.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.40.2 - '@rollup/rollup-linux-riscv64-gnu': 4.40.2 - '@rollup/rollup-linux-riscv64-musl': 4.40.2 - '@rollup/rollup-linux-s390x-gnu': 4.40.2 - '@rollup/rollup-linux-x64-gnu': 4.40.2 - '@rollup/rollup-linux-x64-musl': 4.40.2 - '@rollup/rollup-win32-arm64-msvc': 4.40.2 - '@rollup/rollup-win32-ia32-msvc': 4.40.2 - '@rollup/rollup-win32-x64-msvc': 4.40.2 + '@rollup/rollup-android-arm-eabi': 4.46.2 + '@rollup/rollup-android-arm64': 4.46.2 + '@rollup/rollup-darwin-arm64': 4.46.2 + '@rollup/rollup-darwin-x64': 4.46.2 + '@rollup/rollup-freebsd-arm64': 4.46.2 + '@rollup/rollup-freebsd-x64': 4.46.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 + '@rollup/rollup-linux-arm-musleabihf': 4.46.2 + '@rollup/rollup-linux-arm64-gnu': 4.46.2 + '@rollup/rollup-linux-arm64-musl': 4.46.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 + '@rollup/rollup-linux-ppc64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-musl': 4.46.2 + '@rollup/rollup-linux-s390x-gnu': 4.46.2 + '@rollup/rollup-linux-x64-gnu': 4.46.2 + '@rollup/rollup-linux-x64-musl': 4.46.2 + '@rollup/rollup-win32-arm64-msvc': 4.46.2 + '@rollup/rollup-win32-ia32-msvc': 4.46.2 + '@rollup/rollup-win32-x64-msvc': 4.46.2 fsevents: 2.3.3 - sass@1.89.2: + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + optional: true + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + optional: true + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + optional: true + + sass@1.93.2: dependencies: chokidar: 4.0.3 immutable: 5.1.2 @@ -5756,22 +8023,81 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.1 - scheduler@0.26.0: {} + scheduler@0.27.0: {} screenfull@5.2.0: {} semver@6.3.1: {} + semver@7.7.2: {} + server-only@0.0.1: {} set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + optional: true + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + optional: true + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + optional: true + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + optional: true + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + optional: true + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + optional: true + signal-exit@4.1.0: {} snake-case@3.0.4: @@ -5779,8 +8105,6 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - sockette@2.0.6: {} - source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -5794,6 +8118,16 @@ snapshots: space-separated-tokens@2.0.2: {} + stable-hash-x@0.2.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + optional: true + + string-ts@2.2.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5806,6 +8140,32 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + optional: true + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + optional: true + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + optional: true + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -5819,6 +8179,11 @@ snapshots: dependencies: ansi-regex: 6.1.0 + strip-bom@3.0.0: + optional: true + + strip-json-comments@3.1.1: {} + style-to-js@1.1.16: dependencies: style-to-object: 1.0.8 @@ -5829,31 +8194,38 @@ snapshots: stylis@4.2.0: {} + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} svg-parser@2.0.4: {} - swr@2.3.4(react@19.1.0): + swr@2.3.6(react@19.2.0): dependencies: dequal: 2.0.3 - react: 19.1.0 - use-sync-external-store: 1.5.0(react@19.1.0) + react: 19.2.0 + use-sync-external-store: 1.5.0(react@19.2.0) + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 systemjs@6.15.1: {} - tar@7.4.3: + tar@7.5.1: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 + minizlib: 3.1.0 yallist: 5.0.0 - terser@5.43.1: + terser@5.44.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -5862,31 +8234,107 @@ snapshots: es5-ext: 0.10.64 next-tick: 1.1.0 - tiny-invariant@1.3.3: {} - - tinyglobby@0.2.14: + tinyglobby@0.2.15: dependencies: - fdir: 6.4.6(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - optional: true trim-lines@3.0.1: {} trough@2.2.0: {} + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-declaration-location@1.0.7(typescript@5.9.3): + dependencies: + picomatch: 4.0.3 + typescript: 5.9.3 + + ts-pattern@5.8.0: {} + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + optional: true + tslib@2.8.1: {} tunnel@0.0.6: {} + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type@2.7.3: {} + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + optional: true + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + optional: true + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + optional: true + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + optional: true + types-pac@1.0.3: {} - typescript@5.8.3: {} + typescript-eslint@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + optional: true undici@5.29.0: dependencies: @@ -5938,19 +8386,43 @@ snapshots: universal-user-agent@6.0.1: {} + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.3 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + update-browserslist-db@1.1.3(browserslist@4.25.1): dependencies: browserslist: 4.25.1 escalade: 3.2.0 picocolors: 1.1.1 - use-memo-one@1.1.3(react@19.1.0): + uri-js@4.4.1: dependencies: - react: 19.1.0 + punycode: 2.3.1 - use-sync-external-store@1.5.0(react@19.1.0): + use-sync-external-store@1.5.0(react@19.2.0): dependencies: - react: 19.1.0 + react: 19.2.0 vfile-message@4.0.2: dependencies: @@ -5962,33 +8434,34 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-plugin-monaco-editor@1.1.0(monaco-editor@0.52.2): + vite-plugin-monaco-editor@1.1.0(monaco-editor@0.53.0): dependencies: - monaco-editor: 0.52.2 + monaco-editor: 0.53.0 - vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)): + vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1)): dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.40.2) - '@svgr/core': 8.1.0(typescript@5.8.3) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) - vite: 7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) + '@rollup/pluginutils': 5.2.0(rollup@4.46.2) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + vite: 7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite@7.0.6(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1): + vite@7.1.9(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.7.1): dependencies: esbuild: 0.25.4 - fdir: 6.4.6(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.40.2 - tinyglobby: 0.2.14 + rollup: 4.46.2 + tinyglobby: 0.2.15 optionalDependencies: fsevents: 2.3.3 - sass: 1.89.2 - terser: 5.43.1 + jiti: 2.6.1 + sass: 1.93.2 + terser: 5.44.0 yaml: 2.7.1 void-elements@3.1.0: {} @@ -6008,10 +8481,57 @@ snapshots: web-streams-polyfill@3.3.3: {} + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + optional: true + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + optional: true + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + optional: true + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + optional: true + which@2.0.2: dependencies: isexe: 2.0.0 + word-wrap@1.2.5: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -6034,10 +8554,18 @@ snapshots: yaml@2.7.1: {} - zustand@5.0.6(@types/react@19.1.8)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.1.11): + dependencies: + zod: 4.1.11 + + zod@4.1.11: {} + + zustand@5.0.8(@types/react@19.2.0)(react@19.2.0)(use-sync-external-store@1.5.0(react@19.2.0)): optionalDependencies: - '@types/react': 19.1.8 - react: 19.1.0 - use-sync-external-store: 1.5.0(react@19.1.0) + '@types/react': 19.2.0 + react: 19.2.0 + use-sync-external-store: 1.5.0(react@19.2.0) zwitch@2.0.4: {} diff --git a/clash-verge-rev/renovate.json b/clash-verge-rev/renovate.json index 49f89fc50e..7df27b159d 100644 --- a/clash-verge-rev/renovate.json +++ b/clash-verge-rev/renovate.json @@ -35,8 +35,13 @@ "description": "Group all npm dependencies into a single PR", "matchManagers": ["npm"], "groupName": "npm dependencies" + }, + { + "description": "Group all GitHub Actions updates into a single PR", + "matchManagers": ["github-actions"], + "groupName": "github actions" } ], "postUpdateOptions": ["pnpmDedupe"], - "ignoreDeps": ["serde_yaml", "sysinfo"] + "ignoreDeps": ["criterion"] } diff --git a/clash-verge-rev/scripts-workflow/get_latest_tauri_commit.bash b/clash-verge-rev/scripts-workflow/get_latest_tauri_commit.bash new file mode 100755 index 0000000000..7fa950b1a8 --- /dev/null +++ b/clash-verge-rev/scripts-workflow/get_latest_tauri_commit.bash @@ -0,0 +1,56 @@ +#!/bin/bash + +# 获取最近一个和 Tauri 相关的改动的 commit hash +# This script finds the latest commit that modified Tauri-related files + +# Tauri 相关文件的模式 +TAURI_PATTERNS=( + "src-tauri/" + "Cargo.toml" + "Cargo.lock" + "tauri.*.conf.json" + "package.json" + "pnpm-lock.yaml" + "src/" +) + +# 排除的文件模式(build artifacts 等) +EXCLUDE_PATTERNS=( + "src-tauri/target/" + "src-tauri/gen/" + "*.log" + "*.tmp" + "node_modules/" + ".git/" +) + +# 构建 git log 的路径过滤参数 +PATHS="" +for pattern in "${TAURI_PATTERNS[@]}"; do + if [[ -e "$pattern" ]]; then + PATHS="$PATHS $pattern" + fi +done + +# 如果没有找到相关路径,返回错误 +if [[ -z "$PATHS" ]]; then + echo "Error: No Tauri-related paths found in current directory" >&2 + exit 1 +fi + +# 获取最新的 commit hash +# 使用 git log 查找最近修改了 Tauri 相关文件的提交 +LATEST_COMMIT=$(git log --format="%H" -n 1 -- $PATHS) + +# 验证是否找到了 commit +if [[ -z "$LATEST_COMMIT" ]]; then + echo "Error: No commits found for Tauri-related files" >&2 + exit 1 +fi + +# 输出结果 +echo "$LATEST_COMMIT" + +# 如果需要更多信息,可以取消注释以下行 +# echo "Latest Tauri-related commit: $LATEST_COMMIT" +# git show --stat --oneline "$LATEST_COMMIT" \ No newline at end of file diff --git a/clash-verge-rev/scripts/prebuild.mjs b/clash-verge-rev/scripts/prebuild.mjs index fd7a9cd03d..514b2df910 100644 --- a/clash-verge-rev/scripts/prebuild.mjs +++ b/clash-verge-rev/scripts/prebuild.mjs @@ -12,7 +12,7 @@ import { glob } from "glob"; const cwd = process.cwd(); const TEMP_DIR = path.join(cwd, "node_modules/.verge"); -const FORCE = process.argv.includes("--force"); +const FORCE = process.argv.includes("--force") || process.argv.includes("-f"); const PLATFORM_MAP = { "x86_64-pc-windows-msvc": "win32", @@ -43,7 +43,8 @@ const ARCH_MAP = { const arg1 = process.argv.slice(2)[0]; const arg2 = process.argv.slice(2)[1]; -const target = arg1 === "--force" ? arg2 : arg1; +let target; +target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1; const { platform, arch } = target ? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] } : process; @@ -61,12 +62,12 @@ const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/down let META_ALPHA_VERSION; const META_ALPHA_MAP = { - "win32-x64": "mihomo-windows-amd64-compatible", + "win32-x64": "mihomo-windows-amd64-v2", "win32-ia32": "mihomo-windows-386", "win32-arm64": "mihomo-windows-arm64", - "darwin-x64": "mihomo-darwin-amd64-compatible", + "darwin-x64": "mihomo-darwin-amd64-v1", "darwin-arm64": "mihomo-darwin-arm64", - "linux-x64": "mihomo-linux-amd64-compatible", + "linux-x64": "mihomo-linux-amd64-v2", "linux-ia32": "mihomo-linux-386", "linux-arm64": "mihomo-linux-arm64", "linux-arm": "mihomo-linux-armv7", @@ -108,12 +109,12 @@ const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`; let META_VERSION; const META_MAP = { - "win32-x64": "mihomo-windows-amd64-compatible", + "win32-x64": "mihomo-windows-amd64-v2", "win32-ia32": "mihomo-windows-386", "win32-arm64": "mihomo-windows-arm64", - "darwin-x64": "mihomo-darwin-amd64-compatible", + "darwin-x64": "mihomo-darwin-amd64-v2", "darwin-arm64": "mihomo-darwin-arm64", - "linux-x64": "mihomo-linux-amd64-compatible", + "linux-x64": "mihomo-linux-amd64-v2", "linux-ia32": "mihomo-linux-386", "linux-arm64": "mihomo-linux-arm64", "linux-arm": "mihomo-linux-armv7", diff --git a/clash-verge-rev/scripts/publish-version.mjs b/clash-verge-rev/scripts/publish-version.mjs index e5f4158fd8..b983932da5 100644 --- a/clash-verge-rev/scripts/publish-version.mjs +++ b/clash-verge-rev/scripts/publish-version.mjs @@ -54,7 +54,7 @@ async function run() { execSync(`git tag ${tag}`, { stdio: "inherit" }); execSync(`git push origin ${tag}`, { stdio: "inherit" }); console.log(`[INFO]: Git tag ${tag} created and pushed.`); - } catch (e) { + } catch { console.error(`[ERROR]: Failed to create or push git tag: ${tag}`); process.exit(1); } diff --git a/clash-verge-rev/scripts/release-version.mjs b/clash-verge-rev/scripts/release-version.mjs index 6e91068016..bc177a29c6 100644 --- a/clash-verge-rev/scripts/release-version.mjs +++ b/clash-verge-rev/scripts/release-version.mjs @@ -6,9 +6,10 @@ * * can be: * - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build) - * - A tag: "alpha", "beta", "rc", "autobuild", or "deploytest" + * - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest" * - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta) * - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530) + * - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3+autobuild.0614.a1b2c3d) * - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3+deploytest.2406101530) * * Examples: @@ -16,6 +17,7 @@ * pnpm release-version v1.2.3-beta * pnpm release-version beta * pnpm release-version autobuild + * pnpm release-version autobuild-latest * pnpm release-version deploytest * * The script will: @@ -27,10 +29,10 @@ * Errors are logged and the process exits with code 1 on failure. */ +import { execSync } from "child_process"; +import { program } from "commander"; import fs from "fs/promises"; import path from "path"; -import { program } from "commander"; -import { execSync } from "child_process"; /** * 获取当前 git 短 commit hash @@ -39,23 +41,61 @@ import { execSync } from "child_process"; function getGitShortCommit() { try { return execSync("git rev-parse --short HEAD").toString().trim(); - } catch (e) { + } catch { console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'"); return "nogit"; } } /** - * 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27) - * @param {boolean} withCommit 是否带 commit + * 获取最新 Tauri 相关提交的短 hash * @returns {string} */ -function generateShortTimestamp(withCommit = false) { +function getLatestTauriCommit() { + try { + const fullHash = execSync( + "bash ./scripts-workflow/get_latest_tauri_commit.bash", + ) + .toString() + .trim(); + const shortHash = execSync(`git rev-parse --short ${fullHash}`) + .toString() + .trim(); + console.log(`[INFO]: Latest Tauri-related commit: ${shortHash}`); + return shortHash; + } catch (error) { + console.warn( + "[WARN]: Failed to get latest Tauri commit, fallback to current git short commit", + ); + console.warn(`[WARN]: Error details: ${error.message}`); + return getGitShortCommit(); + } +} + +/** + * 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27) + * 使用 Asia/Shanghai 时区 + * @param {boolean} withCommit 是否带 commit + * @param {boolean} useTauriCommit 是否使用 Tauri 相关的 commit(仅当 withCommit 为 true 时有效) + * @returns {string} + */ +function generateShortTimestamp(withCommit = false, useTauriCommit = false) { const now = new Date(); - const month = String(now.getMonth() + 1).padStart(2, "0"); - const day = String(now.getDate()).padStart(2, "0"); + + const formatter = new Intl.DateTimeFormat("en-CA", { + timeZone: "Asia/Shanghai", + month: "2-digit", + day: "2-digit", + }); + + const parts = formatter.formatToParts(now); + const month = parts.find((part) => part.type === "month").value; + const day = parts.find((part) => part.type === "day").value; + if (withCommit) { - const gitShort = getGitShortCommit(); + const gitShort = useTauriCommit + ? getLatestTauriCommit() + : getGitShortCommit(); return `${month}${day}.${gitShort}`; } return `${month}${day}`; @@ -137,20 +177,19 @@ async function updateCargoVersion(newVersion) { const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; - const baseVersion = getBaseVersion(versionWithoutV); const updatedLines = lines.map((line) => { if (line.trim().startsWith("version =")) { return line.replace( /version\s*=\s*"[^"]+"/, - `version = "${baseVersion}"`, + `version = "${versionWithoutV}"`, ); } return line; }); await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8"); - console.log(`[INFO]: Cargo.toml version updated to: ${baseVersion}`); + console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`); } catch (error) { console.error("Error updating Cargo.toml version:", error); throw error; @@ -170,19 +209,23 @@ async function updateTauriConfigVersion(newVersion) { const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; - const baseVersion = getBaseVersion(versionWithoutV); console.log( "[INFO]: Current tauri.conf.json version is: ", tauriConfig.version, ); - tauriConfig.version = baseVersion; + + // 使用完整版本信息,包含build metadata + tauriConfig.version = versionWithoutV; + await fs.writeFile( tauriConfigPath, JSON.stringify(tauriConfig, null, 2), "utf8", ); - console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`); + console.log( + `[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`, + ); } catch (error) { console.error("Error updating tauri.conf.json version:", error); throw error; @@ -216,18 +259,31 @@ async function main(versionArg) { try { let newVersion; - const validTags = ["alpha", "beta", "rc", "autobuild", "deploytest"]; + const validTags = [ + "alpha", + "beta", + "rc", + "autobuild", + "autobuild-latest", + "deploytest", + ]; if (validTags.includes(versionArg.toLowerCase())) { const currentVersion = await getCurrentVersion(); const baseVersion = getBaseVersion(currentVersion); if (versionArg.toLowerCase() === "autobuild") { - // 格式: 2.3.0+autobuild.250613.cc39b27 - newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`; + // 格式: 2.3.0+autobuild.1004.cc39b27 + // 使用 Tauri 相关的最新 commit hash + newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`; + } else if (versionArg.toLowerCase() === "autobuild-latest") { + // 格式: 2.3.0+autobuild.1004.a1b2c3d (使用最新 Tauri 提交) + const latestTauriCommit = getLatestTauriCommit(); + newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`; } else if (versionArg.toLowerCase() === "deploytest") { - // 格式: 2.3.0+deploytest.250613.cc39b27 - newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true)}`; + // 格式: 2.3.0+deploytest.1004.cc39b27 + // 使用 Tauri 相关的最新 commit hash + newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`; } else { newVersion = `${baseVersion}-${versionArg.toLowerCase()}`; } diff --git a/clash-verge-rev/scripts/telegram.mjs b/clash-verge-rev/scripts/telegram.mjs new file mode 100644 index 0000000000..d7c741fc83 --- /dev/null +++ b/clash-verge-rev/scripts/telegram.mjs @@ -0,0 +1,110 @@ +import axios from "axios"; +import { readFileSync } from "fs"; +import { log_success, log_error, log_info } from "./utils.mjs"; + +const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道 +const CHAT_ID_TEST = "@vergetest"; // 测试频道 + +async function sendTelegramNotification() { + if (!process.env.TELEGRAM_BOT_TOKEN) { + throw new Error("TELEGRAM_BOT_TOKEN is required"); + } + + const version = + process.env.VERSION || + (() => { + const pkg = readFileSync("package.json", "utf-8"); + return JSON.parse(pkg).version; + })(); + + const downloadUrl = + process.env.DOWNLOAD_URL || + `https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}`; + + const isAutobuild = + process.env.BUILD_TYPE === "autobuild" || version.includes("autobuild"); + const chatId = isAutobuild ? CHAT_ID_TEST : CHAT_ID_RELEASE; + const buildType = isAutobuild ? "滚动更新版" : "正式版"; + + log_info(`Preparing Telegram notification for ${buildType} ${version}`); + log_info(`Target channel: ${chatId}`); + log_info(`Download URL: ${downloadUrl}`); + + // 读取发布说明和下载地址 + let releaseContent = ""; + try { + releaseContent = readFileSync("release.txt", "utf-8"); + log_info("成功读取 release.txt 文件"); + } catch (error) { + log_error("无法读取 release.txt,使用默认发布说明", error); + releaseContent = "更多新功能现已支持,详细更新日志请查看发布页面。"; + } + + // Markdown 转换为 HTML + function convertMarkdownToTelegramHTML(content) { + return content + .split("\n") + .map((line) => { + if (line.trim().length === 0) { + return ""; + } else if (line.startsWith("## ")) { + return `${line.replace("## ", "")}`; + } else if (line.startsWith("### ")) { + return `${line.replace("### ", "")}`; + } else if (line.startsWith("#### ")) { + return `${line.replace("#### ", "")}`; + } else { + let processedLine = line.replace( + /\[([^\]]+)\]\(([^)]+)\)/g, + (match, text, url) => { + const encodedUrl = encodeURI(url); + return `${text}`; + }, + ); + processedLine = processedLine.replace( + /\*\*([^*]+)\*\*/g, + "$1", + ); + return processedLine; + } + }) + .join("\n"); + } + + const formattedContent = convertMarkdownToTelegramHTML(releaseContent); + + const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布"; + const encodedVersion = encodeURIComponent(version); + const content = `🎉 Clash Verge Rev v${version} ${releaseTitle}\n\n${formattedContent}`; + + // 发送到 Telegram + try { + await axios.post( + `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, + { + chat_id: chatId, + text: content, + link_preview_options: { + is_disabled: false, + url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${encodedVersion}`, + prefer_large_media: true, + }, + parse_mode: "HTML", + }, + ); + log_success(`✅ Telegram 通知发送成功到 ${chatId}`); + } catch (error) { + log_error( + `❌ Telegram 通知发送失败到 ${chatId}:`, + error.response?.data || error.message, + error, + ); + process.exit(1); + } +} + +// 执行函数 +sendTelegramNotification().catch((error) => { + log_error("脚本执行失败:", error); + process.exit(1); +}); diff --git a/clash-verge-rev/scripts/updatelog.mjs b/clash-verge-rev/scripts/updatelog.mjs index 9c0979dd72..d3c90b581a 100644 --- a/clash-verge-rev/scripts/updatelog.mjs +++ b/clash-verge-rev/scripts/updatelog.mjs @@ -8,7 +8,7 @@ const UPDATE_LOG = "UPDATELOG.md"; export async function resolveUpdateLog(tag) { const cwd = process.cwd(); - const reTitle = /^## v[\d\.]+/; + const reTitle = /^## v[\d.]+/; const reEnd = /^---/; const file = path.join(cwd, UPDATE_LOG); @@ -54,7 +54,7 @@ export async function resolveUpdateLogDefault() { const data = await fsp.readFile(file, "utf-8"); - const reTitle = /^## v[\d\.]+/; + const reTitle = /^## v[\d.]+/; const reEnd = /^---/; let isCapturing = false; diff --git a/clash-verge-rev/src-tauri/Cargo.lock b/clash-verge-rev/src-tauri/Cargo.lock index 45ada569cb..ca49513df4 100644 --- a/clash-verge-rev/src-tauri/Cargo.lock +++ b/clash-verge-rev/src-tauri/Cargo.lock @@ -74,15 +74,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -104,12 +95,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -133,57 +118,40 @@ checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "arboard" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.1", - "parking_lot", + "parking_lot 0.12.4", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "wl-clipboard-rs", "x11rb", ] -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "arrayvec" version = "0.7.6" @@ -248,14 +216,14 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", "fastrand 2.3.0", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "pin-project-lite", "slab", ] @@ -294,20 +262,20 @@ dependencies = [ [[package]] name = "async-io" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock 3.4.0", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "parking", - "polling 3.9.0", - "rustix 1.0.8", + "polling 3.11.0", + "rustix 1.1.2", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -321,9 +289,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ "event-listener 5.3.0", "event-listener-strategy", @@ -360,20 +328,20 @@ dependencies = [ [[package]] name = "async-process" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel 2.5.0", - "async-io 2.5.0", - "async-lock 3.4.0", + "async-io 2.6.0", + "async-lock 3.4.1", "async-signal", "async-task", "blocking", "cfg-if", "event-listener 5.3.0", - "futures-lite 2.6.0", - "rustix 1.0.8", + "futures-lite 2.6.1", + "rustix 1.1.2", ] [[package]] @@ -384,25 +352,25 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "async-signal" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ - "async-io 2.5.0", - "async-lock 3.4.0", + "async-io 2.6.0", + "async-lock 3.4.1", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 1.0.8", + "rustix 1.1.2", "signal-hook-registry", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -424,7 +392,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -435,13 +403,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -490,29 +458,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "av1-grain" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom 7.1.3", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42" -dependencies = [ - "arrayvec", -] - [[package]] name = "axum" version = "0.6.20" @@ -520,7 +465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", @@ -541,6 +486,33 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 1.0.2", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-core" version = "0.3.4" @@ -558,6 +530,40 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.16", + "instant", + "pin-project-lite", + "rand 0.8.5", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -591,12 +597,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - [[package]] name = "bitflags" version = "1.3.2" @@ -605,19 +605,13 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ "serde", ] -[[package]] -name = "bitstream-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" - [[package]] name = "block" version = "0.1.6" @@ -657,7 +651,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.2", ] [[package]] @@ -669,7 +663,7 @@ dependencies = [ "async-channel 2.5.0", "async-task", "futures-io", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "piper", ] @@ -679,11 +673,11 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c340fe0f0b267787095cbe35240c6786ff19da63ec7b69367ba338eace8169b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "boa_interner", "boa_macros", "boa_string", - "indexmap 2.10.0", + "indexmap 2.11.4", "num-bigint", "rustc-hash", ] @@ -695,7 +689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f620c3f06f51e65c0504ddf04978be1b814ac6586f0b45f6019801ab5efd37f9" dependencies = [ "arrayvec", - "bitflags 2.9.1", + "bitflags 2.9.4", "boa_ast", "boa_gc", "boa_interner", @@ -707,9 +701,9 @@ dependencies = [ "cfg-if", "dashmap 6.1.0", "fast-float2", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "icu_normalizer 1.5.0", - "indexmap 2.10.0", + "indexmap 2.11.4", "intrusive-collections", "itertools 0.13.0", "num-bigint", @@ -729,7 +723,7 @@ dependencies = [ "static_assertions", "tap", "thin-vec", - "thiserror 2.0.12", + "thiserror 2.0.16", "time", ] @@ -742,7 +736,7 @@ dependencies = [ "boa_macros", "boa_profiler", "boa_string", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "thin-vec", ] @@ -754,8 +748,8 @@ checksum = "42407a3b724cfaecde8f7d4af566df4b56af32a2f11f0956f5570bb974e7f749" dependencies = [ "boa_gc", "boa_macros", - "hashbrown 0.15.4", - "indexmap 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.11.4", "once_cell", "phf 0.11.3", "rustc-hash", @@ -770,7 +764,7 @@ checksum = "9fd3f870829131332587f607a7ff909f1af5fc523fd1b192db55fbbdf52e8d3c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -780,7 +774,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cc142dac798cdc6e2dbccfddeb50f36d2523bb977a976e19bdb3ae19b740804" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "boa_ast", "boa_interner", "boa_macros", @@ -814,9 +808,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -833,12 +827,6 @@ dependencies = [ "alloc-stdlib", ] -[[package]] -name = "built" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" - [[package]] name = "bumpalo" version = "3.19.0" @@ -847,22 +835,22 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -888,21 +876,11 @@ dependencies = [ [[package]] name = "bzip2" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", + "libbz2-rs-sys", ] [[package]] @@ -911,7 +889,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cairo-sys-rs", "glib", "libc", @@ -932,11 +910,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.10" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" +checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -956,10 +934,10 @@ checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -969,7 +947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.2", + "toml 0.9.7", ] [[package]] @@ -979,11 +957,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] -name = "cc" -version = "1.2.30" +name = "castaway" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" + +[[package]] +name = "cc" +version = "1.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1018,9 +1003,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -1030,17 +1015,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -1082,18 +1066,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstyle", "clap_lex", @@ -1107,37 +1091,38 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "clash-verge" -version = "2.4.0" +version = "2.4.3" dependencies = [ "aes-gcm", "anyhow", - "async-trait", + "backoff", "base64 0.22.1", "boa_engine", + "cfg-if", "chrono", + "console-subscriber", "criterion", "dashmap 6.1.0", "deelevate", "delay_timer", "dirs 6.0.0", "dunce", + "flexi_logger", "futures", - "futures-util", - "gethostname 1.0.2", + "gethostname", "getrandom 0.3.3", "hex", "hmac", - "image", + "isahc", "kode-bridge", - "lazy_static", "libc", "log", - "log4rs", "nanoid", "network-interface", + "nu-ansi-term", "once_cell", "open", - "parking_lot", + "parking_lot 0.12.4", "percent-encoding", "port_scanner", "regex", @@ -1147,7 +1132,7 @@ dependencies = [ "scopeguard", "serde", "serde_json", - "serde_yaml", + "serde_yaml_ng", "sha2 0.10.9", "sys-locale", "sysinfo", @@ -1161,18 +1146,19 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-global-shortcut", + "tauri-plugin-http", "tauri-plugin-notification", "tauri-plugin-process", "tauri-plugin-shell", "tauri-plugin-updater", "tauri-plugin-window-state", - "tempfile", "tokio", + "tokio-stream", "users", "warp", "winapi", "winreg 0.55.0", - "zip", + "zip 5.1.1", ] [[package]] @@ -1190,7 +1176,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "block", "cocoa-foundation", "core-foundation 0.10.1", @@ -1206,19 +1192,13 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "block", "core-foundation 0.10.1", "core-graphics-types", "objc", ] -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colored" version = "2.2.0" @@ -1246,7 +1226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1258,6 +1238,45 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console-api" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" +dependencies = [ + "futures-core", + "prost 0.13.5", + "prost-types 0.13.5", + "tonic 0.12.3", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "hyper-util", + "prost 0.13.5", + "prost-types 0.13.5", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic 0.12.3", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "const-random" version = "0.1.18" @@ -1351,7 +1370,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -1364,7 +1383,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation 0.10.1", "libc", ] @@ -1378,6 +1397,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -1389,9 +1423,9 @@ dependencies = [ [[package]] name = "criterion" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" +checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" dependencies = [ "anes", "cast", @@ -1412,12 +1446,12 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" dependencies = [ "cast", - "itertools 0.10.5", + "itertools 0.13.0", ] [[package]] @@ -1506,7 +1540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1516,7 +1550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1529,10 +1563,40 @@ dependencies = [ ] [[package]] -name = "darling" -version = "0.20.11" +name = "curl" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "79fc3b6dd0b87ba36e565715bf9a2ced221311db47bd18011676f24a6066edbc" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2 0.6.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "curl-sys" +version = "0.4.83+curl-8.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5830daf304027db10c82632a464879d46a3f7c4ba17a31592657ad16c719b483" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.59.0", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ "darling_core", "darling_macro", @@ -1540,27 +1604,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1573,7 +1637,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.11", ] [[package]] @@ -1587,14 +1651,14 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.11", ] [[package]] -name = "data-encoding" -version = "2.9.0" +name = "data-url" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" [[package]] name = "deelevate" @@ -1643,34 +1707,23 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", "serde", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1683,20 +1736,14 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.104", + "syn 2.0.106", ] -[[package]] -name = "destructure_traitobject" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" - [[package]] name = "devtools-core" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78cdd51f6f62ad4eb9b6581d7e238e1779db3144ddbd711388f552e6ed3194b" +checksum = "b7abafdcb55ff28587b551fd20408c65032c1b7f405e7c2b889f20e3b8289d8f" dependencies = [ "async-stream", "bytes", @@ -1705,12 +1752,12 @@ dependencies = [ "http 0.2.12", "hyper 0.14.32", "log", - "prost-types", + "prost-types 0.12.6", "ringbuf", "thiserror 1.0.69", "tokio", "tokio-stream", - "tonic", + "tonic 0.10.2", "tonic-health", "tonic-web", "tower 0.4.13", @@ -1723,14 +1770,14 @@ dependencies = [ [[package]] name = "devtools-wire-format" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1c0de542960449c9566001c1879d10ede95f3f2e0013fdae0cc3b153bfabb0d" +checksum = "2c06ffa9aeb3fb248b41d4e71ab3c0aa89177afc6669459da4320b97a4c77948" dependencies = [ - "bitflags 2.9.1", - "prost", - "prost-types", - "tonic", + "bitflags 2.9.4", + "prost 0.12.6", + "prost-types 0.12.6", + "tonic 0.10.2", "tracing-core", ] @@ -1804,8 +1851,8 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.0", - "windows-sys 0.60.2", + "redox_users 0.5.2", + "windows-sys 0.61.0", ] [[package]] @@ -1820,10 +1867,10 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "block2 0.6.1", "libc", - "objc2 0.6.1", + "objc2 0.6.2", ] [[package]] @@ -1834,7 +1881,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1848,9 +1895,9 @@ dependencies = [ [[package]] name = "dlopen2" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" dependencies = [ "dlopen2_derive", "libc", @@ -1866,7 +1913,7 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1931,9 +1978,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dyn-clone" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" @@ -1943,14 +1990,14 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "embed-resource" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6d81016d6c977deefb2ef8d8290da019e27cc26167e102185da528e6c0ab38" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" dependencies = [ "cc", "memchr", "rustc_version 0.4.1", - "toml 0.9.2", + "toml 0.9.7", "vswhom", "winreg 0.55.0", ] @@ -1994,27 +2041,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", -] - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2025,22 +2052,23 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" dependencies = [ "serde", + "serde_core", "typeid", ] [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -2087,21 +2115,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "exr" -version = "1.73.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - [[package]] name = "fast-float2" version = "0.2.3" @@ -2123,6 +2136,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -2155,16 +2188,22 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -2182,6 +2221,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flexi_logger" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff38b61724dd492b5171d5dbb0921dfc8e859022c5993b22f80f74e9afe6d573" +dependencies = [ + "chrono", + "log", + "nu-ansi-term", + "regex", + "thiserror 2.0.16", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2221,7 +2273,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2238,9 +2290,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -2320,9 +2372,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand 2.3.0", "futures-core", @@ -2339,7 +2391,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2490,23 +2542,13 @@ dependencies = [ "version_check", ] -[[package]] -name = "gethostname" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" -dependencies = [ - "libc", - "windows-targets 0.48.5", -] - [[package]] name = "gethostname" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" dependencies = [ - "rustix 1.0.8", + "rustix 1.1.2", "windows-targets 0.52.6", ] @@ -2544,7 +2586,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] @@ -2558,16 +2600,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gif" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" -dependencies = [ - "color_quant", - "weezl", -] - [[package]] name = "gimli" version = "0.31.1" @@ -2612,7 +2644,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "futures-channel", "futures-core", "futures-executor", @@ -2636,11 +2668,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", - "proc-macro-crate 2.0.0", + "proc-macro-crate 2.0.2", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2655,9 +2687,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "global-hotkey" @@ -2667,11 +2699,11 @@ checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" dependencies = [ "crossbeam-channel", "keyboard-types", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "once_cell", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", "windows-sys 0.59.0", "x11rb", "xkeysym", @@ -2737,7 +2769,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2752,7 +2784,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.10.0", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -2761,9 +2793,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -2771,7 +2803,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.10.0", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -2816,9 +2848,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -2826,15 +2858,34 @@ dependencies = [ ] [[package]] -name = "headers" -version = "0.3.9" +name = "hashbrown" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" dependencies = [ "base64 0.21.7", + "byteorder", + "flate2", + "nom 7.1.3", + "num-traits", +] + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", "bytes", "headers-core", - "http 0.2.12", + "http 1.3.1", "httpdate", "mime", "sha1", @@ -2842,11 +2893,11 @@ dependencies = [ [[package]] name = "headers-core" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 0.2.12", + "http 1.3.1", ] [[package]] @@ -2991,9 +3042,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" @@ -3021,19 +3072,22 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", - "h2 0.4.11", + "futures-core", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -3046,7 +3100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "rustls", "rustls-pki-types", @@ -3068,6 +3122,19 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.7.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -3076,7 +3143,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "native-tls", "tokio", @@ -3086,9 +3153,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64 0.22.1", "bytes", @@ -3097,7 +3164,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.7.0", "ipnet", "libc", "percent-encoding", @@ -3112,9 +3179,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3122,7 +3189,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.0", ] [[package]] @@ -3141,7 +3208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -3166,7 +3233,7 @@ dependencies = [ "potential_utf", "yoke 0.8.0", "zerofrom", - "zerovec 0.11.2", + "zerovec 0.11.4", ] [[package]] @@ -3179,7 +3246,7 @@ dependencies = [ "litemap 0.8.0", "tinystr 0.8.1", "writeable 0.6.1", - "zerovec 0.11.2", + "zerovec 0.11.4", ] [[package]] @@ -3245,7 +3312,7 @@ dependencies = [ "icu_properties 2.0.1", "icu_provider 2.0.0", "smallvec", - "zerovec 0.11.2", + "zerovec 0.11.4", ] [[package]] @@ -3288,7 +3355,7 @@ dependencies = [ "icu_provider 2.0.0", "potential_utf", "zerotrie", - "zerovec 0.11.2", + "zerovec 0.11.4", ] [[package]] @@ -3334,7 +3401,7 @@ dependencies = [ "yoke 0.8.0", "zerofrom", "zerotrie", - "zerovec 0.11.2", + "zerovec 0.11.4", ] [[package]] @@ -3345,7 +3412,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3356,9 +3423,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3377,43 +3444,18 @@ dependencies = [ [[package]] name = "image" -version = "0.25.6" +version = "0.25.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" dependencies = [ "bytemuck", "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", + "moxcms", "num-traits", - "png", - "qoi", - "ravif", - "rayon", - "rgb", + "png 0.18.0", "tiff", - "zune-core", - "zune-jpeg", ] -[[package]] -name = "image-webp" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imgref" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" - [[package]] name = "indexmap" version = "1.9.3" @@ -3427,13 +3469,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -3469,7 +3512,7 @@ version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb6250a98af259a26fd5a4a6081fccea9ac116e4c3178acf4aeb86d32d2b7715" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cc", "handlebars", "lazy_static", @@ -3479,17 +3522,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "interprocess" version = "2.2.3" @@ -3527,11 +3559,11 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -3583,12 +3615,31 @@ dependencies = [ ] [[package]] -name = "itertools" -version = "0.10.5" +name = "isahc" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" dependencies = [ - "either", + "async-channel 1.9.0", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener 2.5.3", + "futures-lite 1.13.0", + "http 0.2.12", + "log", + "mime", + "once_cell", + "parking_lot 0.11.2", + "polling 2.8.0", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", ] [[package]] @@ -3609,6 +3660,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -3662,25 +3722,19 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", ] -[[package]] -name = "jpeg-decoder" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" - [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -3714,33 +3768,35 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "serde", "unicode-segmentation", ] [[package]] name = "kode-bridge" -version = "0.1.6-rc" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bf66a2690fdac4a30e3e60c5bb7e4479db318f2cd6eb95a353acf79d35855a" +checksum = "368479099245d8ecd5b74e6b2b6279a69b38556a442aefbbaadd3ecf8246ffc3" dependencies = [ "bytes", "futures", "http 1.3.1", "httparse", "interprocess", - "parking_lot", + "libc", + "parking_lot 0.12.4", "pin-project-lite", - "rand 0.8.5", + "rand 0.9.2", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tokio-stream", "tokio-util", - "toml 0.8.23", + "toml 0.9.7", "tracing", + "widestring", ] [[package]] @@ -3751,7 +3807,7 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 2.10.0", + "indexmap 2.11.4", "selectors", ] @@ -3761,12 +3817,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lebe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" - [[package]] name = "libappindicator" version = "0.9.0" @@ -3792,20 +3842,16 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.174" +name = "libbz2-rs-sys" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] -name = "libfuzzer-sys" -version = "0.4.10" +name = "libc" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" -dependencies = [ - "arbitrary", - "cc", -] +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libloading" @@ -3824,49 +3870,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.2", -] - -[[package]] -name = "liblzma" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0791ab7e08ccc8e0ce893f6906eb2703ed8739d8e89b57c0714e71bad09024c8" -dependencies = [ - "liblzma-sys", -] - -[[package]] -name = "liblzma-sys" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" -dependencies = [ - "cc", - "libc", - "pkg-config", + "windows-targets 0.53.3", ] [[package]] name = "libredox" -version = "0.1.6" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "libc", - "redox_syscall", + "redox_syscall 0.5.17", ] [[package]] name = "libz-rs-sys" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" dependencies = [ "zlib-rs", ] +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -3881,9 +3919,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -3927,55 +3965,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -dependencies = [ - "serde", -] - -[[package]] -name = "log-mdc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" - -[[package]] -name = "log4rs" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" -dependencies = [ - "anyhow", - "arc-swap", - "chrono", - "derivative", - "fnv", - "humantime", - "libc", - "log", - "log-mdc", - "once_cell", - "parking_lot", - "rand 0.8.5", - "serde", - "serde-value", - "serde_json", - "serde_yaml", - "thiserror 1.0.69", - "thread-id", - "typemap-ors", - "winapi", -] - -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" @@ -3983,7 +3975,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -3992,6 +3984,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2 0.10.9", +] + [[package]] name = "mac" version = "0.1.1" @@ -4005,7 +4007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4" dependencies = [ "cc", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-foundation 0.3.1", "time", ] @@ -4041,16 +4043,16 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -4065,16 +4067,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - [[package]] name = "md-5" version = "0.10.6" @@ -4165,42 +4157,34 @@ dependencies = [ ] [[package]] -name = "muda" -version = "0.17.0" +name = "moxcms" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b89bf91c19bf036347f1ab85a81c560f08c0667c8601bece664d860a600988" +checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", "once_cell", - "png", + "png 0.17.16", "serde", - "thiserror 2.0.12", - "windows-sys 0.59.0", -] - -[[package]] -name = "multer" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http 0.2.12", - "httparse", - "log", - "memchr", - "mime", - "spin", - "version_check", + "thiserror 2.0.16", + "windows-sys 0.60.2", ] [[package]] @@ -4235,7 +4219,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "jni-sys", "log", "ndk-sys", @@ -4286,14 +4270,14 @@ dependencies = [ [[package]] name = "network-interface" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862f41f1276e7148fb597fc55ed8666423bebe045199a1298c3515a73ec5cdd9" +checksum = "07709a6d4eba90ab10ec170a0530b3aafc81cb8a2d380e4423ae41fc55fe5745" dependencies = [ "cc", "libc", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", "winapi", ] @@ -4322,7 +4306,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", @@ -4355,19 +4339,13 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - [[package]] name = "notify-rust" version = "4.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" dependencies = [ - "futures-lite 2.6.0", + "futures-lite 2.6.1", "log", "mac-notification-sys", "serde", @@ -4386,12 +4364,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -4422,17 +4399,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "num-integer" version = "0.1.46" @@ -4442,17 +4408,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -4478,10 +4433,10 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 3.3.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4520,9 +4475,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -4534,10 +4489,10 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "block2 0.6.1", "libc", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-foundation", @@ -4553,8 +4508,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", + "bitflags 2.9.4", + "objc2 0.6.2", "objc2-foundation 0.3.1", ] @@ -4564,8 +4519,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", + "bitflags 2.9.4", + "objc2 0.6.2", "objc2-foundation 0.3.1", ] @@ -4575,9 +4530,9 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "dispatch2", - "objc2 0.6.1", + "objc2 0.6.2", ] [[package]] @@ -4586,9 +4541,9 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "dispatch2", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-core-foundation", "objc2-io-surface", ] @@ -4599,7 +4554,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.2", "objc2-foundation 0.3.1", ] @@ -4624,7 +4579,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4636,10 +4591,10 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "block2 0.6.1", "libc", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-core-foundation", ] @@ -4659,8 +4614,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", + "bitflags 2.9.4", + "objc2 0.6.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a" +dependencies = [ + "objc2 0.6.2", "objc2-core-foundation", ] @@ -4670,7 +4635,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4682,8 +4647,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", + "bitflags 2.9.4", + "objc2 0.6.2", "objc2-app-kit", "objc2-foundation 0.3.1", ] @@ -4694,7 +4659,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4707,19 +4672,30 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", + "bitflags 2.9.4", + "objc2 0.6.2", "objc2-foundation 0.3.1", ] +[[package]] +name = "objc2-security" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.2", + "objc2-core-foundation", +] + [[package]] name = "objc2-ui-kit" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", + "bitflags 2.9.4", + "objc2 0.6.2", "objc2-core-foundation", "objc2-foundation 0.3.1", ] @@ -4730,12 +4706,14 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "block2 0.6.1", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", + "objc2-javascript-core", + "objc2-security", ] [[package]] @@ -4783,7 +4761,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "foreign-types 0.3.2", "libc", @@ -4800,7 +4778,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4872,20 +4850,14 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.2", "objc2-foundation 0.3.1", "objc2-osa-kit", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "pango" version = "0.18.3" @@ -4917,6 +4889,17 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.4" @@ -4924,7 +4907,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.11", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -4935,7 +4932,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.17", "smallvec", "windows-targets 0.52.6", ] @@ -4974,26 +4971,26 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" dependencies = [ "pest", "pest_generator", @@ -5001,22 +4998,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "pest_meta" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" dependencies = [ "pest", "sha2 0.10.9", @@ -5029,7 +5026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.10.0", + "indexmap 2.11.4", ] [[package]] @@ -5136,7 +5133,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5183,7 +5180,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5217,13 +5214,13 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.7.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.10.0", - "quick-xml 0.38.0", + "indexmap 2.11.4", + "quick-xml 0.38.3", "serde", "time", ] @@ -5269,6 +5266,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.9.4", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "2.8.0" @@ -5287,16 +5297,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.5.2", "pin-project-lite", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix 1.1.2", + "windows-sys 0.61.0", ] [[package]] @@ -5340,11 +5350,11 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ - "zerovec 0.11.2", + "zerovec 0.11.4", ] [[package]] @@ -5353,6 +5363,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppmd-rust" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -5380,20 +5396,21 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" dependencies = [ - "toml_edit 0.20.7", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", ] [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.22.27", + "toml_edit 0.23.6", ] [[package]] @@ -5428,32 +5445,13 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" -dependencies = [ - "quote", - "syn 2.0.104", -] - [[package]] name = "prost" version = "0.12.6" @@ -5461,7 +5459,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.12.6", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive 0.13.5", ] [[package]] @@ -5474,7 +5482,20 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -5483,7 +5504,16 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ - "prost", + "prost 0.12.6", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost 0.13.5", ] [[package]] @@ -5503,12 +5533,12 @@ dependencies = [ ] [[package]] -name = "qoi" -version = "0.4.1" +name = "pxfm" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" dependencies = [ - "bytemuck", + "num-traits", ] [[package]] @@ -5528,18 +5558,18 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.0" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", ] [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -5548,8 +5578,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", - "thiserror 2.0.12", + "socket2 0.6.0", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -5557,9 +5587,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.3", @@ -5570,7 +5600,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -5578,16 +5608,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.0", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5715,56 +5745,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rav1e" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" -dependencies = [ - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.12.1", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive 0.4.2", - "num-traits", - "once_cell", - "paste", - "profiling", - "rand 0.8.5", - "rand_chacha 0.3.1", - "simd_helpers", - "system-deps", - "thiserror 1.0.69", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.11.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -5773,9 +5753,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -5783,9 +5763,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -5799,11 +5779,20 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" [[package]] name = "redox_syscall" -version = "0.5.16" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7251471db004e509f4e75a62cca9435365b5ec7bcdff530d612ac7c87c44a792" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags 2.9.1", + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.4", ] [[package]] @@ -5819,13 +5808,13 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -5845,52 +5834,37 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "regress" @@ -5898,15 +5872,15 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145bb27393fe455dd64d6cbc8d059adfa392590a45eadf079c01b11857e7b010" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", "memchr", ] [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", @@ -5915,11 +5889,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.4.11", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls", "hyper-tls", "hyper-util", @@ -5953,9 +5927,9 @@ dependencies = [ [[package]] name = "reqwest_dav" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280c1af39de7c1fb07ab9a036b37cc9b52a411c12a15bee0b520e07500d48d30" +checksum = "18a30b57f293dcb1a54163fc51fbbdbdaa3b622c4dbd8675a1062e0c671fab9a" dependencies = [ "async-trait", "chrono", @@ -5985,7 +5959,7 @@ dependencies = [ "gtk-sys", "js-sys", "log", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -5996,12 +5970,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" - [[package]] name = "ring" version = "0.17.14" @@ -6047,9 +6015,9 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7295b7ce3bf4806b419dc3420745998b447178b7005e2011947b38fc5aa6791" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" dependencies = [ "cfg-if", "ordered-multimap", @@ -6082,7 +6050,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.26", + "semver 1.0.27", ] [[package]] @@ -6105,7 +6073,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -6114,22 +6082,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.0", ] [[package]] name = "rustls" -version = "0.23.30" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069a8df149a16b1a12dcc31497c3396a173844be3cac4bd40c9e7671fef96671" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "ring", @@ -6151,9 +6119,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "ring", "rustls-pki-types", @@ -6162,9 +6130,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -6189,11 +6157,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -6244,7 +6212,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6265,7 +6233,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -6274,9 +6242,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -6320,11 +6288,12 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -6344,34 +6313,26 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde-untagged" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" dependencies = [ "erased-serde", "serde", + "serde_core", "typeid", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde-xml-rs" version = "0.6.0" @@ -6385,14 +6346,23 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6403,19 +6373,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -6426,7 +6397,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6440,11 +6411,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -6461,15 +6432,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.11.4", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -6481,23 +6452,23 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "serde_yaml_ng" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.4", "itoa", "ryu", "serde", @@ -6506,9 +6477,9 @@ dependencies = [ [[package]] name = "serialize-to-javascript" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" dependencies = [ "serde", "serde_json", @@ -6517,13 +6488,13 @@ dependencies = [ [[package]] name = "serialize-to-javascript-impl" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.106", ] [[package]] @@ -6640,9 +6611,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -6653,15 +6624,6 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - [[package]] name = "siphasher" version = "0.3.11" @@ -6676,9 +6638,20 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel 1.9.0", + "futures-core", + "futures-io", +] [[package]] name = "smallvec" @@ -6749,7 +6722,7 @@ dependencies = [ "objc2-foundation 0.2.2", "objc2-quartz-core 0.2.2", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.17", "wasm-bindgen", "web-sys", "windows-sys 0.59.0", @@ -6781,12 +6754,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "sptr" version = "0.3.2" @@ -6812,7 +6779,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", - "parking_lot", + "parking_lot 0.12.4", "phf_shared 0.11.3", "precomputed-hash", "serde", @@ -6866,9 +6833,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -6898,7 +6865,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6912,9 +6879,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.35.2" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e" +checksum = "3bddd368fda2f82ead69c03d46d351987cfa0c2a57abfa37a017f3aa3e9bf69a" dependencies = [ "libc", "memchr", @@ -6927,7 +6894,7 @@ dependencies = [ [[package]] name = "sysproxy" version = "0.3.0" -source = "git+https://github.com/clash-verge-rev/sysproxy-rs#28c3df9005694354cbaf725c23fe243854aac8a0" +source = "git+https://github.com/clash-verge-rev/sysproxy-rs#9fe61ca25dc5808cb6d7f13ae73a7a250ab56173" dependencies = [ "interfaces", "iptools", @@ -6945,7 +6912,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6969,17 +6936,18 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.23", + "toml 0.8.2", "version-compare", ] [[package]] name = "tao" -version = "0.34.0" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a" +checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", + "block2 0.6.1", "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", @@ -6996,11 +6964,11 @@ dependencies = [ "ndk", "ndk-context", "ndk-sys", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "objc2-foundation 0.3.1", "once_cell", - "parking_lot", + "parking_lot 0.12.4", "raw-window-handle", "scopeguard", "tao-macros", @@ -7020,7 +6988,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7048,12 +7016,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.7.0" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "352a4bc7bf6c25f5624227e3641adf475a6535707451b09bb83271df8b7a6ac7" +checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" dependencies = [ "anyhow", "bytes", + "cookie", "dirs 6.0.0", "dunce", "embed_plist", @@ -7069,10 +7038,11 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "objc2-foundation 0.3.1", "objc2-ui-kit", + "objc2-web-kit", "percent-encoding", "plist", "raw-window-handle", @@ -7087,7 +7057,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "tray-icon", @@ -7101,9 +7071,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "182d688496c06bf08ea896459bf483eb29cdff35c1c4c115fb14053514303064" +checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" dependencies = [ "anyhow", "cargo_toml", @@ -7112,36 +7082,36 @@ dependencies = [ "heck 0.5.0", "json-patch", "schemars 0.8.22", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "tauri-utils", "tauri-winres", - "toml 0.8.23", + "toml 0.9.7", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a" +checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "sha2 0.10.9", - "syn 2.0.104", + "syn 2.0.106", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.16", "time", "url", "uuid", @@ -7150,23 +7120,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.3.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7945b14dc45e23532f2ded6e120170bbdd4af5ceaa45784a6b33d250fbce3f9e" +checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd5c1e56990c70a906ef67a9851bbdba9136d26075ee9a2b19c8b46986b3e02" +checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" dependencies = [ "anyhow", "glob", @@ -7175,7 +7145,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.8.23", + "toml 0.9.7", "walkdir", ] @@ -7190,7 +7160,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -7205,23 +7175,24 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "tauri-plugin-deep-link" -version = "2.4.1" +version = "2.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fec67f32d7a06d80bd3dc009fdb678c35a66116d9cb8cd2bb32e406c2b5bbd2" +checksum = "cd67112fb1131834c2a7398ffcba520dbbf62c17de3b10329acd1a3554b1a9bb" dependencies = [ "dunce", + "plist", "rust-ini", "serde", "serde_json", "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.16", "tracing", "url", "windows-registry", @@ -7230,9 +7201,9 @@ dependencies = [ [[package]] name = "tauri-plugin-devtools" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5cd17faa36a826e5686bd0fda5bc3f4c903682263f00cd50f2f778fc4bb866" +checksum = "52dceb7bd8d7a19d2feb5f9f27122b620e85819063ab76f34c3f69d1bc29645c" dependencies = [ "async-stream", "bytes", @@ -7249,7 +7220,7 @@ dependencies = [ "tauri", "tauri-plugin", "tokio", - "tonic", + "tonic 0.10.2", "tonic-health", "tracing", "tracing-subscriber", @@ -7257,9 +7228,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05bedd4c3cf6f7aa97918a8814a736bd3695c9ddf3ede2d50eda6069c3290edc" +checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" dependencies = [ "log", "raw-window-handle", @@ -7269,15 +7240,15 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.12", + "thiserror 2.0.16", "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6ef84ee2f2094ce093e55106d90d763ba343fad57566992962e8f76d113f99" +checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" dependencies = [ "anyhow", "dunce", @@ -7290,8 +7261,8 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.12", - "toml 0.8.23", + "thiserror 2.0.16", + "toml 0.9.7", "url", ] @@ -7307,24 +7278,48 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.16", +] + +[[package]] +name = "tauri-plugin-http" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938a3d7051c9a82b431e3a0f3468f85715b3442b3c3a3913095e9fa509e2652c" +dependencies = [ + "bytes", + "cookie_store", + "data-url", + "http 1.3.1", + "regex", + "reqwest", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.16", + "tokio", + "url", + "urlpattern", ] [[package]] name = "tauri-plugin-notification" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe06ed89cff6d0ec06ff4f544fb961e4718348a33309f56ccb2086e77bc9116" +checksum = "d2fbc86b929b5376ab84b25c060f966d146b2fbd59b6af8264027b343c82c219" dependencies = [ "log", "notify-rust", - "rand 0.8.5", + "rand 0.9.2", "serde", "serde_json", "serde_repr", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.16", "time", "url", ] @@ -7341,9 +7336,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b9ffadec5c3523f11e8273465cacb3d86ea7652a28e6e2a2e9b5c182f791d25" +checksum = "54777d0c0d8add34eea3ced84378619ef5b97996bd967d3038c668feefd21071" dependencies = [ "encoding_rs", "log", @@ -7356,7 +7351,7 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", ] @@ -7377,19 +7372,19 @@ dependencies = [ "osakit", "percent-encoding", "reqwest", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "tar", "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.16", "time", "tokio", "url", "windows-sys 0.60.2", - "zip", + "zip 4.6.1", ] [[package]] @@ -7398,48 +7393,51 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5f6fe3291bfa609c7e0b0ee3bedac294d94c7018934086ce782c1d0f2a468e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "log", "serde", "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "tauri-runtime" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b1cc885be806ea15ff7b0eb47098a7b16323d9228876afda329e34e2d6c4676" +checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" dependencies = [ "cookie", "dpi", "gtk", "http 1.3.1", "jni", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-ui-kit", + "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.16", "url", + "webkit2gtk", + "webview2-com", "windows 0.61.3", ] [[package]] name = "tauri-runtime-wry" -version = "2.7.2" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe653a2fbbef19fe898efc774bc52c8742576342a33d3d028c189b57eb1d2439" +checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" dependencies = [ "gtk", "http 1.3.1", "jni", "log", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "objc2-foundation 0.3.1", "once_cell", @@ -7459,9 +7457,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e" +checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" dependencies = [ "anyhow", "brotli", @@ -7481,14 +7479,14 @@ dependencies = [ "quote", "regex", "schemars 0.8.22", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde-untagged", "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.12", - "toml 0.8.23", + "thiserror 2.0.16", + "toml 0.9.7", "url", "urlpattern", "uuid", @@ -7497,13 +7495,12 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4" +checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" dependencies = [ "embed-resource", - "indexmap 2.10.0", - "toml 0.8.23", + "toml 0.9.7", ] [[package]] @@ -7513,22 +7510,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml 0.37.5", - "thiserror 2.0.12", + "thiserror 2.0.16", "windows 0.61.3", "windows-version", ] [[package]] name = "tempfile" -version = "3.20.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand 2.3.0", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.0", ] [[package]] @@ -7580,7 +7577,7 @@ dependencies = [ "libc", "log", "memmem", - "num-derive 0.3.3", + "num-derive", "num-traits", "ordered-float", "regex", @@ -7613,11 +7610,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -7628,28 +7625,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", -] - -[[package]] -name = "thread-id" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" -dependencies = [ - "libc", - "winapi", + "syn 2.0.106", ] [[package]] @@ -7663,20 +7650,23 @@ dependencies = [ [[package]] name = "tiff" -version = "0.9.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" dependencies = [ + "fax", "flate2", - "jpeg-decoder", + "half", + "quick-error", "weezl", + "zune-jpeg", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -7692,15 +7682,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -7732,7 +7722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", - "zerovec 0.11.2", + "zerovec 0.11.4", ] [[package]] @@ -7747,9 +7737,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -7762,16 +7752,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", "io-uring", "libc", "mio", - "parking_lot", + "parking_lot 0.12.4", "pin-project-lite", "signal-hook-registry", "slab", @@ -7799,7 +7789,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7814,9 +7804,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ "rustls", "tokio", @@ -7833,23 +7823,11 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -7860,47 +7838,47 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", ] [[package]] name = "toml" -version = "0.9.2" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" dependencies = [ - "indexmap 2.10.0", - "serde", - "serde_spanned 1.0.0", - "toml_datetime 0.7.0", + "indexmap 2.11.4", + "serde_core", + "serde_spanned 1.0.2", + "toml_datetime 0.7.2", "toml_parser", "toml_writer", - "winnow 0.7.12", + "winnow 0.7.13", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_datetime" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -7909,56 +7887,50 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.10.0", - "toml_datetime 0.6.11", + "indexmap 2.11.4", + "toml_datetime 0.6.3", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.10.0", - "toml_datetime 0.6.11", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.4", "serde", "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow 0.7.12", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap 2.11.4", + "toml_datetime 0.7.2", + "toml_parser", + "winnow 0.7.13", ] [[package]] name = "toml_parser" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" dependencies = [ - "winnow 0.7.12", + "winnow 0.7.13", ] -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "toml_writer" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" [[package]] name = "tonic" @@ -7968,17 +7940,47 @@ checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.21.7", "bytes", "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-timeout", + "hyper-timeout 0.4.1", "percent-encoding", "pin-project", - "prost", + "prost 0.12.6", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum 0.7.9", + "base64 0.22.1", + "bytes", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-timeout 0.5.2", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.5", + "socket2 0.5.10", "tokio", "tokio-stream", "tower 0.4.13", @@ -7994,10 +7996,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f80db390246dfb46553481f6024f0082ba00178ea495dbb99e70ba9a4fafb5e1" dependencies = [ "async-stream", - "prost", + "prost 0.12.6", "tokio", "tokio-stream", - "tonic", + "tonic 0.10.2", ] [[package]] @@ -8013,7 +8015,7 @@ dependencies = [ "hyper 0.14.32", "pin-project", "tokio-stream", - "tonic", + "tonic 0.10.2", "tower-http 0.4.4", "tower-layer", "tower-service", @@ -8061,7 +8063,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "bytes", "futures-core", "futures-util", @@ -8079,7 +8081,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "bytes", "futures-util", "http 1.3.1", @@ -8123,7 +8125,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8136,6 +8138,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -8149,14 +8161,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -8167,33 +8179,32 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a" +checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" dependencies = [ "crossbeam-channel", "dirs 6.0.0", "libappindicator", "muda", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.1", "once_cell", - "png", + "png 0.17.16", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", "windows-sys 0.59.0", ] [[package]] name = "tree_magic_mini" -version = "3.1.6" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63" +checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c" dependencies = [ - "fnv", "memchr", "nom 7.1.3", "once_cell", @@ -8206,40 +8217,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - [[package]] name = "typeid" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" -[[package]] -name = "typemap-ors" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" -dependencies = [ - "unsafe-any-ors", -] - [[package]] name = "typenum" version = "1.18.0" @@ -8312,9 +8295,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -8332,15 +8315,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "unsafe-any-ors" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" -dependencies = [ - "destructure_traitobject", -] - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -8355,9 +8329,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -8413,9 +8387,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -8423,17 +8397,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - [[package]] name = "valuable" version = "0.1.1" @@ -8514,20 +8477,21 @@ dependencies = [ [[package]] name = "warp" -version = "0.3.7" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" +checksum = "51d06d9202adc1f15d709c4f4a2069be5428aa912cc025d6f268ac441ab066b0" dependencies = [ "bytes", - "futures-channel", "futures-util", "headers", - "http 0.2.12", - "hyper 0.14.32", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-util", "log", "mime", "mime_guess", - "multer", "percent-encoding", "pin-project", "scoped-tls", @@ -8535,7 +8499,6 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", - "tokio-tungstenite", "tokio-util", "tower-service", "tracing", @@ -8555,44 +8518,54 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" dependencies = [ "cfg-if", "js-sys", @@ -8603,9 +8576,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8613,22 +8586,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] @@ -8648,13 +8621,13 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.44", + "rustix 1.1.2", "scoped-tls", "smallvec", "wayland-sys", @@ -8662,23 +8635,23 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.1", - "rustix 0.38.44", + "bitflags 2.9.4", + "rustix 1.1.2", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-protocols" -version = "0.32.8" +version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "wayland-backend", "wayland-client", "wayland-scanner", @@ -8686,11 +8659,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8699,9 +8672,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" dependencies = [ "proc-macro2", "quick-xml 0.37.5", @@ -8710,9 +8683,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" dependencies = [ "dlib", "log", @@ -8721,9 +8694,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" dependencies = [ "js-sys", "wasm-bindgen", @@ -8814,7 +8787,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8823,7 +8796,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ - "thiserror 2.0.12", + "thiserror 2.0.16", "windows 0.61.3", "windows-core 0.61.2", ] @@ -8870,11 +8843,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -8889,7 +8862,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -8917,7 +8890,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.2", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -8951,11 +8924,24 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", ] +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", +] + [[package]] name = "windows-future" version = "0.2.1" @@ -8963,7 +8949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -8975,7 +8961,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8986,7 +8972,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8997,7 +8983,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9008,7 +8994,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9017,6 +9003,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -9024,7 +9016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -9033,7 +9025,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", ] @@ -9053,7 +9045,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -9072,7 +9073,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -9117,7 +9127,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -9168,10 +9187,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -9188,16 +9208,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] name = "windows-version" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" +checksum = "69e061eb0a22b4a1d778ad70f7575ec7845490abb35b08fa320df7895882cacb" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -9391,9 +9411,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -9428,13 +9448,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wl-clipboard-rs" @@ -9447,7 +9464,7 @@ dependencies = [ "os_pipe", "rustix 0.38.44", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.16", "tree_magic_mini", "wayland-backend", "wayland-client", @@ -9475,14 +9492,15 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wry" -version = "0.52.1" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9" +checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90" dependencies = [ "base64 0.22.1", "block2 0.6.1", "cookie", "crossbeam-channel", + "dirs 6.0.0", "dpi", "dunce", "gdkx11", @@ -9494,7 +9512,7 @@ dependencies = [ "kuchikiki", "libc", "ndk", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -9506,7 +9524,7 @@ dependencies = [ "sha2 0.10.9", "soup3", "tao-macros", - "thiserror 2.0.12", + "thiserror 2.0.16", "tracing", "url", "webkit2gtk", @@ -9541,20 +9559,20 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ - "gethostname 0.4.3", - "rustix 0.38.44", + "gethostname", + "rustix 1.1.2", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" [[package]] name = "xattr" @@ -9563,7 +9581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" dependencies = [ "libc", - "rustix 1.0.8", + "rustix 1.1.2", ] [[package]] @@ -9616,7 +9634,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -9628,21 +9646,21 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] [[package]] name = "zbus" -version = "5.9.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" +checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" dependencies = [ "async-broadcast", "async-executor", - "async-io 2.5.0", - "async-lock 3.4.0", - "async-process 2.4.0", + "async-io 2.6.0", + "async-lock 3.4.1", + "async-process 2.5.0", "async-recursion", "async-task", "async-trait", @@ -9650,7 +9668,7 @@ dependencies = [ "enumflags2", "event-listener 5.3.0", "futures-core", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "hex", "nix 0.30.1", "ordered-stream", @@ -9659,8 +9677,8 @@ dependencies = [ "tokio", "tracing", "uds_windows", - "windows-sys 0.59.0", - "winnow 0.7.12", + "windows-sys 0.60.2", + "winnow 0.7.13", "zbus_macros", "zbus_names", "zvariant", @@ -9668,14 +9686,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.9.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" +checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" dependencies = [ - "proc-macro-crate 3.3.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "zbus_names", "zvariant", "zvariant_utils", @@ -9689,28 +9707,28 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.7.12", + "winnow 0.7.13", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9730,7 +9748,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -9751,7 +9769,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9778,9 +9796,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke 0.8.0", "zerofrom", @@ -9795,7 +9813,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9806,14 +9824,26 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "zip" -version = "4.2.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ab361742de920c5535880f89bbd611ee62002bf11341d16a5f057bb8ba6899" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.11.4", + "memchr", +] + +[[package]] +name = "zip" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" dependencies = [ "aes", "arbitrary", @@ -9824,10 +9854,11 @@ dependencies = [ "flate2", "getrandom 0.3.3", "hmac", - "indexmap 2.10.0", - "liblzma", + "indexmap 2.11.4", + "lzma-rust2", "memchr", "pbkdf2", + "ppmd-rust", "sha1", "time", "zeroize", @@ -9837,9 +9868,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" @@ -9873,9 +9904,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", @@ -9887,62 +9918,52 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - [[package]] name = "zune-jpeg" -version = "0.4.19" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] [[package]] name = "zvariant" -version = "5.6.0" +version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" +checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" dependencies = [ "endi", "enumflags2", "serde", "url", - "winnow 0.7.12", + "winnow 0.7.13", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.6.0" +version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" +checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" dependencies = [ - "proc-macro-crate 3.3.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" dependencies = [ "proc-macro2", "quote", "serde", - "static_assertions", - "syn 2.0.104", - "winnow 0.7.12", + "syn 2.0.106", + "winnow 0.7.13", ] diff --git a/clash-verge-rev/src-tauri/Cargo.toml b/clash-verge-rev/src-tauri/Cargo.toml index d7185c6675..0dfd1929dc 100755 --- a/clash-verge-rev/src-tauri/Cargo.toml +++ b/clash-verge-rev/src-tauri/Cargo.toml @@ -1,85 +1,92 @@ [package] name = "clash-verge" -version = "2.4.0" +version = "2.4.3" description = "clash verge" -authors = ["zzzgydi", "wonfen", "MystiPanda"] +authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"] license = "GPL-3.0-only" repository = "https://github.com/clash-verge-rev/clash-verge-rev.git" default-run = "clash-verge" -edition = "2021" +edition = "2024" build = "build.rs" [package.metadata.bundle] identifier = "io.github.clash-verge-rev.clash-verge-rev" [build-dependencies] -tauri-build = { version = "2.3.0", features = [] } +tauri-build = { version = "2.4.1", features = [] } [dependencies] -warp = "0.3.7" -anyhow = "1.0.98" +warp = { version = "0.4.2", features = ["server"] } +anyhow = "1.0.100" dirs = "6.0" open = "5.3.2" -log = "0.4.27" +log = "0.4.28" dunce = "1.0.5" -log4rs = "1.3.0" nanoid = "0.4" -chrono = "0.4.41" -sysinfo = "=0.35.2" +chrono = "0.4.42" +sysinfo = { version = "0.37.1", features = ["network", "system"] } boa_engine = "0.20.0" -serde_json = "1.0.140" -serde_yaml = "0.9.34" +serde_json = "1.0.145" +serde_yaml_ng = "0.10.0" once_cell = "1.21.3" -lazy_static = "1.5.0" port_scanner = "0.1.5" delay_timer = "0.11.6" parking_lot = "0.12.4" -percent-encoding = "2.3.1" -tokio = { version = "1.46.1", features = [ +percent-encoding = "2.3.2" +tokio = { version = "1.47.1", features = [ "rt-multi-thread", "macros", "time", "sync", ] } -futures-util = "0.3.31" -serde = { version = "1.0.219", features = ["derive"] } -reqwest = { version = "0.12.22", features = ["json", "rustls-tls", "cookies"] } -regex = "1.11.1" +serde = { version = "1.0.228", features = ["derive"] } +reqwest = { version = "0.12.23", features = ["json", "cookies"] } +regex = "1.11.3" sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } -image = "0.25.6" -tauri = { version = "2.6.2", features = [ +tauri = { version = "2.8.5", features = [ "protocol-asset", "devtools", "tray-icon", "image-ico", "image-png", ] } -network-interface = { version = "2.0.1", features = ["serde"] } -tauri-plugin-shell = "2.3.0" -tauri-plugin-dialog = "2.3.0" -tauri-plugin-fs = "2.4.0" +network-interface = { version = "2.0.3", features = ["serde"] } +tauri-plugin-shell = "2.3.1" +tauri-plugin-dialog = "2.4.0" +tauri-plugin-fs = "2.4.2" tauri-plugin-process = "2.3.0" tauri-plugin-clipboard-manager = "2.3.0" -tauri-plugin-deep-link = "2.4.0" -tauri-plugin-devtools = "2.0.0" -tauri-plugin-window-state = "2.3.0" -zip = "=4.2.0" -reqwest_dav = "0.2.1" +tauri-plugin-deep-link = "2.4.3" +tauri-plugin-window-state = "2.4.0" +zip = "5.1.1" +reqwest_dav = "0.2.2" aes-gcm = { version = "0.10.3", features = ["std"] } base64 = "0.22.1" getrandom = "0.3.3" futures = "0.3.31" sys-locale = "0.3.2" -async-trait = "0.1.88" -libc = "0.2.174" +libc = "0.2.176" gethostname = "1.0.2" hmac = "0.12.1" sha2 = "0.10.9" hex = "0.4.3" scopeguard = "1.2.0" -kode-bridge = "0.1.6-rc" +kode-bridge = "0.3.0" dashmap = "6.1.0" -tauri-plugin-notification = "2.3.0" +tauri-plugin-notification = "2.3.1" +tokio-stream = "0.1.17" +isahc = { version = "1.7.2", default-features = false, features = [ + "text-decoding", + "parking_lot", +] } +backoff = { version = "0.4.0", features = ["tokio"] } +tauri-plugin-http = "2.5.2" +flexi_logger = "0.31.4" +cfg-if = "1.0.3" +nu-ansi-term = { version = "0.50.1", optional = true } +console-subscriber = { version = "0.4.1", optional = true } +tauri-plugin-devtools = { version = "2.0.1" } + [target.'cfg(windows)'.dependencies] runas = "=1.2.0" @@ -110,35 +117,93 @@ tauri-plugin-updater = "2.9.0" [features] default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] -verge-dev = [] +verge-dev = ["nu-ansi-term"] +tauri-dev = [] +tokio-trace = ["console-subscriber"] [profile.release] panic = "abort" -codegen-units = 1 -lto = true -opt-level = "s" +codegen-units = 16 +lto = "thin" +opt-level = 2 +debug = false strip = true +overflow-checks = false +rpath = false [profile.dev] incremental = true -codegen-units = 256 # 增加编译单元,提升编译速度 -opt-level = 0 # 禁用优化,进一步提升编译速度 -debug = true # 保留调试信息 -strip = false # 不剥离符号,保留调试信息 +codegen-units = 64 +opt-level = 0 +debug = true +strip = "none" +overflow-checks = true +lto = false +rpath = false [profile.fast-release] -inherits = "release" # 继承 release 的配置 -panic = "abort" # 与 release 相同 -codegen-units = 256 # 增加编译单元,提升编译速度 -lto = false # 禁用 LTO,提升编译速度 -opt-level = 0 # 禁用优化,大幅提升编译速度 -debug = true # 保留调试信息 -strip = false # 不剥离符号,保留调试信息 +inherits = "release" +incremental = true +codegen-units = 64 +lto = false +opt-level = 0 +debug = true +strip = false [lib] name = "app_lib" crate-type = ["staticlib", "cdylib", "rlib"] [dev-dependencies] -criterion = "0.6.0" -tempfile = "3.20.0" +criterion = "0.7.0" + +[lints.clippy] +# Core categories - most important for code safety and correctness +correctness = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } + +# Critical safety lints - warn for now due to extensive existing usage +unwrap_used = "warn" +expect_used = "warn" +panic = "deny" +unimplemented = "deny" + +# Development quality lints +todo = "warn" +dbg_macro = "warn" +#print_stdout = "warn" +#print_stderr = "warn" + +# Performance lints for proxy application +clone_on_ref_ptr = "warn" +rc_clone_in_vec_init = "warn" +large_stack_arrays = "warn" +large_const_arrays = "warn" + +# Security lints +#integer_division = "warn" +#lossy_float_literal = "warn" +#default_numeric_fallback = "warn" + +# Mutex and async lints - strict control +async_yields_async = "deny" # Prevents missing await in async blocks +mutex_atomic = "deny" # Use atomics instead of Mutex +mutex_integer = "deny" # Use AtomicInt instead of Mutex +rc_mutex = "deny" # Single-threaded Rc with Mutex is wrong +unused_async = "deny" # Too many false positives in Tauri/framework code +await_holding_lock = "deny" +large_futures = "deny" +future_not_send = "deny" + +# Common style improvements +redundant_else = "deny" # Too many in existing code +needless_continue = "deny" # Too many in existing code +needless_raw_string_hashes = "deny" # Too many in existing code + +# Disable noisy categories for existing codebase but keep them available +#style = { level = "allow", priority = -1 } +#complexity = { level = "allow", priority = -1 } +#perf = { level = "allow", priority = -1 } +#pedantic = { level = "allow", priority = -1 } +#nursery = { level = "allow", priority = -1 } +#restriction = { level = "allow", priority = -1 } diff --git a/clash-verge-rev/src-tauri/benches/draft_benchmark.rs b/clash-verge-rev/src-tauri/benches/draft_benchmark.rs index 999f338544..8f1515ed93 100644 --- a/clash-verge-rev/src-tauri/benches/draft_benchmark.rs +++ b/clash-verge-rev/src-tauri/benches/draft_benchmark.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, criterion_group, criterion_main}; use std::hint::black_box; // 业务模型 & Draft diff --git a/clash-verge-rev/src-tauri/capabilities/desktop.json b/clash-verge-rev/src-tauri/capabilities/desktop.json index a61c708af8..7a09c952f8 100755 --- a/clash-verge-rev/src-tauri/capabilities/desktop.json +++ b/clash-verge-rev/src-tauri/capabilities/desktop.json @@ -18,6 +18,12 @@ "autostart:allow-disable", "autostart:allow-is-enabled", "core:window:allow-set-theme", - "notification:default" + "notification:default", + "http:default", + "http:allow-fetch", + { + "identifier": "http:default", + "allow": [{ "url": "https://*/*" }, { "url": "http://*/*" }] + } ] } diff --git a/clash-verge-rev/src-tauri/icons/tray-icon-sys-mono-new.ico b/clash-verge-rev/src-tauri/icons/tray-icon-sys-mono-new.ico index d28cc7cbcc..34735d23c0 100644 Binary files a/clash-verge-rev/src-tauri/icons/tray-icon-sys-mono-new.ico and b/clash-verge-rev/src-tauri/icons/tray-icon-sys-mono-new.ico differ diff --git a/clash-verge-rev/src-tauri/icons/tray-icon-tun-mono-new.ico b/clash-verge-rev/src-tauri/icons/tray-icon-tun-mono-new.ico index d580583abd..0f16515bd6 100644 Binary files a/clash-verge-rev/src-tauri/icons/tray-icon-tun-mono-new.ico and b/clash-verge-rev/src-tauri/icons/tray-icon-tun-mono-new.ico differ diff --git a/clash-verge-rev/src-tauri/packages/windows/installer.nsi b/clash-verge-rev/src-tauri/packages/windows/installer.nsi index 1e5e139539..a34cb3ca1b 100644 --- a/clash-verge-rev/src-tauri/packages/windows/installer.nsi +++ b/clash-verge-rev/src-tauri/packages/windows/installer.nsi @@ -523,7 +523,7 @@ FunctionEnd ${If} $0 == 0 Push $0 ${If} $1 == 0 - DetailPrint "Restart Clash Verge Service..." + DetailPrint "Restart ${PRODUCTNAME} Service..." SimpleSC::StartService "clash_verge_service" "" 30 ${EndIf} ${ElseIf} $0 != 0 @@ -549,20 +549,20 @@ FunctionEnd ${If} $0 == 0 Push $0 ${If} $1 == 1 - DetailPrint "Stop Clash Verge Service..." + DetailPrint "Stop ${PRODUCTNAME} Service..." SimpleSC::StopService "clash_verge_service" 1 30 Pop $0 ; returns an errorcode (<>0) otherwise success (0) ${If} $0 == 0 - DetailPrint "Removing Clash Verge Service..." + DetailPrint "Removing ${PRODUCTNAME} Service..." SimpleSC::RemoveService "clash_verge_service" ${ElseIf} $0 != 0 Push $0 SimpleSC::GetErrorMessage Pop $0 - MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)" + MessageBox MB_OK|MB_ICONSTOP "${PRODUCTNAME} Service Stop Error ($0)" ${EndIf} ${ElseIf} $1 == 0 - DetailPrint "Removing Clash Verge Service..." + DetailPrint "Removing ${PRODUCTNAME} Service..." SimpleSC::RemoveService "clash_verge_service" ${EndIf} ${ElseIf} $0 != 0 @@ -661,7 +661,7 @@ SectionEnd Pop $R0 ${If} $R0 = 0 IfSilent kill 0 - ${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "Clash Verge 正在运行 $\n点击确定以终止运行" IDOK kill IDCANCEL cancel ${|} + ${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "${PRODUCTNAME} 正在运行 $\n点击确定以终止运行" IDOK kill IDCANCEL cancel ${|} kill: !if "${INSTALLMODE}" == "currentUser" nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe" @@ -1001,7 +1001,7 @@ Section Uninstall !insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" ; 删除所有用户的桌面快捷方式 - DetailPrint "开始删除所有用户桌面的 Clash Verge 快捷方式..." + DetailPrint "开始删除所有用户桌面的 ${PRODUCTNAME} 快捷方式..." ; 删除公共桌面快捷方式 Delete "C:\Users\Public\Desktop\Clash Verge.lnk" @@ -1029,7 +1029,7 @@ Section Uninstall Delete "$R4\Clash Verge.lnk" Delete "$R4\clash-verge.lnk" - DetailPrint "尝试删除用户 '$R3' 桌面的 Clash Verge 快捷方式" + DetailPrint "尝试删除用户 '$R3' 桌面的 ${PRODUCTNAME} 快捷方式" ${EndIf} ; 递增循环计数器 @@ -1052,7 +1052,7 @@ Section Uninstall DetailPrint "删除系统级开始菜单中的应用程序文件夹和快捷方式完成" ; 删除所有带 Clash Verge 或 clash-verge 的注册表项 - DetailPrint "开始清理所有 Clash Verge 相关的注册表项..." + DetailPrint "开始清理所有 ${PRODUCTNAME} 相关的注册表项..." ; 设置注册表查看模式 (64位) SetRegView 64 diff --git a/clash-verge-rev/src-tauri/src/cache/mod.rs b/clash-verge-rev/src-tauri/src/cache/mod.rs new file mode 100644 index 0000000000..cea404c141 --- /dev/null +++ b/clash-verge-rev/src-tauri/src/cache/mod.rs @@ -0,0 +1,111 @@ +use crate::singleton; +use anyhow::Result; +use dashmap::DashMap; +use serde_json::Value; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::OnceCell; + +pub const SHORT_TERM_TTL: Duration = Duration::from_millis(4_250); + +pub struct CacheEntry { + pub value: Arc, + pub expires_at: Instant, +} + +pub struct Cache { + pub map: DashMap>>>>, +} + +impl Cache { + fn new() -> Self { + Cache { + map: DashMap::new(), + } + } + + pub fn make_key(prefix: &str, id: &str) -> String { + format!("{prefix}:{id}") + } + + pub async fn get_or_fetch(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc + where + F: Fn() -> Fut + Send + Sync + 'static, + Fut: std::future::Future + Send + 'static, + T: Send + Sync + 'static, + { + loop { + let now = Instant::now(); + let key_cloned = key.clone(); + + // Get or create the cell + let cell = self + .map + .entry(key_cloned.clone()) + .or_insert_with(|| Arc::new(OnceCell::new())) + .clone(); + + // Check if we have a valid cached entry + if let Some(entry) = cell.get() { + if entry.expires_at > now { + return Arc::clone(&entry.value); + } + // Entry is expired, remove it + self.map + .remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell)); + continue; // Retry with fresh cell + } + + // Try to set a new value + let value = fetch_fn().await; + let entry = Box::new(CacheEntry { + value: Arc::new(value), + expires_at: Instant::now() + ttl, + }); + + match cell.set(entry) { + Ok(_) => { + // Successfully set the value, it must exist now + if let Some(set_entry) = cell.get() { + return Arc::clone(&set_entry.value); + } + } + Err(_) => { + if let Some(existing_entry) = cell.get() { + if existing_entry.expires_at > Instant::now() { + return Arc::clone(&existing_entry.value); + } + self.map + .remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell)); + } + } + } + } + } + + // pub fn clean_key(&self, key: &str) { + // self.map.remove(key); + // } + + // TODO + pub fn clean_default_keys(&self) { + // logging!(info, Type::Cache, "Cleaning proxies keys"); + // let proxies_key = Self::make_key("proxies", "default"); + // self.map.remove(&proxies_key); + + // logging!(info, Type::Cache, "Cleaning providers keys"); + // let providers_key = Self::make_key("providers", "default"); + // self.map.remove(&providers_key); + + // !The frontend goes crash if we clean the clash_config cache + // logging!(info, Type::Cache, "Cleaning clash config keys"); + // let clash_config_key = Self::make_key("clash_config", "default"); + // self.map.remove(&clash_config_key); + } +} + +pub type CacheService = Cache>; +pub type CacheProxy = Cache; + +singleton!(Cache, PROXY_INSTANCE); +singleton!(Cache>, SERVICE_INSTANCE); diff --git a/clash-verge-rev/src-tauri/src/cmd/app.rs b/clash-verge-rev/src-tauri/src/cmd/app.rs index f862dbe7c7..68de9ec5f9 100644 --- a/clash-verge-rev/src-tauri/src/cmd/app.rs +++ b/clash-verge-rev/src-tauri/src/cmd/app.rs @@ -4,18 +4,18 @@ use crate::{ utils::{dirs, logging::Type}, wrap_err, }; -use tauri::Manager; +use tauri::{AppHandle, Manager}; /// 打开应用程序所在目录 #[tauri::command] -pub fn open_app_dir() -> CmdResult<()> { +pub async fn open_app_dir() -> CmdResult<()> { let app_dir = wrap_err!(dirs::app_home_dir())?; wrap_err!(open::that(app_dir)) } /// 打开核心所在目录 #[tauri::command] -pub fn open_core_dir() -> CmdResult<()> { +pub async fn open_core_dir() -> CmdResult<()> { let core_dir = wrap_err!(tauri::utils::platform::current_exe())?; let core_dir = core_dir.parent().ok_or("failed to get core dir")?; wrap_err!(open::that(core_dir)) @@ -23,7 +23,7 @@ pub fn open_core_dir() -> CmdResult<()> { /// 打开日志目录 #[tauri::command] -pub fn open_logs_dir() -> CmdResult<()> { +pub async fn open_logs_dir() -> CmdResult<()> { let log_dir = wrap_err!(dirs::app_logs_dir())?; wrap_err!(open::that(log_dir)) } @@ -36,7 +36,7 @@ pub fn open_web_url(url: String) -> CmdResult<()> { /// 打开/关闭开发者工具 #[tauri::command] -pub fn open_devtools(app_handle: tauri::AppHandle) { +pub fn open_devtools(app_handle: AppHandle) { if let Some(window) = app_handle.get_webview_window("main") { if !window.is_devtools_open() { window.open_devtools(); @@ -48,14 +48,14 @@ pub fn open_devtools(app_handle: tauri::AppHandle) { /// 退出应用 #[tauri::command] -pub fn exit_app() { - feat::quit(); +pub async fn exit_app() { + feat::quit().await; } /// 重启应用 #[tauri::command] pub async fn restart_app() -> CmdResult<()> { - feat::restart_app(); + feat::restart_app().await; Ok(()) } @@ -121,9 +121,8 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult Err(_) => { if icon_path.exists() { return Ok(icon_path.to_string_lossy().to_string()); - } else { - return Err("Failed to create temporary file".into()); } + return Err("Failed to create temporary file".into()); } }; @@ -210,7 +209,7 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult { #[tauri::command] pub fn notify_ui_ready() -> CmdResult<()> { log::info!(target: "app", "前端UI已准备就绪"); - crate::utils::resolve::mark_ui_ready(); + crate::utils::resolve::ui::mark_ui_ready(); Ok(()) } @@ -219,7 +218,7 @@ pub fn notify_ui_ready() -> CmdResult<()> { pub fn update_ui_stage(stage: String) -> CmdResult<()> { log::info!(target: "app", "UI加载阶段更新: {stage}"); - use crate::utils::resolve::UiReadyStage; + use crate::utils::resolve::ui::UiReadyStage; let stage_enum = match stage.as_str() { "NotStarted" => UiReadyStage::NotStarted, @@ -233,14 +232,6 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> { } }; - crate::utils::resolve::update_ui_ready_stage(stage_enum); - Ok(()) -} - -/// 重置UI就绪状态 -#[tauri::command] -pub fn reset_ui_ready_state() -> CmdResult<()> { - log::info!(target: "app", "重置UI就绪状态"); - crate::utils::resolve::reset_ui_ready(); + crate::utils::resolve::ui::update_ui_ready_stage(stage_enum); Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/cmd/clash.rs b/clash-verge-rev/src-tauri/src/cmd/clash.rs index ce7f79fd61..0081f91716 100644 --- a/clash-verge-rev/src-tauri/src/cmd/clash.rs +++ b/clash-verge-rev/src-tauri/src/cmd/clash.rs @@ -1,24 +1,33 @@ use super::CmdResult; use crate::{ - config::*, core::*, feat, ipc::IpcManager, process::AsyncHandler, - state::proxy::ProxyRequestCache, wrap_err, + cache::CacheProxy, + config::Config, + core::{CoreManager, handle}, }; -use serde_yaml::Mapping; +use crate::{ + config::*, + feat, + ipc::{self, IpcManager}, + logging, + utils::logging::Type, + wrap_err, +}; +use serde_yaml_ng::Mapping; use std::time::Duration; const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60); /// 复制Clash环境变量 #[tauri::command] -pub fn copy_clash_env() -> CmdResult { - feat::copy_clash_env(); +pub async fn copy_clash_env() -> CmdResult { + feat::copy_clash_env().await; Ok(()) } /// 获取Clash信息 #[tauri::command] -pub fn get_clash_info() -> CmdResult { - Ok(Config::clash().latest_ref().get_client_info()) +pub async fn get_clash_info() -> CmdResult { + Ok(Config::clash().await.latest_ref().get_client_info()) } /// 修改Clash配置 @@ -30,14 +39,14 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult { /// 修改Clash模式 #[tauri::command] pub async fn patch_clash_mode(payload: String) -> CmdResult { - feat::change_clash_mode(payload); + feat::change_clash_mode(payload).await; Ok(()) } /// 切换Clash核心 #[tauri::command] pub async fn change_clash_core(clash_core: String) -> CmdResult> { - log::info!(target: "app", "changing core to {clash_core}"); + logging!(info, Type::Config, "changing core to {clash_core}"); match CoreManager::global() .change_core(Some(clash_core.clone())) @@ -47,14 +56,18 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult> // 切换内核后重启内核 match CoreManager::global().restart_core().await { Ok(_) => { - log::info!(target: "app", "core changed and restarted to {clash_core}"); + logging!( + info, + Type::Core, + "core changed and restarted to {clash_core}" + ); handle::Handle::notice_message("config_core::change_success", &clash_core); handle::Handle::refresh_clash(); Ok(None) } Err(err) => { let error_msg = format!("Core changed but failed to restart: {err}"); - log::error!(target: "app", "{error_msg}"); + logging!(error, Type::Core, "{error_msg}"); handle::Handle::notice_message("config_core::change_error", &error_msg); Ok(Some(error_msg)) } @@ -62,7 +75,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult> } Err(err) => { let error_msg = err.to_string(); - log::error!(target: "app", "failed to change core: {error_msg}"); + logging!(error, Type::Core, "failed to change core: {error_msg}"); handle::Handle::notice_message("config_core::change_error", &error_msg); Ok(Some(error_msg)) } @@ -116,15 +129,22 @@ pub async fn clash_api_get_proxy_delay( /// 测试URL延迟 #[tauri::command] pub async fn test_delay(url: String) -> CmdResult { - Ok(feat::test_delay(url).await.unwrap_or(10000u32)) + let result = match feat::test_delay(url).await { + Ok(delay) => delay, + Err(e) => { + log::error!(target: "app", "{}", e); + 10000u32 + } + }; + Ok(result) } /// 保存DNS配置到单独文件 #[tauri::command] pub async fn save_dns_config(dns_config: Mapping) -> CmdResult { use crate::utils::dirs; - use serde_yaml; - use std::fs; + use serde_yaml_ng; + use tokio::fs; // 获取DNS配置文件路径 let dns_path = dirs::app_home_dir() @@ -132,104 +152,103 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult { .join("dns_config.yaml"); // 保存DNS配置到文件 - let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?; - fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?; - log::info!(target: "app", "DNS config saved to {dns_path:?}"); + let yaml_str = serde_yaml_ng::to_string(&dns_config).map_err(|e| e.to_string())?; + fs::write(&dns_path, yaml_str) + .await + .map_err(|e| e.to_string())?; + logging!(info, Type::Config, "DNS config saved to {dns_path:?}"); Ok(()) } /// 应用或撤销DNS配置 #[tauri::command] -pub fn apply_dns_config(apply: bool) -> CmdResult { +pub async fn apply_dns_config(apply: bool) -> CmdResult { use crate::{ config::Config, - core::{handle, CoreManager}, + core::{CoreManager, handle}, utils::dirs, }; - // 使用spawn来处理异步操作 - AsyncHandler::spawn(move || async move { - if apply { - // 读取DNS配置文件 - let dns_path = match dirs::app_home_dir() { - Ok(path) => path.join("dns_config.yaml"), - Err(e) => { - log::error!(target: "app", "Failed to get home dir: {e}"); - return; - } - }; + if apply { + // 读取DNS配置文件 + let dns_path = dirs::app_home_dir() + .map_err(|e| e.to_string())? + .join("dns_config.yaml"); - if !dns_path.exists() { - log::warn!(target: "app", "DNS config file not found"); - return; - } - - let dns_yaml = match std::fs::read_to_string(&dns_path) { - Ok(content) => content, - Err(e) => { - log::error!(target: "app", "Failed to read DNS config: {e}"); - return; - } - }; - - // 解析DNS配置并创建patch - let patch_config = match serde_yaml::from_str::(&dns_yaml) { - Ok(config) => { - let mut patch = serde_yaml::Mapping::new(); - patch.insert("dns".into(), config.into()); - patch - } - Err(e) => { - log::error!(target: "app", "Failed to parse DNS config: {e}"); - return; - } - }; - - log::info!(target: "app", "Applying DNS config from file"); - - // 重新生成配置,确保DNS配置被正确应用 - // 这里不调用patch_clash以避免将DNS配置写入config.yaml - Config::runtime() - .draft_mut() - .patch_config(patch_config.clone()); - - // 首先重新生成配置 - if let Err(err) = Config::generate().await { - log::error!(target: "app", "Failed to regenerate config with DNS: {err}"); - return; - } - - // 然后应用新配置 - if let Err(err) = CoreManager::global().update_config().await { - log::error!(target: "app", "Failed to apply config with DNS: {err}"); - } else { - log::info!(target: "app", "DNS config successfully applied"); - handle::Handle::refresh_clash(); - } - } else { - // 当关闭DNS设置时,不需要对配置进行任何修改 - // 直接重新生成配置,让enhance函数自动跳过DNS配置的加载 - log::info!(target: "app", "DNS settings disabled, regenerating config"); - - // 重新生成配置 - if let Err(err) = Config::generate().await { - log::error!(target: "app", "Failed to regenerate config: {err}"); - return; - } - - // 应用新配置 - match CoreManager::global().update_config().await { - Ok(_) => { - log::info!(target: "app", "Config regenerated successfully"); - handle::Handle::refresh_clash(); - } - Err(err) => { - log::error!(target: "app", "Failed to apply regenerated config: {err}"); - } - } + if !dns_path.exists() { + logging!(warn, Type::Config, "DNS config file not found"); + return Err("DNS config file not found".into()); } - }); + + let dns_yaml = tokio::fs::read_to_string(&dns_path).await.map_err(|e| { + logging!(error, Type::Config, "Failed to read DNS config: {e}"); + e.to_string() + })?; + + // 解析DNS配置 + let patch_config = + serde_yaml_ng::from_str::(&dns_yaml).map_err(|e| { + logging!(error, Type::Config, "Failed to parse DNS config: {e}"); + e.to_string() + })?; + + logging!(info, Type::Config, "Applying DNS config from file"); + + // 创建包含DNS配置的patch + let mut patch = serde_yaml_ng::Mapping::new(); + patch.insert("dns".into(), patch_config.into()); + + // 应用DNS配置到运行时配置 + Config::runtime().await.draft_mut().patch_config(patch); + + // 重新生成配置 + Config::generate().await.map_err(|err| { + logging!( + error, + Type::Config, + "Failed to regenerate config with DNS: {err}" + ); + "Failed to regenerate config with DNS".to_string() + })?; + + // 应用新配置 + CoreManager::global().update_config().await.map_err(|err| { + logging!( + error, + Type::Config, + "Failed to apply config with DNS: {err}" + ); + "Failed to apply config with DNS".to_string() + })?; + + logging!(info, Type::Config, "DNS config successfully applied"); + handle::Handle::refresh_clash(); + } else { + // 当关闭DNS设置时,重新生成配置(不加载DNS配置文件) + logging!( + info, + Type::Config, + "DNS settings disabled, regenerating config" + ); + + Config::generate().await.map_err(|err| { + logging!(error, Type::Config, "Failed to regenerate config: {err}"); + "Failed to regenerate config".to_string() + })?; + + CoreManager::global().update_config().await.map_err(|err| { + logging!( + error, + Type::Config, + "Failed to apply regenerated config: {err}" + ); + "Failed to apply regenerated config".to_string() + })?; + + logging!(info, Type::Config, "Config regenerated successfully"); + handle::Handle::refresh_clash(); + } Ok(()) } @@ -250,17 +269,19 @@ pub fn check_dns_config_exists() -> CmdResult { #[tauri::command] pub async fn get_dns_config_content() -> CmdResult { use crate::utils::dirs; - use std::fs; + use tokio::fs; let dns_path = dirs::app_home_dir() .map_err(|e| e.to_string())? .join("dns_config.yaml"); - if !dns_path.exists() { + if !fs::try_exists(&dns_path).await.map_err(|e| e.to_string())? { return Err("DNS config file not found".into()); } - let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?; + let content = fs::read_to_string(&dns_path) + .await + .map_err(|e| e.to_string())?; Ok(content) } @@ -296,11 +317,14 @@ pub async fn get_clash_version() -> CmdResult { #[tauri::command] pub async fn get_clash_config() -> CmdResult { let manager = IpcManager::global(); - let cache = ProxyRequestCache::global(); - let key = ProxyRequestCache::make_key("clash_config", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("clash_config", "default"); let value = cache .get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async { - manager.get_config().await.expect("fetch failed") + manager.get_config().await.unwrap_or_else(|e| { + logging!(error, Type::Cmd, "Failed to fetch clash config: {e}"); + serde_json::Value::Object(serde_json::Map::new()) + }) }) .await; Ok((*value).clone()) @@ -309,8 +333,8 @@ pub async fn get_clash_config() -> CmdResult { /// 强制刷新Clash配置缓存 #[tauri::command] pub async fn force_refresh_clash_config() -> CmdResult { - let cache = ProxyRequestCache::global(); - let key = ProxyRequestCache::make_key("clash_config", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("clash_config", "default"); cache.map.remove(&key); get_clash_config().await } @@ -394,7 +418,6 @@ pub async fn close_all_clash_connections() -> CmdResult { /// 获取流量数据 (使用新的IPC流式监控) #[tauri::command] pub async fn get_traffic_data() -> CmdResult { - log::info!(target: "app", "开始获取流量数据 (IPC流式)"); let traffic = crate::ipc::get_current_traffic().await; let result = serde_json::json!({ "up": traffic.total_up, @@ -403,15 +426,12 @@ pub async fn get_traffic_data() -> CmdResult { "down_rate": traffic.down_rate, "last_updated": traffic.last_updated.elapsed().as_secs() }); - log::info!(target: "app", "获取流量数据结果: up={}, down={}, up_rate={}, down_rate={}", - traffic.total_up, traffic.total_down, traffic.up_rate, traffic.down_rate); Ok(result) } /// 获取内存数据 (使用新的IPC流式监控) #[tauri::command] pub async fn get_memory_data() -> CmdResult { - log::info!(target: "app", "开始获取内存数据 (IPC流式)"); let memory = crate::ipc::get_current_memory().await; let usage_percent = if memory.oslimit > 0 { (memory.inuse as f64 / memory.oslimit as f64) * 100.0 @@ -424,36 +444,34 @@ pub async fn get_memory_data() -> CmdResult { "usage_percent": usage_percent, "last_updated": memory.last_updated.elapsed().as_secs() }); - log::info!(target: "app", "获取内存数据结果: inuse={}, oslimit={}, usage={}%", - memory.inuse, memory.oslimit, usage_percent); Ok(result) } /// 启动流量监控服务 (IPC流式监控自动启动,此函数为兼容性保留) #[tauri::command] pub async fn start_traffic_service() -> CmdResult { - log::info!(target: "app", "启动流量监控服务 (IPC流式监控)"); + logging!(trace, Type::Ipc, "启动流量监控服务 (IPC流式监控)"); // 新的IPC监控在首次访问时自动启动 // 触发一次访问以确保监控器已初始化 let _ = crate::ipc::get_current_traffic().await; let _ = crate::ipc::get_current_memory().await; - log::info!(target: "app", "IPC流式监控已激活"); + logging!(info, Type::Ipc, "IPC流式监控已激活"); Ok(()) } /// 停止流量监控服务 (IPC流式监控无需显式停止,此函数为兼容性保留) #[tauri::command] pub async fn stop_traffic_service() -> CmdResult { - log::info!(target: "app", "停止流量监控服务请求 (IPC流式监控)"); + logging!(trace, Type::Ipc, "停止流量监控服务请求 (IPC流式监控)"); // 新的IPC监控是持久的,无需显式停止 - log::info!(target: "app", "IPC流式监控继续运行"); + logging!(info, Type::Ipc, "IPC流式监控继续运行"); Ok(()) } /// 获取格式化的流量数据 (包含单位,便于前端显示) #[tauri::command] pub async fn get_formatted_traffic_data() -> CmdResult { - log::info!(target: "app", "获取格式化流量数据"); + logging!(trace, Type::Ipc, "获取格式化流量数据"); let (up_rate, down_rate, total_up, total_down, is_fresh) = crate::ipc::get_formatted_traffic().await; let result = serde_json::json!({ @@ -463,16 +481,18 @@ pub async fn get_formatted_traffic_data() -> CmdResult { "total_down_formatted": total_down, "is_fresh": is_fresh }); - log::debug!(target: "app", "格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})"); - // Clippy: variables can be used directly in the format string - // log::debug!(target: "app", "格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})"); + logging!( + debug, + Type::Ipc, + "格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})" + ); Ok(result) } /// 获取格式化的内存数据 (包含单位,便于前端显示) #[tauri::command] pub async fn get_formatted_memory_data() -> CmdResult { - log::info!(target: "app", "获取格式化内存数据"); + logging!(info, Type::Ipc, "获取格式化内存数据"); let (inuse, oslimit, usage_percent, is_fresh) = crate::ipc::get_formatted_memory().await; let result = serde_json::json!({ "inuse_formatted": inuse, @@ -480,16 +500,18 @@ pub async fn get_formatted_memory_data() -> CmdResult { "usage_percent": usage_percent, "is_fresh": is_fresh }); - log::debug!(target: "app", "格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)"); - // Clippy: variables can be used directly in the format string - // log::debug!(target: "app", "格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)"); + logging!( + debug, + Type::Ipc, + "格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)" + ); Ok(result) } /// 获取系统监控概览 (流量+内存,便于前端一次性获取所有状态) #[tauri::command] pub async fn get_system_monitor_overview() -> CmdResult { - log::debug!(target: "app", "获取系统监控概览"); + logging!(debug, Type::Ipc, "获取系统监控概览"); // 并发获取流量和内存数据 let (traffic, memory) = tokio::join!( @@ -572,3 +594,30 @@ pub async fn is_clash_debug_enabled() -> CmdResult { pub async fn clash_gc() -> CmdResult { wrap_err!(IpcManager::global().gc().await) } + +/// 获取日志 (使用新的流式实现) +#[tauri::command] +pub async fn get_clash_logs() -> CmdResult { + Ok(ipc::get_logs_json().await) +} + +/// 启动日志监控 +#[tauri::command] +pub async fn start_logs_monitoring(level: Option) -> CmdResult { + ipc::start_logs_monitoring(level).await; + Ok(()) +} + +/// 停止日志监控 +#[tauri::command] +pub async fn stop_logs_monitoring() -> CmdResult { + ipc::stop_logs_monitoring().await; + Ok(()) +} + +/// 清除日志 +#[tauri::command] +pub async fn clear_logs() -> CmdResult { + ipc::clear_logs().await; + Ok(()) +} diff --git a/clash-verge-rev/src-tauri/src/cmd/lightweight.rs b/clash-verge-rev/src-tauri/src/cmd/lightweight.rs index 7afbe0ef9c..a3ba51bfbf 100644 --- a/clash-verge-rev/src-tauri/src/cmd/lightweight.rs +++ b/clash-verge-rev/src-tauri/src/cmd/lightweight.rs @@ -4,12 +4,12 @@ use super::CmdResult; #[tauri::command] pub async fn entry_lightweight_mode() -> CmdResult { - lightweight::entry_lightweight_mode(); + lightweight::entry_lightweight_mode().await; Ok(()) } #[tauri::command] pub async fn exit_lightweight_mode() -> CmdResult { - lightweight::exit_lightweight_mode(); + lightweight::exit_lightweight_mode().await; Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker.rs index 1b4c056b9f..9bccc46bc5 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker.rs @@ -1,3 +1,4 @@ +use crate::{logging, utils::logging::Type}; use chrono::Local; use regex::Regex; use reqwest::Client; @@ -250,7 +251,23 @@ async fn check_gemini(client: &Client) -> UnlockItem { let status = if is_ok { "Yes" } else { "No" }; // 尝试提取国家代码 - let re = Regex::new(r#",2,1,200,"([A-Z]{3})""#).unwrap(); + let re = match Regex::new(r#",2,1,200,"([A-Z]{3})""#) { + Ok(re) => re, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile Gemini regex: {}", + e + ); + return UnlockItem { + name: "Gemini".to_string(), + status: "Failed".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; let region = re.captures(&body).and_then(|caps| { caps.get(1).map(|m| { let country_code = m.as_str(); @@ -303,7 +320,23 @@ async fn check_youtube_premium(client: &Client) -> UnlockItem { } } else if body_lower.contains("ad-free") { // 尝试解析国家代码 - let re = Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#).unwrap(); + let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) { + Ok(re) => re, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile YouTube Premium regex: {}", + e + ); + return UnlockItem { + name: "Youtube Premium".to_string(), + status: "Failed".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; let region = re.captures(&body).and_then(|caps| { caps.get(1).map(|m| { let country_code = m.as_str().trim(); @@ -350,11 +383,16 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem { let cookie_store = Arc::new(reqwest::cookie::Jar::default()); // 使用带Cookie的客户端 - let client_with_cookies = reqwest::Client::builder() + let client_with_cookies = match reqwest::Client::builder() .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36") .cookie_provider(Arc::clone(&cookie_store)) - .build() - .unwrap_or_else(|_| client.clone()); + .build() { + Ok(client) => client, + Err(e) => { + logging!(error, Type::Network, "Failed to create client with cookies for Bahamut Anime: {}", e); + client.clone() + } + }; // 第一步:获取设备ID (会自动保存Cookie) let device_url = "https://ani.gamer.com.tw/ajax/getdeviceid.php"; @@ -363,10 +401,21 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem { match response.text().await { Ok(text) => { // 使用正则提取deviceid - let re = Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#).unwrap(); - re.captures(&text) - .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())) - .unwrap_or_default() + match Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#) { + Ok(re) => re + .captures(&text) + .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())) + .unwrap_or_default(), + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile deviceid regex for Bahamut Anime: {}", + e + ); + String::new() + } + } } Err(_) => String::new(), } @@ -421,17 +470,25 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem { .await { Ok(response) => match response.text().await { - Ok(body) => { - let region_re = Regex::new(r#"data-geo="([^"]+)"#).unwrap(); - region_re + Ok(body) => match Regex::new(r#"data-geo="([^"]+)"#) { + Ok(region_re) => region_re .captures(&body) .and_then(|caps| caps.get(1)) .map(|m| { let country_code = m.as_str(); let emoji = country_code_to_emoji(country_code); format!("{emoji}{country_code}") - }) - } + }), + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile region regex for Bahamut Anime: {}", + e + ); + None + } + }, Err(_) => None, }, Err(_) => None, @@ -495,8 +552,40 @@ async fn check_netflix(client: &Client) -> UnlockItem { } // 获取状态码 - let status1 = result1.unwrap().status().as_u16(); - let status2 = result2.unwrap().status().as_u16(); + let status1 = match result1 { + Ok(response) => response.status().as_u16(), + Err(e) => { + logging!( + error, + Type::Network, + "Failed to get Netflix response 1: {}", + e + ); + return UnlockItem { + name: "Netflix".to_string(), + status: "Failed".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; + let status2 = match result2 { + Ok(response) => response.status().as_u16(), + Err(e) => { + logging!( + error, + Type::Network, + "Failed to get Netflix response 2: {}", + e + ); + return UnlockItem { + name: "Netflix".to_string(), + status: "Failed".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; // 根据状态码判断解锁状况 if status1 == 404 && status2 == 404 { @@ -529,20 +618,20 @@ async fn check_netflix(client: &Client) -> UnlockItem { { Ok(response) => { // 检查重定向位置 - if let Some(location) = response.headers().get("location") { - if let Ok(location_str) = location.to_str() { - // 解析位置获取区域 - let parts: Vec<&str> = location_str.split('/').collect(); - if parts.len() >= 4 { - let region_code = parts[3].split('-').next().unwrap_or("unknown"); - let emoji = country_code_to_emoji(region_code); - return UnlockItem { - name: "Netflix".to_string(), - status: "Yes".to_string(), - region: Some(format!("{emoji}{region_code}")), - check_time: Some(get_local_date_string()), - }; - } + if let Some(location) = response.headers().get("location") + && let Ok(location_str) = location.to_str() + { + // 解析位置获取区域 + let parts: Vec<&str> = location_str.split('/').collect(); + if parts.len() >= 4 { + let region_code = parts[3].split('-').next().unwrap_or("unknown"); + let emoji = country_code_to_emoji(region_code); + return UnlockItem { + name: "Netflix".to_string(), + status: "Yes".to_string(), + region: Some(format!("{emoji}{region_code}")), + check_time: Some(get_local_date_string()), + }; } } // 如果没有重定向,假设是美国 @@ -602,22 +691,18 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem { match response.json::().await { Ok(data) => { // 尝试从数据中提取区域信息 - if let Some(targets) = data.get("targets").and_then(|t| t.as_array()) { - if !targets.is_empty() { - if let Some(location) = targets[0].get("location") { - if let Some(country) = - location.get("country").and_then(|c| c.as_str()) - { - let emoji = country_code_to_emoji(country); - return UnlockItem { - name: "Netflix".to_string(), - status: "Yes".to_string(), - region: Some(format!("{emoji}{country}")), - check_time: Some(get_local_date_string()), - }; - } - } - } + if let Some(targets) = data.get("targets").and_then(|t| t.as_array()) + && !targets.is_empty() + && let Some(location) = targets[0].get("location") + && let Some(country) = location.get("country").and_then(|c| c.as_str()) + { + let emoji = country_code_to_emoji(country); + return UnlockItem { + name: "Netflix".to_string(), + status: "Yes".to_string(), + region: Some(format!("{emoji}{country}")), + check_time: Some(get_local_date_string()), + }; } // 如果无法解析区域信息 @@ -685,7 +770,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem { }; } - let device_response = device_result.unwrap(); + let device_response = match device_result { + Ok(response) => response, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to get Disney+ device response: {}", + e + ); + return UnlockItem { + name: "Disney+".to_string(), + status: "Failed (Network Connection)".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; // 检查是否 403 错误 if device_response.status().as_u16() == 403 { @@ -710,7 +811,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem { }; // 提取 assertion - let re = Regex::new(r#""assertion"\s*:\s*"([^"]+)"#).unwrap(); + let re = match Regex::new(r#""assertion"\s*:\s*"([^"]+)"#) { + Ok(re) => re, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile assertion regex for Disney+: {}", + e + ); + return UnlockItem { + name: "Disney+".to_string(), + status: "Failed (Regex Error)".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; let assertion = match re.captures(&device_body) { Some(caps) => caps.get(1).map(|m| m.as_str().to_string()), None => None, @@ -729,7 +846,18 @@ async fn check_disney_plus(client: &Client) -> UnlockItem { let token_url = "https://disney.api.edge.bamgrid.com/token"; // 构建请求体 - 使用表单数据格式而非 JSON - let assertion_str = assertion.unwrap(); + let assertion_str = match assertion { + Some(assertion) => assertion, + None => { + logging!(error, Type::Network, "No assertion found for Disney+"); + return UnlockItem { + name: "Disney+".to_string(), + status: "Failed (No Assertion)".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; let token_body = [ ( "grant_type", @@ -762,7 +890,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem { }; } - let token_response = token_result.unwrap(); + let token_response = match token_result { + Ok(response) => response, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to get Disney+ token response: {}", + e + ); + return UnlockItem { + name: "Disney+".to_string(), + status: "Failed (Network Connection)".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; let token_status = token_response.status(); // 保存原始响应用于调试 @@ -798,10 +942,20 @@ async fn check_disney_plus(client: &Client) -> UnlockItem { .map(|s| s.to_string()), Err(_) => { // 如果 JSON 解析失败,尝试使用正则表达式 - let refresh_token_re = Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#).unwrap(); - refresh_token_re - .captures(&token_body_text) - .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())) + match Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#) { + Ok(refresh_token_re) => refresh_token_re + .captures(&token_body_text) + .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())), + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile refresh_token regex for Disney+: {}", + e + ); + None + } + } } }; @@ -825,7 +979,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem { // GraphQL API 通常接受 JSON 格式 let graphql_payload = format!( r#"{{"query":"mutation refreshToken($input: RefreshTokenInput!) {{ refreshToken(refreshToken: $input) {{ activeSession {{ sessionId }} }} }}","variables":{{"input":{{"refreshToken":"{}"}}}}}}"#, - refresh_token.unwrap() + refresh_token.unwrap_or_default() ); let graphql_result = client @@ -857,21 +1011,56 @@ async fn check_disney_plus(client: &Client) -> UnlockItem { }; // 解析 GraphQL 响应获取区域信息 - let graphql_response = graphql_result.unwrap(); + let graphql_response = match graphql_result { + Ok(response) => response, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to get Disney+ GraphQL response: {}", + e + ); + return UnlockItem { + name: "Disney+".to_string(), + status: "Failed (Network Connection)".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; let graphql_status = graphql_response.status(); - let graphql_body_text = (graphql_response.text().await).unwrap_or_default(); + let graphql_body_text = match graphql_response.text().await { + Ok(text) => text, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to read Disney+ GraphQL response text: {}", + e + ); + String::new() + } + }; // 如果 GraphQL 响应为空或明显错误,尝试直接获取区域信息 if graphql_body_text.is_empty() || graphql_status.as_u16() >= 400 { // 尝试直接从主页获取区域信息 let region_from_main = match client.get("https://www.disneyplus.com/").send().await { Ok(response) => match response.text().await { - Ok(body) => { - let region_re = Regex::new(r#"region"\s*:\s*"([^"]+)"#).unwrap(); - region_re + Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) { + Ok(region_re) => region_re .captures(&body) - .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())) - } + .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())), + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile Disney+ main page region regex: {}", + e + ); + None + } + }, Err(_) => None, }, Err(_) => None, @@ -898,28 +1087,59 @@ async fn check_disney_plus(client: &Client) -> UnlockItem { region: None, check_time: Some(get_local_date_string()), }; - } else { + } + return UnlockItem { + name: "Disney+".to_string(), + status: format!( + "Failed (GraphQL error: {}, status: {})", + graphql_body_text.chars().take(50).collect::() + "...", + graphql_status.as_u16() + ), + region: None, + check_time: Some(get_local_date_string()), + }; + } + + // 提取国家代码 + let region_re = match Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#) { + Ok(re) => re, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile Disney+ countryCode regex: {}", + e + ); return UnlockItem { name: "Disney+".to_string(), - status: format!( - "Failed (GraphQL error: {}, status: {})", - graphql_body_text.chars().take(50).collect::() + "...", - graphql_status.as_u16() - ), + status: "Failed (Regex Error)".to_string(), region: None, check_time: Some(get_local_date_string()), }; } - } - - // 提取国家代码 - let region_re = Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#).unwrap(); + }; let region_code = region_re .captures(&graphql_body_text) .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())); // 提取支持状态 - let supported_re = Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#).unwrap(); + let supported_re = match Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#) { + Ok(re) => re, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile Disney+ supported location regex: {}", + e + ); + return UnlockItem { + name: "Disney+".to_string(), + status: "Failed (Regex Error)".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; let in_supported_location = supported_re .captures(&graphql_body_text) .and_then(|caps| caps.get(1).map(|m| m.as_str() == "true")); @@ -929,12 +1149,20 @@ async fn check_disney_plus(client: &Client) -> UnlockItem { // 尝试直接从主页获取区域信息 let region_from_main = match client.get("https://www.disneyplus.com/").send().await { Ok(response) => match response.text().await { - Ok(body) => { - let region_re = Regex::new(r#"region"\s*:\s*"([^"]+)"#).unwrap(); - region_re + Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) { + Ok(region_re) => region_re .captures(&body) - .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())) - } + .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())), + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile Disney+ main page region regex: {}", + e + ); + None + } + }, Err(_) => None, }, Err(_) => None, @@ -958,7 +1186,18 @@ async fn check_disney_plus(client: &Client) -> UnlockItem { }; } - let region = region_code.unwrap(); + let region = match region_code { + Some(code) => code, + None => { + logging!(error, Type::Network, "No region code found for Disney+"); + return UnlockItem { + name: "Disney+".to_string(), + status: "No".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; // 判断日本地区 if region == "JP" { @@ -1028,13 +1267,47 @@ async fn check_prime_video(client: &Client) -> UnlockItem { } // 解析响应内容 - match result.unwrap().text().await { + let response = match result { + Ok(response) => response, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to get Prime Video response: {}", + e + ); + return UnlockItem { + name: "Prime Video".to_string(), + status: "Failed (Network Connection)".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; + + match response.text().await { Ok(body) => { // 检查是否被地区限制 let is_blocked = body.contains("isServiceRestricted"); // 提取地区信息 - let region_re = Regex::new(r#""currentTerritory":"([^"]+)"#).unwrap(); + let region_re = match Regex::new(r#""currentTerritory":"([^"]+)"#) { + Ok(re) => re, + Err(e) => { + logging!( + error, + Type::Network, + "Failed to compile Prime Video region regex: {}", + e + ); + return UnlockItem { + name: "Prime Video".to_string(), + status: "Failed (Regex Error)".to_string(), + region: None, + check_time: Some(get_local_date_string()), + }; + } + }; let region_code = region_re .captures(&body) .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())); @@ -1182,8 +1455,8 @@ pub async fn check_media_unlock() -> Result, String> { // 添加哔哩哔哩大陆检测任务 { - let client = client_arc.clone(); - let results = results.clone(); + let client = Arc::clone(&client_arc); + let results = Arc::clone(&results); tasks.spawn(async move { let result = check_bilibili_china_mainland(&client).await; let mut results = results.lock().await; @@ -1193,8 +1466,8 @@ pub async fn check_media_unlock() -> Result, String> { // 添加哔哩哔哩港澳台检测任务 { - let client = client_arc.clone(); - let results = results.clone(); + let client = Arc::clone(&client_arc); + let results = Arc::clone(&results); tasks.spawn(async move { let result = check_bilibili_hk_mc_tw(&client).await; let mut results = results.lock().await; @@ -1204,8 +1477,8 @@ pub async fn check_media_unlock() -> Result, String> { // 添加合并的ChatGPT检测任务 { - let client = client_arc.clone(); - let results = results.clone(); + let client = Arc::clone(&client_arc); + let results = Arc::clone(&results); tasks.spawn(async move { let chatgpt_results = check_chatgpt_combined(&client).await; let mut results = results.lock().await; @@ -1215,8 +1488,8 @@ pub async fn check_media_unlock() -> Result, String> { // 添加Gemini检测任务 { - let client = client_arc.clone(); - let results = results.clone(); + let client = Arc::clone(&client_arc); + let results = Arc::clone(&results); tasks.spawn(async move { let result = check_gemini(&client).await; let mut results = results.lock().await; @@ -1226,8 +1499,8 @@ pub async fn check_media_unlock() -> Result, String> { // 添加YouTube Premium检测任务 { - let client = client_arc.clone(); - let results = results.clone(); + let client = Arc::clone(&client_arc); + let results = Arc::clone(&results); tasks.spawn(async move { let result = check_youtube_premium(&client).await; let mut results = results.lock().await; @@ -1237,8 +1510,8 @@ pub async fn check_media_unlock() -> Result, String> { // 添加动画疯检测任务 { - let client = client_arc.clone(); - let results = results.clone(); + let client = Arc::clone(&client_arc); + let results = Arc::clone(&results); tasks.spawn(async move { let result = check_bahamut_anime(&client).await; let mut results = results.lock().await; @@ -1248,8 +1521,8 @@ pub async fn check_media_unlock() -> Result, String> { // 添加 Netflix 检测任务 { - let client = client_arc.clone(); - let results = results.clone(); + let client = Arc::clone(&client_arc); + let results = Arc::clone(&results); tasks.spawn(async move { let result = check_netflix(&client).await; let mut results = results.lock().await; @@ -1259,8 +1532,8 @@ pub async fn check_media_unlock() -> Result, String> { // 添加 Disney+ 检测任务 { - let client = client_arc.clone(); - let results = results.clone(); + let client = Arc::clone(&client_arc); + let results = Arc::clone(&results); tasks.spawn(async move { let result = check_disney_plus(&client).await; let mut results = results.lock().await; @@ -1270,8 +1543,8 @@ pub async fn check_media_unlock() -> Result, String> { // 添加 Prime Video 检测任务 { - let client = client_arc.clone(); - let results = results.clone(); + let client = Arc::clone(&client_arc); + let results = Arc::clone(&results); tasks.spawn(async move { let result = check_prime_video(&client).await; let mut results = results.lock().await; @@ -1287,9 +1560,17 @@ pub async fn check_media_unlock() -> Result, String> { } // 获取所有结果 - let results = Arc::try_unwrap(results) - .expect("无法获取结果,可能仍有引用存在") - .into_inner(); + let results = match Arc::try_unwrap(results) { + Ok(mutex) => mutex.into_inner(), + Err(_) => { + logging!( + error, + Type::Network, + "Failed to unwrap results Arc, references still exist" + ); + return Err("Failed to collect results".to_string()); + } + }; Ok(results) } diff --git a/clash-verge-rev/src-tauri/src/cmd/network.rs b/clash-verge-rev/src-tauri/src/cmd/network.rs index 52f88eff68..86ce8ed70c 100644 --- a/clash-verge-rev/src-tauri/src/cmd/network.rs +++ b/clash-verge-rev/src-tauri/src/cmd/network.rs @@ -1,8 +1,9 @@ use super::CmdResult; -use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager}; +use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery}; +use crate::process::AsyncHandler; use crate::wrap_err; use network_interface::NetworkInterface; -use serde_yaml::Mapping; +use serde_yaml_ng::Mapping; /// get the system proxy #[tauri::command] @@ -30,9 +31,9 @@ pub async fn get_auto_proxy() -> CmdResult { let proxy_manager = EventDrivenProxyManager::global(); - let current = proxy_manager.get_auto_proxy_cached(); + let current = proxy_manager.get_auto_proxy_cached().await; // 异步请求更新,立即返回缓存数据 - tokio::spawn(async move { + AsyncHandler::spawn(move || async move { let _ = proxy_manager.get_auto_proxy_async().await; }); diff --git a/clash-verge-rev/src-tauri/src/cmd/profile.rs b/clash-verge-rev/src-tauri/src/cmd/profile.rs index a6ff91538a..d76dc00b51 100644 --- a/clash-verge-rev/src-tauri/src/cmd/profile.rs +++ b/clash-verge-rev/src-tauri/src/cmd/profile.rs @@ -1,97 +1,76 @@ use super::CmdResult; use crate::{ - config::{Config, IProfiles, PrfItem, PrfOption}, - core::{handle, timer::Timer, tray::Tray, CoreManager}, - feat, logging, ret_err, + config::{ + Config, IProfiles, PrfItem, PrfOption, + profiles::{ + profiles_append_item_with_filedata_safe, profiles_delete_item_safe, + profiles_patch_item_safe, profiles_reorder_safe, profiles_save_file_safe, + }, + profiles_append_item_safe, + }, + core::{CoreManager, handle, timer::Timer, tray::Tray}, + feat, logging, + process::AsyncHandler, + ret_err, utils::{dirs, help, logging::Type}, wrap_err, }; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::time::Duration; -use tokio::sync::{Mutex, RwLock}; - -// 全局互斥锁防止并发配置更新 -static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(()); // 全局请求序列号跟踪,用于避免队列化执行 static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0); -static CURRENT_PROCESSING_PROFILE: RwLock> = RwLock::const_new(None); +static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false); -/// 清理配置处理状态 -async fn cleanup_processing_state(sequence: u64, reason: &str) { - *CURRENT_PROCESSING_PROFILE.write().await = None; - logging!( - info, - Type::Cmd, - true, - "{},清理状态,序列号: {}", - reason, - sequence - ); -} - -/// 获取配置文件避免锁竞争 #[tauri::command] pub async fn get_profiles() -> CmdResult { // 策略1: 尝试快速获取latest数据 - let latest_result = tokio::time::timeout( - Duration::from_millis(500), - tokio::task::spawn_blocking(move || { - let profiles = Config::profiles(); - let latest = profiles.latest_ref(); - IProfiles { - current: latest.current.clone(), - items: latest.items.clone(), - } - }), - ) + let latest_result = tokio::time::timeout(Duration::from_millis(500), async { + let profiles = Config::profiles().await; + let latest = profiles.latest_ref(); + IProfiles { + current: latest.current.clone(), + items: latest.items.clone(), + } + }) .await; match latest_result { - Ok(Ok(profiles)) => { + Ok(profiles) => { logging!(info, Type::Cmd, false, "快速获取配置列表成功"); return Ok(profiles); } - Ok(Err(join_err)) => { - logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err); - } Err(_) => { logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)"); } } // 策略2: 如果快速获取失败,尝试获取data() - let data_result = tokio::time::timeout( - Duration::from_secs(2), - tokio::task::spawn_blocking(move || { - let profiles = Config::profiles(); - let data = profiles.latest_ref(); - IProfiles { - current: data.current.clone(), - items: data.items.clone(), - } - }), - ) + let data_result = tokio::time::timeout(Duration::from_secs(2), async { + let profiles = Config::profiles().await; + let data = profiles.latest_ref(); + IProfiles { + current: data.current.clone(), + items: data.items.clone(), + } + }) .await; match data_result { - Ok(Ok(profiles)) => { + Ok(profiles) => { logging!(info, Type::Cmd, false, "获取draft配置列表成功"); return Ok(profiles); } - Ok(Err(join_err)) => { + Err(join_err) => { logging!( error, Type::Cmd, true, - "获取draft配置任务失败: {}", + "获取draft配置任务失败或超时: {}", join_err ); } - Err(_) => { - logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)"); - } } // 策略3: fallback,尝试重新创建配置 @@ -102,26 +81,19 @@ pub async fn get_profiles() -> CmdResult { "所有获取配置策略都失败,尝试fallback" ); - match tokio::task::spawn_blocking(IProfiles::new).await { - Ok(profiles) => { - logging!(info, Type::Cmd, true, "使用fallback配置成功"); - Ok(profiles) - } - Err(err) => { - logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err); - // 返回空配置避免崩溃 - Ok(IProfiles { - current: None, - items: Some(vec![]), - }) - } - } + Ok(IProfiles::new().await) } /// 增强配置文件 #[tauri::command] pub async fn enhance_profiles() -> CmdResult { - wrap_err!(feat::enhance_profiles().await)?; + match feat::enhance_profiles().await { + Ok(_) => {} + Err(e) => { + log::error!(target: "app", "{}", e); + return Err(e.to_string()); + } + } handle::Handle::refresh_clash(); Ok(()) } @@ -129,40 +101,152 @@ pub async fn enhance_profiles() -> CmdResult { /// 导入配置文件 #[tauri::command] pub async fn import_profile(url: String, option: Option) -> CmdResult { - let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?; - wrap_err!(Config::profiles().data_mut().append_item(item)) + logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url); + + let import_result = tokio::time::timeout(Duration::from_secs(60), async { + let item = PrfItem::from_url(&url, None, None, option).await?; + logging!(info, Type::Cmd, true, "[导入订阅] 下载完成,开始保存配置"); + + let profiles = Config::profiles().await; + let pre_count = profiles + .latest_ref() + .items + .as_ref() + .map_or(0, |items| items.len()); + + let result = profiles_append_item_safe(item.clone()).await; + result?; + + let post_count = profiles + .latest_ref() + .items + .as_ref() + .map_or(0, |items| items.len()); + if post_count <= pre_count { + logging!( + error, + Type::Cmd, + true, + "[导入订阅] 配置未增加,导入可能失败" + ); + return Err(anyhow::anyhow!("配置导入后数量未增加")); + } + + logging!( + info, + Type::Cmd, + true, + "[导入订阅] 配置保存成功,数量: {} -> {}", + pre_count, + post_count + ); + + // 立即发送配置变更通知 + if let Some(uid) = &item.uid { + logging!( + info, + Type::Cmd, + true, + "[导入订阅] 发送配置变更通知: {}", + uid + ); + handle::Handle::notify_profile_changed(uid.clone()); + } + + // 异步保存配置文件并发送全局通知 + let uid_clone = item.uid.clone(); + crate::process::AsyncHandler::spawn(move || async move { + // 使用Send-safe helper函数 + if let Err(e) = profiles_save_file_safe().await { + logging!(error, Type::Cmd, true, "[导入订阅] 保存配置文件失败: {}", e); + } else { + logging!(info, Type::Cmd, true, "[导入订阅] 配置文件保存成功"); + + // 发送全局配置更新通知 + if let Some(uid) = uid_clone { + // 延迟发送,确保文件已完全写入 + tokio::time::sleep(Duration::from_millis(100)).await; + handle::Handle::notify_profile_changed(uid); + } + } + }); + + Ok(()) + }) + .await; + + match import_result { + Ok(Ok(())) => { + logging!(info, Type::Cmd, true, "[导入订阅] 导入完成: {}", url); + Ok(()) + } + Ok(Err(e)) => { + logging!(error, Type::Cmd, true, "[导入订阅] 导入失败: {}", e); + Err(format!("导入订阅失败: {e}")) + } + Err(_) => { + logging!(error, Type::Cmd, true, "[导入订阅] 导入超时(60秒): {}", url); + Err("导入订阅超时,请检查网络连接".into()) + } + } } -/// 重新排序配置文件 +/// 调整profile的顺序 #[tauri::command] pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult { - wrap_err!(Config::profiles().data_mut().reorder(active_id, over_id)) + match profiles_reorder_safe(active_id, over_id).await { + Ok(_) => { + log::info!(target: "app", "重新排序配置文件"); + Ok(()) + } + Err(err) => { + log::error!(target: "app", "重新排序配置文件失败: {}", err); + Err(format!("重新排序配置文件失败: {}", err)) + } + } } -/// 创建配置文件 +/// 创建新的profile +/// 创建一个新的配置文件 #[tauri::command] pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResult { - let item = wrap_err!(PrfItem::from(item, file_data).await)?; - wrap_err!(Config::profiles().data_mut().append_item(item)) + match profiles_append_item_with_filedata_safe(item, file_data).await { + Ok(_) => Ok(()), + Err(err) => match err.to_string().as_str() { + "the file already exists" => Err("the file already exists".into()), + _ => Err(format!("add profile error: {err}")), + }, + } } /// 更新配置文件 #[tauri::command] pub async fn update_profile(index: String, option: Option) -> CmdResult { - wrap_err!(feat::update_profile(index, option, Some(true)).await) + match feat::update_profile(index, option, Some(true)).await { + Ok(_) => Ok(()), + Err(e) => { + log::error!(target: "app", "{}", e); + Err(e.to_string()) + } + } } /// 删除配置文件 #[tauri::command] pub async fn delete_profile(index: String) -> CmdResult { - let should_update = wrap_err!({ Config::profiles().data_mut().delete_item(index) })?; - - // 删除后自动清理冗余文件 - let _ = Config::profiles().latest_ref().auto_cleanup(); + // 使用Send-safe helper函数 + let should_update = wrap_err!(profiles_delete_item_safe(index).await)?; if should_update { - wrap_err!(CoreManager::global().update_config().await)?; - handle::Handle::refresh_clash(); + match CoreManager::global().update_config().await { + Ok(_) => { + handle::Handle::refresh_clash(); + } + Err(e) => { + log::error!(target: "app", "{}", e); + return Err(e.to_string()); + } + } } Ok(()) } @@ -170,6 +254,12 @@ pub async fn delete_profile(index: String) -> CmdResult { /// 修改profiles的配置 #[tauri::command] pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { + if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) { + logging!(info, Type::Cmd, true, "当前正在切换配置,放弃请求"); + return Ok(false); + } + CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst); + // 为当前请求分配序列号 let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1; let target_profile = profiles.current.clone(); @@ -183,35 +273,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { target_profile ); - let mutex_result = - tokio::time::timeout(Duration::from_millis(100), PROFILE_UPDATE_MUTEX.lock()).await; - - let _guard = match mutex_result { - Ok(guard) => guard, - Err(_) => { - let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); - if current_sequence < latest_sequence { - logging!( - info, - Type::Cmd, - true, - "检测到更新的请求 (序列号: {} < {}),放弃当前请求", - current_sequence, - latest_sequence - ); - return Ok(false); - } - logging!( - info, - Type::Cmd, - true, - "强制获取锁以处理最新请求: {}", - current_sequence - ); - PROFILE_UPDATE_MUTEX.lock().await - } - }; - let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); if current_sequence < latest_sequence { logging!( @@ -226,113 +287,110 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { } // 保存当前配置,以便在验证失败时恢复 - let current_profile = Config::profiles().latest_ref().current.clone(); + let current_profile = Config::profiles().await.latest_ref().current.clone(); logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile); // 如果要切换配置,先检查目标配置文件是否有语法错误 - if let Some(new_profile) = profiles.current.as_ref() { - if current_profile.as_ref() != Some(new_profile) { - logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile); + if let Some(new_profile) = profiles.current.as_ref() + && current_profile.as_ref() != Some(new_profile) + { + logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile); - // 获取目标配置文件路径 - let config_file_result = { - let profiles_config = Config::profiles(); - let profiles_data = profiles_config.latest_ref(); - match profiles_data.get_item(new_profile) { - Ok(item) => { - if let Some(file) = &item.file { - let path = dirs::app_profiles_dir().map(|dir| dir.join(file)); - path.ok() - } else { - None - } - } - Err(e) => { - logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e); + // 获取目标配置文件路径 + let config_file_result = { + let profiles_config = Config::profiles().await; + let profiles_data = profiles_config.latest_ref(); + match profiles_data.get_item(new_profile) { + Ok(item) => { + if let Some(file) = &item.file { + let path = dirs::app_profiles_dir().map(|dir| dir.join(file)); + path.ok() + } else { None } } - }; - - // 如果获取到文件路径,检查YAML语法 - if let Some(file_path) = config_file_result { - if !file_path.exists() { - logging!( - error, - Type::Cmd, - true, - "目标配置文件不存在: {}", - file_path.display() - ); - handle::Handle::notice_message( - "config_validate::file_not_found", - format!("{}", file_path.display()), - ); - return Ok(false); + Err(e) => { + logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e); + None } + } + }; - // 超时保护 - let file_read_result = tokio::time::timeout( - Duration::from_secs(5), - tokio::fs::read_to_string(&file_path), - ) - .await; + // 如果获取到文件路径,检查YAML语法 + if let Some(file_path) = config_file_result { + if !file_path.exists() { + logging!( + error, + Type::Cmd, + true, + "目标配置文件不存在: {}", + file_path.display() + ); + handle::Handle::notice_message( + "config_validate::file_not_found", + format!("{}", file_path.display()), + ); + return Ok(false); + } - match file_read_result { - Ok(Ok(content)) => { - let yaml_parse_result = tokio::task::spawn_blocking(move || { - serde_yaml::from_str::(&content) - }) - .await; + // 超时保护 + let file_read_result = tokio::time::timeout( + Duration::from_secs(5), + tokio::fs::read_to_string(&file_path), + ) + .await; - match yaml_parse_result { - Ok(Ok(_)) => { - logging!(info, Type::Cmd, true, "目标配置文件语法正确"); - } - Ok(Err(err)) => { - let error_msg = format!(" {err}"); - logging!( - error, - Type::Cmd, - true, - "目标配置文件存在YAML语法错误:{}", - error_msg - ); - handle::Handle::notice_message( - "config_validate::yaml_syntax_error", - &error_msg, - ); - return Ok(false); - } - Err(join_err) => { - let error_msg = format!("YAML解析任务失败: {join_err}"); - logging!(error, Type::Cmd, true, "{}", error_msg); - handle::Handle::notice_message( - "config_validate::yaml_parse_error", - &error_msg, - ); - return Ok(false); - } + match file_read_result { + Ok(Ok(content)) => { + let yaml_parse_result = AsyncHandler::spawn_blocking(move || { + serde_yaml_ng::from_str::(&content) + }) + .await; + + match yaml_parse_result { + Ok(Ok(_)) => { + logging!(info, Type::Cmd, true, "目标配置文件语法正确"); + } + Ok(Err(err)) => { + let error_msg = format!(" {err}"); + logging!( + error, + Type::Cmd, + true, + "目标配置文件存在YAML语法错误:{}", + error_msg + ); + handle::Handle::notice_message( + "config_validate::yaml_syntax_error", + &error_msg, + ); + return Ok(false); + } + Err(join_err) => { + let error_msg = format!("YAML解析任务失败: {join_err}"); + logging!(error, Type::Cmd, true, "{}", error_msg); + handle::Handle::notice_message( + "config_validate::yaml_parse_error", + &error_msg, + ); + return Ok(false); } } - Ok(Err(err)) => { - let error_msg = format!("无法读取目标配置文件: {err}"); - logging!(error, Type::Cmd, true, "{}", error_msg); - handle::Handle::notice_message( - "config_validate::file_read_error", - &error_msg, - ); - return Ok(false); - } - Err(_) => { - let error_msg = "读取配置文件超时(5秒)".to_string(); - logging!(error, Type::Cmd, true, "{}", error_msg); - handle::Handle::notice_message( - "config_validate::file_read_timeout", - &error_msg, - ); - return Ok(false); - } + } + Ok(Err(err)) => { + let error_msg = format!("无法读取目标配置文件: {err}"); + logging!(error, Type::Cmd, true, "{}", error_msg); + handle::Handle::notice_message("config_validate::file_read_error", &error_msg); + return Ok(false); + } + Err(_) => { + let error_msg = "读取配置文件超时(5秒)".to_string(); + logging!(error, Type::Cmd, true, "{}", error_msg); + handle::Handle::notice_message( + "config_validate::file_read_timeout", + &error_msg, + ); + return Ok(false); } } } @@ -352,18 +410,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { return Ok(false); } - if let Some(ref profile) = target_profile { - *CURRENT_PROCESSING_PROFILE.write().await = Some(profile.clone()); - logging!( - info, - Type::Cmd, - true, - "设置当前处理profile: {}, 序列号: {}", - profile, - current_sequence - ); - } - // 更新profiles配置 logging!( info, @@ -375,7 +421,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { let current_value = profiles.current.clone(); - let _ = Config::profiles().draft_mut().patch_config(profiles); + let _ = Config::profiles().await.draft_mut().patch_config(profiles); // 在调用内核前再次验证请求有效性 let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); @@ -388,7 +434,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { current_sequence, latest_sequence ); - Config::profiles().discard(); + Config::profiles().await.discard(); return Ok(false); } @@ -420,7 +466,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { current_sequence, latest_sequence ); - Config::profiles().discard(); + Config::profiles().await.discard(); return Ok(false); } @@ -431,7 +477,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { "配置更新成功,序列号: {}", current_sequence ); - Config::profiles().apply(); + Config::profiles().await.apply(); handle::Handle::refresh_clash(); // 强制刷新代理缓存,确保profile切换后立即获取最新节点数据 @@ -441,20 +487,18 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { } }); - crate::process::AsyncHandler::spawn(|| async move { - if let Err(e) = Tray::global().update_tooltip() { - log::warn!(target: "app", "异步更新托盘提示失败: {e}"); - } + if let Err(e) = Tray::global().update_tooltip().await { + log::warn!(target: "app", "异步更新托盘提示失败: {e}"); + } - if let Err(e) = Tray::global().update_menu() { - log::warn!(target: "app", "异步更新托盘菜单失败: {e}"); - } + if let Err(e) = Tray::global().update_menu().await { + log::warn!(target: "app", "异步更新托盘菜单失败: {e}"); + } - // 保存配置文件 - if let Err(e) = Config::profiles().data_mut().save_file() { - log::warn!(target: "app", "异步保存配置文件失败: {e}"); - } - }); + // 保存配置文件 + if let Err(e) = profiles_save_file_safe().await { + log::warn!(target: "app", "异步保存配置文件失败: {e}"); + } // 立即通知前端配置变更 if let Some(current) = ¤t_value { @@ -469,13 +513,12 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { handle::Handle::notify_profile_changed(current.clone()); } - cleanup_processing_state(current_sequence, "配置切换完成").await; - + CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); Ok(true) } Ok(Ok((false, error_msg))) => { logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg); - Config::profiles().discard(); + Config::profiles().await.discard(); // 如果验证失败,恢复到之前的配置 if let Some(prev_profile) = current_profile { logging!( @@ -492,13 +535,14 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { // 静默恢复,不触发验证 wrap_err!({ Config::profiles() + .await .draft_mut() .patch_config(restore_profiles) })?; - Config::profiles().apply(); + Config::profiles().await.apply(); crate::process::AsyncHandler::spawn(|| async move { - if let Err(e) = Config::profiles().data_mut().save_file() { + if let Err(e) = profiles_save_file_safe().await { log::warn!(target: "app", "异步保存恢复配置文件失败: {e}"); } }); @@ -508,9 +552,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { // 发送验证错误通知 handle::Handle::notice_message("config_validate::error", &error_msg); - - cleanup_processing_state(current_sequence, "配置验证失败").await; - + CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); Ok(false) } Ok(Err(e)) => { @@ -522,11 +564,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { e, current_sequence ); - Config::profiles().discard(); + Config::profiles().await.discard(); handle::Handle::notice_message("config_validate::boot_error", e.to_string()); - cleanup_processing_state(current_sequence, "更新过程错误").await; - + CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); Ok(false) } Err(_) => { @@ -540,7 +581,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { timeout_msg, current_sequence ); - Config::profiles().discard(); + Config::profiles().await.discard(); if let Some(prev_profile) = current_profile { logging!( @@ -557,16 +598,15 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { }; wrap_err!({ Config::profiles() + .await .draft_mut() .patch_config(restore_profiles) })?; - Config::profiles().apply(); + Config::profiles().await.apply(); } handle::Handle::notice_message("config_validate::timeout", timeout_msg); - - cleanup_processing_state(current_sequence, "配置更新超时").await; - + CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); Ok(false) } } @@ -574,10 +614,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { /// 根据profile name修改profiles #[tauri::command] -pub async fn patch_profiles_config_by_profile_index( - _app_handle: tauri::AppHandle, - profile_index: String, -) -> CmdResult { +pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult { logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index); let profiles = IProfiles { @@ -589,28 +626,26 @@ pub async fn patch_profiles_config_by_profile_index( /// 修改某个profile item的 #[tauri::command] -pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult { +pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { // 保存修改前检查是否有更新 update_interval - let update_interval_changed = - if let Ok(old_profile) = Config::profiles().latest_ref().get_item(&index) { - let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval); - let new_interval = profile.option.as_ref().and_then(|o| o.update_interval); - old_interval != new_interval - } else { - false - }; + let profiles = Config::profiles().await; + let update_interval_changed = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) { + let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval); + let new_interval = profile.option.as_ref().and_then(|o| o.update_interval); + old_interval != new_interval + } else { + false + }; // 保存修改 - wrap_err!(Config::profiles() - .data_mut() - .patch_item(index.clone(), profile))?; + wrap_err!(profiles_patch_item_safe(index.clone(), profile).await)?; // 如果更新间隔变更,异步刷新定时器 if update_interval_changed { let index_clone = index.clone(); crate::process::AsyncHandler::spawn(move || async move { logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器..."); - if let Err(e) = crate::core::Timer::global().refresh() { + if let Err(e) = crate::core::Timer::global().refresh().await { logging!(error, Type::Timer, "刷新定时器失败: {}", e); } else { // 刷新成功后发送自定义事件,不触发配置重载 @@ -624,9 +659,11 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult { /// 查看配置文件 #[tauri::command] -pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult { +pub async fn view_profile(index: String) -> CmdResult { + let profiles = Config::profiles().await; + let profiles_ref = profiles.latest_ref(); let file = { - wrap_err!(Config::profiles().latest_ref().get_item(&index))? + wrap_err!(profiles_ref.get_item(&index))? .file .clone() .ok_or("the file field is null") @@ -637,23 +674,23 @@ pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult { ret_err!("the file not found"); } - wrap_err!(help::open_file(app_handle, path)) + wrap_err!(help::open_file(path)) } /// 读取配置文件内容 #[tauri::command] -pub fn read_profile_file(index: String) -> CmdResult { - let profiles = Config::profiles(); - let profiles = profiles.latest_ref(); - let item = wrap_err!(profiles.get_item(&index))?; +pub async fn read_profile_file(index: String) -> CmdResult { + let profiles = Config::profiles().await; + let profiles_ref = profiles.latest_ref(); + let item = wrap_err!(profiles_ref.get_item(&index))?; let data = wrap_err!(item.read_file())?; Ok(data) } /// 获取下一次更新时间 #[tauri::command] -pub fn get_next_update_time(uid: String) -> CmdResult> { +pub async fn get_next_update_time(uid: String) -> CmdResult> { let timer = Timer::global(); - let next_time = timer.get_next_update_time(&uid); + let next_time = timer.get_next_update_time(&uid).await; Ok(next_time) } diff --git a/clash-verge-rev/src-tauri/src/cmd/proxy.rs b/clash-verge-rev/src-tauri/src/cmd/proxy.rs index 007118a92a..387da3ad7d 100644 --- a/clash-verge-rev/src-tauri/src/cmd/proxy.rs +++ b/clash-verge-rev/src-tauri/src/cmd/proxy.rs @@ -1,5 +1,13 @@ +use tauri::Emitter; + use super::CmdResult; -use crate::{ipc::IpcManager, state::proxy::ProxyRequestCache}; +use crate::{ + cache::CacheProxy, + core::{handle::Handle, tray::Tray}, + ipc::IpcManager, + logging, + utils::logging::Type, +}; use std::time::Duration; const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60); @@ -7,12 +15,15 @@ const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60); #[tauri::command] pub async fn get_proxies() -> CmdResult { - let manager = IpcManager::global(); - let cache = ProxyRequestCache::global(); - let key = ProxyRequestCache::make_key("proxies", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("proxies", "default"); let value = cache .get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async { - manager.get_proxies().await.expect("fetch failed") + let manager = IpcManager::global(); + manager.get_proxies().await.unwrap_or_else(|e| { + logging!(error, Type::Cmd, "Failed to fetch proxies: {e}"); + serde_json::Value::Object(serde_json::Map::new()) + }) }) .await; Ok((*value).clone()) @@ -21,21 +32,92 @@ pub async fn get_proxies() -> CmdResult { /// 强制刷新代理缓存用于profile切换 #[tauri::command] pub async fn force_refresh_proxies() -> CmdResult { - let cache = ProxyRequestCache::global(); - let key = ProxyRequestCache::make_key("proxies", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("proxies", "default"); cache.map.remove(&key); get_proxies().await } #[tauri::command] pub async fn get_providers_proxies() -> CmdResult { - let manager = IpcManager::global(); - let cache = ProxyRequestCache::global(); - let key = ProxyRequestCache::make_key("providers", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("providers", "default"); let value = cache .get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async { - manager.get_providers_proxies().await.expect("fetch failed") + let manager = IpcManager::global(); + manager.get_providers_proxies().await.unwrap_or_else(|e| { + logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}"); + serde_json::Value::Object(serde_json::Map::new()) + }) }) .await; Ok((*value).clone()) } + +/// 同步托盘和GUI的代理选择状态 +#[tauri::command] +pub async fn sync_tray_proxy_selection() -> CmdResult<()> { + use crate::core::tray::Tray; + + match Tray::global().update_menu().await { + Ok(_) => { + logging!(info, Type::Cmd, "Tray proxy selection synced successfully"); + Ok(()) + } + Err(e) => { + logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e}"); + Err(e.to_string()) + } + } +} + +/// 更新代理选择并同步托盘和GUI状态 +#[tauri::command] +pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> { + match IpcManager::global().update_proxy(&group, &proxy).await { + Ok(_) => { + // println!("Proxy updated successfully: {} -> {}", group,proxy); + logging!( + info, + Type::Cmd, + "Proxy updated successfully: {} -> {}", + group, + proxy + ); + + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("proxies", "default"); + cache.map.remove(&key); + + if let Err(e) = Tray::global().update_menu().await { + logging!(error, Type::Cmd, "Failed to sync tray menu: {}", e); + } + + if let Some(app_handle) = Handle::global().app_handle() { + let _ = app_handle.emit("verge://force-refresh-proxies", ()); + let _ = app_handle.emit("verge://refresh-proxy-config", ()); + } + + logging!( + info, + Type::Cmd, + "Proxy and sync completed successfully: {} -> {}", + group, + proxy + ); + Ok(()) + } + Err(e) => { + println!("1111111111111111"); + logging!( + error, + Type::Cmd, + "Failed to update proxy: {} -> {}, error: {}", + group, + proxy, + e + ); + Err(e.to_string()) + } + } +} diff --git a/clash-verge-rev/src-tauri/src/cmd/runtime.rs b/clash-verge-rev/src-tauri/src/cmd/runtime.rs index ff539b79fd..8e335b5787 100644 --- a/clash-verge-rev/src-tauri/src/cmd/runtime.rs +++ b/clash-verge-rev/src-tauri/src/cmd/runtime.rs @@ -1,36 +1,109 @@ use super::CmdResult; -use crate::{config::*, wrap_err}; +use crate::{config::*, core::CoreManager, log_err, wrap_err}; use anyhow::Context; -use serde_yaml::Mapping; +use serde_yaml_ng::Mapping; use std::collections::HashMap; /// 获取运行时配置 #[tauri::command] -pub fn get_runtime_config() -> CmdResult> { - Ok(Config::runtime().latest_ref().config.clone()) +pub async fn get_runtime_config() -> CmdResult> { + Ok(Config::runtime().await.latest_ref().config.clone()) } /// 获取运行时YAML配置 #[tauri::command] -pub fn get_runtime_yaml() -> CmdResult { - let runtime = Config::runtime(); +pub async fn get_runtime_yaml() -> CmdResult { + let runtime = Config::runtime().await; let runtime = runtime.latest_ref(); + let config = runtime.config.as_ref(); - wrap_err!(config - .ok_or(anyhow::anyhow!("failed to parse config to yaml file")) - .and_then( - |config| serde_yaml::to_string(config).context("failed to convert config to yaml") - )) + wrap_err!( + config + .ok_or(anyhow::anyhow!("failed to parse config to yaml file")) + .and_then(|config| serde_yaml_ng::to_string(config) + .context("failed to convert config to yaml")) + ) } /// 获取运行时存在的键 #[tauri::command] -pub fn get_runtime_exists() -> CmdResult> { - Ok(Config::runtime().latest_ref().exists_keys.clone()) +pub async fn get_runtime_exists() -> CmdResult> { + Ok(Config::runtime().await.latest_ref().exists_keys.clone()) } /// 获取运行时日志 #[tauri::command] -pub fn get_runtime_logs() -> CmdResult>> { - Ok(Config::runtime().latest_ref().chain_logs.clone()) +pub async fn get_runtime_logs() -> CmdResult>> { + Ok(Config::runtime().await.latest_ref().chain_logs.clone()) +} + +#[tauri::command] +pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult { + let runtime = Config::runtime().await; + let runtime = runtime.latest_ref(); + + let config = wrap_err!( + runtime + .config + .as_ref() + .ok_or(anyhow::anyhow!("failed to parse config to yaml file")) + )?; + + if let Some(serde_yaml_ng::Value::Sequence(proxies)) = config.get("proxies") { + let mut proxy_name = Some(Some(proxy_chain_exit_node.as_str())); + let mut proxies_chain = Vec::new(); + + while let Some(proxy) = proxies.iter().find(|proxy| { + if let serde_yaml_ng::Value::Mapping(proxy_map) = proxy { + proxy_map.get("name").map(|x| x.as_str()) == proxy_name + && proxy_map.get("dialer-proxy").is_some() + } else { + false + } + }) { + proxies_chain.push(proxy.to_owned()); + proxy_name = proxy.get("dialer-proxy").map(|x| x.as_str()); + } + + if let Some(entry_proxy) = proxies + .iter() + .find(|proxy| proxy.get("name").map(|x| x.as_str()) == proxy_name) + && !proxies_chain.is_empty() + { + // 添加第一个节点 + proxies_chain.push(entry_proxy.to_owned()); + } + + proxies_chain.reverse(); + + let mut config: HashMap> = HashMap::new(); + + config.insert("proxies".to_string(), proxies_chain); + + wrap_err!(serde_yaml_ng::to_string(&config).context("YAML generation failed")) + } else { + wrap_err!(Err(anyhow::anyhow!( + "failed to get proxies or proxy-groups".to_string() + ))) + } +} + +/// 更新运行时链式代理配置 +#[tauri::command] +pub async fn update_proxy_chain_config_in_runtime( + proxy_chain_config: Option, +) -> CmdResult<()> { + { + let runtime = Config::runtime().await; + let mut draft = runtime.draft_mut(); + draft.update_proxy_chain_config(proxy_chain_config); + drop(draft); + runtime.apply(); + } + + // 生成新的运行配置文件并通知 Clash 核心重新加载 + let run_path = wrap_err!(Config::generate_file(ConfigType::Run).await)?; + log_err!(CoreManager::global().put_configs_force(run_path).await); + + Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/cmd/save_profile.rs b/clash-verge-rev/src-tauri/src/cmd/save_profile.rs index 9d9bdfa019..b1f75455f2 100644 --- a/clash-verge-rev/src-tauri/src/cmd/save_profile.rs +++ b/clash-verge-rev/src-tauri/src/cmd/save_profile.rs @@ -6,7 +6,7 @@ use crate::{ utils::{dirs, logging::Type}, wrap_err, }; -use std::fs; +use tokio::fs; /// 保存profiles的配置 #[tauri::command] @@ -17,7 +17,7 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR // 在异步操作前完成所有文件操作 let (file_path, original_content, is_merge_file) = { - let profiles = Config::profiles(); + let profiles = Config::profiles().await; let profiles_guard = profiles.latest_ref(); let item = wrap_err!(profiles_guard.get_item(&index))?; // 确定是否为merge类型文件 @@ -29,7 +29,8 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR }; // 保存新的配置文件 - wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?; + let file_data = file_data.ok_or("file_data is None")?; + wrap_err!(fs::write(&file_path, &file_data).await)?; let file_path_str = file_path.to_string_lossy().to_string(); logging!( @@ -87,7 +88,7 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR error_msg ); // 恢复原始配置文件 - wrap_err!(fs::write(&file_path, original_content))?; + wrap_err!(fs::write(&file_path, original_content).await)?; // 发送合并文件专用错误通知 let result = (false, error_msg.clone()); crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件"); @@ -102,7 +103,7 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR e ); // 恢复原始配置文件 - wrap_err!(fs::write(&file_path, original_content))?; + wrap_err!(fs::write(&file_path, original_content).await)?; return Err(e.to_string()); } } @@ -126,7 +127,7 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR error_msg ); // 恢复原始配置文件 - wrap_err!(fs::write(&file_path, original_content))?; + wrap_err!(fs::write(&file_path, original_content).await)?; // 智能判断错误类型 let is_script_error = file_path_str.ends_with(".js") @@ -139,17 +140,29 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR || (!file_path_str.ends_with(".js") && !is_script_error) { // 普通YAML错误使用YAML通知处理 - log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知"); + logging!( + info, + Type::Config, + "[cmd配置save] YAML配置文件验证失败,发送通知" + ); let result = (false, error_msg.clone()); crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件"); } else if is_script_error { // 脚本错误使用专门的通知处理 - log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知"); + logging!( + info, + Type::Config, + "[cmd配置save] 脚本文件验证失败,发送通知" + ); let result = (false, error_msg.clone()); crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件"); } else { // 普通配置错误使用一般通知 - log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知"); + logging!( + info, + Type::Config, + "[cmd配置save] 其他类型验证失败,发送一般通知" + ); handle::Handle::notice_message("config_validate::error", &error_msg); } @@ -164,7 +177,7 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR e ); // 恢复原始配置文件 - wrap_err!(fs::write(&file_path, original_content))?; + wrap_err!(fs::write(&file_path, original_content).await)?; Err(e.to_string()) } } diff --git a/clash-verge-rev/src-tauri/src/cmd/service.rs b/clash-verge-rev/src-tauri/src/cmd/service.rs index c0f5b2fcc6..efb4070de6 100644 --- a/clash-verge-rev/src-tauri/src/cmd/service.rs +++ b/clash-verge-rev/src-tauri/src/cmd/service.rs @@ -1,42 +1,47 @@ use super::CmdResult; use crate::{ - core::{service, CoreManager}, + core::{ + CoreManager, + service::{self, SERVICE_MANAGER, ServiceStatus}, + }, utils::i18n::t, }; -async fn execute_service_operation( - service_op: impl std::future::Future>, - op_type: &str, -) -> CmdResult { - if service_op.await.is_err() { - let emsg = format!("{} {} failed", op_type, "Service"); - return Err(t(emsg.as_str())); +async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult { + if let Err(e) = SERVICE_MANAGER + .lock() + .await + .handle_service_status(&status) + .await + { + let emsg = format!("{} Service failed: {}", op_type, e); + return Err(t(emsg.as_str()).await); } if CoreManager::global().restart_core().await.is_err() { - let emsg = format!("{} {} failed", "Restart", "Core"); - return Err(t(emsg.as_str())); + let emsg = "Restart Core failed"; + return Err(t(emsg).await); } Ok(()) } #[tauri::command] pub async fn install_service() -> CmdResult { - execute_service_operation(service::install_service(), "Install").await + execute_service_operation_sync(ServiceStatus::InstallRequired, "Install").await } #[tauri::command] pub async fn uninstall_service() -> CmdResult { - execute_service_operation(service::uninstall_service(), "Uninstall").await + execute_service_operation_sync(ServiceStatus::UninstallRequired, "Uninstall").await } #[tauri::command] pub async fn reinstall_service() -> CmdResult { - execute_service_operation(service::reinstall_service(), "Reinstall").await + execute_service_operation_sync(ServiceStatus::ReinstallRequired, "Reinstall").await } #[tauri::command] pub async fn repair_service() -> CmdResult { - execute_service_operation(service::force_reinstall_service(), "Repair").await + execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "Repair").await } #[tauri::command] diff --git a/clash-verge-rev/src-tauri/src/cmd/system.rs b/clash-verge-rev/src-tauri/src/cmd/system.rs index 6dcc312ad6..c3d2ab4125 100644 --- a/clash-verge-rev/src-tauri/src/cmd/system.rs +++ b/clash-verge-rev/src-tauri/src/cmd/system.rs @@ -1,7 +1,9 @@ use super::CmdResult; use crate::{ - core::{handle, CoreManager}, + core::{CoreManager, handle}, + logging, module::sysinfo::PlatformSpecification, + utils::logging::Type, }; use once_cell::sync::Lazy; use std::{ @@ -23,20 +25,22 @@ static APP_START_TIME: Lazy = Lazy::new(|| { #[tauri::command] pub async fn export_diagnostic_info() -> CmdResult<()> { - let sysinfo = PlatformSpecification::new_async().await; + let sysinfo = PlatformSpecification::new_sync(); let info = format!("{sysinfo:?}"); - let app_handle = handle::Handle::global().app_handle().unwrap(); + let app_handle = handle::Handle::global() + .app_handle() + .ok_or("Failed to get app handle")?; let cliboard = app_handle.clipboard(); if cliboard.write_text(info).is_err() { - log::error!(target: "app", "Failed to write to clipboard"); + logging!(error, Type::System, "Failed to write to clipboard"); } Ok(()) } #[tauri::command] pub async fn get_system_info() -> CmdResult { - let sysinfo = PlatformSpecification::new_async().await; + let sysinfo = PlatformSpecification::new_sync(); let info = format!("{sysinfo:?}"); Ok(info) } @@ -44,7 +48,7 @@ pub async fn get_system_info() -> CmdResult { /// 获取当前内核运行模式 #[tauri::command] pub async fn get_running_mode() -> Result { - Ok(CoreManager::global().get_running_mode().await.to_string()) + Ok(CoreManager::global().get_running_mode().to_string()) } /// 获取应用的运行时间(毫秒) diff --git a/clash-verge-rev/src-tauri/src/cmd/uwp.rs b/clash-verge-rev/src-tauri/src/cmd/uwp.rs index 28b88bd48d..dc2eb12015 100644 --- a/clash-verge-rev/src-tauri/src/cmd/uwp.rs +++ b/clash-verge-rev/src-tauri/src/cmd/uwp.rs @@ -6,8 +6,8 @@ mod platform { use super::CmdResult; use crate::{core::win_uwp, wrap_err}; - pub async fn invoke_uwp_tool() -> CmdResult { - wrap_err!(win_uwp::invoke_uwptools().await) + pub fn invoke_uwp_tool() -> CmdResult { + wrap_err!(win_uwp::invoke_uwptools()) } } @@ -16,7 +16,7 @@ mod platform { mod platform { use super::CmdResult; - pub async fn invoke_uwp_tool() -> CmdResult { + pub fn invoke_uwp_tool() -> CmdResult { Ok(()) } } @@ -24,5 +24,5 @@ mod platform { /// Command exposed to Tauri #[tauri::command] pub async fn invoke_uwp_tool() -> CmdResult { - platform::invoke_uwp_tool().await + platform::invoke_uwp_tool() } diff --git a/clash-verge-rev/src-tauri/src/cmd/verge.rs b/clash-verge-rev/src-tauri/src/cmd/verge.rs index d183d50956..04f8a65195 100644 --- a/clash-verge-rev/src-tauri/src/cmd/verge.rs +++ b/clash-verge-rev/src-tauri/src/cmd/verge.rs @@ -3,10 +3,14 @@ use crate::{config::*, feat, wrap_err}; /// 获取Verge配置 #[tauri::command] -pub fn get_verge_config() -> CmdResult { - let verge = Config::verge(); - let verge_data = verge.latest_ref().clone(); - Ok(IVergeResponse::from(*verge_data)) +pub async fn get_verge_config() -> CmdResult { + let verge = Config::verge().await; + let verge_data = { + let ref_data = verge.latest_ref(); + ref_data.clone() + }; + let verge_response = IVergeResponse::from(*verge_data); + Ok(verge_response) } /// 修改Verge配置 diff --git a/clash-verge-rev/src-tauri/src/cmd/webdav.rs b/clash-verge-rev/src-tauri/src/cmd/webdav.rs index d9f8573194..7375ea5dbb 100644 --- a/clash-verge-rev/src-tauri/src/cmd/webdav.rs +++ b/clash-verge-rev/src-tauri/src/cmd/webdav.rs @@ -11,11 +11,17 @@ pub async fn save_webdav_config(url: String, username: String, password: String) webdav_password: Some(password), ..IVerge::default() }; - Config::verge().draft_mut().patch_config(patch.clone()); - Config::verge().apply(); Config::verge() - .latest_ref() + .await + .draft_mut() + .patch_config(patch.clone()); + Config::verge().await.apply(); + + // 分离数据获取和异步调用 + let verge_data = Config::verge().await.latest_ref().clone(); + verge_data .save_file() + .await .map_err(|err| err.to_string())?; core::backup::WebDavClient::global().reset(); Ok(()) diff --git a/clash-verge-rev/src-tauri/src/config/clash.rs b/clash-verge-rev/src-tauri/src/config/clash.rs index 81a91d342f..a46874e067 100644 --- a/clash-verge-rev/src-tauri/src/config/clash.rs +++ b/clash-verge-rev/src-tauri/src/config/clash.rs @@ -3,7 +3,7 @@ use crate::utils::dirs::{ipc_path, path_to_str}; use crate::utils::{dirs, help}; use anyhow::Result; use serde::{Deserialize, Serialize}; -use serde_yaml::{Mapping, Value}; +use serde_yaml_ng::{Mapping, Value}; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, str::FromStr, @@ -13,20 +13,29 @@ use std::{ pub struct IClashTemp(pub Mapping); impl IClashTemp { - pub fn new() -> Self { + pub async fn new() -> Self { let template = Self::template(); - match dirs::clash_path().and_then(|path| help::read_mapping(&path)) { + let clash_path_result = dirs::clash_path(); + let map_result = if let Ok(path) = clash_path_result { + help::read_mapping(&path).await + } else { + Err(anyhow::anyhow!("Failed to get clash path")) + }; + + match map_result { Ok(mut map) => { template.0.keys().for_each(|key| { - if !map.contains_key(key) { - map.insert(key.clone(), template.0.get(key).unwrap().clone()); + if !map.contains_key(key) + && let Some(value) = template.0.get(key) + { + map.insert(key.clone(), value.clone()); } }); // 确保 secret 字段存在且不为空 - if let Some(Value::String(s)) = map.get_mut("secret") { - if s.is_empty() { - *s = "set-your-secret".to_string(); - } + if let Some(Value::String(s)) = map.get_mut("secret") + && s.is_empty() + { + *s = "set-your-secret".to_string(); } Self(Self::guard(map)) } @@ -133,12 +142,13 @@ impl IClashTemp { } } - pub fn save_config(&self) -> Result<()> { + pub async fn save_config(&self) -> Result<()> { help::save_yaml( &dirs::clash_path()?, &self.0, Some("# Generated by Clash Verge"), ) + .await } pub fn get_mixed_port(&self) -> u16 { @@ -278,9 +288,10 @@ impl IClashTemp { Self::guard_server_ctrl(config) } - pub fn guard_external_controller_with_setting(config: &Mapping) -> String { + pub async fn guard_external_controller_with_setting(config: &Mapping) -> String { // 检查 enable_external_controller 设置,用于运行时配置生成 let enable_external_controller = Config::verge() + .await .latest_ref() .enable_external_controller .unwrap_or(false); @@ -307,7 +318,13 @@ impl IClashTemp { pub fn guard_external_controller_ipc() -> String { // 总是使用当前的 IPC 路径,确保配置文件与运行时路径一致 - path_to_str(&ipc_path().unwrap()).unwrap().to_string() + ipc_path() + .ok() + .and_then(|path| path_to_str(&path).ok().map(|s| s.to_string())) + .unwrap_or_else(|| { + log::error!(target: "app", "Failed to get IPC path, using default"); + "127.0.0.1:9090".to_string() + }) } } diff --git a/clash-verge-rev/src-tauri/src/config/config.rs b/clash-verge-rev/src-tauri/src/config/config.rs index ef4620872e..9b065b88ba 100644 --- a/clash-verge-rev/src-tauri/src/config/config.rs +++ b/clash-verge-rev/src-tauri/src/config/config.rs @@ -1,15 +1,14 @@ use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge}; use crate::{ - config::PrfItem, - core::{handle, CoreManager}, + config::{PrfItem, profiles_append_item_safe}, + core::{CoreManager, handle}, enhance, logging, - process::AsyncHandler, utils::{dirs, help, logging::Type}, }; -use anyhow::{anyhow, Result}; -use once_cell::sync::OnceCell; +use anyhow::{Result, anyhow}; use std::path::PathBuf; -use tokio::time::{sleep, Duration}; +use tokio::sync::OnceCell; +use tokio::time::{Duration, sleep}; pub const RUNTIME_CONFIG: &str = "clash-verge.yaml"; pub const CHECK_CONFIG: &str = "clash-verge-check.yaml"; @@ -22,54 +21,55 @@ pub struct Config { } impl Config { - pub fn global() -> &'static Config { - static CONFIG: OnceCell = OnceCell::new(); - - CONFIG.get_or_init(|| Config { - clash_config: Draft::from(Box::new(IClashTemp::new())), - verge_config: Draft::from(Box::new(IVerge::new())), - profiles_config: Draft::from(Box::new(IProfiles::new())), - runtime_config: Draft::from(Box::new(IRuntime::new())), - }) + pub async fn global() -> &'static Config { + static CONFIG: OnceCell = OnceCell::const_new(); + CONFIG + .get_or_init(|| async { + Config { + clash_config: Draft::from(Box::new(IClashTemp::new().await)), + verge_config: Draft::from(Box::new(IVerge::new().await)), + profiles_config: Draft::from(Box::new(IProfiles::new().await)), + runtime_config: Draft::from(Box::new(IRuntime::new())), + } + }) + .await } - pub fn clash() -> Draft> { - Self::global().clash_config.clone() + pub async fn clash() -> Draft> { + Self::global().await.clash_config.clone() } - pub fn verge() -> Draft> { - Self::global().verge_config.clone() + pub async fn verge() -> Draft> { + Self::global().await.verge_config.clone() } - pub fn profiles() -> Draft> { - Self::global().profiles_config.clone() + pub async fn profiles() -> Draft> { + Self::global().await.profiles_config.clone() } - pub fn runtime() -> Draft> { - Self::global().runtime_config.clone() + pub async fn runtime() -> Draft> { + Self::global().await.runtime_config.clone() } /// 初始化订阅 pub async fn init_config() -> Result<()> { if Self::profiles() + .await .latest_ref() .get_item(&"Merge".to_string()) .is_err() { let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?; - Self::profiles() - .data_mut() - .append_item(merge_item.clone())?; + profiles_append_item_safe(merge_item.clone()).await?; } if Self::profiles() + .await .latest_ref() .get_item(&"Script".to_string()) .is_err() { let script_item = PrfItem::from_script(Some("Script".to_string()))?; - Self::profiles() - .data_mut() - .append_item(script_item.clone())?; + profiles_append_item_safe(script_item.clone()).await?; } // 生成运行时配置 if let Err(err) = Self::generate().await { @@ -79,7 +79,7 @@ impl Config { } // 生成运行时配置文件并验证 - let config_result = Self::generate_file(ConfigType::Run); + let config_result = Self::generate_file(ConfigType::Run).await; let validation_result = if config_result.is_ok() { // 验证配置文件 @@ -101,7 +101,9 @@ impl Config { Some(("config_validate::boot_error", error_msg)) } else { logging!(info, Type::Config, true, "配置验证成功"); - Some(("config_validate::success", String::new())) + // 前端没有必要知道验证成功的消息,也没有事件驱动 + // Some(("config_validate::success", String::new())) + None } } Err(err) => { @@ -122,30 +124,30 @@ impl Config { // 在单独的任务中发送通知 if let Some((msg_type, msg_content)) = validation_result { - AsyncHandler::spawn(move || async move { - sleep(Duration::from_secs(2)).await; - handle::Handle::notice_message(msg_type, &msg_content); - }); + sleep(Duration::from_secs(2)).await; + handle::Handle::notice_message(msg_type, &msg_content); } Ok(()) } /// 将订阅丢到对应的文件中 - pub fn generate_file(typ: ConfigType) -> Result { + pub async fn generate_file(typ: ConfigType) -> Result { let path = match typ { ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG), ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG), }; - let runtime = Config::runtime(); - let runtime = runtime.latest_ref(); + let runtime = Config::runtime().await; let config = runtime + .latest_ref() .config .as_ref() - .ok_or(anyhow!("failed to get runtime config"))?; + .ok_or(anyhow!("failed to get runtime config"))? + .clone(); + drop(runtime); // 显式释放锁 - help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?; + help::save_yaml(&path, &config, Some("# Generated by Clash Verge")).await?; Ok(path) } @@ -153,7 +155,7 @@ impl Config { pub async fn generate() -> Result<()> { let (config, exists_keys, logs) = enhance::enhance().await; - *Config::runtime().draft_mut() = Box::new(IRuntime { + *Config::runtime().await.draft_mut() = Box::new(IRuntime { config: Some(config), exists_keys, chain_logs: logs, @@ -174,33 +176,33 @@ mod tests { use std::mem; #[test] + #[allow(unused_variables)] + #[allow(clippy::expect_used)] fn test_prfitem_from_merge_size() { - let merge_item = PrfItem::from_merge(Some("Merge".to_string())).unwrap(); - dbg!(&merge_item); + let merge_item = PrfItem::from_merge(Some("Merge".to_string())) + .expect("Failed to create merge item in test"); let prfitem_size = mem::size_of_val(&merge_item); - dbg!(prfitem_size); // Boxed version let boxed_merge_item = Box::new(merge_item); let box_prfitem_size = mem::size_of_val(&boxed_merge_item); - dbg!(box_prfitem_size); // The size of Box is always pointer-sized (usually 8 bytes on 64-bit) // assert_eq!(box_prfitem_size, mem::size_of::>()); assert!(box_prfitem_size < prfitem_size); } #[test] + #[allow(unused_variables)] fn test_draft_size_non_boxed() { let draft = Draft::from(IRuntime::new()); let iruntime_size = std::mem::size_of_val(&draft); - dbg!(iruntime_size); assert_eq!(iruntime_size, std::mem::size_of::>()); } #[test] + #[allow(unused_variables)] fn test_draft_size_boxed() { let draft = Draft::from(Box::new(IRuntime::new())); let box_iruntime_size = std::mem::size_of_val(&draft); - dbg!(box_iruntime_size); assert_eq!( box_iruntime_size, std::mem::size_of::>>() diff --git a/clash-verge-rev/src-tauri/src/config/draft.rs b/clash-verge-rev/src-tauri/src/config/draft.rs index 041c79e65f..ac82d685b9 100644 --- a/clash-verge-rev/src-tauri/src/config/draft.rs +++ b/clash-verge-rev/src-tauri/src/config/draft.rs @@ -46,11 +46,18 @@ impl Draft> { if guard.1.is_none() { let mut guard = RwLockUpgradableReadGuard::upgrade(guard); guard.1 = Some(guard.0.clone()); - return RwLockWriteGuard::map(guard, |inner| inner.1.as_mut().unwrap()); + return RwLockWriteGuard::map(guard, |inner| { + inner.1.as_mut().unwrap_or_else(|| { + unreachable!("Draft was just created above, this should never fail") + }) + }); } // 已存在草稿,升级为写锁映射 RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| { - inner.1.as_mut().unwrap() + inner + .1 + .as_mut() + .unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()")) }) } diff --git a/clash-verge-rev/src-tauri/src/config/encrypt.rs b/clash-verge-rev/src-tauri/src/config/encrypt.rs index 87b9eb85a6..96ba577058 100644 --- a/clash-verge-rev/src-tauri/src/config/encrypt.rs +++ b/clash-verge-rev/src-tauri/src/config/encrypt.rs @@ -1,9 +1,9 @@ use crate::utils::dirs::get_encryption_key; use aes_gcm::{ - aead::{Aead, KeyInit}, Aes256Gcm, Key, + aead::{Aead, KeyInit}, }; -use base64::{engine::general_purpose::STANDARD, Engine}; +use base64::{Engine, engine::general_purpose::STANDARD}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; const NONCE_LENGTH: usize = 12; diff --git a/clash-verge-rev/src-tauri/src/config/mod.rs b/clash-verge-rev/src-tauri/src/config/mod.rs index bbd76770cf..5116875711 100644 --- a/clash-verge-rev/src-tauri/src/config/mod.rs +++ b/clash-verge-rev/src-tauri/src/config/mod.rs @@ -4,7 +4,7 @@ mod config; mod draft; mod encrypt; mod prfitem; -mod profiles; +pub mod profiles; mod runtime; mod verge; diff --git a/clash-verge-rev/src-tauri/src/config/prfitem.rs b/clash-verge-rev/src-tauri/src/config/prfitem.rs index a526cc56ca..bb748501a6 100644 --- a/clash-verge-rev/src-tauri/src/config/prfitem.rs +++ b/clash-verge-rev/src-tauri/src/config/prfitem.rs @@ -3,14 +3,11 @@ use crate::utils::{ network::{NetworkManager, ProxyType}, tmpl, }; -use anyhow::{bail, Context, Result}; -use reqwest::StatusCode; +use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; -use serde_yaml::Mapping; +use serde_yaml_ng::Mapping; use std::{fs, time::Duration}; -use super::Config; - #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct PrfItem { pub uid: Option, @@ -147,12 +144,15 @@ impl PrfItem { bail!("type should not be null"); } - match item.itype.unwrap().as_str() { + let itype = item + .itype + .ok_or_else(|| anyhow::anyhow!("type should not be null"))?; + match itype.as_str() { "remote" => { - if item.url.is_none() { - bail!("url should not be null"); - } - let url = item.url.as_ref().unwrap().as_str(); + let url = item + .url + .as_ref() + .ok_or_else(|| anyhow::anyhow!("url should not be null"))?; let name = item.name; let desc = item.desc; PrfItem::from_url(url, name, desc, item.option).await @@ -160,7 +160,7 @@ impl PrfItem { "local" => { let name = item.name.unwrap_or("Local File".into()); let desc = item.desc.unwrap_or("".into()); - PrfItem::from_local(name, desc, file_data, item.option) + PrfItem::from_local(name, desc, file_data, item.option).await } typ => bail!("invalid profile item type \"{typ}\""), } @@ -168,7 +168,7 @@ impl PrfItem { /// ## Local type /// create a new item from name/desc - pub fn from_local( + pub async fn from_local( name: String, desc: String, file_data: Option, @@ -186,37 +186,27 @@ impl PrfItem { if merge.is_none() { let merge_item = PrfItem::from_merge(None)?; - Config::profiles() - .data_mut() - .append_item(merge_item.clone())?; + crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?; merge = merge_item.uid; } if script.is_none() { let script_item = PrfItem::from_script(None)?; - Config::profiles() - .data_mut() - .append_item(script_item.clone())?; + crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?; script = script_item.uid; } if rules.is_none() { let rules_item = PrfItem::from_rules()?; - Config::profiles() - .data_mut() - .append_item(rules_item.clone())?; + crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?; rules = rules_item.uid; } if proxies.is_none() { let proxies_item = PrfItem::from_proxies()?; - Config::profiles() - .data_mut() - .append_item(proxies_item.clone())?; + crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?; proxies = proxies_item.uid; } if groups.is_none() { let groups_item = PrfItem::from_groups()?; - Config::profiles() - .data_mut() - .append_item(groups_item.clone())?; + crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?; groups = groups_item.uid; } Ok(PrfItem { @@ -275,7 +265,7 @@ impl PrfItem { }; // 使用网络管理器发送请求 - let resp = match NetworkManager::global() + let resp = match NetworkManager::new() .get_with_interrupt( url, proxy_type, @@ -293,7 +283,7 @@ impl PrfItem { }; let status_code = resp.status(); - if !StatusCode::is_success(&status_code) { + if !status_code.is_success() { bail!("failed to fetch remote profile with status {status_code}") } @@ -359,13 +349,13 @@ impl PrfItem { let uid = help::get_uid("R"); let file = format!("{uid}.yaml"); let name = name.unwrap_or(filename.unwrap_or("Remote File".into())); - let data = resp.text_with_charset("utf-8").await?; + let data = resp.text_with_charset()?; // process the charset "UTF-8 with BOM" let data = data.trim_start_matches('\u{feff}'); // check the data whether the valid yaml format - let yaml = serde_yaml::from_str::(data) + let yaml = serde_yaml_ng::from_str::(data) .context("the remote profile data is invalid yaml")?; if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") { @@ -374,37 +364,27 @@ impl PrfItem { if merge.is_none() { let merge_item = PrfItem::from_merge(None)?; - Config::profiles() - .data_mut() - .append_item(merge_item.clone())?; + crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?; merge = merge_item.uid; } if script.is_none() { let script_item = PrfItem::from_script(None)?; - Config::profiles() - .data_mut() - .append_item(script_item.clone())?; + crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?; script = script_item.uid; } if rules.is_none() { let rules_item = PrfItem::from_rules()?; - Config::profiles() - .data_mut() - .append_item(rules_item.clone())?; + crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?; rules = rules_item.uid; } if proxies.is_none() { let proxies_item = PrfItem::from_proxies()?; - Config::profiles() - .data_mut() - .append_item(proxies_item.clone())?; + crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?; proxies = proxies_item.uid; } if groups.is_none() { let groups_item = PrfItem::from_groups()?; - Config::profiles() - .data_mut() - .append_item(groups_item.clone())?; + crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?; groups = groups_item.uid; } @@ -549,22 +529,20 @@ impl PrfItem { /// get the file data pub fn read_file(&self) -> Result { - if self.file.is_none() { - bail!("could not find the file"); - } - - let file = self.file.clone().unwrap(); + let file = self + .file + .clone() + .ok_or_else(|| anyhow::anyhow!("could not find the file"))?; let path = dirs::app_profiles_dir()?.join(file); fs::read_to_string(path).context("failed to read the file") } /// save the file data pub fn save_file(&self, data: String) -> Result<()> { - if self.file.is_none() { - bail!("could not find the file"); - } - - let file = self.file.clone().unwrap(); + let file = self + .file + .clone() + .ok_or_else(|| anyhow::anyhow!("could not find the file"))?; let path = dirs::app_profiles_dir()?.join(file); fs::write(path, data.as_bytes()).context("failed to save the file") } diff --git a/clash-verge-rev/src-tauri/src/config/profiles.rs b/clash-verge-rev/src-tauri/src/config/profiles.rs index c717065be6..0c1e268dac 100644 --- a/clash-verge-rev/src-tauri/src/config/profiles.rs +++ b/clash-verge-rev/src-tauri/src/config/profiles.rs @@ -1,9 +1,14 @@ -use super::{prfitem::PrfItem, PrfOption}; -use crate::utils::{dirs, help}; -use anyhow::{bail, Context, Result}; +use super::{PrfOption, prfitem::PrfItem}; +use crate::{ + logging_error, + process::AsyncHandler, + utils::{dirs, help, logging::Type}, +}; +use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; -use serde_yaml::Mapping; -use std::{collections::HashSet, fs, io::Write}; +use serde_yaml_ng::Mapping; +use std::collections::HashSet; +use tokio::fs; /// Define the `profiles.yaml` schema #[derive(Default, Debug, Clone, Deserialize, Serialize)] @@ -32,22 +37,28 @@ macro_rules! patch { } impl IProfiles { - pub fn new() -> Self { - match dirs::profiles_path().and_then(|path| help::read_yaml::(&path)) { - Ok(mut profiles) => { - if profiles.items.is_none() { - profiles.items = Some(vec![]); - } - // compatible with the old old old version - if let Some(items) = profiles.items.as_mut() { - for item in items.iter_mut() { - if item.uid.is_none() { - item.uid = Some(help::get_uid("d")); + pub async fn new() -> Self { + match dirs::profiles_path() { + Ok(path) => match help::read_yaml::(&path).await { + Ok(mut profiles) => { + if profiles.items.is_none() { + profiles.items = Some(vec![]); + } + // compatible with the old old old version + if let Some(items) = profiles.items.as_mut() { + for item in items.iter_mut() { + if item.uid.is_none() { + item.uid = Some(help::get_uid("d")); + } } } + profiles } - profiles - } + Err(err) => { + log::error!(target: "app", "{err}"); + Self::template() + } + }, Err(err) => { log::error!(target: "app", "{err}"); Self::template() @@ -62,12 +73,13 @@ impl IProfiles { } } - pub fn save_file(&self) -> Result<()> { + pub async fn save_file(&self) -> Result<()> { help::save_yaml( &dirs::profiles_path()?, self, Some("# Profiles Config for Clash Verge"), ) + .await } /// 只修改current,valid和chain @@ -76,8 +88,9 @@ impl IProfiles { self.items = Some(vec![]); } - if let Some(current) = patch.current { - let items = self.items.as_ref().unwrap(); + if let Some(current) = patch.current + && let Some(items) = self.items.as_ref() + { let some_uid = Some(current); if items.iter().any(|e| e.uid == some_uid) { self.current = some_uid; @@ -114,7 +127,7 @@ impl IProfiles { /// append new item /// if the file_data is some /// then should save the data to file - pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> { + pub async fn append_item(&mut self, mut item: PrfItem) -> Result<()> { if item.uid.is_none() { bail!("the uid should not be null"); } @@ -127,12 +140,13 @@ impl IProfiles { bail!("the file should not be null"); } - let file = item.file.clone().unwrap(); + let file = item.file.clone().ok_or_else(|| { + anyhow::anyhow!("file field is required when file_data is provided") + })?; let path = dirs::app_profiles_dir()?.join(&file); - fs::File::create(path) - .with_context(|| format!("failed to create file \"{file}\""))? - .write(file_data.as_bytes()) + fs::write(&path, file_data.as_bytes()) + .await .with_context(|| format!("failed to write to file \"{file}\""))?; } @@ -150,11 +164,11 @@ impl IProfiles { items.push(item) } - self.save_file() + self.save_file().await } /// reorder items - pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> { + pub async fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> { let mut items = self.items.take().unwrap_or_default(); let mut old_index = None; let mut new_index = None; @@ -168,17 +182,18 @@ impl IProfiles { } } - if old_index.is_none() || new_index.is_none() { - return Ok(()); - } - let item = items.remove(old_index.unwrap()); - items.insert(new_index.unwrap(), item); + let (old_idx, new_idx) = match (old_index, new_index) { + (Some(old), Some(new)) => (old, new), + _ => return Ok(()), + }; + let item = items.remove(old_idx); + items.insert(new_idx, item); self.items = Some(items); - self.save_file() + self.save_file().await } /// update the item value - pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { + pub async fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { let mut items = self.items.take().unwrap_or_default(); for each in items.iter_mut() { @@ -194,7 +209,7 @@ impl IProfiles { patch!(each, item, option); self.items = Some(items); - return self.save_file(); + return self.save_file().await; } } @@ -204,7 +219,7 @@ impl IProfiles { /// be used to update the remote item /// only patch `updated` `extra` `file_data` - pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> { + pub async fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> { if self.items.is_none() { self.items = Some(vec![]); } @@ -233,9 +248,8 @@ impl IProfiles { let path = dirs::app_profiles_dir()?.join(&file); - fs::File::create(path) - .with_context(|| format!("failed to create file \"{file}\""))? - .write(file_data.as_bytes()) + fs::write(&path, file_data.as_bytes()) + .await .with_context(|| format!("failed to write to file \"{file}\""))?; } @@ -244,12 +258,12 @@ impl IProfiles { } } - self.save_file() + self.save_file().await } /// delete item /// if delete the current then return true - pub fn delete_item(&mut self, uid: String) -> Result { + pub async fn delete_item(&mut self, uid: String) -> Result { let current = self.current.as_ref().unwrap_or(&uid); let current = current.clone(); let item = self.get_item(&uid)?; @@ -273,15 +287,24 @@ impl IProfiles { break; } } - if let Some(index) = index { - if let Some(file) = items.remove(index).file { - let _ = dirs::app_profiles_dir().map(|path| { - let path = path.join(file); - if path.exists() { - let _ = fs::remove_file(path); + if let Some(index) = index + && let Some(file) = items.remove(index).file + { + let _ = dirs::app_profiles_dir().map(async move |path| { + let path = path.join(file); + if path.exists() { + let result = fs::remove_file(path.clone()).await; + if let Err(err) = result { + logging_error!( + Type::Config, + false, + "[配置文件删除] 删除文件 {} 失败: {}", + path.display(), + err + ); } - }); - } + } + }); } // get the merge index for (i, _) in items.iter().enumerate() { @@ -290,15 +313,24 @@ impl IProfiles { break; } } - if let Some(index) = merge_index { - if let Some(file) = items.remove(index).file { - let _ = dirs::app_profiles_dir().map(|path| { - let path = path.join(file); - if path.exists() { - let _ = fs::remove_file(path); + if let Some(index) = merge_index + && let Some(file) = items.remove(index).file + { + let _ = dirs::app_profiles_dir().map(async move |path| { + let path = path.join(file); + if path.exists() { + let result = fs::remove_file(path.clone()).await; + if let Err(err) = result { + logging_error!( + Type::Config, + false, + "[配置文件删除] 删除文件 {} 失败: {}", + path.display(), + err + ); } - }); - } + } + }); } // get the script index for (i, _) in items.iter().enumerate() { @@ -307,15 +339,24 @@ impl IProfiles { break; } } - if let Some(index) = script_index { - if let Some(file) = items.remove(index).file { - let _ = dirs::app_profiles_dir().map(|path| { - let path = path.join(file); - if path.exists() { - let _ = fs::remove_file(path); + if let Some(index) = script_index + && let Some(file) = items.remove(index).file + { + let _ = dirs::app_profiles_dir().map(async move |path| { + let path = path.join(file); + if path.exists() { + let result = fs::remove_file(path.clone()).await; + if let Err(err) = result { + logging_error!( + Type::Config, + false, + "[配置文件删除] 删除文件 {} 失败: {}", + path.display(), + err + ); } - }); - } + } + }); } // get the rules index for (i, _) in items.iter().enumerate() { @@ -324,15 +365,24 @@ impl IProfiles { break; } } - if let Some(index) = rules_index { - if let Some(file) = items.remove(index).file { - let _ = dirs::app_profiles_dir().map(|path| { - let path = path.join(file); - if path.exists() { - let _ = fs::remove_file(path); + if let Some(index) = rules_index + && let Some(file) = items.remove(index).file + { + let _ = dirs::app_profiles_dir().map(async move |path| { + let path = path.join(file); + if path.exists() { + let result = fs::remove_file(path.clone()).await; + if let Err(err) = result { + logging_error!( + Type::Config, + false, + "[配置文件删除] 删除文件 {} 失败: {}", + path.display(), + err + ); } - }); - } + } + }); } // get the proxies index for (i, _) in items.iter().enumerate() { @@ -341,15 +391,24 @@ impl IProfiles { break; } } - if let Some(index) = proxies_index { - if let Some(file) = items.remove(index).file { - let _ = dirs::app_profiles_dir().map(|path| { - let path = path.join(file); - if path.exists() { - let _ = fs::remove_file(path); + if let Some(index) = proxies_index + && let Some(file) = items.remove(index).file + { + let _ = dirs::app_profiles_dir().map(async move |path| { + let path = path.join(file); + if path.exists() { + let result = fs::remove_file(path.clone()).await; + if let Err(err) = result { + logging_error!( + Type::Config, + false, + "[配置文件删除] 删除文件 {} 失败: {}", + path.display(), + err + ); } - }); - } + } + }); } // get the groups index for (i, _) in items.iter().enumerate() { @@ -358,15 +417,24 @@ impl IProfiles { break; } } - if let Some(index) = groups_index { - if let Some(file) = items.remove(index).file { - let _ = dirs::app_profiles_dir().map(|path| { - let path = path.join(file); - if path.exists() { - let _ = fs::remove_file(path); + if let Some(index) = groups_index + && let Some(file) = items.remove(index).file + { + let _ = dirs::app_profiles_dir().map(async move |path| { + let path = path.join(file); + if path.exists() { + let result = fs::remove_file(path.clone()).await; + if let Err(err) = result { + logging_error!( + Type::Config, + false, + "[配置文件删除] 删除文件 {} 失败: {}", + path.display(), + err + ); } - }); - } + } + }); } // delete the original uid if current == uid { @@ -382,12 +450,12 @@ impl IProfiles { } self.items = Some(items); - self.save_file()?; + self.save_file().await?; Ok(current == uid) } /// 获取current指向的订阅内容 - pub fn current_mapping(&self) -> Result { + pub async fn current_mapping(&self) -> Result { match (self.current.as_ref(), self.items.as_ref()) { (Some(current), Some(items)) => { if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) { @@ -395,7 +463,7 @@ impl IProfiles { Some(file) => dirs::app_profiles_dir()?.join(file), None => bail!("failed to get the file field"), }; - return help::read_mapping(&file_path); + return help::read_mapping(&file_path).await; } bail!("failed to find the current profile \"uid:{current}\""); } @@ -527,25 +595,25 @@ impl IProfiles { total_files += 1; - if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { - if Self::is_profile_file(file_name) { - // 检查是否为全局扩展文件 - if protected_files.contains(file_name) { - log::debug!(target: "app", "保护全局扩展配置文件: {file_name}"); - continue; - } + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) + && Self::is_profile_file(file_name) + { + // 检查是否为全局扩展文件 + if protected_files.contains(file_name) { + log::debug!(target: "app", "保护全局扩展配置文件: {file_name}"); + continue; + } - // 检查是否为活跃文件 - if !active_files.contains(file_name) { - match std::fs::remove_file(&path) { - Ok(_) => { - deleted_files.push(file_name.to_string()); - log::info!(target: "app", "已清理冗余文件: {file_name}"); - } - Err(e) => { - failed_deletions.push(format!("{file_name}: {e}")); - log::warn!(target: "app", "清理文件失败: {file_name} - {e}"); - } + // 检查是否为活跃文件 + if !active_files.contains(file_name) { + match std::fs::remove_file(&path) { + Ok(_) => { + deleted_files.push(file_name.to_string()); + log::info!(target: "app", "已清理冗余文件: {file_name}"); + } + Err(e) => { + failed_deletions.push(format!("{file_name}: {e}")); + log::warn!(target: "app", "清理文件失败: {file_name} - {e}"); } } } @@ -591,50 +659,44 @@ impl IProfiles { } // 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件 - if let Some(itype) = &item.itype { - if itype == "remote" || itype == "local" { - if let Some(option) = &item.option { - // 收集关联的扩展文件 - if let Some(merge_uid) = &option.merge { - if let Ok(merge_item) = self.get_item(merge_uid) { - if let Some(file) = &merge_item.file { - active_files.insert(file.clone()); - } - } - } + if let Some(itype) = &item.itype + && (itype == "remote" || itype == "local") + && let Some(option) = &item.option + { + // 收集关联的扩展文件 + if let Some(merge_uid) = &option.merge + && let Ok(merge_item) = self.get_item(merge_uid) + && let Some(file) = &merge_item.file + { + active_files.insert(file.clone()); + } - if let Some(script_uid) = &option.script { - if let Ok(script_item) = self.get_item(script_uid) { - if let Some(file) = &script_item.file { - active_files.insert(file.clone()); - } - } - } + if let Some(script_uid) = &option.script + && let Ok(script_item) = self.get_item(script_uid) + && let Some(file) = &script_item.file + { + active_files.insert(file.clone()); + } - if let Some(rules_uid) = &option.rules { - if let Ok(rules_item) = self.get_item(rules_uid) { - if let Some(file) = &rules_item.file { - active_files.insert(file.clone()); - } - } - } + if let Some(rules_uid) = &option.rules + && let Ok(rules_item) = self.get_item(rules_uid) + && let Some(file) = &rules_item.file + { + active_files.insert(file.clone()); + } - if let Some(proxies_uid) = &option.proxies { - if let Ok(proxies_item) = self.get_item(proxies_uid) { - if let Some(file) = &proxies_item.file { - active_files.insert(file.clone()); - } - } - } + if let Some(proxies_uid) = &option.proxies + && let Ok(proxies_item) = self.get_item(proxies_uid) + && let Some(file) = &proxies_item.file + { + active_files.insert(file.clone()); + } - if let Some(groups_uid) = &option.groups { - if let Ok(groups_item) = self.get_item(groups_uid) { - if let Some(file) = &groups_item.file { - active_files.insert(file.clone()); - } - } - } - } + if let Some(groups_uid) = &option.groups + && let Ok(groups_item) = self.get_item(groups_uid) + && let Some(file) = &groups_item.file + { + active_files.insert(file.clone()); } } } @@ -667,23 +729,95 @@ impl IProfiles { .unwrap_or(false) }) } - - pub fn auto_cleanup(&self) -> Result<()> { - match self.cleanup_orphaned_files() { - Ok(result) => { - if !result.deleted_files.is_empty() { - log::info!( - target: "app", - "自动清理完成,删除了 {} 个冗余文件", - result.deleted_files.len() - ); - } - Ok(()) - } - Err(e) => { - log::warn!(target: "app", "自动清理失败: {e}"); - Ok(()) - } - } - } +} + +// 特殊的Send-safe helper函数,完全避免跨await持有guard +use crate::config::Config; + +pub async fn profiles_append_item_with_filedata_safe( + item: PrfItem, + file_data: Option, +) -> Result<()> { + AsyncHandler::spawn_blocking(move || { + AsyncHandler::handle().block_on(async { + let item = PrfItem::from(item, file_data).await?; + let profiles = Config::profiles().await; + let mut profiles_guard = profiles.data_mut(); + profiles_guard.append_item(item).await + }) + }) + .await + .map_err(|e| anyhow::anyhow!("Task join error: {}", e))? +} + +pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> { + AsyncHandler::spawn_blocking(move || { + AsyncHandler::handle().block_on(async { + let profiles = Config::profiles().await; + let mut profiles_guard = profiles.data_mut(); + profiles_guard.append_item(item).await + }) + }) + .await + .map_err(|e| anyhow::anyhow!("Task join error: {}", e))? +} + +pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()> { + AsyncHandler::spawn_blocking(move || { + AsyncHandler::handle().block_on(async { + let profiles = Config::profiles().await; + let mut profiles_guard = profiles.data_mut(); + profiles_guard.patch_item(index, item).await + }) + }) + .await + .map_err(|e| anyhow::anyhow!("Task join error: {}", e))? +} + +pub async fn profiles_delete_item_safe(index: String) -> Result { + AsyncHandler::spawn_blocking(move || { + AsyncHandler::handle().block_on(async { + let profiles = Config::profiles().await; + let mut profiles_guard = profiles.data_mut(); + profiles_guard.delete_item(index).await + }) + }) + .await + .map_err(|e| anyhow::anyhow!("Task join error: {}", e))? +} + +pub async fn profiles_reorder_safe(active_id: String, over_id: String) -> Result<()> { + AsyncHandler::spawn_blocking(move || { + AsyncHandler::handle().block_on(async { + let profiles = Config::profiles().await; + let mut profiles_guard = profiles.data_mut(); + profiles_guard.reorder(active_id, over_id).await + }) + }) + .await + .map_err(|e| anyhow::anyhow!("Task join error: {}", e))? +} + +pub async fn profiles_save_file_safe() -> Result<()> { + AsyncHandler::spawn_blocking(move || { + AsyncHandler::handle().block_on(async { + let profiles = Config::profiles().await; + let profiles_guard = profiles.data_mut(); + profiles_guard.save_file().await + }) + }) + .await + .map_err(|e| anyhow::anyhow!("Task join error: {}", e))? +} + +pub async fn profiles_draft_update_item_safe(index: String, item: PrfItem) -> Result<()> { + AsyncHandler::spawn_blocking(move || { + AsyncHandler::handle().block_on(async { + let profiles = Config::profiles().await; + let mut profiles_guard = profiles.draft_mut(); + profiles_guard.update_item(index, item).await + }) + }) + .await + .map_err(|e| anyhow::anyhow!("Task join error: {}", e))? } diff --git a/clash-verge-rev/src-tauri/src/config/runtime.rs b/clash-verge-rev/src-tauri/src/config/runtime.rs index 66b3fec2f7..7113402b4e 100644 --- a/clash-verge-rev/src-tauri/src/config/runtime.rs +++ b/clash-verge-rev/src-tauri/src/config/runtime.rs @@ -1,6 +1,6 @@ use crate::enhance::field::use_keys; use serde::{Deserialize, Serialize}; -use serde_yaml::{Mapping, Value}; +use serde_yaml_ng::{Mapping, Value}; use std::collections::HashMap; #[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct IRuntime { @@ -46,4 +46,76 @@ impl IRuntime { } } } + + //跟新链式代理配置文件 + /// { + /// "proxies":[ + /// { + /// name : 入口节点, + /// type: xxx + /// server: xxx + /// port: xxx + /// ports: xxx + /// password: xxx + /// skip-cert-verify: xxx, + /// }, + /// { + /// name : hop_node_1_xxxx, + /// type: xxx + /// server: xxx + /// port: xxx + /// ports: xxx + /// password: xxx + /// skip-cert-verify: xxx, + /// dialer-proxy : "入口节点" + /// }, + /// { + /// name : 出口节点, + /// type: xxx + /// server: xxx + /// port: xxx + /// ports: xxx + /// password: xxx + /// skip-cert-verify: xxx, + /// dialer-proxy : "hop_node_1_xxxx" + /// } + /// ], + /// "proxy-groups" : [ + /// { + /// name : "proxy_chain", + /// type: "select", + /// proxies ["出口节点"] + /// } + /// ] + /// } + /// + /// 传入none 为删除 + pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option) { + if let Some(config) = self.config.as_mut() { + if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") { + proxies.iter_mut().for_each(|proxy| { + if let Some(proxy) = proxy.as_mapping_mut() + && proxy.get("dialer-proxy").is_some() + { + proxy.remove("dialer-proxy"); + } + }); + } + + if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config + && let Some(Value::Sequence(proxies)) = config.get_mut("proxies") + { + for (i, dialer_proxy) in dialer_proxies.iter().enumerate() { + if let Some(Value::Mapping(proxy)) = proxies + .iter_mut() + .find(|proxy| proxy.get("name") == Some(dialer_proxy)) + && i != 0 + && let Some(dialer_proxy) = dialer_proxies.get(i - 1) + { + proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned()); + } + } + } + } + } } diff --git a/clash-verge-rev/src-tauri/src/config/verge.rs b/clash-verge-rev/src-tauri/src/config/verge.rs index b22d801ca5..4a02bc9743 100644 --- a/clash-verge-rev/src-tauri/src/config/verge.rs +++ b/clash-verge-rev/src-tauri/src/config/verge.rs @@ -1,5 +1,5 @@ use crate::{ - config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC}, + config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted}, logging, utils::{dirs, help, i18n, logging::Type}, }; @@ -14,6 +14,12 @@ pub struct IVerge { /// silent | error | warn | info | debug | trace pub app_log_level: Option, + /// app log max size in KB + pub app_log_max_size: Option, + + /// app log max count + pub app_log_max_count: Option, + // i18n pub language: Option, @@ -138,9 +144,6 @@ pub struct IVerge { /// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 pub auto_log_clean: Option, - /// 是否启用随机端口 - pub enable_random_port: Option, - /// verge 的各种 port 用于覆盖 clash 的各种 port #[cfg(not(target_os = "windows"))] pub verge_redir_port: Option, @@ -206,9 +209,6 @@ pub struct IVerge { /// 启用外部控制器 pub enable_external_controller: Option, - - /// 服务状态跟踪 - pub service_state: Option, } #[derive(Default, Debug, Clone, Deserialize, Serialize)] @@ -240,9 +240,9 @@ impl IVerge { pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"]; /// 验证并修正配置文件中的clash_core值 - pub fn validate_and_fix_config() -> Result<()> { + pub async fn validate_and_fix_config() -> Result<()> { let config_path = dirs::verge_path()?; - let mut config = match help::read_yaml::(&config_path) { + let mut config = match help::read_yaml::(&config_path).await { Ok(config) => config, Err(_) => Self::template(), }; @@ -276,7 +276,7 @@ impl IVerge { // 修正后保存配置 if needs_fix { logging!(info, Type::Config, true, "正在保存修正后的配置文件..."); - help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?; + help::save_yaml(&config_path, &config, Some("# Clash Verge Config")).await?; logging!( info, Type::Config, @@ -284,7 +284,7 @@ impl IVerge { "配置文件修正完成,需要重新加载配置" ); - Self::reload_config_after_fix(config)?; + Self::reload_config_after_fix(config).await?; } else { logging!( info, @@ -299,10 +299,10 @@ impl IVerge { } /// 配置修正后重新加载配置 - fn reload_config_after_fix(updated_config: IVerge) -> Result<()> { + async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> { use crate::config::Config; - let config_draft = Config::verge(); + let config_draft = Config::verge().await; *config_draft.draft_mut() = Box::new(updated_config.clone()); config_draft.apply(); @@ -338,9 +338,15 @@ impl IVerge { } } - pub fn new() -> Self { - match dirs::verge_path().and_then(|path| help::read_yaml::(&path)) { - Ok(config) => config, + pub async fn new() -> Self { + match dirs::verge_path() { + Ok(path) => match help::read_yaml::(&path).await { + Ok(config) => config, + Err(err) => { + log::error!(target: "app", "{err}"); + Self::template() + } + }, Err(err) => { log::error!(target: "app", "{err}"); Self::template() @@ -350,6 +356,8 @@ impl IVerge { pub fn template() -> Self { Self { + app_log_max_size: Some(128), + app_log_max_count: Some(8), clash_core: Some("verge-mihomo".into()), language: Some(Self::get_system_language()), theme_mode: Some("system".into()), @@ -374,7 +382,6 @@ impl IVerge { proxy_auto_config: Some(false), pac_file_content: Some(DEFAULT_PAC.into()), proxy_host: Some("127.0.0.1".into()), - enable_random_port: Some(false), #[cfg(not(target_os = "windows"))] verge_redir_port: Some(7895), #[cfg(not(target_os = "windows"))] @@ -394,7 +401,7 @@ impl IVerge { auto_close_connection: Some(true), auto_check_update: Some(true), enable_builtin_enhanced: Some(true), - auto_log_clean: Some(2), + auto_log_clean: Some(2), // 1: 1天, 2: 7天, 3: 30天, 4: 90天 webdav_url: None, webdav_username: None, webdav_password: None, @@ -405,15 +412,14 @@ impl IVerge { auto_light_weight_minutes: Some(10), enable_dns_settings: Some(false), home_cards: None, - service_state: None, enable_external_controller: Some(false), ..Self::default() } } /// Save IVerge App Config - pub fn save_file(&self) -> Result<()> { - help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")) + pub async fn save_file(&self) -> Result<()> { + help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")).await } /// patch verge config @@ -428,6 +434,9 @@ impl IVerge { } patch!(app_log_level); + patch!(app_log_max_size); + patch!(app_log_max_count); + patch!(language); patch!(theme_mode); patch!(tray_event); @@ -448,7 +457,6 @@ impl IVerge { patch!(enable_auto_launch); patch!(enable_silent_start); patch!(enable_hover_jump_navigator); - patch!(enable_random_port); #[cfg(not(target_os = "windows"))] patch!(verge_redir_port); #[cfg(not(target_os = "windows"))] @@ -494,7 +502,6 @@ impl IVerge { patch!(auto_light_weight_minutes); patch!(enable_dns_settings); patch!(home_cards); - patch!(service_state); patch!(enable_external_controller); } @@ -528,6 +535,8 @@ impl IVerge { #[derive(Debug, Clone, Serialize)] pub struct IVergeResponse { pub app_log_level: Option, + pub app_log_max_size: Option, + pub app_log_max_count: Option, pub language: Option, pub theme_mode: Option, pub tray_event: Option, @@ -567,7 +576,6 @@ pub struct IVergeResponse { pub proxy_layout_column: Option, pub test_list: Option>, pub auto_log_clean: Option, - pub enable_random_port: Option, #[cfg(not(target_os = "windows"))] pub verge_redir_port: Option, #[cfg(not(target_os = "windows"))] @@ -592,7 +600,6 @@ pub struct IVergeResponse { pub home_cards: Option, pub enable_hover_jump_navigator: Option, pub enable_external_controller: Option, - pub service_state: Option, } impl From for IVergeResponse { @@ -601,6 +608,8 @@ impl From for IVergeResponse { let valid_clash_core = verge.get_valid_clash_core(); Self { app_log_level: verge.app_log_level, + app_log_max_size: verge.app_log_max_size, + app_log_max_count: verge.app_log_max_count, language: verge.language, theme_mode: verge.theme_mode, tray_event: verge.tray_event, @@ -640,7 +649,6 @@ impl From for IVergeResponse { proxy_layout_column: verge.proxy_layout_column, test_list: verge.test_list, auto_log_clean: verge.auto_log_clean, - enable_random_port: verge.enable_random_port, #[cfg(not(target_os = "windows"))] verge_redir_port: verge.verge_redir_port, #[cfg(not(target_os = "windows"))] @@ -665,7 +673,6 @@ impl From for IVergeResponse { home_cards: verge.home_cards, enable_hover_jump_navigator: verge.enable_hover_jump_navigator, enable_external_controller: verge.enable_external_controller, - service_state: verge.service_state, } } } diff --git a/clash-verge-rev/src-tauri/src/core/async_proxy_query.rs b/clash-verge-rev/src-tauri/src/core/async_proxy_query.rs index a6a20cc1f6..304d91d3fc 100644 --- a/clash-verge-rev/src-tauri/src/core/async_proxy_query.rs +++ b/clash-verge-rev/src-tauri/src/core/async_proxy_query.rs @@ -1,6 +1,8 @@ +#[cfg(target_os = "windows")] +use crate::process::AsyncHandler; use anyhow::Result; use serde::{Deserialize, Serialize}; -use tokio::time::{timeout, Duration}; +use tokio::time::{Duration, timeout}; #[cfg(target_os = "linux")] use anyhow::anyhow; @@ -26,7 +28,7 @@ impl Default for AsyncSysproxy { Self { enable: false, host: "127.0.0.1".to_string(), - port: 7890, + port: 7897, bypass: String::new(), } } @@ -74,7 +76,7 @@ impl AsyncProxyQuery { #[cfg(target_os = "windows")] async fn get_auto_proxy_impl() -> Result { // Windows: 从注册表读取PAC配置 - tokio::task::spawn_blocking(move || -> Result { + AsyncHandler::spawn_blocking(move || -> Result { Self::get_pac_config_from_registry() }) .await? @@ -85,7 +87,7 @@ impl AsyncProxyQuery { use std::ptr; use winapi::shared::minwindef::{DWORD, HKEY}; use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; - use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER}; + use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW}; unsafe { let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" @@ -207,13 +209,13 @@ impl AsyncProxyQuery { // Linux: 检查环境变量和GNOME设置 // 首先检查环境变量 - if let Ok(auto_proxy) = std::env::var("auto_proxy") { - if !auto_proxy.is_empty() { - return Ok(AsyncAutoproxy { - enable: true, - url: auto_proxy, - }); - } + if let Ok(auto_proxy) = std::env::var("auto_proxy") + && !auto_proxy.is_empty() + { + return Ok(AsyncAutoproxy { + enable: true, + url: auto_proxy, + }); } // 尝试使用 gsettings 获取 GNOME 代理设置 @@ -222,31 +224,31 @@ impl AsyncProxyQuery { .output() .await; - if let Ok(output) = output { - if output.status.success() { - let mode = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if mode.contains("auto") { - // 获取 PAC URL - let pac_output = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy", "autoconfig-url"]) - .output() - .await; + if let Ok(output) = output + && output.status.success() + { + let mode = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if mode.contains("auto") { + // 获取 PAC URL + let pac_output = Command::new("gsettings") + .args(["get", "org.gnome.system.proxy", "autoconfig-url"]) + .output() + .await; - if let Ok(pac_output) = pac_output { - if pac_output.status.success() { - let pac_url = String::from_utf8_lossy(&pac_output.stdout) - .trim() - .trim_matches('\'') - .trim_matches('"') - .to_string(); + if let Ok(pac_output) = pac_output + && pac_output.status.success() + { + let pac_url = String::from_utf8_lossy(&pac_output.stdout) + .trim() + .trim_matches('\'') + .trim_matches('"') + .to_string(); - if !pac_url.is_empty() { - return Ok(AsyncAutoproxy { - enable: true, - url: pac_url, - }); - } - } + if !pac_url.is_empty() { + return Ok(AsyncAutoproxy { + enable: true, + url: pac_url, + }); } } } @@ -258,7 +260,7 @@ impl AsyncProxyQuery { #[cfg(target_os = "windows")] async fn get_system_proxy_impl() -> Result { // Windows: 使用注册表直接读取代理设置 - tokio::task::spawn_blocking(move || -> Result { + AsyncHandler::spawn_blocking(move || -> Result { Self::get_system_proxy_from_registry() }) .await? @@ -269,7 +271,7 @@ impl AsyncProxyQuery { use std::ptr; use winapi::shared::minwindef::{DWORD, HKEY}; use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; - use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER}; + use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW}; unsafe { let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" @@ -400,10 +402,10 @@ impl AsyncProxyQuery { http_host = host_part.trim().to_string(); } } else if line.contains("HTTPPort") { - if let Some(port_part) = line.split(':').nth(1) { - if let Ok(port) = port_part.trim().parse::() { - http_port = port; - } + if let Some(port_part) = line.split(':').nth(1) + && let Ok(port) = port_part.trim().parse::() + { + http_port = port; } } else if line.contains("ExceptionsList") { // 解析异常列表 @@ -429,16 +431,16 @@ impl AsyncProxyQuery { // Linux: 检查环境变量和桌面环境设置 // 首先检查环境变量 - if let Ok(http_proxy) = std::env::var("http_proxy") { - if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) { - return Ok(proxy_info); - } + if let Ok(http_proxy) = std::env::var("http_proxy") + && let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) + { + return Ok(proxy_info); } - if let Ok(https_proxy) = std::env::var("https_proxy") { - if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) { - return Ok(proxy_info); - } + if let Ok(https_proxy) = std::env::var("https_proxy") + && let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) + { + return Ok(proxy_info); } // 尝试使用 gsettings 获取 GNOME 代理设置 @@ -447,45 +449,46 @@ impl AsyncProxyQuery { .output() .await; - if let Ok(mode_output) = mode_output { - if mode_output.status.success() { - let mode = String::from_utf8_lossy(&mode_output.stdout) - .trim() - .to_string(); - if mode.contains("manual") { - // 获取HTTP代理设置 - let host_result = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy.http", "host"]) - .output() - .await; + if let Ok(mode_output) = mode_output + && mode_output.status.success() + { + let mode = String::from_utf8_lossy(&mode_output.stdout) + .trim() + .to_string(); + if mode.contains("manual") { + // 获取HTTP代理设置 + let host_result = Command::new("gsettings") + .args(["get", "org.gnome.system.proxy.http", "host"]) + .output() + .await; - let port_result = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy.http", "port"]) - .output() - .await; + let port_result = Command::new("gsettings") + .args(["get", "org.gnome.system.proxy.http", "port"]) + .output() + .await; - if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) { - if host_output.status.success() && port_output.status.success() { - let host = String::from_utf8_lossy(&host_output.stdout) - .trim() - .trim_matches('\'') - .trim_matches('"') - .to_string(); + if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) + && host_output.status.success() + && port_output.status.success() + { + let host = String::from_utf8_lossy(&host_output.stdout) + .trim() + .trim_matches('\'') + .trim_matches('"') + .to_string(); - let port = String::from_utf8_lossy(&port_output.stdout) - .trim() - .parse::() - .unwrap_or(8080); + let port = String::from_utf8_lossy(&port_output.stdout) + .trim() + .parse::() + .unwrap_or(8080); - if !host.is_empty() { - return Ok(AsyncSysproxy { - enable: true, - host, - port, - bypass: String::new(), - }); - } - } + if !host.is_empty() { + return Ok(AsyncSysproxy { + enable: true, + host, + port, + bypass: String::new(), + }); } } } diff --git a/clash-verge-rev/src-tauri/src/core/backup.rs b/clash-verge-rev/src-tauri/src/core/backup.rs index 47052eef9f..3f9cfba42a 100644 --- a/clash-verge-rev/src-tauri/src/core/backup.rs +++ b/clash-verge-rev/src-tauri/src/core/backup.rs @@ -74,11 +74,14 @@ impl WebDavClient { // 获取或创建配置 let config = { - let mut lock = self.config.lock(); - if let Some(cfg) = lock.as_ref() { - cfg.clone() + // 首先检查是否已有配置 + let existing_config = self.config.lock().as_ref().cloned(); + + if let Some(cfg) = existing_config { + cfg } else { - let verge = Config::verge().latest_ref().clone(); + // 释放锁后获取异步配置 + let verge = Config::verge().await.latest_ref().clone(); if verge.webdav_url.is_none() || verge.webdav_username.is_none() || verge.webdav_password.is_none() @@ -97,7 +100,8 @@ impl WebDavClient { password: verge.webdav_password.unwrap_or_default(), }; - *lock = Some(config.clone()); + // 重新获取锁并存储配置 + *self.config.lock() = Some(config.clone()); config } }; @@ -117,8 +121,7 @@ impl WebDavClient { attempt.follow() } })) - .build() - .unwrap(), + .build()?, ) .set_host(config.url) .set_auth(reqwest_dav::Auth::Basic(config.username, config.password)) @@ -243,12 +246,17 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> { let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) { for entry in entries { - let entry = entry.unwrap(); + let entry = entry?; let path = entry.path(); if path.is_file() { - let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap()); + let file_name_os = entry.file_name(); + let file_name = file_name_os + .to_str() + .ok_or_else(|| anyhow::Error::msg("Invalid file name encoding"))?; + let backup_path = format!("profiles/{}", file_name); zip.start_file(backup_path, options)?; - zip.write_all(fs::read(path).unwrap().as_slice())?; + let file_content = fs::read(&path)?; + zip.write_all(&file_content)?; } } } @@ -256,14 +264,14 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> { zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?; let mut verge_config: serde_json::Value = - serde_yaml::from_str(&fs::read_to_string(dirs::verge_path()?)?)?; + serde_yaml_ng::from_str(&fs::read_to_string(dirs::verge_path()?)?)?; if let Some(obj) = verge_config.as_object_mut() { obj.remove("webdav_username"); obj.remove("webdav_password"); obj.remove("webdav_url"); } zip.start_file(dirs::VERGE_CONFIG, options)?; - zip.write_all(serde_yaml::to_string(&verge_config)?.as_bytes())?; + zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?; zip.start_file(dirs::PROFILE_YAML, options)?; zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?; diff --git a/clash-verge-rev/src-tauri/src/core/core.rs b/clash-verge-rev/src-tauri/src/core/core.rs index be5f2c493a..ff1e1cc0d2 100644 --- a/clash-verge-rev/src-tauri/src/core/core.rs +++ b/clash-verge-rev/src-tauri/src/core/core.rs @@ -1,11 +1,12 @@ +use crate::AsyncHandler; use crate::{ config::*, core::{ handle, - service::{self}, + service::{self, SERVICE_MANAGER, ServiceStatus}, }, ipc::IpcManager, - logging, logging_error, + logging, logging_error, singleton_lazy, utils::{ dirs, help::{self}, @@ -14,16 +15,15 @@ use crate::{ }; use anyhow::Result; use chrono::Local; -use once_cell::sync::OnceCell; +use parking_lot::Mutex; use std::{ fmt, - fs::{create_dir_all, File}, + fs::{File, create_dir_all}, io::Write, path::PathBuf, sync::Arc, }; -use tauri_plugin_shell::{process::CommandChild, ShellExt}; -use tokio::sync::Mutex; +use tauri_plugin_shell::{ShellExt, process::CommandChild}; #[derive(Debug)] pub struct CoreManager { @@ -138,23 +138,23 @@ impl CoreManager { /// 使用默认配置 pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> { let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG); - *Config::runtime().draft_mut() = Box::new(IRuntime { - config: Some(Config::clash().latest_ref().0.clone()), + + // Extract clash config before async operations + let clash_config = Config::clash().await.latest_ref().0.clone(); + + *Config::runtime().await.draft_mut() = Box::new(IRuntime { + config: Some(clash_config.clone()), exists_keys: vec![], chain_logs: Default::default(), }); - help::save_yaml( - &runtime_path, - &Config::clash().latest_ref().0, - Some("# Clash Verge Runtime"), - )?; + help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?; handle::Handle::notice_message(msg_type, msg_content); Ok(()) } /// 验证运行时配置 pub async fn validate_config(&self) -> Result<(bool, String)> { logging!(info, Type::Config, true, "生成临时配置文件用于验证"); - let config_path = Config::generate_file(ConfigType::Check)?; + let config_path = Config::generate_file(ConfigType::Check).await?; let config_path = dirs::path_to_str(&config_path)?; self.validate_config_internal(config_path).await } @@ -186,7 +186,7 @@ impl CoreManager { "检测到Merge文件,仅进行语法检查: {}", config_path ); - return self.validate_file_syntax(config_path).await; + return self.validate_file_syntax(config_path); } // 检查是否为脚本文件 @@ -218,7 +218,7 @@ impl CoreManager { "检测到脚本文件,使用JavaScript验证: {}", config_path ); - return self.validate_script_file(config_path).await; + return self.validate_script_file(config_path); } // 对YAML配置文件使用Clash内核验证 @@ -247,10 +247,14 @@ impl CoreManager { config_path ); - let clash_core = Config::verge().latest_ref().get_valid_clash_core(); + let clash_core = Config::verge().await.latest_ref().get_valid_clash_core(); logging!(info, Type::Config, true, "使用内核: {}", clash_core); - let app_handle = handle::Handle::global().app_handle().unwrap(); + let app_handle = handle::Handle::global().app_handle().ok_or_else(|| { + let msg = "Failed to get app handle"; + logging!(error, Type::Core, true, "{}", msg); + anyhow::anyhow!(msg) + })?; let app_dir = dirs::app_home_dir()?; let app_dir_str = dirs::path_to_str(&app_dir)?; logging!(info, Type::Config, true, "验证目录: {}", app_dir_str); @@ -298,7 +302,7 @@ impl CoreManager { } } /// 只进行文件语法检查,不进行完整验证 - async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> { + fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> { logging!(info, Type::Config, true, "开始检查文件: {}", config_path); // 读取文件内容 @@ -312,7 +316,7 @@ impl CoreManager { }; // 对YAML文件尝试解析,只检查语法正确性 logging!(info, Type::Config, true, "进行YAML语法检查"); - match serde_yaml::from_str::(&content) { + match serde_yaml_ng::from_str::(&content) { Ok(_) => { logging!(info, Type::Config, true, "YAML语法检查通过"); Ok((true, String::new())) @@ -326,7 +330,7 @@ impl CoreManager { } } /// 验证脚本文件语法 - async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> { + fn validate_script_file(&self, path: &str) -> Result<(bool, String)> { // 读取脚本内容 let content = match std::fs::read_to_string(path) { Ok(content) => content, @@ -379,8 +383,6 @@ impl CoreManager { return Ok((true, String::new())); } - logging!(info, Type::Config, true, "开始更新配置"); - // 1. 先生成新的配置内容 logging!(info, Type::Config, true, "生成新的配置内容"); Config::generate().await?; @@ -388,21 +390,20 @@ impl CoreManager { // 2. 验证配置 match self.validate_config().await { Ok((true, _)) => { - logging!(info, Type::Config, true, "配置验证通过"); // 4. 验证通过后,生成正式的运行时配置 - logging!(info, Type::Config, true, "生成运行时配置"); - let run_path = Config::generate_file(ConfigType::Run)?; + logging!(info, Type::Config, true, "配置验证通过, 生成运行时配置"); + let run_path = Config::generate_file(ConfigType::Run).await?; logging_error!(Type::Config, true, self.put_configs_force(run_path).await); Ok((true, "something".into())) } Ok((false, error_msg)) => { logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg); - Config::runtime().discard(); + Config::runtime().await.discard(); Ok((false, error_msg)) } Err(e) => { logging!(warn, Type::Config, true, "验证过程发生错误: {}", e); - Config::runtime().discard(); + Config::runtime().await.discard(); Err(e) } } @@ -415,13 +416,13 @@ impl CoreManager { }); match IpcManager::global().put_configs_force(run_path_str?).await { Ok(_) => { - Config::runtime().apply(); + Config::runtime().await.apply(); logging!(info, Type::Core, true, "Configuration updated successfully"); Ok(()) } Err(e) => { let msg = e.to_string(); - Config::runtime().discard(); + Config::runtime().await.discard(); logging_error!(Type::Core, true, "Failed to update configuration: {}", msg); Err(msg) } @@ -436,7 +437,7 @@ impl CoreManager { // 获取当前管理的进程 PID let current_pid = { - let child_guard = self.child_sidecar.lock().await; + let child_guard = self.child_sidecar.lock(); child_guard.as_ref().map(|child| child.pid()) }; @@ -462,18 +463,18 @@ impl CoreManager { Ok((pids, process_name)) => { for pid in pids { // 跳过当前管理的进程 - if let Some(current) = current_pid { - if pid == current { - logging!( - debug, - Type::Core, - true, - "跳过当前管理的进程: {} (PID: {})", - process_name, - pid - ); - continue; - } + if let Some(current) = current_pid + && pid == current + { + logging!( + debug, + Type::Core, + true, + "跳过当前管理的进程: {} (PID: {})", + process_name, + pid + ); + continue; } pids_to_kill.push((pid, process_name.clone())); } @@ -522,13 +523,13 @@ impl CoreManager { use std::mem; use winapi::um::handleapi::CloseHandle; use winapi::um::tlhelp32::{ - CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, + CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS, }; use winapi::um::winnt::HANDLE; let process_name_clone = process_name.clone(); - let pids = tokio::task::spawn_blocking(move || -> Result> { + let pids = AsyncHandler::spawn_blocking(move || -> Result> { let mut pids = Vec::new(); unsafe { @@ -623,7 +624,7 @@ impl CoreManager { use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess}; use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE}; - tokio::task::spawn_blocking(move || -> bool { + AsyncHandler::spawn_blocking(move || -> bool { unsafe { let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid); if process_handle.is_null() { @@ -698,7 +699,7 @@ impl CoreManager { use winapi::um::processthreadsapi::OpenProcess; use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION}; - let result = tokio::task::spawn_blocking(move || -> Result { + AsyncHandler::spawn_blocking(move || -> Result { unsafe { let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid); if process_handle.is_null() { @@ -714,9 +715,7 @@ impl CoreManager { Ok(exit_code == 259) } }) - .await?; - - result + .await? } #[cfg(not(windows))] @@ -731,12 +730,13 @@ impl CoreManager { } async fn start_core_by_sidecar(&self) -> Result<()> { - logging!(trace, Type::Core, true, "Running core by sidecar"); - let config_file = &Config::generate_file(ConfigType::Run)?; + logging!(info, Type::Core, true, "Running core by sidecar"); + + let config_file = &Config::generate_file(ConfigType::Run).await?; let app_handle = handle::Handle::global() .app_handle() .ok_or(anyhow::anyhow!("failed to get app handle"))?; - let clash_core = Config::verge().latest_ref().get_valid_clash_core(); + let clash_core = Config::verge().await.latest_ref().get_valid_clash_core(); let config_dir = dirs::app_home_dir()?; let service_log_dir = dirs::app_home_dir()?.join("logs").join("service"); @@ -747,8 +747,6 @@ impl CoreManager { let log_path = service_log_dir.join(format!("sidecar_{timestamp}.log")); - let mut log_file = File::create(log_path)?; - let (mut rx, child) = app_handle .shell() .sidecar(&clash_core)? @@ -760,22 +758,6 @@ impl CoreManager { ]) .spawn()?; - tokio::spawn(async move { - while let Some(event) = rx.recv().await { - if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event { - if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) { - logging!( - error, - Type::Core, - true, - "[Sidecar] Failed to write stdout to file: {}", - e - ); - } - } - } - }); - let pid = child.pid(); logging!( trace, @@ -784,14 +766,36 @@ impl CoreManager { "Started core by sidecar pid: {}", pid ); - *self.child_sidecar.lock().await = Some(child); - self.set_running_mode(RunningMode::Sidecar).await; + *self.child_sidecar.lock() = Some(child); + self.set_running_mode(RunningMode::Sidecar); + + let mut log_file = std::io::BufWriter::new(File::create(log_path)?); + AsyncHandler::spawn(|| async move { + while let Some(event) = rx.recv().await { + match event { + tauri_plugin_shell::process::CommandEvent::Stdout(line) => { + if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) { + eprintln!("[Sidecar] write stdout failed: {e}"); + } + } + tauri_plugin_shell::process::CommandEvent::Stderr(line) => { + let _ = writeln!(log_file, "[stderr] {}", String::from_utf8_lossy(&line)); + } + tauri_plugin_shell::process::CommandEvent::Terminated(term) => { + let _ = writeln!(log_file, "[terminated] {:?}", term); + break; + } + _ => {} + } + } + }); + Ok(()) } - async fn stop_core_by_sidecar(&self) -> Result<()> { - logging!(trace, Type::Core, true, "Stopping core by sidecar"); + fn stop_core_by_sidecar(&self) -> Result<()> { + logging!(info, Type::Core, true, "Stopping core by sidecar"); - if let Some(child) = self.child_sidecar.lock().await.take() { + if let Some(child) = self.child_sidecar.lock().take() { let pid = child.pid(); child.kill()?; logging!( @@ -802,83 +806,39 @@ impl CoreManager { pid ); } - self.set_running_mode(RunningMode::NotRunning).await; + self.set_running_mode(RunningMode::NotRunning); Ok(()) } } impl CoreManager { async fn start_core_by_service(&self) -> Result<()> { - logging!(trace, Type::Core, true, "Running core by service"); - let config_file = &Config::generate_file(ConfigType::Run)?; + logging!(info, Type::Core, true, "Running core by service"); + let config_file = &Config::generate_file(ConfigType::Run).await?; service::run_core_by_service(config_file).await?; - self.set_running_mode(RunningMode::Service).await; + self.set_running_mode(RunningMode::Service); Ok(()) } async fn stop_core_by_service(&self) -> Result<()> { - logging!(trace, Type::Core, true, "Stopping core by service"); + logging!(info, Type::Core, true, "Stopping core by service"); service::stop_core_by_service().await?; - self.set_running_mode(RunningMode::NotRunning).await; + self.set_running_mode(RunningMode::NotRunning); Ok(()) } } -impl CoreManager { - pub fn global() -> &'static CoreManager { - static CORE_MANAGER: OnceCell = OnceCell::new(); - CORE_MANAGER.get_or_init(|| CoreManager { +impl Default for CoreManager { + fn default() -> Self { + CoreManager { running: Arc::new(Mutex::new(RunningMode::NotRunning)), child_sidecar: Arc::new(Mutex::new(None)), - }) - } - // 当服务安装失败时的回退逻辑 - async fn attempt_service_init(&self) -> Result<()> { - if service::check_service_needs_reinstall().await { - logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装"); - if let Err(e) = service::reinstall_service().await { - logging!( - warn, - Type::Core, - true, - "服务重装失败 during attempt_service_init: {}", - e - ); - return Err(e); - } - // 如果重装成功,还需要尝试启动服务 - logging!(info, Type::Core, true, "服务重装成功,尝试启动服务"); } - - if let Err(e) = self.start_core_by_service().await { - logging!( - warn, - Type::Core, - true, - "通过服务启动核心失败 during attempt_service_init: {}", - e - ); - // 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置 - let mut state = service::ServiceState::get(); - if !state.prefer_sidecar { - state.prefer_sidecar = true; - state.last_error = Some(format!("通过服务启动核心失败: {e}")); - if let Err(save_err) = state.save() { - logging!( - error, - Type::Core, - true, - "保存ServiceState失败 (in attempt_service_init/start_core_by_service): {}", - save_err - ); - } - } - return Err(e); - } - Ok(()) } +} +impl CoreManager { pub async fn init(&self) -> Result<()> { - logging!(trace, Type::Core, "Initializing core"); + logging!(info, Type::Core, "Initializing core"); // 应用启动时先清理任何遗留的 mihomo 进程 if let Err(e) = self.cleanup_orphaned_mihomo_processes().await { @@ -891,208 +851,66 @@ impl CoreManager { ); } - let mut core_started_successfully = false; + // 使用简化的启动流程 + logging!(info, Type::Core, true, "开始核心初始化"); + self.start_core().await?; - if service::is_service_available().await.is_ok() { - logging!( - info, - Type::Core, - true, - "服务当前可用或看似可用,尝试通过服务模式启动/重装" - ); - match self.attempt_service_init().await { - Ok(_) => { - logging!(info, Type::Core, true, "服务模式成功启动核心"); - core_started_successfully = true; - } - Err(_err) => { - logging!( - warn, - Type::Core, - true, - "服务模式启动或重装失败。将尝试Sidecar模式回退。" - ); - } - } - } else { - logging!( - info, - Type::Core, - true, - "服务初始不可用 (is_service_available 调用失败)" - ); - } - - if !core_started_successfully { - logging!( - info, - Type::Core, - true, - "核心未通过服务模式启动,执行Sidecar回退或首次安装逻辑" - ); - - let service_state = service::ServiceState::get(); - - if service_state.prefer_sidecar { - logging!( - info, - Type::Core, - true, - "用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动" - ); - self.start_core_by_sidecar().await?; - // 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束 - // 后续的 Tray::global().subscribe_traffic().await 仍然会执行 - } else { - let has_service_install_record = service_state.last_install_time > 0; - if !has_service_install_record { - logging!( - info, - Type::Core, - true, - "无服务安装记录 (首次运行或状态重置),尝试安装服务" - ); - match service::install_service().await { - Ok(_) => { - logging!(info, Type::Core, true, "服务安装成功(首次尝试)"); - let mut new_state = service::ServiceState::default(); - new_state.record_install(); - new_state.prefer_sidecar = false; - new_state.save()?; - - if service::is_service_available().await.is_ok() { - logging!(info, Type::Core, true, "新安装的服务可用,尝试启动"); - if self.start_core_by_service().await.is_ok() { - logging!(info, Type::Core, true, "新安装的服务启动成功"); - } else { - logging!( - warn, - Type::Core, - true, - "新安装的服务启动失败,回退到Sidecar模式" - ); - let mut final_state = service::ServiceState::get(); - final_state.prefer_sidecar = true; - final_state.last_error = - Some("Newly installed service failed to start".to_string()); - final_state.save()?; - self.start_core_by_sidecar().await?; - } - } else { - logging!( - warn, - Type::Core, - true, - "服务安装成功但未能连接/立即可用,回退到Sidecar模式" - ); - let mut final_state = service::ServiceState::get(); - final_state.prefer_sidecar = true; - final_state.last_error = Some( - "Newly installed service not immediately available/connectable" - .to_string(), - ); - final_state.save()?; - self.start_core_by_sidecar().await?; - } - } - Err(err) => { - logging!(warn, Type::Core, true, "服务首次安装失败: {}", err); - let new_state = service::ServiceState { - last_error: Some(err.to_string()), - prefer_sidecar: true, - ..Default::default() - }; - new_state.save()?; - self.start_core_by_sidecar().await?; - } - } - } else { - // 有安装记录,服务未成功启动,且初始不偏好sidecar - // 这意味着服务之前可能可用,但 attempt_service_init 失败了(并应已设置 prefer_sidecar), - // 或者服务初始不可用,无偏好,有记录。应强制使用 sidecar。 - logging!( - info, - Type::Core, - true, - "有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式" - ); - let mut final_state = service::ServiceState::get(); - if !final_state.prefer_sidecar { - logging!( - warn, - Type::Core, - true, - "prefer_sidecar 为 false,因服务启动失败或不可用而强制设置为 true" - ); - final_state.prefer_sidecar = true; - final_state.last_error = - Some(final_state.last_error.unwrap_or_else(|| { - "Service startup failed or unavailable before sidecar fallback" - .to_string() - })); - final_state.save()?; - } - self.start_core_by_sidecar().await?; - } - } - } - - logging!(trace, Type::Core, "Initied core logic completed"); - // #[cfg(target_os = "macos")] - // logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await); + logging!(info, Type::Core, true, "核心初始化完成"); Ok(()) } - pub async fn set_running_mode(&self, mode: RunningMode) { - let mut guard = self.running.lock().await; + pub fn set_running_mode(&self, mode: RunningMode) { + let mut guard = self.running.lock(); *guard = mode; } - pub async fn get_running_mode(&self) -> RunningMode { - let guard = self.running.lock().await; + pub fn get_running_mode(&self) -> RunningMode { + let guard = self.running.lock(); (*guard).clone() } + pub async fn prestart_core(&self) -> Result<()> { + SERVICE_MANAGER.lock().await.refresh().await?; + match SERVICE_MANAGER.lock().await.current() { + ServiceStatus::Ready => { + self.set_running_mode(RunningMode::Service); + } + _ => { + self.set_running_mode(RunningMode::Sidecar); + } + } + Ok(()) + } + /// 启动核心 pub async fn start_core(&self) -> Result<()> { - if service::is_service_available().await.is_ok() { - if service::check_service_needs_reinstall().await { - service::reinstall_service().await?; + self.prestart_core().await?; + + match self.get_running_mode() { + RunningMode::Service => { + logging_error!(Type::Core, true, self.start_core_by_service().await); } - logging!(info, Type::Core, true, "服务可用,使用服务模式启动"); - self.start_core_by_service().await?; - } else { - // 服务不可用,检查用户偏好 - let service_state = service::ServiceState::get(); - if service_state.prefer_sidecar { - logging!( - info, - Type::Core, - true, - "服务不可用,根据用户偏好使用Sidecar模式" - ); - self.start_core_by_sidecar().await?; - } else { - logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式"); - self.start_core_by_sidecar().await?; + RunningMode::NotRunning | RunningMode::Sidecar => { + logging_error!(Type::Core, true, self.start_core_by_sidecar().await); } - } + }; + Ok(()) } /// 停止核心运行 pub async fn stop_core(&self) -> Result<()> { - match self.get_running_mode().await { + match self.get_running_mode() { RunningMode::Service => self.stop_core_by_service().await, - RunningMode::Sidecar => self.stop_core_by_sidecar().await, + RunningMode::Sidecar => self.stop_core_by_sidecar(), RunningMode::NotRunning => Ok(()), } } /// 重启内核 pub async fn restart_core(&self) -> Result<()> { + logging!(info, Type::Core, true, "Restarting core"); self.stop_core().await?; - self.start_core().await?; Ok(()) } @@ -1104,18 +922,25 @@ impl CoreManager { logging!(error, Type::Core, true, "{}", error_message); return Err(error_message.to_string()); } - let core: &str = &clash_core.clone().unwrap(); - if !IVerge::VALID_CLASH_CORES.contains(&core) { + let core = clash_core.as_ref().ok_or_else(|| { + let msg = "Clash core should not be None"; + logging!(error, Type::Core, true, "{}", msg); + msg.to_string() + })?; + if !IVerge::VALID_CLASH_CORES.contains(&core.as_str()) { let error_message = format!("Clash core invalid name: {core}"); logging!(error, Type::Core, true, "{}", error_message); return Err(error_message); } - Config::verge().draft_mut().clash_core = clash_core.clone(); - Config::verge().apply(); - logging_error!(Type::Core, true, Config::verge().latest_ref().save_file()); + Config::verge().await.draft_mut().clash_core = clash_core.clone(); + Config::verge().await.apply(); - let run_path = Config::generate_file(ConfigType::Run).map_err(|e| { + // 分离数据获取和异步调用避免Send问题 + let verge_data = Config::verge().await.latest_ref().clone(); + logging_error!(Type::Core, true, verge_data.save_file().await); + + let run_path = Config::generate_file(ConfigType::Run).await.map_err(|e| { let msg = e.to_string(); logging_error!(Type::Core, true, "{}", msg); msg @@ -1126,3 +951,6 @@ impl CoreManager { Ok(()) } } + +// Use simplified singleton_lazy macro +singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default); diff --git a/clash-verge-rev/src-tauri/src/core/event_driven_proxy.rs b/clash-verge-rev/src-tauri/src/core/event_driven_proxy.rs index 9e0bd68ba2..71bf2ea60b 100644 --- a/clash-verge-rev/src-tauri/src/core/event_driven_proxy.rs +++ b/clash-verge-rev/src-tauri/src/core/event_driven_proxy.rs @@ -1,11 +1,13 @@ -use parking_lot::RwLock; use std::sync::Arc; +use tokio::sync::RwLock; use tokio::sync::{mpsc, oneshot}; -use tokio::time::{sleep, timeout, Duration}; +use tokio::time::{Duration, sleep, timeout}; +use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use crate::config::{Config, IVerge}; use crate::core::async_proxy_query::AsyncProxyQuery; use crate::logging_error; +use crate::process::AsyncHandler; use crate::utils::logging::Type; use once_cell::sync::Lazy; use sysproxy::{Autoproxy, Sysproxy}; @@ -58,7 +60,7 @@ impl Default for ProxyState { sys_proxy: Sysproxy { enable: false, host: "127.0.0.1".to_string(), - port: 7890, + port: 7897, bypass: "".to_string(), }, last_updated: std::time::Instant::now(), @@ -74,7 +76,7 @@ pub struct EventDrivenProxyManager { } #[derive(Debug)] -struct QueryRequest { +pub struct QueryRequest { response_tx: oneshot::Sender, } @@ -97,7 +99,8 @@ impl EventDrivenProxyManager { let (event_tx, event_rx) = mpsc::unbounded_channel(); let (query_tx, query_rx) = mpsc::unbounded_channel(); - Self::start_event_loop(state.clone(), event_rx, query_rx); + let state_clone = Arc::clone(&state); + AsyncHandler::spawn(move || Self::start_event_loop(state_clone, event_rx, query_rx)); Self { state, @@ -107,8 +110,8 @@ impl EventDrivenProxyManager { } /// 获取自动代理配置(缓存) - pub fn get_auto_proxy_cached(&self) -> Autoproxy { - self.state.read().auto_proxy.clone() + pub async fn get_auto_proxy_cached(&self) -> Autoproxy { + self.state.read().await.auto_proxy.clone() } /// 异步获取最新的自动代理配置 @@ -118,14 +121,14 @@ impl EventDrivenProxyManager { if self.query_sender.send(query).is_err() { log::error!(target: "app", "发送查询请求失败,返回缓存数据"); - return self.get_auto_proxy_cached(); + return self.get_auto_proxy_cached().await; } match timeout(Duration::from_secs(5), rx).await { Ok(Ok(result)) => result, _ => { log::warn!(target: "app", "查询超时,返回缓存数据"); - self.get_auto_proxy_cached() + self.get_auto_proxy_cached().await } } } @@ -170,43 +173,34 @@ impl EventDrivenProxyManager { } } - fn start_event_loop( + pub async fn start_event_loop( state: Arc>, - mut event_rx: mpsc::UnboundedReceiver, - mut query_rx: mpsc::UnboundedReceiver, + event_rx: mpsc::UnboundedReceiver, + query_rx: mpsc::UnboundedReceiver, ) { - tokio::spawn(async move { - log::info!(target: "app", "事件驱动代理管理器启动"); + log::info!(target: "app", "事件驱动代理管理器启动"); - loop { - tokio::select! { - event = event_rx.recv() => { - match event { - Some(event) => { - log::debug!(target: "app", "处理代理事件: {event:?}"); - Self::handle_event(&state, event).await; - } - None => { - log::info!(target: "app", "事件通道关闭,代理管理器停止"); - break; - } - } - } - query = query_rx.recv() => { - match query { - Some(query) => { - let result = Self::handle_query(&state).await; - let _ = query.response_tx.send(result); - } - None => { - log::info!(target: "app", "查询通道关闭"); - break; - } - } - } + // 将 mpsc 接收器包装成 Stream,避免每次循环创建 future + let mut event_stream = UnboundedReceiverStream::new(event_rx); + let mut query_stream = UnboundedReceiverStream::new(query_rx); + + loop { + tokio::select! { + Some(event) = event_stream.next() => { + log::debug!(target: "app", "处理代理事件: {event:?}"); + Self::handle_event(&state, event).await; + } + Some(query) = query_stream.next() => { + let result = Self::handle_query(&state).await; + let _ = query.response_tx.send(result); + } + else => { + // 两个通道都关闭时退出 + log::info!(target: "app", "事件或查询通道关闭,代理管理器停止"); + break; } } - }); + } } async fn handle_event(state: &Arc>, event: ProxyEvent) { @@ -218,7 +212,7 @@ impl EventDrivenProxyManager { Self::enable_system_proxy(state).await; } ProxyEvent::DisableProxy => { - Self::disable_system_proxy(state).await; + Self::disable_system_proxy(state); } ProxyEvent::SwitchToPac => { Self::switch_proxy_mode(state, true).await; @@ -240,7 +234,8 @@ impl EventDrivenProxyManager { Self::update_state_timestamp(state, |s| { s.auto_proxy = auto_proxy.clone(); - }); + }) + .await; auto_proxy } @@ -248,7 +243,7 @@ impl EventDrivenProxyManager { async fn initialize_proxy_state(state: &Arc>) { log::info!(target: "app", "初始化代理状态"); - let config = Self::get_proxy_config(); + let config = Self::get_proxy_config().await; let auto_proxy = Self::get_auto_proxy_with_timeout().await; let sys_proxy = Self::get_sys_proxy_with_timeout().await; @@ -258,7 +253,8 @@ impl EventDrivenProxyManager { s.auto_proxy = auto_proxy; s.sys_proxy = sys_proxy; s.is_healthy = true; - }); + }) + .await; log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled); } @@ -266,12 +262,13 @@ impl EventDrivenProxyManager { async fn update_proxy_config(state: &Arc>) { log::debug!(target: "app", "更新代理配置"); - let config = Self::get_proxy_config(); + let config = Self::get_proxy_config().await; Self::update_state_timestamp(state, |s| { s.sys_enabled = config.sys_enabled; s.pac_enabled = config.pac_enabled; - }); + }) + .await; if config.guard_enabled && config.sys_enabled { Self::check_and_restore_proxy(state).await; @@ -280,7 +277,7 @@ impl EventDrivenProxyManager { async fn check_and_restore_proxy(state: &Arc>) { let (sys_enabled, pac_enabled) = { - let s = state.read(); + let s = state.read().await; (s.sys_enabled, s.pac_enabled) }; @@ -299,15 +296,18 @@ impl EventDrivenProxyManager { async fn check_and_restore_pac_proxy(state: &Arc>) { let current = Self::get_auto_proxy_with_timeout().await; - let expected = Self::get_expected_pac_config(); + let expected = Self::get_expected_pac_config().await; Self::update_state_timestamp(state, |s| { s.auto_proxy = current.clone(); - }); + }) + .await; if !current.enable || current.url != expected.url { log::info!(target: "app", "PAC代理设置异常,正在恢复..."); - Self::restore_pac_proxy(&expected.url).await; + if let Err(e) = Self::restore_pac_proxy(&expected.url).await { + log::error!(target: "app", "恢复PAC代理失败: {}", e); + } sleep(Duration::from_millis(500)).await; let restored = Self::get_auto_proxy_with_timeout().await; @@ -315,21 +315,25 @@ impl EventDrivenProxyManager { Self::update_state_timestamp(state, |s| { s.is_healthy = restored.enable && restored.url == expected.url; s.auto_proxy = restored; - }); + }) + .await; } } async fn check_and_restore_sys_proxy(state: &Arc>) { let current = Self::get_sys_proxy_with_timeout().await; - let expected = Self::get_expected_sys_proxy(); + let expected = Self::get_expected_sys_proxy().await; Self::update_state_timestamp(state, |s| { s.sys_proxy = current.clone(); - }); + }) + .await; if !current.enable || current.host != expected.host || current.port != expected.port { log::info!(target: "app", "系统代理设置异常,正在恢复..."); - Self::restore_sys_proxy(&expected).await; + if let Err(e) = Self::restore_sys_proxy(&expected).await { + log::error!(target: "app", "恢复系统代理失败: {}", e); + } sleep(Duration::from_millis(500)).await; let restored = Self::get_sys_proxy_with_timeout().await; @@ -339,27 +343,32 @@ impl EventDrivenProxyManager { && restored.host == expected.host && restored.port == expected.port; s.sys_proxy = restored; - }); + }) + .await; } } async fn enable_system_proxy(state: &Arc>) { log::info!(target: "app", "启用系统代理"); - let pac_enabled = state.read().pac_enabled; + let pac_enabled = state.read().await.pac_enabled; if pac_enabled { - let expected = Self::get_expected_pac_config(); - Self::restore_pac_proxy(&expected.url).await; + let expected = Self::get_expected_pac_config().await; + if let Err(e) = Self::restore_pac_proxy(&expected.url).await { + log::error!(target: "app", "启用PAC代理失败: {}", e); + } } else { - let expected = Self::get_expected_sys_proxy(); - Self::restore_sys_proxy(&expected).await; + let expected = Self::get_expected_sys_proxy().await; + if let Err(e) = Self::restore_sys_proxy(&expected).await { + log::error!(target: "app", "启用系统代理失败: {}", e); + } } Self::check_and_restore_proxy(state).await; } - async fn disable_system_proxy(_state: &Arc>) { + fn disable_system_proxy(_state: &Arc>) { log::info!(target: "app", "禁用系统代理"); #[cfg(not(target_os = "windows"))] @@ -379,17 +388,21 @@ impl EventDrivenProxyManager { let disabled_sys = Sysproxy::default(); logging_error!(Type::System, true, disabled_sys.set_system_proxy()); - let expected = Self::get_expected_pac_config(); - Self::restore_pac_proxy(&expected.url).await; + let expected = Self::get_expected_pac_config().await; + if let Err(e) = Self::restore_pac_proxy(&expected.url).await { + log::error!(target: "app", "切换到PAC模式失败: {}", e); + } } else { let disabled_auto = Autoproxy::default(); logging_error!(Type::System, true, disabled_auto.set_auto_proxy()); - let expected = Self::get_expected_sys_proxy(); - Self::restore_sys_proxy(&expected).await; + let expected = Self::get_expected_sys_proxy().await; + if let Err(e) = Self::restore_sys_proxy(&expected).await { + log::error!(target: "app", "切换到HTTP代理模式失败: {}", e); + } } - Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac); + Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac).await; Self::check_and_restore_proxy(state).await; } @@ -416,18 +429,18 @@ impl EventDrivenProxyManager { } // 统一的状态更新方法 - fn update_state_timestamp(state: &Arc>, update_fn: F) + async fn update_state_timestamp(state: &Arc>, update_fn: F) where F: FnOnce(&mut ProxyState), { - let mut state_guard = state.write(); + let mut state_guard = state.write().await; update_fn(&mut state_guard); state_guard.last_updated = std::time::Instant::now(); } - fn get_proxy_config() -> ProxyConfig { + async fn get_proxy_config() -> ProxyConfig { let (sys_enabled, pac_enabled, guard_enabled) = { - let verge_config = Config::verge(); + let verge_config = Config::verge().await; let verge = verge_config.latest_ref(); ( verge.enable_system_proxy.unwrap_or(false), @@ -442,9 +455,9 @@ impl EventDrivenProxyManager { } } - fn get_expected_pac_config() -> Autoproxy { + async fn get_expected_pac_config() -> Autoproxy { let proxy_host = { - let verge_config = Config::verge(); + let verge_config = Config::verge().await; let verge = verge_config.latest_ref(); verge .proxy_host @@ -458,28 +471,25 @@ impl EventDrivenProxyManager { } } - fn get_expected_sys_proxy() -> Sysproxy { - let verge_config = Config::verge(); - let verge = verge_config.latest_ref(); - let port = verge - .verge_mixed_port - .unwrap_or(Config::clash().latest_ref().get_mixed_port()); - let proxy_host = verge - .proxy_host - .clone() - .unwrap_or_else(|| "127.0.0.1".to_string()); + async fn get_expected_sys_proxy() -> Sysproxy { + let verge_config = Config::verge().await; + let verge_mixed_port = verge_config.latest_ref().verge_mixed_port; + let proxy_host = verge_config.latest_ref().proxy_host.clone(); + + let port = verge_mixed_port.unwrap_or(Config::clash().await.latest_ref().get_mixed_port()); + let proxy_host = proxy_host.unwrap_or_else(|| "127.0.0.1".to_string()); Sysproxy { enable: true, host: proxy_host, port, - bypass: Self::get_bypass_config(), + bypass: Self::get_bypass_config().await, } } - fn get_bypass_config() -> String { + async fn get_bypass_config() -> String { let (use_default, custom_bypass) = { - let verge_config = Config::verge(); + let verge_config = Config::verge().await; let verge = verge_config.latest_ref(); ( verge.use_default_bypass.unwrap_or(true), @@ -506,37 +516,45 @@ impl EventDrivenProxyManager { } } - async fn restore_pac_proxy(expected_url: &str) { - #[cfg(not(target_os = "windows"))] + #[cfg(target_os = "windows")] + async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> { + Self::execute_sysproxy_command(&["pac", expected_url]).await + } + + #[allow(clippy::unused_async)] + #[cfg(not(target_os = "windows"))] + async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> { { let new_autoproxy = Autoproxy { enable: true, url: expected_url.to_string(), }; - logging_error!(Type::System, true, new_autoproxy.set_auto_proxy()); - } - - #[cfg(target_os = "windows")] - { - Self::execute_sysproxy_command(&["pac", expected_url]).await; - } - } - - async fn restore_sys_proxy(expected: &Sysproxy) { - #[cfg(not(target_os = "windows"))] - { - logging_error!(Type::System, true, expected.set_system_proxy()); - } - - #[cfg(target_os = "windows")] - { - let address = format!("{}:{}", expected.host, expected.port); - Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await; + // logging_error!(Type::System, true, new_autoproxy.set_auto_proxy()); + new_autoproxy + .set_auto_proxy() + .map_err(|e| anyhow::anyhow!("Failed to set auto proxy: {}", e)) } } #[cfg(target_os = "windows")] - async fn execute_sysproxy_command(args: &[&str]) { + async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> { + let address = format!("{}:{}", expected.host, expected.port); + Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await + } + + #[allow(clippy::unused_async)] + #[cfg(not(target_os = "windows"))] + async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> { + { + // logging_error!(Type::System, true, expected.set_system_proxy()); + expected + .set_system_proxy() + .map_err(|e| anyhow::anyhow!("Failed to set system proxy: {}", e)) + } + } + + #[cfg(target_os = "windows")] + async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> { use crate::utils::dirs; #[allow(unused_imports)] // creation_flags必须 use std::os::windows::process::CommandExt; @@ -546,37 +564,22 @@ impl EventDrivenProxyManager { Ok(path) => path, Err(e) => { log::error!(target: "app", "获取服务路径失败: {e}"); - return; + return Err(e); } }; let sysproxy_exe = binary_path.with_file_name("sysproxy.exe"); if !sysproxy_exe.exists() { log::error!(target: "app", "sysproxy.exe 不存在"); - return; } + anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist"); - let output = Command::new(sysproxy_exe) + let _output = Command::new(sysproxy_exe) .args(args) .creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口 .output() - .await; + .await?; - match output { - Ok(output) => { - if !output.status.success() { - log::error!(target: "app", "执行sysproxy命令失败: {args:?}"); - let stderr = String::from_utf8_lossy(&output.stderr); - if !stderr.is_empty() { - log::error!(target: "app", "sysproxy错误输出: {stderr}"); - } - } else { - log::debug!(target: "app", "成功执行sysproxy命令: {args:?}"); - } - } - Err(e) => { - log::error!(target: "app", "执行sysproxy命令出错: {e}"); - } - } + Ok(()) } } diff --git a/clash-verge-rev/src-tauri/src/core/handle.rs b/clash-verge-rev/src-tauri/src/core/handle.rs index d7e1518767..c0f4eddbd9 100644 --- a/clash-verge-rev/src-tauri/src/core/handle.rs +++ b/clash-verge-rev/src-tauri/src/core/handle.rs @@ -1,9 +1,10 @@ -use once_cell::sync::OnceCell; +use crate::singleton; use parking_lot::RwLock; use std::{ sync::{ + Arc, atomic::{AtomicU64, Ordering}, - mpsc, Arc, + mpsc, }, thread, time::{Duration, Instant}, @@ -20,7 +21,6 @@ enum FrontendEvent { NoticeMessage { status: String, message: String }, ProfileChanged { current_profile_id: String }, TimerUpdated { profile_index: String }, - StartupCompleted, ProfileUpdateStarted { uid: String }, ProfileUpdateCompleted { uid: String }, } @@ -82,132 +82,128 @@ impl NotificationSystem { *self.last_emit_time.write() = Instant::now(); - self.worker_handle = Some( - thread::Builder::new() - .name("frontend-notifier".into()) - .spawn(move || { - let handle = Handle::global(); + match thread::Builder::new() + .name("frontend-notifier".into()) + .spawn(move || { + let handle = Handle::global(); - while !handle.is_exiting() { - match rx.recv_timeout(Duration::from_millis(100)) { - Ok(event) => { - let system_guard = handle.notification_system.read(); - if system_guard.as_ref().is_none() { - log::warn!("NotificationSystem not found in handle while processing event."); - continue; - } - let system = system_guard.as_ref().unwrap(); + while !handle.is_exiting() { + match rx.recv_timeout(Duration::from_millis(100)) { + Ok(event) => { + let system_guard = handle.notification_system.read(); + let Some(system) = system_guard.as_ref() else { + log::warn!("NotificationSystem not found in handle while processing event."); + continue; + }; - let is_emergency = *system.emergency_mode.read(); + let is_emergency = *system.emergency_mode.read(); - if is_emergency { - if let FrontendEvent::NoticeMessage { ref status, .. } = event { - if status == "info" { - log::warn!( - "Emergency mode active, skipping info message" - ); - continue; + if is_emergency + && let FrontendEvent::NoticeMessage { ref status, .. } = event + && status == "info" { + log::warn!( + "Emergency mode active, skipping info message" + ); + continue; + } + + if let Some(window) = handle.get_window() { + *system.last_emit_time.write() = Instant::now(); + + let (event_name_str, payload_result) = match event { + FrontendEvent::RefreshClash => { + ("verge://refresh-clash-config", Ok(serde_json::json!("yes"))) + } + FrontendEvent::RefreshVerge => { + ("verge://refresh-verge-config", Ok(serde_json::json!("yes"))) + } + FrontendEvent::NoticeMessage { status, message } => { + match serde_json::to_value((status, message)) { + Ok(p) => ("verge://notice-message", Ok(p)), + Err(e) => { + log::error!("Failed to serialize NoticeMessage payload: {e}"); + ("verge://notice-message", Err(e)) + } } } - } + FrontendEvent::ProfileChanged { current_profile_id } => { + ("profile-changed", Ok(serde_json::json!(current_profile_id))) + } + FrontendEvent::TimerUpdated { profile_index } => { + ("verge://timer-updated", Ok(serde_json::json!(profile_index))) + } + FrontendEvent::ProfileUpdateStarted { uid } => { + ("profile-update-started", Ok(serde_json::json!({ "uid": uid }))) + } + FrontendEvent::ProfileUpdateCompleted { uid } => { + ("profile-update-completed", Ok(serde_json::json!({ "uid": uid }))) + } + }; - if let Some(window) = handle.get_window() { - *system.last_emit_time.write() = Instant::now(); - - let (event_name_str, payload_result) = match event { - FrontendEvent::RefreshClash => { - ("verge://refresh-clash-config", Ok(serde_json::json!("yes"))) - } - FrontendEvent::RefreshVerge => { - ("verge://refresh-verge-config", Ok(serde_json::json!("yes"))) - } - FrontendEvent::NoticeMessage { status, message } => { - match serde_json::to_value((status, message)) { - Ok(p) => ("verge://notice-message", Ok(p)), - Err(e) => { - log::error!("Failed to serialize NoticeMessage payload: {e}"); - ("verge://notice-message", Err(e)) - } + if let Ok(payload) = payload_result { + match window.emit(event_name_str, payload) { + Ok(_) => { + system.stats.total_sent.fetch_add(1, Ordering::SeqCst); + // 记录成功发送的事件 + if log::log_enabled!(log::Level::Debug) { + log::debug!("Successfully emitted event: {event_name_str}"); } } - FrontendEvent::ProfileChanged { current_profile_id } => { - ("profile-changed", Ok(serde_json::json!(current_profile_id))) - } - FrontendEvent::TimerUpdated { profile_index } => { - ("verge://timer-updated", Ok(serde_json::json!(profile_index))) - } - FrontendEvent::StartupCompleted => { - ("verge://startup-completed", Ok(serde_json::json!(null))) - } - FrontendEvent::ProfileUpdateStarted { uid } => { - ("profile-update-started", Ok(serde_json::json!({ "uid": uid }))) - } - FrontendEvent::ProfileUpdateCompleted { uid } => { - ("profile-update-completed", Ok(serde_json::json!({ "uid": uid }))) - } - }; + Err(e) => { + log::warn!("Failed to emit event {event_name_str}: {e}"); + system.stats.total_errors.fetch_add(1, Ordering::SeqCst); + *system.stats.last_error_time.write() = Some(Instant::now()); - if let Ok(payload) = payload_result { - match window.emit(event_name_str, payload) { - Ok(_) => { - system.stats.total_sent.fetch_add(1, Ordering::SeqCst); - // 记录成功发送的事件 - if log::log_enabled!(log::Level::Debug) { - log::debug!("Successfully emitted event: {event_name_str}"); - } - } - Err(e) => { - log::warn!("Failed to emit event {event_name_str}: {e}"); - system.stats.total_errors.fetch_add(1, Ordering::SeqCst); - *system.stats.last_error_time.write() = Some(Instant::now()); - - let errors = system.stats.total_errors.load(Ordering::SeqCst); - const EMIT_ERROR_THRESHOLD: u64 = 10; - if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() { - log::warn!( - "Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode" - ); - *system.emergency_mode.write() = true; - } + let errors = system.stats.total_errors.load(Ordering::SeqCst); + const EMIT_ERROR_THRESHOLD: u64 = 10; + if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() { + log::warn!( + "Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode" + ); + *system.emergency_mode.write() = true; } } - } else { - system.stats.total_errors.fetch_add(1, Ordering::SeqCst); - *system.stats.last_error_time.write() = Some(Instant::now()); - log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}"); } } else { - log::warn!("No window found, skipping event emit."); + system.stats.total_errors.fetch_add(1, Ordering::SeqCst); + *system.stats.last_error_time.write() = Some(Instant::now()); + log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}"); } - thread::sleep(Duration::from_millis(20)); - } - Err(mpsc::RecvTimeoutError::Timeout) => { - continue; - } - Err(mpsc::RecvTimeoutError::Disconnected) => { - log::info!( - "Notification channel disconnected, exiting worker thread" - ); - break; + } else { + log::warn!("No window found, skipping event emit."); } + thread::sleep(Duration::from_millis(20)); + } + Err(mpsc::RecvTimeoutError::Timeout) => { + } + Err(mpsc::RecvTimeoutError::Disconnected) => { + log::info!( + "Notification channel disconnected, exiting worker thread" + ); + break; } } + } - log::info!("Notification worker thread exiting"); - }) - .expect("Failed to start notification worker thread"), - ); + log::info!("Notification worker thread exiting"); + }) { + Ok(handle) => { + self.worker_handle = Some(handle); + } + Err(e) => { + log::error!("Failed to start notification worker thread: {e}"); + } + } } /// 发送事件到队列 fn send_event(&self, event: FrontendEvent) -> bool { - if *self.emergency_mode.read() { - if let FrontendEvent::NoticeMessage { ref status, .. } = event { - if status == "info" { - log::info!("Skipping info message in emergency mode"); - return false; - } - } + if *self.emergency_mode.read() + && let FrontendEvent::NoticeMessage { ref status, .. } = event + && status == "info" + { + log::info!("Skipping info message in emergency mode"); + return false; } if let Some(sender) = &self.sender { @@ -272,16 +268,18 @@ impl Default for Handle { } } +// Use singleton macro +singleton!(Handle, HANDLE); + impl Handle { - pub fn global() -> &'static Handle { - static HANDLE: OnceCell = OnceCell::new(); - HANDLE.get_or_init(Handle::default) + pub fn new() -> Self { + Self::default() } - pub fn init(&self, app_handle: &AppHandle) { + pub fn init(&self, app_handle: AppHandle) { { let mut handle = self.app_handle.write(); - *handle = Some(app_handle.clone()); + *handle = Some(app_handle); } let mut system_opt = self.notification_system.write(); @@ -290,6 +288,7 @@ impl Handle { } } + /// 获取 AppHandle pub fn app_handle(&self) -> Option { self.app_handle.read().clone() } @@ -361,22 +360,6 @@ impl Handle { } } - pub fn notify_startup_completed() { - let handle = Self::global(); - if handle.is_exiting() { - return; - } - - let system_opt = handle.notification_system.read(); - if let Some(system) = system_opt.as_ref() { - system.send_event(FrontendEvent::StartupCompleted); - } else { - log::warn!( - "Notification system not initialized when trying to send StartupCompleted event." - ); - } - } - pub fn notify_profile_update_started(uid: String) { let handle = Self::global(); if handle.is_exiting() { @@ -387,7 +370,9 @@ impl Handle { if let Some(system) = system_opt.as_ref() { system.send_event(FrontendEvent::ProfileUpdateStarted { uid }); } else { - log::warn!("Notification system not initialized when trying to send ProfileUpdateStarted event."); + log::warn!( + "Notification system not initialized when trying to send ProfileUpdateStarted event." + ); } } @@ -401,7 +386,9 @@ impl Handle { if let Some(system) = system_opt.as_ref() { system.send_event(FrontendEvent::ProfileUpdateCompleted { uid }); } else { - log::warn!("Notification system not initialized when trying to send ProfileUpdateCompleted event."); + log::warn!( + "Notification system not initialized when trying to send ProfileUpdateCompleted event." + ); } } @@ -466,8 +453,9 @@ impl Handle { info, Type::Frontend, true, - "发送{}条启动时累积的错误消息", - errors.len() + "发送{}条启动时累积的错误消息: {:?}", + errors.len(), + errors ); // 启动单独线程处理启动错误,避免阻塞主线程 @@ -517,3 +505,54 @@ impl Handle { *self.is_exiting.read() } } + +#[cfg(target_os = "macos")] +impl Handle { + pub fn set_activation_policy(&self, policy: tauri::ActivationPolicy) -> Result<(), String> { + let app_handle = self.app_handle(); + if let Some(app_handle) = app_handle.as_ref() { + app_handle + .set_activation_policy(policy) + .map_err(|e| e.to_string()) + } else { + Err("AppHandle not initialized".to_string()) + } + } + + pub fn set_activation_policy_regular(&self) { + if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Regular) { + logging!( + warn, + Type::Setup, + true, + "Failed to set regular activation policy: {}", + e + ); + } + } + + pub fn set_activation_policy_accessory(&self) { + if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Accessory) { + logging!( + warn, + Type::Setup, + true, + "Failed to set accessory activation policy: {}", + e + ); + } + } + + #[allow(dead_code)] + pub fn set_activation_policy_prohibited(&self) { + if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Prohibited) { + logging!( + warn, + Type::Setup, + true, + "Failed to set prohibited activation policy: {}", + e + ); + } + } +} diff --git a/clash-verge-rev/src-tauri/src/core/hotkey.rs b/clash-verge-rev/src-tauri/src/core/hotkey.rs index fae8240d77..67258242d3 100755 --- a/clash-verge-rev/src-tauri/src/core/hotkey.rs +++ b/clash-verge-rev/src-tauri/src/core/hotkey.rs @@ -1,30 +1,296 @@ -use crate::utils::notification::{notify_event, NotificationEvent}; +use crate::process::AsyncHandler; +use crate::utils::notification::{NotificationEvent, notify_event}; use crate::{ config::Config, core::handle, feat, logging, logging_error, - module::lightweight::entry_lightweight_mode, utils::logging::Type, + module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type, }; -use anyhow::{bail, Result}; -use once_cell::sync::OnceCell; +use anyhow::{Result, bail}; use parking_lot::Mutex; -use std::{collections::HashMap, sync::Arc}; -use tauri::Manager; +use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; +use tauri::{AppHandle, Manager}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; +/// Enum representing all available hotkey functions +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum HotkeyFunction { + OpenOrCloseDashboard, + ClashModeRule, + ClashModeGlobal, + ClashModeDirect, + ToggleSystemProxy, + ToggleTunMode, + EntryLightweightMode, + Quit, + #[cfg(target_os = "macos")] + Hide, +} + +impl fmt::Display for HotkeyFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard", + HotkeyFunction::ClashModeRule => "clash_mode_rule", + HotkeyFunction::ClashModeGlobal => "clash_mode_global", + HotkeyFunction::ClashModeDirect => "clash_mode_direct", + HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy", + HotkeyFunction::ToggleTunMode => "toggle_tun_mode", + HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode", + HotkeyFunction::Quit => "quit", + #[cfg(target_os = "macos")] + HotkeyFunction::Hide => "hide", + }; + write!(f, "{s}") + } +} + +impl FromStr for HotkeyFunction { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.trim() { + "open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard), + "clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule), + "clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal), + "clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect), + "toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy), + "toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode), + "entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode), + "quit" => Ok(HotkeyFunction::Quit), + #[cfg(target_os = "macos")] + "hide" => Ok(HotkeyFunction::Hide), + _ => bail!("invalid hotkey function: {}", s), + } + } +} + +#[cfg(target_os = "macos")] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// Enum representing predefined system hotkeys +pub enum SystemHotkey { + CmdQ, + CmdW, +} + +#[cfg(target_os = "macos")] +impl fmt::Display for SystemHotkey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + SystemHotkey::CmdQ => "CMD+Q", + SystemHotkey::CmdW => "CMD+W", + }; + write!(f, "{s}") + } +} + +#[cfg(target_os = "macos")] +impl SystemHotkey { + pub fn function(self) -> HotkeyFunction { + match self { + SystemHotkey::CmdQ => HotkeyFunction::Quit, + SystemHotkey::CmdW => HotkeyFunction::Hide, + } + } +} + pub struct Hotkey { current: Arc>>, } impl Hotkey { - pub fn global() -> &'static Hotkey { - static HOTKEY: OnceCell = OnceCell::new(); - - HOTKEY.get_or_init(|| Hotkey { + fn new() -> Self { + Self { current: Arc::new(Mutex::new(Vec::new())), - }) + } } - pub fn init(&self) -> Result<()> { - let verge = Config::verge(); + /// Execute the function associated with a hotkey function enum + fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) { + let app_handle = app_handle.clone(); + match function { + HotkeyFunction::OpenOrCloseDashboard => { + AsyncHandler::spawn(async move || { + crate::feat::open_or_close_dashboard().await; + notify_event(app_handle, NotificationEvent::DashboardToggled).await; + }); + } + HotkeyFunction::ClashModeRule => { + AsyncHandler::spawn(async move || { + feat::change_clash_mode("rule".into()).await; + notify_event( + app_handle, + NotificationEvent::ClashModeChanged { mode: "Rule" }, + ) + .await; + }); + } + HotkeyFunction::ClashModeGlobal => { + AsyncHandler::spawn(async move || { + feat::change_clash_mode("global".into()).await; + notify_event( + app_handle, + NotificationEvent::ClashModeChanged { mode: "Global" }, + ) + .await; + }); + } + HotkeyFunction::ClashModeDirect => { + AsyncHandler::spawn(async move || { + feat::change_clash_mode("direct".into()).await; + notify_event( + app_handle, + NotificationEvent::ClashModeChanged { mode: "Direct" }, + ) + .await; + }); + } + HotkeyFunction::ToggleSystemProxy => { + AsyncHandler::spawn(async move || { + feat::toggle_system_proxy().await; + notify_event(app_handle, NotificationEvent::SystemProxyToggled).await; + }); + } + HotkeyFunction::ToggleTunMode => { + AsyncHandler::spawn(async move || { + feat::toggle_tun_mode(None).await; + notify_event(app_handle, NotificationEvent::TunModeToggled).await; + }); + } + HotkeyFunction::EntryLightweightMode => { + AsyncHandler::spawn(async move || { + entry_lightweight_mode().await; + notify_event(app_handle, NotificationEvent::LightweightModeEntered).await; + }); + } + HotkeyFunction::Quit => { + AsyncHandler::spawn(async move || { + notify_event(app_handle, NotificationEvent::AppQuit).await; + feat::quit().await; + }); + } + #[cfg(target_os = "macos")] + HotkeyFunction::Hide => { + AsyncHandler::spawn(async move || { + feat::hide().await; + notify_event(app_handle, NotificationEvent::AppHidden).await; + }); + } + } + } + + #[cfg(target_os = "macos")] + /// Register a system hotkey using enum + pub async fn register_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> { + let hotkey_str = hotkey.to_string(); + let function = hotkey.function(); + self.register_hotkey_with_function(&hotkey_str, function) + .await + } + + #[cfg(target_os = "macos")] + /// Unregister a system hotkey using enum + pub fn unregister_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> { + let hotkey_str = hotkey.to_string(); + self.unregister(&hotkey_str) + } + + /// Register a hotkey with function enum + #[allow(clippy::unused_async)] + pub async fn register_hotkey_with_function( + &self, + hotkey: &str, + function: HotkeyFunction, + ) -> Result<()> { + let app_handle = handle::Handle::global() + .app_handle() + .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?; + let manager = app_handle.global_shortcut(); + + logging!( + debug, + Type::Hotkey, + "Attempting to register hotkey: {} for function: {}", + hotkey, + function + ); + + if manager.is_registered(hotkey) { + logging!( + debug, + Type::Hotkey, + "Hotkey {} was already registered, unregistering first", + hotkey + ); + manager.unregister(hotkey)?; + } + + let is_quit = matches!(function, HotkeyFunction::Quit); + + let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| { + let hotkey_event_owned = *hotkey_event; + let event_owned = event; + let function_owned = function; + let is_quit_owned = is_quit; + + let app_handle_cloned = app_handle.clone(); + + AsyncHandler::spawn(move || async move { + if event_owned.state == ShortcutState::Pressed { + logging!( + debug, + Type::Hotkey, + "Hotkey pressed: {:?}", + hotkey_event_owned + ); + + if hotkey_event_owned.key == Code::KeyQ && is_quit_owned { + if let Some(window) = app_handle_cloned.get_webview_window("main") + && window.is_focused().unwrap_or(false) + { + logging!(debug, Type::Hotkey, "Executing quit function"); + Self::execute_function(function_owned, &app_handle_cloned); + } + } else { + logging!(debug, Type::Hotkey, "Executing function directly"); + + let is_enable_global_hotkey = Config::verge() + .await + .latest_ref() + .enable_global_hotkey + .unwrap_or(true); + + if is_enable_global_hotkey { + Self::execute_function(function_owned, &app_handle_cloned); + } else { + use crate::utils::window_manager::WindowManager; + let is_visible = WindowManager::is_main_window_visible(); + let is_focused = WindowManager::is_main_window_focused(); + + if is_focused && is_visible { + Self::execute_function(function_owned, &app_handle_cloned); + } + } + } + } + }); + }); + + logging!( + debug, + Type::Hotkey, + "Successfully registered hotkey {} for {}", + hotkey, + function + ); + Ok(()) + } +} + +// Use unified singleton macro +singleton_with_logging!(Hotkey, INSTANCE, "Hotkey"); + +impl Hotkey { + pub async fn init(&self) -> Result<()> { + let verge = Config::verge().await; let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true); logging!( @@ -39,7 +305,10 @@ impl Hotkey { return Ok(()); } - if let Some(hotkeys) = verge.latest_ref().hotkeys.as_ref() { + // Extract hotkeys data before async operations + let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned(); + + if let Some(hotkeys) = hotkeys { logging!( debug, Type::Hotkey, @@ -63,7 +332,7 @@ impl Hotkey { key, func ); - if let Err(e) = self.register(key, func) { + if let Err(e) = self.register(key, func).await { logging!( error, Type::Hotkey, @@ -97,7 +366,7 @@ impl Hotkey { } } } - self.current.lock().clone_from(hotkeys); + self.current.lock().clone_from(&hotkeys); } else { logging!(debug, Type::Hotkey, "No hotkeys configured"); } @@ -106,192 +375,34 @@ impl Hotkey { } pub fn reset(&self) -> Result<()> { - let app_handle = handle::Handle::global().app_handle().unwrap(); + let app_handle = handle::Handle::global() + .app_handle() + .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?; let manager = app_handle.global_shortcut(); manager.unregister_all()?; Ok(()) } - pub fn register(&self, hotkey: &str, func: &str) -> Result<()> { - let app_handle = handle::Handle::global().app_handle().unwrap(); - let manager = app_handle.global_shortcut(); - - logging!( - debug, - Type::Hotkey, - "Attempting to register hotkey: {} for function: {}", - hotkey, - func - ); - - if manager.is_registered(hotkey) { - logging!( - debug, - Type::Hotkey, - "Hotkey {} was already registered, unregistering first", - hotkey - ); - manager.unregister(hotkey)?; - } - - let app_handle_clone = app_handle.clone(); - let f: Box = match func.trim() { - "open_or_close_dashboard" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - logging!( - debug, - Type::Hotkey, - true, - "=== Hotkey Dashboard Window Operation Start ===" - ); - - logging!( - info, - Type::Hotkey, - true, - "Using unified WindowManager for hotkey operation (bypass debounce)" - ); - - crate::feat::open_or_close_dashboard_hotkey(); - - logging!( - debug, - Type::Hotkey, - "=== Hotkey Dashboard Window Operation End ===" - ); - notify_event(&app_handle, NotificationEvent::DashboardToggled); - }) - } - "clash_mode_rule" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::change_clash_mode("rule".into()); - notify_event( - &app_handle, - NotificationEvent::ClashModeChanged { mode: "Rule" }, - ); - }) - } - "clash_mode_global" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::change_clash_mode("global".into()); - notify_event( - &app_handle, - NotificationEvent::ClashModeChanged { mode: "Global" }, - ); - }) - } - "clash_mode_direct" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::change_clash_mode("direct".into()); - notify_event( - &app_handle, - NotificationEvent::ClashModeChanged { mode: "Direct" }, - ); - }) - } - "toggle_system_proxy" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::toggle_system_proxy(); - notify_event(&app_handle, NotificationEvent::SystemProxyToggled); - }) - } - "toggle_tun_mode" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::toggle_tun_mode(None); - notify_event(&app_handle, NotificationEvent::TunModeToggled); - }) - } - "entry_lightweight_mode" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - entry_lightweight_mode(); - notify_event(&app_handle, NotificationEvent::LightweightModeEntered); - }) - } - "quit" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::quit(); - notify_event(&app_handle, NotificationEvent::AppQuit); - }) - } - #[cfg(target_os = "macos")] - "hide" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::hide(); - notify_event(&app_handle, NotificationEvent::AppHidden); - }) - } - _ => { - logging!(error, Type::Hotkey, "Invalid function: {}", func); - bail!("invalid function \"{func}\""); - } - }; - - let is_quit = func.trim() == "quit"; - - let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| { - if event.state == ShortcutState::Pressed { - logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey); - - if hotkey.key == Code::KeyQ && is_quit { - if let Some(window) = app_handle.get_webview_window("main") { - if window.is_focused().unwrap_or(false) { - logging!(debug, Type::Hotkey, "Executing quit function"); - f(); - } - } - } else { - logging!(debug, Type::Hotkey, "Executing function directly"); - - let is_enable_global_hotkey = Config::verge() - .latest_ref() - .enable_global_hotkey - .unwrap_or(true); - - if is_enable_global_hotkey { - f(); - } else { - use crate::utils::window_manager::WindowManager; - let is_visible = WindowManager::is_main_window_visible(); - let is_focused = WindowManager::is_main_window_focused(); - - if is_focused && is_visible { - f(); - } - } - } - } - }); - - logging!( - debug, - Type::Hotkey, - "Successfully registered hotkey {} for {}", - hotkey, - func - ); - Ok(()) + /// Register a hotkey with string-based function (backward compatibility) + pub async fn register(&self, hotkey: &str, func: &str) -> Result<()> { + let function = HotkeyFunction::from_str(func)?; + self.register_hotkey_with_function(hotkey, function).await } pub fn unregister(&self, hotkey: &str) -> Result<()> { - let app_handle = handle::Handle::global().app_handle().unwrap(); + let app_handle = handle::Handle::global() + .app_handle() + .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?; let manager = app_handle.global_shortcut(); manager.unregister(hotkey)?; logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey); Ok(()) } - pub fn update(&self, new_hotkeys: Vec) -> Result<()> { - let mut current = self.current.lock(); - let old_map = Self::get_map_from_vec(¤t); + pub async fn update(&self, new_hotkeys: Vec) -> Result<()> { + // Extract current hotkeys before async operations + let current_hotkeys = self.current.lock().clone(); + let old_map = Self::get_map_from_vec(¤t_hotkeys); let new_map = Self::get_map_from_vec(&new_hotkeys); let (del, add) = Self::get_diff(old_map, new_map); @@ -300,11 +411,12 @@ impl Hotkey { let _ = self.unregister(key); }); - add.iter().for_each(|(key, func)| { - logging_error!(Type::Hotkey, self.register(key, func)); - }); + for (key, func) in add.iter() { + logging_error!(Type::Hotkey, self.register(key, func).await); + } - *current = new_hotkeys; + // Update the current hotkeys after all async operations + *self.current.lock() = new_hotkeys; Ok(()) } @@ -356,7 +468,17 @@ impl Hotkey { impl Drop for Hotkey { fn drop(&mut self) { - let app_handle = handle::Handle::global().app_handle().unwrap(); + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + logging!( + error, + Type::Hotkey, + "Failed to get app handle during hotkey cleanup" + ); + return; + } + }; if let Err(e) = app_handle.global_shortcut().unregister_all() { logging!( error, diff --git a/clash-verge-rev/src-tauri/src/core/service.rs b/clash-verge-rev/src-tauri/src/core/service.rs index 3ec2482989..3312f6c069 100644 --- a/clash-verge-rev/src-tauri/src/core/service.rs +++ b/clash-verge-rev/src-tauri/src/core/service.rs @@ -1,118 +1,34 @@ use crate::{ + cache::{CacheService, SHORT_TERM_TTL}, config::Config, - core::service_ipc::{send_ipc_request, IpcCommand}, - logging, + core::service_ipc::{IpcCommand, send_ipc_request}, + logging, logging_error, utils::{dirs, logging::Type}, }; -use anyhow::{bail, Context, Result}; -use serde::{Deserialize, Serialize}; -use std::{ - env::current_exe, - path::PathBuf, - process::Command as StdCommand, - time::{SystemTime, UNIX_EPOCH}, -}; +use anyhow::{Context, Result, bail}; +use once_cell::sync::Lazy; +use std::{env::current_exe, path::PathBuf, process::Command as StdCommand}; +use tokio::sync::Mutex; -const REQUIRED_SERVICE_VERSION: &str = "1.1.0"; // 定义所需的服务版本号 +const REQUIRED_SERVICE_VERSION: &str = "1.1.2"; // 定义所需的服务版本号 -// 限制重装时间和次数的常量 -const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期 -const MAX_REINSTALLS_PER_DAY: u32 = 3; // 每24小时最多重装3次 -const ONE_DAY_SECS: u64 = 86400; // 24小时的秒数 - -#[derive(Debug, Deserialize, Serialize, Clone, Default)] -pub struct ServiceState { - pub last_install_time: u64, // 上次安装时间戳 (Unix 时间戳,秒) - pub install_count: u32, // 24小时内安装次数 - pub last_check_time: u64, // 上次检查时间 - pub last_error: Option, // 上次错误信息 - pub prefer_sidecar: bool, // 用户是否偏好sidecar模式,如拒绝安装服务或安装失败 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ServiceStatus { + Ready, + NeedsReinstall, + InstallRequired, + UninstallRequired, + ReinstallRequired, + ForceReinstallRequired, + Unavailable(String), } -impl ServiceState { - // 获取当前的服务状态 - pub fn get() -> Self { - if let Some(state) = Config::verge().latest_ref().service_state.clone() { - return state; - } - Self::default() - } - - // 保存服务状态 - pub fn save(&self) -> Result<()> { - let config = Config::verge(); - let mut latest = config.latest_ref().clone(); - latest.service_state = Some(self.clone()); - *config.draft_mut() = latest; - config.apply(); - let result = config.latest_ref().save_file(); - result - } - - // 更新安装信息 - pub fn record_install(&mut self) { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - // 检查是否需要重置计数器(24小时已过) - if now - self.last_install_time > ONE_DAY_SECS { - self.install_count = 0; - } - - self.last_install_time = now; - self.install_count += 1; - } - - // 检查是否可以重新安装 - pub fn can_reinstall(&self) -> bool { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - // 如果在冷却期内,不允许重装 - if now - self.last_install_time < REINSTALL_COOLDOWN_SECS { - return false; - } - - // 如果24小时内安装次数过多,也不允许 - if now - self.last_install_time < ONE_DAY_SECS - && self.install_count >= MAX_REINSTALLS_PER_DAY - { - return false; - } - - true - } -} - -// 保留核心数据结构,但将HTTP特定的结构体合并为通用结构体 -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct ResponseBody { - pub core_type: Option, - pub bin_path: String, - pub config_dir: String, - pub log_file: String, -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct VersionResponse { - pub service: String, - pub version: String, -} - -// 保留通用的响应结构体,用于IPC通信后的数据解析 -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct JsonResponse { - pub code: u64, - pub msg: String, - pub data: Option, -} +#[derive(Clone)] +pub struct ServiceManager(ServiceStatus); +#[allow(clippy::unused_async)] #[cfg(target_os = "windows")] -pub async fn uninstall_service() -> Result<()> { +async fn uninstall_service() -> Result<()> { logging!(info, Type::Service, true, "uninstall service"); use deelevate::{PrivilegeLevel, Token}; @@ -138,15 +54,16 @@ pub async fn uninstall_service() -> Result<()> { if !status.success() { bail!( "failed to uninstall service with status {}", - status.code().unwrap() + status.code().unwrap_or(-1) ); } Ok(()) } +#[allow(clippy::unused_async)] #[cfg(target_os = "windows")] -pub async fn install_service() -> Result<()> { +async fn install_service() -> Result<()> { logging!(info, Type::Service, true, "install service"); use deelevate::{PrivilegeLevel, Token}; @@ -172,7 +89,7 @@ pub async fn install_service() -> Result<()> { if !status.success() { bail!( "failed to install service with status {}", - status.code().unwrap() + status.code().unwrap_or(-1) ); } @@ -180,23 +97,9 @@ pub async fn install_service() -> Result<()> { } #[cfg(target_os = "windows")] -pub async fn reinstall_service() -> Result<()> { +async fn reinstall_service() -> Result<()> { logging!(info, Type::Service, true, "reinstall service"); - // 获取当前服务状态 - let mut service_state = ServiceState::get(); - - // 检查是否允许重装 - if !service_state.can_reinstall() { - logging!( - warn, - Type::Service, - true, - "service reinstall rejected: cooldown period or max attempts reached" - ); - bail!("Service reinstallation is rate limited. Please try again later."); - } - // 先卸载服务 if let Err(err) = uninstall_service().await { logging!( @@ -210,25 +113,16 @@ pub async fn reinstall_service() -> Result<()> { // 再安装服务 match install_service().await { - Ok(_) => { - // 记录安装信息并保存 - service_state.record_install(); - service_state.last_error = None; - service_state.save()?; - Ok(()) - } + Ok(_) => Ok(()), Err(err) => { - let error = format!("failed to install service: {err}"); - service_state.last_error = Some(error.clone()); - service_state.prefer_sidecar = true; - service_state.save()?; - bail!(error) + bail!(format!("failed to install service: {err}")) } } } +#[allow(clippy::unused_async)] #[cfg(target_os = "linux")] -pub async fn uninstall_service() -> Result<()> { +async fn uninstall_service() -> Result<()> { logging!(info, Type::Service, true, "uninstall service"); use users::get_effective_uid; @@ -254,13 +148,13 @@ pub async fn uninstall_service() -> Result<()> { Type::Service, true, "uninstall status code:{}", - status.code().unwrap() + status.code().unwrap_or(-1) ); if !status.success() { bail!( "failed to uninstall service with status {}", - status.code().unwrap() + status.code().unwrap_or(-1) ); } @@ -268,7 +162,8 @@ pub async fn uninstall_service() -> Result<()> { } #[cfg(target_os = "linux")] -pub async fn install_service() -> Result<()> { +#[allow(clippy::unused_async)] +async fn install_service() -> Result<()> { logging!(info, Type::Service, true, "install service"); use users::get_effective_uid; @@ -294,13 +189,13 @@ pub async fn install_service() -> Result<()> { Type::Service, true, "install status code:{}", - status.code().unwrap() + status.code().unwrap_or(-1) ); if !status.success() { bail!( "failed to install service with status {}", - status.code().unwrap() + status.code().unwrap_or(-1) ); } @@ -308,23 +203,9 @@ pub async fn install_service() -> Result<()> { } #[cfg(target_os = "linux")] -pub async fn reinstall_service() -> Result<()> { +async fn reinstall_service() -> Result<()> { logging!(info, Type::Service, true, "reinstall service"); - // 获取当前服务状态 - let mut service_state = ServiceState::get(); - - // 检查是否允许重装 - if !service_state.can_reinstall() { - logging!( - warn, - Type::Service, - true, - "service reinstall rejected: cooldown period or max attempts reached" - ); - bail!("Service reinstallation is rate limited. Please try again later."); - } - // 先卸载服务 if let Err(err) = uninstall_service().await { logging!( @@ -338,25 +219,15 @@ pub async fn reinstall_service() -> Result<()> { // 再安装服务 match install_service().await { - Ok(_) => { - // 记录安装信息并保存 - service_state.record_install(); - service_state.last_error = None; - service_state.save()?; - Ok(()) - } + Ok(_) => Ok(()), Err(err) => { - let error = format!("failed to install service: {err}"); - service_state.last_error = Some(error.clone()); - service_state.prefer_sidecar = true; - service_state.save()?; - bail!(error) + bail!(format!("failed to install service: {err}")) } } } #[cfg(target_os = "macos")] -pub async fn uninstall_service() -> Result<()> { +async fn uninstall_service() -> Result<()> { use crate::utils::i18n::t; logging!(info, Type::Service, true, "uninstall service"); @@ -370,7 +241,7 @@ pub async fn uninstall_service() -> Result<()> { let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned(); - let prompt = t("Service Administrator Prompt"); + let prompt = t("Service Administrator Prompt").await; let command = format!( r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""# ); @@ -384,7 +255,7 @@ pub async fn uninstall_service() -> Result<()> { if !status.success() { bail!( "failed to uninstall service with status {}", - status.code().unwrap() + status.code().unwrap_or(-1) ); } @@ -392,7 +263,7 @@ pub async fn uninstall_service() -> Result<()> { } #[cfg(target_os = "macos")] -pub async fn install_service() -> Result<()> { +async fn install_service() -> Result<()> { use crate::utils::i18n::t; logging!(info, Type::Service, true, "install service"); @@ -406,7 +277,7 @@ pub async fn install_service() -> Result<()> { let install_shell: String = install_path.to_string_lossy().into_owned(); - let prompt = t("Service Administrator Prompt"); + let prompt = t("Service Administrator Prompt").await; let command = format!( r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""# ); @@ -420,7 +291,7 @@ pub async fn install_service() -> Result<()> { if !status.success() { bail!( "failed to install service with status {}", - status.code().unwrap() + status.code().unwrap_or(-1) ); } @@ -428,23 +299,9 @@ pub async fn install_service() -> Result<()> { } #[cfg(target_os = "macos")] -pub async fn reinstall_service() -> Result<()> { +async fn reinstall_service() -> Result<()> { logging!(info, Type::Service, true, "reinstall service"); - // 获取当前服务状态 - let mut service_state = ServiceState::get(); - - // 检查是否允许重装 - if !service_state.can_reinstall() { - logging!( - warn, - Type::Service, - true, - "service reinstall rejected: cooldown period or max attempts reached" - ); - bail!("Service reinstallation is rate limited. Please try again later."); - } - // 先卸载服务 if let Err(err) = uninstall_service().await { logging!( @@ -458,449 +315,114 @@ pub async fn reinstall_service() -> Result<()> { // 再安装服务 match install_service().await { - Ok(_) => { - // 记录安装信息并保存 - service_state.record_install(); - service_state.last_error = None; - service_state.save()?; - Ok(()) - } + Ok(_) => Ok(()), Err(err) => { - let error = format!("failed to install service: {err}"); - service_state.last_error = Some(error.clone()); - service_state.prefer_sidecar = true; - service_state.save()?; - bail!(error) + bail!(format!("failed to install service: {err}")) } } } -/// 检查服务状态 - 使用IPC通信 -pub async fn check_ipc_service_status() -> Result { - logging!(info, Type::Service, true, "开始检查服务状态 (IPC)"); - - // 使用IPC通信 - let payload = serde_json::json!({}); - // logging!(debug, Type::Service, true, "发送GetClash请求"); - - match send_ipc_request(IpcCommand::GetClash, payload).await { - Ok(response) => { - /* logging!( - debug, - Type::Service, - true, - "收到GetClash响应: success={}, error={:?}", - response.success, - response.error - ); */ - - if !response.success { - let err_msg = response.error.unwrap_or_else(|| "未知服务错误".to_string()); - logging!(error, Type::Service, true, "服务响应错误: {}", err_msg); - bail!(err_msg); - } - - match response.data { - Some(data) => { - // 检查嵌套结构 - if let (Some(code), Some(msg)) = (data.get("code"), data.get("msg")) { - let code_value = code.as_u64().unwrap_or(0); - let msg_value = msg.as_str().unwrap_or("ok").to_string(); - - // 提取嵌套的data字段并解析为ResponseBody - let response_body = if let Some(nested_data) = data.get("data") { - match serde_json::from_value::(nested_data.clone()) { - Ok(body) => Some(body), - Err(e) => { - logging!( - warn, - Type::Service, - true, - "解析嵌套的ResponseBody失败: {}; 尝试其他方式", - e - ); - None - } - } - } else { - None - }; - - let json_response = JsonResponse { - code: code_value, - msg: msg_value, - data: response_body, - }; - - logging!( - info, - Type::Service, - true, - "服务检测成功: code={}, msg={}, data存在={}", - json_response.code, - json_response.msg, - json_response.data.is_some() - ); - Ok(json_response) - } else { - // 尝试直接解析 - match serde_json::from_value::(data.clone()) { - Ok(json_response) => { - logging!( - info, - Type::Service, - true, - "服务检测成功: code={}, msg={}", - json_response.code, - json_response.msg - ); - Ok(json_response) - } - Err(e) => { - logging!( - error, - Type::Service, - true, - "解析服务响应失败: {}; 原始数据: {:?}", - e, - data - ); - bail!("无法解析服务响应数据: {}", e) - } - } - } - } - None => { - logging!(error, Type::Service, true, "服务响应中没有数据"); - bail!("服务响应中没有数据") - } - } - } - Err(e) => { - logging!(error, Type::Service, true, "IPC通信失败: {}", e); - bail!("无法连接到Clash Verge Service: {}", e) - } - } +/// 强制重装服务(UI修复按钮) +pub async fn force_reinstall_service() -> Result<()> { + logging!(info, Type::Service, true, "用户请求强制重装服务"); + reinstall_service().await.map_err(|err| { + logging!(error, Type::Service, true, "强制重装服务失败: {}", err); + err + }) } /// 检查服务版本 - 使用IPC通信 -pub async fn check_service_version() -> Result { - logging!(info, Type::Service, true, "开始检查服务版本 (IPC)"); +async fn check_service_version() -> Result { + let cache = CacheService::global(); + let key = CacheService::make_key("service", "version"); + let version_arc = cache + .get_or_fetch(key, SHORT_TERM_TTL, || async { + logging!(info, Type::Service, true, "开始检查服务版本 (IPC)"); + let payload = serde_json::json!({}); + let response = send_ipc_request(IpcCommand::GetVersion, payload).await?; - let payload = serde_json::json!({}); - // logging!(debug, Type::Service, true, "发送GetVersion请求"); + let data = response + .data + .ok_or_else(|| anyhow::anyhow!("服务版本响应中没有数据"))?; - match send_ipc_request(IpcCommand::GetVersion, payload).await { - Ok(response) => { - /* logging!( - debug, - Type::Service, - true, - "收到GetVersion响应: success={}, error={:?}", - response.success, - response.error - ); */ - - if !response.success { - let err_msg = response - .error - .unwrap_or_else(|| "获取服务版本失败".to_string()); - logging!(error, Type::Service, true, "获取版本错误: {}", err_msg); - bail!(err_msg); + if let Some(nested_data) = data.get("data") + && let Some(version) = nested_data.get("version").and_then(|v| v.as_str()) + { + // logging!(info, Type::Service, true, "获取到服务版本: {}", version); + return Ok(version.to_string()); } - match response.data { - Some(data) => { - if let Some(nested_data) = data.get("data") { - if let Some(version) = nested_data.get("version") { - if let Some(version_str) = version.as_str() { - logging!( - info, - Type::Service, - true, - "获取到服务版本: {}", - version_str - ); - return Ok(version_str.to_string()); - } - } - logging!( - error, - Type::Service, - true, - "嵌套数据中没有version字段: {:?}", - nested_data - ); - } else { - // 兼容旧格式 - match serde_json::from_value::(data.clone()) { - Ok(version_response) => { - logging!( - info, - Type::Service, - true, - "获取到服务版本: {}", - version_response.version - ); - return Ok(version_response.version); - } - Err(e) => { - logging!( - error, - Type::Service, - true, - "解析版本响应失败: {}; 原始数据: {:?}", - e, - data - ); - bail!("无法解析服务版本数据: {}", e) - } - } - } - bail!("响应中未找到有效的版本信息") - } - None => { - logging!(error, Type::Service, true, "版本响应中没有数据"); - bail!("服务版本响应中没有数据") - } - } - } - Err(e) => { - logging!(error, Type::Service, true, "IPC通信失败: {}", e); - bail!("无法连接到Clash Verge Service: {}", e) - } + Ok("unknown".to_string()) + }) + .await; + + match version_arc.as_ref() { + Ok(v) => Ok(v.clone()), + Err(e) => Err(anyhow::Error::msg(e.to_string())), } } /// 检查服务是否需要重装 pub async fn check_service_needs_reinstall() -> bool { - logging!(info, Type::Service, true, "开始检查服务是否需要重装"); - - let service_state = ServiceState::get(); - - if !service_state.can_reinstall() { - log::info!(target: "app", "服务重装检查: 处于冷却期或已达最大尝试次数"); - return false; - } - - // 检查版本和可用性 match check_service_version().await { - Ok(version) => { - log::info!(target: "app", "服务版本检测:当前={version}, 要求={REQUIRED_SERVICE_VERSION}"); - /* logging!( - info, - Type::Service, - true, - "服务版本检测:当前={}, 要求={}", - version, - REQUIRED_SERVICE_VERSION - ); */ - - let needs_reinstall = version != REQUIRED_SERVICE_VERSION; - if needs_reinstall { - log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={version}, 要求={REQUIRED_SERVICE_VERSION}"); - logging!(warn, Type::Service, true, "服务版本不匹配,需要重装"); - - // log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes()); - // log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes()); - } else { - log::info!(target: "app", "服务版本匹配,无需重装"); - // logging!(info, Type::Service, true, "服务版本匹配,无需重装"); - } - - needs_reinstall - } - Err(err) => { - logging!(error, Type::Service, true, "检查服务版本失败: {}", err); - - // 检查服务是否可用 - match is_service_available().await { - Ok(()) => { - log::info!(target: "app", "服务正在运行但版本检查失败: {err}"); - /* logging!( - info, - Type::Service, - true, - "服务正在运行但版本检查失败: {}", - err - ); */ - false - } - _ => { - log::info!(target: "app", "服务不可用或未运行,需要重装"); - // logging!(info, Type::Service, true, "服务不可用或未运行,需要重装"); - true - } - } - } + Ok(version) => version != REQUIRED_SERVICE_VERSION, + Err(_) => false, } } /// 尝试使用服务启动core pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> { - log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)"); - // logging!(info, Type::Service, true, "尝试使用现有服务启动核心"); + logging!(info, Type::Service, true, "尝试使用现有服务启动核心"); - let clash_core = Config::verge().latest_ref().get_valid_clash_core(); + let verge_config = Config::verge().await; + let clash_core = verge_config.latest_ref().get_valid_clash_core(); + drop(verge_config); let bin_ext = if cfg!(windows) { ".exe" } else { "" }; - let clash_bin = format!("{clash_core}{bin_ext}"); - let bin_path = current_exe()?.with_file_name(clash_bin); - let bin_path = dirs::path_to_str(&bin_path)?; + let bin_path = current_exe()?.with_file_name(format!("{clash_core}{bin_ext}")); - let config_dir = dirs::app_home_dir()?; - let config_dir = dirs::path_to_str(&config_dir)?; - - let log_path = dirs::service_log_file()?; - let log_path = dirs::path_to_str(&log_path)?; - - let config_file = dirs::path_to_str(config_file)?; - - // 构建启动参数 let payload = serde_json::json!({ "core_type": clash_core, - "bin_path": bin_path, - "config_dir": config_dir, - "config_file": config_file, - "log_file": log_path, + "bin_path": dirs::path_to_str(&bin_path)?, + "config_dir": dirs::path_to_str(&dirs::app_home_dir()?)?, + "config_file": dirs::path_to_str(config_file)?, + "log_file": dirs::path_to_str(&dirs::service_log_file()?)?, }); - // log::info!(target:"app", "启动服务参数: {:?}", payload); - // logging!(info, Type::Service, true, "发送StartClash请求"); + let response = send_ipc_request(IpcCommand::StartClash, payload) + .await + .context("无法连接到Clash Verge Service")?; - // 使用IPC通信 - match send_ipc_request(IpcCommand::StartClash, payload).await { - Ok(response) => { - /* logging!( - info, - Type::Service, - true, - "收到StartClash响应: success={}, error={:?}", - response.success, - response.error - ); */ - - if !response.success { - let err_msg = response.error.unwrap_or_else(|| "启动核心失败".to_string()); - logging!(error, Type::Service, true, "启动核心失败: {}", err_msg); - bail!(err_msg); - } - - // 添加对嵌套JSON结构的处理 - if let Some(data) = &response.data { - if let Some(code) = data.get("code") { - let code_value = code.as_u64().unwrap_or(1); - let msg = data - .get("msg") - .and_then(|m| m.as_str()) - .unwrap_or("未知错误"); - - if code_value != 0 { - logging!( - error, - Type::Service, - true, - "启动核心返回错误: code={}, msg={}", - code_value, - msg - ); - bail!("启动核心失败: {}", msg); - } - } - } - - logging!(info, Type::Service, true, "服务成功启动核心"); - Ok(()) - } - Err(e) => { - logging!(error, Type::Service, true, "启动核心IPC通信失败: {}", e); - bail!("无法连接到Clash Verge Service: {}", e) - } + if !response.success { + let err_msg = response.error.unwrap_or_else(|| "启动核心失败".to_string()); + bail!(err_msg); } + + if let Some(data) = &response.data + && let Some(code) = data.get("code").and_then(|c| c.as_u64()) + && code != 0 + { + let msg = data + .get("msg") + .and_then(|m| m.as_str()) + .unwrap_or("未知错误"); + bail!("启动核心失败: {}", msg); + } + + logging!(info, Type::Service, true, "服务成功启动核心"); + Ok(()) } // 以服务启动core pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> { - log::info!(target: "app", "正在尝试通过服务启动核心"); + logging!(info, Type::Service, true, "正在尝试通过服务启动核心"); - // 先检查服务版本,不受冷却期限制 - let version_check = match check_service_version().await { - Ok(version) => { - log::info!(target: "app", "检测到服务版本: {version}, 要求版本: {REQUIRED_SERVICE_VERSION}"); - - if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() { - log::warn!(target: "app", "服务版本不匹配,需要重装"); - false - } else { - log::info!(target: "app", "服务版本匹配"); - true - } - } - Err(err) => { - log::warn!(target: "app", "无法获取服务版本: {err}"); - false - } - }; - - if version_check && is_service_available().await.is_ok() { - log::info!(target: "app", "服务已在运行且版本匹配,尝试使用"); - return start_with_existing_service(config_file).await; - } - - if !version_check { - log::info!(target: "app", "服务版本不匹配,尝试重装"); - - let service_state = ServiceState::get(); - if !service_state.can_reinstall() { - log::warn!(target: "app", "由于限制无法重装服务"); - if let Ok(()) = start_with_existing_service(config_file).await { - log::info!(target: "app", "尽管版本不匹配,但成功启动了服务"); - return Ok(()); - } else { - bail!("服务版本不匹配且无法重装,启动失败"); - } - } - - log::info!(target: "app", "开始重装服务"); - if let Err(err) = reinstall_service().await { - log::warn!(target: "app", "服务重装失败: {err}"); - - log::info!(target: "app", "尝试使用现有服务"); - return start_with_existing_service(config_file).await; - } - - log::info!(target: "app", "服务重装成功,尝试启动"); - return start_with_existing_service(config_file).await; - } - - // 检查服务状态 - match check_ipc_service_status().await { - Ok(_) => { - log::info!(target: "app", "服务可用但未运行核心,尝试启动"); - if let Ok(()) = start_with_existing_service(config_file).await { - return Ok(()); - } - } - Err(err) => { - log::warn!(target: "app", "服务检查失败: {err}"); - } - } - - // 服务不可用或启动失败,检查是否需要重装 if check_service_needs_reinstall().await { - log::info!(target: "app", "服务需要重装"); - - if let Err(err) = reinstall_service().await { - log::warn!(target: "app", "服务重装失败: {err}"); - bail!("Failed to reinstall service: {}", err); - } - - log::info!(target: "app", "服务重装完成,尝试启动核心"); - start_with_existing_service(config_file).await - } else { - log::warn!(target: "app", "服务不可用且无法重装"); - bail!("Service is not available and cannot be reinstalled at this time") + reinstall_service().await?; } + + logging!(info, Type::Service, true, "服务已运行且版本匹配,直接使用"); + start_with_existing_service(config_file).await } /// 通过服务停止core @@ -913,254 +435,132 @@ pub(super) async fn stop_core_by_service() -> Result<()> { .context("无法连接到Clash Verge Service")?; if !response.success { - bail!(response.error.unwrap_or_else(|| "停止核心失败".to_string())); + let err_msg = response.error.unwrap_or_else(|| "停止核心失败".to_string()); + logging!(error, Type::Service, true, "停止核心失败: {}", err_msg); + bail!(err_msg); } - if let Some(data) = &response.data { - if let Some(code) = data.get("code") { - let code_value = code.as_u64().unwrap_or(1); - let msg = data - .get("msg") - .and_then(|m| m.as_str()) - .unwrap_or("未知错误"); + if let Some(data) = &response.data + && let Some(code) = data.get("code") + { + let code_value = code.as_u64().unwrap_or(1); + let msg = data + .get("msg") + .and_then(|m| m.as_str()) + .unwrap_or("未知错误"); - if code_value != 0 { - logging!( - error, - Type::Service, - true, - "停止核心返回错误: code={}, msg={}", - code_value, - msg - ); - bail!("停止核心失败: {}", msg); - } + if code_value != 0 { + logging!( + error, + Type::Service, + true, + "停止核心返回错误: code={}, msg={}", + code_value, + msg + ); + bail!("停止核心失败: {}", msg); } } + logging!(info, Type::Service, true, "服务成功停止核心"); Ok(()) } /// 检查服务是否正在运行 pub async fn is_service_available() -> Result<()> { - logging!(info, Type::Service, true, "开始检查服务是否正在运行"); + check_service_version().await?; + Ok(()) +} - match check_ipc_service_status().await { - Ok(resp) => { - if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() { - logging!(info, Type::Service, true, "服务正在运行"); +impl ServiceManager { + pub fn default() -> Self { + Self(ServiceStatus::Unavailable("Need Checks".into())) + } + + pub fn current(&self) -> ServiceStatus { + self.0.clone() + } + + pub async fn refresh(&mut self) -> Result<()> { + let status = self.check_service_comprehensive().await; + logging_error!( + Type::Service, + true, + self.handle_service_status(&status).await + ); + self.0 = status; + Ok(()) + } + + /// 综合服务状态检查(一次性完成所有检查) + pub async fn check_service_comprehensive(&self) -> ServiceStatus { + match is_service_available().await { + Ok(_) => { + logging!(info, Type::Service, true, "服务当前可用,检查是否需要重装"); + if check_service_needs_reinstall().await { + logging!(info, Type::Service, true, "服务需要重装且允许重装"); + ServiceStatus::NeedsReinstall + } else { + ServiceStatus::Ready + } + } + Err(err) => { + logging!(warn, Type::Service, true, "服务不可用,检查安装状态"); + ServiceStatus::Unavailable(err.to_string()) + } + } + } + + /// 根据服务状态执行相应操作 + pub async fn handle_service_status(&mut self, status: &ServiceStatus) -> Result<()> { + match status { + ServiceStatus::Ready => { + logging!(info, Type::Service, true, "服务就绪,直接启动"); Ok(()) - } else { + } + ServiceStatus::NeedsReinstall | ServiceStatus::ReinstallRequired => { + logging!(info, Type::Service, true, "服务需要重装,执行重装流程"); + reinstall_service().await?; + self.0 = ServiceStatus::Ready; + Ok(()) + } + ServiceStatus::ForceReinstallRequired => { logging!( - warn, + info, Type::Service, true, - "服务未正常运行: code={}, msg={}", - resp.code, - resp.msg + "服务需要强制重装,执行强制重装流程" ); + force_reinstall_service().await?; + self.0 = ServiceStatus::Ready; Ok(()) } - } - Err(err) => { - logging!(error, Type::Service, true, "检查服务运行状态失败: {}", err); - Err(err) + ServiceStatus::InstallRequired => { + logging!(info, Type::Service, true, "需要安装服务,执行安装流程"); + install_service().await?; + self.0 = ServiceStatus::Ready; + Ok(()) + } + ServiceStatus::UninstallRequired => { + logging!(info, Type::Service, true, "服务需要卸载,执行卸载流程"); + uninstall_service().await?; + self.0 = ServiceStatus::Unavailable("Service Uninstalled".into()); + Ok(()) + } + ServiceStatus::Unavailable(reason) => { + logging!( + info, + Type::Service, + true, + "服务不可用: {},将使用Sidecar模式", + reason + ); + self.0 = ServiceStatus::Unavailable(reason.clone()); + Err(anyhow::anyhow!("服务不可用: {}", reason)) + } } } } -/// 强制重装服务(UI修复按钮) -pub async fn force_reinstall_service() -> Result<()> { - log::info!(target: "app", "用户请求强制重装服务"); - - let service_state = ServiceState::default(); - service_state.save()?; - - log::info!(target: "app", "已重置服务状态,开始执行重装"); - - match reinstall_service().await { - Ok(()) => { - log::info!(target: "app", "服务重装成功"); - Ok(()) - } - Err(err) => { - log::error!(target: "app", "强制重装服务失败: {err}"); - bail!("强制重装服务失败: {}", err) - } - } -} -/* -/// 彻底诊断服务状态,检查安装状态、IPC通信和服务版本 - pub async fn diagnose_service() -> Result<()> { - logging!(info, Type::Service, true, "============= 开始服务诊断 ============="); - - // 1. 检查服务文件是否存在 - let service_path = dirs::service_path(); - match service_path { - Ok(path) => { - let service_exists = path.exists(); - logging!(info, Type::Service, true, "服务可执行文件路径: {:?}, 存在: {}", path, service_exists); - - if !service_exists { - logging!(error, Type::Service, true, "服务可执行文件不存在,需要重新安装"); - bail!("服务可执行文件不存在,需要重新安装"); - } - - // 检查服务版本文件 - let version_file = path.with_file_name("version.txt"); - if version_file.exists() { - match std::fs::read_to_string(&version_file) { - Ok(content) => { - logging!(info, Type::Service, true, "服务版本文件内容: {}", content.trim()); - } - Err(e) => { - logging!(warn, Type::Service, true, "读取服务版本文件失败: {}", e); - } - } - } else { - logging!(warn, Type::Service, true, "服务版本文件不存在: {:?}", version_file); - } - } - Err(e) => { - logging!(error, Type::Service, true, "获取服务路径失败: {}", e); - bail!("获取服务路径失败: {}", e); - } - } - - // 2. 检查IPC通信 - 命名管道/Unix套接字 - let socket_path = if cfg!(windows) { - r"\\.\pipe\clash-verge-service" - } else { - "/tmp/clash-verge-service.sock" - }; - - logging!(info, Type::Service, true, "IPC通信路径: {}", socket_path); - - if !cfg!(windows) { - // Unix系统检查套接字文件是否存在 - let socket_exists = std::path::Path::new(socket_path).exists(); - logging!(info, Type::Service, true, "Unix套接字文件是否存在: {}", socket_exists); - - if !socket_exists { - logging!(warn, Type::Service, true, "Unix套接字文件不存在,服务可能未运行"); - } - } - - // 3. 尝试通过IPC检查服务状态 - logging!(info, Type::Service, true, "尝试通过IPC通信检查服务状态..."); - match check_service().await { - Ok(resp) => { - logging!(info, Type::Service, true, "服务状态检查成功: code={}, msg={}", resp.code, resp.msg); - - // 4. 检查服务版本 - match check_service_version().await { - Ok(version) => { - logging!(info, Type::Service, true, "服务版本: {}, 要求版本: {}", - version, REQUIRED_SERVICE_VERSION); - - if version != REQUIRED_SERVICE_VERSION { - logging!(warn, Type::Service, true, "服务版本不匹配,建议重装服务"); - } else { - logging!(info, Type::Service, true, "服务版本匹配"); - } - } - Err(err) => { - logging!(error, Type::Service, true, "检查服务版本失败: {}", err); - } - } - } - Err(err) => { - logging!(error, Type::Service, true, "服务状态检查失败: {}", err); - - // 5. 检查系统服务状态 - Windows专用 - #[cfg(windows)] - { - use std::process::Command; - logging!(info, Type::Service, true, "尝试检查Windows服务状态..."); - - let output = Command::new("sc") - .args(["query", "clash_verge_service"]) - .output(); - - match output { - Ok(out) => { - let stdout = String::from_utf8_lossy(&out.stdout); - let contains_running = stdout.contains("RUNNING"); - - logging!(info, Type::Service, true, "Windows服务查询结果: {}", - if contains_running { "正在运行" } else { "未运行" }); - - if !contains_running { - logging!(info, Type::Service, true, "服务输出: {}", stdout); - } - } - Err(e) => { - logging!(error, Type::Service, true, "检查Windows服务状态失败: {}", e); - } - } - } - - // macOS专用 - #[cfg(target_os = "macos")] - { - use std::process::Command; - logging!(info, Type::Service, true, "尝试检查macOS服务状态..."); - - let output = Command::new("launchctl") - .args(["list", "io.github.clash-verge-rev.clash-verge-rev.service"]) - .output(); - - match output { - Ok(out) => { - let stdout = String::from_utf8_lossy(&out.stdout); - let stderr = String::from_utf8_lossy(&out.stderr); - - if out.status.success() { - logging!(info, Type::Service, true, "macOS服务正在运行"); - logging!(debug, Type::Service, true, "服务详情: {}", stdout); - } else { - logging!(warn, Type::Service, true, "macOS服务未运行"); - if !stderr.is_empty() { - logging!(info, Type::Service, true, "错误信息: {}", stderr); - } - } - } - Err(e) => { - logging!(error, Type::Service, true, "检查macOS服务状态失败: {}", e); - } - } - } - - // Linux专用 - #[cfg(target_os = "linux")] - { - use std::process::Command; - logging!(info, Type::Service, true, "尝试检查Linux服务状态..."); - - let output = Command::new("systemctl") - .args(["status", "clash_verge_service"]) - .output(); - - match output { - Ok(out) => { - let stdout = String::from_utf8_lossy(&out.stdout); - let is_active = stdout.contains("Active: active (running)"); - - logging!(info, Type::Service, true, "Linux服务状态: {}", - if is_active { "活跃运行中" } else { "未运行" }); - - if !is_active { - logging!(info, Type::Service, true, "服务状态详情: {}", stdout); - } - } - Err(e) => { - logging!(error, Type::Service, true, "检查Linux服务状态失败: {}", e); - } - } - } - } - } - - logging!(info, Type::Service, true, "============= 服务诊断完成 ============="); - Ok(()) -} */ +pub static SERVICE_MANAGER: Lazy> = + Lazy::new(|| Mutex::new(ServiceManager::default())); diff --git a/clash-verge-rev/src-tauri/src/core/service_ipc.rs b/clash-verge-rev/src-tauri/src/core/service_ipc.rs index d14d8484a5..c32b9de42d 100644 --- a/clash-verge-rev/src-tauri/src/core/service_ipc.rs +++ b/clash-verge-rev/src-tauri/src/core/service_ipc.rs @@ -1,9 +1,15 @@ use crate::{logging, utils::logging::Type}; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; +use backoff::{Error as BackoffError, ExponentialBackoff}; use hmac::{Hmac, Mac}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +#[cfg(unix)] +use tokio::net::UnixStream; +#[cfg(windows)] +use tokio::net::windows::named_pipe::ClientOptions; const IPC_SOCKET_NAME: &str = if cfg!(windows) { r"\\.\pipe\clash-verge-service" @@ -110,170 +116,134 @@ pub fn verify_response_signature(response: &IpcResponse) -> Result { Ok(expected_signature == response.signature) } -// IPC连接管理-win -#[cfg(target_os = "windows")] +fn create_backoff_strategy() -> ExponentialBackoff { + ExponentialBackoff { + initial_interval: Duration::from_millis(50), + max_interval: Duration::from_secs(1), + max_elapsed_time: Some(Duration::from_secs(3)), + multiplier: 1.5, + ..Default::default() + } +} + pub async fn send_ipc_request( command: IpcCommand, payload: serde_json::Value, ) -> Result { - use std::{ - ffi::CString, - fs::File, - io::{Read, Write}, - os::windows::io::{FromRawHandle, RawHandle}, - ptr, - }; - use winapi::um::{ - fileapi::{CreateFileA, OPEN_EXISTING}, - handleapi::INVALID_HANDLE_VALUE, - winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE}, - }; - - logging!(info, Type::Service, true, "正在连接服务 (Windows)..."); - let command_type = format!("{command:?}"); - let request = match create_signed_request(command, payload) { - Ok(req) => req, - Err(e) => { - logging!(error, Type::Service, true, "创建签名请求失败: {}", e); - return Err(e); + let operation = || async { + match send_ipc_request_internal(command.clone(), payload.clone()).await { + Ok(response) => Ok(response), + Err(e) => { + logging!( + warn, + Type::Service, + true, + "IPC请求失败,准备重试: 命令={}, 错误={}", + command_type, + e + ); + Err(BackoffError::transient(e)) + } } }; - let request_json = serde_json::to_string(&request)?; - - let result = tokio::task::spawn_blocking(move || -> Result { - let c_pipe_name = match CString::new(IPC_SOCKET_NAME) { - Ok(name) => name, - Err(e) => { - logging!(error, Type::Service, true, "创建CString失败: {}", e); - return Err(anyhow::anyhow!("创建CString失败: {}", e)); - } - }; - - let handle = unsafe { - CreateFileA( - c_pipe_name.as_ptr(), - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - ptr::null_mut(), - OPEN_EXISTING, - 0, - ptr::null_mut(), - ) - }; - - if handle == INVALID_HANDLE_VALUE { - let error = std::io::Error::last_os_error(); + match backoff::future::retry(create_backoff_strategy(), operation).await { + Ok(response) => { + // logging!( + // info, + // Type::Service, + // true, + // "IPC请求成功: 命令={}, 成功={}", + // command_type, + // response.success + // ); + Ok(response) + } + Err(e) => { logging!( error, Type::Service, true, - "连接到服务命名管道失败: {}", - error + "IPC请求最终失败,重试已耗尽: 命令={}, 错误={}", + command_type, + e ); - return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error)); + Err(anyhow::anyhow!("IPC请求重试失败: {}", e)) } + } +} - let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) }; - logging!(info, Type::Service, true, "服务连接成功 (Windows)"); +// 内部IPC请求实现(不带重试) +async fn send_ipc_request_internal( + command: IpcCommand, + payload: serde_json::Value, +) -> Result { + #[cfg(target_os = "windows")] + { + send_ipc_request_windows(command, payload).await + } + #[cfg(target_family = "unix")] + { + send_ipc_request_unix(command, payload).await + } +} - let request_bytes = request_json.as_bytes(); - let len_bytes = (request_bytes.len() as u32).to_be_bytes(); +// IPC连接管理-win +#[cfg(target_os = "windows")] +async fn send_ipc_request_windows( + command: IpcCommand, + payload: serde_json::Value, +) -> Result { + let request = create_signed_request(command, payload)?; + let request_json = serde_json::to_string(&request)?; + let request_bytes = request_json.as_bytes(); + let len_bytes = (request_bytes.len() as u32).to_be_bytes(); - if let Err(e) = pipe.write_all(&len_bytes) { - logging!(error, Type::Service, true, "写入请求长度失败: {}", e); - return Err(anyhow::anyhow!("写入请求长度失败: {}", e)); + let mut pipe = match ClientOptions::new().open(IPC_SOCKET_NAME) { + Ok(p) => p, + Err(e) => { + logging!(error, Type::Service, true, "连接到服务命名管道失败: {}", e); + return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", e)); } + }; - if let Err(e) = pipe.write_all(request_bytes) { - logging!(error, Type::Service, true, "写入请求内容失败: {}", e); - return Err(anyhow::anyhow!("写入请求内容失败: {}", e)); - } + logging!(info, Type::Service, true, "服务连接成功 (Windows)"); - if let Err(e) = pipe.flush() { - logging!(error, Type::Service, true, "刷新管道失败: {}", e); - return Err(anyhow::anyhow!("刷新管道失败: {}", e)); - } + pipe.write_all(&len_bytes).await?; + pipe.write_all(request_bytes).await?; + pipe.flush().await?; - let mut response_len_bytes = [0u8; 4]; - if let Err(e) = pipe.read_exact(&mut response_len_bytes) { - logging!(error, Type::Service, true, "读取响应长度失败: {}", e); - return Err(anyhow::anyhow!("读取响应长度失败: {}", e)); - } + let mut response_len_bytes = [0u8; 4]; + pipe.read_exact(&mut response_len_bytes).await?; + let response_len = u32::from_be_bytes(response_len_bytes) as usize; - let response_len = u32::from_be_bytes(response_len_bytes) as usize; + let mut response_bytes = vec![0u8; response_len]; + pipe.read_exact(&mut response_bytes).await?; - let mut response_bytes = vec![0u8; response_len]; - if let Err(e) = pipe.read_exact(&mut response_bytes) { - logging!(error, Type::Service, true, "读取响应内容失败: {}", e); - return Err(anyhow::anyhow!("读取响应内容失败: {}", e)); - } + let response: IpcResponse = serde_json::from_slice(&response_bytes) + .map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?; - let response: IpcResponse = match serde_json::from_slice::(&response_bytes) { - Ok(r) => r, - Err(e) => { - logging!(error, Type::Service, true, "服务响应解析失败: {}", e); - return Err(anyhow::anyhow!("解析响应失败: {}", e)); - } - }; + if !verify_response_signature(&response)? { + logging!(error, Type::Service, true, "服务响应签名验证失败"); + bail!("服务响应签名验证失败"); + } - match verify_response_signature(&response) { - Ok(valid) => { - if !valid { - logging!(error, Type::Service, true, "服务响应签名验证失败"); - bail!("服务响应签名验证失败"); - } - } - Err(e) => { - logging!(error, Type::Service, true, "验证响应签名时出错: {}", e); - return Err(e); - } - } - - logging!( - info, - Type::Service, - true, - "IPC请求完成: 命令={}, 成功={}", - command_type, - response.success - ); - Ok(response) - }) - .await??; - - Ok(result) + Ok(response) } // IPC连接管理-unix #[cfg(target_family = "unix")] -pub async fn send_ipc_request( +async fn send_ipc_request_unix( command: IpcCommand, payload: serde_json::Value, ) -> Result { - use std::os::unix::net::UnixStream; - - logging!(info, Type::Service, true, "正在连接服务 (Unix)..."); - - let command_type = format!("{command:?}"); - - let request = match create_signed_request(command, payload) { - Ok(req) => req, - Err(e) => { - logging!(error, Type::Service, true, "创建签名请求失败: {}", e); - return Err(e); - } - }; - + let request = create_signed_request(command, payload)?; let request_json = serde_json::to_string(&request)?; - let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) { - Ok(s) => { - logging!(info, Type::Service, true, "服务连接成功 (Unix)"); - s - } + let mut stream = match UnixStream::connect(IPC_SOCKET_NAME).await { + Ok(s) => s, Err(e) => { logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e); return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e)); @@ -283,58 +253,97 @@ pub async fn send_ipc_request( let request_bytes = request_json.as_bytes(); let len_bytes = (request_bytes.len() as u32).to_be_bytes(); - if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) { - logging!(error, Type::Service, true, "写入请求长度失败: {}", e); - return Err(anyhow::anyhow!("写入请求长度失败: {}", e)); - } - - if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) { - logging!(error, Type::Service, true, "写入请求内容失败: {}", e); - return Err(anyhow::anyhow!("写入请求内容失败: {}", e)); - } + stream.write_all(&len_bytes).await?; + stream.write_all(request_bytes).await?; + stream.flush().await?; + // 读取响应长度 let mut response_len_bytes = [0u8; 4]; - if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) { - logging!(error, Type::Service, true, "读取响应长度失败: {}", e); - return Err(anyhow::anyhow!("读取响应长度失败: {}", e)); - } - + stream.read_exact(&mut response_len_bytes).await?; let response_len = u32::from_be_bytes(response_len_bytes) as usize; let mut response_bytes = vec![0u8; response_len]; - if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) { - logging!(error, Type::Service, true, "读取响应内容失败: {}", e); - return Err(anyhow::anyhow!("读取响应内容失败: {}", e)); + stream.read_exact(&mut response_bytes).await?; + + let response: IpcResponse = serde_json::from_slice(&response_bytes) + .map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?; + + if !verify_response_signature(&response)? { + logging!(error, Type::Service, true, "服务响应签名验证失败"); + bail!("服务响应签名验证失败"); } - let response: IpcResponse = match serde_json::from_slice::(&response_bytes) { - Ok(r) => r, - Err(e) => { - logging!(error, Type::Service, true, "服务响应解析失败: {}", e,); - return Err(anyhow::anyhow!("解析响应失败: {}", e)); - } - }; - - match verify_response_signature(&response) { - Ok(valid) => { - if !valid { - logging!(error, Type::Service, true, "服务响应签名验证失败"); - bail!("服务响应签名验证失败"); - } - } - Err(e) => { - logging!(error, Type::Service, true, "验证响应签名时出错: {}", e); - return Err(e); - } - } - - logging!( - info, - Type::Service, - true, - "IPC请求完成: 命令={}, 成功={}", - command_type, - response.success - ); Ok(response) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_signed_request() { + let command = IpcCommand::GetVersion; + let payload = serde_json::json!({"test": "data"}); + + let result = create_signed_request(command, payload); + assert!(result.is_ok()); + + if let Ok(request) = result { + assert!(!request.id.is_empty()); + assert!(!request.signature.is_empty()); + assert_eq!(request.command, IpcCommand::GetVersion); + } + } + + #[test] + fn test_sign_and_verify_message() { + let test_message = "test message for signing"; + + let signature_result = sign_message(test_message); + assert!(signature_result.is_ok()); + + if let Ok(signature) = signature_result { + assert!(!signature.is_empty()); + + // 测试相同消息产生相同签名 + if let Ok(signature2) = sign_message(test_message) { + assert_eq!(signature, signature2); + } + } + } + + #[test] + fn test_verify_response_signature() { + let response = IpcResponse { + id: "test-id".to_string(), + success: true, + data: Some(serde_json::json!({"result": "success"})), + error: None, + signature: String::new(), + }; + + // 创建正确的签名 + let verification_response = IpcResponse { + id: response.id.clone(), + success: response.success, + data: response.data.clone(), + error: response.error.clone(), + signature: String::new(), + }; + + if let Ok(message) = serde_json::to_string(&verification_response) + && let Ok(correct_signature) = sign_message(&message) + { + let signed_response = IpcResponse { + signature: correct_signature, + ..response + }; + + let verification_result = verify_response_signature(&signed_response); + assert!(verification_result.is_ok()); + if let Ok(is_valid) = verification_result { + assert!(is_valid); + } + } + } +} diff --git a/clash-verge-rev/src-tauri/src/core/sysopt.rs b/clash-verge-rev/src-tauri/src/core/sysopt.rs index 758331fb63..59c886c87e 100644 --- a/clash-verge-rev/src-tauri/src/core/sysopt.rs +++ b/clash-verge-rev/src-tauri/src/core/sysopt.rs @@ -2,12 +2,11 @@ use crate::utils::autostart as startup_shortcut; use crate::{ config::{Config, IVerge}, - core::{handle::Handle, EventDrivenProxyManager}, - logging, logging_error, + core::{EventDrivenProxyManager, handle::Handle}, + logging, logging_error, singleton_lazy, utils::logging::Type, }; use anyhow::Result; -use once_cell::sync::OnceCell; use std::sync::Arc; #[cfg(not(target_os = "windows"))] use sysproxy::{Autoproxy, Sysproxy}; @@ -25,16 +24,16 @@ static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*; static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1"; #[cfg(target_os = "macos")] -static DEFAULT_BYPASS: &str = - "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,"; +static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,"; -fn get_bypass() -> String { +async fn get_bypass() -> String { let use_default = Config::verge() + .await .latest_ref() .use_default_bypass .unwrap_or(true); let res = { - let verge = Config::verge(); + let verge = Config::verge().await; let verge = verge.latest_ref(); verge.system_proxy_bypass.clone() }; @@ -52,15 +51,19 @@ fn get_bypass() -> String { } } -impl Sysopt { - pub fn global() -> &'static Sysopt { - static SYSOPT: OnceCell = OnceCell::new(); - SYSOPT.get_or_init(|| Sysopt { +impl Default for Sysopt { + fn default() -> Self { + Sysopt { update_sysproxy: Arc::new(TokioMutex::new(false)), reset_sysproxy: Arc::new(TokioMutex::new(false)), - }) + } } +} +// Use simplified singleton_lazy macro +singleton_lazy!(Sysopt, SYSOPT, Sysopt::default); + +impl Sysopt { pub fn init_guard_sysproxy(&self) -> Result<()> { // 使用事件驱动代理管理器 let proxy_manager = EventDrivenProxyManager::global(); @@ -74,14 +77,17 @@ impl Sysopt { pub async fn update_sysproxy(&self) -> Result<()> { let _lock = self.update_sysproxy.lock().await; - let port = Config::verge() - .latest_ref() - .verge_mixed_port - .unwrap_or(Config::clash().latest_ref().get_mixed_port()); + let port = { + let verge_port = Config::verge().await.latest_ref().verge_mixed_port; + match verge_port { + Some(port) => port, + None => Config::clash().await.latest_ref().get_mixed_port(), + } + }; let pac_port = IVerge::get_singleton_port(); let (sys_enable, pac_enable, proxy_host) = { - let verge = Config::verge(); + let verge = Config::verge().await; let verge = verge.latest_ref(); ( verge.enable_system_proxy.unwrap_or(false), @@ -99,7 +105,7 @@ impl Sysopt { enable: false, host: proxy_host.clone(), port, - bypass: get_bypass(), + bypass: get_bypass().await, }; let mut auto = Autoproxy { enable: false, @@ -146,7 +152,9 @@ impl Sysopt { use anyhow::bail; use tauri_plugin_shell::ShellExt; - let app_handle = Handle::global().app_handle().unwrap(); + let app_handle = Handle::global() + .app_handle() + .ok_or_else(|| anyhow::anyhow!("App handle not available"))?; let binary_path = dirs::service_path()?; let sysproxy_exe = binary_path.with_file_name("sysproxy.exe"); @@ -157,23 +165,27 @@ impl Sysopt { let shell = app_handle.shell(); let output = if pac_enable { let address = format!("http://{proxy_host}:{pac_port}/commands/pac"); - let output = shell - .command(sysproxy_exe.as_path().to_str().unwrap()) + let sysproxy_str = sysproxy_exe + .as_path() + .to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?; + shell + .command(sysproxy_str) .args(["pac", address.as_str()]) .output() - .await - .unwrap(); - output + .await? } else { let address = format!("{proxy_host}:{port}"); - let bypass = get_bypass(); - let output = shell - .command(sysproxy_exe.as_path().to_str().unwrap()) + let bypass = get_bypass().await; + let sysproxy_str = sysproxy_exe + .as_path() + .to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?; + shell + .command(sysproxy_str) .args(["global", address.as_str(), bypass.as_ref()]) .output() - .await - .unwrap(); - output + .await? }; if !output.status.success() { @@ -215,7 +227,9 @@ impl Sysopt { use anyhow::bail; use tauri_plugin_shell::ShellExt; - let app_handle = Handle::global().app_handle().unwrap(); + let app_handle = Handle::global() + .app_handle() + .ok_or_else(|| anyhow::anyhow!("App handle not available"))?; let binary_path = dirs::service_path()?; let sysproxy_exe = binary_path.with_file_name("sysproxy.exe"); @@ -225,12 +239,15 @@ impl Sysopt { } let shell = app_handle.shell(); + let sysproxy_str = sysproxy_exe + .as_path() + .to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?; let output = shell - .command(sysproxy_exe.as_path().to_str().unwrap()) + .command(sysproxy_str) .args(["set", "1"]) .output() - .await - .unwrap(); + .await?; if !output.status.success() { bail!("sysproxy exe run failed"); @@ -241,8 +258,8 @@ impl Sysopt { } /// update the startup - pub fn update_launch(&self) -> Result<()> { - let enable_auto_launch = { Config::verge().latest_ref().enable_auto_launch }; + pub async fn update_launch(&self) -> Result<()> { + let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch }; let is_enable = enable_auto_launch.unwrap_or(false); logging!(info, true, "Setting auto-launch state to: {:?}", is_enable); @@ -276,7 +293,10 @@ impl Sysopt { /// 尝试使用原来的自启动方法 fn try_original_autostart_method(&self, is_enable: bool) { - let app_handle = Handle::global().app_handle().unwrap(); + let Some(app_handle) = Handle::global().app_handle() else { + log::error!(target: "app", "App handle not available for autostart"); + return; + }; let autostart_manager = app_handle.autolaunch(); if is_enable { @@ -303,7 +323,9 @@ impl Sysopt { } // 回退到原来的方法 - let app_handle = Handle::global().app_handle().unwrap(); + let app_handle = Handle::global() + .app_handle() + .ok_or_else(|| anyhow::anyhow!("App handle not available"))?; let autostart_manager = app_handle.autolaunch(); match autostart_manager.is_enabled() { diff --git a/clash-verge-rev/src-tauri/src/core/timer.rs b/clash-verge-rev/src-tauri/src/core/timer.rs index 668a23e072..f3f01c6b3b 100644 --- a/clash-verge-rev/src-tauri/src/core/timer.rs +++ b/clash-verge-rev/src-tauri/src/core/timer.rs @@ -1,9 +1,15 @@ -use crate::{config::Config, feat, logging, logging_error, utils::logging::Type}; +use crate::{config::Config, feat, logging, logging_error, singleton, utils::logging::Type}; use anyhow::{Context, Result}; use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder}; -use once_cell::sync::OnceCell; -use parking_lot::{Mutex, RwLock}; -use std::{collections::HashMap, sync::Arc}; +use parking_lot::RwLock; +use std::{ + collections::HashMap, + pin::Pin, + sync::{ + Arc, + atomic::{AtomicBool, AtomicU64, Ordering}, + }, +}; type TaskID = u64; @@ -22,94 +28,91 @@ pub struct Timer { /// save the current state - using RwLock for better read concurrency pub timer_map: Arc>>, - /// increment id - kept as mutex since it's just a counter - pub timer_count: Arc>, + /// increment id - atomic counter for better performance + pub timer_count: AtomicU64, /// Flag to mark if timer is initialized - atomic for better performance - pub initialized: Arc, + pub initialized: AtomicBool, } -impl Timer { - pub fn global() -> &'static Timer { - static TIMER: OnceCell = OnceCell::new(); +// Use singleton macro +singleton!(Timer, TIMER_INSTANCE); - TIMER.get_or_init(|| Timer { +impl Timer { + fn new() -> Self { + Timer { delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())), timer_map: Arc::new(RwLock::new(HashMap::new())), - timer_count: Arc::new(Mutex::new(1)), - initialized: Arc::new(std::sync::atomic::AtomicBool::new(false)), - }) + timer_count: AtomicU64::new(1), + initialized: AtomicBool::new(false), + } } /// Initialize timer with better error handling and atomic operations - pub fn init(&self) -> Result<()> { + pub async fn init(&self) -> Result<()> { // Use compare_exchange for thread-safe initialization check if self .initialized - .compare_exchange( - false, - true, - std::sync::atomic::Ordering::SeqCst, - std::sync::atomic::Ordering::SeqCst, - ) + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .is_err() { logging!(debug, Type::Timer, "Timer already initialized, skipping..."); return Ok(()); } - logging!(info, Type::Timer, true, "Initializing timer..."); - // Initialize timer tasks - if let Err(e) = self.refresh() { + if let Err(e) = self.refresh().await { // Reset initialization flag on error - self.initialized - .store(false, std::sync::atomic::Ordering::SeqCst); + self.initialized.store(false, Ordering::SeqCst); logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e); return Err(e); } - let timer_map = self.timer_map.read(); - logging!( - info, - Type::Timer, - "已注册的定时任务数量: {}", - timer_map.len() - ); - - for (uid, task) in timer_map.iter() { + // Log timer info first + { + let timer_map = self.timer_map.read(); logging!( info, Type::Timer, - "注册了定时任务 - uid={}, interval={}min, task_id={}", - uid, - task.interval_minutes, - task.task_id + "已注册的定时任务数量: {}", + timer_map.len() ); + + for (uid, task) in timer_map.iter() { + logging!( + info, + Type::Timer, + "注册了定时任务 - uid={}, interval={}min, task_id={}", + uid, + task.interval_minutes, + task.task_id + ); + } } let cur_timestamp = chrono::Local::now().timestamp(); // Collect profiles that need immediate update - let profiles_to_update = if let Some(items) = Config::profiles().latest_ref().get_items() { - items - .iter() - .filter_map(|item| { - let interval = item.option.as_ref()?.update_interval? as i64; - let updated = item.updated? as i64; - let uid = item.uid.as_ref()?; + let profiles_to_update = + if let Some(items) = Config::profiles().await.latest_ref().get_items() { + items + .iter() + .filter_map(|item| { + let interval = item.option.as_ref()?.update_interval? as i64; + let updated = item.updated? as i64; + let uid = item.uid.as_ref()?; - if interval > 0 && cur_timestamp - updated >= interval * 60 { - logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid); - Some(uid.clone()) - } else { - None - } - }) - .collect::>() - } else { - Vec::new() - }; + if interval > 0 && cur_timestamp - updated >= interval * 60 { + logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid); + Some(uid.clone()) + } else { + None + } + }) + .collect::>() + } else { + Vec::new() + }; // Advance tasks outside of locks to minimize lock contention if !profiles_to_update.is_empty() { @@ -137,9 +140,9 @@ impl Timer { } /// Refresh timer tasks with better error handling - pub fn refresh(&self) -> Result<()> { + pub async fn refresh(&self) -> Result<()> { // Generate diff outside of lock to minimize lock contention - let diff_map = self.gen_diff(); + let diff_map = self.gen_diff().await; if diff_map.is_empty() { logging!(debug, Type::Timer, "No timer changes needed"); @@ -153,72 +156,80 @@ impl Timer { diff_map.len() ); - // Apply changes while holding locks - let mut timer_map = self.timer_map.write(); - let mut delay_timer = self.delay_timer.write(); + // Apply changes - first collect operations to perform without holding locks + let mut operations_to_add: Vec<(String, TaskID, u64)> = Vec::new(); + let _operations_to_remove: Vec = Vec::new(); - for (uid, diff) in diff_map { - match diff { - DiffFlag::Del(tid) => { - timer_map.remove(&uid); - if let Err(e) = delay_timer.remove_task(tid) { - logging!( - warn, - Type::Timer, - "Failed to remove task {} for uid {}: {}", - tid, - uid, - e - ); - } else { - logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid); + // Perform sync operations while holding locks + { + let mut timer_map = self.timer_map.write(); + let delay_timer = self.delay_timer.write(); + + for (uid, diff) in diff_map { + match diff { + DiffFlag::Del(tid) => { + timer_map.remove(&uid); + if let Err(e) = delay_timer.remove_task(tid) { + logging!( + warn, + Type::Timer, + "Failed to remove task {} for uid {}: {}", + tid, + uid, + e + ); + } else { + logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid); + } + } + DiffFlag::Add(tid, interval) => { + let task = TimerTask { + task_id: tid, + interval_minutes: interval, + last_run: chrono::Local::now().timestamp(), + }; + + timer_map.insert(uid.clone(), task); + operations_to_add.push((uid, tid, interval)); + } + DiffFlag::Mod(tid, interval) => { + // Remove old task first + if let Err(e) = delay_timer.remove_task(tid) { + logging!( + warn, + Type::Timer, + "Failed to remove old task {} for uid {}: {}", + tid, + uid, + e + ); + } + + // Then add the new one + let task = TimerTask { + task_id: tid, + interval_minutes: interval, + last_run: chrono::Local::now().timestamp(), + }; + + timer_map.insert(uid.clone(), task); + operations_to_add.push((uid, tid, interval)); } } - DiffFlag::Add(tid, interval) => { - let task = TimerTask { - task_id: tid, - interval_minutes: interval, - last_run: chrono::Local::now().timestamp(), - }; + } + } // Locks are dropped here - timer_map.insert(uid.clone(), task); + // Now perform async operations without holding locks + for (uid, tid, interval) in operations_to_add { + // Re-acquire locks for individual operations + let mut delay_timer = self.delay_timer.write(); + if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) { + logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e); - if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) { - logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e); - timer_map.remove(&uid); // Rollback on failure - } else { - logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid); - } - } - DiffFlag::Mod(tid, interval) => { - // Remove old task first - if let Err(e) = delay_timer.remove_task(tid) { - logging!( - warn, - Type::Timer, - "Failed to remove old task {} for uid {}: {}", - tid, - uid, - e - ); - } - - // Then add the new one - let task = TimerTask { - task_id: tid, - interval_minutes: interval, - last_run: chrono::Local::now().timestamp(), - }; - - timer_map.insert(uid.clone(), task); - - if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) { - logging_error!(Type::Timer, "Failed to update task for uid {}: {}", uid, e); - timer_map.remove(&uid); // Rollback on failure - } else { - logging!(debug, Type::Timer, "Updated task {} for uid {}", tid, uid); - } - } + // Rollback on failure - remove from timer_map + self.timer_map.write().remove(&uid); + } else { + logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid); } } @@ -226,24 +237,23 @@ impl Timer { } /// Generate map of profile UIDs to update intervals - fn gen_map(&self) -> HashMap { + async fn gen_map(&self) -> HashMap { let mut new_map = HashMap::new(); - if let Some(items) = Config::profiles().latest_ref().get_items() { + if let Some(items) = Config::profiles().await.latest_ref().get_items() { for item in items.iter() { - if let Some(option) = item.option.as_ref() { - if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) { - if interval > 0 { - logging!( - debug, - Type::Timer, - "找到定时更新配置: uid={}, interval={}min", - uid, - interval - ); - new_map.insert(uid.clone(), interval); - } - } + if let Some(option) = item.option.as_ref() + && let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) + && interval > 0 + { + logging!( + debug, + Type::Timer, + "找到定时更新配置: uid={}, interval={}min", + uid, + interval + ); + new_map.insert(uid.clone(), interval); } } } @@ -258,9 +268,9 @@ impl Timer { } /// Generate differences between current and new timer configuration - fn gen_diff(&self) -> HashMap { + async fn gen_diff(&self) -> HashMap { let mut diff_map = HashMap::new(); - let new_map = self.gen_map(); + let new_map = self.gen_map().await; // Read lock for comparing current state let timer_map = self.timer_map.read(); @@ -299,7 +309,8 @@ impl Timer { } // Find new tasks to add - let mut next_id = *self.timer_count.lock(); + let mut next_id = self.timer_count.load(Ordering::Relaxed); + let original_id = next_id; for (uid, &interval) in new_map.iter() { if !timer_map.contains_key(uid) { @@ -316,8 +327,8 @@ impl Timer { } // Update counter only if we added new tasks - if next_id > *self.timer_count.lock() { - *self.timer_count.lock() = next_id; + if next_id > original_id { + self.timer_count.store(next_id, Ordering::Relaxed); } logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len()); @@ -348,9 +359,9 @@ impl Timer { .set_frequency_repeated_by_minutes(minutes) .spawn_async_routine(move || { let uid = uid.clone(); - async move { + Box::pin(async move { Self::async_task(uid).await; - } + }) as Pin + Send>> }) .context("failed to create timer task")?; @@ -362,21 +373,24 @@ impl Timer { } /// Get next update time for a profile - pub fn get_next_update_time(&self, uid: &str) -> Option { + pub async fn get_next_update_time(&self, uid: &str) -> Option { logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid); - let timer_map = self.timer_map.read(); - let task = match timer_map.get(uid) { - Some(t) => t, - None => { - logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid); - return None; + // First extract timer task data without holding the lock across await + let task_interval = { + let timer_map = self.timer_map.read(); + match timer_map.get(uid) { + Some(t) => t.interval_minutes, + None => { + logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid); + return None; + } } }; - // Get the profile updated timestamp - let profiles_config = Config::profiles(); - let profiles = profiles_config.latest_ref(); + // Get the profile updated timestamp - now safe to await + let config_profiles = Config::profiles().await; + let profiles = config_profiles.data_ref().clone(); let items = match profiles.get_items() { Some(i) => i, None => { @@ -396,8 +410,8 @@ impl Timer { let updated = profile.updated.unwrap_or(0) as i64; // Calculate next update time - if updated > 0 && task.interval_minutes > 0 { - let next_time = updated + (task.interval_minutes as i64 * 60); + if updated > 0 && task_interval > 0 { + let next_time = updated + (task_interval as i64 * 60); logging!( info, Type::Timer, @@ -412,7 +426,7 @@ impl Timer { Type::Timer, "更新时间或间隔无效,updated={}, interval={}", updated, - task.interval_minutes + task_interval ); None } @@ -420,7 +434,6 @@ impl Timer { /// Emit update events for frontend notification fn emit_update_event(_uid: &str, _is_start: bool) { - #[cfg(any(feature = "verge-dev", feature = "default"))] { if _is_start { super::handle::Handle::notify_profile_update_started(_uid.to_string()); @@ -438,7 +451,7 @@ impl Timer { match tokio::time::timeout(std::time::Duration::from_secs(40), async { Self::emit_update_event(&uid, true); - let is_current = Config::profiles().latest_ref().current.as_ref() == Some(&uid); + let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(&uid); logging!( info, Type::Timer, diff --git a/clash-verge-rev/src-tauri/src/core/tray/mod.rs b/clash-verge-rev/src-tauri/src/core/tray/mod.rs index e7b1c2f78a..3df6a8204b 100644 --- a/clash-verge-rev/src-tauri/src/core/tray/mod.rs +++ b/clash-verge-rev/src-tauri/src/core/tray/mod.rs @@ -1,32 +1,39 @@ use once_cell::sync::OnceCell; +use tauri::Emitter; use tauri::tray::TrayIconBuilder; #[cfg(target_os = "macos")] pub mod speed_rate; use crate::ipc::Rate; +use crate::module::lightweight; +use crate::process::AsyncHandler; +use crate::utils::window_manager::WindowManager; use crate::{ - cmd, + Type, cmd, config::Config, - feat, logging, + feat, + ipc::IpcManager, + logging, module::lightweight::is_in_lightweight_mode, - utils::{dirs::find_target_icons, i18n::t, resolve::VERSION}, - Type, + singleton_lazy, + utils::{dirs::find_target_icons, i18n::t}, }; +use super::handle; use anyhow::Result; +use futures::future::join_all; use parking_lot::Mutex; +use std::collections::HashMap; use std::{ fs, sync::atomic::{AtomicBool, Ordering}, time::{Duration, Instant}, }; use tauri::{ + AppHandle, Wry, menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}, tray::{MouseButton, MouseButtonState, TrayIconEvent}, - AppHandle, Wry, }; -use super::handle; - #[derive(Clone)] struct TrayState {} @@ -66,14 +73,14 @@ pub struct Tray { } impl TrayState { - pub fn get_common_tray_icon() -> (bool, Vec) { - let verge = Config::verge().latest_ref().clone(); + pub async fn get_common_tray_icon() -> (bool, Vec) { + let verge = Config::verge().await.latest_ref().clone(); let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false); - if is_common_tray_icon { - if let Some(common_icon_path) = find_target_icons("common").unwrap() { - let icon_data = fs::read(common_icon_path).unwrap(); - return (true, icon_data); - } + if is_common_tray_icon + && let Ok(Some(common_icon_path)) = find_target_icons("common") + && let Ok(icon_data) = fs::read(common_icon_path) + { + return (true, icon_data); } #[cfg(target_os = "macos")] { @@ -100,14 +107,14 @@ impl TrayState { } } - pub fn get_sysproxy_tray_icon() -> (bool, Vec) { - let verge = Config::verge().latest_ref().clone(); + pub async fn get_sysproxy_tray_icon() -> (bool, Vec) { + let verge = Config::verge().await.latest_ref().clone(); let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false); - if is_sysproxy_tray_icon { - if let Some(sysproxy_icon_path) = find_target_icons("sysproxy").unwrap() { - let icon_data = fs::read(sysproxy_icon_path).unwrap(); - return (true, icon_data); - } + if is_sysproxy_tray_icon + && let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") + && let Ok(icon_data) = fs::read(sysproxy_icon_path) + { + return (true, icon_data); } #[cfg(target_os = "macos")] { @@ -134,14 +141,14 @@ impl TrayState { } } - pub fn get_tun_tray_icon() -> (bool, Vec) { - let verge = Config::verge().latest_ref().clone(); + pub async fn get_tun_tray_icon() -> (bool, Vec) { + let verge = Config::verge().await.latest_ref().clone(); let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false); - if is_tun_tray_icon { - if let Some(tun_icon_path) = find_target_icons("tun").unwrap() { - let icon_data = fs::read(tun_icon_path).unwrap(); - return (true, icon_data); - } + if is_tun_tray_icon + && let Ok(Some(tun_icon_path)) = find_target_icons("tun") + && let Ok(icon_data) = fs::read(tun_icon_path) + { + return (true, icon_data); } #[cfg(target_os = "macos")] { @@ -168,33 +175,37 @@ impl TrayState { } } -impl Tray { - pub fn global() -> &'static Tray { - static TRAY: OnceCell = OnceCell::new(); - - #[cfg(target_os = "macos")] - return TRAY.get_or_init(|| Tray { +impl Default for Tray { + fn default() -> Self { + Tray { last_menu_update: Mutex::new(None), menu_updating: AtomicBool::new(false), - }); - - #[cfg(not(target_os = "macos"))] - return TRAY.get_or_init(|| Tray { - last_menu_update: Mutex::new(None), - menu_updating: AtomicBool::new(false), - }); + } } +} - pub fn init(&self) -> Result<()> { +// Use simplified singleton_lazy macro +singleton_lazy!(Tray, TRAY, Tray::default); + +impl Tray { + pub async fn init(&self) -> Result<()> { + let app_handle = handle::Handle::global() + .app_handle() + .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray initialization"))?; + self.create_tray_from_handle(&app_handle).await?; Ok(()) } /// 更新托盘点击行为 - pub fn update_click_behavior(&self) -> Result<()> { - let app_handle = handle::Handle::global().app_handle().unwrap(); - let tray_event = { Config::verge().latest_ref().tray_event.clone() }; + pub async fn update_click_behavior(&self) -> Result<()> { + let app_handle = handle::Handle::global() + .app_handle() + .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?; + let tray_event = { Config::verge().await.latest_ref().tray_event.clone() }; let tray_event: String = tray_event.unwrap_or("main_window".into()); - let tray = app_handle.tray_by_id("main").unwrap(); + let tray = app_handle + .tray_by_id("main") + .ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?; match tray_event.as_str() { "tray_menu" => tray.set_show_menu_on_left_click(true)?, _ => tray.set_show_menu_on_left_click(false)?, @@ -203,7 +214,7 @@ impl Tray { } /// 更新托盘菜单 - pub fn update_menu(&self) -> Result<()> { + pub async fn update_menu(&self) -> Result<()> { // 调整最小更新间隔,确保状态及时刷新 const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100); @@ -240,7 +251,7 @@ impl Tray { // 设置更新状态 self.menu_updating.store(true, Ordering::Release); - let result = self.update_menu_internal(&app_handle); + let result = self.update_menu_internal(&app_handle).await; { let mut last_update = self.last_menu_update.lock(); @@ -251,12 +262,13 @@ impl Tray { result } - fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> { - let verge = Config::verge().latest_ref().clone(); + async fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> { + let verge = Config::verge().await.latest_ref().clone(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); let mode = { Config::clash() + .await .latest_ref() .0 .get("mode") @@ -265,6 +277,7 @@ impl Tray { .to_owned() }; let profile_uid_and_name = Config::profiles() + .await .data_mut() .all_profile_uid_and_name() .unwrap_or_default(); @@ -272,14 +285,17 @@ impl Tray { match app_handle.tray_by_id("main") { Some(tray) => { - let _ = tray.set_menu(Some(create_tray_menu( - app_handle, - Some(mode.as_str()), - *system_proxy, - *tun_mode, - profile_uid_and_name, - is_lightweight_mode, - )?)); + let _ = tray.set_menu(Some( + create_tray_menu( + app_handle, + Some(mode.as_str()), + *system_proxy, + *tun_mode, + profile_uid_and_name, + is_lightweight_mode, + ) + .await?, + )); log::debug!(target: "app", "托盘菜单更新成功"); Ok(()) } @@ -292,7 +308,7 @@ impl Tray { /// 更新托盘图标 #[cfg(target_os = "macos")] - pub fn update_icon(&self, _rate: Option) -> Result<()> { + pub async fn update_icon(&self, _rate: Option) -> Result<()> { let app_handle = match handle::Handle::global().app_handle() { Some(handle) => handle, None => { @@ -309,15 +325,15 @@ impl Tray { } }; - let verge = Config::verge().latest_ref().clone(); + let verge = Config::verge().await.latest_ref().clone(); let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { - (true, true) => TrayState::get_tun_tray_icon(), - (true, false) => TrayState::get_sysproxy_tray_icon(), - (false, true) => TrayState::get_tun_tray_icon(), - (false, false) => TrayState::get_common_tray_icon(), + (true, true) => TrayState::get_tun_tray_icon().await, + (true, false) => TrayState::get_sysproxy_tray_icon().await, + (false, true) => TrayState::get_tun_tray_icon().await, + (false, false) => TrayState::get_common_tray_icon().await, }; let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string()); @@ -329,7 +345,7 @@ impl Tray { } #[cfg(not(target_os = "macos"))] - pub fn update_icon(&self, _rate: Option) -> Result<()> { + pub async fn update_icon(&self, _rate: Option) -> Result<()> { let app_handle = match handle::Handle::global().app_handle() { Some(handle) => handle, None => { @@ -346,15 +362,15 @@ impl Tray { } }; - let verge = Config::verge().latest_ref().clone(); + let verge = Config::verge().await.latest_ref().clone(); let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { - (true, true) => TrayState::get_tun_tray_icon(), - (true, false) => TrayState::get_sysproxy_tray_icon(), - (false, true) => TrayState::get_tun_tray_icon(), - (false, false) => TrayState::get_common_tray_icon(), + (true, true) => TrayState::get_tun_tray_icon().await, + (true, false) => TrayState::get_sysproxy_tray_icon().await, + (false, true) => TrayState::get_tun_tray_icon().await, + (false, false) => TrayState::get_common_tray_icon().await, }; let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); @@ -362,18 +378,22 @@ impl Tray { } /// 更新托盘显示状态的函数 - pub fn update_tray_display(&self) -> Result<()> { - let app_handle = handle::Handle::global().app_handle().unwrap(); - let _tray = app_handle.tray_by_id("main").unwrap(); + pub async fn update_tray_display(&self) -> Result<()> { + let app_handle = handle::Handle::global() + .app_handle() + .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?; + let _tray = app_handle + .tray_by_id("main") + .ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?; // 更新菜单 - self.update_menu()?; + self.update_menu().await?; Ok(()) } /// 更新托盘提示 - pub fn update_tooltip(&self) -> Result<()> { + pub async fn update_tooltip(&self) -> Result<()> { let app_handle = match handle::Handle::global().app_handle() { Some(handle) => handle, None => { @@ -382,15 +402,7 @@ impl Tray { } }; - let version = match VERSION.get() { - Some(v) => v, - None => { - log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在"); - return Ok(()); - } - }; - - let verge = Config::verge().latest_ref().clone(); + let verge = Config::verge().await.latest_ref().clone(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); @@ -402,25 +414,33 @@ impl Tray { }; let mut current_profile_name = "None".to_string(); - let profiles = Config::profiles(); - let profiles = profiles.latest_ref(); - if let Some(current_profile_uid) = profiles.get_current() { - if let Ok(profile) = profiles.get_item(¤t_profile_uid) { + { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + if let Some(current_profile_uid) = profiles.get_current() + && let Ok(profile) = profiles.get_item(¤t_profile_uid) + { current_profile_name = match &profile.name { Some(profile_name) => profile_name.to_string(), None => current_profile_name, }; } - }; + } + // Get localized strings before using them + let sys_proxy_text = t("SysProxy").await; + let tun_text = t("TUN").await; + let profile_text = t("Profile").await; + + let version = env!("CARGO_PKG_VERSION"); if let Some(tray) = app_handle.tray_by_id("main") { let _ = tray.set_tooltip(Some(&format!( "Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}", - t("SysProxy"), + sys_proxy_text, switch_map[system_proxy], - t("TUN"), + tun_text, switch_map[tun_mode], - t("Profile"), + profile_text, current_profile_name ))); } else { @@ -430,24 +450,20 @@ impl Tray { Ok(()) } - pub fn update_part(&self) -> Result<()> { - self.update_menu()?; - self.update_icon(None)?; - self.update_tooltip()?; + pub async fn update_part(&self) -> Result<()> { + // self.update_menu().await?; // 更新轻量模式显示状态 - self.update_tray_display()?; + self.update_tray_display().await?; + self.update_icon(None).await?; + self.update_tooltip().await?; Ok(()) } - /// 取消订阅 traffic 数据 - #[cfg(target_os = "macos")] - pub fn unsubscribe_traffic(&self) {} - - pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { + pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { log::info!(target: "app", "正在从AppHandle创建系统托盘"); // 获取图标 - let icon_bytes = TrayState::get_common_tray_icon().1; + let icon_bytes = TrayState::get_common_tray_icon().await.1; let icon = tauri::image::Image::from_bytes(&icon_bytes)?; #[cfg(target_os = "linux")] @@ -455,6 +471,13 @@ impl Tray { .icon(icon) .icon_as_template(false); + #[cfg(any(target_os = "macos", target_os = "windows"))] + let show_menu_on_left_click = { + let tray_event = { Config::verge().await.latest_ref().tray_event.clone() }; + let tray_event: String = tray_event.unwrap_or("main_window".into()); + tray_event.as_str() == "tray_menu" + }; + #[cfg(not(target_os = "linux"))] let mut builder = TrayIconBuilder::with_id("main") .icon(icon) @@ -462,47 +485,50 @@ impl Tray { #[cfg(any(target_os = "macos", target_os = "windows"))] { - let tray_event = { Config::verge().latest_ref().tray_event.clone() }; - let tray_event: String = tray_event.unwrap_or("main_window".into()); - if tray_event.as_str() != "tray_menu" { + if !show_menu_on_left_click { builder = builder.show_menu_on_left_click(false); } } let tray = builder.build(app_handle)?; - tray.on_tray_icon_event(|_, event| { - let tray_event = { Config::verge().latest_ref().tray_event.clone() }; - let tray_event: String = tray_event.unwrap_or("main_window".into()); - log::debug!(target: "app","tray event: {tray_event:?}"); + tray.on_tray_icon_event(|_app_handle, event| { + AsyncHandler::spawn(|| async move { + let tray_event = { Config::verge().await.latest_ref().tray_event.clone() }; + let tray_event: String = tray_event.unwrap_or("main_window".into()); + log::debug!(target: "app", "tray event: {tray_event:?}"); - if let TrayIconEvent::Click { - button: MouseButton::Left, - button_state: MouseButtonState::Down, - .. - } = event - { - // 添加防抖检查,防止快速连击 - if !should_handle_tray_click() { - return; - } - - match tray_event.as_str() { - "system_proxy" => feat::toggle_system_proxy(), - "tun_mode" => feat::toggle_tun_mode(None), - "main_window" => { - use crate::utils::window_manager::WindowManager; - log::info!(target: "app", "Tray点击事件: 显示主窗口"); - if crate::module::lightweight::is_in_lightweight_mode() { - log::info!(target: "app", "当前在轻量模式,正在退出轻量模式"); - crate::module::lightweight::exit_lightweight_mode(); - } - let result = WindowManager::show_main_window(); - log::info!(target: "app", "窗口显示结果: {result:?}"); + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Down, + .. + } = event + { + // 添加防抖检查,防止快速连击 + if !should_handle_tray_click() { + return; } - _ => {} + + use std::future::Future; + use std::pin::Pin; + + let fut: Pin + Send>> = match tray_event.as_str() { + "system_proxy" => Box::pin(async move { + feat::toggle_system_proxy().await; + }), + "tun_mode" => Box::pin(async move { + feat::toggle_tun_mode(None).await; + }), + "main_window" => Box::pin(async move { + if !lightweight::exit_lightweight_mode().await { + WindowManager::show_main_window().await; + }; + }), + _ => Box::pin(async move {}), + }; + fut.await; } - } + }); }); tray.on_menu_event(on_menu_event); log::info!(target: "app", "系统托盘创建成功"); @@ -510,18 +536,18 @@ impl Tray { } // 托盘统一的状态更新函数 - pub fn update_all_states(&self) -> Result<()> { + pub async fn update_all_states(&self) -> Result<()> { // 确保所有状态更新完成 - self.update_menu()?; - self.update_icon(None)?; - self.update_tooltip()?; - self.update_tray_display()?; + self.update_tray_display().await?; + // self.update_menu().await?; + self.update_icon(None).await?; + self.update_tooltip().await?; Ok(()) } } -fn create_tray_menu( +async fn create_tray_menu( app_handle: &AppHandle, mode: Option<&str>, system_proxy_enabled: bool, @@ -531,10 +557,30 @@ fn create_tray_menu( ) -> Result> { let mode = mode.unwrap_or(""); - let unknown_version = String::from("unknown"); - let version = VERSION.get().unwrap_or(&unknown_version); + // 获取当前配置文件的选中代理组信息 + let current_profile_selected = { + let profiles_config = Config::profiles().await; + let profiles_ref = profiles_config.latest_ref(); + profiles_ref + .get_current() + .and_then(|uid| profiles_ref.get_item(&uid).ok()) + .and_then(|profile| profile.selected.clone()) + .unwrap_or_default() + }; + + let proxy_nodes_data = cmd::get_proxies().await.unwrap_or_else(|e| { + logging!( + error, + Type::Cmd, + "Failed to fetch proxies for tray menu: {e}" + ); + serde_json::Value::Object(serde_json::Map::new()) + }); + + let version = env!("CARGO_PKG_VERSION"); let hotkeys = Config::verge() + .await .latest_ref() .hotkeys .as_ref() @@ -551,24 +597,201 @@ fn create_tray_menu( }) .unwrap_or_default(); - let profile_menu_items: Vec> = profile_uid_and_name - .iter() - .map(|(profile_uid, profile_name)| { - let is_current_profile = Config::profiles() - .data_mut() - .is_current_profile_index(profile_uid.to_string()); - CheckMenuItem::with_id( - app_handle, - format!("profiles_{profile_uid}"), - t(profile_name), - true, - is_current_profile, - None::<&str>, - ) - .unwrap() - }) - .collect(); - let profile_menu_items: Vec<&dyn IsMenuItem> = profile_menu_items + let profile_menu_items: Vec> = { + let futures = profile_uid_and_name + .iter() + .map(|(profile_uid, profile_name)| { + let app_handle = app_handle.clone(); + let profile_uid = profile_uid.clone(); + let profile_name = profile_name.clone(); + async move { + let is_current_profile = Config::profiles() + .await + .data_mut() + .is_current_profile_index(profile_uid.to_string()); + CheckMenuItem::with_id( + &app_handle, + format!("profiles_{profile_uid}"), + t(&profile_name).await, + true, + is_current_profile, + None::<&str>, + ) + } + }); + let results = join_all(futures).await; + results.into_iter().collect::, _>>()? + }; + + // 代理组子菜单 + let proxy_submenus: Vec> = { + let mut submenus = Vec::new(); + let mut group_name_submenus_hash = HashMap::new(); + + if let Some(proxies) = proxy_nodes_data.get("proxies").and_then(|v| v.as_object()) { + for (group_name, group_data) in proxies.iter() { + // Filter groups based on mode + let should_show = match mode { + "global" => group_name == "GLOBAL", + _ => group_name != "GLOBAL", + } && + // Check if the group is hidden + !group_data.get("hidden").and_then(|v| v.as_bool()).unwrap_or(false); + + if !should_show { + continue; + } + + let Some(all_proxies) = group_data.get("all").and_then(|v| v.as_array()) else { + continue; + }; + + let now_proxy = group_data.get("now").and_then(|v| v.as_str()).unwrap_or(""); + + // Create proxy items + let group_items: Vec> = all_proxies + .iter() + .filter_map(|proxy_name| proxy_name.as_str()) + .filter_map(|proxy_str| { + let is_selected = proxy_str == now_proxy; + let item_id = format!("proxy_{}_{}", group_name, proxy_str); + + // Get delay for display + let delay_text = proxies + .get(proxy_str) + .and_then(|p| p.get("history")) + .and_then(|h| h.as_array()) + .and_then(|h| h.last()) + .and_then(|r| r.get("delay")) + .and_then(|d| d.as_i64()) + .map(|delay| match delay { + -1 => "-ms".to_string(), + delay if delay >= 10000 => "-ms".to_string(), + _ => format!("{}ms", delay), + }) + .unwrap_or_else(|| "-ms".to_string()); + + let display_text = format!("{} | {}", proxy_str, delay_text); + + CheckMenuItem::with_id( + app_handle, + item_id, + display_text, + true, + is_selected, + None::<&str>, + ) + .map_err(|e| log::warn!(target: "app", "创建代理菜单项失败: {}", e)) + .ok() + }) + .collect(); + + if group_items.is_empty() { + continue; + } + + // Determine if group is active + let is_group_active = match mode { + "global" => group_name == "GLOBAL" && !now_proxy.is_empty(), + "direct" => false, + _ => { + current_profile_selected + .iter() + .any(|s| s.name.as_deref() == Some(group_name)) + && !now_proxy.is_empty() + } + }; + + let group_display_name = if is_group_active { + format!("✓ {}", group_name) + } else { + group_name.to_string() + }; + + let group_items_refs: Vec<&dyn IsMenuItem> = group_items + .iter() + .map(|item| item as &dyn IsMenuItem) + .collect(); + + if let Ok(submenu) = Submenu::with_id_and_items( + app_handle, + format!("proxy_group_{}", group_name), + group_display_name, + true, + &group_items_refs, + ) { + group_name_submenus_hash.insert(group_name.to_string(), submenu); + } else { + log::warn!(target: "app", "创建代理组子菜单失败: {}", group_name); + } + } + } + + // 获取运行时代理组配置 + let runtime_proxy_groups_config = cmd::get_runtime_config() + .await + .map_err(|e| { + logging!( + error, + Type::Cmd, + "Failed to fetch runtime proxy groups for tray menu: {e}" + ); + }) + .ok() + .flatten() + .map(|config| { + config + .get("proxy-groups") + .and_then(|groups| groups.as_sequence()) + .map(|groups| { + groups + .iter() + .filter_map(|group| group.get("name")) + .filter_map(|name| name.as_str()) + .map(|name| name.to_string()) + .collect::>() + }) + .unwrap_or_default() + }); + + if let Some(runtime_proxy_groups_config) = runtime_proxy_groups_config { + for group_name in runtime_proxy_groups_config { + if let Some(submenu) = group_name_submenus_hash.get(&group_name) { + submenus.push(submenu.clone()); + } + } + } else { + for (_, submenu) in group_name_submenus_hash { + submenus.push(submenu); + } + } + + submenus + }; + + // Pre-fetch all localized strings + let dashboard_text = t("Dashboard").await; + let rule_mode_text = t("Rule Mode").await; + let global_mode_text = t("Global Mode").await; + let direct_mode_text = t("Direct Mode").await; + let profiles_text = t("Profiles").await; + let proxies_text = t("Proxies").await; + let system_proxy_text = t("System Proxy").await; + let tun_mode_text = t("TUN Mode").await; + let lightweight_mode_text = t("LightWeight Mode").await; + let copy_env_text = t("Copy Env").await; + let conf_dir_text = t("Conf Dir").await; + let core_dir_text = t("Core Dir").await; + let logs_dir_text = t("Logs Dir").await; + let open_dir_text = t("Open Dir").await; + let restart_clash_text = t("Restart Clash Core").await; + let restart_app_text = t("Restart App").await; + let verge_version_text = t("Verge Version").await; + let more_text = t("More").await; + let exit_text = t("Exit").await; + + // Convert to references only when needed + let profile_menu_items_refs: Vec<&dyn IsMenuItem> = profile_menu_items .iter() .map(|item| item as &dyn IsMenuItem) .collect(); @@ -576,261 +799,297 @@ fn create_tray_menu( let open_window = &MenuItem::with_id( app_handle, "open_window", - t("Dashboard"), + dashboard_text, true, hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()), - ) - .unwrap(); + )?; let rule_mode = &CheckMenuItem::with_id( app_handle, "rule_mode", - t("Rule Mode"), + rule_mode_text, true, mode == "rule", hotkeys.get("clash_mode_rule").map(|s| s.as_str()), - ) - .unwrap(); + )?; let global_mode = &CheckMenuItem::with_id( app_handle, "global_mode", - t("Global Mode"), + global_mode_text, true, mode == "global", hotkeys.get("clash_mode_global").map(|s| s.as_str()), - ) - .unwrap(); + )?; let direct_mode = &CheckMenuItem::with_id( app_handle, "direct_mode", - t("Direct Mode"), + direct_mode_text, true, mode == "direct", hotkeys.get("clash_mode_direct").map(|s| s.as_str()), - ) - .unwrap(); + )?; let profiles = &Submenu::with_id_and_items( app_handle, "profiles", - t("Profiles"), + profiles_text, true, - &profile_menu_items, - ) - .unwrap(); + &profile_menu_items_refs, + )?; + + // 创建代理主菜单 + let proxies_submenu = if !proxy_submenus.is_empty() { + let proxy_submenu_refs: Vec<&dyn IsMenuItem> = proxy_submenus + .iter() + .map(|submenu| submenu as &dyn IsMenuItem) + .collect(); + + Some(Submenu::with_id_and_items( + app_handle, + "proxies", + proxies_text, + true, + &proxy_submenu_refs, + )?) + } else { + None + }; let system_proxy = &CheckMenuItem::with_id( app_handle, "system_proxy", - t("System Proxy"), + system_proxy_text, true, system_proxy_enabled, hotkeys.get("toggle_system_proxy").map(|s| s.as_str()), - ) - .unwrap(); + )?; let tun_mode = &CheckMenuItem::with_id( app_handle, "tun_mode", - t("TUN Mode"), + tun_mode_text, true, tun_mode_enabled, hotkeys.get("toggle_tun_mode").map(|s| s.as_str()), - ) - .unwrap(); + )?; let lighteweight_mode = &CheckMenuItem::with_id( app_handle, "entry_lightweight_mode", - t("LightWeight Mode"), + lightweight_mode_text, true, is_lightweight_mode, hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()), - ) - .unwrap(); + )?; - let copy_env = - &MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap(); + let copy_env = &MenuItem::with_id(app_handle, "copy_env", copy_env_text, true, None::<&str>)?; let open_app_dir = &MenuItem::with_id( app_handle, "open_app_dir", - t("Conf Dir"), + conf_dir_text, true, None::<&str>, - ) - .unwrap(); + )?; let open_core_dir = &MenuItem::with_id( app_handle, "open_core_dir", - t("Core Dir"), + core_dir_text, true, None::<&str>, - ) - .unwrap(); + )?; let open_logs_dir = &MenuItem::with_id( app_handle, "open_logs_dir", - t("Logs Dir"), + logs_dir_text, true, None::<&str>, - ) - .unwrap(); + )?; let open_dir = &Submenu::with_id_and_items( app_handle, "open_dir", - t("Open Dir"), + open_dir_text, true, &[open_app_dir, open_core_dir, open_logs_dir], - ) - .unwrap(); + )?; let restart_clash = &MenuItem::with_id( app_handle, "restart_clash", - t("Restart Clash Core"), + restart_clash_text, true, None::<&str>, - ) - .unwrap(); + )?; let restart_app = &MenuItem::with_id( app_handle, "restart_app", - t("Restart App"), + restart_app_text, true, None::<&str>, - ) - .unwrap(); + )?; let app_version = &MenuItem::with_id( app_handle, "app_version", - format!("{} {version}", t("Verge Version")), + format!("{} {version}", verge_version_text), true, None::<&str>, - ) - .unwrap(); + )?; let more = &Submenu::with_id_and_items( app_handle, "more", - t("More"), + more_text, true, &[restart_clash, restart_app, app_version], - ) - .unwrap(); + )?; - let quit = - &MenuItem::with_id(app_handle, "quit", t("Exit"), true, Some("CmdOrControl+Q")).unwrap(); + let quit = &MenuItem::with_id(app_handle, "quit", exit_text, true, Some("CmdOrControl+Q"))?; - let separator = &PredefinedMenuItem::separator(app_handle).unwrap(); + let separator = &PredefinedMenuItem::separator(app_handle)?; + + // 动态构建菜单项 + let mut menu_items: Vec<&dyn IsMenuItem> = vec![ + open_window, + separator, + rule_mode, + global_mode, + direct_mode, + separator, + profiles, + ]; + + // 如果有代理节点,添加代理节点菜单 + if let Some(ref proxies_menu) = proxies_submenu { + menu_items.push(proxies_menu); + } + + menu_items.extend_from_slice(&[ + separator, + system_proxy as &dyn IsMenuItem, + tun_mode as &dyn IsMenuItem, + separator, + lighteweight_mode as &dyn IsMenuItem, + copy_env as &dyn IsMenuItem, + open_dir as &dyn IsMenuItem, + more as &dyn IsMenuItem, + separator, + quit as &dyn IsMenuItem, + ]); let menu = tauri::menu::MenuBuilder::new(app_handle) - .items(&[ - open_window, - separator, - rule_mode, - global_mode, - direct_mode, - separator, - profiles, - separator, - system_proxy, - tun_mode, - separator, - lighteweight_mode, - copy_env, - open_dir, - more, - separator, - quit, - ]) - .build() - .unwrap(); + .items(&menu_items) + .build()?; Ok(menu) } fn on_menu_event(_: &AppHandle, event: MenuEvent) { - match event.id.as_ref() { - mode @ ("rule_mode" | "global_mode" | "direct_mode") => { - let mode = &mode[0..mode.len() - 5]; - logging!( - info, - Type::ProxyMode, - true, - "Switch Proxy Mode To: {}", - mode - ); - feat::change_clash_mode(mode.into()); - } - "open_window" => { - use crate::utils::window_manager::WindowManager; - log::info!(target: "app", "托盘菜单点击: 打开窗口"); - - if !should_handle_tray_click() { - return; + AsyncHandler::spawn(|| async move { + match event.id.as_ref() { + mode @ ("rule_mode" | "global_mode" | "direct_mode") => { + let mode = &mode[0..mode.len() - 5]; // Removing the "_mode" suffix + logging!( + info, + Type::ProxyMode, + true, + "Switch Proxy Mode To: {}", + mode + ); + feat::change_clash_mode(mode.into()).await; } + "open_window" => { + log::info!(target: "app", "托盘菜单点击: 打开窗口"); - if crate::module::lightweight::is_in_lightweight_mode() { - log::info!(target: "app", "当前在轻量模式,正在退出"); - crate::module::lightweight::exit_lightweight_mode(); + if !should_handle_tray_click() { + return; + } + if !lightweight::exit_lightweight_mode().await { + WindowManager::show_main_window().await; + }; } - let result = WindowManager::show_main_window(); - log::info!(target: "app", "窗口显示结果: {result:?}"); - } - "system_proxy" => { - feat::toggle_system_proxy(); - } - "tun_mode" => { - feat::toggle_tun_mode(None); - } - "copy_env" => feat::copy_clash_env(), - "open_app_dir" => { - let _ = cmd::open_app_dir(); - } - "open_core_dir" => { - let _ = cmd::open_core_dir(); - } - "open_logs_dir" => { - let _ = cmd::open_logs_dir(); - } - "restart_clash" => feat::restart_clash_core(), - "restart_app" => feat::restart_app(), - "entry_lightweight_mode" => { - if !should_handle_tray_click() { - return; + "system_proxy" => { + feat::toggle_system_proxy().await; } + "tun_mode" => { + feat::toggle_tun_mode(None).await; + } + "copy_env" => feat::copy_clash_env().await, + "open_app_dir" => { + let _ = cmd::open_app_dir().await; + } + "open_core_dir" => { + let _ = cmd::open_core_dir().await; + } + "open_logs_dir" => { + let _ = cmd::open_logs_dir().await; + } + "restart_clash" => feat::restart_clash_core().await, + "restart_app" => feat::restart_app().await, + "entry_lightweight_mode" => { + if !should_handle_tray_click() { + return; + } + if !is_in_lightweight_mode() { + lightweight::entry_lightweight_mode().await; // Await async function + } else { + lightweight::exit_lightweight_mode().await; // Await async function + } + } + "quit" => { + feat::quit().await; + } + id if id.starts_with("profiles_") => { + let profile_index = &id["profiles_".len()..]; + feat::toggle_proxy_profile(profile_index.into()).await; + } + id if id.starts_with("proxy_") => { + // proxy_{group_name}_{proxy_name} + let parts: Vec<&str> = id.splitn(3, '_').collect(); - let was_lightweight = crate::module::lightweight::is_in_lightweight_mode(); - if was_lightweight { - crate::module::lightweight::exit_lightweight_mode(); - } else { - crate::module::lightweight::entry_lightweight_mode(); - } + if parts.len() == 3 && parts[0] == "proxy" { + let group_name = parts[1]; + let proxy_name = parts[2]; - if was_lightweight { - use crate::utils::window_manager::WindowManager; - let result = WindowManager::show_main_window(); - log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}"); - } - } - "quit" => { - feat::quit(); - } - id if id.starts_with("profiles_") => { - let profile_index = &id["profiles_".len()..]; - feat::toggle_proxy_profile(profile_index.into()); - } - _ => {} - } + match cmd::proxy::update_proxy_and_sync( + group_name.to_string(), + proxy_name.to_string(), + ) + .await + { + Ok(_) => { + log::info!(target: "app", "切换代理成功: {} -> {}", group_name, proxy_name); + } + Err(e) => { + log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e); - if let Err(e) = Tray::global().update_all_states() { - log::warn!(target: "app", "更新托盘状态失败: {e}"); - } + // Fallback to IPC update + if (IpcManager::global() + .update_proxy(group_name, proxy_name) + .await) + .is_ok() + { + log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name); + + if let Some(app_handle) = handle::Handle::global().app_handle() { + let _ = app_handle.emit("verge://force-refresh-proxies", ()); + } + } + } + } + } + } + _ => {} + } + + // Ensure tray state update is awaited and properly handled + if let Err(e) = Tray::global().update_all_states().await { + log::warn!(target: "app", "更新托盘状态失败: {e}"); + } + }); } diff --git a/clash-verge-rev/src-tauri/src/core/win_uwp.rs b/clash-verge-rev/src-tauri/src/core/win_uwp.rs index 4a19b93fd4..3db9a1a44d 100644 --- a/clash-verge-rev/src-tauri/src/core/win_uwp.rs +++ b/clash-verge-rev/src-tauri/src/core/win_uwp.rs @@ -1,12 +1,12 @@ #![cfg(target_os = "windows")] use crate::utils::dirs; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use deelevate::{PrivilegeLevel, Token}; use runas::Command as RunasCommand; use std::process::Command as StdCommand; -pub async fn invoke_uwptools() -> Result<()> { +pub fn invoke_uwptools() -> Result<()> { let resource_dir = dirs::app_resources_dir()?; let tool_path = resource_dir.join("enableLoopback.exe"); diff --git a/clash-verge-rev/src-tauri/src/enhance/builtin/meta_guard.js b/clash-verge-rev/src-tauri/src/enhance/builtin/meta_guard.js index 33b048f238..5dd56980f9 100644 --- a/clash-verge-rev/src-tauri/src/enhance/builtin/meta_guard.js +++ b/clash-verge-rev/src-tauri/src/enhance/builtin/meta_guard.js @@ -1,3 +1,5 @@ +// This function is exported for use by the Clash core +// eslint-disable-next-line no-unused-vars function main(config, _name) { if (config.mode === "script") { config.mode = "rule"; diff --git a/clash-verge-rev/src-tauri/src/enhance/builtin/meta_hy_alpn.js b/clash-verge-rev/src-tauri/src/enhance/builtin/meta_hy_alpn.js index dda6366caa..b542ec95e5 100644 --- a/clash-verge-rev/src-tauri/src/enhance/builtin/meta_hy_alpn.js +++ b/clash-verge-rev/src-tauri/src/enhance/builtin/meta_hy_alpn.js @@ -1,3 +1,5 @@ +// This function is exported for use by the Clash core +// eslint-disable-next-line no-unused-vars function main(config, _name) { if (Array.isArray(config.proxies)) { config.proxies.forEach((p, i) => { diff --git a/clash-verge-rev/src-tauri/src/enhance/chain.rs b/clash-verge-rev/src-tauri/src/enhance/chain.rs index 2fb7e95129..9f9b0e8b06 100644 --- a/clash-verge-rev/src-tauri/src/enhance/chain.rs +++ b/clash-verge-rev/src-tauri/src/enhance/chain.rs @@ -3,7 +3,7 @@ use crate::{ config::PrfItem, utils::{dirs, help}, }; -use serde_yaml::Mapping; +use serde_yaml_ng::Mapping; use std::fs; #[derive(Debug, Clone)] @@ -22,6 +22,7 @@ pub enum ChainType { } #[derive(Debug, Clone)] +#[allow(dead_code)] pub enum ChainSupport { Clash, ClashMeta, @@ -29,8 +30,49 @@ pub enum ChainSupport { All, } -impl From<&PrfItem> for Option { - fn from(item: &PrfItem) -> Self { +// impl From<&PrfItem> for Option { +// fn from(item: &PrfItem) -> Self { +// let itype = item.itype.as_ref()?.as_str(); +// let file = item.file.clone()?; +// let uid = item.uid.clone().unwrap_or("".into()); +// let path = dirs::app_profiles_dir().ok()?.join(file); + +// if !path.exists() { +// return None; +// } + +// match itype { +// "script" => Some(ChainItem { +// uid, +// data: ChainType::Script(fs::read_to_string(path).ok()?), +// }), +// "merge" => Some(ChainItem { +// uid, +// data: ChainType::Merge(help::read_mapping(&path).ok()?), +// }), +// "rules" => Some(ChainItem { +// uid, +// data: ChainType::Rules(help::read_seq_map(&path).ok()?), +// }), +// "proxies" => Some(ChainItem { +// uid, +// data: ChainType::Proxies(help::read_seq_map(&path).ok()?), +// }), +// "groups" => Some(ChainItem { +// uid, +// data: ChainType::Groups(help::read_seq_map(&path).ok()?), +// }), +// _ => None, +// } +// } +// } +// Helper trait to allow async conversion +pub trait AsyncChainItemFrom { + async fn from_async(item: &PrfItem) -> Option; +} + +impl AsyncChainItemFrom for Option { + async fn from_async(item: &PrfItem) -> Option { let itype = item.itype.as_ref()?.as_str(); let file = item.file.clone()?; let uid = item.uid.clone().unwrap_or("".into()); @@ -47,25 +89,33 @@ impl From<&PrfItem> for Option { }), "merge" => Some(ChainItem { uid, - data: ChainType::Merge(help::read_mapping(&path).ok()?), - }), - "rules" => Some(ChainItem { - uid, - data: ChainType::Rules(help::read_seq_map(&path).ok()?), - }), - "proxies" => Some(ChainItem { - uid, - data: ChainType::Proxies(help::read_seq_map(&path).ok()?), - }), - "groups" => Some(ChainItem { - uid, - data: ChainType::Groups(help::read_seq_map(&path).ok()?), + data: ChainType::Merge(help::read_mapping(&path).await.ok()?), }), + "rules" => { + let seq_map = help::read_seq_map(&path).await.ok()?; + Some(ChainItem { + uid, + data: ChainType::Rules(seq_map), + }) + } + "proxies" => { + let seq_map = help::read_seq_map(&path).await.ok()?; + Some(ChainItem { + uid, + data: ChainType::Proxies(seq_map), + }) + } + "groups" => { + let seq_map = help::read_seq_map(&path).await.ok()?; + Some(ChainItem { + uid, + data: ChainType::Groups(seq_map), + }) + } _ => None, } } } - impl ChainItem { /// 内建支持一些脚本 pub fn builtin() -> Vec<(ChainSupport, ChainItem)> { diff --git a/clash-verge-rev/src-tauri/src/enhance/field.rs b/clash-verge-rev/src-tauri/src/enhance/field.rs index f58b588c15..77dbeced57 100644 --- a/clash-verge-rev/src-tauri/src/enhance/field.rs +++ b/clash-verge-rev/src-tauri/src/enhance/field.rs @@ -1,4 +1,4 @@ -use serde_yaml::{Mapping, Value}; +use serde_yaml_ng::{Mapping, Value}; use std::collections::HashSet; pub const HANDLE_FIELDS: [&str; 12] = [ diff --git a/clash-verge-rev/src-tauri/src/enhance/merge.rs b/clash-verge-rev/src-tauri/src/enhance/merge.rs index 0aa81e7e90..0210851d34 100644 --- a/clash-verge-rev/src-tauri/src/enhance/merge.rs +++ b/clash-verge-rev/src-tauri/src/enhance/merge.rs @@ -1,5 +1,5 @@ use super::use_lowercase; -use serde_yaml::{self, Mapping, Value}; +use serde_yaml_ng::{self, Mapping, Value}; fn deep_merge(a: &mut Value, b: &Value) { match (a, b) { @@ -18,9 +18,10 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping { deep_merge(&mut config, &Value::from(merge)); - let config = config.as_mapping().unwrap().clone(); - - config + config.as_mapping().cloned().unwrap_or_else(|| { + log::error!("Failed to convert merged config to mapping, using empty mapping"); + Mapping::new() + }) } #[test] @@ -51,10 +52,10 @@ fn test_merge() -> anyhow::Result<()> { script1: test "; - let merge = serde_yaml::from_str::(merge)?; - let config = serde_yaml::from_str::(config)?; + let merge = serde_yaml_ng::from_str::(merge)?; + let config = serde_yaml_ng::from_str::(config)?; - let _ = serde_yaml::to_string(&use_merge(merge, config))?; + let _ = serde_yaml_ng::to_string(&use_merge(merge, config))?; Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/enhance/mod.rs b/clash-verge-rev/src-tauri/src/enhance/mod.rs index 9ede0bad0a..e3dfbfa51d 100644 --- a/clash-verge-rev/src-tauri/src/enhance/mod.rs +++ b/clash-verge-rev/src-tauri/src/enhance/mod.rs @@ -7,7 +7,7 @@ mod tun; use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*}; use crate::{config::Config, utils::tmpl}; -use serde_yaml::Mapping; +use serde_yaml_ng::Mapping; use std::collections::{HashMap, HashSet}; type ResultLog = Vec<(String, String)>; @@ -16,10 +16,10 @@ type ResultLog = Vec<(String, String)>; /// 返回最终订阅、该订阅包含的键、和script执行的结果 pub async fn enhance() -> (Mapping, Vec, HashMap) { // config.yaml 的订阅 - let clash_config = { Config::clash().latest_ref().0.clone() }; + let clash_config = { Config::clash().await.latest_ref().0.clone() }; let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = { - let verge = Config::verge(); + let verge = Config::verge().await; let verge = verge.latest_ref(); ( Some(verge.get_valid_clash_core()), @@ -32,18 +32,18 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { }; #[cfg(not(target_os = "windows"))] let redir_enabled = { - let verge = Config::verge(); + let verge = Config::verge().await; let verge = verge.latest_ref(); verge.verge_redir_enabled.unwrap_or(false) }; #[cfg(target_os = "linux")] let tproxy_enabled = { - let verge = Config::verge(); + let verge = Config::verge().await; let verge = verge.latest_ref(); verge.verge_tproxy_enabled.unwrap_or(false) }; - // 从profiles里拿东西 + // 从profiles里拿东西 - 先收集需要的数据,然后释放锁 let ( mut config, merge_item, @@ -55,74 +55,172 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { global_script, profile_name, ) = { - let profiles = Config::profiles(); - let profiles = profiles.latest_ref(); + // 收集所有需要的数据,然后释放profiles锁 + let ( + current, + merge_uid, + script_uid, + rules_uid, + proxies_uid, + groups_uid, + _current_profile_uid, + name, + ) = { + // 分离async调用和数据获取,避免借用检查问题 + let current = { + let profiles = Config::profiles().await; + let profiles_clone = profiles.latest_ref().clone(); + profiles_clone.current_mapping().await.unwrap_or_default() + }; - let current = profiles.current_mapping().unwrap_or_default(); - let merge = profiles - .get_item(&profiles.current_merge().unwrap_or_default()) - .ok() - .and_then(>::from) - .unwrap_or_else(|| ChainItem { - uid: "".into(), - data: ChainType::Merge(Mapping::new()), - }); - let script = profiles - .get_item(&profiles.current_script().unwrap_or_default()) - .ok() - .and_then(>::from) - .unwrap_or_else(|| ChainItem { - uid: "".into(), - data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), - }); - let rules = profiles - .get_item(&profiles.current_rules().unwrap_or_default()) - .ok() - .and_then(>::from) - .unwrap_or_else(|| ChainItem { - uid: "".into(), - data: ChainType::Rules(SeqMap::default()), - }); - let proxies = profiles - .get_item(&profiles.current_proxies().unwrap_or_default()) - .ok() - .and_then(>::from) - .unwrap_or_else(|| ChainItem { - uid: "".into(), - data: ChainType::Proxies(SeqMap::default()), - }); - let groups = profiles - .get_item(&profiles.current_groups().unwrap_or_default()) - .ok() - .and_then(>::from) - .unwrap_or_else(|| ChainItem { - uid: "".into(), - data: ChainType::Groups(SeqMap::default()), - }); + // 重新获取锁进行其他操作 + let profiles = Config::profiles().await; + let profiles_ref = profiles.latest_ref(); - let global_merge = profiles - .get_item(&"Merge".to_string()) - .ok() - .and_then(>::from) - .unwrap_or_else(|| ChainItem { - uid: "Merge".into(), - data: ChainType::Merge(Mapping::new()), - }); + let merge_uid = profiles_ref.current_merge().unwrap_or_default(); + let script_uid = profiles_ref.current_script().unwrap_or_default(); + let rules_uid = profiles_ref.current_rules().unwrap_or_default(); + let proxies_uid = profiles_ref.current_proxies().unwrap_or_default(); + let groups_uid = profiles_ref.current_groups().unwrap_or_default(); + let current_profile_uid = profiles_ref.get_current().unwrap_or_default(); - let global_script = profiles - .get_item(&"Script".to_string()) - .ok() - .and_then(>::from) - .unwrap_or_else(|| ChainItem { - uid: "Script".into(), - data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), - }); + let name = profiles_ref + .get_item(¤t_profile_uid) + .ok() + .and_then(|item| item.name.clone()) + .unwrap_or_default(); - let name = profiles - .get_item(&profiles.get_current().unwrap_or_default()) - .ok() - .and_then(|item| item.name.clone()) - .unwrap_or_default(); + ( + current, + merge_uid, + script_uid, + rules_uid, + proxies_uid, + groups_uid, + current_profile_uid, + name, + ) + }; + + // 现在获取具体的items,此时profiles锁已经释放 + let merge = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(&merge_uid).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Merge(Mapping::new()), + }); + + let script = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(&script_uid).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), + }); + + let rules = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(&rules_uid).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Rules(SeqMap::default()), + }); + + let proxies = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(&proxies_uid).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Proxies(SeqMap::default()), + }); + + let groups = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(&groups_uid).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Groups(SeqMap::default()), + }); + + let global_merge = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(&"Merge".to_string()).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "Merge".into(), + data: ChainType::Merge(Mapping::new()), + }); + + let global_script = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(&"Script".to_string()).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "Script".into(), + data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), + }); ( current, @@ -237,6 +335,7 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { // 处理 external-controller 键的开关逻辑 if key.as_str() == Some("external-controller") { let enable_external_controller = Config::verge() + .await .latest_ref() .enable_external_controller .unwrap_or(false); @@ -274,7 +373,7 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { }); } - config = use_tun(config, enable_tun).await; + config = use_tun(config, enable_tun); config = use_sort(config); // 应用独立的DNS配置(如果启用) @@ -285,27 +384,26 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { if let Ok(app_dir) = dirs::app_home_dir() { let dns_path = app_dir.join("dns_config.yaml"); - if dns_path.exists() { - if let Ok(dns_yaml) = fs::read_to_string(&dns_path) { - if let Ok(dns_config) = serde_yaml::from_str::(&dns_yaml) { - // 处理hosts配置 - if let Some(hosts_value) = dns_config.get("hosts") { - if hosts_value.is_mapping() { - config.insert("hosts".into(), hosts_value.clone()); - log::info!(target: "app", "apply hosts configuration"); - } - } + if dns_path.exists() + && let Ok(dns_yaml) = fs::read_to_string(&dns_path) + && let Ok(dns_config) = serde_yaml_ng::from_str::(&dns_yaml) + { + // 处理hosts配置 + if let Some(hosts_value) = dns_config.get("hosts") + && hosts_value.is_mapping() + { + config.insert("hosts".into(), hosts_value.clone()); + log::info!(target: "app", "apply hosts configuration"); + } - if let Some(dns_value) = dns_config.get("dns") { - if let Some(dns_mapping) = dns_value.as_mapping() { - config.insert("dns".into(), dns_mapping.clone().into()); - log::info!(target: "app", "apply dns_config.yaml (dns section)"); - } - } else { - config.insert("dns".into(), dns_config.into()); - log::info!(target: "app", "apply dns_config.yaml"); - } + if let Some(dns_value) = dns_config.get("dns") { + if let Some(dns_mapping) = dns_value.as_mapping() { + config.insert("dns".into(), dns_mapping.clone().into()); + log::info!(target: "app", "apply dns_config.yaml (dns section)"); } + } else { + config.insert("dns".into(), dns_config.into()); + log::info!(target: "app", "apply dns_config.yaml"); } } } diff --git a/clash-verge-rev/src-tauri/src/enhance/script.rs b/clash-verge-rev/src-tauri/src/enhance/script.rs index b36a9214a9..42a968aafe 100644 --- a/clash-verge-rev/src-tauri/src/enhance/script.rs +++ b/clash-verge-rev/src-tauri/src/enhance/script.rs @@ -1,31 +1,49 @@ use super::use_lowercase; use anyhow::{Error, Result}; -use serde_yaml::Mapping; +use serde_yaml_ng::Mapping; pub fn use_script( script: String, config: Mapping, name: String, ) -> Result<(Mapping, Vec<(String, String)>)> { - use boa_engine::{native_function::NativeFunction, Context, JsValue, Source}; - use parking_lot::Mutex; - use std::sync::Arc; + use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction}; + use std::{cell::RefCell, rc::Rc}; let mut context = Context::default(); - let outputs = Arc::new(Mutex::new(vec![])); + let outputs = Rc::new(RefCell::new(vec![])); - let copy_outputs = outputs.clone(); + let copy_outputs = Rc::clone(&outputs); unsafe { let _ = context.register_global_builtin_callable( "__verge_log__".into(), 2, NativeFunction::from_closure( move |_: &JsValue, args: &[JsValue], context: &mut Context| { - let level = args.first().unwrap().to_string(context)?; - let level = level.to_std_string().unwrap(); - let data = args.get(1).unwrap().to_string(context)?; - let data = data.to_std_string().unwrap(); - let mut out = copy_outputs.lock(); + let level = args.first().ok_or_else(|| { + boa_engine::JsError::from_opaque( + JsString::from("Missing level argument").into(), + ) + })?; + let level = level.to_string(context)?; + let level = level.to_std_string().map_err(|_| { + boa_engine::JsError::from_opaque( + JsString::from("Failed to convert level to string").into(), + ) + })?; + + let data = args.get(1).ok_or_else(|| { + boa_engine::JsError::from_opaque( + JsString::from("Missing data argument").into(), + ) + })?; + let data = data.to_string(context)?; + let data = data.to_std_string().map_err(|_| { + boa_engine::JsError::from_opaque( + JsString::from("Failed to convert data to string").into(), + ) + })?; + let mut out = copy_outputs.borrow_mut(); out.push((level, data)); Ok(JsValue::undefined()) }, @@ -50,25 +68,29 @@ pub fn use_script( let safe_name = escape_js_string_for_single_quote(&name); let code = format!( - r#"try{{ + r"try{{ {script}; JSON.stringify(main({config_str},'{safe_name}')||'') }} catch(err) {{ `__error_flag__ ${{err.toString()}}` - }}"# + }}" ); if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) { if !result.is_string() { anyhow::bail!("main function should return object"); } - let result = result.to_string(&mut context).unwrap(); - let result = result.to_std_string().unwrap(); + let result = result + .to_string(&mut context) + .map_err(|e| anyhow::anyhow!("Failed to convert JS result to string: {}", e))?; + let result = result + .to_std_string() + .map_err(|_| anyhow::anyhow!("Failed to convert JS string to std string"))?; // 直接解析JSON结果,不做其他解析 let res: Result = parse_json_safely(&result); - let mut out = outputs.lock(); + let mut out = outputs.borrow_mut(); match res { Ok(config) => Ok((use_lowercase(config), out.to_vec())), Err(err) => { @@ -103,6 +125,8 @@ fn escape_js_string_for_single_quote(s: &str) -> String { } #[test] +#[allow(unused_variables)] +#[allow(clippy::expect_used)] fn test_script() { let script = r#" function main(config) { @@ -115,7 +139,7 @@ fn test_script() { } "#; - let config = r#" + let config = r" rules: - 111 - 222 @@ -123,22 +147,21 @@ fn test_script() { enable: false dns: enable: false - "#; + "; - let config = serde_yaml::from_str(config).unwrap(); - let (config, results) = use_script(script.into(), config, "".to_string()).unwrap(); + let config = serde_yaml_ng::from_str(config).expect("Failed to parse test config YAML"); + let (config, results) = use_script(script.into(), config, "".to_string()) + .expect("Script execution should succeed in test"); - let _ = serde_yaml::to_string(&config).unwrap(); + let _ = serde_yaml_ng::to_string(&config).expect("Failed to serialize config to YAML"); let yaml_config_size = std::mem::size_of_val(&config); - dbg!(yaml_config_size); let box_yaml_config_size = std::mem::size_of_val(&Box::new(config)); - dbg!(box_yaml_config_size); - dbg!(results); assert!(box_yaml_config_size < yaml_config_size); } // 测试特殊字符转义功能 #[test] +#[allow(clippy::expect_used)] fn test_escape_unescape() { let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#; let escaped = escape_js_string_for_single_quote(test_string); @@ -146,13 +169,14 @@ fn test_escape_unescape() { println!("Escaped: {escaped}"); let json_str = r#"{"key":"value","nested":{"key":"value"}}"#; - let parsed = parse_json_safely(json_str).unwrap(); + let parsed = parse_json_safely(json_str).expect("Failed to parse test JSON safely"); assert!(parsed.contains_key("key")); assert!(parsed.contains_key("nested")); let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#; - let parsed_quoted = parse_json_safely(quoted_json_str).unwrap(); + let parsed_quoted = + parse_json_safely(quoted_json_str).expect("Failed to parse quoted test JSON safely"); assert!(parsed_quoted.contains_key("key")); assert!(parsed_quoted.contains_key("nested")); diff --git a/clash-verge-rev/src-tauri/src/enhance/seq.rs b/clash-verge-rev/src-tauri/src/enhance/seq.rs index 950a30b63c..97d143303e 100644 --- a/clash-verge-rev/src-tauri/src/enhance/seq.rs +++ b/clash-verge-rev/src-tauri/src/enhance/seq.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use serde_yaml::{Mapping, Sequence, Value}; +use serde_yaml_ng::{Mapping, Sequence, Value}; #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct SeqMap { @@ -44,39 +44,39 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping { config.insert(Value::String(field.into()), Value::Sequence(new_seq)); // If this is proxies field, we also need to filter proxy-groups - if field == "proxies" { - if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") { - let mut new_groups = Sequence::new(); - for group in groups { - if let Value::Mapping(group_map) = group { - let mut new_group = group_map.clone(); - if let Some(Value::Sequence(proxies)) = group_map.get("proxies") { - let filtered_proxies: Sequence = proxies - .iter() - .filter(|p| { - if let Value::String(name) = p { - !delete.contains(name) - } else { - true - } - }) - .cloned() - .collect(); - new_group.insert( - Value::String("proxies".into()), - Value::Sequence(filtered_proxies), - ); - } - new_groups.push(Value::Mapping(new_group)); - } else { - new_groups.push(group.clone()); + if field == "proxies" + && let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") + { + let mut new_groups = Sequence::new(); + for group in groups { + if let Value::Mapping(group_map) = group { + let mut new_group = group_map.clone(); + if let Some(Value::Sequence(proxies)) = group_map.get("proxies") { + let filtered_proxies: Sequence = proxies + .iter() + .filter(|p| { + if let Value::String(name) = p { + !delete.contains(name) + } else { + true + } + }) + .cloned() + .collect(); + new_group.insert( + Value::String("proxies".into()), + Value::Sequence(filtered_proxies), + ); } + new_groups.push(Value::Mapping(new_group)); + } else { + new_groups.push(group.clone()); } - config.insert( - Value::String("proxy-groups".into()), - Value::Sequence(new_groups), - ); } + config.insert( + Value::String("proxy-groups".into()), + Value::Sequence(new_groups), + ); } config @@ -86,9 +86,11 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping { mod tests { use super::*; #[allow(unused_imports)] - use serde_yaml::Value; + use serde_yaml_ng::Value; #[test] + #[allow(clippy::unwrap_used)] + #[allow(clippy::expect_used)] fn test_delete_proxy_and_references() { let config_str = r#" proxies: @@ -107,7 +109,8 @@ proxy-groups: proxies: - "proxy1" "#; - let mut config: Mapping = serde_yaml::from_str(config_str).unwrap(); + let mut config: Mapping = + serde_yaml_ng::from_str(config_str).expect("Failed to parse test config YAML"); let seq = SeqMap { prepend: Sequence::new(), @@ -118,38 +121,51 @@ proxy-groups: config = use_seq(seq, config, "proxies"); // Check if proxy1 is removed from proxies - let proxies = config.get("proxies").unwrap().as_sequence().unwrap(); + let proxies = config + .get("proxies") + .expect("proxies field should exist") + .as_sequence() + .expect("proxies should be a sequence"); assert_eq!(proxies.len(), 1); assert_eq!( proxies[0] .as_mapping() - .unwrap() + .expect("proxy should be a mapping") .get("name") - .unwrap() + .expect("proxy should have name") .as_str() - .unwrap(), + .expect("name should be string"), "proxy2" ); // Check if proxy1 is removed from all groups - let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap(); + let groups = config + .get("proxy-groups") + .expect("proxy-groups field should exist") + .as_sequence() + .expect("proxy-groups should be a sequence"); let group1_proxies = groups[0] .as_mapping() - .unwrap() + .expect("group should be a mapping") .get("proxies") - .unwrap() + .expect("group should have proxies") .as_sequence() - .unwrap(); + .expect("group proxies should be a sequence"); let group2_proxies = groups[1] .as_mapping() - .unwrap() + .expect("group should be a mapping") .get("proxies") - .unwrap() + .expect("group should have proxies") .as_sequence() - .unwrap(); + .expect("group proxies should be a sequence"); assert_eq!(group1_proxies.len(), 1); - assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2"); + assert_eq!( + group1_proxies[0] + .as_str() + .expect("proxy name should be string"), + "proxy2" + ); assert_eq!(group2_proxies.len(), 0); } } diff --git a/clash-verge-rev/src-tauri/src/enhance/tun.rs b/clash-verge-rev/src-tauri/src/enhance/tun.rs index 3e06b0c713..5f81a99f1f 100644 --- a/clash-verge-rev/src-tauri/src/enhance/tun.rs +++ b/clash-verge-rev/src-tauri/src/enhance/tun.rs @@ -1,4 +1,7 @@ -use serde_yaml::{Mapping, Value}; +use serde_yaml_ng::{Mapping, Value}; + +#[cfg(target_os = "macos")] +use crate::process::AsyncHandler; macro_rules! revise { ($map: expr, $key: expr, $val: expr) => { @@ -18,7 +21,7 @@ macro_rules! append { }; } -pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping { +pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { let tun_key = Value::from("tun"); let tun_val = config.get(&tun_key); let mut tun_val = tun_val.map_or(Mapping::new(), |val| { @@ -59,8 +62,10 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping { #[cfg(target_os = "macos")] { - crate::utils::resolve::restore_public_dns().await; - crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await; + AsyncHandler::spawn(move || async move { + crate::utils::resolve::dns::restore_public_dns().await; + crate::utils::resolve::dns::set_public_dns("223.6.6.6".to_string()).await; + }); } } @@ -69,7 +74,9 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping { } else { // TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置 #[cfg(target_os = "macos")] - crate::utils::resolve::restore_public_dns().await; + AsyncHandler::spawn(move || async move { + crate::utils::resolve::dns::restore_public_dns().await; + }); } // 更新TUN配置 diff --git a/clash-verge-rev/src-tauri/src/feat/backup.rs b/clash-verge-rev/src-tauri/src/feat/backup.rs index 07adc5fc2c..d92418c808 100644 --- a/clash-verge-rev/src-tauri/src/feat/backup.rs +++ b/clash-verge-rev/src-tauri/src/feat/backup.rs @@ -51,13 +51,15 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> { /// Restore WebDAV backup pub async fn restore_webdav_backup(filename: String) -> Result<()> { - let verge = Config::verge(); + let verge = Config::verge().await; let verge_data = verge.latest_ref().clone(); let webdav_url = verge_data.webdav_url.clone(); let webdav_username = verge_data.webdav_username.clone(); let webdav_password = verge_data.webdav_password.clone(); - let backup_storage_path = app_home_dir().unwrap().join(&filename); + let backup_storage_path = app_home_dir() + .map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))? + .join(&filename); backup::WebDavClient::global() .download(filename, backup_storage_path.clone()) .await diff --git a/clash-verge-rev/src-tauri/src/feat/clash.rs b/clash-verge-rev/src-tauri/src/feat/clash.rs index 33c1fb1525..74e4a961c7 100644 --- a/clash-verge-rev/src-tauri/src/feat/clash.rs +++ b/clash-verge-rev/src-tauri/src/feat/clash.rs @@ -1,39 +1,45 @@ use crate::{ config::Config, - core::{handle, tray, CoreManager}, + core::{CoreManager, handle, tray}, ipc::IpcManager, logging_error, process::AsyncHandler, utils::{logging::Type, resolve}, }; -use serde_yaml::{Mapping, Value}; -use tauri::Manager; +use serde_yaml_ng::{Mapping, Value}; /// Restart the Clash core -pub fn restart_clash_core() { - AsyncHandler::spawn(move || async move { - match CoreManager::global().restart_core().await { - Ok(_) => { - handle::Handle::refresh_clash(); - handle::Handle::notice_message("set_config::ok", "ok"); - } - Err(err) => { - handle::Handle::notice_message("set_config::error", format!("{err}")); - log::error!(target:"app", "{err}"); - } +pub async fn restart_clash_core() { + match CoreManager::global().restart_core().await { + Ok(_) => { + handle::Handle::refresh_clash(); + handle::Handle::notice_message("set_config::ok", "ok"); } - }); + Err(err) => { + handle::Handle::notice_message("set_config::error", format!("{err}")); + log::error!(target:"app", "{err}"); + } + } } /// Restart the application -pub fn restart_app() { - AsyncHandler::spawn(move || async move { - logging_error!(Type::Core, true, CoreManager::global().stop_core().await); - resolve::resolve_reset_async().await; - let app_handle = handle::Handle::global().app_handle().unwrap(); - std::thread::sleep(std::time::Duration::from_secs(1)); - tauri::process::restart(&app_handle.env()); - }); +pub async fn restart_app() { + // logging_error!(Type::Core, true, CoreManager::global().stop_core().await); + resolve::resolve_reset_async().await; + + handle::Handle::global() + .app_handle() + .map(|app_handle| { + app_handle.restart(); + }) + .unwrap_or_else(|| { + logging_error!( + Type::System, + false, + "{}", + "Failed to get app handle for restart" + ); + }); } fn after_change_clash_mode() { @@ -56,37 +62,42 @@ fn after_change_clash_mode() { } /// Change Clash mode (rule/global/direct/script) -pub fn change_clash_mode(mode: String) { +pub async fn change_clash_mode(mode: String) { let mut mapping = Mapping::new(); mapping.insert(Value::from("mode"), mode.clone().into()); // Convert YAML mapping to JSON Value let json_value = serde_json::json!({ "mode": mode }); - AsyncHandler::spawn(move || async move { - log::debug!(target: "app", "change clash mode to {mode}"); - match IpcManager::global().patch_configs(json_value).await { - Ok(_) => { - // 更新订阅 - Config::clash().data_mut().patch_config(mapping); + log::debug!(target: "app", "change clash mode to {mode}"); + match IpcManager::global().patch_configs(json_value).await { + Ok(_) => { + // 更新订阅 + Config::clash().await.data_mut().patch_config(mapping); - if Config::clash().data_mut().save_config().is_ok() { - handle::Handle::refresh_clash(); - logging_error!(Type::Tray, true, tray::Tray::global().update_menu()); - logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None)); - } - - let is_auto_close_connection = Config::verge() - .data_mut() - .auto_close_connection - .unwrap_or(false); - if is_auto_close_connection { - after_change_clash_mode(); - } + // 分离数据获取和异步调用 + let clash_data = Config::clash().await.data_mut().clone(); + if clash_data.save_config().await.is_ok() { + handle::Handle::refresh_clash(); + logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await); + logging_error!( + Type::Tray, + true, + tray::Tray::global().update_icon(None).await + ); + } + + let is_auto_close_connection = Config::verge() + .await + .data_mut() + .auto_close_connection + .unwrap_or(false); + if is_auto_close_connection { + after_change_clash_mode(); } - Err(err) => log::error!(target: "app", "{err}"), } - }); + Err(err) => log::error!(target: "app", "{err}"), + } } /// Test connection delay to a URL @@ -95,6 +106,7 @@ pub async fn test_delay(url: String) -> anyhow::Result { use tokio::time::Instant; let tun_mode = Config::verge() + .await .latest_ref() .enable_tun_mode .unwrap_or(false); @@ -110,7 +122,7 @@ pub async fn test_delay(url: String) -> anyhow::Result { let start = Instant::now(); - let response = NetworkManager::global() + let response = NetworkManager::new() .get_with_interrupt(&url, proxy_type, Some(10), user_agent, false) .await; diff --git a/clash-verge-rev/src-tauri/src/feat/config.rs b/clash-verge-rev/src-tauri/src/feat/config.rs index 7a7b727387..15f0325b93 100644 --- a/clash-verge-rev/src-tauri/src/feat/config.rs +++ b/clash-verge-rev/src-tauri/src/feat/config.rs @@ -1,16 +1,19 @@ use crate::{ config::{Config, IVerge}, - core::{handle, hotkey, sysopt, tray, CoreManager}, + core::{CoreManager, handle, hotkey, sysopt, tray}, logging_error, module::lightweight, utils::logging::Type, }; use anyhow::Result; -use serde_yaml::Mapping; +use serde_yaml_ng::Mapping; /// Patch Clash configuration pub async fn patch_clash(patch: Mapping) -> Result<()> { - Config::clash().draft_mut().patch_config(patch.clone()); + Config::clash() + .await + .draft_mut() + .patch_config(patch.clone()); let res = { // 激活订阅 @@ -19,10 +22,14 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> { CoreManager::global().restart_core().await?; } else { if patch.get("mode").is_some() { - logging_error!(Type::Tray, true, tray::Tray::global().update_menu()); - logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None)); + logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await); + logging_error!( + Type::Tray, + true, + tray::Tray::global().update_icon(None).await + ); } - Config::runtime().draft_mut().patch_config(patch); + Config::runtime().await.draft_mut().patch_config(patch); CoreManager::global().update_config().await?; } handle::Handle::refresh_clash(); @@ -30,12 +37,14 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> { }; match res { Ok(()) => { - Config::clash().apply(); - Config::clash().data_mut().save_config()?; + Config::clash().await.apply(); + // 分离数据获取和异步调用 + let clash_data = Config::clash().await.data_mut().clone(); + clash_data.save_config().await?; Ok(()) } Err(err) => { - Config::clash().discard(); + Config::clash().await.discard(); Err(err) } } @@ -60,7 +69,10 @@ enum UpdateFlags { /// Patch Verge configuration pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> { - Config::verge().draft_mut().patch_config(patch.clone()); + Config::verge() + .await + .draft_mut() + .patch_config(patch.clone()); let tun_mode = patch.enable_tun_mode; let auto_launch = patch.enable_auto_launch; @@ -181,33 +193,35 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> { handle::Handle::refresh_clash(); } if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 { - Config::verge().draft_mut().enable_global_hotkey = enable_global_hotkey; + Config::verge().await.draft_mut().enable_global_hotkey = enable_global_hotkey; handle::Handle::refresh_verge(); } if (update_flags & (UpdateFlags::Launch as i32)) != 0 { - sysopt::Sysopt::global().update_launch()?; + sysopt::Sysopt::global().update_launch().await?; } if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 { sysopt::Sysopt::global().update_sysproxy().await?; } - if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 { - hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?; + if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 + && let Some(hotkeys) = patch.hotkeys + { + hotkey::Hotkey::global().update(hotkeys).await?; } if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 { - tray::Tray::global().update_menu()?; + tray::Tray::global().update_menu().await?; } if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 { - tray::Tray::global().update_icon(None)?; + tray::Tray::global().update_icon(None).await?; } if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 { - tray::Tray::global().update_tooltip()?; + tray::Tray::global().update_tooltip().await?; } if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 { - tray::Tray::global().update_click_behavior()?; + tray::Tray::global().update_click_behavior().await?; } if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 { - if enable_auto_light_weight.unwrap() { - lightweight::enable_auto_light_weight_mode(); + if enable_auto_light_weight.unwrap_or(false) { + lightweight::enable_auto_light_weight_mode().await; } else { lightweight::disable_auto_light_weight_mode(); } @@ -217,15 +231,17 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> { }; match res { Ok(()) => { - Config::verge().apply(); + Config::verge().await.apply(); if !not_save_file { - Config::verge().data_mut().save_file()?; + // 分离数据获取和异步调用 + let verge_data = Config::verge().await.data_mut().clone(); + verge_data.save_file().await?; } Ok(()) } Err(err) => { - Config::verge().discard(); + Config::verge().await.discard(); Err(err) } } diff --git a/clash-verge-rev/src-tauri/src/feat/profile.rs b/clash-verge-rev/src-tauri/src/feat/profile.rs index 8b2c088f20..2bf2249cb7 100644 --- a/clash-verge-rev/src-tauri/src/feat/profile.rs +++ b/clash-verge-rev/src-tauri/src/feat/profile.rs @@ -1,26 +1,25 @@ use crate::{ cmd, - config::{Config, PrfItem, PrfOption}, - core::{handle, CoreManager, *}, + config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe}, + core::{CoreManager, handle, tray}, logging, - process::AsyncHandler, utils::logging::Type, }; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; /// Toggle proxy profile -pub fn toggle_proxy_profile(profile_index: String) { - AsyncHandler::spawn(|| async move { - let app_handle = handle::Handle::global().app_handle().unwrap(); - match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await { - Ok(_) => { - let _ = tray::Tray::global().update_menu(); - } - Err(err) => { - log::error!(target: "app", "{err}"); +pub async fn toggle_proxy_profile(profile_index: String) { + match cmd::patch_profiles_config_by_profile_index(profile_index).await { + Ok(_) => { + let result = tray::Tray::global().update_menu().await; + if let Err(err) = result { + logging!(error, Type::Tray, true, "更新菜单失败: {}", err); } } - }); + Err(err) => { + log::error!(target: "app", "{err}"); + } + } } /// Update a profile @@ -35,7 +34,7 @@ pub async fn update_profile( let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性 let url_opt = { - let profiles = Config::profiles(); + let profiles = Config::profiles().await; let profiles = profiles.latest_ref(); let item = profiles.get_item(&uid)?; let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote"); @@ -50,9 +49,14 @@ pub async fn update_profile( log::info!(target: "app", "[订阅更新] {} 是远程订阅,URL: {}", uid, - item.url.clone().unwrap() + item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))? ); - Some((item.url.clone().unwrap(), item.option.clone())) + Some(( + item.url + .clone() + .ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?, + item.option.clone(), + )) } }; @@ -65,11 +69,13 @@ pub async fn update_profile( match PrfItem::from_url(&url, None, None, merged_opt.clone()).await { Ok(item) => { log::info!(target: "app", "[订阅更新] 更新订阅配置成功"); - let profiles = Config::profiles(); - let mut profiles = profiles.draft_mut(); - profiles.update_item(uid.clone(), item)?; + let profiles = Config::profiles().await; - let is_current = Some(uid.clone()) == profiles.get_current(); + // 使用Send-safe helper函数 + let result = profiles_draft_update_item_safe(uid.clone(), item).await; + result?; + + let is_current = Some(uid.clone()) == profiles.latest_ref().get_current(); log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}"); is_current && auto_refresh } @@ -101,9 +107,10 @@ pub async fn update_profile( } // 更新到配置 - let profiles = Config::profiles(); - let mut profiles = profiles.draft_mut(); - profiles.update_item(uid.clone(), item.clone())?; + let profiles = Config::profiles().await; + + // 使用 Send-safe 方法进行数据操作 + profiles_draft_update_item_safe(uid.clone(), item.clone()).await?; // 获取配置名称用于通知 let profile_name = item.name.clone().unwrap_or_else(|| uid.clone()); @@ -111,7 +118,7 @@ pub async fn update_profile( // 发送通知告知用户自动更新使用了回退机制 handle::Handle::notice_message("update_with_clash_proxy", profile_name); - let is_current = Some(uid.clone()) == profiles.get_current(); + let is_current = Some(uid.clone()) == profiles.data_ref().get_current(); log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}"); is_current && auto_refresh } @@ -136,6 +143,15 @@ pub async fn update_profile( Ok(_) => { logging!(info, Type::Config, true, "[订阅更新] 更新成功"); handle::Handle::refresh_clash(); + if let Err(err) = cmd::proxy::force_refresh_proxies().await { + logging!( + error, + Type::Config, + true, + "[订阅更新] 代理组刷新失败: {}", + err + ); + } } Err(err) => { logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err); diff --git a/clash-verge-rev/src-tauri/src/feat/proxy.rs b/clash-verge-rev/src-tauri/src/feat/proxy.rs index c356423826..5934193b31 100644 --- a/clash-verge-rev/src-tauri/src/feat/proxy.rs +++ b/clash-verge-rev/src-tauri/src/feat/proxy.rs @@ -2,78 +2,93 @@ use crate::{ config::{Config, IVerge}, core::handle, ipc::IpcManager, - process::AsyncHandler, + logging, + utils::logging::Type, }; use std::env; use tauri_plugin_clipboard_manager::ClipboardExt; /// Toggle system proxy on/off -pub fn toggle_system_proxy() { - let enable = Config::verge().draft_mut().enable_system_proxy; - let enable = enable.unwrap_or(false); - let auto_close_connection = Config::verge() - .data_mut() - .auto_close_connection - .unwrap_or(false); +pub async fn toggle_system_proxy() { + // 获取当前系统代理状态 + let enable = { + let verge = Config::verge().await; - AsyncHandler::spawn(move || async move { - // 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接 - if enable && auto_close_connection { - if let Err(err) = IpcManager::global().close_all_connections().await { - log::error!(target: "app", "Failed to close all connections: {err}"); - } - } + verge.latest_ref().enable_system_proxy.unwrap_or(false) + }; + // 获取自动关闭连接设置 + let auto_close_connection = { + let verge = Config::verge().await; - match super::patch_verge( - IVerge { - enable_system_proxy: Some(!enable), - ..IVerge::default() - }, - false, - ) - .await - { - Ok(_) => handle::Handle::refresh_verge(), - Err(err) => log::error!(target: "app", "{err}"), - } - }); + verge.latest_ref().auto_close_connection.unwrap_or(false) + }; + + // 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接 + if enable + && auto_close_connection + && let Err(err) = IpcManager::global().close_all_connections().await + { + log::error!(target: "app", "Failed to close all connections: {err}"); + } + + let patch_result = super::patch_verge( + IVerge { + enable_system_proxy: Some(!enable), + ..IVerge::default() + }, + false, + ) + .await; + + match patch_result { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } } /// Toggle TUN mode on/off -pub fn toggle_tun_mode(not_save_file: Option) { - let enable = Config::verge().data_mut().enable_tun_mode; +pub async fn toggle_tun_mode(not_save_file: Option) { + let enable = Config::verge().await.data_mut().enable_tun_mode; let enable = enable.unwrap_or(false); - AsyncHandler::spawn(async move || { - match super::patch_verge( - IVerge { - enable_tun_mode: Some(!enable), - ..IVerge::default() - }, - not_save_file.unwrap_or(false), - ) - .await - { - Ok(_) => handle::Handle::refresh_verge(), - Err(err) => log::error!(target: "app", "{err}"), - } - }); + match super::patch_verge( + IVerge { + enable_tun_mode: Some(!enable), + ..IVerge::default() + }, + not_save_file.unwrap_or(false), + ) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } } /// Copy proxy environment variables to clipboard -pub fn copy_clash_env() { +pub async fn copy_clash_env() { // 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1 - let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| { - Config::verge() + let clash_verge_rev_ip = match env::var("CLASH_VERGE_REV_IP") { + Ok(ip) => ip, + Err(_) => Config::verge() + .await .latest_ref() .proxy_host .clone() - .unwrap_or_else(|| "127.0.0.1".to_string()) - }); + .unwrap_or_else(|| "127.0.0.1".to_string()), + }; - let app_handle = handle::Handle::global().app_handle().unwrap(); + let Some(app_handle) = handle::Handle::global().app_handle() else { + logging!( + error, + Type::System, + "Failed to get app handle for proxy operation" + ); + return; + }; let port = { Config::verge() + .await .latest_ref() .verge_mixed_port .unwrap_or(7897) @@ -82,7 +97,7 @@ pub fn copy_clash_env() { let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}"); let cliboard = app_handle.clipboard(); - let env_type = { Config::verge().latest_ref().env_type.clone() }; + let env_type = { Config::verge().await.latest_ref().env_type.clone() }; let env_type = match env_type { Some(env_type) => env_type, None => { diff --git a/clash-verge-rev/src-tauri/src/feat/window.rs b/clash-verge-rev/src-tauri/src/feat/window.rs index d301eb7cc3..0188b27a9a 100644 --- a/clash-verge-rev/src-tauri/src/feat/window.rs +++ b/clash-verge-rev/src-tauri/src/feat/window.rs @@ -1,73 +1,38 @@ -#[cfg(target_os = "macos")] -use crate::AppHandleManager; +use crate::utils::window_manager::WindowManager; use crate::{ config::Config, - core::{handle, sysopt, CoreManager}, + core::{CoreManager, handle, sysopt}, ipc::IpcManager, logging, + module::lightweight, utils::logging::Type, }; /// Open or close the dashboard window -#[allow(dead_code)] -pub fn open_or_close_dashboard() { - open_or_close_dashboard_internal(false) -} - -/// Open or close the dashboard window (hotkey call, dispatched to main thread) -#[allow(dead_code)] -pub fn open_or_close_dashboard_hotkey() { - open_or_close_dashboard_internal(true) +pub async fn open_or_close_dashboard() { + open_or_close_dashboard_internal().await } /// Internal implementation for opening/closing dashboard -fn open_or_close_dashboard_internal(bypass_debounce: bool) { - use crate::process::AsyncHandler; - use crate::utils::window_manager::WindowManager; - - log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})"); - - // 热键调用调度到主线程执行,避免 WebView 创建死锁 - if bypass_debounce { - log::info!(target: "app", "热键调用,调度到主线程执行窗口操作"); - - AsyncHandler::spawn(move || async move { - log::info!(target: "app", "主线程中执行热键窗口操作"); - - if crate::module::lightweight::is_in_lightweight_mode() { - log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode"); - crate::module::lightweight::exit_lightweight_mode(); - log::info!(target: "app", "Creating new window after exiting lightweight mode"); - let result = WindowManager::show_main_window(); - log::info!(target: "app", "Window operation result: {result:?}"); - return; - } - - let result = WindowManager::toggle_main_window(); - log::info!(target: "app", "Window toggle result: {result:?}"); - }); - return; - } - if crate::module::lightweight::is_in_lightweight_mode() { - log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode"); - crate::module::lightweight::exit_lightweight_mode(); - log::info!(target: "app", "Creating new window after exiting lightweight mode"); - let result = WindowManager::show_main_window(); - log::info!(target: "app", "Window operation result: {result:?}"); - return; - } - - let result = WindowManager::toggle_main_window(); +async fn open_or_close_dashboard_internal() { + let _ = lightweight::exit_lightweight_mode().await; + let result = WindowManager::toggle_main_window().await; log::info!(target: "app", "Window toggle result: {result:?}"); } /// 异步优化的应用退出函数 -pub fn quit() { - use crate::process::AsyncHandler; +pub async fn quit() { logging!(debug, Type::System, true, "启动退出流程"); // 获取应用句柄并设置退出标志 - let app_handle = handle::Handle::global().app_handle().unwrap(); + let Some(app_handle) = handle::Handle::global().app_handle() else { + logging!( + error, + Type::System, + "Failed to get app handle for quit operation" + ); + return; + }; handle::Handle::global().set_is_exiting(); // 优先关闭窗口,提供立即反馈 @@ -77,52 +42,54 @@ pub fn quit() { } // 使用异步任务处理资源清理,避免阻塞 - AsyncHandler::spawn(move || async move { - logging!(info, Type::System, true, "开始异步清理资源"); - let cleanup_result = clean_async().await; + logging!(info, Type::System, true, "开始异步清理资源"); + let cleanup_result = clean_async().await; - logging!( - info, - Type::System, - true, - "资源清理完成,退出代码: {}", - if cleanup_result { 0 } else { 1 } - ); - app_handle.exit(if cleanup_result { 0 } else { 1 }); - }); + logging!( + info, + Type::System, + true, + "资源清理完成,退出代码: {}", + if cleanup_result { 0 } else { 1 } + ); + app_handle.exit(if cleanup_result { 0 } else { 1 }); } async fn clean_async() -> bool { - use tokio::time::{timeout, Duration}; + use tokio::time::{Duration, timeout}; logging!(info, Type::System, true, "开始执行异步清理操作..."); // 1. 处理TUN模式 - let tun_task = async { - if Config::verge().data_mut().enable_tun_mode.unwrap_or(false) { - let disable_tun = serde_json::json!({ - "tun": { - "enable": false - } - }); - match timeout( - Duration::from_secs(2), - IpcManager::global().patch_configs(disable_tun), - ) - .await - { - Ok(_) => { - log::info!(target: "app", "TUN模式已禁用"); - true - } - Err(_) => { - log::warn!(target: "app", "禁用TUN模式超时"); - false - } + let tun_success = if Config::verge() + .await + .data_mut() + .enable_tun_mode + .unwrap_or(false) + { + let disable_tun = serde_json::json!({"tun": {"enable": false}}); + match timeout( + Duration::from_secs(3), + IpcManager::global().patch_configs(disable_tun), + ) + .await + { + Ok(Ok(_)) => { + log::info!(target: "app", "TUN模式已禁用"); + tokio::time::sleep(Duration::from_millis(300)).await; + true + } + Ok(Err(e)) => { + log::warn!(target: "app", "禁用TUN模式失败: {e}"); + false + } + Err(_) => { + log::warn!(target: "app", "禁用TUN模式超时"); + false } - } else { - true } + } else { + true }; // 2. 系统代理重置 @@ -163,7 +130,7 @@ async fn clean_async() -> bool { let dns_task = async { match timeout( Duration::from_millis(1000), - crate::utils::resolve::restore_public_dns(), + crate::utils::resolve::dns::restore_public_dns(), ) .await { @@ -178,8 +145,8 @@ async fn clean_async() -> bool { } }; - // 并行执行所有清理任务 - let (tun_success, proxy_success, core_success) = tokio::join!(tun_task, proxy_task, core_task); + // 并行执行剩余清理任务 + let (proxy_success, core_success) = tokio::join!(proxy_task, core_task); #[cfg(target_os = "macos")] let dns_success = dns_task.await; @@ -192,7 +159,7 @@ async fn clean_async() -> bool { info, Type::System, true, - "异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}", + "异步关闭操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}", tun_success, proxy_success, core_success, @@ -209,7 +176,7 @@ pub fn clean() -> bool { let (tx, rx) = std::sync::mpsc::channel(); AsyncHandler::spawn(move || async move { - logging!(info, Type::System, true, "开始执行清理操作..."); + logging!(info, Type::System, true, "开始执行关闭操作..."); // 使用已有的异步清理函数 let cleanup_result = clean_async().await; @@ -220,7 +187,7 @@ pub fn clean() -> bool { match rx.recv_timeout(std::time::Duration::from_secs(8)) { Ok(result) => { - logging!(info, Type::System, true, "清理操作完成,结果: {}", result); + logging!(info, Type::System, true, "关闭操作完成,结果: {}", result); result } Err(_) => { @@ -236,22 +203,23 @@ pub fn clean() -> bool { } #[cfg(target_os = "macos")] -pub fn hide() { +pub async fn hide() { use crate::module::lightweight::add_light_weight_timer; let enable_auto_light_weight_mode = Config::verge() + .await .data_mut() .enable_auto_light_weight_mode .unwrap_or(false); if enable_auto_light_weight_mode { - add_light_weight_timer(); + add_light_weight_timer().await; } - if let Some(window) = handle::Handle::global().get_window() { - if window.is_visible().unwrap_or(false) { - let _ = window.hide(); - } + if let Some(window) = handle::Handle::global().get_window() + && window.is_visible().unwrap_or(false) + { + let _ = window.hide(); } - AppHandleManager::global().set_activation_policy_accessory(); + handle::Handle::global().set_activation_policy_accessory(); } diff --git a/clash-verge-rev/src-tauri/src/ipc/general.rs b/clash-verge-rev/src-tauri/src/ipc/general.rs index 647edca1b4..9e90ae9052 100644 --- a/clash-verge-rev/src-tauri/src/ipc/general.rs +++ b/clash-verge-rev/src-tauri/src/ipc/general.rs @@ -1,9 +1,15 @@ +use std::time::Duration; + use kode_bridge::{ + ClientConfig, IpcHttpClient, LegacyResponse, errors::{AnyError, AnyResult}, - IpcHttpClient, LegacyResponse, }; -use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; -use std::sync::OnceLock; +use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; + +use crate::{ + logging, singleton_with_logging, + utils::{dirs::ipc_path, logging::Type}, +}; // 定义用于URL路径的编码集合,只编码真正必要的字符 const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS @@ -14,39 +20,35 @@ const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS .add(b'&') // 和号 .add(b'%'); // 百分号 -use crate::{ - logging, - utils::{dirs::ipc_path, logging::Type}, -}; - // Helper function to create AnyError from string fn create_error(msg: impl Into) -> AnyError { Box::new(std::io::Error::other(msg.into())) } pub struct IpcManager { - ipc_path: String, + client: IpcHttpClient, } -static INSTANCE: OnceLock = OnceLock::new(); - impl IpcManager { - pub fn global() -> &'static IpcManager { - INSTANCE.get_or_init(|| { - let ipc_path_buf = ipc_path().unwrap(); - let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); - let instance = IpcManager { - ipc_path: ipc_path.to_string(), - }; - logging!( - info, - Type::Ipc, - true, - "IpcManager initialized with IPC path: {}", - instance.ipc_path - ); - instance - }) + pub fn new() -> Self { + logging!(info, Type::Ipc, true, "Creating new IpcManager instance"); + let ipc_path_buf = ipc_path().unwrap_or_else(|e| { + logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e); + std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path + }); + let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); + let config = ClientConfig { + default_timeout: Duration::from_secs(5), + enable_pooling: false, + max_retries: 4, + retry_delay: Duration::from_millis(125), + max_concurrent_requests: 16, + max_requests_per_second: Some(64.0), + ..Default::default() + }; + #[allow(clippy::unwrap_used)] + let client = IpcHttpClient::with_config(ipc_path, config).unwrap(); + Self { client } } } @@ -57,8 +59,7 @@ impl IpcManager { path: &str, body: Option<&serde_json::Value>, ) -> AnyResult { - let client = IpcHttpClient::new(&self.ipc_path)?; - client.request(method, path, body).await + self.client.request(method, path, body).await } } @@ -79,11 +80,10 @@ impl IpcManager { Ok(response.json()?) } } - "PUT" => { + "PUT" | "DELETE" => { if response.status == 204 { Ok(serde_json::json!({"code": 204})) } else { - // 尝试解析JSON,如果失败则返回错误信息 match response.json() { Ok(json) => Ok(json), Err(_) => Ok(serde_json::json!({ @@ -94,7 +94,14 @@ impl IpcManager { } } } - _ => Ok(response.json()?), + _ => match response.json() { + Ok(json) => Ok(json), + Err(_) => Ok(serde_json::json!({ + "code": response.status, + "message": response.body, + "error": "failed to parse response as JSON" + })), + }, } } @@ -190,8 +197,7 @@ impl IpcManager { // 测速URL不再编码,直接传递 let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}"); - let response = self.send_request("GET", &url, None).await; - response + self.send_request("GET", &url, None).await } // 版本和配置相关 @@ -271,9 +277,14 @@ impl IpcManager { "name": proxy }); - let response = match self.send_request("PUT", &url, Some(&payload)).await { - Ok(resp) => resp, + // println!("group: {}, proxy: {}", group, proxy); + match self.send_request("PUT", &url, Some(&payload)).await { + Ok(_) => { + // println!("updateProxy response: {:?}", response); + Ok(()) + } Err(e) => { + // println!("updateProxy encountered error: {}", e); logging!( error, crate::utils::logging::Type::Ipc, @@ -281,31 +292,8 @@ impl IpcManager { "IPC: updateProxy encountered error: {} (ignored, always returning true)", e ); - // Always return a successful response as serde_json::Value - serde_json::json!({"code": 204}) + Ok(()) } - }; - - if response["code"] == 204 { - Ok(()) - } else { - let error_msg = response["message"].as_str().unwrap_or_else(|| { - if let Some(error) = response.get("error") { - error.as_str().unwrap_or("unknown error") - } else { - "failed to update proxy" - } - }); - - logging!( - error, - crate::utils::logging::Type::Ipc, - true, - "IPC: updateProxy failed: {}", - error_msg - ); - - Err(create_error(error_msg.to_string())) } } @@ -354,8 +342,7 @@ impl IpcManager { // 测速URL不再编码,直接传递 let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}"); - let response = self.send_request("GET", &url, None).await; - response + self.send_request("GET", &url, None).await } // 调试相关 @@ -382,29 +369,8 @@ impl IpcManager { } } - // 流量数据相关 - #[allow(dead_code)] - pub async fn get_traffic(&self) -> AnyResult { - let url = "/traffic"; - logging!(info, Type::Ipc, true, "IPC: 发送 GET 请求到 {}", url); - let result = self.send_request("GET", url, None).await; - logging!( - info, - Type::Ipc, - true, - "IPC: /traffic 请求结果: {:?}", - result - ); - result - } - - // 内存相关 - #[allow(dead_code)] - pub async fn get_memory(&self) -> AnyResult { - let url = "/memory"; - logging!(info, Type::Ipc, true, "IPC: 发送 GET 请求到 {}", url); - let result = self.send_request("GET", url, None).await; - logging!(info, Type::Ipc, true, "IPC: /memory 请求结果: {:?}", result); - result - } + // 日志相关功能已迁移到 logs.rs 模块,使用流式处理 } + +// Use singleton macro with logging +singleton_with_logging!(IpcManager, INSTANCE, "IpcManager"); diff --git a/clash-verge-rev/src-tauri/src/ipc/logs.rs b/clash-verge-rev/src-tauri/src/ipc/logs.rs new file mode 100644 index 0000000000..f0fd0ae5ff --- /dev/null +++ b/clash-verge-rev/src-tauri/src/ipc/logs.rs @@ -0,0 +1,330 @@ +use serde::{Deserialize, Serialize}; +use std::{collections::VecDeque, sync::Arc, time::Instant}; +use tauri::async_runtime::JoinHandle; +use tokio::{sync::RwLock, time::Duration}; + +use crate::{ + ipc::monitor::MonitorData, + logging, + process::AsyncHandler, + singleton_with_logging, + utils::{dirs::ipc_path, logging::Type}, +}; + +const MAX_LOGS: usize = 1000; // Maximum number of logs to keep in memory + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct LogData { + #[serde(rename = "type")] + pub log_type: String, + pub payload: String, +} + +#[derive(Debug, Clone)] +pub struct LogItem { + pub log_type: String, + pub payload: String, + pub time: String, +} + +impl LogItem { + fn new(log_type: String, payload: String) -> Self { + use std::time::{SystemTime, UNIX_EPOCH}; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| std::time::Duration::from_secs(0)) + .as_secs(); + + // Simple time formatting (HH:MM:SS) + let hours = (now / 3600) % 24; + let minutes = (now / 60) % 60; + let seconds = now % 60; + let time_str = format!("{hours:02}:{minutes:02}:{seconds:02}"); + + Self { + log_type, + payload, + time: time_str, + } + } +} + +#[derive(Debug, Clone)] +pub struct CurrentLogs { + pub logs: VecDeque, + // pub level: String, + pub last_updated: Instant, +} + +impl Default for CurrentLogs { + fn default() -> Self { + Self { + logs: VecDeque::with_capacity(MAX_LOGS), + // level: "info".to_string(), + last_updated: Instant::now(), + } + } +} + +impl MonitorData for CurrentLogs { + fn mark_fresh(&mut self) { + self.last_updated = Instant::now(); + } + + fn is_fresh_within(&self, duration: Duration) -> bool { + self.last_updated.elapsed() < duration + } +} + +// Logs monitor with streaming support +pub struct LogsMonitor { + current: Arc>, + task_handle: Arc>>>, + current_monitoring_level: Arc>>, +} + +// Use singleton_with_logging macro +singleton_with_logging!(LogsMonitor, INSTANCE, "LogsMonitor"); + +impl LogsMonitor { + fn new() -> Self { + let current = Arc::new(RwLock::new(CurrentLogs::default())); + + Self { + current, + task_handle: Arc::new(RwLock::new(None)), + current_monitoring_level: Arc::new(RwLock::new(None)), + } + } + + pub async fn start_monitoring(&self, level: Option) { + let filter_level = level.clone().unwrap_or_else(|| "info".to_string()); + + // Check if we're already monitoring the same level + // let level_changed = { + // let current_level = self.current_monitoring_level.read().await; + // if let Some(existing_level) = current_level.as_ref() { + // if existing_level == &filter_level { + // logging!( + // info, + // Type::Ipc, + // true, + // "LogsMonitor: Already monitoring level '{}', skipping duplicate request", + // filter_level + // ); + // return; + // } + // true // Level changed + // } else { + // true // First time or was stopped + // } + // }; + + // Stop existing monitoring task if level changed or first time + { + let mut handle = self.task_handle.write().await; + if let Some(task) = handle.take() { + task.abort(); + logging!( + info, + Type::Ipc, + true, + "LogsMonitor: Stopped previous monitoring task (level changed)" + ); + } + } + + // We want to keep the logs cache even if the level changes, + // so we don't clear it here. The cache will be cleared only when the level changes + // and a new task is started. This allows us to keep logs from previous levels + // even if the level changes during monitoring. + // Clear logs cache when level changes to ensure fresh data + // if level_changed { + // let mut current = self.current.write().await; + // current.logs.clear(); + // current.level = filter_level.clone(); + // current.mark_fresh(); + // logging!( + // info, + // Type::Ipc, + // true, + // "LogsMonitor: Cleared logs cache due to level change to '{}'", + // filter_level + // ); + // } + + // Update current monitoring level + { + let mut current_level = self.current_monitoring_level.write().await; + *current_level = Some(filter_level.clone()); + } + + let monitor_current = Arc::clone(&self.current); + + let task = AsyncHandler::spawn(move || async move { + loop { + // Get fresh IPC path and client for each connection attempt + let (_ipc_path_buf, client) = match Self::create_ipc_client() { + Ok((path, client)) => (path, client), + Err(e) => { + logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e); + tokio::time::sleep(Duration::from_secs(2)).await; + continue; + } + }; + + let url = if filter_level == "all" { + "/logs".to_string() + } else { + format!("/logs?level={filter_level}") + }; + + logging!( + info, + Type::Ipc, + true, + "LogsMonitor: Starting stream for {}", + url + ); + + let _ = client + .get(&url) + .timeout(Duration::from_secs(30)) + .process_lines(|line| { + Self::process_log_line(line, Arc::clone(&monitor_current)) + }) + .await; + + // Wait before retrying + tokio::time::sleep(Duration::from_secs(2)).await; + } + }); + + // Store the task handle + { + let mut handle = self.task_handle.write().await; + *handle = Some(task); + } + + logging!( + info, + Type::Ipc, + true, + "LogsMonitor: Started new monitoring task for level: {:?}", + level + ); + } + + pub async fn stop_monitoring(&self) { + // Stop monitoring task but keep logs + { + let mut handle = self.task_handle.write().await; + if let Some(task) = handle.take() { + task.abort(); + logging!( + info, + Type::Ipc, + true, + "LogsMonitor: Stopped monitoring task" + ); + } + } + + // Reset monitoring level + { + let mut monitoring_level = self.current_monitoring_level.write().await; + *monitoring_level = None; + } + } + + fn create_ipc_client() -> Result< + (std::path::PathBuf, kode_bridge::IpcStreamClient), + Box, + > { + use kode_bridge::IpcStreamClient; + + let ipc_path_buf = ipc_path()?; + let ipc_path = ipc_path_buf.to_str().ok_or("Invalid IPC path")?; + let client = IpcStreamClient::new(ipc_path)?; + Ok((ipc_path_buf, client)) + } + + fn process_log_line( + line: &str, + current: Arc>, + ) -> Result<(), Box> { + if let Ok(log_data) = serde_json::from_str::(line.trim()) { + // Server-side filtering via query parameters handles the level filtering + // We only need to accept all logs since filtering is done at the endpoint level + let log_item = LogItem::new(log_data.log_type, log_data.payload); + + AsyncHandler::spawn(move || async move { + let mut logs = current.write().await; + + // Add new log + logs.logs.push_back(log_item); + + // Keep only the last 1000 logs + if logs.logs.len() > 1000 { + logs.logs.pop_front(); + } + + logs.mark_fresh(); + }); + } + Ok(()) + } + + pub async fn current(&self) -> CurrentLogs { + self.current.read().await.clone() + } + + pub async fn clear_logs(&self) { + let mut current = self.current.write().await; + current.logs.clear(); + current.mark_fresh(); + logging!( + info, + Type::Ipc, + true, + "LogsMonitor: Cleared frontend logs (monitoring continues)" + ); + } + + pub async fn get_logs_as_json(&self) -> serde_json::Value { + let current = self.current().await; + + // Simply return all cached logs since filtering is handled by start_monitoring + // and the cache is cleared when level changes + let logs: Vec = current + .logs + .iter() + .map(|log| { + serde_json::json!({ + "type": log.log_type, + "payload": log.payload, + "time": log.time + }) + }) + .collect(); + + serde_json::Value::Array(logs) + } +} + +pub async fn start_logs_monitoring(level: Option) { + LogsMonitor::global().start_monitoring(level).await; +} + +pub async fn stop_logs_monitoring() { + LogsMonitor::global().stop_monitoring().await; +} + +pub async fn clear_logs() { + LogsMonitor::global().clear_logs().await; +} + +pub async fn get_logs_json() -> serde_json::Value { + LogsMonitor::global().get_logs_as_json().await +} diff --git a/clash-verge-rev/src-tauri/src/ipc/memory.rs b/clash-verge-rev/src-tauri/src/ipc/memory.rs index 778f6e2d93..ed05168935 100644 --- a/clash-verge-rev/src-tauri/src/ipc/memory.rs +++ b/clash-verge-rev/src-tauri/src/ipc/memory.rs @@ -1,14 +1,12 @@ -use kode_bridge::IpcStreamClient; use serde::{Deserialize, Serialize}; -use std::{ - sync::{Arc, OnceLock}, - time::Instant, -}; +use std::{sync::Arc, time::Instant}; use tokio::{sync::RwLock, time::Duration}; use crate::{ - logging, - utils::{dirs::ipc_path, logging::Type}, + ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser}, + process::AsyncHandler, + singleton_lazy_with_logging, + utils::format::fmt_bytes, }; #[derive(Debug, Clone, Deserialize, Serialize)] @@ -34,83 +32,69 @@ impl Default for CurrentMemory { } } -// Minimal memory monitor -pub struct MemoryMonitor { - current: Arc>, +impl MonitorData for CurrentMemory { + fn mark_fresh(&mut self) { + self.last_updated = Instant::now(); + } + + fn is_fresh_within(&self, duration: Duration) -> bool { + self.last_updated.elapsed() < duration + } } -static INSTANCE: OnceLock = OnceLock::new(); +impl StreamingParser for CurrentMemory { + fn parse_and_update( + line: &str, + current: Arc>, + ) -> Result<(), Box> { + if let Ok(memory) = serde_json::from_str::(line.trim()) { + AsyncHandler::spawn(move || async move { + let mut current_guard = current.write().await; + current_guard.inuse = memory.inuse; + current_guard.oslimit = memory.oslimit; + current_guard.mark_fresh(); + }); + } + Ok(()) + } +} + +// Minimal memory monitor using the new architecture +pub struct MemoryMonitor { + monitor: IpcStreamMonitor, +} + +impl Default for MemoryMonitor { + fn default() -> Self { + MemoryMonitor { + monitor: IpcStreamMonitor::new( + "/memory".to_string(), + Duration::from_secs(10), + Duration::from_secs(2), + Duration::from_secs(10), + ), + } + } +} + +// Use simplified singleton_lazy_with_logging macro +singleton_lazy_with_logging!( + MemoryMonitor, + INSTANCE, + "MemoryMonitor", + MemoryMonitor::default +); impl MemoryMonitor { - pub fn global() -> &'static MemoryMonitor { - INSTANCE.get_or_init(|| { - let ipc_path_buf = ipc_path().unwrap(); - let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); - let client = IpcStreamClient::new(ipc_path).unwrap(); - - let instance = MemoryMonitor::new(client); - logging!( - info, - Type::Ipc, - true, - "MemoryMonitor initialized with IPC path: {}", - ipc_path - ); - instance - }) - } - - fn new(client: IpcStreamClient) -> Self { - let current = Arc::new(RwLock::new(CurrentMemory::default())); - let monitor_current = current.clone(); - - tokio::spawn(async move { - loop { - let _ = client - .get("/memory") - .timeout(Duration::from_secs(10)) - .process_lines(|line| { - if let Ok(memory) = serde_json::from_str::(line.trim()) { - tokio::spawn({ - let current = monitor_current.clone(); - async move { - *current.write().await = CurrentMemory { - inuse: memory.inuse, - oslimit: memory.oslimit, - last_updated: Instant::now(), - }; - } - }); - } - Ok(()) - }) - .await; - tokio::time::sleep(Duration::from_secs(2)).await; // Memory updates less frequently - } - }); - - Self { current } - } - pub async fn current(&self) -> CurrentMemory { - self.current.read().await.clone() + self.monitor.current().await } pub async fn is_fresh(&self) -> bool { - self.current.read().await.last_updated.elapsed() < Duration::from_secs(10) + self.monitor.is_fresh().await } } -fn fmt_bytes(bytes: u64) -> String { - const UNITS: &[&str] = &["B", "KB", "MB", "GB"]; - let (mut val, mut unit) = (bytes as f64, 0); - while val >= 1024.0 && unit < 3 { - val /= 1024.0; - unit += 1; - } - format!("{:.1}{}", val, UNITS[unit]) -} - pub async fn get_current_memory() -> CurrentMemory { MemoryMonitor::global().current().await } diff --git a/clash-verge-rev/src-tauri/src/ipc/mod.rs b/clash-verge-rev/src-tauri/src/ipc/mod.rs index 7bddb68ef9..8b2f42f877 100644 --- a/clash-verge-rev/src-tauri/src/ipc/mod.rs +++ b/clash-verge-rev/src-tauri/src/ipc/mod.rs @@ -1,8 +1,11 @@ pub mod general; +pub mod logs; pub mod memory; +pub mod monitor; pub mod traffic; pub use general::IpcManager; +pub use logs::{clear_logs, get_logs_json, start_logs_monitoring, stop_logs_monitoring}; pub use memory::{get_current_memory, get_formatted_memory}; pub use traffic::{get_current_traffic, get_formatted_traffic}; diff --git a/clash-verge-rev/src-tauri/src/ipc/monitor.rs b/clash-verge-rev/src-tauri/src/ipc/monitor.rs new file mode 100644 index 0000000000..f12f363358 --- /dev/null +++ b/clash-verge-rev/src-tauri/src/ipc/monitor.rs @@ -0,0 +1,120 @@ +use kode_bridge::IpcStreamClient; +use std::sync::Arc; +use tokio::{sync::RwLock, time::Duration}; + +use crate::{ + logging, + process::AsyncHandler, + utils::{dirs::ipc_path, logging::Type}, +}; + +/// Generic base structure for IPC monitoring data with freshness tracking +pub trait MonitorData: Clone + Send + Sync + 'static { + /// Update the last_updated timestamp to now + fn mark_fresh(&mut self); + + /// Check if data is fresh based on the given duration + fn is_fresh_within(&self, duration: Duration) -> bool; +} + +/// Trait for parsing streaming data and updating monitor state +pub trait StreamingParser: MonitorData { + /// Parse a line of streaming data and update the current state + fn parse_and_update( + line: &str, + current: Arc>, + ) -> Result<(), Box>; +} + +/// Generic IPC stream monitor that handles the common streaming pattern +pub struct IpcStreamMonitor +where + T: MonitorData + StreamingParser + Default, +{ + current: Arc>, + #[allow(dead_code)] + endpoint: String, + #[allow(dead_code)] + timeout: Duration, + #[allow(dead_code)] + retry_interval: Duration, + freshness_duration: Duration, +} + +impl IpcStreamMonitor +where + T: MonitorData + StreamingParser + Default, +{ + pub fn new( + endpoint: String, + timeout: Duration, + retry_interval: Duration, + freshness_duration: Duration, + ) -> Self { + let current = Arc::new(RwLock::new(T::default())); + let monitor_current = Arc::clone(¤t); + let endpoint_clone = endpoint.clone(); + + // Start the monitoring task + AsyncHandler::spawn(move || async move { + Self::streaming_task(monitor_current, endpoint_clone, timeout, retry_interval).await; + }); + + Self { + current, + endpoint, + timeout, + retry_interval, + freshness_duration, + } + } + + pub async fn current(&self) -> T { + self.current.read().await.clone() + } + + pub async fn is_fresh(&self) -> bool { + self.current + .read() + .await + .is_fresh_within(self.freshness_duration) + } + + /// The core streaming task that can be specialized per monitor type + async fn streaming_task( + current: Arc>, + endpoint: String, + timeout: Duration, + retry_interval: Duration, + ) { + loop { + let ipc_path_buf = match ipc_path() { + Ok(path) => path, + Err(e) => { + logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e); + tokio::time::sleep(retry_interval).await; + continue; + } + }; + + let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); + + let client = match IpcStreamClient::new(ipc_path) { + Ok(client) => client, + Err(e) => { + logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e); + tokio::time::sleep(retry_interval).await; + continue; + } + }; + + let _ = client + .get(&endpoint) + .timeout(timeout) + .process_lines(|line| T::parse_and_update(line, Arc::clone(¤t))) + .await; + + tokio::time::sleep(retry_interval).await; + } + } +} diff --git a/clash-verge-rev/src-tauri/src/ipc/traffic.rs b/clash-verge-rev/src-tauri/src/ipc/traffic.rs index 0e40e88f24..ac30820ef5 100644 --- a/clash-verge-rev/src-tauri/src/ipc/traffic.rs +++ b/clash-verge-rev/src-tauri/src/ipc/traffic.rs @@ -1,14 +1,12 @@ -use kode_bridge::IpcStreamClient; use serde::{Deserialize, Serialize}; -use std::{ - sync::{Arc, OnceLock}, - time::Instant, -}; +use std::{sync::Arc, time::Instant}; use tokio::{sync::RwLock, time::Duration}; use crate::{ - logging, - utils::{dirs::ipc_path, logging::Type}, + ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser}, + process::AsyncHandler, + singleton_lazy_with_logging, + utils::format::fmt_bytes, }; #[derive(Debug, Clone, Deserialize, Serialize)] @@ -38,97 +36,104 @@ impl Default for CurrentTraffic { } } -// Minimal traffic monitor -pub struct TrafficMonitor { - current: Arc>, +impl MonitorData for CurrentTraffic { + fn mark_fresh(&mut self) { + self.last_updated = Instant::now(); + } + + fn is_fresh_within(&self, duration: Duration) -> bool { + self.last_updated.elapsed() < duration + } } -static INSTANCE: OnceLock = OnceLock::new(); +// Traffic monitoring state for calculating rates +#[derive(Debug, Clone, Default)] +pub struct TrafficMonitorState { + pub current: CurrentTraffic, + pub last_traffic: Option, +} + +impl MonitorData for TrafficMonitorState { + fn mark_fresh(&mut self) { + self.current.mark_fresh(); + } + + fn is_fresh_within(&self, duration: Duration) -> bool { + self.current.is_fresh_within(duration) + } +} + +impl StreamingParser for TrafficMonitorState { + fn parse_and_update( + line: &str, + current: Arc>, + ) -> Result<(), Box> { + if let Ok(traffic) = serde_json::from_str::(line.trim()) { + AsyncHandler::spawn(move || async move { + let mut state_guard = current.write().await; + + let (up_rate, down_rate) = state_guard + .last_traffic + .as_ref() + .map(|l| { + ( + traffic.up.saturating_sub(l.up), + traffic.down.saturating_sub(l.down), + ) + }) + .unwrap_or((0, 0)); + + state_guard.current = CurrentTraffic { + up_rate, + down_rate, + total_up: traffic.up, + total_down: traffic.down, + last_updated: Instant::now(), + }; + + state_guard.last_traffic = Some(traffic); + }); + } + Ok(()) + } +} + +// Minimal traffic monitor using the new architecture +pub struct TrafficMonitor { + monitor: IpcStreamMonitor, +} + +impl Default for TrafficMonitor { + fn default() -> Self { + TrafficMonitor { + monitor: IpcStreamMonitor::new( + "/traffic".to_string(), + Duration::from_secs(10), + Duration::from_secs(1), + Duration::from_secs(5), + ), + } + } +} + +// Use simplified singleton_lazy_with_logging macro +singleton_lazy_with_logging!( + TrafficMonitor, + INSTANCE, + "TrafficMonitor", + TrafficMonitor::default +); impl TrafficMonitor { - pub fn global() -> &'static TrafficMonitor { - INSTANCE.get_or_init(|| { - let ipc_path_buf = ipc_path().unwrap(); - let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); - let client = IpcStreamClient::new(ipc_path).unwrap(); - - let instance = TrafficMonitor::new(client); - logging!( - info, - Type::Ipc, - true, - "TrafficMonitor initialized with IPC path: {}", - ipc_path - ); - instance - }) - } - - fn new(client: IpcStreamClient) -> Self { - let current = Arc::new(RwLock::new(CurrentTraffic::default())); - let monitor_current = current.clone(); - - tokio::spawn(async move { - let mut last: Option = None; - loop { - let _ = client - .get("/traffic") - .timeout(Duration::from_secs(10)) - .process_lines(|line| { - if let Ok(traffic) = serde_json::from_str::(line.trim()) { - let (up_rate, down_rate) = last - .as_ref() - .map(|l| { - ( - traffic.up.saturating_sub(l.up), - traffic.down.saturating_sub(l.down), - ) - }) - .unwrap_or((0, 0)); - - tokio::spawn({ - let current = monitor_current.clone(); - async move { - *current.write().await = CurrentTraffic { - up_rate, - down_rate, - total_up: traffic.up, - total_down: traffic.down, - last_updated: Instant::now(), - }; - } - }); - last = Some(traffic); - } - Ok(()) - }) - .await; - tokio::time::sleep(Duration::from_secs(1)).await; - } - }); - - Self { current } - } - pub async fn current(&self) -> CurrentTraffic { - self.current.read().await.clone() + self.monitor.current().await.current } pub async fn is_fresh(&self) -> bool { - self.current.read().await.last_updated.elapsed() < Duration::from_secs(5) + self.monitor.is_fresh().await } } -fn fmt_bytes(bytes: u64) -> String { - const UNITS: &[&str] = &["B", "KB", "MB", "GB"]; - let (mut val, mut unit) = (bytes as f64, 0); - while val >= 1024.0 && unit < 3 { - val /= 1024.0; - unit += 1; - } - format!("{:.1}{}", val, UNITS[unit]) -} - pub async fn get_current_traffic() -> CurrentTraffic { TrafficMonitor::global().current().await } diff --git a/clash-verge-rev/src-tauri/src/lib.rs b/clash-verge-rev/src-tauri/src/lib.rs index 213795d43d..44683d27d2 100644 --- a/clash-verge-rev/src-tauri/src/lib.rs +++ b/clash-verge-rev/src-tauri/src/lib.rs @@ -1,3 +1,7 @@ +#![allow(non_snake_case)] +#![recursion_limit = "512"] + +mod cache; mod cmd; pub mod config; mod core; @@ -6,222 +10,137 @@ mod feat; mod ipc; mod module; mod process; -mod state; mod utils; +#[cfg(target_os = "macos")] +use crate::utils::window_manager::WindowManager; use crate::{ + core::handle, core::hotkey, process::AsyncHandler, - utils::{resolve, resolve::resolve_scheme, server}, + utils::{resolve, server}, }; use config::Config; -use parking_lot::Mutex; -use std::sync::Once; use tauri::AppHandle; #[cfg(target_os = "macos")] use tauri::Manager; #[cfg(target_os = "macos")] use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_deep_link::DeepLinkExt; -use tokio::time::{timeout, Duration}; +use tokio::time::{Duration, timeout}; use utils::logging::Type; -/// A global singleton handle to the application. -pub struct AppHandleManager { - inner: Mutex>, - init: Once, -} +/// Application initialization helper functions +mod app_init { + use super::*; -impl AppHandleManager { - /// Get the global instance of the app handle manager. - pub fn global() -> &'static Self { - static INSTANCE: AppHandleManager = AppHandleManager { - inner: Mutex::new(None), - init: Once::new(), - }; - &INSTANCE - } - - /// Initialize the app handle manager with an app handle. - pub fn init(&self, handle: AppHandle) { - self.init.call_once(|| { - let mut app_handle = self.inner.lock(); - *app_handle = Some(handle); + /// Initialize singleton monitoring for other instances + pub fn init_singleton_check() { + AsyncHandler::spawn_blocking(move || async move { + logging!(info, Type::Setup, true, "开始检查单例实例..."); + match timeout(Duration::from_millis(500), server::check_singleton()).await { + Ok(result) => { + if result.is_err() { + logging!(info, Type::Setup, true, "检测到已有应用实例运行"); + if let Some(app_handle) = handle::Handle::global().app_handle() { + app_handle.exit(0); + } else { + std::process::exit(0); + } + } else { + logging!(info, Type::Setup, true, "未检测到其他应用实例"); + } + } + Err(_) => { + logging!( + warn, + Type::Setup, + true, + "单例检查超时,假定没有其他实例运行" + ); + } + } }); } - /// Get the app handle if it has been initialized. - pub fn get(&self) -> Option { - self.inner.lock().clone() - } + /// Setup plugins for the Tauri builder + pub fn setup_plugins(builder: tauri::Builder) -> tauri::Builder { + #[allow(unused_mut)] + let mut builder = builder + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_clipboard_manager::init()) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_global_shortcut::Builder::new().build()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_http::init()); - /// Get the app handle, panics if it hasn't been initialized. - pub fn get_handle(&self) -> AppHandle { - self.get().expect("AppHandle not initialized") - } - - pub fn set_activation_policy_regular(&self) { - #[cfg(target_os = "macos")] + // Devtools plugin only in debug mode with feature tauri-dev + // to avoid duplicated registering of logger since the devtools plugin also registers a logger + #[cfg(all(debug_assertions, not(feature = "tokio-trace"), feature = "tauri-dev"))] { - let app_handle = self.inner.lock(); - let app_handle = app_handle.as_ref().unwrap(); - let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular); + builder = builder.plugin(tauri_plugin_devtools::init()); } + builder } - pub fn set_activation_policy_accessory(&self) { - #[cfg(target_os = "macos")] + /// Setup deep link handling + pub fn setup_deep_links(app: &tauri::App) -> Result<(), Box> { + #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { - let app_handle = self.inner.lock(); - let app_handle = app_handle.as_ref().unwrap(); - let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory); + logging!(info, Type::Setup, true, "注册深层链接..."); + app.deep_link().register_all()?; } - } - pub fn set_activation_policy_prohibited(&self) { - #[cfg(target_os = "macos")] - { - let app_handle = self.inner.lock(); - let app_handle = app_handle.as_ref().unwrap(); - let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Prohibited); - } - } -} - -#[allow(clippy::panic)] -pub fn run() { - utils::network::NetworkManager::global().init(); - - let _ = utils::dirs::init_portable_flag(); - - // 异步单例检测 - AsyncHandler::spawn(move || async move { - logging!(info, Type::Setup, true, "开始检查单例实例..."); - match timeout(Duration::from_secs(3), server::check_singleton()).await { - Ok(result) => { - if result.is_err() { - logging!(info, Type::Setup, true, "检测到已有应用实例运行"); - if let Some(app_handle) = AppHandleManager::global().get() { - app_handle.exit(0); - } else { - std::process::exit(0); - } - } else { - logging!(info, Type::Setup, true, "未检测到其他应用实例"); - } - } - Err(_) => { - logging!( - warn, - Type::Setup, - true, - "单例检查超时,假定没有其他实例运行" - ); - } - } - }); - - #[cfg(target_os = "linux")] - std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); - - #[cfg(debug_assertions)] - let devtools = tauri_plugin_devtools::init(); - - #[allow(unused_mut)] - let mut builder = tauri::Builder::default() - .plugin(tauri_plugin_notification::init()) - .plugin(tauri_plugin_updater::Builder::new().build()) - .plugin(tauri_plugin_clipboard_manager::init()) - .plugin(tauri_plugin_process::init()) - .plugin(tauri_plugin_global_shortcut::Builder::new().build()) - .plugin(tauri_plugin_fs::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_deep_link::init()) - .manage(Mutex::new(state::lightweight::LightWeightState::default())) - .setup(|app| { - logging!(info, Type::Setup, true, "开始应用初始化..."); - let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new(); - #[cfg(target_os = "macos")] - { - auto_start_plugin_builder = auto_start_plugin_builder - .macos_launcher(MacosLauncher::LaunchAgent) - .app_name(app.config().identifier.clone()); - } - let _ = app.handle().plugin(auto_start_plugin_builder.build()); - - #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] - { - use tauri_plugin_deep_link::DeepLinkExt; - logging!(info, Type::Setup, true, "注册深层链接..."); - logging_error!(Type::System, true, app.deep_link().register_all()); - } - - app.deep_link().on_open_url(|event| { - AsyncHandler::spawn(move || { - let url = event.urls().first().map(|u| u.to_string()); - async move { - if let Some(url) = url { - logging_error!(Type::Setup, true, resolve_scheme(url).await); - } + app.deep_link().on_open_url(|event| { + let url = event.urls().first().map(|u| u.to_string()); + if let Some(url) = url { + AsyncHandler::spawn(|| async { + if let Err(e) = resolve::resolve_scheme(url).await { + logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e); } }); - }); - - // 窗口管理 - logging!(info, Type::Setup, true, "初始化窗口状态管理..."); - let window_state_plugin = tauri_plugin_window_state::Builder::new() - .with_filename("window_state.json") - .with_state_flags(tauri_plugin_window_state::StateFlags::default()) - .build(); - let _ = app.handle().plugin(window_state_plugin); - - // 异步处理 - let app_handle = app.handle().clone(); - AsyncHandler::spawn(move || async move { - logging!(info, Type::Setup, true, "异步执行应用设置..."); - match timeout( - Duration::from_secs(30), - resolve::resolve_setup_async(&app_handle), - ) - .await - { - Ok(_) => { - logging!(info, Type::Setup, true, "应用设置成功完成"); - } - Err(_) => { - logging!( - error, - Type::Setup, - true, - "应用设置超时(30秒),继续执行后续流程" - ); - } - } - }); - - logging!(info, Type::Setup, true, "执行主要设置操作..."); - - logging!(info, Type::Setup, true, "初始化AppHandleManager..."); - AppHandleManager::global().init(app.handle().clone()); - - logging!(info, Type::Setup, true, "初始化核心句柄..."); - core::handle::Handle::global().init(app.handle()); - - logging!(info, Type::Setup, true, "初始化配置..."); - if let Err(e) = utils::init::init_config() { - logging!(error, Type::Setup, true, "初始化配置失败: {}", e); } + }); - logging!(info, Type::Setup, true, "初始化资源..."); - if let Err(e) = utils::init::init_resources() { - logging!(error, Type::Setup, true, "初始化资源失败: {}", e); - } + Ok(()) + } - logging!(info, Type::Setup, true, "初始化完成,继续执行"); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - // common + /// Setup autostart plugin + pub fn setup_autostart(app: &tauri::App) -> Result<(), Box> { + #[cfg(target_os = "macos")] + let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new(); + #[cfg(not(target_os = "macos"))] + let auto_start_plugin_builder = tauri_plugin_autostart::Builder::new(); + + #[cfg(target_os = "macos")] + { + auto_start_plugin_builder = auto_start_plugin_builder + .macos_launcher(MacosLauncher::LaunchAgent) + .app_name(app.config().identifier.clone()); + } + app.handle().plugin(auto_start_plugin_builder.build())?; + Ok(()) + } + + /// Setup window state management + pub fn setup_window_state(app: &tauri::App) -> Result<(), Box> { + logging!(info, Type::Setup, true, "初始化窗口状态管理..."); + let window_state_plugin = tauri_plugin_window_state::Builder::new() + .with_filename("window_state.json") + .with_state_flags(tauri_plugin_window_state::StateFlags::default()) + .build(); + app.handle().plugin(window_state_plugin)?; + Ok(()) + } + + /// Generate all command handlers for the application + pub fn generate_handlers() + -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { + tauri::generate_handler![ + // Common commands cmd::get_sys_proxy, cmd::get_auto_proxy, cmd::open_app_dir, @@ -232,28 +151,27 @@ pub fn run() { cmd::get_network_interfaces, cmd::get_system_hostname, cmd::restart_app, - // 内核管理 + // Core management cmd::start_core, cmd::stop_core, cmd::restart_core, - // 启动命令 + // Application lifecycle cmd::notify_ui_ready, cmd::update_ui_stage, - cmd::reset_ui_ready_state, cmd::get_running_mode, cmd::get_app_uptime, cmd::get_auto_launch_status, cmd::is_admin, - // 添加轻量模式相关命令 + // Lightweight mode cmd::entry_lightweight_mode, cmd::exit_lightweight_mode, - // service 管理 + // Service management cmd::install_service, cmd::uninstall_service, cmd::reinstall_service, cmd::repair_service, cmd::is_service_available, - // clash + // Clash core commands cmd::get_clash_info, cmd::patch_clash_config, cmd::patch_clash_mode, @@ -262,11 +180,15 @@ pub fn run() { cmd::get_runtime_yaml, cmd::get_runtime_exists, cmd::get_runtime_logs, + cmd::get_runtime_proxy_chain_config, + cmd::update_proxy_chain_config_in_runtime, cmd::invoke_uwp_tool, cmd::copy_clash_env, cmd::get_proxies, cmd::force_refresh_proxies, cmd::get_providers_proxies, + cmd::sync_tray_proxy_selection, + cmd::update_proxy_and_sync, cmd::save_dns_config, cmd::apply_dns_config, cmd::check_dns_config_exists, @@ -290,6 +212,11 @@ pub fn run() { cmd::get_group_proxy_delays, cmd::is_clash_debug_enabled, cmd::clash_gc, + // Logging and monitoring + cmd::get_clash_logs, + cmd::start_logs_monitoring, + cmd::stop_logs_monitoring, + cmd::clear_logs, cmd::get_traffic_data, cmd::get_memory_data, cmd::get_formatted_traffic_data, @@ -297,7 +224,7 @@ pub fn run() { cmd::get_system_monitor_overview, cmd::start_traffic_service, cmd::stop_traffic_service, - // verge + // Verge configuration cmd::get_verge_config, cmd::patch_verge_config, cmd::test_delay, @@ -307,7 +234,7 @@ pub fn run() { cmd::open_devtools, cmd::exit_app, cmd::get_network_interfaces_info, - // profile + // Profile management cmd::get_profiles, cmd::enhance_profiles, cmd::patch_profiles_config, @@ -321,164 +248,334 @@ pub fn run() { cmd::read_profile_file, cmd::save_profile_file, cmd::get_next_update_time, - // script validation + // Script validation cmd::script_validate_notice, cmd::validate_script_file, - // clash api + // Clash API cmd::clash_api_get_proxy_delay, - // backup + // Backup and WebDAV cmd::create_webdav_backup, cmd::save_webdav_config, cmd::list_webdav_backup, cmd::delete_webdav_backup, cmd::restore_webdav_backup, - // export diagnostic info for issue reporting + // Diagnostics and system info cmd::export_diagnostic_info, - // get system info for display cmd::get_system_info, - // media unlock checker + // Media unlock checker cmd::get_unlock_items, cmd::check_media_unlock, - // light-weight model - cmd::entry_lightweight_mode, - ]); + ] + } +} - #[cfg(debug_assertions)] +pub fn run() { + // Setup singleton check + app_init::init_singleton_check(); + + let _ = utils::dirs::init_portable_flag(); + + // Set Linux environment variable + #[cfg(target_os = "linux")] { - builder = builder.plugin(devtools); + unsafe { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + + let desktop_env = std::env::var("XDG_CURRENT_DESKTOP") + .unwrap_or_default() + .to_uppercase(); + let is_kde_desktop = desktop_env.contains("KDE"); + let is_plasma_desktop = desktop_env.contains("PLASMA"); + + if is_kde_desktop || is_plasma_desktop { + unsafe { + std::env::set_var("GTK_CSD", "0"); + } + logging!( + info, + Type::Setup, + true, + "KDE detected: Disabled GTK CSD for better titlebar stability." + ); + } } - // Macos Application Menu - #[cfg(target_os = "macos")] - { - // Temporary Achived due to cannot CMD+C/V/A - } + // Create and configure the Tauri builder + let builder = app_init::setup_plugins(tauri::Builder::default()) + .setup(|app| { + logging!(info, Type::Setup, true, "开始应用初始化..."); - let app = builder - .build(tauri::generate_context!()) - .expect("error while running tauri application"); + // Setup autostart plugin + if let Err(e) = app_init::setup_autostart(app) { + logging!(error, Type::Setup, true, "Failed to setup autostart: {}", e); + } - app.run(|app_handle, e| match e { - tauri::RunEvent::Ready | tauri::RunEvent::Resumed => { + // Setup deep links + if let Err(e) = app_init::setup_deep_links(app) { + logging!( + error, + Type::Setup, + true, + "Failed to setup deep links: {}", + e + ); + } + + // Setup window state management + if let Err(e) = app_init::setup_window_state(app) { + logging!( + error, + Type::Setup, + true, + "Failed to setup window state: {}", + e + ); + } + + let app_handle = app.handle().clone(); + + logging!(info, Type::Setup, true, "执行主要设置操作..."); + + resolve::resolve_setup_handle(app_handle); + resolve::resolve_setup_async(); + resolve::resolve_setup_sync(); + + logging!(info, Type::Setup, true, "初始化完成,继续执行"); + Ok(()) + }) + .invoke_handler(app_init::generate_handlers()); + + /// Event handling helper functions + mod event_handlers { + use crate::core::handle; + + use super::*; + + /// Handle application ready/resumed events + pub fn handle_ready_resumed(app_handle: &AppHandle) { logging!(info, Type::System, true, "应用就绪或恢复"); - AppHandleManager::global().init(app_handle.clone()); + handle::Handle::global().init(app_handle.clone()); + #[cfg(target_os = "macos")] { - if let Some(window) = AppHandleManager::global() - .get_handle() - .get_webview_window("main") - { + if let Some(window) = app_handle.get_webview_window("main") { logging!(info, Type::Window, true, "设置macOS窗口标题"); let _ = window.set_title("Clash Verge"); } } } + + /// Handle application reopen events (macOS) #[cfg(target_os = "macos")] - tauri::RunEvent::Reopen { - has_visible_windows, - .. - } => { + pub async fn handle_reopen(app_handle: &AppHandle, has_visible_windows: bool) { + logging!( + info, + Type::System, + true, + "处理 macOS 应用重新打开事件: has_visible_windows={}", + has_visible_windows + ); + + handle::Handle::global().init(app_handle.clone()); + if !has_visible_windows { - AppHandleManager::global().set_activation_policy_regular(); - } - AppHandleManager::global().init(app_handle.clone()); - } - tauri::RunEvent::ExitRequested { api, code, .. } => { - if code.is_none() { - api.prevent_exit(); + // 当没有可见窗口时,设置为 regular 模式并显示主窗口 + handle::Handle::global().set_activation_policy_regular(); + + logging!(info, Type::System, true, "没有可见窗口,尝试显示主窗口"); + + let result = WindowManager::show_main_window().await; + logging!( + info, + Type::System, + true, + "窗口显示操作完成,结果: {:?}", + result + ); + } else { + logging!(info, Type::System, true, "已有可见窗口,无需额外操作"); } } - tauri::RunEvent::Exit => { - // avoid duplicate cleanup + + /// Handle window close requests + pub fn handle_window_close(api: &tauri::WindowEvent) { + #[cfg(target_os = "macos")] + handle::Handle::global().set_activation_policy_accessory(); + if core::handle::Handle::global().is_exiting() { return; } - feat::clean(); - } - tauri::RunEvent::WindowEvent { label, event, .. } => { - if label == "main" { - match event { - tauri::WindowEvent::CloseRequested { api, .. } => { - #[cfg(target_os = "macos")] - AppHandleManager::global().set_activation_policy_accessory(); - if core::handle::Handle::global().is_exiting() { - return; - } - log::info!(target: "app", "closing window..."); - api.prevent_close(); - if let Some(window) = core::handle::Handle::global().get_window() { - let _ = window.hide(); - } else { - logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在"); - } - } - tauri::WindowEvent::Focused(true) => { - #[cfg(target_os = "macos")] - { - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().register("CMD+Q", "quit") - ); - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().register("CMD+W", "hide") - ); - } - { - let is_enable_global_hotkey = Config::verge() - .latest_ref() - .enable_global_hotkey - .unwrap_or(true); - if !is_enable_global_hotkey { - logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().init()) - } - } - } - tauri::WindowEvent::Focused(false) => { - #[cfg(target_os = "macos")] - { - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().unregister("CMD+Q") - ); - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().unregister("CMD+W") - ); - } - { - let is_enable_global_hotkey = Config::verge() - .latest_ref() - .enable_global_hotkey - .unwrap_or(true); - if !is_enable_global_hotkey { - logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().reset()) - } - } - } - tauri::WindowEvent::Destroyed => { - #[cfg(target_os = "macos")] - { - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().unregister("CMD+Q") - ); - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().unregister("CMD+W") - ); - } - } - _ => {} + + log::info!(target: "app", "closing window..."); + if let tauri::WindowEvent::CloseRequested { api, .. } = api { + api.prevent_close(); + if let Some(window) = core::handle::Handle::global().get_window() { + let _ = window.hide(); + } else { + logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在"); } } } - _ => {} + + /// Handle window focus events + pub fn handle_window_focus(focused: bool) { + AsyncHandler::spawn(move || async move { + let is_enable_global_hotkey = Config::verge() + .await + .latest_ref() + .enable_global_hotkey + .unwrap_or(true); + + if focused { + #[cfg(target_os = "macos")] + { + use crate::core::hotkey::SystemHotkey; + if let Err(e) = hotkey::Hotkey::global() + .register_system_hotkey(SystemHotkey::CmdQ) + .await + { + logging!(error, Type::Hotkey, true, "Failed to register CMD+Q: {}", e); + } + if let Err(e) = hotkey::Hotkey::global() + .register_system_hotkey(SystemHotkey::CmdW) + .await + { + logging!(error, Type::Hotkey, true, "Failed to register CMD+W: {}", e); + } + } + + if !is_enable_global_hotkey + && let Err(e) = hotkey::Hotkey::global().init().await + { + logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e); + } + return; + } + + // Handle unfocused state + #[cfg(target_os = "macos")] + { + use crate::core::hotkey::SystemHotkey; + if let Err(e) = + hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ) + { + logging!( + error, + Type::Hotkey, + true, + "Failed to unregister CMD+Q: {}", + e + ); + } + if let Err(e) = + hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW) + { + logging!( + error, + Type::Hotkey, + true, + "Failed to unregister CMD+W: {}", + e + ); + } + } + + if !is_enable_global_hotkey && let Err(e) = hotkey::Hotkey::global().reset() { + logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e); + } + }); + } + + /// Handle window destroyed events + pub fn handle_window_destroyed() { + #[cfg(target_os = "macos")] + { + use crate::core::hotkey::SystemHotkey; + if let Err(e) = + hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ) + { + logging!( + error, + Type::Hotkey, + true, + "Failed to unregister CMD+Q on destroy: {}", + e + ); + } + if let Err(e) = + hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW) + { + logging!( + error, + Type::Hotkey, + true, + "Failed to unregister CMD+W on destroy: {}", + e + ); + } + } + } + } + + // Build the application + let app = builder + .build(tauri::generate_context!()) + .unwrap_or_else(|e| { + logging!( + error, + Type::Setup, + true, + "Failed to build Tauri application: {}", + e + ); + std::process::exit(1); + }); + + app.run(|app_handle, e| { + match e { + tauri::RunEvent::Ready | tauri::RunEvent::Resumed => { + event_handlers::handle_ready_resumed(app_handle); + } + #[cfg(target_os = "macos")] + tauri::RunEvent::Reopen { + has_visible_windows, + .. + } => { + let app_handle = app_handle.clone(); + AsyncHandler::spawn(move || async move { + event_handlers::handle_reopen(&app_handle, has_visible_windows).await; + }); + } + tauri::RunEvent::ExitRequested { api, code, .. } => { + if code.is_none() { + api.prevent_exit(); + } + } + tauri::RunEvent::Exit => { + // Avoid duplicate cleanup + if core::handle::Handle::global().is_exiting() { + return; + } + feat::clean(); + } + tauri::RunEvent::WindowEvent { label, event, .. } => { + if label == "main" { + match event { + tauri::WindowEvent::CloseRequested { .. } => { + event_handlers::handle_window_close(&event); + } + tauri::WindowEvent::Focused(focused) => { + event_handlers::handle_window_focus(focused); + } + tauri::WindowEvent::Destroyed => { + event_handlers::handle_window_destroyed(); + } + _ => {} + } + } + } + _ => {} + } }); } diff --git a/clash-verge-rev/src-tauri/src/main.rs b/clash-verge-rev/src-tauri/src/main.rs index 293b1b22a0..6a088a2ef1 100755 --- a/clash-verge-rev/src-tauri/src/main.rs +++ b/clash-verge-rev/src-tauri/src/main.rs @@ -1,4 +1,6 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { + #[cfg(feature = "tokio-trace")] + console_subscriber::init(); app_lib::run(); } diff --git a/clash-verge-rev/src-tauri/src/module/lightweight.rs b/clash-verge-rev/src-tauri/src/module/lightweight.rs index ff9ae361c5..ba42963b5e 100644 --- a/clash-verge-rev/src-tauri/src/module/lightweight.rs +++ b/clash-verge-rev/src-tauri/src/module/lightweight.rs @@ -1,109 +1,146 @@ use crate::{ + cache::CacheProxy, config::Config, core::{handle, timer::Timer, tray::Tray}, log_err, logging, - state::lightweight::LightWeightState, + process::AsyncHandler, utils::logging::Type, }; #[cfg(target_os = "macos")] use crate::logging_error; -#[cfg(target_os = "macos")] -use crate::AppHandleManager; +use crate::utils::window_manager::WindowManager; use anyhow::{Context, Result}; use delay_timer::prelude::TaskBuilder; -use parking_lot::Mutex; -use std::sync::atomic::{AtomicBool, Ordering}; -use tauri::{Listener, Manager}; +use std::sync::atomic::{AtomicU8, AtomicU32, Ordering}; +use tauri::Listener; const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; -// 添加退出轻量模式的锁,防止并发调用 -static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false); - -fn with_lightweight_status(f: F) -> R -where - F: FnOnce(&mut LightWeightState) -> R, -{ - let app_handle = handle::Handle::global().app_handle().unwrap(); - let state = app_handle.state::>(); - let mut guard = state.lock(); - f(&mut guard) +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum LightweightState { + Normal = 0, + In = 1, + Exiting = 2, } -pub fn run_once_auto_lightweight() { - LightWeightState::default().run_once_time(|| { - let is_silent_start = Config::verge() - .latest_ref() - .enable_silent_start - .unwrap_or(false); - let enable_auto = Config::verge() - .data_mut() - .enable_auto_light_weight_mode - .unwrap_or(false); - if enable_auto && is_silent_start { - logging!( - info, - Type::Lightweight, - true, - "在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器" - ); - set_lightweight_mode(false); - enable_auto_light_weight_mode(); - - // 触发托盘更新 - if let Err(e) = Tray::global().update_part() { - log::warn!("Failed to update tray: {e}"); - } - } - }); -} - -pub fn auto_lightweight_mode_init() { - if let Some(app_handle) = handle::Handle::global().app_handle() { - let _ = app_handle.state::>(); - let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false); - let enable_auto = - { Config::verge().latest_ref().enable_auto_light_weight_mode }.unwrap_or(false); - - if enable_auto && !is_silent_start { - logging!( - info, - Type::Lightweight, - true, - "非静默启动直接挂载自动进入轻量模式监听器!" - ); - set_lightweight_mode(true); - enable_auto_light_weight_mode(); - - // 确保托盘状态更新 - if let Err(e) = Tray::global().update_part() { - log::warn!("Failed to update tray: {e}"); - } +impl From for LightweightState { + fn from(v: u8) -> Self { + match v { + 1 => LightweightState::In, + 2 => LightweightState::Exiting, + _ => LightweightState::Normal, } } } +impl LightweightState { + fn as_u8(self) -> u8 { + self as u8 + } +} + +static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8); + +static WINDOW_CLOSE_HANDLER: AtomicU32 = AtomicU32::new(0); +static WEBVIEW_FOCUS_HANDLER: AtomicU32 = AtomicU32::new(0); + +fn set_state(new: LightweightState) { + LIGHTWEIGHT_STATE.store(new.as_u8(), Ordering::Release); + match new { + LightweightState::Normal => { + logging!(info, Type::Lightweight, true, "轻量模式已关闭"); + } + LightweightState::In => { + logging!(info, Type::Lightweight, true, "轻量模式已开启"); + } + LightweightState::Exiting => { + logging!(info, Type::Lightweight, true, "正在退出轻量模式"); + } + } +} + +fn get_state() -> LightweightState { + LIGHTWEIGHT_STATE.load(Ordering::Acquire).into() +} + // 检查是否处于轻量模式 pub fn is_in_lightweight_mode() -> bool { - with_lightweight_status(|state| state.is_lightweight) + get_state() == LightweightState::In } -// 设置轻量模式状态 -pub fn set_lightweight_mode(value: bool) { - with_lightweight_status(|state| { - state.set_lightweight_mode(value); - }); +// 设置轻量模式状态(仅 Normal <-> In) +async fn set_lightweight_mode(value: bool) { + let current = get_state(); + if value && current != LightweightState::In { + set_state(LightweightState::In); + } else if !value && current != LightweightState::Normal { + set_state(LightweightState::Normal); + } - // 触发托盘更新 - if let Err(e) = Tray::global().update_part() { + // 只有在状态可用时才触发托盘更新 + if let Err(e) = Tray::global().update_part().await { log::warn!("Failed to update tray: {e}"); } } -pub fn enable_auto_light_weight_mode() { - Timer::global().init().unwrap(); +pub async fn run_once_auto_lightweight() { + let verge_config = Config::verge().await; + let enable_auto = verge_config + .data_mut() + .enable_auto_light_weight_mode + .unwrap_or(false); + let is_silent_start = verge_config + .latest_ref() + .enable_silent_start + .unwrap_or(false); + + if !(enable_auto && is_silent_start) { + logging!( + info, + Type::Lightweight, + true, + "不满足静默启动且自动进入轻量模式的条件,跳过自动进入轻量模式" + ); + return; + } + + set_lightweight_mode(true).await; + enable_auto_light_weight_mode().await; +} + +pub async fn auto_lightweight_mode_init() -> Result<()> { + let is_silent_start = + { Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false); + let enable_auto = { + Config::verge() + .await + .latest_ref() + .enable_auto_light_weight_mode + } + .unwrap_or(false); + + if enable_auto && !is_silent_start { + logging!( + info, + Type::Lightweight, + true, + "非静默启动直接挂载自动进入轻量模式监听器!" + ); + set_state(LightweightState::Normal); + enable_auto_light_weight_mode().await; + } + + Ok(()) +} + +pub async fn enable_auto_light_weight_mode() { + if let Err(e) = Timer::global().init().await { + logging!(error, Type::Lightweight, "Failed to initialize timer: {e}"); + return; + } logging!(info, Type::Lightweight, true, "开启自动轻量模式"); setup_window_close_listener(); setup_webview_focus_listener(); @@ -113,83 +150,82 @@ pub fn disable_auto_light_weight_mode() { logging!(info, Type::Lightweight, true, "关闭自动轻量模式"); let _ = cancel_light_weight_timer(); cancel_window_close_listener(); + cancel_webview_focus_listener(); } -pub fn entry_lightweight_mode() { - use crate::utils::window_manager::WindowManager; - - let result = WindowManager::hide_main_window(); - logging!( - info, - Type::Lightweight, - true, - "轻量模式隐藏窗口结果: {:?}", - result - ); - - if let Some(window) = handle::Handle::global().get_window() { - if let Some(webview) = window.get_webview_window("main") { - let _ = webview.destroy(); - } - #[cfg(target_os = "macos")] - AppHandleManager::global().set_activation_policy_accessory(); +pub async fn entry_lightweight_mode() -> bool { + // 尝试从 Normal -> In + if LIGHTWEIGHT_STATE + .compare_exchange( + LightweightState::Normal as u8, + LightweightState::In as u8, + Ordering::Acquire, + Ordering::Relaxed, + ) + .is_err() + { + logging!(info, Type::Lightweight, true, "无需进入轻量模式,跳过调用"); + return false; } - set_lightweight_mode(true); + + WindowManager::destroy_main_window(); + + set_lightweight_mode(true).await; let _ = cancel_light_weight_timer(); - // 更新托盘显示 - let _tray = crate::core::tray::Tray::global(); + // 回到 In + set_state(LightweightState::In); + + CacheProxy::global().clean_default_keys(); + true } // 添加从轻量模式恢复的函数 -pub fn exit_lightweight_mode() { - // 使用原子操作检查是否已经在退出过程中,防止并发调用 - if EXITING_LIGHTWEIGHT - .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) +pub async fn exit_lightweight_mode() -> bool { + // 尝试从 In -> Exiting + if LIGHTWEIGHT_STATE + .compare_exchange( + LightweightState::In as u8, + LightweightState::Exiting as u8, + Ordering::Acquire, + Ordering::Relaxed, + ) .is_err() { logging!( info, Type::Lightweight, true, - "轻量模式退出操作已在进行中,跳过重复调用" + "轻量模式不在退出条件(可能已退出或正在退出),跳过调用" ); - return; + return false; } - // 使用defer确保无论如何都会重置标志 - let _guard = scopeguard::guard((), |_| { - EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst); - }); + WindowManager::show_main_window().await; - // 确保当前确实处于轻量模式才执行退出操作 - if !is_in_lightweight_mode() { - logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出"); - return; - } + set_lightweight_mode(false).await; + let _ = cancel_light_weight_timer(); - set_lightweight_mode(false); + // 回到 Normal + set_state(LightweightState::Normal); - // macOS激活策略 - #[cfg(target_os = "macos")] - AppHandleManager::global().set_activation_policy_regular(); - - // 重置UI就绪状态 - crate::utils::resolve::reset_ui_ready(); - - // 更新托盘显示 - let _tray = crate::core::tray::Tray::global(); + logging!(info, Type::Lightweight, true, "轻量模式退出完成"); + true } #[cfg(target_os = "macos")] -pub fn add_light_weight_timer() { - logging_error!(Type::Lightweight, setup_light_weight_timer()); +pub async fn add_light_weight_timer() { + logging_error!(Type::Lightweight, setup_light_weight_timer().await); } -fn setup_window_close_listener() -> u32 { +fn setup_window_close_listener() { if let Some(window) = handle::Handle::global().get_window() { let handler = window.listen("tauri://close-requested", move |_event| { - let _ = setup_light_weight_timer(); + std::mem::drop(AsyncHandler::spawn(|| async { + if let Err(e) = setup_light_weight_timer().await { + log::warn!("Failed to setup light weight timer: {e}"); + } + })); logging!( info, Type::Lightweight, @@ -197,12 +233,22 @@ fn setup_window_close_listener() -> u32 { "监听到关闭请求,开始轻量模式计时" ); }); - return handler; + + WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release); } - 0 } -fn setup_webview_focus_listener() -> u32 { +fn cancel_window_close_listener() { + if let Some(window) = handle::Handle::global().get_window() { + let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel); + if handler != 0 { + window.unlisten(handler); + logging!(info, Type::Lightweight, true, "取消了窗口关闭监听"); + } + } +} + +fn setup_webview_focus_listener() { if let Some(window) = handle::Handle::global().get_window() { let handler = window.listen("tauri://focus", move |_event| { log_err!(cancel_light_weight_timer()); @@ -212,31 +258,34 @@ fn setup_webview_focus_listener() -> u32 { "监听到窗口获得焦点,取消轻量模式计时" ); }); - return handler; + + WEBVIEW_FOCUS_HANDLER.store(handler, Ordering::Release); } - 0 } -fn cancel_window_close_listener() { +fn cancel_webview_focus_listener() { if let Some(window) = handle::Handle::global().get_window() { - window.unlisten(setup_window_close_listener()); - logging!(info, Type::Lightweight, true, "取消了窗口关闭监听"); + let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel); + if handler != 0 { + window.unlisten(handler); + logging!(info, Type::Lightweight, true, "取消了窗口焦点监听"); + } } } -fn setup_light_weight_timer() -> Result<()> { - Timer::global().init()?; +async fn setup_light_weight_timer() -> Result<()> { + Timer::global().init().await?; let once_by_minutes = Config::verge() + .await .latest_ref() .auto_light_weight_minutes .unwrap_or(10); // 获取task_id let task_id = { - let mut timer_count = Timer::global().timer_count.lock(); - let id = *timer_count; - *timer_count += 1; - id + Timer::global() + .timer_count + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) }; // 创建任务 @@ -246,7 +295,7 @@ fn setup_light_weight_timer() -> Result<()> { .set_frequency_once_by_minutes(once_by_minutes) .spawn_async_routine(move || async move { logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式"); - entry_lightweight_mode(); + entry_lightweight_mode().await; }) .context("failed to create timer task")?; diff --git a/clash-verge-rev/src-tauri/src/module/sysinfo.rs b/clash-verge-rev/src-tauri/src/module/sysinfo.rs index 0d594fb809..fc348f49af 100644 --- a/clash-verge-rev/src-tauri/src/module/sysinfo.rs +++ b/clash-verge-rev/src-tauri/src/module/sysinfo.rs @@ -1,6 +1,6 @@ use crate::{ cmd::system, - core::{handle, CoreManager}, + core::{CoreManager, handle}, }; use std::fmt::{self, Debug, Formatter}; use sysinfo::System; @@ -20,7 +20,13 @@ impl Debug for PlatformSpecification { write!( f, "System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}", - self.system_name, self.system_version, self.system_kernel_version, self.system_arch, self.verge_version, self.running_mode, self.is_admin + self.system_name, + self.system_version, + self.system_kernel_version, + self.system_arch, + self.verge_version, + self.running_mode, + self.is_admin ) } } @@ -32,7 +38,17 @@ impl PlatformSpecification { let system_kernel_version = System::kernel_version().unwrap_or("Null".into()); let system_arch = System::cpu_arch(); - let handler = handle::Handle::global().app_handle().unwrap(); + let Some(handler) = handle::Handle::global().app_handle() else { + return Self { + system_name, + system_version, + system_kernel_version, + system_arch, + verge_version: "unknown".into(), + running_mode: "NotRunning".to_string(), + is_admin: false, + }; + }; let verge_version = handler.package_info().version.to_string(); // 使用默认值避免在同步上下文中执行异步操作 @@ -52,10 +68,10 @@ impl PlatformSpecification { } // 异步方法来获取完整的系统信息 - pub async fn new_async() -> Self { + pub fn new_sync() -> Self { let mut info = Self::new(); - let running_mode = CoreManager::global().get_running_mode().await; + let running_mode = CoreManager::global().get_running_mode(); info.running_mode = running_mode.to_string(); info diff --git a/clash-verge-rev/src-tauri/src/process/async_handler.rs b/clash-verge-rev/src-tauri/src/process/async_handler.rs index c5a809d4e5..1b796485ef 100644 --- a/clash-verge-rev/src-tauri/src/process/async_handler.rs +++ b/clash-verge-rev/src-tauri/src/process/async_handler.rs @@ -1,14 +1,84 @@ +#[cfg(feature = "tokio-trace")] +use std::any::type_name; use std::future::Future; +#[cfg(feature = "tokio-trace")] +use std::panic::Location; use tauri::{async_runtime, async_runtime::JoinHandle}; pub struct AsyncHandler; impl AsyncHandler { + pub fn handle() -> async_runtime::RuntimeHandle { + async_runtime::handle() + } + + #[track_caller] pub fn spawn(f: F) -> JoinHandle<()> where F: FnOnce() -> Fut + Send + 'static, Fut: Future + Send + 'static, { + #[cfg(feature = "tokio-trace")] + Self::log_task_info(&f); async_runtime::spawn(f()) } + + #[track_caller] + pub fn spawn_blocking(f: F) -> JoinHandle + where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, + { + #[cfg(feature = "tokio-trace")] + Self::log_task_info(&f); + async_runtime::spawn_blocking(f) + } + + #[allow(dead_code)] + #[track_caller] + pub fn block_on(fut: Fut) -> Fut::Output + where + Fut: Future + Send + 'static, + { + #[cfg(feature = "tokio-trace")] + Self::log_task_info(&fut); + async_runtime::block_on(fut) + } + + #[cfg(feature = "tokio-trace")] + #[track_caller] + fn log_task_info(f: &F) + where + F: ?Sized, + { + const TRACE_SPECIAL_SIZE: [usize; 3] = [0, 4, 24]; + let size = std::mem::size_of_val(f); + if TRACE_SPECIAL_SIZE.contains(&size) { + return; + } + + let location = Location::caller(); + let type_str = type_name::(); + let size_str = format!("{} bytes", size); + let loc_str = format!( + "{}:{}:{}", + location.file(), + location.line(), + location.column() + ); + + println!( + "┌────────────────────┬─────────────────────────────────────────────────────────────────────────────┐" + ); + println!("│ {:<18} │ {:<80} │", "Field", "Value"); + println!( + "├────────────────────┼─────────────────────────────────────────────────────────────────────────────┤" + ); + println!("│ {:<18} │ {:<80} │", "Type of task", type_str); + println!("│ {:<18} │ {:<80} │", "Size of task", size_str); + println!("│ {:<18} │ {:<80} │", "Called from", loc_str); + println!( + "└────────────────────┴─────────────────────────────────────────────────────────────────────────────┘" + ); + } } diff --git a/clash-verge-rev/src-tauri/src/state/lightweight.rs b/clash-verge-rev/src-tauri/src/state/lightweight.rs deleted file mode 100644 index da9609848d..0000000000 --- a/clash-verge-rev/src-tauri/src/state/lightweight.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::sync::{Arc, Once, OnceLock}; - -use crate::{logging, utils::logging::Type}; - -#[derive(Clone)] -pub struct LightWeightState { - #[allow(unused)] - once: Arc, - pub is_lightweight: bool, -} - -impl LightWeightState { - pub fn new() -> Self { - Self { - once: Arc::new(Once::new()), - is_lightweight: false, - } - } - - #[allow(unused)] - pub fn run_once_time(&self, f: F) - where - F: FnOnce() + Send + 'static, - { - self.once.call_once(f); - } - - pub fn set_lightweight_mode(&mut self, value: bool) -> &Self { - self.is_lightweight = value; - if value { - logging!(info, Type::Lightweight, true, "轻量模式已开启"); - } else { - logging!(info, Type::Lightweight, true, "轻量模式已关闭"); - } - self - } -} - -impl Default for LightWeightState { - fn default() -> Self { - static INSTANCE: OnceLock = OnceLock::new(); - INSTANCE.get_or_init(LightWeightState::new).clone() - } -} diff --git a/clash-verge-rev/src-tauri/src/state/mod.rs b/clash-verge-rev/src-tauri/src/state/mod.rs deleted file mode 100644 index d8acadefe9..0000000000 --- a/clash-verge-rev/src-tauri/src/state/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Tauri Manager 会进行 Arc 管理,无需额外 Arc -// https://tauri.app/develop/state-management/#do-you-need-arc - -pub mod lightweight; -pub mod proxy; diff --git a/clash-verge-rev/src-tauri/src/state/proxy.rs b/clash-verge-rev/src-tauri/src/state/proxy.rs deleted file mode 100644 index 56733fda87..0000000000 --- a/clash-verge-rev/src-tauri/src/state/proxy.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::time::{Duration, Instant}; -pub struct CacheEntry { - pub value: Arc, - pub expires_at: Instant, -} -use dashmap::DashMap; -use serde_json::Value; -use std::sync::Arc; -use tokio::sync::OnceCell; - -pub struct ProxyRequestCache { - pub map: DashMap>>, -} - -impl ProxyRequestCache { - pub fn global() -> &'static Self { - static INSTANCE: once_cell::sync::OnceCell = - once_cell::sync::OnceCell::new(); - INSTANCE.get_or_init(|| ProxyRequestCache { - map: DashMap::new(), - }) - } - - pub fn make_key(prefix: &str, id: &str) -> String { - format!("{prefix}:{id}") - } - - pub async fn get_or_fetch(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc - where - F: Fn() -> Fut, - Fut: std::future::Future, - { - let now = Instant::now(); - let key_cloned = key.clone(); - let cell = self - .map - .entry(key) - .or_insert_with(|| Arc::new(OnceCell::new())) - .clone(); - - if let Some(entry) = cell.get() { - if entry.expires_at > now { - return Arc::clone(&entry.value); - } - } - - if let Some(entry) = cell.get() { - if entry.expires_at <= now { - self.map - .remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell)); - let new_cell = Arc::new(OnceCell::new()); - self.map.insert(key_cloned.clone(), new_cell.clone()); - return Box::pin(self.get_or_fetch(key_cloned, ttl, fetch_fn)).await; - } - } - - let value = fetch_fn().await; - let entry = CacheEntry { - value: Arc::new(value), - expires_at: Instant::now() + ttl, - }; - let _ = cell.set(entry); - Arc::clone(&cell.get().unwrap().value) - } -} diff --git a/clash-verge-rev/src-tauri/src/utils/autostart.rs b/clash-verge-rev/src-tauri/src/utils/autostart.rs index f13009727d..9ce10abcc6 100644 --- a/clash-verge-rev/src-tauri/src/utils/autostart.rs +++ b/clash-verge-rev/src-tauri/src/utils/autostart.rs @@ -1,5 +1,5 @@ #[cfg(target_os = "windows")] -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; #[cfg(target_os = "windows")] use log::info; diff --git a/clash-verge-rev/src-tauri/src/utils/dirs.rs b/clash-verge-rev/src-tauri/src/utils/dirs.rs index 8f58936acd..9b6c681ed5 100644 --- a/clash-verge-rev/src-tauri/src/utils/dirs.rs +++ b/clash-verge-rev/src-tauri/src/utils/dirs.rs @@ -151,20 +151,24 @@ pub fn find_target_icons(target: &str) -> Result> { let entry = entry?; let path = entry.path(); - if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { - if file_name.starts_with(target) - && (file_name.ends_with(".ico") || file_name.ends_with(".png")) - { - matching_files.push(path); - } + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) + && file_name.starts_with(target) + && (file_name.ends_with(".ico") || file_name.ends_with(".png")) + { + matching_files.push(path); } } if matching_files.is_empty() { Ok(None) } else { - let first = path_to_str(matching_files.first().unwrap())?; - Ok(Some(first.to_string())) + match matching_files.first() { + Some(first_path) => { + let first = path_to_str(first_path)?; + Ok(Some(first.to_string())) + } + None => Ok(None), + } } } diff --git a/clash-verge-rev/src-tauri/src/utils/format.rs b/clash-verge-rev/src-tauri/src/utils/format.rs new file mode 100644 index 0000000000..270206c3bc --- /dev/null +++ b/clash-verge-rev/src-tauri/src/utils/format.rs @@ -0,0 +1,25 @@ +/// Format bytes into human readable string (B, KB, MB, GB) +pub fn fmt_bytes(bytes: u64) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB"]; + let (mut val, mut unit) = (bytes as f64, 0); + while val >= 1024.0 && unit < 3 { + val /= 1024.0; + unit += 1; + } + format!("{:.1}{}", val, UNITS[unit]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fmt_bytes() { + assert_eq!(fmt_bytes(0), "0.0B"); + assert_eq!(fmt_bytes(512), "512.0B"); + assert_eq!(fmt_bytes(1024), "1.0KB"); + assert_eq!(fmt_bytes(1536), "1.5KB"); + assert_eq!(fmt_bytes(1024 * 1024), "1.0MB"); + assert_eq!(fmt_bytes(1024 * 1024 * 1024), "1.0GB"); + } +} diff --git a/clash-verge-rev/src-tauri/src/utils/help.rs b/clash-verge-rev/src-tauri/src/utils/help.rs index 4c30b0f1a8..4991bdab3a 100644 --- a/clash-verge-rev/src-tauri/src/utils/help.rs +++ b/clash-verge-rev/src-tauri/src/utils/help.rs @@ -1,38 +1,33 @@ use crate::{enhance::seq::SeqMap, logging, utils::logging::Type}; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{Context, Result, anyhow, bail}; use nanoid::nanoid; -use serde::{de::DeserializeOwned, Serialize}; -use serde_yaml::Mapping; -use std::{fs, path::PathBuf, str::FromStr}; +use serde::{Serialize, de::DeserializeOwned}; +use serde_yaml_ng::Mapping; +use std::{path::PathBuf, str::FromStr}; /// read data from yaml as struct T -pub fn read_yaml(path: &PathBuf) -> Result { - if !path.exists() { +pub async fn read_yaml(path: &PathBuf) -> Result { + if !tokio::fs::try_exists(path).await.unwrap_or(false) { bail!("file not found \"{}\"", path.display()); } - let yaml_str = fs::read_to_string(path) - .with_context(|| format!("failed to read the file \"{}\"", path.display()))?; + let yaml_str = tokio::fs::read_to_string(path).await?; - serde_yaml::from_str::(&yaml_str).with_context(|| { - format!( - "failed to read the file with yaml format \"{}\"", - path.display() - ) - }) + Ok(serde_yaml_ng::from_str::(&yaml_str)?) } /// read mapping from yaml -pub fn read_mapping(path: &PathBuf) -> Result { - if !path.exists() { +pub async fn read_mapping(path: &PathBuf) -> Result { + if !tokio::fs::try_exists(path).await.unwrap_or(false) { bail!("file not found \"{}\"", path.display()); } - let yaml_str = fs::read_to_string(path) + let yaml_str = tokio::fs::read_to_string(path) + .await .with_context(|| format!("failed to read the file \"{}\"", path.display()))?; // YAML语法检查 - match serde_yaml::from_str::(&yaml_str) { + match serde_yaml_ng::from_str::(&yaml_str) { Ok(mut val) => { val.apply_merge() .with_context(|| format!("failed to apply merge \"{}\"", path.display()))?; @@ -60,16 +55,18 @@ pub fn read_mapping(path: &PathBuf) -> Result { } /// read mapping from yaml fix #165 -pub fn read_seq_map(path: &PathBuf) -> Result { - let val: SeqMap = read_yaml(path)?; - - Ok(val) +pub async fn read_seq_map(path: &PathBuf) -> Result { + read_yaml(path).await } /// save the data to the file /// can set `prefix` string to add some comments -pub fn save_yaml(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> { - let data_str = serde_yaml::to_string(data)?; +pub async fn save_yaml( + path: &PathBuf, + data: &T, + prefix: Option<&str>, +) -> Result<()> { + let data_str = serde_yaml_ng::to_string(data)?; let yaml_str = match prefix { Some(prefix) => format!("{prefix}\n\n{data_str}"), @@ -77,7 +74,8 @@ pub fn save_yaml(path: &PathBuf, data: &T, prefix: Option<&str>) - }; let path_str = path.as_os_str().to_string_lossy().to_string(); - fs::write(path, yaml_str.as_bytes()) + tokio::fs::write(path, yaml_str.as_bytes()) + .await .with_context(|| format!("failed to save file \"{path_str}\"")) } @@ -120,7 +118,7 @@ pub fn get_last_part_and_decode(url: &str) -> Option { } /// open file -pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> { +pub fn open_file(path: PathBuf) -> Result<()> { open::that_detached(path.as_os_str())?; Ok(()) } @@ -156,10 +154,6 @@ macro_rules! ret_err { #[macro_export] macro_rules! t { ($en:expr, $zh:expr, $use_zh:expr) => { - if $use_zh { - $zh - } else { - $en - } + if $use_zh { $zh } else { $en } }; } diff --git a/clash-verge-rev/src-tauri/src/utils/i18n.rs b/clash-verge-rev/src-tauri/src/utils/i18n.rs index d6b6f0bc12..9f80c56330 100644 --- a/clash-verge-rev/src-tauri/src/utils/i18n.rs +++ b/clash-verge-rev/src-tauri/src/utils/i18n.rs @@ -1,7 +1,7 @@ use crate::{config::Config, utils::dirs}; use once_cell::sync::Lazy; use serde_json::Value; -use std::{collections::HashMap, fs, path::PathBuf}; +use std::{fs, path::PathBuf, sync::RwLock}; use sys_locale; const DEFAULT_LANGUAGE: &str = "zh"; @@ -15,14 +15,14 @@ fn get_locales_dir() -> Option { pub fn get_supported_languages() -> Vec { let mut languages = Vec::new(); - if let Some(locales_dir) = get_locales_dir() { - if let Ok(entries) = fs::read_dir(locales_dir) { - for entry in entries.flatten() { - if let Some(file_name) = entry.file_name().to_str() { - if let Some(lang) = file_name.strip_suffix(".json") { - languages.push(lang.to_string()); - } - } + if let Some(locales_dir) = get_locales_dir() + && let Ok(entries) = fs::read_dir(locales_dir) + { + for entry in entries.flatten() { + if let Some(file_name) = entry.file_name().to_str() + && let Some(lang) = file_name.strip_suffix(".json") + { + languages.push(lang.to_string()); } } } @@ -33,22 +33,20 @@ pub fn get_supported_languages() -> Vec { languages } -static TRANSLATIONS: Lazy> = Lazy::new(|| { - let mut translations = HashMap::new(); - - if let Some(locales_dir) = get_locales_dir() { - for lang in get_supported_languages() { - let file_path = locales_dir.join(format!("{lang}.json")); - if let Ok(content) = fs::read_to_string(file_path) { - if let Ok(json) = serde_json::from_str(&content) { - translations.insert(lang.to_string(), json); - } - } - } - } - translations +static TRANSLATIONS: Lazy> = Lazy::new(|| { + let lang = get_system_language(); + let json = load_lang_file(&lang).unwrap_or_else(|| Value::Object(Default::default())); + RwLock::new((lang, json)) }); +fn load_lang_file(lang: &str) -> Option { + let locales_dir = get_locales_dir()?; + let file_path = locales_dir.join(format!("{lang}.json")); + fs::read_to_string(file_path) + .ok() + .and_then(|content| serde_json::from_str(&content).ok()) +} + fn get_system_language() -> String { sys_locale::get_locale() .map(|locale| locale.to_lowercase()) @@ -57,28 +55,41 @@ fn get_system_language() -> String { .unwrap_or_else(|| DEFAULT_LANGUAGE.to_string()) } -pub fn t(key: &str) -> String { +pub async fn t(key: &str) -> String { let current_lang = Config::verge() + .await .latest_ref() .language .as_deref() .map(String::from) .unwrap_or_else(get_system_language); - if let Some(text) = TRANSLATIONS - .get(¤t_lang) - .and_then(|trans| trans.get(key)) - .and_then(|val| val.as_str()) { - return text.to_string(); + if let Ok(cache) = TRANSLATIONS.read() + && cache.0 == current_lang + && let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) + { + return text.to_string(); + } } - if current_lang != DEFAULT_LANGUAGE { - if let Some(text) = TRANSLATIONS - .get(DEFAULT_LANGUAGE) - .and_then(|trans| trans.get(key)) - .and_then(|val| val.as_str()) - { + if let Some(new_json) = load_lang_file(¤t_lang) + && let Ok(mut cache) = TRANSLATIONS.write() + { + *cache = (current_lang.clone(), new_json); + + if let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) { + return text.to_string(); + } + } + + if current_lang != DEFAULT_LANGUAGE + && let Some(default_json) = load_lang_file(DEFAULT_LANGUAGE) + && let Ok(mut cache) = TRANSLATIONS.write() + { + *cache = (DEFAULT_LANGUAGE.to_string(), default_json); + + if let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) { return text.to_string(); } } diff --git a/clash-verge-rev/src-tauri/src/utils/init.rs b/clash-verge-rev/src-tauri/src/utils/init.rs index cfa39da0d5..325fbbb7b3 100644 --- a/clash-verge-rev/src-tauri/src/utils/init.rs +++ b/clash-verge-rev/src-tauri/src/utils/init.rs @@ -1,91 +1,94 @@ +cfg_if::cfg_if! { + if #[cfg(not(feature = "tauri-dev"))] { + use crate::utils::logging::{console_colored_format, file_format, NoExternModule}; + use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, LogSpecification, Logger}; + } +} + use crate::{ config::*, core::handle, - utils::{dirs, help}, + logging, + process::AsyncHandler, + utils::{dirs, help, logging::Type}, }; use anyhow::Result; use chrono::{Local, TimeZone}; -use log::LevelFilter; -use log4rs::{ - append::{console::ConsoleAppender, file::FileAppender}, - config::{Appender, Logger, Root}, - encode::pattern::PatternEncoder, -}; -use std::{ - fs::{self, DirEntry}, - path::PathBuf, - str::FromStr, -}; +use std::{path::PathBuf, str::FromStr}; use tauri_plugin_shell::ShellExt; +use tokio::fs; +use tokio::fs::DirEntry; /// initialize this instance's log file -fn init_log() -> Result<()> { - let log_dir = dirs::app_logs_dir()?; - if !log_dir.exists() { - let _ = fs::create_dir_all(&log_dir); - } - - let log_level = Config::verge().latest_ref().get_log_level(); - if log_level == LevelFilter::Off { - return Ok(()); - } - - let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string(); - let log_file = format!("{local_time}.log"); - let log_file = log_dir.join(log_file); - - let log_pattern = match log_level { - LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}", - _ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}", +#[cfg(not(feature = "tauri-dev"))] +pub async fn init_logger() -> Result<()> { + // TODO 提供 runtime 级别实时修改 + let (log_level, log_max_size, log_max_count) = { + let verge_guard = Config::verge().await; + let verge = verge_guard.latest_ref(); + ( + verge.get_log_level(), + verge.app_log_max_size.unwrap_or(128), + verge.app_log_max_count.unwrap_or(8), + ) }; - let encode = Box::new(PatternEncoder::new(log_pattern)); + let log_dir = dirs::app_logs_dir()?; + let logger = Logger::with(LogSpecification::from(log_level)) + .log_to_file(FileSpec::default().directory(log_dir).basename("")) + .duplicate_to_stdout(Duplicate::Debug) + .format(console_colored_format) + .format_for_files(file_format) + .rotate( + Criterion::Size(log_max_size * 1024), + flexi_logger::Naming::TimestampsCustomFormat { + current_infix: Some("latest"), + format: "%Y-%m-%d_%H-%M-%S", + }, + Cleanup::KeepLogFiles(log_max_count), + ) + .filter(Box::new(NoExternModule)); - let stdout = ConsoleAppender::builder().encoder(encode.clone()).build(); - let tofile = FileAppender::builder().encoder(encode).build(log_file)?; + let _handle = logger.start()?; - let mut logger_builder = Logger::builder(); - let mut root_builder = Root::builder(); - - let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug; - - logger_builder = logger_builder.appenders(["file"]); - if log_more { - root_builder = root_builder.appenders(["file"]); - } - - let (config, _) = log4rs::config::Config::builder() - .appender(Appender::builder().build("stdout", Box::new(stdout))) - .appender(Appender::builder().build("file", Box::new(tofile))) - .logger(logger_builder.additive(false).build("app", log_level)) - .build_lossy(root_builder.build(log_level)); - - log4rs::init_config(config)?; + // TODO 全局 logger handle 控制 + // GlobalLoggerProxy::global().set_inner(handle); + // TODO 提供前端设置等级,热更新等级 + // logger.parse_new_spec(spec) Ok(()) } +// TODO flexi_logger 提供了最大保留天数,或许我们应该用内置删除log文件 /// 删除log文件 -pub fn delete_log() -> Result<()> { +pub async fn delete_log() -> Result<()> { let log_dir = dirs::app_logs_dir()?; if !log_dir.exists() { return Ok(()); } let auto_log_clean = { - let verge = Config::verge(); + let verge = Config::verge().await; let verge = verge.latest_ref(); verge.auto_log_clean.unwrap_or(0) }; + // 1: 1天, 2: 7天, 3: 30天, 4: 90天 let day = match auto_log_clean { - 1 => 7, - 2 => 30, - 3 => 90, + 1 => 1, + 2 => 7, + 3 => 30, + 4 => 90, _ => return Ok(()), }; - log::debug!(target: "app", "try to delete log files, day: {day}"); + logging!( + info, + Type::Setup, + true, + "try to delete log files, day: {}", + day + ); // %Y-%m-%d to NaiveDateTime let parse_time_str = |s: &str| { @@ -104,7 +107,7 @@ pub fn delete_log() -> Result<()> { Ok(time) }; - let process_file = |file: DirEntry| -> Result<()> { + let process_file = async move |file: DirEntry| -> Result<()> { let file_name = file.file_name(); let file_name = file_name.to_str().unwrap_or_default(); @@ -119,31 +122,33 @@ pub fn delete_log() -> Result<()> { let duration = now.signed_duration_since(file_time); if duration.num_days() > day { let file_path = file.path(); - let _ = fs::remove_file(file_path); - log::info!(target: "app", "delete log file: {file_name}"); + let _ = fs::remove_file(file_path).await; + logging!(info, Type::Setup, true, "delete log file: {}", file_name); } } Ok(()) }; - for file in fs::read_dir(&log_dir)?.flatten() { - let _ = process_file(file); + let mut log_read_dir = fs::read_dir(&log_dir).await?; + while let Some(entry) = log_read_dir.next_entry().await? { + std::mem::drop(process_file(entry).await); } let service_log_dir = log_dir.join("service"); - for file in fs::read_dir(service_log_dir)?.flatten() { - let _ = process_file(file); + let mut service_log_read_dir = fs::read_dir(service_log_dir).await?; + while let Some(entry) = service_log_read_dir.next_entry().await? { + std::mem::drop(process_file(entry).await); } Ok(()) } /// 初始化DNS配置文件 -fn init_dns_config() -> Result<()> { - use serde_yaml::Value; +async fn init_dns_config() -> Result<()> { + use serde_yaml_ng::Value; // 创建DNS子配置 - let dns_config = serde_yaml::Mapping::from_iter([ + let dns_config = serde_yaml_ng::Mapping::from_iter([ ("enable".into(), Value::Bool(true)), ("listen".into(), Value::String(":53".into())), ("enhanced-mode".into(), Value::String("fake-ip".into())), @@ -195,7 +200,7 @@ fn init_dns_config() -> Result<()> { ("fallback".into(), Value::Sequence(vec![])), ( "nameserver-policy".into(), - Value::Mapping(serde_yaml::Mapping::new()), + Value::Mapping(serde_yaml_ng::Mapping::new()), ), ( "proxy-server-nameserver".into(), @@ -209,7 +214,7 @@ fn init_dns_config() -> Result<()> { ("direct-nameserver-follow-policy".into(), Value::Bool(false)), ( "fallback-filter".into(), - Value::Mapping(serde_yaml::Mapping::from_iter([ + Value::Mapping(serde_yaml_ng::Mapping::from_iter([ ("geoip".into(), Value::Bool(true)), ("geoip-code".into(), Value::String("CN".into())), ( @@ -232,9 +237,12 @@ fn init_dns_config() -> Result<()> { ]); // 获取默认DNS和host配置 - let default_dns_config = serde_yaml::Mapping::from_iter([ + let default_dns_config = serde_yaml_ng::Mapping::from_iter([ ("dns".into(), Value::Mapping(dns_config)), - ("hosts".into(), Value::Mapping(serde_yaml::Mapping::new())), + ( + "hosts".into(), + Value::Mapping(serde_yaml_ng::Mapping::new()), + ), ]); // 检查DNS配置文件是否存在 @@ -242,77 +250,149 @@ fn init_dns_config() -> Result<()> { let dns_path = app_dir.join("dns_config.yaml"); if !dns_path.exists() { - log::info!(target: "app", "Creating default DNS config file"); + logging!(info, Type::Setup, true, "Creating default DNS config file"); help::save_yaml( &dns_path, &default_dns_config, Some("# Clash Verge DNS Config"), - )?; + ) + .await?; } Ok(()) } +/// 确保目录结构存在 +async fn ensure_directories() -> Result<()> { + let directories = [ + ("app_home", dirs::app_home_dir()?), + ("app_profiles", dirs::app_profiles_dir()?), + ("app_logs", dirs::app_logs_dir()?), + ]; + + for (name, dir) in directories { + if !dir.exists() { + fs::create_dir_all(&dir).await.map_err(|e| { + anyhow::anyhow!("Failed to create {} directory {:?}: {}", name, dir, e) + })?; + logging!( + info, + Type::Setup, + true, + "Created {} directory: {:?}", + name, + dir + ); + } + } + + Ok(()) +} + +/// 初始化配置文件 +async fn initialize_config_files() -> Result<()> { + if let Ok(path) = dirs::clash_path() + && !path.exists() + { + let template = IClashTemp::template().0; + help::save_yaml(&path, &template, Some("# Clash Verge")) + .await + .map_err(|e| anyhow::anyhow!("Failed to create clash config: {}", e))?; + logging!( + info, + Type::Setup, + true, + "Created clash config at {:?}", + path + ); + } + + if let Ok(path) = dirs::verge_path() + && !path.exists() + { + let template = IVerge::template(); + help::save_yaml(&path, &template, Some("# Clash Verge")) + .await + .map_err(|e| anyhow::anyhow!("Failed to create verge config: {}", e))?; + logging!( + info, + Type::Setup, + true, + "Created verge config at {:?}", + path + ); + } + + if let Ok(path) = dirs::profiles_path() + && !path.exists() + { + let template = IProfiles::template(); + help::save_yaml(&path, &template, Some("# Clash Verge")) + .await + .map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?; + logging!( + info, + Type::Setup, + true, + "Created profiles config at {:?}", + path + ); + } + + // 验证并修正verge配置 + IVerge::validate_and_fix_config() + .await + .map_err(|e| anyhow::anyhow!("Failed to validate verge config: {}", e))?; + + Ok(()) +} + /// Initialize all the config files /// before tauri setup -pub fn init_config() -> Result<()> { - let _ = dirs::init_portable_flag(); - let _ = init_log(); - let _ = delete_log(); +pub async fn init_config() -> Result<()> { + // We do not need init_portable_flag here anymore due to lib.rs will to the things + // let _ = dirs::init_portable_flag(); - crate::log_err!(dirs::app_home_dir().map(|app_dir| { - if !app_dir.exists() { - let _ = fs::create_dir_all(&app_dir); + // We do not need init_log here anymore due to resolve will to the things + // if let Err(e) = init_log().await { + // eprintln!("Failed to initialize logging: {}", e); + // } + + ensure_directories().await?; + + initialize_config_files().await?; + + AsyncHandler::spawn(|| async { + if let Err(e) = delete_log().await { + logging!(warn, Type::Setup, true, "Failed to clean old logs: {}", e); } - })); + logging!(info, Type::Setup, true, "后台日志清理任务完成"); + }); - crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| { - if !profiles_dir.exists() { - let _ = fs::create_dir_all(&profiles_dir); - } - })); - - crate::log_err!(dirs::clash_path().map(|path| { - if !path.exists() { - help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Vergeasu"))?; - } - >::Ok(()) - })); - - crate::log_err!(dirs::verge_path().map(|path| { - if !path.exists() { - help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?; - } - >::Ok(()) - })); - - // 验证并修正verge.yaml中的clash_core配置 - crate::log_err!(IVerge::validate_and_fix_config()); - - crate::log_err!(dirs::profiles_path().map(|path| { - if !path.exists() { - help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?; - } - >::Ok(()) - })); - - // 初始化DNS配置文件 - let _ = init_dns_config(); + if let Err(e) = init_dns_config().await { + logging!( + warn, + Type::Setup, + true, + "DNS config initialization failed: {}", + e + ); + } Ok(()) } /// initialize app resources /// after tauri setup -pub fn init_resources() -> Result<()> { +pub async fn init_resources() -> Result<()> { let app_dir = dirs::app_home_dir()?; let res_dir = dirs::app_resources_dir()?; if !app_dir.exists() { - let _ = fs::create_dir_all(&app_dir); + std::mem::drop(fs::create_dir_all(&app_dir).await); } if !res_dir.exists() { - let _ = fs::create_dir_all(&res_dir); + std::mem::drop(fs::create_dir_all(&res_dir).await); } let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"]; @@ -322,36 +402,49 @@ pub fn init_resources() -> Result<()> { for file in file_list.iter() { let src_path = res_dir.join(file); let dest_path = app_dir.join(file); - log::debug!(target: "app", "src_path: {src_path:?}, dest_path: {dest_path:?}"); - let handle_copy = |dest: &PathBuf| { - match fs::copy(&src_path, dest) { - Ok(_) => log::debug!(target: "app", "resources copied '{file}'"), + let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move { + match fs::copy(&src, &dest).await { + Ok(_) => { + logging!(debug, Type::Setup, true, "resources copied '{}'", file); + } Err(err) => { - log::error!(target: "app", "failed to copy resources '{file}' to '{dest:?}', {err}") + logging!( + error, + Type::Setup, + true, + "failed to copy resources '{}' to '{:?}', {}", + file, + dest, + err + ); } }; }; if src_path.exists() && !dest_path.exists() { - handle_copy(&dest_path); + handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await; continue; } - let src_modified = fs::metadata(&src_path).and_then(|m| m.modified()); - let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified()); + let src_modified = fs::metadata(&src_path).await.and_then(|m| m.modified()); + let dest_modified = fs::metadata(&dest_path).await.and_then(|m| m.modified()); match (src_modified, dest_modified) { (Ok(src_modified), Ok(dest_modified)) => { if src_modified > dest_modified { - handle_copy(&dest_path); - } else { - log::debug!(target: "app", "skipping resource copy '{file}'"); + handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await; } } _ => { - log::debug!(target: "app", "failed to get modified '{file}'"); - handle_copy(&dest_path); + logging!( + debug, + Type::Setup, + true, + "failed to get modified '{}'", + file + ); + handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await; } }; } @@ -363,7 +456,7 @@ pub fn init_resources() -> Result<()> { #[cfg(target_os = "windows")] pub fn init_scheme() -> Result<()> { use tauri::utils::platform::current_exe; - use winreg::{enums::*, RegKey}; + use winreg::{RegKey, enums::*}; let app_exe = current_exe()?; let app_exe = dunce::canonicalize(app_exe)?; @@ -401,10 +494,17 @@ pub fn init_scheme() -> Result<()> { } pub async fn startup_script() -> Result<()> { - let app_handle = handle::Handle::global().app_handle().unwrap(); + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + return Err(anyhow::anyhow!( + "app_handle not available for startup script execution" + )); + } + }; let script_path = { - let verge = Config::verge(); + let verge = Config::verge().await; let verge = verge.latest_ref(); verge.startup_script.clone().unwrap_or("".to_string()) }; diff --git a/clash-verge-rev/src-tauri/src/utils/logging.rs b/clash-verge-rev/src-tauri/src/utils/logging.rs index 3aa7866e02..1390ca0e77 100644 --- a/clash-verge-rev/src-tauri/src/utils/logging.rs +++ b/clash-verge-rev/src-tauri/src/utils/logging.rs @@ -1,4 +1,15 @@ -use std::fmt; +cfg_if::cfg_if! { + if #[cfg(feature = "tauri-dev")] { + use std::fmt; + } else { + #[cfg(feature = "verge-dev")] + use nu_ansi_term::Color; + use std::{fmt, io::Write, thread}; + use flexi_logger::DeferredNow; + use log::{LevelFilter, Record}; + use flexi_logger::filter::LogLineFilter; + } +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Type { @@ -18,6 +29,8 @@ pub enum Type { Network, ProxyMode, Ipc, + // Cache, + ClashVergeRev, } impl fmt::Display for Type { @@ -39,6 +52,8 @@ impl fmt::Display for Type { Type::Network => write!(f, "[Network]"), Type::ProxyMode => write!(f, "[ProxMode]"), Type::Ipc => write!(f, "[IPC]"), + // Type::Cache => write!(f, "[Cache]"), + Type::ClashVergeRev => write!(f, "[ClashVergeRev]"), } } } @@ -78,22 +93,35 @@ macro_rules! trace_err { /// transform the error to String #[macro_export] macro_rules! wrap_err { - ($stat: expr) => { + // Case 1: Future> + ($stat:expr, async) => {{ + match $stat.await { + Ok(a) => Ok(a), + Err(err) => { + log::error!(target: "app", "{}", err); + Err(err.to_string()) + } + } + }}; + + // Case 2: Result + ($stat:expr) => {{ match $stat { Ok(a) => Ok(a), Err(err) => { - log::error!(target: "app", "{}", err.to_string()); - Err(format!("{}", err.to_string())) + log::error!(target: "app", "{}", err); + Err(err.to_string()) } } - }; + }}; } #[macro_export] macro_rules! logging { // 带 println 的版本(支持格式化参数) ($level:ident, $type:expr, true, $($arg:tt)*) => { - println!("{} {}", $type, format_args!($($arg)*)); + // We dont need println here anymore + // println!("{} {}", $type, format_args!($($arg)*)); log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*)); }; @@ -143,3 +171,77 @@ macro_rules! logging_error { logging_error!($type, false, $fmt $(, $arg)*); }; } + +#[cfg(not(feature = "tauri-dev"))] +static IGNORE_MODULES: &[&str] = &["tauri", "wry"]; +#[cfg(not(feature = "tauri-dev"))] +pub struct NoExternModule; +#[cfg(not(feature = "tauri-dev"))] +impl LogLineFilter for NoExternModule { + fn write( + &self, + now: &mut DeferredNow, + record: &Record, + log_line_writer: &dyn flexi_logger::filter::LogLineWriter, + ) -> std::io::Result<()> { + let module_path = record.module_path().unwrap_or_default(); + if IGNORE_MODULES.iter().any(|m| module_path.starts_with(m)) { + Ok(()) + } else { + log_line_writer.write(now, record) + } + } +} + +#[cfg(not(feature = "tauri-dev"))] +pub fn get_log_level(log_level: &LevelFilter) -> String { + #[cfg(feature = "verge-dev")] + match log_level { + LevelFilter::Off => Color::Fixed(8).paint("OFF").to_string(), + LevelFilter::Error => Color::Red.paint("ERROR").to_string(), + LevelFilter::Warn => Color::Yellow.paint("WARN ").to_string(), + LevelFilter::Info => Color::Green.paint("INFO ").to_string(), + LevelFilter::Debug => Color::Blue.paint("DEBUG").to_string(), + LevelFilter::Trace => Color::Purple.paint("TRACE").to_string(), + } + #[cfg(not(feature = "verge-dev"))] + log_level.to_string() +} + +#[cfg(not(feature = "tauri-dev"))] +pub fn console_colored_format( + w: &mut dyn Write, + now: &mut DeferredNow, + record: &log::Record, +) -> std::io::Result<()> { + let current_thread = thread::current(); + let thread_name = current_thread.name().unwrap_or("unnamed"); + + let level = get_log_level(&record.level().to_level_filter()); + let line = record.line().unwrap_or(0); + write!( + w, + "[{}] {} [{}:{}] T[{}] {}", + now.format("%H:%M:%S%.3f"), + level, + record.module_path().unwrap_or(""), + line, + thread_name, + record.args(), + ) +} + +#[cfg(not(feature = "tauri-dev"))] +pub fn file_format( + w: &mut dyn Write, + now: &mut DeferredNow, + record: &Record, +) -> std::io::Result<()> { + write!( + w, + "[{}] {} {}", + now.format("%Y-%m-%d %H:%M:%S%.3f"), + record.level(), + record.args(), + ) +} diff --git a/clash-verge-rev/src-tauri/src/utils/mod.rs b/clash-verge-rev/src-tauri/src/utils/mod.rs index 0cb644b40a..74b895bdb9 100644 --- a/clash-verge-rev/src-tauri/src/utils/mod.rs +++ b/clash-verge-rev/src-tauri/src/utils/mod.rs @@ -1,5 +1,6 @@ pub mod autostart; pub mod dirs; +pub mod format; pub mod help; pub mod i18n; pub mod init; @@ -8,5 +9,6 @@ pub mod network; pub mod notification; pub mod resolve; pub mod server; +pub mod singleton; pub mod tmpl; pub mod window_manager; diff --git a/clash-verge-rev/src-tauri/src/utils/network.rs b/clash-verge-rev/src-tauri/src/utils/network.rs index d72284d80f..1df28399a2 100644 --- a/clash-verge-rev/src-tauri/src/utils/network.rs +++ b/clash-verge-rev/src-tauri/src/utils/network.rs @@ -1,267 +1,189 @@ use anyhow::Result; -use lazy_static::lazy_static; -use parking_lot::Mutex; -use reqwest::{Client, ClientBuilder, Proxy, RequestBuilder, Response}; -use std::{ - sync::{Arc, Once}, - time::{Duration, Instant}, +use base64::{Engine as _, engine::general_purpose}; +use isahc::prelude::*; +use isahc::{HttpClient, config::SslOption}; +use isahc::{ + config::RedirectPolicy, + http::{ + StatusCode, Uri, + header::{HeaderMap, HeaderValue, USER_AGENT}, + }, }; -use tokio::runtime::{Builder, Runtime}; +use std::time::{Duration, Instant}; +use sysproxy::Sysproxy; +use tauri::Url; +use tokio::sync::Mutex; +use tokio::time::timeout; -use crate::{config::Config, logging, utils::logging::Type}; +use crate::config::Config; -// HTTP2 相关 -const H2_CONNECTION_WINDOW_SIZE: u32 = 1024 * 1024; -const H2_STREAM_WINDOW_SIZE: u32 = 1024 * 1024; -const H2_MAX_FRAME_SIZE: u32 = 16 * 1024; -const H2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(5); -const H2_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(5); -const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); -const POOL_MAX_IDLE_PER_HOST: usize = 5; -const POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(15); - -/// 网络管理器 -pub struct NetworkManager { - runtime: Arc, - self_proxy_client: Arc>>, - system_proxy_client: Arc>>, - no_proxy_client: Arc>>, - init: Once, - last_connection_error: Arc>>, - connection_error_count: Arc>, +#[derive(Debug)] +pub struct HttpResponse { + status: StatusCode, + headers: HeaderMap, + body: String, } -lazy_static! { - static ref NETWORK_MANAGER: NetworkManager = NetworkManager::new(); +impl HttpResponse { + pub fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self { + Self { + status, + headers, + body, + } + } + + pub fn status(&self) -> StatusCode { + self.status + } + + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + pub fn text_with_charset(&self) -> Result<&str> { + Ok(&self.body) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ProxyType { + None, + Localhost, + System, +} + +pub struct NetworkManager { + self_proxy_client: Mutex>, + system_proxy_client: Mutex>, + no_proxy_client: Mutex>, + last_connection_error: Mutex>, + connection_error_count: Mutex, } impl NetworkManager { - fn new() -> Self { - // 创建专用的异步运行时,线程数限制为4个 - let runtime = Builder::new_multi_thread() - .worker_threads(4) - .thread_name("clash-verge-network") - .enable_io() - .enable_time() - .build() - .expect("Failed to create network runtime"); - - NetworkManager { - runtime: Arc::new(runtime), - self_proxy_client: Arc::new(Mutex::new(None)), - system_proxy_client: Arc::new(Mutex::new(None)), - no_proxy_client: Arc::new(Mutex::new(None)), - init: Once::new(), - last_connection_error: Arc::new(Mutex::new(None)), - connection_error_count: Arc::new(Mutex::new(0)), + pub fn new() -> Self { + Self { + self_proxy_client: Mutex::new(None), + system_proxy_client: Mutex::new(None), + no_proxy_client: Mutex::new(None), + last_connection_error: Mutex::new(None), + connection_error_count: Mutex::new(0), } } - pub fn global() -> &'static Self { - &NETWORK_MANAGER - } - - /// 初始化网络客户端 - pub fn init(&self) { - self.init.call_once(|| { - self.runtime.spawn(async { - logging!(info, Type::Network, true, "初始化网络管理器"); - - // 创建无代理客户端 - let no_proxy_client = ClientBuilder::new() - .use_rustls_tls() - .no_proxy() - .pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST) - .pool_idle_timeout(POOL_IDLE_TIMEOUT) - .connect_timeout(Duration::from_secs(10)) - .timeout(Duration::from_secs(30)) - .build() - .expect("Failed to build no_proxy client"); - - let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock(); - *no_proxy_guard = Some(no_proxy_client); - - logging!(info, Type::Network, true, "网络管理器初始化完成"); - }); - }); - } - - fn record_connection_error(&self, error: &str) { - let mut last_error = self.last_connection_error.lock(); + async fn record_connection_error(&self, error: &str) { + let mut last_error = self.last_connection_error.lock().await; *last_error = Some((Instant::now(), error.to_string())); - let mut error_count = self.connection_error_count.lock(); - *error_count += 1; + let mut count = self.connection_error_count.lock().await; + *count += 1; } - fn should_reset_clients(&self) -> bool { - let error_count = *self.connection_error_count.lock(); - let last_error = self.last_connection_error.lock(); + async fn should_reset_clients(&self) -> bool { + let count = *self.connection_error_count.lock().await; + let last_error_guard = self.last_connection_error.lock().await; - if error_count > 5 { + if count > 5 { return true; } - if let Some((time, _)) = *last_error { - if time.elapsed() < Duration::from_secs(30) && error_count > 2 { - return true; - } + if let Some((time, _)) = &*last_error_guard + && time.elapsed() < Duration::from_secs(30) + && count > 2 + { + return true; } false } - pub fn reset_clients(&self) { - logging!(info, Type::Network, true, "正在重置所有HTTP客户端"); - { - let mut client = self.self_proxy_client.lock(); - *client = None; - } - { - let mut client = self.system_proxy_client.lock(); - *client = None; - } - { - let mut client = self.no_proxy_client.lock(); - *client = None; - } - { - let mut error_count = self.connection_error_count.lock(); - *error_count = 0; - } + pub async fn reset_clients(&self) { + *self.self_proxy_client.lock().await = None; + *self.system_proxy_client.lock().await = None; + *self.no_proxy_client.lock().await = None; + *self.connection_error_count.lock().await = 0; } - /// 创建带有自定义选项的HTTP请求 - pub fn create_request( + fn build_client( &self, - url: &str, - proxy_type: ProxyType, - timeout_secs: Option, - user_agent: Option, + proxy_uri: Option, + default_headers: HeaderMap, accept_invalid_certs: bool, - ) -> RequestBuilder { - if self.should_reset_clients() { - self.reset_clients(); - } + timeout_secs: Option, + ) -> Result { + let proxy_uri_clone = proxy_uri.clone(); + let headers_clone = default_headers.clone(); - let mut builder = ClientBuilder::new() - .use_rustls_tls() - .pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST) - .pool_idle_timeout(POOL_IDLE_TIMEOUT) - .connect_timeout(DEFAULT_CONNECT_TIMEOUT) - .http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE) - .http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE) - .http2_adaptive_window(true) - .http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL)) - .http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT) - .http2_max_frame_size(H2_MAX_FRAME_SIZE) - .tcp_keepalive(Some(Duration::from_secs(10))) - .http2_max_header_list_size(16 * 1024); + { + let mut builder = HttpClient::builder(); - if let Some(timeout) = timeout_secs { - builder = builder.timeout(Duration::from_secs(timeout)); - } else { - builder = builder.timeout(DEFAULT_REQUEST_TIMEOUT); - } - - match proxy_type { - ProxyType::None => { - builder = builder.no_proxy(); - } - ProxyType::Localhost => { - let port = Config::verge() - .latest_ref() - .verge_mixed_port - .unwrap_or(Config::clash().latest_ref().get_mixed_port()); - - let proxy_scheme = format!("http://127.0.0.1:{port}"); - - if let Ok(proxy) = Proxy::http(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::https(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::all(&proxy_scheme) { - builder = builder.proxy(proxy); - } - } - ProxyType::System => { - use sysproxy::Sysproxy; - - if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() { - let proxy_scheme = format!("http://{}:{}", p.host, p.port); - - if let Ok(proxy) = Proxy::http(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::https(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::all(&proxy_scheme) { - builder = builder.proxy(proxy); - } - } - } - } - - builder = builder.danger_accept_invalid_certs(accept_invalid_certs); - - if let Some(ua) = user_agent { - builder = builder.user_agent(ua); - } else { - use crate::utils::resolve::VERSION; - - let version = match VERSION.get() { - Some(v) => format!("clash-verge/v{v}"), - None => "clash-verge/unknown".to_string(), + builder = match proxy_uri_clone { + Some(uri) => builder.proxy(Some(uri)), + None => builder.proxy(None), }; - builder = builder.user_agent(version); + for (name, value) in headers_clone.iter() { + builder = builder.default_header(name, value); + } + + if accept_invalid_certs { + builder = builder.ssl_options(SslOption::DANGER_ACCEPT_INVALID_CERTS); + } + + if let Some(secs) = timeout_secs { + builder = builder.timeout(Duration::from_secs(secs)); + } + + builder = builder.redirect_policy(RedirectPolicy::Follow); + + Ok(builder.build()?) } - - let client = builder.build().expect("Failed to build custom HTTP client"); - - client.get(url) } - /* /// 执行GET请求,添加错误跟踪 - pub async fn get( + pub async fn create_request( &self, - url: &str, proxy_type: ProxyType, timeout_secs: Option, user_agent: Option, accept_invalid_certs: bool, - ) -> Result { - let request = self.create_request( - url, - proxy_type, - timeout_secs, - user_agent, - accept_invalid_certs, + ) -> Result { + let proxy_uri = match proxy_type { + ProxyType::None => None, + ProxyType::Localhost => { + let port = { + let verge_port = Config::verge().await.latest_ref().verge_mixed_port; + match verge_port { + Some(port) => port, + None => Config::clash().await.latest_ref().get_mixed_port(), + } + }; + let proxy_scheme = format!("http://127.0.0.1:{port}"); + proxy_scheme.parse::().ok() + } + ProxyType::System => { + if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() { + let proxy_scheme = format!("http://{}:{}", p.host, p.port); + proxy_scheme.parse::().ok() + } else { + None + } + } + }; + + let mut headers = HeaderMap::new(); + headers.insert( + USER_AGENT, + HeaderValue::from_str( + &user_agent + .unwrap_or_else(|| format!("clash-verge/v{}", env!("CARGO_PKG_VERSION"))), + )?, ); - let timeout_duration = timeout_secs.unwrap_or(30); + let client = self.build_client(proxy_uri, headers, accept_invalid_certs, timeout_secs)?; - match tokio::time::timeout(Duration::from_secs(timeout_duration), request.send()).await { - Ok(result) => match result { - Ok(response) => Ok(response), - Err(e) => { - self.record_connection_error(&e.to_string()); - Err(anyhow::anyhow!("Failed to send HTTP request: {}", e)) - } - }, - Err(_) => { - self.record_connection_error("Request timeout"); - Err(anyhow::anyhow!( - "HTTP request timed out after {} seconds", - timeout_duration - )) - } - } - } */ + Ok(client) + } pub async fn get_with_interrupt( &self, @@ -270,49 +192,63 @@ impl NetworkManager { timeout_secs: Option, user_agent: Option, accept_invalid_certs: bool, - ) -> Result { - let request = self.create_request( - url, - proxy_type, - timeout_secs, - user_agent, - accept_invalid_certs, - ); + ) -> Result { + if self.should_reset_clients().await { + self.reset_clients().await; + } - let timeout_duration = timeout_secs.unwrap_or(20); + let parsed = Url::parse(url)?; + let mut extra_headers = HeaderMap::new(); - let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>(); + if !parsed.username().is_empty() + && let Some(pass) = parsed.password() + { + let auth_str = format!("{}:{}", parsed.username(), pass); + let encoded = general_purpose::STANDARD.encode(auth_str); + extra_headers.insert( + "Authorization", + HeaderValue::from_str(&format!("Basic {}", encoded))?, + ); + } - let url_clone = url.to_string(); - let watchdog = tokio::spawn(async move { - tokio::time::sleep(Duration::from_secs(timeout_duration)).await; - let _ = cancel_tx.send(()); - logging!(warn, Type::Network, true, "请求超时取消: {}", url_clone); - }); + let clean_url = { + let mut no_auth = parsed.clone(); + no_auth.set_username("").ok(); + no_auth.set_password(None).ok(); + no_auth.to_string() + }; - let result = tokio::select! { - result = request.send() => result, - _ = cancel_rx => { - self.record_connection_error(&format!("Request interrupted for: {url}")); - return Err(anyhow::anyhow!("Request interrupted after {} seconds", timeout_duration)); + let client = self + .create_request(proxy_type, timeout_secs, user_agent, accept_invalid_certs) + .await?; + + let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(20)); + let response = match timeout(timeout_duration, async { + let mut req = isahc::Request::get(&clean_url); + + for (k, v) in extra_headers.iter() { + req = req.header(k, v); + } + + let mut response = client.send_async(req.body(())?).await?; + let status = response.status(); + let headers = response.headers().clone(); + let body = response.text().await?; + Ok::<_, anyhow::Error>(HttpResponse::new(status, headers, body)) + }) + .await + { + Ok(res) => res?, + Err(_) => { + self.record_connection_error(&format!("Request interrupted: {}", url)) + .await; + return Err(anyhow::anyhow!( + "Request interrupted after {}s", + timeout_duration.as_secs() + )); } }; - watchdog.abort(); - match result { - Ok(response) => Ok(response), - Err(e) => { - self.record_connection_error(&e.to_string()); - Err(anyhow::anyhow!("Failed to send HTTP request: {}", e)) - } - } + Ok(response) } } - -/// 代理类型 -#[derive(Debug, Clone, Copy)] -pub enum ProxyType { - None, - Localhost, - System, -} diff --git a/clash-verge-rev/src-tauri/src/utils/notification.rs b/clash-verge-rev/src-tauri/src/utils/notification.rs index 305e37e7d0..0b0b719962 100644 --- a/clash-verge-rev/src-tauri/src/utils/notification.rs +++ b/clash-verge-rev/src-tauri/src/utils/notification.rs @@ -1,3 +1,5 @@ +use crate::utils::i18n::t; + use tauri::AppHandle; use tauri_plugin_notification::NotificationExt; @@ -23,48 +25,54 @@ fn notify(app: &AppHandle, title: &str, body: &str) { .ok(); } -pub fn notify_event(app: &AppHandle, event: NotificationEvent) { - use crate::utils::i18n::t; +pub async fn notify_event<'a>(app: AppHandle, event: NotificationEvent<'a>) { match event { NotificationEvent::DashboardToggled => { - notify(app, &t("DashboardToggledTitle"), &t("DashboardToggledBody")); + notify( + &app, + &t("DashboardToggledTitle").await, + &t("DashboardToggledBody").await, + ); } NotificationEvent::ClashModeChanged { mode } => { notify( - app, - &t("ClashModeChangedTitle"), - &t_with_args("ClashModeChangedBody", mode), + &app, + &t("ClashModeChangedTitle").await, + &t_with_args("ClashModeChangedBody", mode).await, ); } NotificationEvent::SystemProxyToggled => { notify( - app, - &t("SystemProxyToggledTitle"), - &t("SystemProxyToggledBody"), + &app, + &t("SystemProxyToggledTitle").await, + &t("SystemProxyToggledBody").await, ); } NotificationEvent::TunModeToggled => { - notify(app, &t("TunModeToggledTitle"), &t("TunModeToggledBody")); + notify( + &app, + &t("TunModeToggledTitle").await, + &t("TunModeToggledBody").await, + ); } NotificationEvent::LightweightModeEntered => { notify( - app, - &t("LightweightModeEnteredTitle"), - &t("LightweightModeEnteredBody"), + &app, + &t("LightweightModeEnteredTitle").await, + &t("LightweightModeEnteredBody").await, ); } NotificationEvent::AppQuit => { - notify(app, &t("AppQuitTitle"), &t("AppQuitBody")); + notify(&app, &t("AppQuitTitle").await, &t("AppQuitBody").await); } #[cfg(target_os = "macos")] NotificationEvent::AppHidden => { - notify(app, &t("AppHiddenTitle"), &t("AppHiddenBody")); + notify(&app, &t("AppHiddenTitle").await, &t("AppHiddenBody").await); } } } // 辅助函数,带参数的i18n -fn t_with_args(key: &str, mode: &str) -> String { - use crate::utils::i18n::t; - t(key).replace("{mode}", mode) +async fn t_with_args(key: &str, mode: &str) -> String { + t(key).await.replace("{mode}", mode) } diff --git a/clash-verge-rev/src-tauri/src/utils/resolve.rs b/clash-verge-rev/src-tauri/src/utils/resolve.rs deleted file mode 100644 index 10a33c2882..0000000000 --- a/clash-verge-rev/src-tauri/src/utils/resolve.rs +++ /dev/null @@ -1,732 +0,0 @@ -#[cfg(target_os = "macos")] -use crate::AppHandleManager; -use crate::{ - config::{Config, IVerge, PrfItem}, - core::*, - logging, logging_error, - module::lightweight::{self, auto_lightweight_mode_init}, - process::AsyncHandler, - utils::{init, logging::Type, server}, - wrap_err, -}; -use anyhow::{bail, Result}; -use once_cell::sync::OnceCell; -use parking_lot::{Mutex, RwLock}; -use percent_encoding::percent_decode_str; -use scopeguard; -use serde_yaml::Mapping; -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; -use tauri::{AppHandle, Manager}; -use tokio::net::TcpListener; - -use tauri::Url; -//#[cfg(not(target_os = "linux"))] -// use window_shadows::set_shadow; - -pub static VERSION: OnceCell = OnceCell::new(); - -// 定义默认窗口尺寸常量 -const DEFAULT_WIDTH: u32 = 940; -const DEFAULT_HEIGHT: u32 = 700; - -// 添加全局UI准备就绪标志 -static UI_READY: OnceCell>> = OnceCell::new(); - -// 窗口创建锁,防止并发创建窗口 -static WINDOW_CREATING: OnceCell> = OnceCell::new(); - -// UI就绪阶段状态枚举 -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum UiReadyStage { - NotStarted, - Loading, - DomReady, - ResourcesLoaded, - Ready, -} - -// UI就绪详细状态 -#[derive(Debug)] -struct UiReadyState { - stage: RwLock, -} - -impl Default for UiReadyState { - fn default() -> Self { - Self { - stage: RwLock::new(UiReadyStage::NotStarted), - } - } -} - -// 获取UI就绪状态细节 -static UI_READY_STATE: OnceCell> = OnceCell::new(); - -fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> { - WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now()))) -} - -fn get_ui_ready() -> &'static Arc> { - UI_READY.get_or_init(|| Arc::new(RwLock::new(false))) -} - -fn get_ui_ready_state() -> &'static Arc { - UI_READY_STATE.get_or_init(|| Arc::new(UiReadyState::default())) -} - -// 更新UI准备阶段 -pub fn update_ui_ready_stage(stage: UiReadyStage) { - let state = get_ui_ready_state(); - let mut stage_lock = state.stage.write(); - - *stage_lock = stage; - // 如果是最终阶段,标记UI完全就绪 - if stage == UiReadyStage::Ready { - mark_ui_ready(); - } -} - -// 标记UI已准备就绪 -pub fn mark_ui_ready() { - let mut ready = get_ui_ready().write(); - *ready = true; - logging!(info, Type::Window, true, "UI已标记为完全就绪"); -} - -// 重置UI就绪状态 -pub fn reset_ui_ready() { - { - let mut ready = get_ui_ready().write(); - *ready = false; - } - { - let state = get_ui_ready_state(); - let mut stage = state.stage.write(); - *stage = UiReadyStage::NotStarted; - } - logging!(info, Type::Window, true, "UI就绪状态已重置"); -} - -pub async fn find_unused_port() -> Result { - match TcpListener::bind("127.0.0.1:0").await { - Ok(listener) => { - let port = listener.local_addr()?.port(); - Ok(port) - } - Err(_) => { - let port = Config::verge() - .latest_ref() - .verge_mixed_port - .unwrap_or(Config::clash().latest_ref().get_mixed_port()); - log::warn!(target: "app", "use default port: {port}"); - Ok(port) - } - } -} - -/// 异步方式处理启动后的额外任务 -pub async fn resolve_setup_async(app_handle: &AppHandle) { - let start_time = std::time::Instant::now(); - logging!(info, Type::Setup, true, "开始执行异步设置任务..."); - - if VERSION.get().is_none() { - let version = app_handle.package_info().version.to_string(); - VERSION.get_or_init(|| { - logging!(info, Type::Setup, true, "初始化版本信息: {}", version); - version.clone() - }); - } - - logging_error!(Type::Setup, true, init::init_scheme()); - - logging_error!(Type::Setup, true, init::startup_script().await); - - if let Err(err) = resolve_random_port_config().await { - logging!( - error, - Type::System, - true, - "Failed to resolve random port config: {}", - err - ); - } - - logging!(trace, Type::Config, true, "初始化配置..."); - logging_error!(Type::Config, true, Config::init_config().await); - - // 启动时清理冗余的 Profile 文件 - logging!(info, Type::Setup, true, "清理冗余的Profile文件..."); - let profiles = Config::profiles(); - if let Err(e) = profiles.latest_ref().auto_cleanup() { - logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e); - } else { - logging!(info, Type::Setup, true, "启动时Profile文件清理完成"); - } - - logging!(trace, Type::Core, true, "启动核心管理器..."); - logging_error!(Type::Core, true, CoreManager::global().init().await); - - log::trace!(target: "app", "启动内嵌服务器..."); - server::embed_server(); - - logging!(trace, Type::Core, true, "启动 IPC 监控服务..."); - // IPC 监控器将在首次调用时自动初始化 - - // // 启动测试线程,持续打印流量数据 - // logging!(info, Type::Core, true, "启动流量数据测试线程..."); - // AsyncHandler::spawn(|| async { - // let mut interval = tokio::time::interval(std::time::Duration::from_secs(2)); - // loop { - // interval.tick().await; - - // let traffic_data = get_current_traffic().await; - // let memory_data = get_current_memory().await; - - // println!("=== Traffic Data Test (IPC) ==="); - // println!( - // "Traffic - Up: {} bytes/s, Down: {} bytes/s, Last Updated: {:?}", - // traffic_data.up_rate, traffic_data.down_rate, traffic_data.last_updated - // ); - // println!( - // "Memory - InUse: {} bytes, OSLimit: {:?}, Last Updated: {:?}", - // memory_data.inuse, memory_data.oslimit, memory_data.last_updated - // ); - // println!("=============================="); - // } - // }); - - logging_error!(Type::Tray, true, tray::Tray::global().init()); - - if let Some(app_handle) = handle::Handle::global().app_handle() { - logging!(info, Type::Tray, true, "创建系统托盘..."); - let result = tray::Tray::global().create_tray_from_handle(&app_handle); - if result.is_ok() { - logging!(info, Type::Tray, true, "系统托盘创建成功"); - } else if let Err(e) = result { - logging!(error, Type::Tray, true, "系统托盘创建失败: {}", e); - } - } else { - logging!( - error, - Type::Tray, - true, - "无法创建系统托盘: app_handle不存在" - ); - } - - // 更新系统代理 - logging_error!( - Type::System, - true, - sysopt::Sysopt::global().update_sysproxy().await - ); - logging_error!( - Type::System, - true, - sysopt::Sysopt::global().init_guard_sysproxy() - ); - - // 创建窗口 - let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false); - #[cfg(target_os = "macos")] - { - if is_silent_start { - use crate::AppHandleManager; - - AppHandleManager::global().set_activation_policy_accessory(); - } - } - create_window(!is_silent_start); - - // 初始化定时器 - logging_error!(Type::System, true, timer::Timer::global().init()); - - // 自动进入轻量模式 - auto_lightweight_mode_init(); - - logging_error!(Type::Tray, true, tray::Tray::global().update_part()); - - logging!(trace, Type::System, true, "初始化热键..."); - logging_error!(Type::System, true, hotkey::Hotkey::global().init()); - - let elapsed = start_time.elapsed(); - logging!( - info, - Type::Setup, - true, - "异步设置任务完成,耗时: {:?}", - elapsed - ); - - // 如果初始化时间过长,记录警告 - if elapsed.as_secs() > 10 { - logging!( - warn, - Type::Setup, - true, - "异步设置任务耗时较长({:?})", - elapsed - ); - } -} - -/// reset system proxy (异步) -pub async fn resolve_reset_async() { - #[cfg(target_os = "macos")] - logging!(info, Type::Tray, true, "Unsubscribing from traffic updates"); - #[cfg(target_os = "macos")] - tray::Tray::global().unsubscribe_traffic(); - - logging_error!( - Type::System, - true, - sysopt::Sysopt::global().reset_sysproxy().await - ); - logging_error!(Type::Core, true, CoreManager::global().stop_core().await); - #[cfg(target_os = "macos")] - { - logging!(info, Type::System, true, "Restoring system DNS settings"); - restore_public_dns().await; - } -} - -/// Create the main window -pub fn create_window(is_show: bool) -> bool { - logging!( - info, - Type::Window, - true, - "开始创建/显示主窗口, is_show={}", - is_show - ); - - if !is_show { - logging!(info, Type::Window, true, "静默模式启动时不创建窗口"); - lightweight::set_lightweight_mode(true); - handle::Handle::notify_startup_completed(); - return false; - } - - if let Some(app_handle) = handle::Handle::global().app_handle() { - if let Some(window) = app_handle.get_webview_window("main") { - logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口"); - if is_show { - if window.is_minimized().unwrap_or(false) { - logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化"); - let _ = window.unminimize(); - } - let _ = window.show(); - let _ = window.set_focus(); - - #[cfg(target_os = "macos")] - { - AppHandleManager::global().set_activation_policy_regular(); - } - } - return true; - } - } - - let creating_lock = get_window_creating_lock(); - let mut creating = creating_lock.lock(); - - let (is_creating, last_time) = *creating; - let elapsed = last_time.elapsed(); - - if is_creating && elapsed < Duration::from_secs(2) { - logging!( - info, - Type::Window, - true, - "窗口创建请求被忽略,因为最近创建过 ({:?}ms)", - elapsed.as_millis() - ); - return false; - } - - *creating = (true, Instant::now()); - - // ScopeGuard 确保创建状态重置,防止 webview 卡死 - let _guard = scopeguard::guard(creating, |mut creating_guard| { - *creating_guard = (false, Instant::now()); - logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置"); - }); - - match tauri::WebviewWindowBuilder::new( - &handle::Handle::global().app_handle().unwrap(), - "main", /* the unique window label */ - tauri::WebviewUrl::App("index.html".into()), - ) - .title("Clash Verge") - .center() - .decorations(true) - .fullscreen(false) - .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) - .min_inner_size(520.0, 520.0) - .visible(true) // 立即显示窗口,避免用户等待 - .initialization_script( - r#" - console.log('[Tauri] 窗口初始化脚本开始执行'); - - function createLoadingOverlay() { - - if (document.getElementById('initial-loading-overlay')) { - console.log('[Tauri] 加载指示器已存在'); - return; - } - - console.log('[Tauri] 创建加载指示器'); - const loadingDiv = document.createElement('div'); - loadingDiv.id = 'initial-loading-overlay'; - loadingDiv.innerHTML = ` -
-
-
-
-
Loading Clash Verge...
-
- - `; - - if (document.body) { - document.body.appendChild(loadingDiv); - } else { - document.addEventListener('DOMContentLoaded', () => { - if (document.body && !document.getElementById('initial-loading-overlay')) { - document.body.appendChild(loadingDiv); - } - }); - } - } - - createLoadingOverlay(); - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', createLoadingOverlay); - } else { - createLoadingOverlay(); - } - - console.log('[Tauri] 窗口初始化脚本执行完成'); - "#, - ) - .build() - { - Ok(newly_created_window) => { - logging!(debug, Type::Window, true, "主窗口实例创建成功"); - - update_ui_ready_stage(UiReadyStage::NotStarted); - - AsyncHandler::spawn(move || async move { - handle::Handle::global().mark_startup_completed(); - logging!( - debug, - Type::Window, - true, - "异步窗口任务开始 (启动已标记完成)" - ); - - // 先运行轻量模式检测 - lightweight::run_once_auto_lightweight(); - - // 发送启动完成事件,触发前端开始加载 - logging!( - debug, - Type::Window, - true, - "发送 verge://startup-completed 事件" - ); - handle::Handle::notify_startup_completed(); - - if is_show { - let window_clone = newly_created_window.clone(); - - // 立即显示窗口 - let _ = window_clone.show(); - let _ = window_clone.set_focus(); - logging!(info, Type::Window, true, "窗口已立即显示"); - #[cfg(target_os = "macos")] - { - AppHandleManager::global().set_activation_policy_regular(); - } - - let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() { - 3 - } else { - 8 - }; - - logging!( - info, - Type::Window, - true, - "开始监控UI加载状态 (最多{}秒)...", - timeout_seconds - ); - - // 异步监控UI状态,不影响窗口显示 - tokio::spawn(async move { - let wait_result = - tokio::time::timeout(Duration::from_secs(timeout_seconds), async { - let mut check_count = 0; - while !*get_ui_ready().read() { - tokio::time::sleep(Duration::from_millis(100)).await; - check_count += 1; - - // 每2秒记录一次等待状态 - if check_count % 20 == 0 { - logging!( - debug, - Type::Window, - true, - "UI加载状态检查... ({}秒)", - check_count / 10 - ); - } - } - }) - .await; - - match wait_result { - Ok(_) => { - logging!(info, Type::Window, true, "UI已完全加载就绪"); - // 移除初始加载指示器 - if let Some(window) = handle::Handle::global().get_window() { - let _ = window.eval(r#" - const overlay = document.getElementById('initial-loading-overlay'); - if (overlay) { - overlay.style.opacity = '0'; - setTimeout(() => overlay.remove(), 300); - } - "#); - } - } - Err(_) => { - logging!( - warn, - Type::Window, - true, - "UI加载监控超时({}秒),但窗口已正常显示", - timeout_seconds - ); - *get_ui_ready().write() = true; - } - } - }); - - logging!(info, Type::Window, true, "窗口显示流程完成"); - } else { - logging!( - debug, - Type::Window, - true, - "is_show为false,窗口保持隐藏状态" - ); - } - }); - true - } - Err(e) => { - logging!(error, Type::Window, true, "主窗口构建失败: {}", e); - false - } - } -} - -pub async fn resolve_scheme(param: String) -> Result<()> { - log::info!(target:"app", "received deep link: {param}"); - - let param_str = if param.starts_with("[") && param.len() > 4 { - param - .get(2..param.len() - 2) - .ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))? - } else { - param.as_str() - }; - - // 解析 URL - let link_parsed = match Url::parse(param_str) { - Ok(url) => url, - Err(e) => { - bail!("failed to parse deep link: {:?}, param: {:?}", e, param); - } - }; - - if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" { - let name = link_parsed - .query_pairs() - .find(|(key, _)| key == "name") - .map(|(_, value)| value.into_owned()); - - let url_param = if let Some(query) = link_parsed.query() { - let prefix = "url="; - if let Some(pos) = query.find(prefix) { - let raw_url = &query[pos + prefix.len()..]; - Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string()) - } else { - None - } - } else { - None - }; - - match url_param { - Some(url) => { - log::info!(target:"app", "decoded subscription url: {url}"); - - create_window(false); - match PrfItem::from_url(url.as_ref(), name, None, None).await { - Ok(item) => { - let uid = item.uid.clone().unwrap(); - let _ = wrap_err!(Config::profiles().data_mut().append_item(item)); - handle::Handle::notice_message("import_sub_url::ok", uid); - } - Err(e) => { - handle::Handle::notice_message("import_sub_url::error", e.to_string()); - } - } - } - None => bail!("failed to get profile url"), - } - } - - Ok(()) -} - -async fn resolve_random_port_config() -> Result<()> { - let verge_config = Config::verge(); - let clash_config = Config::clash(); - let enable_random_port = verge_config - .latest_ref() - .enable_random_port - .unwrap_or(false); - - let default_port = verge_config - .latest_ref() - .verge_mixed_port - .unwrap_or(clash_config.latest_ref().get_mixed_port()); - - let port = if enable_random_port { - find_unused_port().await.unwrap_or(default_port) - } else { - default_port - }; - - let port_to_save = port; - - tokio::task::spawn_blocking(move || { - let verge_config_accessor = Config::verge(); - let mut verge_data = verge_config_accessor.data_mut(); - verge_data.patch_config(IVerge { - verge_mixed_port: Some(port_to_save), - ..IVerge::default() - }); - verge_data.save_file() - }) - .await??; // First ? for spawn_blocking error, second for save_file Result - - tokio::task::spawn_blocking(move || { - let clash_config_accessor = Config::clash(); // Extend lifetime of the accessor - let mut clash_data = clash_config_accessor.data_mut(); // Access within blocking task, made mutable - let mut mapping = Mapping::new(); - mapping.insert("mixed-port".into(), port_to_save.into()); - clash_data.patch_config(mapping); - clash_data.save_config() - }) - .await??; - - Ok(()) -} - -#[cfg(target_os = "macos")] -pub async fn set_public_dns(dns_server: String) { - use crate::{core::handle, utils::dirs}; - use tauri_plugin_shell::ShellExt; - let app_handle = handle::Handle::global().app_handle().unwrap(); - - log::info!(target: "app", "try to set system dns"); - let resource_dir = dirs::app_resources_dir().unwrap(); - let script = resource_dir.join("set_dns.sh"); - if !script.exists() { - log::error!(target: "app", "set_dns.sh not found"); - return; - } - let script = script.to_string_lossy().into_owned(); - match app_handle - .shell() - .command("bash") - .args([script, dns_server]) - .current_dir(resource_dir) - .status() - .await - { - Ok(status) => { - if status.success() { - log::info!(target: "app", "set system dns successfully"); - } else { - let code = status.code().unwrap_or(-1); - log::error!(target: "app", "set system dns failed: {code}"); - } - } - Err(err) => { - log::error!(target: "app", "set system dns failed: {err}"); - } - } -} - -#[cfg(target_os = "macos")] -pub async fn restore_public_dns() { - use crate::{core::handle, utils::dirs}; - use tauri_plugin_shell::ShellExt; - let app_handle = handle::Handle::global().app_handle().unwrap(); - log::info!(target: "app", "try to unset system dns"); - let resource_dir = dirs::app_resources_dir().unwrap(); - let script = resource_dir.join("unset_dns.sh"); - if !script.exists() { - log::error!(target: "app", "unset_dns.sh not found"); - return; - } - let script = script.to_string_lossy().into_owned(); - match app_handle - .shell() - .command("bash") - .args([script]) - .current_dir(resource_dir) - .status() - .await - { - Ok(status) => { - if status.success() { - log::info!(target: "app", "unset system dns successfully"); - } else { - let code = status.code().unwrap_or(-1); - log::error!(target: "app", "unset system dns failed: {code}"); - } - } - Err(err) => { - log::error!(target: "app", "unset system dns failed: {err}"); - } - } -} diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs b/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs new file mode 100644 index 0000000000..e3618178fb --- /dev/null +++ b/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs @@ -0,0 +1,94 @@ +#[cfg(target_os = "macos")] +pub async fn set_public_dns(dns_server: String) { + use crate::{core::handle, utils::dirs}; + use tauri_plugin_shell::ShellExt; + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + log::error!(target: "app", "app_handle not available for DNS configuration"); + return; + } + }; + + log::info!(target: "app", "try to set system dns"); + let resource_dir = match dirs::app_resources_dir() { + Ok(dir) => dir, + Err(e) => { + log::error!(target: "app", "Failed to get resource directory: {}", e); + return; + } + }; + let script = resource_dir.join("set_dns.sh"); + if !script.exists() { + log::error!(target: "app", "set_dns.sh not found"); + return; + } + let script = script.to_string_lossy().into_owned(); + match app_handle + .shell() + .command("bash") + .args([script, dns_server]) + .current_dir(resource_dir) + .status() + .await + { + Ok(status) => { + if status.success() { + log::info!(target: "app", "set system dns successfully"); + } else { + let code = status.code().unwrap_or(-1); + log::error!(target: "app", "set system dns failed: {code}"); + } + } + Err(err) => { + log::error!(target: "app", "set system dns failed: {err}"); + } + } +} + +#[cfg(target_os = "macos")] +pub async fn restore_public_dns() { + use crate::{core::handle, utils::dirs}; + use tauri_plugin_shell::ShellExt; + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + log::error!(target: "app", "app_handle not available for DNS restoration"); + return; + } + }; + log::info!(target: "app", "try to unset system dns"); + let resource_dir = match dirs::app_resources_dir() { + Ok(dir) => dir, + Err(e) => { + log::error!(target: "app", "Failed to get resource directory: {}", e); + return; + } + }; + let script = resource_dir.join("unset_dns.sh"); + if !script.exists() { + log::error!(target: "app", "unset_dns.sh not found"); + return; + } + let script = script.to_string_lossy().into_owned(); + match app_handle + .shell() + .command("bash") + .args([script]) + .current_dir(resource_dir) + .status() + .await + { + Ok(status) => { + if status.success() { + log::info!(target: "app", "unset system dns successfully"); + } else { + let code = status.code().unwrap_or(-1); + log::error!(target: "app", "unset system dns failed: {code}"); + } + } + Err(err) => { + log::error!(target: "app", "unset system dns failed: {err}"); + } + } +} diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs b/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs new file mode 100644 index 0000000000..1bee81fda1 --- /dev/null +++ b/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs @@ -0,0 +1,273 @@ +use anyhow::Result; +use tauri::AppHandle; + +use crate::{ + config::Config, + core::{ + CoreManager, Timer, handle, hotkey::Hotkey, service::SERVICE_MANAGER, sysopt, tray::Tray, + }, + logging, logging_error, + module::lightweight::{auto_lightweight_mode_init, run_once_auto_lightweight}, + process::AsyncHandler, + utils::{init, logging::Type, server, window_manager::WindowManager}, +}; + +pub mod dns; +pub mod scheme; +pub mod ui; +pub mod window; +pub mod window_script; + +pub fn resolve_setup_handle(app_handle: AppHandle) { + init_handle(app_handle); +} + +pub fn resolve_setup_sync() { + AsyncHandler::spawn(|| async { + AsyncHandler::spawn_blocking(init_scheme); + AsyncHandler::spawn_blocking(init_embed_server); + }); +} + +pub fn resolve_setup_async() { + let start_time = std::time::Instant::now(); + logging!( + info, + Type::Setup, + true, + "开始执行异步设置任务... 线程ID: {:?}", + std::thread::current().id() + ); + + AsyncHandler::spawn(|| async { + #[cfg(not(feature = "tauri-dev"))] + resolve_setup_logger().await; + logging!( + info, + Type::ClashVergeRev, + true, + "Version: {}", + env!("CARGO_PKG_VERSION") + ); + init_service_manager().await; + + futures::join!( + init_work_config(), + init_resources(), + init_startup_script(), + init_hotkey(), + ); + + init_timer().await; + init_once_auto_lightweight().await; + init_auto_lightweight_mode().await; + + init_verge_config().await; + init_core_manager().await; + + init_system_proxy().await; + AsyncHandler::spawn_blocking(|| { + init_system_proxy_guard(); + }); + + let tray_and_refresh = async { + init_tray().await; + refresh_tray_menu().await; + }; + futures::join!(init_window(), tray_and_refresh,); + }); + + let elapsed = start_time.elapsed(); + logging!( + info, + Type::Setup, + true, + "异步设置任务完成,耗时: {:?}", + elapsed + ); + + if elapsed.as_secs() > 10 { + logging!( + warn, + Type::Setup, + true, + "异步设置任务耗时较长({:?})", + elapsed + ); + } +} + +// 其它辅助函数不变 +pub async fn resolve_reset_async() { + logging!(info, Type::Tray, true, "Resetting system proxy"); + logging_error!( + Type::System, + true, + sysopt::Sysopt::global().reset_sysproxy().await + ); + + logging!(info, Type::Core, true, "Stopping core service"); + logging_error!(Type::Core, true, CoreManager::global().stop_core().await); + + #[cfg(target_os = "macos")] + { + use dns::restore_public_dns; + + logging!(info, Type::System, true, "Restoring system DNS settings"); + restore_public_dns().await; + } +} + +pub fn init_handle(app_handle: AppHandle) { + logging!(info, Type::Setup, true, "Initializing app handle..."); + handle::Handle::global().init(app_handle); +} + +pub(super) fn init_scheme() { + logging!(info, Type::Setup, true, "Initializing custom URL scheme"); + logging_error!(Type::Setup, true, init::init_scheme()); +} + +#[cfg(not(feature = "tauri-dev"))] +pub(super) async fn resolve_setup_logger() { + logging!(info, Type::Setup, true, "Initializing global logger..."); + logging_error!(Type::Setup, true, init::init_logger().await); +} + +pub async fn resolve_scheme(param: String) -> Result<()> { + logging!( + info, + Type::Setup, + true, + "Resolving scheme for param: {}", + param + ); + logging_error!(Type::Setup, true, scheme::resolve_scheme(param).await); + Ok(()) +} + +pub(super) fn init_embed_server() { + logging!(info, Type::Setup, true, "Initializing embedded server..."); + server::embed_server(); +} +pub(super) async fn init_resources() { + logging!(info, Type::Setup, true, "Initializing resources..."); + logging_error!(Type::Setup, true, init::init_resources().await); +} + +pub(super) async fn init_startup_script() { + logging!(info, Type::Setup, true, "Initializing startup script"); + logging_error!(Type::Setup, true, init::startup_script().await); +} + +pub(super) async fn init_timer() { + logging!(info, Type::Setup, true, "Initializing timer..."); + logging_error!(Type::Setup, true, Timer::global().init().await); +} + +pub(super) async fn init_hotkey() { + logging!(info, Type::Setup, true, "Initializing hotkey..."); + logging_error!(Type::Setup, true, Hotkey::global().init().await); +} + +pub(super) async fn init_once_auto_lightweight() { + logging!( + info, + Type::Lightweight, + true, + "Running auto lightweight mode check..." + ); + run_once_auto_lightweight().await; +} + +pub(super) async fn init_auto_lightweight_mode() { + logging!( + info, + Type::Setup, + true, + "Initializing auto lightweight mode..." + ); + logging_error!(Type::Setup, true, auto_lightweight_mode_init().await); +} + +pub async fn init_work_config() { + logging!( + info, + Type::Setup, + true, + "Initializing work configuration..." + ); + logging_error!(Type::Setup, true, init::init_config().await); +} + +pub(super) async fn init_tray() { + logging!(info, Type::Setup, true, "Initializing system tray..."); + logging_error!(Type::Setup, true, Tray::global().init().await); +} + +pub(super) async fn init_verge_config() { + logging!( + info, + Type::Setup, + true, + "Initializing verge configuration..." + ); + logging_error!(Type::Setup, true, Config::init_config().await); +} + +pub(super) async fn init_service_manager() { + logging!(info, Type::Setup, true, "Initializing service manager..."); + logging_error!( + Type::Setup, + true, + SERVICE_MANAGER.lock().await.refresh().await + ); +} + +pub(super) async fn init_core_manager() { + logging!(info, Type::Setup, true, "Initializing core manager..."); + logging_error!(Type::Setup, true, CoreManager::global().init().await); +} + +pub(super) async fn init_system_proxy() { + logging!(info, Type::Setup, true, "Initializing system proxy..."); + logging_error!( + Type::Setup, + true, + sysopt::Sysopt::global().update_sysproxy().await + ); +} + +pub(super) fn init_system_proxy_guard() { + logging!( + info, + Type::Setup, + true, + "Initializing system proxy guard..." + ); + logging_error!( + Type::Setup, + true, + sysopt::Sysopt::global().init_guard_sysproxy() + ); +} + +pub(super) async fn refresh_tray_menu() { + logging!(info, Type::Setup, true, "Refreshing tray menu..."); + logging_error!(Type::Setup, true, Tray::global().update_part().await); +} + +pub(super) async fn init_window() { + logging!(info, Type::Setup, true, "Initializing main window..."); + let is_silent_start = + { Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false); + #[cfg(target_os = "macos")] + { + if is_silent_start { + use crate::core::handle::Handle; + + Handle::global().set_activation_policy_accessory(); + } + } + WindowManager::create_window(!is_silent_start).await; +} diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs b/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs new file mode 100644 index 0000000000..99e6588ea4 --- /dev/null +++ b/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs @@ -0,0 +1,74 @@ +use anyhow::{Result, bail}; +use percent_encoding::percent_decode_str; +use tauri::Url; + +use crate::{config::PrfItem, core::handle, logging, utils::logging::Type, wrap_err}; + +pub(super) async fn resolve_scheme(param: String) -> Result<()> { + log::info!(target:"app", "received deep link: {param}"); + + let param_str = if param.starts_with("[") && param.len() > 4 { + param + .get(2..param.len() - 2) + .ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))? + } else { + param.as_str() + }; + + // 解析 URL + let link_parsed = match Url::parse(param_str) { + Ok(url) => url, + Err(e) => { + bail!("failed to parse deep link: {:?}, param: {:?}", e, param); + } + }; + + if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" { + let name = link_parsed + .query_pairs() + .find(|(key, _)| key == "name") + .map(|(_, value)| value.into_owned()); + + let url_param = if let Some(query) = link_parsed.query() { + let prefix = "url="; + if let Some(pos) = query.find(prefix) { + let raw_url = &query[pos + prefix.len()..]; + Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string()) + } else { + None + } + } else { + None + }; + + match url_param { + Some(url) => { + log::info!(target:"app", "decoded subscription url: {url}"); + match PrfItem::from_url(url.as_ref(), name, None, None).await { + Ok(item) => { + let uid = match item.uid.clone() { + Some(uid) => uid, + None => { + logging!(error, Type::Config, true, "Profile item missing UID"); + handle::Handle::notice_message( + "import_sub_url::error", + "Profile item missing UID".to_string(), + ); + return Ok(()); + } + }; + let result = crate::config::profiles::profiles_append_item_safe(item).await; + let _ = wrap_err!(result); + handle::Handle::notice_message("import_sub_url::ok", uid); + } + Err(e) => { + handle::Handle::notice_message("import_sub_url::error", e.to_string()); + } + } + } + None => bail!("failed to get profile url"), + } + } + + Ok(()) +} diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/ui.rs b/clash-verge-rev/src-tauri/src/utils/resolve/ui.rs new file mode 100644 index 0000000000..009e55ab5c --- /dev/null +++ b/clash-verge-rev/src-tauri/src/utils/resolve/ui.rs @@ -0,0 +1,73 @@ +use once_cell::sync::OnceCell; +use parking_lot::RwLock; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; +use tokio::sync::Notify; + +use crate::{logging, utils::logging::Type}; + +// 使用 AtomicBool 替代 RwLock,性能更好且无锁 +static UI_READY: OnceCell = OnceCell::new(); +// 获取UI就绪状态细节 +static UI_READY_STATE: OnceCell = OnceCell::new(); +// 添加通知机制,用于事件驱动的 UI 就绪检测 +static UI_READY_NOTIFY: OnceCell> = OnceCell::new(); + +// UI就绪阶段状态枚举 +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UiReadyStage { + NotStarted, + Loading, + DomReady, + ResourcesLoaded, + Ready, +} + +// UI就绪详细状态 +#[derive(Debug)] +struct UiReadyState { + stage: RwLock, +} + +impl Default for UiReadyState { + fn default() -> Self { + Self { + stage: RwLock::new(UiReadyStage::NotStarted), + } + } +} + +pub(super) fn get_ui_ready() -> &'static AtomicBool { + UI_READY.get_or_init(|| AtomicBool::new(false)) +} + +fn get_ui_ready_state() -> &'static UiReadyState { + UI_READY_STATE.get_or_init(UiReadyState::default) +} + +fn get_ui_ready_notify() -> &'static Arc { + UI_READY_NOTIFY.get_or_init(|| Arc::new(Notify::new())) +} + +// 更新UI准备阶段 +pub fn update_ui_ready_stage(stage: UiReadyStage) { + let state = get_ui_ready_state(); + let mut stage_lock = state.stage.write(); + + *stage_lock = stage; + // 如果是最终阶段,标记UI完全就绪 + if stage == UiReadyStage::Ready { + mark_ui_ready(); + } +} + +// 标记UI已准备就绪 +pub fn mark_ui_ready() { + get_ui_ready().store(true, Ordering::Release); + logging!(info, Type::Window, true, "UI已标记为完全就绪"); + + // 通知所有等待的任务 + get_ui_ready_notify().notify_waiters(); +} diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/window.rs b/clash-verge-rev/src-tauri/src/utils/resolve/window.rs new file mode 100644 index 0000000000..e99e3307f1 --- /dev/null +++ b/clash-verge-rev/src-tauri/src/utils/resolve/window.rs @@ -0,0 +1,52 @@ +use tauri::WebviewWindow; + +use crate::{ + core::handle, + logging, logging_error, + utils::{ + logging::Type, + resolve::window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT}, + }, +}; + +// 定义默认窗口尺寸常量 +const DEFAULT_WIDTH: f64 = 940.0; +const DEFAULT_HEIGHT: f64 = 700.0; + +const MINIMAL_WIDTH: f64 = 520.0; +const MINIMAL_HEIGHT: f64 = 520.0; + +/// 构建新的 WebView 窗口 +pub fn build_new_window() -> Result { + let app_handle = handle::Handle::global().app_handle().ok_or_else(|| { + logging!( + error, + Type::Window, + true, + "无法获取app_handle,窗口创建失败" + ); + "无法获取app_handle".to_string() + })?; + + match tauri::WebviewWindowBuilder::new( + &app_handle, + "main", /* the unique window label */ + tauri::WebviewUrl::App("index.html".into()), + ) + .title("Clash Verge") + .center() + .decorations(true) + .fullscreen(false) + .inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT) + .min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT) + .visible(true) // 立即显示窗口,避免用户等待 + .initialization_script(WINDOW_INITIAL_SCRIPT) + .build() + { + Ok(window) => { + logging_error!(Type::Window, true, window.eval(INITIAL_LOADING_OVERLAY)); + Ok(window) + } + Err(e) => Err(e.to_string()), + } +} diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs b/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs new file mode 100644 index 0000000000..632b0bf864 --- /dev/null +++ b/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs @@ -0,0 +1,71 @@ +pub const WINDOW_INITIAL_SCRIPT: &str = r#" + console.log('[Tauri] 窗口初始化脚本开始执行'); + + function createLoadingOverlay() { + + if (document.getElementById('initial-loading-overlay')) { + console.log('[Tauri] 加载指示器已存在'); + return; + } + + console.log('[Tauri] 创建加载指示器'); + const loadingDiv = document.createElement('div'); + loadingDiv.id = 'initial-loading-overlay'; + loadingDiv.innerHTML = ` +
+
+
+
+
Loading Clash Verge...
+
+ + `; + + if (document.body) { + document.body.appendChild(loadingDiv); + } else { + document.addEventListener('DOMContentLoaded', () => { + if (document.body && !document.getElementById('initial-loading-overlay')) { + document.body.appendChild(loadingDiv); + } + }); + } + } + + createLoadingOverlay(); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', createLoadingOverlay); + } else { + createLoadingOverlay(); + } + + console.log('[Tauri] 窗口初始化脚本执行完成'); +"#; + +pub const INITIAL_LOADING_OVERLAY: &str = r" + const overlay = document.getElementById('initial-loading-overlay'); + if (overlay) { + overlay.style.opacity = '0'; + setTimeout(() => overlay.remove(), 300); + } +"; diff --git a/clash-verge-rev/src-tauri/src/utils/server.rs b/clash-verge-rev/src-tauri/src/utils/server.rs index 7747d30a17..e16986e41d 100644 --- a/clash-verge-rev/src-tauri/src/utils/server.rs +++ b/clash-verge-rev/src-tauri/src/utils/server.rs @@ -1,15 +1,12 @@ -extern crate warp; - use super::resolve; use crate::{ - config::{Config, IVerge, DEFAULT_PAC}, + config::{Config, DEFAULT_PAC, IVerge}, logging_error, process::AsyncHandler, utils::logging::Type, }; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use port_scanner::local_port_available; -use std::convert::Infallible; use warp::Filter; #[derive(serde::Deserialize, Debug)] @@ -38,9 +35,8 @@ pub async fn check_singleton() -> Result<()> { } log::error!("failed to setup singleton listen server"); bail!("app exists"); - } else { - Ok(()) } + Ok(()) } /// The embed server only be used to implement singleton process @@ -49,39 +45,50 @@ pub fn embed_server() { let port = IVerge::get_singleton_port(); AsyncHandler::spawn(move || async move { - let visible = warp::path!("commands" / "visible").map(move || { - resolve::create_window(false); - "ok" + let visible = warp::path!("commands" / "visible").and_then(|| async { + Ok::<_, warp::Rejection>(warp::reply::with_status( + "ok".to_string(), + warp::http::StatusCode::OK, + )) }); + let verge_config = Config::verge().await; + let clash_config = Config::clash().await; + + let content = verge_config + .latest_ref() + .pac_file_content + .clone() + .unwrap_or(DEFAULT_PAC.to_string()); + + let mixed_port = verge_config + .latest_ref() + .verge_mixed_port + .unwrap_or(clash_config.latest_ref().get_mixed_port()); + + // Clone the content and port for the closure to avoid borrowing issues + let pac_content = content.clone(); + let pac_port = mixed_port; let pac = warp::path!("commands" / "pac").map(move || { - let content = Config::verge() - .latest_ref() - .pac_file_content - .clone() - .unwrap_or(DEFAULT_PAC.to_string()); - let port = Config::verge() - .latest_ref() - .verge_mixed_port - .unwrap_or(Config::clash().latest_ref().get_mixed_port()); - let content = content.replace("%mixed-port%", &format!("{port}")); + let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}")); warp::http::Response::builder() .header("Content-Type", "application/x-ns-proxy-autoconfig") - .body(content) + .body(processed_content) .unwrap_or_default() }); - async fn scheme_handler(query: QueryParam) -> Result { - logging_error!( - Type::Setup, - true, - resolve::resolve_scheme(query.param).await - ); - Ok("ok") - } + // Use map instead of and_then to avoid Send issues let scheme = warp::path!("commands" / "scheme") .and(warp::query::()) - .and_then(scheme_handler); + .map(|query: QueryParam| { + // Spawn async work in a fire-and-forget manner + let param = query.param.clone(); + tokio::task::spawn_local(async move { + logging_error!(Type::Setup, true, resolve::resolve_scheme(param).await); + }); + warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK) + }); + let commands = visible.or(scheme).or(pac); warp::serve(commands).run(([127, 0, 0, 1], port)).await; }); diff --git a/clash-verge-rev/src-tauri/src/utils/singleton.rs b/clash-verge-rev/src-tauri/src/utils/singleton.rs new file mode 100644 index 0000000000..2a18edc98b --- /dev/null +++ b/clash-verge-rev/src-tauri/src/utils/singleton.rs @@ -0,0 +1,124 @@ +/// Macro to generate singleton pattern for structs +/// +/// Usage: +/// ```rust,ignore +/// use crate::utils::singleton::singleton; +/// +/// struct MyStruct { +/// value: i32, +/// } +/// impl MyStruct { +/// fn new() -> Self { +/// MyStruct { value: 0 } +/// } +/// } +/// singleton!(MyStruct, INSTANCE); +/// ``` +#[macro_export] +macro_rules! singleton { + ($struct_name:ty, $instance_name:ident) => { + static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); + + impl $struct_name { + pub fn global() -> &'static $struct_name { + $instance_name.get_or_init(|| Self::new()) + } + } + }; + + ($struct_name:ty, $instance_name:ident, $init_expr:expr) => { + static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); + + impl $struct_name { + pub fn global() -> &'static $struct_name { + $instance_name.get_or_init(|| $init_expr) + } + } + }; +} + +/// Macro for singleton pattern with logging +#[macro_export] +macro_rules! singleton_with_logging { + ($struct_name:ty, $instance_name:ident, $struct_name_str:literal) => { + static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); + + impl $struct_name { + pub fn global() -> &'static $struct_name { + $instance_name.get_or_init(|| { + let instance = Self::new(); + $crate::logging!( + info, + $crate::utils::logging::Type::Setup, + true, + concat!($struct_name_str, " initialized") + ); + instance + }) + } + } + }; +} + +/// Macro for singleton pattern with lazy initialization using a closure +/// This replaces patterns like lazy_static! or complex OnceLock initialization +#[macro_export] +macro_rules! singleton_lazy { + ($struct_name:ty, $instance_name:ident, $init_closure:expr) => { + static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); + + impl $struct_name { + pub fn global() -> &'static $struct_name { + $instance_name.get_or_init($init_closure) + } + } + }; +} + +/// Macro for singleton pattern with lazy initialization and logging +#[macro_export] +macro_rules! singleton_lazy_with_logging { + ($struct_name:ty, $instance_name:ident, $struct_name_str:literal, $init_closure:expr) => { + static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); + + impl $struct_name { + pub fn global() -> &'static $struct_name { + $instance_name.get_or_init(|| { + let instance = $init_closure(); + $crate::logging!( + info, + $crate::utils::logging::Type::Setup, + true, + concat!($struct_name_str, " initialized") + ); + instance + }) + } + } + }; +} + +#[cfg(test)] +mod tests { + struct TestStruct { + value: i32, + } + + impl TestStruct { + fn new() -> Self { + Self { value: 42 } + } + } + + singleton!(TestStruct, TEST_INSTANCE); + + #[test] + fn test_singleton_macro() { + let instance1 = TestStruct::global(); + let instance2 = TestStruct::global(); + + assert_eq!(instance1.value, 42); + assert_eq!(instance2.value, 42); + assert!(std::ptr::eq(instance1, instance2)); + } +} diff --git a/clash-verge-rev/src-tauri/src/utils/window_manager.rs b/clash-verge-rev/src-tauri/src/utils/window_manager.rs index eea075c95f..d982a8e254 100644 --- a/clash-verge-rev/src-tauri/src/utils/window_manager.rs +++ b/clash-verge-rev/src-tauri/src/utils/window_manager.rs @@ -1,9 +1,12 @@ -use crate::{core::handle, logging, utils::logging::Type}; +use crate::{ + core::handle, + logging, + utils::{logging::Type, resolve::window::build_new_window}, +}; +use std::future::Future; +use std::pin::Pin; use tauri::{Manager, WebviewWindow, Wry}; -#[cfg(target_os = "macos")] -use crate::AppHandleManager; - use once_cell::sync::OnceCell; use parking_lot::Mutex; use scopeguard; @@ -21,6 +24,8 @@ pub enum WindowOperationResult { Hidden, /// 创建了新窗口 Created, + /// 摧毁了窗口 + Destroyed, /// 操作失败 Failed, /// 无需操作 @@ -118,7 +123,7 @@ impl WindowManager { } /// 智能显示主窗口 - pub fn show_main_window() -> WindowOperationResult { + pub async fn show_main_window() -> WindowOperationResult { // 防抖检查 if !should_handle_window_operation() { return WindowOperationResult::NoAction; @@ -141,7 +146,7 @@ impl WindowManager { match current_state { WindowState::NotExist => { logging!(info, Type::Window, true, "窗口不存在,创建新窗口"); - if Self::create_new_window() { + if Self::create_window(true).await { logging!(info, Type::Window, true, "窗口创建成功"); std::thread::sleep(std::time::Duration::from_millis(100)); WindowOperationResult::Created @@ -175,7 +180,7 @@ impl WindowManager { } /// 切换主窗口显示状态(显示/隐藏) - pub fn toggle_main_window() -> WindowOperationResult { + pub async fn toggle_main_window() -> WindowOperationResult { // 防抖检查 if !should_handle_window_operation() { return WindowOperationResult::NoAction; @@ -201,7 +206,7 @@ impl WindowManager { // 窗口不存在,创建新窗口 logging!(info, Type::Window, true, "窗口不存在,将创建新窗口"); // 由于已经有防抖保护,直接调用内部方法 - if Self::create_new_window() { + if Self::create_window(true).await { WindowOperationResult::Created } else { WindowOperationResult::Failed @@ -283,7 +288,7 @@ impl WindowManager { #[cfg(target_os = "macos")] { logging!(info, Type::Window, true, "应用 macOS 特定的激活策略"); - AppHandleManager::global().set_activation_policy_regular(); + handle::Handle::global().set_activation_policy_regular(); } #[cfg(target_os = "windows")] @@ -319,28 +324,6 @@ impl WindowManager { } } - /// 隐藏主窗口 - pub fn hide_main_window() -> WindowOperationResult { - logging!(info, Type::Window, true, "开始隐藏主窗口"); - - match Self::get_main_window() { - Some(window) => match window.hide() { - Ok(_) => { - logging!(info, Type::Window, true, "窗口已隐藏"); - WindowOperationResult::Hidden - } - Err(e) => { - logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e); - WindowOperationResult::Failed - } - }, - None => { - logging!(info, Type::Window, true, "窗口不存在,无需隐藏"); - WindowOperationResult::NoAction - } - } - } - /// 检查窗口是否可见 pub fn is_main_window_visible() -> bool { Self::get_main_window() @@ -363,9 +346,54 @@ impl WindowManager { } /// 创建新窗口,防抖避免重复调用 - fn create_new_window() -> bool { - use crate::utils::resolve; - resolve::create_window(true) + pub fn create_window(is_show: bool) -> Pin + Send>> { + Box::pin(async move { + logging!( + info, + Type::Window, + true, + "开始创建/显示主窗口, is_show={}", + is_show + ); + + if !is_show { + return false; + } + + match build_new_window() { + Ok(_) => { + logging!(info, Type::Window, true, "新窗口创建成功"); + } + Err(e) => { + logging!(error, Type::Window, true, "新窗口创建失败: {}", e); + return false; + } + } + + if WindowOperationResult::Failed != Self::show_main_window().await { + return false; + } + + handle::Handle::global().mark_startup_completed(); + + true + }) + } + + /// 摧毁窗口 + pub fn destroy_main_window() -> WindowOperationResult { + if let Some(window) = Self::get_main_window() { + let _ = window.destroy(); + logging!(info, Type::Window, true, "窗口已摧毁"); + #[cfg(target_os = "macos")] + { + logging!(info, Type::Window, true, "应用 macOS 特定的激活策略"); + handle::Handle::global().set_activation_policy_accessory(); + } + return WindowOperationResult::Destroyed; + } + logging!(warn, Type::Window, true, "窗口摧毁失败"); + WindowOperationResult::Failed } /// 获取详细的窗口状态信息 diff --git a/clash-verge-rev/src-tauri/tauri.conf.json b/clash-verge-rev/src-tauri/tauri.conf.json index f382c739b8..747e9909a9 100755 --- a/clash-verge-rev/src-tauri/tauri.conf.json +++ b/clash-verge-rev/src-tauri/tauri.conf.json @@ -1,5 +1,5 @@ { - "version": "2.4.0", + "version": "2.4.3", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "bundle": { "active": true, @@ -33,10 +33,7 @@ "endpoints": [ "https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json", "https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json", - "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json", - "https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json", - "https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json", - "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha.json" + "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json" ], "windows": { "installMode": "basicUi" diff --git a/clash-verge-rev/src-tauri/tauri.linux.conf.json b/clash-verge-rev/src-tauri/tauri.linux.conf.json index 398bff0339..655106b3e1 100644 --- a/clash-verge-rev/src-tauri/tauri.linux.conf.json +++ b/clash-verge-rev/src-tauri/tauri.linux.conf.json @@ -5,7 +5,7 @@ "targets": ["deb", "rpm"], "linux": { "deb": { - "depends": ["openssl"], + "depends": ["openssl", "pkexec"], "desktopTemplate": "./packages/linux/clash-verge.desktop", "provides": ["clash-verge"], "conflicts": ["clash-verge"], @@ -14,7 +14,7 @@ "preRemoveScript": "./packages/linux/pre-remove.sh" }, "rpm": { - "depends": ["openssl"], + "depends": ["openssl", "pkexec"], "desktopTemplate": "./packages/linux/clash-verge.desktop", "provides": ["clash-verge"], "conflicts": ["clash-verge"], diff --git a/clash-verge-rev/src/App.tsx b/clash-verge-rev/src/App.tsx index 3ca17dc06a..c1fe73a7d9 100644 --- a/clash-verge-rev/src/App.tsx +++ b/clash-verge-rev/src/App.tsx @@ -1,9 +1,7 @@ -import { AppDataProvider } from "./providers/app-data-provider"; import Layout from "./pages/_layout"; -import { useNotificationPermission } from "./hooks/useNotificationPermission"; +import { AppDataProvider } from "./providers/app-data-provider"; function App() { - useNotificationPermission(); return ( diff --git a/clash-verge-rev/src/components/base/NoticeManager.tsx b/clash-verge-rev/src/components/base/NoticeManager.tsx index 223447a5b2..f2ae5ef21c 100644 --- a/clash-verge-rev/src/components/base/NoticeManager.tsx +++ b/clash-verge-rev/src/components/base/NoticeManager.tsx @@ -1,6 +1,7 @@ -import React, { useSyncExternalStore } from "react"; -import { Snackbar, Alert, IconButton, Box } from "@mui/material"; import { CloseRounded } from "@mui/icons-material"; +import { Snackbar, Alert, IconButton, Box } from "@mui/material"; +import React, { useSyncExternalStore } from "react"; + import { subscribeNotices, hideNotice, diff --git a/clash-verge-rev/src/components/base/base-dialog.tsx b/clash-verge-rev/src/components/base/base-dialog.tsx index 56d90eaa47..219b5108a4 100644 --- a/clash-verge-rev/src/components/base/base-dialog.tsx +++ b/clash-verge-rev/src/components/base/base-dialog.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import { LoadingButton } from "@mui/lab"; import { Button, Dialog, @@ -8,7 +8,7 @@ import { type SxProps, type Theme, } from "@mui/material"; -import { LoadingButton } from "@mui/lab"; +import { ReactNode } from "react"; interface Props { title: ReactNode; diff --git a/clash-verge-rev/src/components/base/base-empty.tsx b/clash-verge-rev/src/components/base/base-empty.tsx index 665c12c5c2..dcf04bbb92 100644 --- a/clash-verge-rev/src/components/base/base-empty.tsx +++ b/clash-verge-rev/src/components/base/base-empty.tsx @@ -1,5 +1,5 @@ -import { alpha, Box, Typography } from "@mui/material"; import { InboxRounded } from "@mui/icons-material"; +import { alpha, Box, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; interface Props { diff --git a/clash-verge-rev/src/components/base/base-fieldset.tsx b/clash-verge-rev/src/components/base/base-fieldset.tsx index 131f6758b0..8374897d38 100644 --- a/clash-verge-rev/src/components/base/base-fieldset.tsx +++ b/clash-verge-rev/src/components/base/base-fieldset.tsx @@ -1,5 +1,5 @@ -import React from "react"; import { Box, styled } from "@mui/material"; +import React from "react"; type Props = { label: string; diff --git a/clash-verge-rev/src/components/base/base-loading-overlay.tsx b/clash-verge-rev/src/components/base/base-loading-overlay.tsx index 036250b997..98727dcb1b 100644 --- a/clash-verge-rev/src/components/base/base-loading-overlay.tsx +++ b/clash-verge-rev/src/components/base/base-loading-overlay.tsx @@ -1,7 +1,7 @@ -import React from "react"; import { Box, CircularProgress } from "@mui/material"; +import React from "react"; -export interface BaseLoadingOverlayProps { +interface BaseLoadingOverlayProps { isLoading: boolean; } @@ -29,5 +29,3 @@ export const BaseLoadingOverlay: React.FC = ({ ); }; - -export default BaseLoadingOverlay; diff --git a/clash-verge-rev/src/components/base/base-page.tsx b/clash-verge-rev/src/components/base/base-page.tsx index b1f92e2c6a..a93ad2ca66 100644 --- a/clash-verge-rev/src/components/base/base-page.tsx +++ b/clash-verge-rev/src/components/base/base-page.tsx @@ -1,7 +1,8 @@ -import React, { ReactNode } from "react"; import { Typography } from "@mui/material"; -import { BaseErrorBoundary } from "./base-error-boundary"; import { useTheme } from "@mui/material/styles"; +import React, { ReactNode } from "react"; + +import { BaseErrorBoundary } from "./base-error-boundary"; interface Props { title?: React.ReactNode; // the page title diff --git a/clash-verge-rev/src/components/base/base-search-box.tsx b/clash-verge-rev/src/components/base/base-search-box.tsx index 079c3f7399..84f0a83bb3 100644 --- a/clash-verge-rev/src/components/base/base-search-box.tsx +++ b/clash-verge-rev/src/components/base/base-search-box.tsx @@ -1,10 +1,11 @@ import { Box, SvgIcon, TextField, styled } from "@mui/material"; import Tooltip from "@mui/material/Tooltip"; import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; + import matchCaseIcon from "@/assets/image/component/match_case.svg?react"; import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react"; import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react"; -import { useTranslation } from "react-i18next"; export type SearchState = { text: string; @@ -62,6 +63,7 @@ export const BaseSearchBox = (props: SearchProps) => { new RegExp(pattern); return true; } catch (e) { + console.warn("[BaseSearchBox] validateRegex error:", e); return false; } }; @@ -80,8 +82,8 @@ export const BaseSearchBox = (props: SearchProps) => { return (content: string) => { if (!searchText) return true; - let item = !matchCase ? content.toLowerCase() : content; - let searchItem = !matchCase ? searchText.toLowerCase() : searchText; + const item = !matchCase ? content.toLowerCase() : content; + const searchItem = !matchCase ? searchText.toLowerCase() : searchText; if (useRegularExpression) { return new RegExp(searchItem).test(item); diff --git a/clash-verge-rev/src/components/base/base-tooltip-icon.tsx b/clash-verge-rev/src/components/base/base-tooltip-icon.tsx index e7028955f6..3304902331 100644 --- a/clash-verge-rev/src/components/base/base-tooltip-icon.tsx +++ b/clash-verge-rev/src/components/base/base-tooltip-icon.tsx @@ -1,10 +1,10 @@ +import { InfoRounded } from "@mui/icons-material"; import { Tooltip, IconButton, IconButtonProps, SvgIconProps, } from "@mui/material"; -import { InfoRounded } from "@mui/icons-material"; interface Props extends IconButtonProps { title?: string; diff --git a/clash-verge-rev/src/components/common/traffic-error-boundary.tsx b/clash-verge-rev/src/components/common/traffic-error-boundary.tsx index eb5f772053..ce7a3f61c2 100644 --- a/clash-verge-rev/src/components/common/traffic-error-boundary.tsx +++ b/clash-verge-rev/src/components/common/traffic-error-boundary.tsx @@ -1,10 +1,10 @@ -import React, { Component, ErrorInfo, ReactNode } from "react"; -import { Box, Typography, Button, Alert, Collapse } from "@mui/material"; import { ErrorOutlineRounded, RefreshRounded, BugReportRounded, } from "@mui/icons-material"; +import { Box, Typography, Button, Alert, Collapse } from "@mui/material"; +import React, { Component, ErrorInfo, ReactNode } from "react"; import { useTranslation } from "react-i18next"; interface Props { diff --git a/clash-verge-rev/src/components/connection/connection-detail.tsx b/clash-verge-rev/src/components/connection/connection-detail.tsx index 4059e9caeb..dd129f5c38 100644 --- a/clash-verge-rev/src/components/connection/connection-detail.tsx +++ b/clash-verge-rev/src/components/connection/connection-detail.tsx @@ -1,54 +1,53 @@ -import dayjs from "dayjs"; -import { forwardRef, useImperativeHandle, useState } from "react"; -import { useLockFn } from "ahooks"; import { Box, Button, Snackbar, useTheme } from "@mui/material"; +import { useLockFn } from "ahooks"; +import dayjs from "dayjs"; +import { t } from "i18next"; +import { useImperativeHandle, useState, type Ref } from "react"; + import { deleteConnection } from "@/services/cmds"; import parseTraffic from "@/utils/parse-traffic"; -import { t } from "i18next"; export interface ConnectionDetailRef { open: (detail: IConnectionsItem) => void; } -export const ConnectionDetail = forwardRef( - (props, ref) => { - const [open, setOpen] = useState(false); - const [detail, setDetail] = useState(null!); - const theme = useTheme(); +export function ConnectionDetail({ ref }: { ref?: Ref }) { + const [open, setOpen] = useState(false); + const [detail, setDetail] = useState(null!); + const theme = useTheme(); - useImperativeHandle(ref, () => ({ - open: (detail: IConnectionsItem) => { - if (open) return; - setOpen(true); - setDetail(detail); - }, - })); + useImperativeHandle(ref, () => ({ + open: (detail: IConnectionsItem) => { + if (open) return; + setOpen(true); + setDetail(detail); + }, + })); - const onClose = () => setOpen(false); + const onClose = () => setOpen(false); - return ( - - ) : null - } - /> - ); - }, -); + return ( + + ) : null + } + /> + ); +} interface InnerProps { data: IConnectionsItem; diff --git a/clash-verge-rev/src/components/connection/connection-item.tsx b/clash-verge-rev/src/components/connection/connection-item.tsx index 692f3bb4d2..77d2c7e94f 100644 --- a/clash-verge-rev/src/components/connection/connection-item.tsx +++ b/clash-verge-rev/src/components/connection/connection-item.tsx @@ -1,5 +1,4 @@ -import dayjs from "dayjs"; -import { useLockFn } from "ahooks"; +import { CloseRounded } from "@mui/icons-material"; import { styled, ListItem, @@ -8,7 +7,9 @@ import { Box, alpha, } from "@mui/material"; -import { CloseRounded } from "@mui/icons-material"; +import { useLockFn } from "ahooks"; +import dayjs from "dayjs"; + import { deleteConnection } from "@/services/cmds"; import parseTraffic from "@/utils/parse-traffic"; diff --git a/clash-verge-rev/src/components/connection/connection-table.tsx b/clash-verge-rev/src/components/connection/connection-table.tsx index a1f4b52a4d..97f65f089b 100644 --- a/clash-verge-rev/src/components/connection/connection-table.tsx +++ b/clash-verge-rev/src/components/connection/connection-table.tsx @@ -1,11 +1,11 @@ -import dayjs from "dayjs"; -import { useMemo, useState } from "react"; import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid"; -import { useThemeMode } from "@/services/states"; -import { truncateStr } from "@/utils/truncate-str"; -import parseTraffic from "@/utils/parse-traffic"; -import { t } from "i18next"; +import dayjs from "dayjs"; import { useLocalStorage } from "foxact/use-local-storage"; +import { t } from "i18next"; +import { useMemo, useState } from "react"; + +import parseTraffic from "@/utils/parse-traffic"; +import { truncateStr } from "@/utils/truncate-str"; interface Props { connections: IConnectionsItem[]; @@ -14,9 +14,6 @@ interface Props { export const ConnectionTable = (props: Props) => { const { connections, onShowDetail } = props; - const mode = useThemeMode(); - const isDark = mode === "light" ? false : true; - const backgroundColor = isDark ? "#282A36" : "#ffffff"; const [columnVisible, setColumnVisible] = useState< Partial> diff --git a/clash-verge-rev/src/components/home/clash-info-card.tsx b/clash-verge-rev/src/components/home/clash-info-card.tsx index dcdd22a8e5..a3ca1f75d6 100644 --- a/clash-verge-rev/src/components/home/clash-info-card.tsx +++ b/clash-verge-rev/src/components/home/clash-info-card.tsx @@ -1,10 +1,12 @@ -import { useTranslation } from "react-i18next"; -import { Typography, Stack, Divider } from "@mui/material"; import { DeveloperBoardOutlined } from "@mui/icons-material"; -import { useClash } from "@/hooks/use-clash"; -import { EnhancedCard } from "./enhanced-card"; +import { Divider, Stack, Typography } from "@mui/material"; import { useMemo } from "react"; -import { useAppData } from "@/providers/app-data-provider"; +import { useTranslation } from "react-i18next"; + +import { useClash } from "@/hooks/use-clash"; +import { useAppData } from "@/providers/app-data-context"; + +import { EnhancedCard } from "./enhanced-card"; // 将毫秒转换为时:分:秒格式的函数 const formatUptime = (uptimeMs: number) => { diff --git a/clash-verge-rev/src/components/home/clash-mode-card.tsx b/clash-verge-rev/src/components/home/clash-mode-card.tsx index 35740fd5fc..bceaadb4f9 100644 --- a/clash-verge-rev/src/components/home/clash-mode-card.tsx +++ b/clash-verge-rev/src/components/home/clash-mode-card.tsx @@ -1,16 +1,16 @@ -import { useTranslation } from "react-i18next"; -import { Box, Typography, Paper, Stack } from "@mui/material"; -import { useLockFn } from "ahooks"; -import { closeAllConnections } from "@/services/cmds"; -import { patchClashMode } from "@/services/cmds"; -import { useVerge } from "@/hooks/use-verge"; import { + DirectionsRounded, LanguageRounded, MultipleStopRounded, - DirectionsRounded, } from "@mui/icons-material"; +import { Box, Paper, Stack, Typography } from "@mui/material"; +import { useLockFn } from "ahooks"; import { useMemo } from "react"; -import { useAppData } from "@/providers/app-data-provider"; +import { useTranslation } from "react-i18next"; + +import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-context"; +import { closeAllConnections, patchClashMode } from "@/services/cmds"; export const ClashModeCard = () => { const { t } = useTranslation(); diff --git a/clash-verge-rev/src/components/home/current-proxy-card.tsx b/clash-verge-rev/src/components/home/current-proxy-card.tsx index 23622862a3..b96dbb0525 100644 --- a/clash-verge-rev/src/components/home/current-proxy-card.tsx +++ b/clash-verge-rev/src/components/home/current-proxy-card.tsx @@ -1,38 +1,38 @@ -import { useTranslation } from "react-i18next"; import { - Box, - Typography, - Chip, - Button, - alpha, - useTheme, - Select, - MenuItem, - FormControl, - InputLabel, - SelectChangeEvent, - Tooltip, - IconButton, -} from "@mui/material"; -import { useEffect, useState, useMemo, useCallback } from "react"; -import { - SignalWifi4Bar as SignalStrong, + AccessTimeRounded, + ChevronRight, + WifiOff as SignalError, SignalWifi3Bar as SignalGood, SignalWifi2Bar as SignalMedium, - SignalWifi1Bar as SignalWeak, SignalWifi0Bar as SignalNone, - WifiOff as SignalError, - ChevronRight, - SortRounded, - AccessTimeRounded, + SignalWifi4Bar as SignalStrong, + SignalWifi1Bar as SignalWeak, SortByAlphaRounded, + SortRounded, } from "@mui/icons-material"; +import { + Box, + Button, + Chip, + FormControl, + IconButton, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, + Tooltip, + Typography, + alpha, + useTheme, +} from "@mui/material"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; + import { EnhancedCard } from "@/components/home/enhanced-card"; -import { updateProxy, deleteConnection } from "@/services/cmds"; +import { useProxySelection } from "@/hooks/use-proxy-selection"; +import { useAppData } from "@/providers/app-data-context"; import delayManager from "@/services/delay"; -import { useVerge } from "@/hooks/use-verge"; -import { useAppData } from "@/providers/app-data-provider"; // 本地存储的键名 const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group"; @@ -45,7 +45,7 @@ interface ProxyOption { } // 排序类型: 默认 | 按延迟 | 按字母 -export type ProxySortType = 0 | 1 | 2; +type ProxySortType = 0 | 1 | 2; function convertDelayColor(delayValue: number) { const colorStr = delayManager.formatDelayColor(delayValue); @@ -94,8 +94,18 @@ export const CurrentProxyCard = () => { const { t } = useTranslation(); const navigate = useNavigate(); const theme = useTheme(); - const { verge } = useVerge(); - const { proxies, connections, clashConfig, refreshProxy } = useAppData(); + const { proxies, clashConfig, refreshProxy } = useAppData(); + + // 统一代理选择器 + const { handleSelectChange } = useProxySelection({ + onSuccess: () => { + refreshProxy(); + }, + onError: (error) => { + console.error("代理切换失败", error); + refreshProxy(); + }, + }); // 判断模式 const mode = clashConfig?.mode?.toLowerCase() || "rule"; @@ -113,8 +123,6 @@ export const CurrentProxyCard = () => { proxyData: { groups: { name: string; now: string; all: string[] }[]; records: Record; - globalProxy: string; - directProxy: any; }; selection: { group: string; @@ -127,8 +135,6 @@ export const CurrentProxyCard = () => { proxyData: { groups: [], records: {}, - globalProxy: "", - directProxy: { name: "DIRECT" }, // 默认值避免 undefined }, selection: { group: "", @@ -253,8 +259,6 @@ export const CurrentProxyCard = () => { proxyData: { groups: filteredGroups, records: proxies.records || {}, - globalProxy: proxies.global?.now || "", - directProxy: proxies.records?.DIRECT || { name: "DIRECT" }, }, selection: { group: newGroup, @@ -310,7 +314,7 @@ export const CurrentProxyCard = () => { // 处理代理节点变更 const handleProxyChange = useCallback( - async (event: SelectChangeEvent) => { + (event: SelectChangeEvent) => { if (isDirectMode) return; const newProxy = event.target.value; @@ -330,35 +334,15 @@ export const CurrentProxyCard = () => { localStorage.setItem(STORAGE_KEY_PROXY, newProxy); } - try { - await updateProxy(currentGroup, newProxy); - - // 自动关闭连接设置 - if (verge?.auto_close_connection && previousProxy) { - connections.data.forEach((conn: any) => { - if (conn.chains.includes(previousProxy)) { - deleteConnection(conn.id); - } - }); - } - - // 延长刷新延迟时间 - setTimeout(() => { - refreshProxy(); - }, 500); - } catch (error) { - console.error("更新代理失败", error); - } + const skipConfigSave = isGlobalMode || isDirectMode; + handleSelectChange(currentGroup, previousProxy, skipConfigSave)(event); }, [ isDirectMode, isGlobalMode, - state.proxyData.records, state.selection, - verge?.auto_close_connection, - refreshProxy, debouncedSetState, - connections.data, + handleSelectChange, ], ); diff --git a/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx b/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx index 7528a5508a..47a9c92495 100644 --- a/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx +++ b/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx @@ -1,22 +1,24 @@ +import { Box, useTheme } from "@mui/material"; +import type { Ref } from "react"; import { - forwardRef, - useImperativeHandle, - useState, - useEffect, + memo, useCallback, + useEffect, + useImperativeHandle, useMemo, useRef, - memo, + useState, } from "react"; -import { Box, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; + import { useTrafficGraphDataEnhanced, type ITrafficDataPoint, -} from "@/hooks/use-traffic-monitor-enhanced"; +} from "@/hooks/use-traffic-monitor"; +import parseTraffic from "@/utils/parse-traffic"; // 流量数据项接口 -export interface ITrafficItem { +interface ITrafficItem { up: number; down: number; timestamp?: number; @@ -30,7 +32,18 @@ export interface EnhancedCanvasTrafficGraphRef { type TimeRange = 1 | 5 | 10; // 分钟 -// Canvas图表配置 +// 悬浮提示数据接口 +interface TooltipData { + x: number; + y: number; + upSpeed: string; + downSpeed: string; + timestamp: string; + visible: boolean; + dataIndex: number; // 添加数据索引用于高亮 + highlightY: number; // 高亮Y轴位置 +} + const MAX_POINTS = 300; const TARGET_FPS = 15; // 降低帧率减少闪烁 const LINE_WIDTH_UP = 2.5; @@ -41,7 +54,7 @@ const ALPHA_LINE = 0.9; const PADDING_TOP = 16; const PADDING_RIGHT = 16; // 增加右边距确保时间戳完整显示 const PADDING_BOTTOM = 32; // 进一步增加底部空间给时间轴和统计信息 -const PADDING_LEFT = 16; // 增加左边距确保时间戳完整显示 +const PADDING_LEFT = 35; // 增加左边距为Y轴标签留出空间 const GRAPH_CONFIG = { maxPoints: MAX_POINTS, @@ -63,12 +76,18 @@ const GRAPH_CONFIG = { }, }; +interface EnhancedCanvasTrafficGraphProps { + ref?: Ref; +} + /** * 稳定版Canvas流量图表组件 * 修复闪烁问题,添加时间轴显示 */ export const EnhancedCanvasTrafficGraph = memo( - forwardRef((props, ref) => { + function EnhancedCanvasTrafficGraph({ + ref, + }: EnhancedCanvasTrafficGraphProps) { const theme = useTheme(); const { t } = useTranslation(); @@ -80,6 +99,18 @@ export const EnhancedCanvasTrafficGraph = memo( const [timeRange, setTimeRange] = useState(10); const [chartStyle, setChartStyle] = useState<"bezier" | "line">("bezier"); + // 悬浮提示状态 + const [tooltipData, setTooltipData] = useState({ + x: 0, + y: 0, + upSpeed: "", + downSpeed: "", + timestamp: "", + visible: false, + dataIndex: -1, + highlightY: 0, + }); + // Canvas引用和渲染状态 const canvasRef = useRef(null); const animationFrameRef = useRef(undefined); @@ -101,13 +132,6 @@ export const EnhancedCanvasTrafficGraph = memo( [theme], ); - // 根据时间范围获取数据点数量 - const getPointsForTimeRange = useCallback( - (minutes: TimeRange): number => - Math.min(minutes * 60, GRAPH_CONFIG.maxPoints), - [], - ); - // 更新显示数据(防抖处理) const updateDisplayDataDebounced = useMemo(() => { let timeoutId: number; @@ -130,27 +154,303 @@ export const EnhancedCanvasTrafficGraph = memo( updateDisplayDataDebounced, ]); - // Y轴坐标计算(对数刻度)- 确保不与时间轴重叠 - const calculateY = useCallback((value: number, height: number): number => { - const padding = GRAPH_CONFIG.padding; - const effectiveHeight = height - padding.top - padding.bottom; - const baseY = height - padding.bottom; + // Y轴坐标计算 - 基于刻度范围的线性映射 + const calculateY = useCallback( + (value: number, height: number, data: ITrafficDataPoint[]): number => { + const padding = GRAPH_CONFIG.padding; + const topY = padding.top + 10; // 与刻度系统保持一致 + const bottomY = height - padding.bottom - 5; - if (value === 0) return baseY - 2; // 稍微抬高零值线 + if (data.length === 0) return bottomY; - const steps = effectiveHeight / 7; + // 获取当前的刻度范围 + const allValues = [ + ...data.map((d) => d.up), + ...data.map((d) => d.down), + ]; + const maxValue = Math.max(...allValues); + const minValue = Math.min(...allValues); - if (value <= 10) return baseY - (value / 10) * steps; - if (value <= 100) return baseY - (value / 100 + 1) * steps; - if (value <= 1024) return baseY - (value / 1024 + 2) * steps; - if (value <= 10240) return baseY - (value / 10240 + 3) * steps; - if (value <= 102400) return baseY - (value / 102400 + 4) * steps; - if (value <= 1048576) return baseY - (value / 1048576 + 5) * steps; - if (value <= 10485760) return baseY - (value / 10485760 + 6) * steps; + let topValue, bottomValue; - return padding.top + 1; + if (maxValue === 0) { + topValue = 1024; + bottomValue = 0; + } else { + const range = maxValue - minValue; + const padding_percent = range > 0 ? 0.1 : 0.5; + + if (range === 0) { + bottomValue = 0; + topValue = maxValue * 1.2; + } else { + bottomValue = Math.max(0, minValue - range * padding_percent); + topValue = maxValue + range * padding_percent; + } + } + + // 线性映射到Y坐标 + if (topValue === bottomValue) return bottomY; + + const ratio = (value - bottomValue) / (topValue - bottomValue); + return bottomY - ratio * (bottomY - topY); + }, + [], + ); + + // 鼠标悬浮处理 - 计算最近的数据点 + const handleMouseMove = useCallback( + (event: React.MouseEvent) => { + const canvas = canvasRef.current; + if (!canvas || displayData.length === 0) return; + + const rect = canvas.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; + const mouseY = event.clientY - rect.top; + + const padding = GRAPH_CONFIG.padding; + const effectiveWidth = rect.width - padding.left - padding.right; + + // 计算最接近的数据点索引 + const relativeMouseX = mouseX - padding.left; + const ratio = Math.max(0, Math.min(1, relativeMouseX / effectiveWidth)); + const dataIndex = Math.round(ratio * (displayData.length - 1)); + + if (dataIndex >= 0 && dataIndex < displayData.length) { + const dataPoint = displayData[dataIndex]; + + // 格式化流量数据 + const [upValue, upUnit] = parseTraffic(dataPoint.up); + const [downValue, downUnit] = parseTraffic(dataPoint.down); + + // 格式化时间戳 + const timeStr = dataPoint.timestamp + ? new Date(dataPoint.timestamp).toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }) + : "未知时间"; + + // 计算数据点对应的Y坐标位置(用于高亮) + const upY = calculateY(dataPoint.up, rect.height, displayData); + const downY = calculateY(dataPoint.down, rect.height, displayData); + const highlightY = + Math.max(dataPoint.up, dataPoint.down) === dataPoint.up + ? upY + : downY; + + setTooltipData({ + x: mouseX, + y: mouseY, + upSpeed: `${upValue}${upUnit}/s`, + downSpeed: `${downValue}${downUnit}/s`, + timestamp: timeStr, + visible: true, + dataIndex, + highlightY, + }); + } + }, + [displayData, calculateY], + ); + + // 鼠标离开处理 + const handleMouseLeave = useCallback(() => { + setTooltipData((prev) => ({ ...prev, visible: false })); }, []); + // 获取智能Y轴刻度(三刻度系统:最小值、中间值、最大值) + const getYAxisTicks = useCallback( + (data: ITrafficDataPoint[], height: number) => { + if (data.length === 0) return []; + + // 找到数据的最大值和最小值 + const allValues = [ + ...data.map((d) => d.up), + ...data.map((d) => d.down), + ]; + const maxValue = Math.max(...allValues); + const minValue = Math.min(...allValues); + + // 格式化流量数值 + const formatTrafficValue = (bytes: number): string => { + if (bytes === 0) return "0"; + if (bytes < 1024) return `${Math.round(bytes)}B`; + if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; + }; + + const padding = GRAPH_CONFIG.padding; + + // 强制显示三个刻度:底部、中间、顶部 + const topY = padding.top + 10; // 避免与顶部时间范围按钮重叠 + const bottomY = height - padding.bottom - 5; // 避免与底部时间轴重叠 + const middleY = (topY + bottomY) / 2; + + // 计算对应的值 + let topValue, middleValue, bottomValue; + + if (maxValue === 0) { + // 如果没有流量,显示0到一个小值的范围 + topValue = 1024; // 1KB + middleValue = 512; // 512B + bottomValue = 0; + } else { + // 根据数据范围计算合适的刻度值 + const range = maxValue - minValue; + const padding_percent = range > 0 ? 0.1 : 0.5; // 如果范围为0,使用更大的边距 + + if (range === 0) { + // 所有值相同的情况 + bottomValue = 0; + middleValue = maxValue * 0.5; + topValue = maxValue * 1.2; + } else { + // 正常情况 + bottomValue = Math.max(0, minValue - range * padding_percent); + topValue = maxValue + range * padding_percent; + middleValue = (bottomValue + topValue) / 2; + } + } + + // 创建三个固定位置的刻度 + const ticks = [ + { + value: bottomValue, + label: formatTrafficValue(bottomValue), + y: bottomY, + }, + { + value: middleValue, + label: formatTrafficValue(middleValue), + y: middleY, + }, + { + value: topValue, + label: formatTrafficValue(topValue), + y: topY, + }, + ]; + + return ticks; + }, + [], + ); + + // 绘制Y轴刻度线和标签 + const drawYAxis = useCallback( + ( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + data: ITrafficDataPoint[], + ) => { + const padding = GRAPH_CONFIG.padding; + const ticks = getYAxisTicks(data, height); + + if (ticks.length === 0) return; + + ctx.save(); + + ticks.forEach((tick, index) => { + const isBottomTick = index === 0; // 最底部的刻度 + const isTopTick = index === ticks.length - 1; // 最顶部的刻度 + + // 绘制水平刻度线,只绘制关键刻度线 + if (isBottomTick || isTopTick) { + ctx.strokeStyle = colors.grid; + ctx.lineWidth = isBottomTick ? 0.8 : 0.4; // 底部刻度线稍粗 + ctx.globalAlpha = isBottomTick ? 0.25 : 0.15; + + ctx.beginPath(); + ctx.moveTo(padding.left, tick.y); + ctx.lineTo(width - padding.right, tick.y); + ctx.stroke(); + } + + // 绘制Y轴标签 + ctx.fillStyle = colors.text; + ctx.font = + "8px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif"; + ctx.globalAlpha = 0.9; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + + // 为标签添加更清晰的背景(仅在必要时) + if (tick.label !== "0") { + const labelWidth = ctx.measureText(tick.label).width; + ctx.globalAlpha = 0.15; + ctx.fillStyle = colors.background; + ctx.fillRect( + padding.left - labelWidth - 8, + tick.y - 5, + labelWidth + 4, + 10, + ); + } + + // 绘制标签文字 + ctx.globalAlpha = 0.9; + ctx.fillStyle = colors.text; + ctx.fillText(tick.label, padding.left - 4, tick.y); + }); + + ctx.restore(); + }, + [colors.grid, colors.text, colors.background, getYAxisTicks], + ); + + // 获取时间范围对应的最佳时间显示策略 + const getTimeDisplayStrategy = useCallback( + (timeRangeMinutes: TimeRange) => { + switch (timeRangeMinutes) { + case 1: // 1分钟:更密集的时间标签,显示 MM:SS + return { + maxLabels: 6, // 减少到6个,更适合短时间 + formatTime: (timestamp: number) => { + const date = new Date(timestamp); + const minutes = date.getMinutes().toString().padStart(2, "0"); + const seconds = date.getSeconds().toString().padStart(2, "0"); + return `${minutes}:${seconds}`; // 显示 MM:SS + }, + intervalSeconds: 10, // 每10秒一个标签,更合理 + minPixelDistance: 35, // 减少间距,允许更多标签 + }; + case 5: // 5分钟:中等密度,显示 HH:MM + return { + maxLabels: 6, // 6个标签比较合适 + formatTime: (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + }); // 显示 HH:MM + }, + intervalSeconds: 30, // 约30秒间隔 + minPixelDistance: 38, // 减少间距,允许更多标签 + }; + case 10: // 10分钟:标准密度,显示 HH:MM + default: + return { + maxLabels: 8, // 保持8个 + formatTime: (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + }); // 显示 HH:MM + }, + intervalSeconds: 60, // 1分钟间隔 + minPixelDistance: 40, // 减少间距,允许更多标签 + }; + } + }, + [], + ); + // 绘制时间轴 const drawTimeAxis = useCallback( ( @@ -165,44 +465,89 @@ export const EnhancedCanvasTrafficGraph = memo( const effectiveWidth = width - padding.left - padding.right; const timeAxisY = height - padding.bottom + 14; + const strategy = getTimeDisplayStrategy(timeRange); + ctx.save(); ctx.fillStyle = colors.text; ctx.font = "10px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif"; ctx.globalAlpha = 0.7; - // 显示最多6个时间标签,确保边界完整显示 - const maxLabels = 6; - const step = Math.max(1, Math.floor(data.length / (maxLabels - 1))); + // 根据数据长度和时间范围智能选择显示间隔 + const targetLabels = Math.min(strategy.maxLabels, data.length); + const step = Math.max(1, Math.floor(data.length / (targetLabels - 1))); - // 绘制第一个时间点(左对齐) - if (data.length > 0 && data[0].name) { - ctx.textAlign = "left"; - const timeLabel = data[0].name.substring(0, 5); - ctx.fillText(timeLabel, padding.left, timeAxisY); + // 使用策略中定义的最小像素间距 + const minPixelDistance = strategy.minPixelDistance || 45; + const actualStep = Math.max( + step, + Math.ceil((data.length * minPixelDistance) / effectiveWidth), + ); + + // 收集要显示的时间点 + const timePoints: Array<{ index: number; x: number; label: string }> = + []; + + // 添加第一个时间点 + if (data.length > 0 && data[0].timestamp) { + timePoints.push({ + index: 0, + x: padding.left, + label: strategy.formatTime(data[0].timestamp), + }); } - // 绘制中间的时间点(居中对齐) - ctx.textAlign = "center"; - for (let i = step; i < data.length - step; i += step) { + // 添加中间的时间点 + for ( + let i = actualStep; + i < data.length - actualStep; + i += actualStep + ) { const point = data[i]; - if (!point.name) continue; + if (!point.timestamp) continue; const x = padding.left + (i / (data.length - 1)) * effectiveWidth; - const timeLabel = point.name.substring(0, 5); - ctx.fillText(timeLabel, x, timeAxisY); + timePoints.push({ + index: i, + x, + label: strategy.formatTime(point.timestamp), + }); } - // 绘制最后一个时间点(右对齐) - if (data.length > 1 && data[data.length - 1].name) { - ctx.textAlign = "right"; - const timeLabel = data[data.length - 1].name.substring(0, 5); - ctx.fillText(timeLabel, width - padding.right, timeAxisY); + // 添加最后一个时间点(如果不会与前面的重叠) + if (data.length > 1 && data[data.length - 1].timestamp) { + const lastX = width - padding.right; + const lastPoint = timePoints[timePoints.length - 1]; + + // 确保最后一个标签与前一个标签有足够间距 + if (!lastPoint || lastX - lastPoint.x >= minPixelDistance) { + timePoints.push({ + index: data.length - 1, + x: lastX, + label: strategy.formatTime(data[data.length - 1].timestamp), + }); + } } + // 绘制时间标签 + timePoints.forEach((point, index) => { + if (index === 0) { + // 第一个标签左对齐 + ctx.textAlign = "left"; + } else if (index === timePoints.length - 1) { + // 最后一个标签右对齐 + ctx.textAlign = "right"; + } else { + // 中间标签居中对齐 + ctx.textAlign = "center"; + } + + ctx.fillText(point.label, point.x, timeAxisY); + }); + ctx.restore(); }, - [colors.text], + [colors.text, timeRange, getTimeDisplayStrategy], ); // 绘制网格线 @@ -215,7 +560,7 @@ export const EnhancedCanvasTrafficGraph = memo( ctx.save(); ctx.strokeStyle = colors.grid; ctx.lineWidth = GRAPH_CONFIG.lineWidth.grid; - ctx.globalAlpha = 0.2; + ctx.globalAlpha = 0.7; // 水平网格线 const horizontalLines = 4; @@ -251,6 +596,7 @@ export const EnhancedCanvasTrafficGraph = memo( height: number, color: string, withGradient = false, + data: ITrafficDataPoint[], ) => { if (values.length < 2) return; @@ -259,7 +605,7 @@ export const EnhancedCanvasTrafficGraph = memo( const points = values.map((value, index) => [ padding.left + (index / (values.length - 1)) * effectiveWidth, - calculateY(value, height), + calculateY(value, height, data), ]); ctx.save(); @@ -360,6 +706,9 @@ export const EnhancedCanvasTrafficGraph = memo( // 清空画布 ctx.clearRect(0, 0, width, height); + // 绘制Y轴刻度线(背景层) + drawYAxis(ctx, width, height, displayData); + // 绘制网格 drawGrid(ctx, width, height); @@ -371,13 +720,66 @@ export const EnhancedCanvasTrafficGraph = memo( const downValues = displayData.map((d) => d.down); // 绘制下载线(背景层) - drawTrafficLine(ctx, downValues, width, height, colors.down, true); + drawTrafficLine( + ctx, + downValues, + width, + height, + colors.down, + true, + displayData, + ); // 绘制上传线(前景层) - drawTrafficLine(ctx, upValues, width, height, colors.up, true); + drawTrafficLine( + ctx, + upValues, + width, + height, + colors.up, + true, + displayData, + ); + + // 绘制悬浮高亮线 + if (tooltipData.visible && tooltipData.dataIndex >= 0) { + const padding = GRAPH_CONFIG.padding; + const effectiveWidth = width - padding.left - padding.right; + const dataX = + padding.left + + (tooltipData.dataIndex / (displayData.length - 1)) * effectiveWidth; + + ctx.save(); + ctx.strokeStyle = colors.text; + ctx.lineWidth = 1; + ctx.globalAlpha = 0.6; + ctx.setLineDash([4, 4]); // 虚线效果 + + // 绘制垂直指示线 + ctx.beginPath(); + ctx.moveTo(dataX, padding.top); + ctx.lineTo(dataX, height - padding.bottom); + ctx.stroke(); + + // 绘制水平指示线(高亮Y轴位置) + ctx.beginPath(); + ctx.moveTo(padding.left, tooltipData.highlightY); + ctx.lineTo(width - padding.right, tooltipData.highlightY); + ctx.stroke(); + + ctx.restore(); + } isInitializedRef.current = true; - }, [displayData, colors, drawGrid, drawTimeAxis, drawTrafficLine]); + }, [ + displayData, + colors, + drawYAxis, + drawGrid, + drawTimeAxis, + drawTrafficLine, + tooltipData, + ]); // 受控的动画循环 useEffect(() => { @@ -461,6 +863,8 @@ export const EnhancedCanvasTrafficGraph = memo( height: "100%", display: "block", }} + onMouseMove={handleMouseMove} + onMouseLeave={handleMouseLeave} /> {/* 控制层覆盖 */} @@ -481,7 +885,7 @@ export const EnhancedCanvasTrafficGraph = memo( sx={{ position: "absolute", top: 6, - left: 8, + left: 40, // 向右移动,避免与Y轴最大值标签重叠 fontSize: "11px", fontWeight: "bold", color: "text.secondary", @@ -561,10 +965,46 @@ export const EnhancedCanvasTrafficGraph = memo( Points: {displayData.length} | Fresh: {isDataFresh ? "✓" : "✗"} | Compressed: {samplerStats.compressedBufferSize} + + {/* 悬浮提示框 */} + {tooltipData.visible && ( + 200 ? "translateX(-100%)" : "translateX(0)", + boxShadow: "0 4px 12px rgba(0,0,0,0.15)", + backdropFilter: "none", + opacity: 1, + }} + > + + {tooltipData.timestamp} + + + ↑ {tooltipData.upSpeed} + + + ↓ {tooltipData.downSpeed} + + + )} ); - }), + }, ); EnhancedCanvasTrafficGraph.displayName = "EnhancedCanvasTrafficGraph"; diff --git a/clash-verge-rev/src/components/home/enhanced-card.tsx b/clash-verge-rev/src/components/home/enhanced-card.tsx index 68548a5411..59203c6619 100644 --- a/clash-verge-rev/src/components/home/enhanced-card.tsx +++ b/clash-verge-rev/src/components/home/enhanced-card.tsx @@ -2,7 +2,7 @@ import { Box, Typography, alpha, useTheme } from "@mui/material"; import { ReactNode } from "react"; // 自定义卡片组件接口 -export interface EnhancedCardProps { +interface EnhancedCardProps { title: ReactNode; icon: ReactNode; action?: ReactNode; diff --git a/clash-verge-rev/src/components/home/enhanced-traffic-stats.tsx b/clash-verge-rev/src/components/home/enhanced-traffic-stats.tsx index 714eaa19df..9bf97200a7 100644 --- a/clash-verge-rev/src/components/home/enhanced-traffic-stats.tsx +++ b/clash-verge-rev/src/components/home/enhanced-traffic-stats.tsx @@ -1,48 +1,36 @@ -import { useState, useEffect, useRef, useCallback, memo, useMemo } from "react"; -import { useTranslation } from "react-i18next"; import { - Typography, + ArrowDownwardRounded, + ArrowUpwardRounded, + CloudDownloadRounded, + CloudUploadRounded, + LinkRounded, + MemoryRounded, +} from "@mui/icons-material"; +import { + Box, + Grid, + PaletteColor, Paper, + Typography, alpha, useTheme, - PaletteColor, - Grid, - Box, } from "@mui/material"; -import { - ArrowUpwardRounded, - ArrowDownwardRounded, - MemoryRounded, - LinkRounded, - CloudUploadRounded, - CloudDownloadRounded, -} from "@mui/icons-material"; +import { ReactNode, memo, useCallback, useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; + +import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary"; +import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor"; +import { useVerge } from "@/hooks/use-verge"; +import { useVisibility } from "@/hooks/use-visibility"; +import { useAppData } from "@/providers/app-data-context"; +import { gc, isDebugEnabled } from "@/services/cmds"; +import parseTraffic from "@/utils/parse-traffic"; + import { EnhancedCanvasTrafficGraph, type EnhancedCanvasTrafficGraphRef, - type ITrafficItem, } from "./enhanced-canvas-traffic-graph"; -import { useVisibility } from "@/hooks/use-visibility"; -import { useClashInfo } from "@/hooks/use-clash"; -import { useVerge } from "@/hooks/use-verge"; -import parseTraffic from "@/utils/parse-traffic"; -import { isDebugEnabled, gc } from "@/services/cmds"; -import { ReactNode } from "react"; -import { useAppData } from "@/providers/app-data-provider"; -import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor-enhanced"; -import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary"; -import useSWR from "swr"; - -interface MemoryUsage { - inuse: number; - oslimit?: number; -} - -interface TrafficStatData { - uploadTotal: number; - downloadTotal: number; - activeConnections: number; -} interface StatCardProps { icon: ReactNode; @@ -64,9 +52,6 @@ declare global { } } -// 控制更新频率 -const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据 - // 统计卡片组件 - 使用memo优化 const CompactStatCard = memo( ({ icon, title, value, unit, color, onClick }: StatCardProps) => { @@ -159,13 +144,12 @@ CompactStatCard.displayName = "CompactStatCard"; export const EnhancedTrafficStats = () => { const { t } = useTranslation(); const theme = useTheme(); - const { clashInfo } = useClashInfo(); const { verge } = useVerge(); const trafficRef = useRef(null); const pageVisible = useVisibility(); // 使用AppDataProvider - const { connections, uptime } = useAppData(); + const { connections } = useAppData(); // 使用增强版的统一流量数据Hook const { traffic, memory, isLoading, isDataFresh, hasValidData } = @@ -258,7 +242,7 @@ export const EnhancedTrafficStats = () => { borderRadius: "4px", }} > - DEBUG: {!!trafficRef.current ? "图表已初始化" : "图表未初始化"} + DEBUG: {trafficRef.current ? "图表已初始化" : "图表未初始化"}
状态: {isDataFresh ? "active" : "inactive"}
diff --git a/clash-verge-rev/src/components/home/home-profile-card.tsx b/clash-verge-rev/src/components/home/home-profile-card.tsx index 183c43884d..14f7719280 100644 --- a/clash-verge-rev/src/components/home/home-profile-card.tsx +++ b/clash-verge-rev/src/components/home/home-profile-card.tsx @@ -1,33 +1,35 @@ -import { useTranslation } from "react-i18next"; -import { - Box, - Typography, - Button, - Stack, - LinearProgress, - alpha, - useTheme, - Link, - keyframes, -} from "@mui/material"; -import { useNavigate } from "react-router-dom"; import { CloudUploadOutlined, - StorageOutlined, - UpdateOutlined, DnsOutlined, - SpeedOutlined, EventOutlined, LaunchOutlined, + SpeedOutlined, + StorageOutlined, + UpdateOutlined, } from "@mui/icons-material"; -import dayjs from "dayjs"; -import parseTraffic from "@/utils/parse-traffic"; -import { useMemo, useCallback, useState } from "react"; -import { openWebUrl, updateProfile } from "@/services/cmds"; +import { + Box, + Button, + LinearProgress, + Link, + Stack, + Typography, + alpha, + keyframes, + useTheme, +} from "@mui/material"; import { useLockFn } from "ahooks"; +import dayjs from "dayjs"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; + +import { useAppData } from "@/providers/app-data-context"; +import { openWebUrl, updateProfile } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; +import parseTraffic from "@/utils/parse-traffic"; + import { EnhancedCard } from "./enhanced-card"; -import { useAppData } from "@/providers/app-data-provider"; // 定义旋转动画 const round = keyframes` @@ -55,7 +57,7 @@ interface ProfileExtra { expire: number; } -export interface ProfileItem { +interface ProfileItem { uid: string; type?: "local" | "remote" | "merge" | "script"; name?: string; @@ -68,19 +70,11 @@ export interface ProfileItem { option?: any; } -export interface HomeProfileCardProps { +interface HomeProfileCardProps { current: ProfileItem | null | undefined; onProfileUpdated?: () => void; } -// 添加一个通用的截断样式 -const truncateStyle = { - maxWidth: "calc(100% - 28px)", - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", -}; - // 提取独立组件减少主组件复杂度 const ProfileDetails = ({ current, diff --git a/clash-verge-rev/src/components/home/ip-info-card.tsx b/clash-verge-rev/src/components/home/ip-info-card.tsx index 4c5231376c..585460dc6a 100644 --- a/clash-verge-rev/src/components/home/ip-info-card.tsx +++ b/clash-verge-rev/src/components/home/ip-info-card.tsx @@ -1,14 +1,16 @@ -import { useTranslation } from "react-i18next"; -import { Box, Typography, Button, Skeleton, IconButton } from "@mui/material"; import { LocationOnOutlined, RefreshOutlined, VisibilityOutlined, VisibilityOffOutlined, } from "@mui/icons-material"; -import { EnhancedCard } from "./enhanced-card"; -import { getIpInfo } from "@/services/api"; +import { Box, Typography, Button, Skeleton, IconButton } from "@mui/material"; import { useState, useEffect, useCallback, memo } from "react"; +import { useTranslation } from "react-i18next"; + +import { getIpInfo } from "@/services/api"; + +import { EnhancedCard } from "./enhanced-card"; // 定义刷新时间(秒) const IP_REFRESH_SECONDS = 300; diff --git a/clash-verge-rev/src/components/home/proxy-tun-card.tsx b/clash-verge-rev/src/components/home/proxy-tun-card.tsx index 6dcba565af..60c218a7a1 100644 --- a/clash-verge-rev/src/components/home/proxy-tun-card.tsx +++ b/clash-verge-rev/src/components/home/proxy-tun-card.tsx @@ -1,4 +1,9 @@ -import { useTranslation } from "react-i18next"; +import { + ComputerRounded, + TroubleshootRounded, + HelpOutlineRounded, + SvgIconComponent, +} from "@mui/icons-material"; import { Box, Typography, @@ -9,20 +14,14 @@ import { useTheme, Fade, } from "@mui/material"; -import { useState, useMemo, memo, FC, useEffect } from "react"; +import { useState, useMemo, memo, FC } from "react"; +import { useTranslation } from "react-i18next"; + import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches"; -import { - ComputerRounded, - TroubleshootRounded, - HelpOutlineRounded, - SvgIconComponent, -} from "@mui/icons-material"; -import { useVerge } from "@/hooks/use-verge"; -import { useSystemState } from "@/hooks/use-system-state"; import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; +import { useSystemState } from "@/hooks/use-system-state"; +import { useVerge } from "@/hooks/use-verge"; import { showNotice } from "@/services/noticeService"; -import { getRunningMode } from "@/services/cmds"; -import { mutate } from "swr"; const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab"; @@ -142,31 +141,12 @@ export const ProxyTunCard: FC = () => { () => localStorage.getItem(LOCAL_STORAGE_TAB_KEY) || "system", ); - const [localServiceOk, setLocalServiceOk] = useState(false); - const { verge } = useVerge(); - const { isAdminMode } = useSystemState(); - const { indicator: systemProxyIndicator } = useSystemProxyState(); + const { isTunModeAvailable } = useSystemState(); + const { actualState: systemProxyActualState } = useSystemProxyState(); const { enable_tun_mode } = verge ?? {}; - const updateLocalStatus = async () => { - try { - const runningMode = await getRunningMode(); - const serviceStatus = runningMode === "Service"; - setLocalServiceOk(serviceStatus); - mutate("isServiceAvailable", serviceStatus, false); - } catch (error) { - console.error("更新TUN状态失败:", error); - } - }; - - useEffect(() => { - updateLocalStatus(); - }, []); - - const isTunAvailable = localServiceOk || isAdminMode; - const handleError = (err: Error) => { showNotice("error", err.message || err.toString()); }; @@ -174,22 +154,19 @@ export const ProxyTunCard: FC = () => { const handleTabChange = (tab: string) => { setActiveTab(tab); localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab); - if (tab === "tun") { - updateLocalStatus(); - } }; const tabDescription = useMemo(() => { if (activeTab === "system") { return { - text: systemProxyIndicator + text: systemProxyActualState ? t("System Proxy Enabled") : t("System Proxy Disabled"), tooltip: t("System Proxy Info"), }; } else { return { - text: !isTunAvailable + text: !isTunModeAvailable ? t("TUN Mode Service Required") : enable_tun_mode ? t("TUN Mode Enabled") @@ -197,7 +174,13 @@ export const ProxyTunCard: FC = () => { tooltip: t("TUN Mode Intercept Info"), }; } - }, [activeTab, systemProxyIndicator, enable_tun_mode, isTunAvailable, t]); + }, [ + activeTab, + systemProxyActualState, + enable_tun_mode, + isTunModeAvailable, + t, + ]); return ( @@ -216,14 +199,14 @@ export const ProxyTunCard: FC = () => { onClick={() => handleTabChange("system")} icon={ComputerRounded} label={t("System Proxy")} - hasIndicator={systemProxyIndicator} + hasIndicator={systemProxyActualState} /> handleTabChange("tun")} icon={TroubleshootRounded} label={t("Tun Mode")} - hasIndicator={enable_tun_mode && isTunAvailable} + hasIndicator={enable_tun_mode && isTunModeAvailable} /> @@ -254,6 +237,7 @@ export const ProxyTunCard: FC = () => { diff --git a/clash-verge-rev/src/components/home/system-info-card.tsx b/clash-verge-rev/src/components/home/system-info-card.tsx index 0e35132c10..e9280efa6e 100644 --- a/clash-verge-rev/src/components/home/system-info-card.tsx +++ b/clash-verge-rev/src/components/home/system-info-card.tsx @@ -1,12 +1,3 @@ -import { useTranslation } from "react-i18next"; -import { - Typography, - Stack, - Divider, - Chip, - IconButton, - Tooltip, -} from "@mui/material"; import { InfoOutlined, SettingsOutlined, @@ -15,24 +6,35 @@ import { DnsOutlined, ExtensionOutlined, } from "@mui/icons-material"; -import { useVerge } from "@/hooks/use-verge"; -import { EnhancedCard } from "./enhanced-card"; -import useSWR from "swr"; -import { getSystemInfo } from "@/services/cmds"; -import { useNavigate } from "react-router-dom"; -import { version as appVersion } from "@root/package.json"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + Typography, + Stack, + Divider, + Chip, + IconButton, + Tooltip, +} from "@mui/material"; import { check as checkUpdate } from "@tauri-apps/plugin-updater"; import { useLockFn } from "ahooks"; -import { showNotice } from "@/services/noticeService"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import useSWR from "swr"; + import { useSystemState } from "@/hooks/use-system-state"; +import { useVerge } from "@/hooks/use-verge"; import { useServiceInstaller } from "@/hooks/useServiceInstaller"; +import { getSystemInfo } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; +import { version as appVersion } from "@root/package.json"; + +import { EnhancedCard } from "./enhanced-card"; export const SystemInfoCard = () => { const { t } = useTranslation(); const { verge, patchVerge } = useVerge(); const navigate = useNavigate(); - const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState(); + const { isAdminMode, isSidecarMode } = useSystemState(); const { installServiceAndRestartCore } = useServiceInstaller(); // 系统信息状态 diff --git a/clash-verge-rev/src/components/home/test-card.tsx b/clash-verge-rev/src/components/home/test-card.tsx index 49b5d11455..518500907f 100644 --- a/clash-verge-rev/src/components/home/test-card.tsx +++ b/clash-verge-rev/src/components/home/test-card.tsx @@ -1,6 +1,3 @@ -import { useEffect, useRef, useMemo, useCallback } from "react"; -import { useVerge } from "@/hooks/use-verge"; -import { Box, IconButton, Tooltip, alpha, styled, Grid } from "@mui/material"; import { DndContext, closestCenter, @@ -8,22 +5,26 @@ import { useSensor, useSensors, DragEndEvent, + DragOverlay, } from "@dnd-kit/core"; import { SortableContext } from "@dnd-kit/sortable"; - -import { useTranslation } from "react-i18next"; -import { TestViewer, TestViewerRef } from "@/components/test/test-viewer"; -import { TestItem } from "@/components/test/test-item"; +import { Add, NetworkCheck } from "@mui/icons-material"; +import { Box, IconButton, Tooltip, alpha, styled, Grid } from "@mui/material"; import { emit } from "@tauri-apps/api/event"; import { nanoid } from "nanoid"; -import { Add, NetworkCheck } from "@mui/icons-material"; -import { EnhancedCard } from "./enhanced-card"; +import { useEffect, useRef, useMemo, useCallback } from "react"; +import { useTranslation } from "react-i18next"; // test icons import apple from "@/assets/image/test/apple.svg?raw"; import github from "@/assets/image/test/github.svg?raw"; import google from "@/assets/image/test/google.svg?raw"; import youtube from "@/assets/image/test/youtube.svg?raw"; +import { TestItem } from "@/components/test/test-item"; +import { TestViewer, TestViewerRef } from "@/components/test/test-viewer"; +import { useVerge } from "@/hooks/use-verge"; + +import { EnhancedCard } from "./enhanced-card"; // 自定义滚动条样式 const ScrollBox = styled(Box)(({ theme }) => ({ @@ -196,6 +197,7 @@ export const TestCard = () => { onDragEnd={onDragEnd} > {renderTestItems} + diff --git a/clash-verge-rev/src/components/layout/layout-item.tsx b/clash-verge-rev/src/components/layout/layout-item.tsx index 35e686a2b1..15d4fb93f7 100644 --- a/clash-verge-rev/src/components/layout/layout-item.tsx +++ b/clash-verge-rev/src/components/layout/layout-item.tsx @@ -6,6 +6,7 @@ import { ListItemIcon, } from "@mui/material"; import { useMatch, useResolvedPath, useNavigate } from "react-router-dom"; + import { useVerge } from "@/hooks/use-verge"; interface Props { to: string; diff --git a/clash-verge-rev/src/components/layout/layout-traffic.tsx b/clash-verge-rev/src/components/layout/layout-traffic.tsx index 8eb255ce45..71f662f2f8 100644 --- a/clash-verge-rev/src/components/layout/layout-traffic.tsx +++ b/clash-verge-rev/src/components/layout/layout-traffic.tsx @@ -1,25 +1,22 @@ -import { useEffect, useRef, useState } from "react"; -import { Box, Typography } from "@mui/material"; import { ArrowDownwardRounded, ArrowUpwardRounded, MemoryRounded, } from "@mui/icons-material"; -import { useClashInfo } from "@/hooks/use-clash"; -import { useVerge } from "@/hooks/use-verge"; -import { TrafficGraph, type TrafficRef } from "./traffic-graph"; -import { useVisibility } from "@/hooks/use-visibility"; -import parseTraffic from "@/utils/parse-traffic"; +import { Box, Typography } from "@mui/material"; +import { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; -import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds"; -import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor-enhanced"; -import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary"; import useSWR from "swr"; -interface MemoryUsage { - inuse: number; - oslimit?: number; -} +import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary"; +import { useClashInfo } from "@/hooks/use-clash"; +import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor"; +import { useVerge } from "@/hooks/use-verge"; +import { useVisibility } from "@/hooks/use-visibility"; +import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds"; +import parseTraffic from "@/utils/parse-traffic"; + +import { TrafficGraph, type TrafficRef } from "./traffic-graph"; // setup the traffic export const LayoutTraffic = () => { @@ -46,8 +43,7 @@ export const LayoutTraffic = () => { const pageVisible = useVisibility(); // 使用增强版的统一流量数据Hook - const { traffic, memory, isLoading, isDataFresh, hasValidData } = - useTrafficDataEnhanced(); + const { traffic, memory } = useTrafficDataEnhanced(); // 启动流量服务 useEffect(() => { @@ -78,21 +74,10 @@ export const LayoutTraffic = () => { // 显示内存使用情况的设置 const displayMemory = verge?.enable_memory_usage ?? true; - // 使用格式化的数据,避免重复解析 - const upSpeed = traffic?.formatted?.up_rate || "0B"; - const downSpeed = traffic?.formatted?.down_rate || "0B"; - const memoryUsage = memory?.formatted?.inuse || "0B"; - - // 提取数值和单位 - const [up, upUnit] = upSpeed.includes("B") - ? upSpeed.split(/(?=[KMGT]?B$)/) - : [upSpeed, ""]; - const [down, downUnit] = downSpeed.includes("B") - ? downSpeed.split(/(?=[KMGT]?B$)/) - : [downSpeed, ""]; - const [inuse, inuseUnit] = memoryUsage.includes("B") - ? memoryUsage.split(/(?=[KMGT]?B$)/) - : [memoryUsage, ""]; + // 使用parseTraffic统一处理转换,保持与首页一致的显示格式 + const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0); + const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0); + const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0); const boxStyle: any = { display: "flex", diff --git a/clash-verge-rev/src/components/layout/scroll-top-button.tsx b/clash-verge-rev/src/components/layout/scroll-top-button.tsx index 8813541e1b..66fe80c17f 100644 --- a/clash-verge-rev/src/components/layout/scroll-top-button.tsx +++ b/clash-verge-rev/src/components/layout/scroll-top-button.tsx @@ -1,5 +1,5 @@ -import { IconButton, Fade, SxProps, Theme } from "@mui/material"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import { IconButton, Fade, SxProps, Theme } from "@mui/material"; interface Props { onClick: () => void; diff --git a/clash-verge-rev/src/components/layout/traffic-graph.tsx b/clash-verge-rev/src/components/layout/traffic-graph.tsx index 660d6298ad..81311255ce 100644 --- a/clash-verge-rev/src/components/layout/traffic-graph.tsx +++ b/clash-verge-rev/src/components/layout/traffic-graph.tsx @@ -1,5 +1,5 @@ -import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; import { useTheme } from "@mui/material"; +import { useEffect, useImperativeHandle, useRef, type Ref } from "react"; const maxPoint = 30; @@ -24,7 +24,7 @@ export interface TrafficRef { /** * draw the traffic graph */ -export const TrafficGraph = forwardRef((props, ref) => { +export function TrafficGraph({ ref }: { ref?: Ref }) { const countRef = useRef(0); const styleRef = useRef(true); const listRef = useRef(defaultList); @@ -196,4 +196,4 @@ export const TrafficGraph = forwardRef((props, ref) => { }, [palette]); return ; -}); +} diff --git a/clash-verge-rev/src/components/layout/update-button.tsx b/clash-verge-rev/src/components/layout/update-button.tsx index 74e9e7366b..afa0f83b7e 100644 --- a/clash-verge-rev/src/components/layout/update-button.tsx +++ b/clash-verge-rev/src/components/layout/update-button.tsx @@ -1,11 +1,13 @@ -import useSWR from "swr"; -import { useRef } from "react"; import { Button } from "@mui/material"; import { check } from "@tauri-apps/plugin-updater"; -import { UpdateViewer } from "../setting/mods/update-viewer"; -import { DialogRef } from "../base"; +import { useRef } from "react"; +import useSWR from "swr"; + import { useVerge } from "@/hooks/use-verge"; +import { DialogRef } from "../base"; +import { UpdateViewer } from "../setting/mods/update-viewer"; + interface Props { className?: string; } diff --git a/clash-verge-rev/src/components/layout/use-custom-theme.ts b/clash-verge-rev/src/components/layout/use-custom-theme.ts index 1deba5b799..1bc05b26ae 100644 --- a/clash-verge-rev/src/components/layout/use-custom-theme.ts +++ b/clash-verge-rev/src/components/layout/use-custom-theme.ts @@ -1,6 +1,3 @@ -import { useVerge } from "@/hooks/use-verge"; -import { defaultDarkTheme, defaultTheme } from "@/pages/_theme"; -import { useSetThemeMode, useThemeMode } from "@/services/states"; import { alpha, createTheme, Theme as MuiTheme, Shadows } from "@mui/material"; import { arSD as arXDataGrid, @@ -17,6 +14,10 @@ import { Theme as TauriOsTheme } from "@tauri-apps/api/window"; import { useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { useVerge } from "@/hooks/use-verge"; +import { defaultDarkTheme, defaultTheme } from "@/pages/_theme"; +import { useSetThemeMode, useThemeMode } from "@/services/states"; + const languagePackMap: Record = { zh: { ...zhXDataGrid }, fa: { ...faXDataGrid }, diff --git a/clash-verge-rev/src/components/log/log-item.tsx b/clash-verge-rev/src/components/log/log-item.tsx index 235514730a..3b7a8dee50 100644 --- a/clash-verge-rev/src/components/log/log-item.tsx +++ b/clash-verge-rev/src/components/log/log-item.tsx @@ -1,4 +1,5 @@ import { styled, Box } from "@mui/material"; + import { SearchState } from "@/components/base/base-search-box"; const Item = styled(Box)(({ theme: { palette, typography } }) => ({ diff --git a/clash-verge-rev/src/components/profile/confirm-viewer.tsx b/clash-verge-rev/src/components/profile/confirm-viewer.tsx index 7610abea10..d99eb4262f 100644 --- a/clash-verge-rev/src/components/profile/confirm-viewer.tsx +++ b/clash-verge-rev/src/components/profile/confirm-viewer.tsx @@ -1,5 +1,3 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; import { Button, Dialog, @@ -7,6 +5,8 @@ import { DialogContent, DialogTitle, } from "@mui/material"; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; interface Props { open: boolean; diff --git a/clash-verge-rev/src/components/profile/editor-viewer.tsx b/clash-verge-rev/src/components/profile/editor-viewer.tsx index 7a0e1dd320..a095919fd3 100644 --- a/clash-verge-rev/src/components/profile/editor-viewer.tsx +++ b/clash-verge-rev/src/components/profile/editor-viewer.tsx @@ -1,6 +1,8 @@ -import { ReactNode, useEffect, useRef, useState } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; +import { + FormatPaintRounded, + OpenInFullRounded, + CloseFullscreenRounded, +} from "@mui/icons-material"; import { Button, ButtonGroup, @@ -10,25 +12,23 @@ import { DialogTitle, IconButton, } from "@mui/material"; -import { - FormatPaintRounded, - OpenInFullRounded, - CloseFullscreenRounded, -} from "@mui/icons-material"; -import { useThemeMode } from "@/services/states"; -import { nanoid } from "nanoid"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { showNotice } from "@/services/noticeService"; -import getSystem from "@/utils/get-system"; -import debounce from "@/utils/debounce"; - -import * as monaco from "monaco-editor"; -import MonacoEditor from "react-monaco-editor"; -import { configureMonacoYaml } from "monaco-yaml"; +import { useLockFn } from "ahooks"; import { type JSONSchema7 } from "json-schema"; -import metaSchema from "meta-json-schema/schemas/meta-json-schema.json"; import mergeSchema from "meta-json-schema/schemas/clash-verge-merge-json-schema.json"; +import metaSchema from "meta-json-schema/schemas/meta-json-schema.json"; +import * as monaco from "monaco-editor"; +import { configureMonacoYaml } from "monaco-yaml"; +import { nanoid } from "nanoid"; +import { ReactNode, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import MonacoEditor from "react-monaco-editor"; import pac from "types-pac/pac.d.ts?raw"; + +import { showNotice } from "@/services/noticeService"; +import { useThemeMode } from "@/services/states"; +import debounce from "@/utils/debounce"; +import getSystem from "@/utils/get-system"; const appWindow = getCurrentWebviewWindow(); type Language = "yaml" | "javascript" | "css"; diff --git a/clash-verge-rev/src/components/profile/file-input.tsx b/clash-verge-rev/src/components/profile/file-input.tsx index 326dfca772..5d8511b763 100644 --- a/clash-verge-rev/src/components/profile/file-input.tsx +++ b/clash-verge-rev/src/components/profile/file-input.tsx @@ -1,7 +1,7 @@ -import { useRef, useState } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; import { Box, Button, Typography } from "@mui/material"; +import { useLockFn } from "ahooks"; +import { useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; interface Props { onChange: (file: File, value: string) => void; diff --git a/clash-verge-rev/src/components/profile/group-item.tsx b/clash-verge-rev/src/components/profile/group-item.tsx index b069d187a9..e8e0f63bac 100644 --- a/clash-verge-rev/src/components/profile/group-item.tsx +++ b/clash-verge-rev/src/components/profile/group-item.tsx @@ -1,3 +1,6 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material"; import { Box, IconButton, @@ -6,12 +9,10 @@ import { alpha, styled, } from "@mui/material"; -import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material"; -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { downloadIconCache } from "@/services/cmds"; import { convertFileSrc } from "@tauri-apps/api/core"; import { useEffect, useState } from "react"; + +import { downloadIconCache } from "@/services/cmds"; interface Props { type: "prepend" | "original" | "delete" | "append"; group: IProxyGroupConfig; @@ -19,7 +20,7 @@ interface Props { } export const GroupItem = (props: Props) => { - let { type, group, onDelete } = props; + const { type, group, onDelete } = props; const sortable = type === "prepend" || type === "append"; const { diff --git a/clash-verge-rev/src/components/profile/groups-editor-viewer.tsx b/clash-verge-rev/src/components/profile/groups-editor-viewer.tsx index 75c5a3b9ab..6ec594b0a2 100644 --- a/clash-verge-rev/src/components/profile/groups-editor-viewer.tsx +++ b/clash-verge-rev/src/components/profile/groups-editor-viewer.tsx @@ -1,7 +1,3 @@ -import { useEffect, useMemo, useState } from "react"; -import { useLockFn } from "ahooks"; -import yaml from "js-yaml"; -import { useTranslation } from "react-i18next"; import { DndContext, closestCenter, @@ -15,6 +11,10 @@ import { SortableContext, sortableKeyboardCoordinates, } from "@dnd-kit/sortable"; +import { + VerticalAlignTopRounded, + VerticalAlignBottomRounded, +} from "@mui/icons-material"; import { Autocomplete, Box, @@ -30,28 +30,30 @@ import { TextField, styled, } from "@mui/material"; +import { useLockFn } from "ahooks"; import { - VerticalAlignTopRounded, - VerticalAlignBottomRounded, -} from "@mui/icons-material"; + requestIdleCallback, + cancelIdleCallback, +} from "foxact/request-idle-callback"; +import yaml from "js-yaml"; +import { useEffect, useMemo, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import MonacoEditor from "react-monaco-editor"; +import { Virtuoso } from "react-virtuoso"; + +import { Switch } from "@/components/base"; import { GroupItem } from "@/components/profile/group-item"; import { getNetworkInterfaces, readProfileFile, saveProfileFile, } from "@/services/cmds"; -import { Switch } from "@/components/base"; -import getSystem from "@/utils/get-system"; -import { BaseSearchBox } from "../base/base-search-box"; -import { Virtuoso } from "react-virtuoso"; -import MonacoEditor from "react-monaco-editor"; -import { useThemeMode } from "@/services/states"; -import { Controller, useForm } from "react-hook-form"; import { showNotice } from "@/services/noticeService"; -import { - requestIdleCallback, - cancelIdleCallback, -} from "foxact/request-idle-callback"; +import { useThemeMode } from "@/services/states"; +import getSystem from "@/utils/get-system"; + +import { BaseSearchBox } from "../base/base-search-box"; interface Props { proxiesUid: string; @@ -75,7 +77,7 @@ export const GroupsEditorViewer = (props: Props) => { const [visualization, setVisualization] = useState(true); const [match, setMatch] = useState(() => (_: string) => true); const [interfaceNameList, setInterfaceNameList] = useState([]); - const { control, watch, register, ...formIns } = useForm({ + const { control, ...formIns } = useForm({ defaultValues: { type: "select", name: "", @@ -159,8 +161,8 @@ export const GroupsEditorViewer = (props: Props) => { } }; const fetchContent = async () => { - let data = await readProfileFile(property); - let obj = yaml.load(data) as ISeqProfileConfig | null; + const data = await readProfileFile(property); + const obj = yaml.load(data) as ISeqProfileConfig | null; setPrependSeq(obj?.prepend || []); setAppendSeq(obj?.append || []); @@ -174,7 +176,7 @@ export const GroupsEditorViewer = (props: Props) => { if (currData === "") return; if (visualization !== true) return; - let obj = yaml.load(currData) as { + const obj = yaml.load(currData) as { prepend: []; append: []; delete: []; @@ -196,6 +198,7 @@ export const GroupsEditorViewer = (props: Props) => { ), ); } catch (e) { + console.warn("[GroupsEditorViewer] yaml.dump failed:", e); // 防止异常导致UI卡死 } }; @@ -208,21 +211,21 @@ export const GroupsEditorViewer = (props: Props) => { }, [prependSeq, appendSeq, deleteSeq]); const fetchProxyPolicy = async () => { - let data = await readProfileFile(profileUid); - let proxiesData = await readProfileFile(proxiesUid); - let originGroupsObj = yaml.load(data) as { + const data = await readProfileFile(profileUid); + const proxiesData = await readProfileFile(proxiesUid); + const originGroupsObj = yaml.load(data) as { "proxy-groups": IProxyGroupConfig[]; } | null; - let originProxiesObj = yaml.load(data) as { proxies: [] } | null; - let originProxies = originProxiesObj?.proxies || []; - let moreProxiesObj = yaml.load(proxiesData) as ISeqProfileConfig | null; - let morePrependProxies = moreProxiesObj?.prepend || []; - let moreAppendProxies = moreProxiesObj?.append || []; - let moreDeleteProxies = + const originProxiesObj = yaml.load(data) as { proxies: [] } | null; + const originProxies = originProxiesObj?.proxies || []; + const moreProxiesObj = yaml.load(proxiesData) as ISeqProfileConfig | null; + const morePrependProxies = moreProxiesObj?.prepend || []; + const moreAppendProxies = moreProxiesObj?.append || []; + const moreDeleteProxies = moreProxiesObj?.delete || ([] as string[] | { name: string }[]); - let proxies = morePrependProxies.concat( + const proxies = morePrependProxies.concat( originProxies.filter((proxy: any) => { if (proxy.name) { return !moreDeleteProxies.includes(proxy.name); @@ -245,28 +248,30 @@ export const GroupsEditorViewer = (props: Props) => { ); }; const fetchProfile = async () => { - let data = await readProfileFile(profileUid); - let mergeData = await readProfileFile(mergeUid); - let globalMergeData = await readProfileFile("Merge"); + const data = await readProfileFile(profileUid); + const mergeData = await readProfileFile(mergeUid); + const globalMergeData = await readProfileFile("Merge"); - let originGroupsObj = yaml.load(data) as { + const originGroupsObj = yaml.load(data) as { "proxy-groups": IProxyGroupConfig[]; } | null; - let originProviderObj = yaml.load(data) as { "proxy-providers": {} } | null; - let originProvider = originProviderObj?.["proxy-providers"] || {}; - - let moreProviderObj = yaml.load(mergeData) as { + const originProviderObj = yaml.load(data) as { "proxy-providers": {}; } | null; - let moreProvider = moreProviderObj?.["proxy-providers"] || {}; + const originProvider = originProviderObj?.["proxy-providers"] || {}; - let globalProviderObj = yaml.load(globalMergeData) as { + const moreProviderObj = yaml.load(mergeData) as { "proxy-providers": {}; } | null; - let globalProvider = globalProviderObj?.["proxy-providers"] || {}; + const moreProvider = moreProviderObj?.["proxy-providers"] || {}; - let provider = Object.assign( + const globalProviderObj = yaml.load(globalMergeData) as { + "proxy-providers": {}; + } | null; + const globalProvider = globalProviderObj?.["proxy-providers"] || {}; + + const provider = Object.assign( {}, originProvider, moreProvider, @@ -277,7 +282,7 @@ export const GroupsEditorViewer = (props: Props) => { setGroupList(originGroupsObj?.["proxy-groups"] || []); }; const getInterfaceNameList = async () => { - let list = await getNetworkInterfaces(); + const list = await getNetworkInterfaces(); setInterfaceNameList(list); }; useEffect(() => { @@ -292,7 +297,7 @@ export const GroupsEditorViewer = (props: Props) => { }, [open]); const validateGroup = () => { - let group = formIns.getValues(); + const group = formIns.getValues(); if (group.name === "") { throw new Error(t("Group Name Required")); } @@ -793,7 +798,7 @@ export const GroupsEditorViewer = (props: Props) => { } increaseViewportBy={256} itemContent={(index) => { - let shift = filteredPrependSeq.length > 0 ? 1 : 0; + const shift = filteredPrependSeq.length > 0 ? 1 : 0; if (filteredPrependSeq.length > 0 && index === 0) { return ( { ); } else if (index < filteredGroupList.length + shift) { - let newIndex = index - shift; + const newIndex = index - shift; return ( { // 更新成功,刷新列表 showNotice("success", t("Update subscription successfully")); mutate("getProfiles"); - } catch (err: any) { + } catch { // 更新完全失败(包括后端的回退尝试) // 不需要做处理,后端会通过事件通知系统发送错误 } finally { diff --git a/clash-verge-rev/src/components/profile/profile-more.tsx b/clash-verge-rev/src/components/profile/profile-more.tsx index 8bce9be05c..6d11335768 100644 --- a/clash-verge-rev/src/components/profile/profile-more.tsx +++ b/clash-verge-rev/src/components/profile/profile-more.tsx @@ -1,6 +1,4 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useLockFn } from "ahooks"; +import { FeaturedPlayListRounded } from "@mui/icons-material"; import { Box, Badge, @@ -10,13 +8,17 @@ import { Menu, IconButton, } from "@mui/material"; -import { FeaturedPlayListRounded } from "@mui/icons-material"; -import { viewProfile, readProfileFile, saveProfileFile } from "@/services/cmds"; +import { useLockFn } from "ahooks"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + import { EditorViewer } from "@/components/profile/editor-viewer"; -import { ProfileBox } from "./profile-box"; -import { LogViewer } from "./log-viewer"; +import { viewProfile, readProfileFile, saveProfileFile } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; +import { LogViewer } from "./log-viewer"; +import { ProfileBox } from "./profile-box"; + interface Props { logInfo?: [string, string][]; id: "Merge" | "Script"; @@ -47,11 +49,6 @@ export const ProfileMore = (props: Props) => { } }); - const fnWrapper = (fn: () => void) => () => { - setAnchorEl(null); - return fn(); - }; - const hasError = !!logInfo.find((e) => e[0] === "exception"); const itemMenu = [ diff --git a/clash-verge-rev/src/components/profile/profile-viewer.tsx b/clash-verge-rev/src/components/profile/profile-viewer.tsx index d6ecc7503f..b66a216df4 100644 --- a/clash-verge-rev/src/components/profile/profile-viewer.tsx +++ b/clash-verge-rev/src/components/profile/profile-viewer.tsx @@ -1,13 +1,3 @@ -import { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, -} from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; -import { useForm, Controller } from "react-hook-form"; import { Box, FormControl, @@ -18,12 +8,19 @@ import { styled, TextField, } from "@mui/material"; -import { createProfile, patchProfile } from "@/services/cmds"; +import { useLockFn } from "ahooks"; +import type { Ref } from "react"; +import { useEffect, useImperativeHandle, useRef, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + import { BaseDialog, Switch } from "@/components/base"; -import { version } from "@root/package.json"; -import { FileInput } from "./file-input"; import { useProfiles } from "@/hooks/use-profiles"; +import { createProfile, patchProfile } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; +import { version } from "@root/package.json"; + +import { FileInput } from "./file-input"; interface Props { onChange: (isActivating?: boolean) => void; @@ -36,297 +33,279 @@ export interface ProfileViewerRef { // create or edit the profile // remote / local -export const ProfileViewer = forwardRef( - (props, ref) => { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const [openType, setOpenType] = useState<"new" | "edit">("new"); - const [loading, setLoading] = useState(false); - const { profiles } = useProfiles(); +type ProfileViewerProps = Props & { ref?: Ref }; - // file input - const fileDataRef = useRef(null); +export function ProfileViewer({ onChange, ref }: ProfileViewerProps) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const [openType, setOpenType] = useState<"new" | "edit">("new"); + const [loading, setLoading] = useState(false); + const { profiles } = useProfiles(); - const { control, watch, register, ...formIns } = useForm({ - defaultValues: { - type: "remote", - name: "", - desc: "", - url: "", - option: { - with_proxy: false, - self_proxy: false, - }, + // file input + const fileDataRef = useRef(null); + + const { + control, + watch, + register: _register, + ...formIns + } = useForm({ + defaultValues: { + type: "remote", + name: "", + desc: "", + url: "", + option: { + with_proxy: false, + self_proxy: false, }, - }); + }, + }); - useImperativeHandle(ref, () => ({ - create: () => { - setOpenType("new"); - setOpen(true); - }, - edit: (item) => { - if (item) { - Object.entries(item).forEach(([key, value]) => { - formIns.setValue(key as any, value); - }); - } - setOpenType("edit"); - setOpen(true); - }, - })); + useImperativeHandle(ref, () => ({ + create: () => { + setOpenType("new"); + setOpen(true); + }, + edit: (item: IProfileItem) => { + if (item) { + Object.entries(item).forEach(([key, value]) => { + formIns.setValue(key as any, value); + }); + } + setOpenType("edit"); + setOpen(true); + }, + })); - const selfProxy = watch("option.self_proxy"); - const withProxy = watch("option.with_proxy"); + const selfProxy = watch("option.self_proxy"); + const withProxy = watch("option.with_proxy"); - useEffect(() => { - if (selfProxy) formIns.setValue("option.with_proxy", false); - }, [selfProxy]); + useEffect(() => { + if (selfProxy) formIns.setValue("option.with_proxy", false); + }, [selfProxy]); - useEffect(() => { - if (withProxy) formIns.setValue("option.self_proxy", false); - }, [withProxy]); + useEffect(() => { + if (withProxy) formIns.setValue("option.self_proxy", false); + }, [withProxy]); - const handleOk = useLockFn( - formIns.handleSubmit(async (form) => { - if (form.option?.timeout_seconds) { - form.option.timeout_seconds = +form.option.timeout_seconds; + const handleOk = useLockFn( + formIns.handleSubmit(async (form) => { + if (form.option?.timeout_seconds) { + form.option.timeout_seconds = +form.option.timeout_seconds; + } + + setLoading(true); + try { + // 基本验证 + if (!form.type) throw new Error("`Type` should not be null"); + if (form.type === "remote" && !form.url) { + throw new Error("The URL should not be null"); } - setLoading(true); - try { - // 基本验证 - if (!form.type) throw new Error("`Type` should not be null"); - if (form.type === "remote" && !form.url) { - throw new Error("The URL should not be null"); - } + // 处理表单数据 + if (form.option?.update_interval) { + form.option.update_interval = +form.option.update_interval; + } else { + delete form.option?.update_interval; + } + if (form.option?.user_agent === "") { + delete form.option.user_agent; + } - // 处理表单数据 - if (form.option?.update_interval) { - form.option.update_interval = +form.option.update_interval; + const name = form.name || `${form.type} file`; + const item = { ...form, name }; + const isRemote = form.type === "remote"; + const isUpdate = openType === "edit"; + + // 判断是否是当前激活的配置 + const isActivating = isUpdate && form.uid === (profiles?.current ?? ""); + + // 保存原始代理设置以便回退成功后恢复 + const originalOptions = { + with_proxy: form.option?.with_proxy, + self_proxy: form.option?.self_proxy, + }; + + // 执行创建或更新操作,本地配置不需要回退机制 + if (!isRemote) { + if (openType === "new") { + await createProfile(item, fileDataRef.current); } else { - delete form.option?.update_interval; + if (!form.uid) throw new Error("UID not found"); + await patchProfile(form.uid, item); } - if (form.option?.user_agent === "") { - delete form.option.user_agent; - } - - const name = form.name || `${form.type} file`; - const item = { ...form, name }; - const isRemote = form.type === "remote"; - const isUpdate = openType === "edit"; - - // 判断是否是当前激活的配置 - const isActivating = - isUpdate && form.uid === (profiles?.current ?? ""); - - // 保存原始代理设置以便回退成功后恢复 - const originalOptions = { - with_proxy: form.option?.with_proxy, - self_proxy: form.option?.self_proxy, - }; - - // 执行创建或更新操作,本地配置不需要回退机制 - if (!isRemote) { + } else { + // 远程配置使用回退机制 + try { + // 尝试正常操作 if (openType === "new") { await createProfile(item, fileDataRef.current); } else { if (!form.uid) throw new Error("UID not found"); await patchProfile(form.uid, item); } - } else { - // 远程配置使用回退机制 - try { - // 尝试正常操作 - if (openType === "new") { - await createProfile(item, fileDataRef.current); - } else { - if (!form.uid) throw new Error("UID not found"); - await patchProfile(form.uid, item); - } - } catch (err) { - // 首次创建/更新失败,尝试使用自身代理 - showNotice( - "info", - t("Profile creation failed, retrying with Clash proxy..."), - ); + } catch { + // 首次创建/更新失败,尝试使用自身代理 + showNotice( + "info", + t("Profile creation failed, retrying with Clash proxy..."), + ); - // 使用自身代理的配置 - const retryItem = { - ...item, - option: { - ...item.option, - with_proxy: false, - self_proxy: true, - }, - }; + // 使用自身代理的配置 + const retryItem = { + ...item, + option: { + ...item.option, + with_proxy: false, + self_proxy: true, + }, + }; - // 使用自身代理再次尝试 - if (openType === "new") { - await createProfile(retryItem, fileDataRef.current); - } else { - if (!form.uid) throw new Error("UID not found"); - await patchProfile(form.uid, retryItem); + // 使用自身代理再次尝试 + if (openType === "new") { + await createProfile(retryItem, fileDataRef.current); + } else { + if (!form.uid) throw new Error("UID not found"); + await patchProfile(form.uid, retryItem); - // 编辑模式下恢复原始代理设置 - await patchProfile(form.uid, { option: originalOptions }); - } - - showNotice( - "success", - t("Profile creation succeeded with Clash proxy"), - ); + // 编辑模式下恢复原始代理设置 + await patchProfile(form.uid, { option: originalOptions }); } + + showNotice( + "success", + t("Profile creation succeeded with Clash proxy"), + ); } - - // 成功后的操作 - setOpen(false); - setTimeout(() => formIns.reset(), 500); - fileDataRef.current = null; - - // 优化:UI先关闭,异步通知父组件 - setTimeout(() => { - props.onChange(isActivating); - }, 0); - } catch (err: any) { - showNotice("error", err.message || err.toString()); - } finally { - setLoading(false); } - }), - ); - const handleClose = () => { - try { + // 成功后的操作 setOpen(false); - fileDataRef.current = null; setTimeout(() => formIns.reset(), 500); - } catch {} - }; + fileDataRef.current = null; - const text = { - fullWidth: true, - size: "small", - margin: "normal", - variant: "outlined", - autoComplete: "off", - autoCorrect: "off", - } as const; + // 优化:UI先关闭,异步通知父组件 + setTimeout(() => { + onChange(isActivating); + }, 0); + } catch (err: any) { + showNotice("error", err.message || err.toString()); + } finally { + setLoading(false); + } + }), + ); - const formType = watch("type"); - const isRemote = formType === "remote"; - const isLocal = formType === "local"; + const handleClose = () => { + try { + setOpen(false); + fileDataRef.current = null; + setTimeout(() => formIns.reset(), 500); + } catch (e) { + console.warn("[ProfileViewer] handleClose error:", e); + } + }; - return ( - - ( - - {t("Type")} - - - )} - /> + const text = { + fullWidth: true, + size: "small", + margin: "normal", + variant: "outlined", + autoComplete: "off", + autoCorrect: "off", + } as const; - ( - - )} - /> + const formType = watch("type"); + const isRemote = formType === "remote"; + const isLocal = formType === "local"; - ( - - )} - /> - - {isRemote && ( - <> - ( - - )} - /> - - ( - - )} - /> - - ( - - {t("seconds")} - - ), - }, - }} - /> - )} - /> - + return ( + + ( + + {t("Type")} + + )} + /> - {(isRemote || isLocal) && ( + ( + + )} + /> + + ( + + )} + /> + + {isRemote && ( + <> ( + + )} + /> + + ( + + )} + /> + + ( - {t("mins")} + {t("seconds")} ), }, @@ -334,57 +313,79 @@ export const ProfileViewer = forwardRef( /> )} /> - )} + + )} - {isLocal && openType === "new" && ( - { - formIns.setValue("name", formIns.getValues("name") || file.name); - fileDataRef.current = val; - }} + {(isRemote || isLocal) && ( + ( + {t("mins")} + ), + }, + }} + /> + )} + /> + )} + + {isLocal && openType === "new" && ( + { + formIns.setValue("name", formIns.getValues("name") || file.name); + fileDataRef.current = val; + }} + /> + )} + + {isRemote && ( + <> + ( + + {t("Use System Proxy")} + + + )} /> - )} - {isRemote && ( - <> - ( - - {t("Use System Proxy")} - - - )} - /> + ( + + {t("Use Clash Proxy")} + + + )} + /> - ( - - {t("Use Clash Proxy")} - - - )} - /> - - ( - - {t("Accept Invalid Certs (Danger)")} - - - )} - /> - - )} - - ); - }, -); + ( + + {t("Accept Invalid Certs (Danger)")} + + + )} + /> + + )} + + ); +} const StyledBox = styled(Box)(() => ({ margin: "8px 0 8px 8px", diff --git a/clash-verge-rev/src/components/profile/proxies-editor-viewer.tsx b/clash-verge-rev/src/components/profile/proxies-editor-viewer.tsx index 624d83f9e8..3ec18f3b35 100644 --- a/clash-verge-rev/src/components/profile/proxies-editor-viewer.tsx +++ b/clash-verge-rev/src/components/profile/proxies-editor-viewer.tsx @@ -1,7 +1,3 @@ -import { useEffect, useMemo, useState } from "react"; -import { useLockFn } from "ahooks"; -import yaml from "js-yaml"; -import { useTranslation } from "react-i18next"; import { DndContext, closestCenter, @@ -15,6 +11,10 @@ import { SortableContext, sortableKeyboardCoordinates, } from "@dnd-kit/sortable"; +import { + VerticalAlignTopRounded, + VerticalAlignBottomRounded, +} from "@mui/icons-material"; import { Box, Button, @@ -27,19 +27,21 @@ import { TextField, styled, } from "@mui/material"; -import { - VerticalAlignTopRounded, - VerticalAlignBottomRounded, -} from "@mui/icons-material"; +import { useLockFn } from "ahooks"; +import yaml from "js-yaml"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import MonacoEditor from "react-monaco-editor"; +import { Virtuoso } from "react-virtuoso"; + import { ProxyItem } from "@/components/profile/proxy-item"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; -import getSystem from "@/utils/get-system"; -import { BaseSearchBox } from "../base/base-search-box"; -import { Virtuoso } from "react-virtuoso"; -import MonacoEditor from "react-monaco-editor"; -import { useThemeMode } from "@/services/states"; -import parseUri from "@/utils/uri-parser"; import { showNotice } from "@/services/noticeService"; +import { useThemeMode } from "@/services/states"; +import getSystem from "@/utils/get-system"; +import parseUri from "@/utils/uri-parser"; + +import { BaseSearchBox } from "../base/base-search-box"; interface Props { profileUid: string; @@ -132,8 +134,8 @@ export const ProxiesEditorViewer = (props: Props) => { }; // 优化:异步分片解析,避免主线程阻塞,解析完成后批量setState const handleParseAsync = (cb: (proxies: IProxyConfig[]) => void) => { - let proxies: IProxyConfig[] = []; - let names: string[] = []; + const proxies: IProxyConfig[] = []; + const names: string[] = []; let uris = ""; try { uris = atob(proxyUri); @@ -148,12 +150,17 @@ export const ProxiesEditorViewer = (props: Props) => { for (; idx < end; idx++) { const uri = lines[idx]; try { - let proxy = parseUri(uri.trim()); + const proxy = parseUri(uri.trim()); if (!names.includes(proxy.name)) { proxies.push(proxy); names.push(proxy.name); } } catch (err: any) { + console.warn( + "[ProxiesEditorViewer] parseUri failed for line:", + uri, + err?.message || err, + ); // 不阻塞主流程 } } @@ -166,9 +173,9 @@ export const ProxiesEditorViewer = (props: Props) => { parseBatch(); }; const fetchProfile = async () => { - let data = await readProfileFile(profileUid); + const data = await readProfileFile(profileUid); - let originProxiesObj = yaml.load(data) as { + const originProxiesObj = yaml.load(data) as { proxies: IProxyConfig[]; } | null; @@ -176,8 +183,8 @@ export const ProxiesEditorViewer = (props: Props) => { }; const fetchContent = async () => { - let data = await readProfileFile(property); - let obj = yaml.load(data) as ISeqProfileConfig | null; + const data = await readProfileFile(property); + const obj = yaml.load(data) as ISeqProfileConfig | null; setPrependSeq(obj?.prepend || []); setAppendSeq(obj?.append || []); @@ -191,7 +198,7 @@ export const ProxiesEditorViewer = (props: Props) => { if (currData === "") return; if (visualization !== true) return; - let obj = yaml.load(currData) as { + const obj = yaml.load(currData) as { prepend: []; append: []; delete: []; @@ -212,6 +219,7 @@ export const ProxiesEditorViewer = (props: Props) => { ), ); } catch (e) { + console.warn("[ProxiesEditorViewer] yaml.dump failed:", e); // 防止异常导致UI卡死 } }; @@ -336,7 +344,7 @@ export const ProxiesEditorViewer = (props: Props) => { } increaseViewportBy={256} itemContent={(index) => { - let shift = filteredPrependSeq.length > 0 ? 1 : 0; + const shift = filteredPrependSeq.length > 0 ? 1 : 0; if (filteredPrependSeq.length > 0 && index === 0) { return ( { ); } else if (index < filteredProxyList.length + shift) { - let newIndex = index - shift; + const newIndex = index - shift; return ( { - let { type, proxy, onDelete } = props; + const { type, proxy, onDelete } = props; const sortable = type === "prepend" || type === "append"; const { diff --git a/clash-verge-rev/src/components/profile/rule-item.tsx b/clash-verge-rev/src/components/profile/rule-item.tsx index 7544a4a847..03e5fb547a 100644 --- a/clash-verge-rev/src/components/profile/rule-item.tsx +++ b/clash-verge-rev/src/components/profile/rule-item.tsx @@ -1,3 +1,6 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material"; import { Box, IconButton, @@ -6,9 +9,6 @@ import { alpha, styled, } from "@mui/material"; -import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material"; -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; interface Props { type: "prepend" | "original" | "delete" | "append"; ruleRaw: string; @@ -16,7 +16,7 @@ interface Props { } export const RuleItem = (props: Props) => { - let { type, ruleRaw, onDelete } = props; + const { type, ruleRaw, onDelete } = props; const sortable = type === "prepend" || type === "append"; const rule = ruleRaw.replace(",no-resolve", ""); @@ -24,6 +24,8 @@ export const RuleItem = (props: Props) => { const proxyPolicy = rule.match(/[^,]+$/)?.[0] ?? ""; const ruleContent = rule.slice(ruleType.length + 1, -proxyPolicy.length - 1); + const $sortable = useSortable({ id: ruleRaw }); + const { attributes, listeners, @@ -32,7 +34,7 @@ export const RuleItem = (props: Props) => { transition, isDragging, } = sortable - ? useSortable({ id: ruleRaw }) + ? $sortable : { attributes: {}, listeners: {}, diff --git a/clash-verge-rev/src/components/profile/rules-editor-viewer.tsx b/clash-verge-rev/src/components/profile/rules-editor-viewer.tsx index 85000813e0..fe27bfc2a5 100644 --- a/clash-verge-rev/src/components/profile/rules-editor-viewer.tsx +++ b/clash-verge-rev/src/components/profile/rules-editor-viewer.tsx @@ -1,7 +1,3 @@ -import { useEffect, useMemo, useState } from "react"; -import { useLockFn } from "ahooks"; -import yaml from "js-yaml"; -import { useTranslation } from "react-i18next"; import { DndContext, closestCenter, @@ -15,6 +11,10 @@ import { SortableContext, sortableKeyboardCoordinates, } from "@dnd-kit/sortable"; +import { + VerticalAlignTopRounded, + VerticalAlignBottomRounded, +} from "@mui/icons-material"; import { Autocomplete, Box, @@ -29,19 +29,21 @@ import { TextField, styled, } from "@mui/material"; -import { - VerticalAlignTopRounded, - VerticalAlignBottomRounded, -} from "@mui/icons-material"; -import { readProfileFile, saveProfileFile } from "@/services/cmds"; -import { Switch } from "@/components/base"; -import getSystem from "@/utils/get-system"; -import { RuleItem } from "@/components/profile/rule-item"; -import { BaseSearchBox } from "../base/base-search-box"; -import { Virtuoso } from "react-virtuoso"; +import { useLockFn } from "ahooks"; +import yaml from "js-yaml"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import MonacoEditor from "react-monaco-editor"; -import { useThemeMode } from "@/services/states"; +import { Virtuoso } from "react-virtuoso"; + +import { Switch } from "@/components/base"; +import { RuleItem } from "@/components/profile/rule-item"; +import { readProfileFile, saveProfileFile } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; +import { useThemeMode } from "@/services/states"; +import getSystem from "@/utils/get-system"; + +import { BaseSearchBox } from "../base/base-search-box"; interface Props { groupsUid: string; @@ -156,7 +158,7 @@ const rules: { }, { name: "IN-PORT", - example: "7890", + example: "7897", validator: (value) => portValidator(value), }, { @@ -287,8 +289,8 @@ export const RulesEditorViewer = (props: Props) => { const { active, over } = event; if (over) { if (active.id !== over.id) { - let activeIndex = prependSeq.indexOf(active.id.toString()); - let overIndex = prependSeq.indexOf(over.id.toString()); + const activeIndex = prependSeq.indexOf(active.id.toString()); + const overIndex = prependSeq.indexOf(over.id.toString()); setPrependSeq(reorder(prependSeq, activeIndex, overIndex)); } } @@ -297,15 +299,15 @@ export const RulesEditorViewer = (props: Props) => { const { active, over } = event; if (over) { if (active.id !== over.id) { - let activeIndex = appendSeq.indexOf(active.id.toString()); - let overIndex = appendSeq.indexOf(over.id.toString()); + const activeIndex = appendSeq.indexOf(active.id.toString()); + const overIndex = appendSeq.indexOf(over.id.toString()); setAppendSeq(reorder(appendSeq, activeIndex, overIndex)); } } }; const fetchContent = async () => { - let data = await readProfileFile(property); - let obj = yaml.load(data) as ISeqProfileConfig | null; + const data = await readProfileFile(property); + const obj = yaml.load(data) as ISeqProfileConfig | null; setPrependSeq(obj?.prepend || []); setAppendSeq(obj?.append || []); @@ -319,7 +321,7 @@ export const RulesEditorViewer = (props: Props) => { if (currData === "") return; if (visualization !== true) return; - let obj = yaml.load(currData) as ISeqProfileConfig | null; + const obj = yaml.load(currData) as ISeqProfileConfig | null; setPrependSeq(obj?.prepend || []); setAppendSeq(obj?.append || []); setDeleteSeq(obj?.delete || []); @@ -349,21 +351,21 @@ export const RulesEditorViewer = (props: Props) => { }, [prependSeq, appendSeq, deleteSeq]); const fetchProfile = async () => { - let data = await readProfileFile(profileUid); // 原配置文件 - let groupsData = await readProfileFile(groupsUid); // groups配置文件 - let mergeData = await readProfileFile(mergeUid); // merge配置文件 - let globalMergeData = await readProfileFile("Merge"); // global merge配置文件 + const data = await readProfileFile(profileUid); // 原配置文件 + const groupsData = await readProfileFile(groupsUid); // groups配置文件 + const mergeData = await readProfileFile(mergeUid); // merge配置文件 + const globalMergeData = await readProfileFile("Merge"); // global merge配置文件 - let rulesObj = yaml.load(data) as { rules: [] } | null; + const rulesObj = yaml.load(data) as { rules: [] } | null; - let originGroupsObj = yaml.load(data) as { "proxy-groups": [] } | null; - let originGroups = originGroupsObj?.["proxy-groups"] || []; - let moreGroupsObj = yaml.load(groupsData) as ISeqProfileConfig | null; - let morePrependGroups = moreGroupsObj?.["prepend"] || []; - let moreAppendGroups = moreGroupsObj?.["append"] || []; - let moreDeleteGroups = + const originGroupsObj = yaml.load(data) as { "proxy-groups": [] } | null; + const originGroups = originGroupsObj?.["proxy-groups"] || []; + const moreGroupsObj = yaml.load(groupsData) as ISeqProfileConfig | null; + const morePrependGroups = moreGroupsObj?.["prepend"] || []; + const moreAppendGroups = moreGroupsObj?.["append"] || []; + const moreDeleteGroups = moreGroupsObj?.["delete"] || ([] as string[] | { name: string }[]); - let groups = morePrependGroups.concat( + const groups = morePrependGroups.concat( originGroups.filter((group: any) => { if (group.name) { return !moreDeleteGroups.includes(group.name); @@ -374,27 +376,37 @@ export const RulesEditorViewer = (props: Props) => { moreAppendGroups, ); - let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null; - let originRuleSet = originRuleSetObj?.["rule-providers"] || {}; - let moreRuleSetObj = yaml.load(mergeData) as { + const originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null; + const originRuleSet = originRuleSetObj?.["rule-providers"] || {}; + const moreRuleSetObj = yaml.load(mergeData) as { "rule-providers": {}; } | null; - let moreRuleSet = moreRuleSetObj?.["rule-providers"] || {}; - let globalRuleSetObj = yaml.load(globalMergeData) as { + const moreRuleSet = moreRuleSetObj?.["rule-providers"] || {}; + const globalRuleSetObj = yaml.load(globalMergeData) as { "rule-providers": {}; } | null; - let globalRuleSet = globalRuleSetObj?.["rule-providers"] || {}; - let ruleSet = Object.assign({}, originRuleSet, moreRuleSet, globalRuleSet); + const globalRuleSet = globalRuleSetObj?.["rule-providers"] || {}; + const ruleSet = Object.assign( + {}, + originRuleSet, + moreRuleSet, + globalRuleSet, + ); - let originSubRuleObj = yaml.load(data) as { "sub-rules": {} } | null; - let originSubRule = originSubRuleObj?.["sub-rules"] || {}; - let moreSubRuleObj = yaml.load(mergeData) as { "sub-rules": {} } | null; - let moreSubRule = moreSubRuleObj?.["sub-rules"] || {}; - let globalSubRuleObj = yaml.load(globalMergeData) as { + const originSubRuleObj = yaml.load(data) as { "sub-rules": {} } | null; + const originSubRule = originSubRuleObj?.["sub-rules"] || {}; + const moreSubRuleObj = yaml.load(mergeData) as { "sub-rules": {} } | null; + const moreSubRule = moreSubRuleObj?.["sub-rules"] || {}; + const globalSubRuleObj = yaml.load(globalMergeData) as { "sub-rules": {}; } | null; - let globalSubRule = globalSubRuleObj?.["sub-rules"] || {}; - let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule); + const globalSubRule = globalSubRuleObj?.["sub-rules"] || {}; + const subRule = Object.assign( + {}, + originSubRule, + moreSubRule, + globalSubRule, + ); setProxyPolicyList( builtinProxyPolicies.concat(groups.map((group: any) => group.name)), ); @@ -554,7 +566,7 @@ export const RulesEditorViewer = (props: Props) => { startIcon={} onClick={() => { try { - let raw = validateRule(); + const raw = validateRule(); if (prependSeq.includes(raw)) return; setPrependSeq([raw, ...prependSeq]); } catch (err: any) { @@ -572,7 +584,7 @@ export const RulesEditorViewer = (props: Props) => { startIcon={} onClick={() => { try { - let raw = validateRule(); + const raw = validateRule(); if (appendSeq.includes(raw)) return; setAppendSeq([...appendSeq, raw]); } catch (err: any) { @@ -601,7 +613,7 @@ export const RulesEditorViewer = (props: Props) => { } increaseViewportBy={256} itemContent={(index) => { - let shift = filteredPrependSeq.length > 0 ? 1 : 0; + const shift = filteredPrependSeq.length > 0 ? 1 : 0; if (filteredPrependSeq.length > 0 && index === 0) { return ( { ); } else if (index < filteredRuleList.length + shift) { - let newIndex = index - shift; + const newIndex = index - shift; return ( { export const ProviderButton = () => { const { t } = useTranslation(); - const theme = useTheme(); const [open, setOpen] = useState(false); const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData(); const [updating, setUpdating] = useState>({}); @@ -312,7 +311,7 @@ export const ProviderButton = () => { { + onClick={() => { updateProvider(key); }} disabled={isUpdating} diff --git a/clash-verge-rev/src/components/proxy/proxy-chain.tsx b/clash-verge-rev/src/components/proxy/proxy-chain.tsx new file mode 100644 index 0000000000..285a6889c6 --- /dev/null +++ b/clash-verge-rev/src/components/proxy/proxy-chain.tsx @@ -0,0 +1,623 @@ +import { + closestCenter, + DndContext, + DragEndEvent, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { + Delete as DeleteIcon, + DragIndicator, + Link, + LinkOff, +} from "@mui/icons-material"; +import { + Alert, + Box, + Button, + Chip, + IconButton, + Paper, + Typography, + useTheme, +} from "@mui/material"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; + +import { useAppData } from "@/providers/app-data-context"; +import { + closeAllConnections, + getProxies, + updateProxyAndSync, + updateProxyChainConfigInRuntime, +} from "@/services/cmds"; + +interface ProxyChainItem { + id: string; + name: string; + type?: string; + delay?: number; +} + +interface ParsedChainConfig { + proxies?: Array<{ + name: string; + type: string; + [key: string]: any; + }>; +} + +interface ProxyChainProps { + proxyChain: ProxyChainItem[]; + onUpdateChain: (chain: ProxyChainItem[]) => void; + chainConfigData?: string | null; + onMarkUnsavedChanges?: () => void; + mode?: string; + selectedGroup?: string | null; +} + +interface SortableItemProps { + proxy: ProxyChainItem; + index: number; + onRemove: (id: string) => void; +} + +const SortableItem = ({ proxy, index, onRemove }: SortableItemProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: proxy.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( + + + + + + + + + {proxy.name} + + + {proxy.type && ( + + )} + + {proxy.delay !== undefined && ( + 0 ? `${proxy.delay}ms` : t("timeout") || "超时"} + size="small" + color={ + proxy.delay > 0 && proxy.delay < 200 + ? "success" + : proxy.delay > 0 && proxy.delay < 800 + ? "warning" + : "error" + } + sx={{ mr: 1, fontSize: "0.7rem", minWidth: 50 }} + /> + )} + + onRemove(proxy.id)} + sx={{ + color: theme.palette.error.main, + "&:hover": { + backgroundColor: theme.palette.error.light + "20", + }, + }} + > + + + + ); +}; + +export const ProxyChain = ({ + proxyChain, + onUpdateChain, + chainConfigData, + onMarkUnsavedChanges, + mode, + selectedGroup, +}: ProxyChainProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { proxies } = useAppData(); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); + const [isConnected, setIsConnected] = useState(false); + + // 获取当前代理信息以检查连接状态 + const { data: currentProxies, mutate: mutateProxies } = useSWR( + "getProxies", + getProxies, + { + revalidateOnFocus: true, + revalidateIfStale: true, + refreshInterval: 5000, // 每5秒刷新一次 + }, + ); + + // 检查连接状态 + useEffect(() => { + if (!currentProxies || proxyChain.length < 2) { + setIsConnected(false); + return; + } + + // 获取用户配置的最后一个节点 + const lastNode = proxyChain[proxyChain.length - 1]; + + // 根据模式确定要检查的代理组和当前选中的代理 + if (mode === "global") { + // 全局模式:检查 global 对象 + if (!currentProxies.global || !currentProxies.global.now) { + setIsConnected(false); + return; + } + + // 检查当前选中的代理是否是配置的最后一个节点 + if (currentProxies.global.now === lastNode.name) { + setIsConnected(true); + } else { + setIsConnected(false); + } + } else { + // 规则模式:检查指定的代理组 + if (!selectedGroup) { + setIsConnected(false); + return; + } + + const proxyChainGroup = currentProxies.groups.find( + (group) => group.name === selectedGroup, + ); + if (!proxyChainGroup || !proxyChainGroup.now) { + setIsConnected(false); + return; + } + + // 检查当前选中的代理是否是配置的最后一个节点 + if (proxyChainGroup.now === lastNode.name) { + setIsConnected(true); + } else { + setIsConnected(false); + } + } + }, [currentProxies, proxyChain, mode, selectedGroup]); + + // 监听链的变化,但排除从配置加载的情况 + const chainLengthRef = useRef(proxyChain.length); + useEffect(() => { + // 只有当链长度发生变化且不是初始加载时,才标记为未保存 + if ( + chainLengthRef.current !== proxyChain.length && + chainLengthRef.current !== 0 + ) { + setHasUnsavedChanges(true); + } + chainLengthRef.current = proxyChain.length; + }, [proxyChain.length]); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + + if (active.id !== over?.id) { + const oldIndex = proxyChain.findIndex((item) => item.id === active.id); + const newIndex = proxyChain.findIndex((item) => item.id === over?.id); + + onUpdateChain(arrayMove(proxyChain, oldIndex, newIndex)); + setHasUnsavedChanges(true); + } + }, + [proxyChain, onUpdateChain], + ); + + const handleRemoveProxy = useCallback( + (id: string) => { + const newChain = proxyChain.filter((item) => item.id !== id); + onUpdateChain(newChain); + setHasUnsavedChanges(true); + }, + [proxyChain, onUpdateChain], + ); + + const handleClearAll = useCallback(() => { + onUpdateChain([]); + setHasUnsavedChanges(true); + }, [onUpdateChain]); + + const handleConnect = useCallback(async () => { + if (isConnected) { + // 如果已连接,则断开连接 + setIsConnecting(true); + try { + // 清空链式代理配置 + await updateProxyChainConfigInRuntime(null); + + // 切换到 DIRECT 模式断开代理连接 + // await updateProxyAndSync("GLOBAL", "DIRECT"); + + // 关闭所有连接 + await closeAllConnections(); + + // 刷新代理信息以更新连接状态 + mutateProxies(); + + // 清空链式代理配置UI + // onUpdateChain([]); + // setHasUnsavedChanges(false); + + // 强制更新连接状态 + setIsConnected(false); + } catch (error) { + console.error("Failed to disconnect from proxy chain:", error); + alert(t("Failed to disconnect from proxy chain") || "断开链式代理失败"); + } finally { + setIsConnecting(false); + } + return; + } + + if (proxyChain.length < 2) { + alert( + t("Chain proxy requires at least 2 nodes") || "链式代理至少需要2个节点", + ); + return; + } + + setIsConnecting(true); + try { + // 第一步:保存链式代理配置 + const chainProxies = proxyChain.map((node) => node.name); + console.log("Saving chain config:", chainProxies); + await updateProxyChainConfigInRuntime(chainProxies); + console.log("Chain configuration saved successfully"); + + // 第二步:连接到代理链的最后一个节点 + const lastNode = proxyChain[proxyChain.length - 1]; + console.log(`Connecting to proxy chain, last node: ${lastNode.name}`); + + // 根据模式确定使用的代理组名称 + if (mode !== "global" && !selectedGroup) { + throw new Error("规则模式下必须选择代理组"); + } + + const targetGroup = mode === "global" ? "GLOBAL" : selectedGroup; + + await updateProxyAndSync(targetGroup || "GLOBAL", lastNode.name); + localStorage.setItem("proxy-chain-group", targetGroup || "GLOBAL"); + localStorage.setItem("proxy-chain-exit-node", lastNode.name); + + // 刷新代理信息以更新连接状态 + mutateProxies(); + + // 清除未保存标记 + setHasUnsavedChanges(false); + console.log("Successfully connected to proxy chain"); + } catch (error) { + console.error("Failed to connect to proxy chain:", error); + alert(t("Failed to connect to proxy chain") || "连接链式代理失败"); + } finally { + setIsConnecting(false); + } + }, [proxyChain, isConnected, t, mutateProxies, mode, selectedGroup]); + + const proxyChainRef = useRef(proxyChain); + const onUpdateChainRef = useRef(onUpdateChain); + + useEffect(() => { + proxyChainRef.current = proxyChain; + onUpdateChainRef.current = onUpdateChain; + }, [proxyChain, onUpdateChain]); + + // 处理链式代理配置数据 + useEffect(() => { + if (chainConfigData) { + try { + // Try to parse as YAML using dynamic import + import("js-yaml") + .then((yaml) => { + try { + const parsedConfig = yaml.load( + chainConfigData, + ) as ParsedChainConfig; + const chainItems = + parsedConfig?.proxies?.map((proxy, index: number) => ({ + id: `${proxy.name}_${Date.now()}_${index}`, + name: proxy.name, + type: proxy.type, + delay: undefined, + })) || []; + onUpdateChain(chainItems); + setHasUnsavedChanges(false); + } catch (parseError) { + console.error("Failed to parse YAML:", parseError); + onUpdateChain([]); + } + }) + .catch((importError) => { + // Fallback: try to parse as JSON if YAML is not available + console.warn( + "js-yaml not available, trying JSON parse:", + importError, + ); + try { + const parsedConfig = JSON.parse( + chainConfigData, + ) as ParsedChainConfig; + const chainItems = + parsedConfig?.proxies?.map((proxy, index: number) => ({ + id: `${proxy.name}_${Date.now()}_${index}`, + name: proxy.name, + type: proxy.type, + delay: undefined, + })) || []; + onUpdateChain(chainItems); + setHasUnsavedChanges(false); + } catch (jsonError) { + console.error("Failed to parse as JSON either:", jsonError); + onUpdateChain([]); + } + }); + } catch (error) { + console.error("Failed to process chain config data:", error); + onUpdateChain([]); + } + } else if (chainConfigData === "") { + // Empty string means no proxies available, show empty state + onUpdateChain([]); + setHasUnsavedChanges(false); + } + }, [chainConfigData, onUpdateChain]); + + // 定时更新延迟数据 + useEffect(() => { + if (!proxies?.records) return; + + const updateDelays = () => { + const currentChain = proxyChainRef.current; + if (currentChain.length === 0) return; + + const updatedChain = currentChain.map((item) => { + const proxyRecord = proxies.records[item.name]; + if ( + proxyRecord && + proxyRecord.history && + proxyRecord.history.length > 0 + ) { + const latestDelay = + proxyRecord.history[proxyRecord.history.length - 1].delay; + return { ...item, delay: latestDelay }; + } + return item; + }); + + // 只有在延迟数据确实发生变化时才更新 + const hasChanged = updatedChain.some( + (item, index) => item.delay !== currentChain[index]?.delay, + ); + + if (hasChanged) { + onUpdateChainRef.current(updatedChain); + } + }; + + // 立即更新一次延迟 + updateDelays(); + + // 设置定时器,每5秒更新一次延迟 + const interval = setInterval(updateDelays, 5000); + + return () => clearInterval(interval); + }, [proxies?.records]); // 只依赖proxies.records + + return ( + + + {t("Chain Proxy Config")} + + {proxyChain.length > 0 && ( + { + updateProxyChainConfigInRuntime(null); + onUpdateChain([]); + setHasUnsavedChanges(false); + }} + sx={{ + color: theme.palette.error.main, + "&:hover": { + backgroundColor: theme.palette.error.light + "20", + }, + }} + title={t("Delete Chain Config") || "删除链式配置"} + > + + + )} + + + + + + {proxyChain.length === 1 + ? t( + "Chain proxy requires at least 2 nodes. Please add one more node.", + ) || "链式代理至少需要2个节点,请再添加一个节点。" + : t("Click nodes in order to add to proxy chain") || + "按顺序点击节点添加到代理链中"} + + + + {proxyChain.length === 0 ? ( + + {t("No proxy chain configured")} + + ) : ( + + proxy.id)} + strategy={verticalListSortingStrategy} + > + + {proxyChain.map((proxy, index) => ( + + ))} + + + + )} + + + ); +}; diff --git a/clash-verge-rev/src/components/proxy/proxy-group-navigator.tsx b/clash-verge-rev/src/components/proxy/proxy-group-navigator.tsx new file mode 100644 index 0000000000..4999da49ed --- /dev/null +++ b/clash-verge-rev/src/components/proxy/proxy-group-navigator.tsx @@ -0,0 +1,94 @@ +import { Box, Button, Tooltip } from "@mui/material"; +import { useCallback, useMemo } from "react"; + +interface ProxyGroupNavigatorProps { + proxyGroupNames: string[]; + onGroupLocation: (groupName: string) => void; +} + +// 提取代理组名的第一个字符 +const getGroupDisplayChar = (groupName: string): string => { + if (!groupName) return "?"; + + // 直接返回第一个字符,支持表情符号 + const firstChar = Array.from(groupName)[0]; + return firstChar || "?"; +}; + +export const ProxyGroupNavigator = ({ + proxyGroupNames, + onGroupLocation, +}: ProxyGroupNavigatorProps) => { + const handleGroupClick = useCallback( + (groupName: string) => { + onGroupLocation(groupName); + }, + [onGroupLocation], + ); + + // 处理代理组数据,去重和排序 + const processedGroups = useMemo(() => { + return proxyGroupNames + .filter((name) => name && name.trim()) + .map((name) => ({ + name, + displayChar: getGroupDisplayChar(name), + })); + }, [proxyGroupNames]); + + if (processedGroups.length === 0) { + return null; + } + + return ( + + {processedGroups.map(({ name, displayChar }) => ( + + + + ))} + + ); +}; diff --git a/clash-verge-rev/src/components/proxy/proxy-groups.tsx b/clash-verge-rev/src/components/proxy/proxy-groups.tsx index a4d56d8746..2622dfb1b2 100644 --- a/clash-verge-rev/src/components/proxy/proxy-groups.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-groups.tsx @@ -1,250 +1,103 @@ -import { useRef, useState, useEffect, useCallback, useMemo } from "react"; -import { useLockFn } from "ahooks"; -import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; +import { ExpandMoreRounded } from "@mui/icons-material"; import { - getConnections, - providerHealthCheck, - updateProxy, - deleteConnection, - getGroupProxyDelays, -} from "@/services/cmds"; -import { forceRefreshProxies } from "@/services/cmds"; -import { useProfiles } from "@/hooks/use-profiles"; -import { useVerge } from "@/hooks/use-verge"; -import { BaseEmpty } from "../base"; -import { useRenderList } from "./use-render-list"; -import { ProxyRender } from "./proxy-render"; -import delayManager from "@/services/delay"; + Alert, + Box, + Chip, + IconButton, + Menu, + MenuItem, + Snackbar, + Typography, +} from "@mui/material"; +import { useLockFn } from "ahooks"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; +import useSWR from "swr"; + +import { useProxySelection } from "@/hooks/use-proxy-selection"; +import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-context"; +import { + getGroupProxyDelays, + getRuntimeConfig, + providerHealthCheck, + updateProxyChainConfigInRuntime, +} from "@/services/cmds"; +import delayManager from "@/services/delay"; + +import { BaseEmpty } from "../base"; import { ScrollTopButton } from "../layout/scroll-top-button"; -import { Box, styled } from "@mui/material"; -import { memo } from "react"; -import { createPortal } from "react-dom"; -// 将选择器组件抽离出来,避免主组件重渲染时重复创建样式 -const AlphabetSelector = styled(Box)(({ theme }) => ({ - position: "fixed", - right: 4, - top: "50%", - transform: "translateY(-50%)", - display: "flex", - flexDirection: "column", - background: "transparent", - zIndex: 1000, - gap: "2px", - // padding: "4px 2px", - willChange: "transform", - "&:hover": { - background: theme.palette.background.paper, - boxShadow: theme.shadows[2], - borderRadius: "8px", - }, - "& .scroll-container": { - overflow: "hidden", - maxHeight: "inherit", - willChange: "transform", - }, - "& .letter-container": { - display: "flex", - flexDirection: "column", - gap: "2px", - transition: "transform 0.2s ease", - willChange: "transform", - }, - "& .letter": { - padding: "1px 4px", - fontSize: "12px", - cursor: "pointer", - fontFamily: - "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif", - color: theme.palette.text.secondary, - position: "relative", - width: "1.5em", - height: "1.5em", - display: "flex", - alignItems: "center", - justifyContent: "center", - transition: "all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1)", - transform: "scale(1) translateZ(0)", - backfaceVisibility: "hidden", - borderRadius: "6px", - "&:hover": { - color: theme.palette.primary.main, - transform: "scale(1.4) translateZ(0)", - backgroundColor: theme.palette.action.hover, - }, - }, -})); - -// 创建一个单独的 Tooltip 组件 -const Tooltip = styled("div")(({ theme }) => ({ - position: "fixed", - background: theme.palette.background.paper, - padding: "4px 8px", - borderRadius: "6px", - boxShadow: theme.shadows[3], - whiteSpace: "nowrap", - fontSize: "16px", - color: theme.palette.text.primary, - pointerEvents: "none", - "&::after": { - content: '""', - position: "absolute", - right: "-4px", - top: "50%", - transform: "translateY(-50%)", - width: 0, - height: 0, - borderTop: "4px solid transparent", - borderBottom: "4px solid transparent", - borderLeft: `4px solid ${theme.palette.background.paper}`, - }, -})); - -// 抽离字母选择器子组件 -const LetterItem = memo( - ({ - name, - onClick, - getFirstChar, - enableAutoScroll = true, - }: { - name: string; - onClick: (name: string) => void; - getFirstChar: (str: string) => string; - enableAutoScroll?: boolean; - }) => { - const [showTooltip, setShowTooltip] = useState(false); - const letterRef = useRef(null); - const [tooltipPosition, setTooltipPosition] = useState({ - top: 0, - right: 0, - }); - const hoverTimeoutRef = useRef>(undefined); - - const updateTooltipPosition = useCallback(() => { - if (!letterRef.current) return; - const rect = letterRef.current.getBoundingClientRect(); - setTooltipPosition({ - top: rect.top + rect.height / 2, - right: window.innerWidth - rect.left + 8, - }); - }, []); - - useEffect(() => { - if (showTooltip) { - updateTooltipPosition(); - } - }, [showTooltip, updateTooltipPosition]); - - const handleMouseEnter = useCallback(() => { - setShowTooltip(true); - // 只有在启用自动滚动时才触发滚动 - if (enableAutoScroll) { - // 添加 100ms 的延迟,避免鼠标快速划过时触发滚动 - hoverTimeoutRef.current = setTimeout(() => { - onClick(name); - }, 100); - } - }, [name, onClick, enableAutoScroll]); - - const handleMouseLeave = useCallback(() => { - setShowTooltip(false); - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - } - }, []); - - useEffect(() => { - return () => { - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - } - }; - }, []); - - return ( - <> -
onClick(name)} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - > - {getFirstChar(name)} -
- {showTooltip && - createPortal( - - {name} - , - document.body, - )} - - ); - }, -); +import { ProxyChain } from "./proxy-chain"; +import { ProxyGroupNavigator } from "./proxy-group-navigator"; +import { ProxyRender } from "./proxy-render"; +import { useRenderList } from "./use-render-list"; interface Props { mode: string; + isChainMode?: boolean; + chainConfigData?: string | null; +} + +interface ProxyChainItem { + id: string; + name: string; + type?: string; + delay?: number; } export const ProxyGroups = (props: Props) => { const { t } = useTranslation(); - const { mode } = props; - - const { renderList, onProxies, onHeadState } = useRenderList(mode); + const { mode, isChainMode = false, chainConfigData } = props; + const [proxyChain, setProxyChain] = useState([]); + const [selectedGroup, setSelectedGroup] = useState(null); + const [ruleMenuAnchor, setRuleMenuAnchor] = useState( + null, + ); + const [duplicateWarning, setDuplicateWarning] = useState<{ + open: boolean; + message: string; + }>({ open: false, message: "" }); const { verge } = useVerge(); - const { current, patchCurrent } = useProfiles(); + const { proxies: proxiesData } = useAppData(); + + // 当链式代理模式且规则模式下,如果没有选择代理组,默认选择第一个 + useEffect(() => { + if ( + isChainMode && + mode === "rule" && + !selectedGroup && + proxiesData?.groups?.length > 0 + ) { + setSelectedGroup(proxiesData.groups[0].name); + } + }, [isChainMode, mode, selectedGroup, proxiesData]); + + const { renderList, onProxies, onHeadState } = useRenderList( + mode, + isChainMode, + selectedGroup, + ); + + // 统代理选择 + const { handleProxyGroupChange } = useProxySelection({ + onSuccess: () => { + onProxies(); + }, + onError: (error) => { + console.error("代理切换失败", error); + onProxies(); + }, + }); - // 获取自动滚动开关状态,默认为 true - const enableAutoScroll = verge?.enable_hover_jump_navigator ?? true; const timeout = verge?.default_latency_timeout || 10000; const virtuosoRef = useRef(null); const scrollPositionRef = useRef>({}); const [showScrollTop, setShowScrollTop] = useState(false); const scrollerRef = useRef(null); - const letterContainerRef = useRef(null); - const alphabetSelectorRef = useRef(null); - const [maxHeight, setMaxHeight] = useState("auto"); - - // 使用useMemo缓存字母索引数据 - const { groupFirstLetters, letterIndexMap } = useMemo(() => { - const letters = new Set(); - const indexMap: Record = {}; - - renderList.forEach((item, index) => { - if (item.type === 0) { - const fullName = item.group.name; - letters.add(fullName); - if (!(fullName in indexMap)) { - indexMap[fullName] = index; - } - } - }); - - return { - groupFirstLetters: Array.from(letters), - letterIndexMap: indexMap, - }; - }, [renderList]); - - // 缓存getFirstChar函数 - const getFirstChar = useCallback((str: string) => { - const regex = - /\p{Regional_Indicator}{2}|\p{Extended_Pictographic}|\p{L}|\p{N}|./u; - const match = str.match(regex); - return match ? match[0] : str.charAt(0); - }, []); // 从 localStorage 恢复滚动位置 useEffect(() => { @@ -320,59 +173,86 @@ export const ProxyGroups = (props: Props) => { saveScrollPosition(0); }, [saveScrollPosition]); - // 处理字母点击,使用useCallback - const handleLetterClick = useCallback( - (name: string) => { - const index = letterIndexMap[name]; - if (index !== undefined) { - virtuosoRef.current?.scrollToIndex({ - index, - align: "start", - behavior: "smooth", - }); - } - }, - [letterIndexMap], - ); + // 关闭重复节点警告 + const handleCloseDuplicateWarning = useCallback(() => { + setDuplicateWarning({ open: false, message: "" }); + }, []); + + // 获取当前选中的代理组信息 + const getCurrentGroup = useCallback(() => { + if (!selectedGroup || !proxiesData?.groups) return null; + return proxiesData.groups.find( + (group: any) => group.name === selectedGroup, + ); + }, [selectedGroup, proxiesData]); + + // 获取可用的代理组列表 + const getAvailableGroups = useCallback(() => { + return proxiesData?.groups || []; + }, [proxiesData]); + + // 处理代理组选择菜单 + const handleGroupMenuOpen = (event: React.MouseEvent) => { + setRuleMenuAnchor(event.currentTarget); + }; + + const handleGroupMenuClose = () => { + setRuleMenuAnchor(null); + }; + + const handleGroupSelect = (groupName: string) => { + setSelectedGroup(groupName); + handleGroupMenuClose(); + + // 在链式代理模式的规则模式下,切换代理组时清空链式代理配置 + if (isChainMode && mode === "rule") { + updateProxyChainConfigInRuntime(null); + // 同时清空右侧链式代理配置 + setProxyChain([]); + } + }; + + const currentGroup = getCurrentGroup(); + const availableGroups = getAvailableGroups(); + + const handleChangeProxy = useCallback( + (group: IProxyGroupItem, proxy: IProxyItem) => { + if (isChainMode) { + // 使用函数式更新来避免状态延迟问题 + setProxyChain((prev) => { + // 检查是否已经存在相同名称的代理,防止重复添加 + if (prev.some((item) => item.name === proxy.name)) { + const warningMessage = t("Proxy node already exists in chain"); + setDuplicateWarning({ + open: true, + message: warningMessage, + }); + return prev; // 返回原来的状态,不做任何更改 + } + + // 安全获取延迟数据,如果没有延迟数据则设为 undefined + const delay = + proxy.history && proxy.history.length > 0 + ? proxy.history[proxy.history.length - 1].delay + : undefined; + + const chainItem: ProxyChainItem = { + id: `${proxy.name}_${Date.now()}`, + name: proxy.name, + type: proxy.type, + delay: delay, + }; + + return [...prev, chainItem]; + }); + return; + } - // 切换分组的节点代理 - const handleChangeProxy = useLockFn( - async (group: IProxyGroupItem, proxy: IProxyItem) => { if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return; - const { name, now } = group; - await updateProxy(name, proxy.name); - - await forceRefreshProxies(); - - onProxies(); - - // 断开连接 - if (verge?.auto_close_connection) { - getConnections().then(({ connections }) => { - connections.forEach((conn) => { - if (conn.chains.includes(now!)) { - deleteConnection(conn.id); - } - }); - }); - } - - // 保存到selected中 - if (!current) return; - if (!current.selected) current.selected = []; - - const index = current.selected.findIndex( - (item) => item.name === group.name, - ); - - if (index < 0) { - current.selected.push({ name, now: proxy.name }); - } else { - current.selected[index] = { name, now: proxy.name }; - } - await patchCurrent({ selected: current.selected }); + handleProxyGroupChange(group, proxy); }, + [handleProxyGroupChange, isChainMode, t], ); // 测全部延迟 @@ -445,78 +325,249 @@ export const ProxyGroups = (props: Props) => { } }; - // 添加滚轮事件处理函数 - 改进为只在悬停时触发 - const handleWheel = useCallback((e: WheelEvent) => { - // 只有当鼠标在字母选择器上时才处理滚轮事件 - if (!alphabetSelectorRef.current?.contains(e.target as Node)) return; + // 获取运行时配置 + const { data: runtimeConfig } = useSWR("getRuntimeConfig", getRuntimeConfig, { + revalidateOnFocus: false, + revalidateIfStale: true, + }); - e.preventDefault(); - if (!letterContainerRef.current) return; + // 获取所有代理组名称 + const getProxyGroupNames = useCallback(() => { + const config = runtimeConfig as any; + if (!config?.["proxy-groups"]) return []; - const container = letterContainerRef.current; - const scrollAmount = e.deltaY; - const currentTransform = new WebKitCSSMatrix(container.style.transform); - const currentY = currentTransform.m42 || 0; + return config["proxy-groups"] + .map((group: any) => group.name) + .filter((name: string) => name && name.trim() !== ""); + }, [runtimeConfig]); - const containerHeight = container.getBoundingClientRect().height; - const parentHeight = - container.parentElement?.getBoundingClientRect().height || 0; - const maxScroll = Math.max(0, containerHeight - parentHeight); + // 定位到指定的代理组 + const handleGroupLocationByName = useCallback( + (groupName: string) => { + const index = renderList.findIndex( + (item) => item.type === 0 && item.group?.name === groupName, + ); - let newY = currentY - scrollAmount; - newY = Math.min(0, Math.max(-maxScroll, newY)); + if (index >= 0) { + virtuosoRef.current?.scrollToIndex?.({ + index, + align: "start", + behavior: "smooth", + }); + } + }, + [renderList], + ); - container.style.transform = `translateY(${newY}px)`; - }, []); - - // 添加和移除滚轮事件监听 - useEffect(() => { - const container = letterContainerRef.current?.parentElement; - if (container) { - container.addEventListener("wheel", handleWheel, { passive: false }); - return () => { - container.removeEventListener("wheel", handleWheel); - }; - } - }, [handleWheel]); - - // 监听窗口大小变化 - // layout effect runs before paint - useEffect(() => { - // 添加窗口大小变化监听和最大高度计算 - const updateMaxHeight = () => { - if (!alphabetSelectorRef.current) return; - - const windowHeight = window.innerHeight; - const bottomMargin = 60; // 底部边距 - const topMargin = bottomMargin * 2; // 顶部边距是底部的2倍 - const availableHeight = windowHeight - (topMargin + bottomMargin); - - // 调整选择器的位置,使其偏下 - const offsetPercentage = - (((topMargin - bottomMargin) / windowHeight) * 100) / 2; - alphabetSelectorRef.current.style.top = `calc(48% + ${offsetPercentage}vh)`; - - setMaxHeight(`${availableHeight}px`); - }; - - updateMaxHeight(); - - window.addEventListener("resize", updateMaxHeight); - - return () => { - window.removeEventListener("resize", updateMaxHeight); - }; - }, []); + const proxyGroupNames = useMemo( + () => getProxyGroupNames(), + [getProxyGroupNames], + ); if (mode === "direct") { return ; } + if (isChainMode) { + // 获取所有代理组 + const proxyGroups = proxiesData?.groups || []; + + return ( + <> + + + {/* 代理规则标题和代理组按钮栏 */} + {mode === "rule" && proxyGroups.length > 0 && ( + + {/* 代理规则标题 */} + + + + {t("Proxy Rules")} + + {currentGroup && ( + + + + )} + + + {availableGroups.length > 0 && ( + + + {t("Select Rules")} + + + + )} + + + )} + + 0 + ? "calc(100% - 80px)" // 只有标题的高度 + : "calc(100% - 14px)", + }} + totalCount={renderList.length} + increaseViewportBy={{ top: 200, bottom: 200 }} + overscan={150} + defaultItemHeight={56} + scrollerRef={(ref) => { + scrollerRef.current = ref as Element; + }} + components={{ + Footer: () =>
, + }} + initialScrollTop={scrollPositionRef.current[mode]} + computeItemKey={(index) => renderList[index].key} + itemContent={(index) => ( + + )} + /> + + + + + + + + + + + {duplicateWarning.message} + + + + {/* 代理组选择菜单 */} + + {availableGroups.map((group: any, index: number) => ( + handleGroupSelect(group.name)} + selected={selectedGroup === group.name} + sx={{ + fontSize: "14px", + py: 1, + }} + > + + + {group.name} + + + {group.type} · {group.all.length} 节点 + + + + ))} + {availableGroups.length === 0 && ( + + + 暂无可用代理组 + + + )} + + + ); + } + return (
+ {/* 代理组导航栏 */} + {mode === "rule" && ( + + )} + { )} /> - - -
-
- {groupFirstLetters.map((name) => ( - - ))} -
-
-
); }; @@ -594,15 +629,3 @@ function throttle any>( } }; } - -// 保留防抖函数以兼容其他地方可能的使用 -function debounce any>( - func: T, - wait: number, -): (...args: Parameters) => void { - let timeout: ReturnType | null = null; - return (...args: Parameters) => { - if (timeout) clearTimeout(timeout); - timeout = setTimeout(() => func(...args), wait); - }; -} diff --git a/clash-verge-rev/src/components/proxy/proxy-head.tsx b/clash-verge-rev/src/components/proxy/proxy-head.tsx index 205f729d30..fd0315450f 100644 --- a/clash-verge-rev/src/components/proxy/proxy-head.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-head.tsx @@ -1,6 +1,3 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Box, IconButton, TextField, SxProps } from "@mui/material"; import { AccessTimeRounded, MyLocationRounded, @@ -14,11 +11,16 @@ import { SortByAlphaRounded, SortRounded, } from "@mui/icons-material"; +import { Box, IconButton, TextField, SxProps } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + import { useVerge } from "@/hooks/use-verge"; -import type { HeadState } from "./use-head-state"; -import type { ProxySortType } from "./use-filter-sort"; import delayManager from "@/services/delay"; +import type { ProxySortType } from "./use-filter-sort"; +import type { HeadState } from "./use-head-state"; + interface Props { sx?: SxProps; url?: string; @@ -29,9 +31,15 @@ interface Props { onHeadState: (val: Partial) => void; } -export const ProxyHead = (props: Props) => { - const { sx = {}, url, groupName, headState, onHeadState } = props; - +export const ProxyHead = ({ + sx = {}, + url, + groupName, + headState, + onHeadState, + onLocation, + onCheckDelay, +}: Props) => { const { showType, sortType, filterText, textState, testUrl } = headState; const { t } = useTranslation(); @@ -44,13 +52,11 @@ export const ProxyHead = (props: Props) => { }, []); const { verge } = useVerge(); + const default_latency_test = verge!.default_latency_test!; useEffect(() => { - delayManager.setUrl( - groupName, - testUrl || url || verge?.default_latency_test!, - ); - }, [groupName, testUrl, verge?.default_latency_test]); + delayManager.setUrl(groupName, testUrl || url || default_latency_test); + }, [groupName, testUrl, default_latency_test, url]); return ( @@ -58,7 +64,7 @@ export const ProxyHead = (props: Props) => { size="small" color="inherit" title={t("locate")} - onClick={props.onLocation} + onClick={onLocation} > @@ -74,7 +80,7 @@ export const ProxyHead = (props: Props) => { console.log(`[ProxyHead] 使用自定义测试URL: ${testUrl}`); onHeadState({ textState: "url" }); } - props.onCheckDelay(); + onCheckDelay(); }} > diff --git a/clash-verge-rev/src/components/proxy/proxy-item-mini.tsx b/clash-verge-rev/src/components/proxy/proxy-item-mini.tsx index 55504a1940..aa6e9a6bfb 100644 --- a/clash-verge-rev/src/components/proxy/proxy-item-mini.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-item-mini.tsx @@ -1,12 +1,13 @@ -import { useEffect, useState } from "react"; -import { useLockFn } from "ahooks"; import { CheckCircleOutlineRounded } from "@mui/icons-material"; import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material"; -import { BaseLoading } from "@/components/base"; -import delayManager from "@/services/delay"; -import { useVerge } from "@/hooks/use-verge"; +import { useLockFn } from "ahooks"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { BaseLoading } from "@/components/base"; +import { useVerge } from "@/hooks/use-verge"; +import delayManager from "@/services/delay"; + interface Props { group: IProxyGroupItem; proxy: IProxyItem; @@ -251,7 +252,7 @@ const Widget = styled(Box)(({ theme: { typography } }) => ({ const TypeBox = styled(Box, { shouldForwardProp: (prop) => prop !== "component", -})<{ component?: React.ElementType }>(({ theme: { palette, typography } }) => ({ +})<{ component?: React.ElementType }>(({ theme: { typography } }) => ({ display: "inline-block", border: "1px solid #ccc", borderColor: "text.secondary", diff --git a/clash-verge-rev/src/components/proxy/proxy-item.tsx b/clash-verge-rev/src/components/proxy/proxy-item.tsx index 12bf50ab07..82308b0ec2 100644 --- a/clash-verge-rev/src/components/proxy/proxy-item.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-item.tsx @@ -1,5 +1,3 @@ -import { useEffect, useState } from "react"; -import { useLockFn } from "ahooks"; import { CheckCircleOutlineRounded } from "@mui/icons-material"; import { alpha, @@ -12,9 +10,12 @@ import { SxProps, Theme, } from "@mui/material"; +import { useLockFn } from "ahooks"; +import { useEffect, useState } from "react"; + import { BaseLoading } from "@/components/base"; -import delayManager from "@/services/delay"; import { useVerge } from "@/hooks/use-verge"; +import delayManager from "@/services/delay"; interface Props { group: IProxyGroupItem; @@ -60,12 +61,12 @@ export const ProxyItem = (props: Props) => { return () => { delayManager.removeListener(proxy.name, group.name); }; - }, [proxy.name, group.name]); + }, [proxy.name, group.name, isPreset]); useEffect(() => { if (!proxy) return; setDelay(delayManager.getDelayFix(proxy, group.name)); - }, [proxy]); + }, [group.name, proxy]); const onDelay = useLockFn(async () => { setDelay(-2); diff --git a/clash-verge-rev/src/components/proxy/proxy-render.tsx b/clash-verge-rev/src/components/proxy/proxy-render.tsx index d8022a9369..5d565286b5 100644 --- a/clash-verge-rev/src/components/proxy/proxy-render.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-render.tsx @@ -1,3 +1,8 @@ +import { + ExpandLessRounded, + ExpandMoreRounded, + InboxRounded, +} from "@mui/icons-material"; import { alpha, Box, @@ -8,26 +13,24 @@ import { Chip, Tooltip, } from "@mui/material"; -import { - ExpandLessRounded, - ExpandMoreRounded, - InboxRounded, -} from "@mui/icons-material"; -import { HeadState } from "./use-head-state"; +import { convertFileSrc } from "@tauri-apps/api/core"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { useVerge } from "@/hooks/use-verge"; +import { downloadIconCache } from "@/services/cmds"; +import { useThemeMode } from "@/services/states"; + import { ProxyHead } from "./proxy-head"; import { ProxyItem } from "./proxy-item"; import { ProxyItemMini } from "./proxy-item-mini"; +import { HeadState } from "./use-head-state"; import type { IRenderItem } from "./use-render-list"; -import { useVerge } from "@/hooks/use-verge"; -import { useThemeMode } from "@/services/states"; -import { useEffect, useMemo, useState } from "react"; -import { convertFileSrc } from "@tauri-apps/api/core"; -import { downloadIconCache } from "@/services/cmds"; -import { useTranslation } from "react-i18next"; interface RenderProps { item: IRenderItem; indent: boolean; + isChainMode?: boolean; onLocation: (group: IRenderItem["group"]) => void; onCheckAll: (groupName: string) => void; onHeadState: (groupName: string, patch: Partial) => void; @@ -39,8 +42,15 @@ interface RenderProps { export const ProxyRender = (props: RenderProps) => { const { t } = useTranslation(); - const { indent, item, onLocation, onCheckAll, onHeadState, onChangeProxy } = - props; + const { + indent, + item, + onLocation, + onCheckAll, + onHeadState, + onChangeProxy, + isChainMode = false, + } = props; const { type, group, headState, proxy, proxyCol } = item; const { verge } = useVerge(); const enable_group_icon = verge?.enable_group_icon ?? true; diff --git a/clash-verge-rev/src/components/proxy/use-filter-sort.ts b/clash-verge-rev/src/components/proxy/use-filter-sort.ts index ca7c6527ce..e09f682f8b 100644 --- a/clash-verge-rev/src/components/proxy/use-filter-sort.ts +++ b/clash-verge-rev/src/components/proxy/use-filter-sort.ts @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from "react"; + import delayManager from "@/services/delay"; // default | delay | alphabet @@ -10,7 +11,7 @@ export default function useFilterSort( filterText: string, sortType: ProxySortType, ) { - const [refresh, setRefresh] = useState({}); + const [, setRefresh] = useState({}); useEffect(() => { let last = 0; @@ -33,7 +34,7 @@ export default function useFilterSort( const fp = filterProxies(proxies, groupName, filterText); const sp = sortProxies(fp, groupName, sortType); return sp; - }, [proxies, groupName, filterText, sortType, refresh]); + }, [proxies, groupName, filterText, sortType]); } export function filterSort( diff --git a/clash-verge-rev/src/components/proxy/use-head-state.ts b/clash-verge-rev/src/components/proxy/use-head-state.ts index d6c3113555..427bdff801 100644 --- a/clash-verge-rev/src/components/proxy/use-head-state.ts +++ b/clash-verge-rev/src/components/proxy/use-head-state.ts @@ -1,7 +1,9 @@ import { useCallback, useEffect, useState } from "react"; -import { ProxySortType } from "./use-filter-sort"; + import { useProfiles } from "@/hooks/use-profiles"; +import { ProxySortType } from "./use-filter-sort"; + export interface HeadState { open?: boolean; showType: boolean; diff --git a/clash-verge-rev/src/components/proxy/use-render-list.ts b/clash-verge-rev/src/components/proxy/use-render-list.ts index 1e7f63757a..172d291569 100644 --- a/clash-verge-rev/src/components/proxy/use-render-list.ts +++ b/clash-verge-rev/src/components/proxy/use-render-list.ts @@ -1,13 +1,18 @@ import { useEffect, useMemo } from "react"; +import useSWR from "swr"; + import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-context"; +import { getRuntimeConfig } from "@/services/cmds"; +import delayManager from "@/services/delay"; + import { filterSort } from "./use-filter-sort"; -import { useWindowWidth } from "./use-window-width"; import { - useHeadStateNew, DEFAULT_STATE, + useHeadStateNew, type HeadState, } from "./use-head-state"; -import { useAppData } from "@/providers/app-data-provider"; +import { useWindowWidth } from "./use-window-width"; // 定义代理项接口 interface IProxyItem { @@ -88,13 +93,27 @@ const groupProxies = (list: T[], size: number): T[][] => { }, [] as T[][]); }; -export const useRenderList = (mode: string) => { +export const useRenderList = ( + mode: string, + isChainMode?: boolean, + selectedGroup?: string | null, +) => { // 使用全局数据提供者 const { proxies: proxiesData, refreshProxy } = useAppData(); const { verge } = useVerge(); const { width } = useWindowWidth(); const [headStates, setHeadState] = useHeadStateNew(); + // 获取运行时配置用于链式代理模式 + const { data: runtimeConfig } = useSWR( + isChainMode ? "getRuntimeConfig" : null, + getRuntimeConfig, + { + revalidateOnFocus: false, + revalidateIfStale: true, + }, + ); + // 计算列数 const col = useMemo( () => calculateColumns(width, verge?.proxy_layout_column || 6), @@ -115,10 +134,238 @@ export const useRenderList = (mode: string) => { } }, [proxiesData, mode, refreshProxy]); + // 链式代理模式节点自动计算延迟 + useEffect(() => { + if (!isChainMode || !runtimeConfig) return; + + const allProxies: IProxyItem[] = Object.values( + (runtimeConfig as any).proxies || {}, + ); + if (allProxies.length === 0) return; + + // 设置组监听器,当有延迟更新时自动刷新 + const groupListener = () => { + console.log("[ChainMode] 延迟更新,刷新UI"); + refreshProxy(); + }; + + delayManager.setGroupListener("chain-mode", groupListener); + + const calculateDelays = async () => { + try { + const timeout = verge?.default_latency_timeout || 10000; + const proxyNames = allProxies.map((proxy) => proxy.name); + + console.log(`[ChainMode] 开始计算 ${proxyNames.length} 个节点的延迟`); + + // 使用 delayManager 计算延迟,每个节点计算完成后会自动触发监听器刷新界面 + delayManager.checkListDelay(proxyNames, "chain-mode", timeout); + } catch (error) { + console.error("Failed to calculate delays for chain mode:", error); + } + }; + + // 延迟执行避免阻塞 + const handle = setTimeout(calculateDelays, 100); + + return () => { + clearTimeout(handle); + // 清理组监听器 + delayManager.removeGroupListener("chain-mode"); + }; + }, [ + isChainMode, + runtimeConfig, + verge?.default_latency_timeout, + refreshProxy, + ]); + // 处理渲染列表 const renderList: IRenderItem[] = useMemo(() => { if (!proxiesData) return []; + // 链式代理模式下,显示代理组和其节点 + if (isChainMode && runtimeConfig && mode === "rule") { + // 使用正常的规则模式代理组 + const allGroups = proxiesData.groups.length + ? proxiesData.groups + : [proxiesData.global!]; + + // 如果选择了特定代理组,只显示该组的节点 + if (selectedGroup) { + const targetGroup = allGroups.find( + (g: any) => g.name === selectedGroup, + ); + if (targetGroup) { + const proxies = filterSort(targetGroup.all, targetGroup.name, "", 0); + + if (col > 1) { + return groupProxies(proxies, col).map((proxyCol, colIndex) => ({ + type: 4, + key: `chain-col-${selectedGroup}-${colIndex}`, + group: targetGroup, + headState: DEFAULT_STATE, + col, + proxyCol, + provider: proxyCol[0]?.provider, + })); + } else { + return proxies.map((proxy) => ({ + type: 2, + key: `chain-${selectedGroup}-${proxy!.name}`, + group: targetGroup, + proxy, + headState: DEFAULT_STATE, + provider: proxy.provider, + })); + } + } + return []; + } + + // 如果没有选择特定组,显示第一个组的节点(如果有组的话) + if (allGroups.length > 0) { + const firstGroup = allGroups[0]; + const proxies = filterSort(firstGroup.all, firstGroup.name, "", 0); + + if (col > 1) { + return groupProxies(proxies, col).map((proxyCol, colIndex) => ({ + type: 4, + key: `chain-col-first-${colIndex}`, + group: firstGroup, + headState: DEFAULT_STATE, + col, + proxyCol, + provider: proxyCol[0]?.provider, + })); + } else { + return proxies.map((proxy) => ({ + type: 2, + key: `chain-first-${proxy!.name}`, + group: firstGroup, + proxy, + headState: DEFAULT_STATE, + provider: proxy.provider, + })); + } + } + + // 如果没有组,显示所有节点 + const allProxies: IProxyItem[] = allGroups.flatMap( + (group: any) => group.all, + ); + + // 为每个节点获取延迟信息 + const proxiesWithDelay = allProxies.map((proxy) => { + const delay = delayManager.getDelay(proxy.name, "chain-mode"); + return { + ...proxy, + // 如果delayManager有延迟数据,更新history + history: + delay >= 0 + ? [{ time: new Date().toISOString(), delay }] + : proxy.history || [], + }; + }); + + // 创建一个虚拟的组来容纳所有节点 + const virtualGroup: ProxyGroup = { + name: "All Proxies", + type: "Selector", + udp: false, + xudp: false, + tfo: false, + mptcp: false, + smux: false, + history: [], + now: "", + all: proxiesWithDelay, + }; + + if (col > 1) { + return groupProxies(proxiesWithDelay, col).map( + (proxyCol, colIndex) => ({ + type: 4, + key: `chain-col-all-${colIndex}`, + group: virtualGroup, + headState: DEFAULT_STATE, + col, + proxyCol, + provider: proxyCol[0]?.provider, + }), + ); + } else { + return proxiesWithDelay.map((proxy) => ({ + type: 2, + key: `chain-all-${proxy.name}`, + group: virtualGroup, + proxy, + headState: DEFAULT_STATE, + provider: proxy.provider, + })); + } + } + + // 链式代理模式下的其他模式(如global)仍显示所有节点 + if (isChainMode && runtimeConfig) { + // 从运行时配置直接获取 proxies 列表 (需要类型断言) + const allProxies: IProxyItem[] = Object.values( + (runtimeConfig as any).proxies || {}, + ); + + // 为每个节点获取延迟信息 + const proxiesWithDelay = allProxies.map((proxy) => { + const delay = delayManager.getDelay(proxy.name, "chain-mode"); + return { + ...proxy, + // 如果delayManager有延迟数据,更新history + history: + delay >= 0 + ? [{ time: new Date().toISOString(), delay }] + : proxy.history || [], + }; + }); + + // 创建一个虚拟的组来容纳所有节点 + const virtualGroup: ProxyGroup = { + name: "All Proxies", + type: "Selector", + udp: false, + xudp: false, + tfo: false, + mptcp: false, + smux: false, + history: [], + now: "", + all: proxiesWithDelay, + }; + + // 返回节点列表(不显示组头) + if (col > 1) { + return groupProxies(proxiesWithDelay, col).map( + (proxyCol, colIndex) => ({ + type: 4, + key: `chain-col-${colIndex}`, + group: virtualGroup, + headState: DEFAULT_STATE, + col, + proxyCol, + provider: proxyCol[0]?.provider, + }), + ); + } else { + return proxiesWithDelay.map((proxy) => ({ + type: 2, + key: `chain-${proxy.name}`, + group: virtualGroup, + proxy, + headState: DEFAULT_STATE, + provider: proxy.provider, + })); + } + } + + // 正常模式的渲染逻辑 const useRule = mode === "rule" || mode === "script"; const renderGroups = useRule && proxiesData.groups.length @@ -190,7 +437,15 @@ export const useRenderList = (mode: string) => { if (!useRule) return retList.slice(1); return retList.filter((item: IRenderItem) => !item.group.hidden); - }, [headStates, proxiesData, mode, col]); + }, [ + headStates, + proxiesData, + mode, + col, + isChainMode, + runtimeConfig, + selectedGroup, + ]); return { renderList, diff --git a/clash-verge-rev/src/components/rule/provider-button.tsx b/clash-verge-rev/src/components/rule/provider-button.tsx index ebad1553d5..9059f3e29f 100644 --- a/clash-verge-rev/src/components/rule/provider-button.tsx +++ b/clash-verge-rev/src/components/rule/provider-button.tsx @@ -1,27 +1,27 @@ -import { useState } from "react"; +import { RefreshRounded, StorageOutlined } from "@mui/icons-material"; import { - Button, Box, + Button, Dialog, - DialogTitle, - DialogContent, DialogActions, + DialogContent, + DialogTitle, + Divider, IconButton, List, ListItem, ListItemText, Typography, - Divider, alpha, styled, - useTheme, } from "@mui/material"; -import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; -import { ruleProviderUpdate } from "@/services/cmds"; -import { StorageOutlined, RefreshRounded } from "@mui/icons-material"; -import { useAppData } from "@/providers/app-data-provider"; import dayjs from "dayjs"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { useAppData } from "@/providers/app-data-context"; +import { ruleProviderUpdate } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; // 定义规则提供者类型 @@ -47,7 +47,6 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ export const ProviderButton = () => { const { t } = useTranslation(); - const theme = useTheme(); const [open, setOpen] = useState(false); const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData(); const [updating, setUpdating] = useState>({}); diff --git a/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx b/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx index d6cb9e51e9..6821e50d82 100644 --- a/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx @@ -1,9 +1,5 @@ -import { useState, useRef, memo, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { useForm } from "react-hook-form"; -import { useVerge } from "@/hooks/use-verge"; -import { isValidUrl } from "@/utils/helper"; -import { useLockFn } from "ahooks"; +import Visibility from "@mui/icons-material/Visibility"; +import VisibilityOff from "@mui/icons-material/VisibilityOff"; import { TextField, Button, @@ -12,12 +8,17 @@ import { IconButton, InputAdornment, } from "@mui/material"; -import Visibility from "@mui/icons-material/Visibility"; -import VisibilityOff from "@mui/icons-material/VisibilityOff"; +import { useLockFn } from "ahooks"; +import { useState, useRef, memo, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +import { useVerge } from "@/hooks/use-verge"; import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; +import { isValidUrl } from "@/utils/helper"; -export interface BackupConfigViewerProps { +interface BackupConfigViewerProps { onBackupSuccess: () => Promise; onSaveSuccess: () => Promise; onRefresh: () => Promise; diff --git a/clash-verge-rev/src/components/setting/mods/backup-table-viewer.tsx b/clash-verge-rev/src/components/setting/mods/backup-table-viewer.tsx index b6fb1d6cc0..e68da158c9 100644 --- a/clash-verge-rev/src/components/setting/mods/backup-table-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/backup-table-viewer.tsx @@ -1,4 +1,5 @@ -import { SVGProps, memo } from "react"; +import DeleteIcon from "@mui/icons-material/Delete"; +import RestoreIcon from "@mui/icons-material/Restore"; import { Box, Paper, @@ -14,15 +15,15 @@ import { } from "@mui/material"; import { Typography } from "@mui/material"; import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; import { Dayjs } from "dayjs"; +import { SVGProps, memo } from "react"; +import { useTranslation } from "react-i18next"; + import { deleteWebdavBackup, restoreWebDavBackup, restartApp, } from "@/services/cmds"; -import DeleteIcon from "@mui/icons-material/Delete"; -import RestoreIcon from "@mui/icons-material/Restore"; import { showNotice } from "@/services/noticeService"; export type BackupFile = IWebDavFile & { @@ -33,7 +34,7 @@ export type BackupFile = IWebDavFile & { export const DEFAULT_ROWS_PER_PAGE = 5; -export interface BackupTableViewerProps { +interface BackupTableViewerProps { datasource: BackupFile[]; page: number; onPageChange: ( diff --git a/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx b/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx index b5a52c07c0..ac24081f9d 100644 --- a/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx @@ -1,30 +1,25 @@ -import { - forwardRef, - useImperativeHandle, - useState, - useCallback, - useMemo, -} from "react"; -import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef } from "@/components/base"; -import getSystem from "@/utils/get-system"; -import { BaseLoadingOverlay } from "@/components/base"; +import { Box, Divider, Paper } from "@mui/material"; import dayjs from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat"; +import type { Ref } from "react"; +import { useCallback, useImperativeHandle, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { BaseDialog, BaseLoadingOverlay, DialogRef } from "@/components/base"; +import { listWebDavBackup } from "@/services/cmds"; + +import { BackupConfigViewer } from "./backup-config-viewer"; import { - BackupTableViewer, BackupFile, + BackupTableViewer, DEFAULT_ROWS_PER_PAGE, } from "./backup-table-viewer"; -import { BackupConfigViewer } from "./backup-config-viewer"; -import { Box, Paper, Divider } from "@mui/material"; -import { listWebDavBackup } from "@/services/cmds"; dayjs.extend(customParseFormat); const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss"; const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/; -export const BackupViewer = forwardRef((props, ref) => { +export function BackupViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -33,8 +28,6 @@ export const BackupViewer = forwardRef((props, ref) => { const [total, setTotal] = useState(0); const [page, setPage] = useState(0); - const OS = getSystem(); - useImperativeHandle(ref, () => ({ open: () => { setOpen(true); @@ -132,4 +125,4 @@ export const BackupViewer = forwardRef((props, ref) => { ); -}); +} diff --git a/clash-verge-rev/src/components/setting/mods/clash-core-viewer.tsx b/clash-verge-rev/src/components/setting/mods/clash-core-viewer.tsx index 1b44236f55..373b61fe84 100644 --- a/clash-verge-rev/src/components/setting/mods/clash-core-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/clash-core-viewer.tsx @@ -1,14 +1,8 @@ -import { mutate } from "swr"; -import { forwardRef, useImperativeHandle, useState } from "react"; -import { BaseDialog, DialogRef } from "@/components/base"; -import { useTranslation } from "react-i18next"; -import { useVerge } from "@/hooks/use-verge"; -import { useLockFn } from "ahooks"; -import { LoadingButton } from "@mui/lab"; import { - SwitchAccessShortcutRounded, RestartAltRounded, + SwitchAccessShortcutRounded, } from "@mui/icons-material"; +import { LoadingButton } from "@mui/lab"; import { Box, Chip, @@ -17,11 +11,20 @@ import { ListItemButton, ListItemText, } from "@mui/material"; -import { changeClashCore, restartCore } from "@/services/cmds"; +import { useLockFn } from "ahooks"; +import type { Ref } from "react"; +import { useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { mutate } from "swr"; + +import { BaseDialog, DialogRef } from "@/components/base"; +import { useVerge } from "@/hooks/use-verge"; import { + changeClashCore, closeAllConnections, - upgradeCore, forceRefreshClashConfig, + restartCore, + upgradeCore, } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; @@ -30,7 +33,7 @@ const VALID_CORE = [ { name: "Mihomo Alpha", core: "verge-mihomo-alpha", chip: "Alpha Version" }, ]; -export const ClashCoreViewer = forwardRef((props, ref) => { +export function ClashCoreViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const { verge, mutateVerge } = useVerge(); @@ -168,4 +171,4 @@ export const ClashCoreViewer = forwardRef((props, ref) => { ); -}); +} diff --git a/clash-verge-rev/src/components/setting/mods/clash-port-viewer.tsx b/clash-verge-rev/src/components/setting/mods/clash-port-viewer.tsx index f4a9b1f803..ec49580400 100644 --- a/clash-verge-rev/src/components/setting/mods/clash-port-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/clash-port-viewer.tsx @@ -1,8 +1,3 @@ -import { BaseDialog, Switch } from "@/components/base"; -import { useClashInfo } from "@/hooks/use-clash"; -import { useVerge } from "@/hooks/use-verge"; -import { showNotice } from "@/services/noticeService"; -import getSystem from "@/utils/get-system"; import { Shuffle } from "@mui/icons-material"; import { CircularProgress, @@ -17,6 +12,12 @@ import { useLockFn, useRequest } from "ahooks"; import { forwardRef, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; +import { BaseDialog, Switch } from "@/components/base"; +import { useClashInfo } from "@/hooks/use-clash"; +import { useVerge } from "@/hooks/use-verge"; +import { showNotice } from "@/services/noticeService"; +import getSystem from "@/utils/get-system"; + const OS = getSystem(); interface ClashPortViewerProps {} @@ -150,22 +151,6 @@ export const ClashPortViewer = forwardRef< await saveSettings({ clashConfig, vergeConfig }); }); - // 优化的数字输入处理 - const handleNumericChange = - (setter: (value: number) => void) => - (e: React.ChangeEvent) => { - const value = e.target.value.replace(/\D+/, ""); - if (value === "") { - setter(0); - return; - } - - const num = parseInt(value, 10); - if (!isNaN(num) && num >= 0 && num <= 65535) { - setter(num); - } - }; - return ( ((_, ref) => { const { t } = useTranslation(); diff --git a/clash-verge-rev/src/components/setting/mods/controller-viewer.tsx b/clash-verge-rev/src/components/setting/mods/controller-viewer.tsx index ced6e69fdc..105fb34e91 100644 --- a/clash-verge-rev/src/components/setting/mods/controller-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/controller-viewer.tsx @@ -1,7 +1,3 @@ -import { BaseDialog, DialogRef, Switch } from "@/components/base"; -import { useClashInfo } from "@/hooks/use-clash"; -import { useVerge } from "@/hooks/use-verge"; -import { showNotice } from "@/services/noticeService"; import { ContentCopy } from "@mui/icons-material"; import { Alert, @@ -16,10 +12,15 @@ import { Tooltip, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState, type Ref } from "react"; import { useTranslation } from "react-i18next"; -export const ControllerViewer = forwardRef((props, ref) => { +import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { useClashInfo } from "@/hooks/use-clash"; +import { useVerge } from "@/hooks/use-verge"; +import { showNotice } from "@/services/noticeService"; + +export function ControllerViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [copySuccess, setCopySuccess] = useState(null); @@ -91,6 +92,7 @@ export const ControllerViewer = forwardRef((props, ref) => { setCopySuccess(type); setTimeout(() => setCopySuccess(null)); } catch (err) { + console.warn("[ControllerViewer] copy to clipboard failed:", err); showNotice("error", t("Failed to copy")); } }, @@ -215,4 +217,4 @@ export const ControllerViewer = forwardRef((props, ref) => { ); -}); +} diff --git a/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx b/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx index d7298e4f63..3286cb3781 100644 --- a/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx @@ -1,6 +1,4 @@ -import { forwardRef, useImperativeHandle, useState, useEffect } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; +import { RestartAltRounded } from "@mui/icons-material"; import { Box, Button, @@ -14,17 +12,21 @@ import { TextField, Typography, } from "@mui/material"; -import { RestartAltRounded } from "@mui/icons-material"; -import { useClash } from "@/hooks/use-clash"; -import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { invoke } from "@tauri-apps/api/core"; +import { useLockFn } from "ahooks"; import yaml from "js-yaml"; +import type { Ref } from "react"; +import { useEffect, useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; import MonacoEditor from "react-monaco-editor"; + +import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { useClash } from "@/hooks/use-clash"; +import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import getSystem from "@/utils/get-system"; -import { invoke } from "@tauri-apps/api/core"; -import { showNotice } from "@/services/noticeService"; -const Item = styled(ListItem)(({ theme }) => ({ +const Item = styled(ListItem)(() => ({ padding: "5px 2px", "& textarea": { lineHeight: 1.5, @@ -86,9 +88,9 @@ const DEFAULT_DNS_CONFIG = { }, }; -export const DnsViewer = forwardRef((props, ref) => { +export function DnsViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); - const { clash, mutateClash, patchClash } = useClash(); + const { clash, mutateClash } = useClash(); const themeMode = useThemeMode(); const [open, setOpen] = useState(false); @@ -325,7 +327,7 @@ export const DnsViewer = forwardRef((props, ref) => { if (!parsedYaml) return; updateValuesFromConfig(parsedYaml); - } catch (err: any) { + } catch { showNotice("error", t("Invalid YAML format")); } }; @@ -379,7 +381,7 @@ export const DnsViewer = forwardRef((props, ref) => { const formatHosts = (hosts: any): string => { if (!hosts || typeof hosts !== "object") return ""; - let result: string[] = []; + const result: string[] = []; Object.entries(hosts).forEach(([domain, value]) => { if (Array.isArray(value)) { @@ -1033,4 +1035,4 @@ export const DnsViewer = forwardRef((props, ref) => { )} ); -}); +} diff --git a/clash-verge-rev/src/components/setting/mods/external-controller-cors.tsx b/clash-verge-rev/src/components/setting/mods/external-controller-cors.tsx index 3d9391e50a..d2522f359f 100644 --- a/clash-verge-rev/src/components/setting/mods/external-controller-cors.tsx +++ b/clash-verge-rev/src/components/setting/mods/external-controller-cors.tsx @@ -1,20 +1,13 @@ -import { BaseDialog, Switch } from "@/components/base"; -import { useClash } from "@/hooks/use-clash"; -import { showNotice } from "@/services/noticeService"; import { Delete as DeleteIcon } from "@mui/icons-material"; -import { - Box, - Button, - Divider, - List, - ListItem, - styled, - TextField, -} from "@mui/material"; +import { Box, Button, Divider, List, ListItem, TextField } from "@mui/material"; import { useLockFn, useRequest } from "ahooks"; import { forwardRef, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; +import { BaseDialog, Switch } from "@/components/base"; +import { useClash } from "@/hooks/use-clash"; +import { showNotice } from "@/services/noticeService"; + // 定义开发环境的URL列表 // 这些URL在开发模式下会被自动包含在允许的来源中 // 在生产环境中,这些URL会被过滤掉 @@ -25,29 +18,19 @@ const DEV_URLS = [ "http://localhost:3000", ]; -// 判断是否处于开发模式 -const isDevMode = import.meta.env.MODE === "development"; - -// 过滤开发环境URL -const filterDevOrigins = (origins: string[]) => { - if (isDevMode) { - return origins; - } - return origins.filter((origin: string) => !DEV_URLS.includes(origin.trim())); -}; - // 获取完整的源列表,包括开发URL const getFullOrigins = (origins: string[]) => { - if (!isDevMode) { - return origins; - } - // 合并现有源和开发URL,并去重 const allOrigins = [...origins, ...DEV_URLS]; const uniqueOrigins = [...new Set(allOrigins)]; return uniqueOrigins; }; +// 过滤基础URL(确保后续添加) +const filterBaseOriginsForUI = (origins: string[]) => { + return origins.filter((origin: string) => !DEV_URLS.includes(origin.trim())); +}; + // 统一使用的按钮样式 const buttonStyle = { borderRadius: "8px", @@ -63,16 +46,6 @@ const buttonStyle = { }, }; -// 保存按钮样式 -const saveButtonStyle = { - ...buttonStyle, - backgroundColor: "#165DFF", - color: "white", - "&:hover": { - backgroundColor: "#0E42D2", - }, -}; - // 添加按钮样式 const addButtonStyle = { ...buttonStyle, @@ -110,10 +83,10 @@ export const HeaderConfiguration = forwardRef( allowOrigins: string[]; }>(() => { const cors = clash?.["external-controller-cors"]; - const origins = cors?.["allow-origins"] ?? ["*"]; + const origins = cors?.["allow-origins"] ?? []; return { allowPrivateNetwork: cors?.["allow-private-network"] ?? true, - allowOrigins: filterDevOrigins(origins), + allowOrigins: filterBaseOriginsForUI(origins), }; }); @@ -178,10 +151,10 @@ export const HeaderConfiguration = forwardRef( useImperativeHandle(ref, () => ({ open: () => { const cors = clash?.["external-controller-cors"]; - const origins = cors?.["allow-origins"] ?? ["*"]; + const origins = cors?.["allow-origins"] ?? []; setCorsConfig({ allowPrivateNetwork: cors?.["allow-private-network"] ?? true, - allowOrigins: filterDevOrigins(origins), + allowOrigins: filterBaseOriginsForUI(origins), }); setOpen(true); }, @@ -257,7 +230,7 @@ export const HeaderConfiguration = forwardRef( color="error" size="small" onClick={() => handleDeleteOrigin(index)} - disabled={corsConfig.allowOrigins.length <= 1} + disabled={corsConfig.allowOrigins.length <= 0} sx={deleteButtonStyle} > @@ -273,24 +246,22 @@ export const HeaderConfiguration = forwardRef( {t("Add")} - {isDevMode && ( +
-
- {t( - "Development mode: Automatically includes Tauri and localhost origins", - )} -
+ {t("Always included origins: {{urls}}", { + urls: DEV_URLS.join(", "), + })}
- )} +
diff --git a/clash-verge-rev/src/components/setting/mods/guard-state.tsx b/clash-verge-rev/src/components/setting/mods/guard-state.tsx index 7abbd75651..0e632ad8c7 100644 --- a/clash-verge-rev/src/components/setting/mods/guard-state.tsx +++ b/clash-verge-rev/src/components/setting/mods/guard-state.tsx @@ -1,4 +1,5 @@ import { cloneElement, isValidElement, ReactNode, useRef } from "react"; + import noop from "@/utils/noop"; interface Props { diff --git a/clash-verge-rev/src/components/setting/mods/hotkey-input.tsx b/clash-verge-rev/src/components/setting/mods/hotkey-input.tsx index 2e767465ca..c4fdd71672 100644 --- a/clash-verge-rev/src/components/setting/mods/hotkey-input.tsx +++ b/clash-verge-rev/src/components/setting/mods/hotkey-input.tsx @@ -1,9 +1,10 @@ -import { useRef, useState } from "react"; -import { alpha, Box, IconButton, styled } from "@mui/material"; import { DeleteRounded } from "@mui/icons-material"; -import { parseHotkey } from "@/utils/parse-hotkey"; +import { alpha, Box, IconButton, styled } from "@mui/material"; +import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { parseHotkey } from "@/utils/parse-hotkey"; + const KeyWrapper = styled("div")(({ theme }) => ({ position: "relative", width: 165, @@ -89,13 +90,11 @@ export const HotkeyInput = (props: Props) => {
{keys.map((key, index) => ( - + -
- {key} -
+
{key}
))}
diff --git a/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx b/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx index d1603cbbb0..cd09e244c0 100644 --- a/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx @@ -1,12 +1,14 @@ +import { styled, Typography } from "@mui/material"; +import { useLockFn } from "ahooks"; import { forwardRef, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useLockFn } from "ahooks"; -import { styled, Typography } from "@mui/material"; -import { useVerge } from "@/hooks/use-verge"; + import { BaseDialog, DialogRef, Switch } from "@/components/base"; -import { HotkeyInput } from "./hotkey-input"; +import { useVerge } from "@/hooks/use-verge"; import { showNotice } from "@/services/noticeService"; +import { HotkeyInput } from "./hotkey-input"; + const ItemWrapper = styled("div")` display: flex; align-items: center; diff --git a/clash-verge-rev/src/components/setting/mods/layout-viewer.tsx b/clash-verge-rev/src/components/setting/mods/layout-viewer.tsx index 2b498b76ee..5e0f1e2535 100644 --- a/clash-verge-rev/src/components/setting/mods/layout-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/layout-viewer.tsx @@ -1,5 +1,3 @@ -import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; -import { useTranslation } from "react-i18next"; import { List, Button, @@ -10,17 +8,21 @@ import { ListItemText, Box, } from "@mui/material"; -import { useVerge } from "@/hooks/use-verge"; +import { convertFileSrc } from "@tauri-apps/api/core"; +import { join } from "@tauri-apps/api/path"; +import { open as openDialog } from "@tauri-apps/plugin-dialog"; +import { exists } from "@tauri-apps/plugin-fs"; +import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; + import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; -import { GuardState } from "./guard-state"; -import { open as openDialog } from "@tauri-apps/plugin-dialog"; -import { convertFileSrc } from "@tauri-apps/api/core"; +import { useVerge } from "@/hooks/use-verge"; import { copyIconFile, getAppDir } from "@/services/cmds"; -import { join } from "@tauri-apps/api/path"; -import { exists } from "@tauri-apps/plugin-fs"; -import getSystem from "@/utils/get-system"; import { showNotice } from "@/services/noticeService"; +import getSystem from "@/utils/get-system"; + +import { GuardState } from "./guard-state"; const OS = getSystem(); diff --git a/clash-verge-rev/src/components/setting/mods/lite-mode-viewer.tsx b/clash-verge-rev/src/components/setting/mods/lite-mode-viewer.tsx index 1e0834208a..241b84b9d6 100644 --- a/clash-verge-rev/src/components/setting/mods/lite-mode-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/lite-mode-viewer.tsx @@ -1,21 +1,23 @@ -import { forwardRef, useImperativeHandle, useState } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; import { + InputAdornment, List, ListItem, ListItemText, TextField, Typography, - InputAdornment, } from "@mui/material"; -import { useVerge } from "@/hooks/use-verge"; +import { useLockFn } from "ahooks"; +import type { Ref } from "react"; +import { useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; + import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; +import { useVerge } from "@/hooks/use-verge"; import { entry_lightweight_mode } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; -export const LiteModeViewer = forwardRef((props, ref) => { +export function LiteModeViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const { verge, patchVerge } = useVerge(); @@ -142,4 +144,4 @@ export const LiteModeViewer = forwardRef((props, ref) => { ); -}); +} diff --git a/clash-verge-rev/src/components/setting/mods/misc-viewer.tsx b/clash-verge-rev/src/components/setting/mods/misc-viewer.tsx index 9e5eca78af..b0048fd817 100644 --- a/clash-verge-rev/src/components/setting/mods/misc-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/misc-viewer.tsx @@ -1,18 +1,19 @@ -import { forwardRef, useImperativeHandle, useState } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; import { + InputAdornment, List, ListItem, ListItemText, MenuItem, Select, TextField, - InputAdornment, } from "@mui/material"; -import { useVerge } from "@/hooks/use-verge"; +import { useLockFn } from "ahooks"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; + import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; +import { useVerge } from "@/hooks/use-verge"; import { showNotice } from "@/services/noticeService"; export const MiscViewer = forwardRef((props, ref) => { @@ -22,6 +23,8 @@ export const MiscViewer = forwardRef((props, ref) => { const [open, setOpen] = useState(false); const [values, setValues] = useState({ appLogLevel: "warn", + appLogMaxSize: 8, + appLogMaxCount: 12, autoCloseConnection: true, autoCheckUpdate: true, enableBuiltinEnhanced: true, @@ -36,6 +39,8 @@ export const MiscViewer = forwardRef((props, ref) => { setOpen(true); setValues({ appLogLevel: verge?.app_log_level ?? "warn", + appLogMaxSize: verge?.app_log_max_size ?? 128, + appLogMaxCount: verge?.app_log_max_count ?? 8, autoCloseConnection: verge?.auto_close_connection ?? true, autoCheckUpdate: verge?.auto_check_update ?? true, enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true, @@ -99,6 +104,66 @@ export const MiscViewer = forwardRef((props, ref) => { + + + + setValues((v) => ({ + ...v, + appLogMaxSize: Math.max(1, parseInt(e.target.value) || 128), + })) + } + slotProps={{ + input: { + endAdornment: ( + {t("KB")} + ), + }, + }} + /> + + + + + + setValues((v) => ({ + ...v, + appLogMaxCount: Math.max(1, parseInt(e.target.value) || 1), + })) + } + slotProps={{ + input: { + endAdornment: ( + {t("Files")} + ), + }, + }} + /> + + ((props, ref) => { div": { py: "7.5px" } }} + sx={{ width: 160, "> div": { py: "7.5px" } }} value={values.autoLogClean} onChange={(e) => setValues((v) => ({ @@ -185,6 +250,7 @@ export const MiscViewer = forwardRef((props, ref) => { })) } > + {/* 1: 1天, 2: 7天, 3: 30天, 4: 90天*/} {[ { key: t("Never Clean"), value: 0 }, { key: t("Retain _n Days", { n: 1 }), value: 1 }, diff --git a/clash-verge-rev/src/components/setting/mods/network-interface-viewer.tsx b/clash-verge-rev/src/components/setting/mods/network-interface-viewer.tsx index 772b93c7a2..fd7e1484c0 100644 --- a/clash-verge-rev/src/components/setting/mods/network-interface-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/network-interface-viewer.tsx @@ -1,14 +1,16 @@ -import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef } from "@/components/base"; -import { getNetworkInterfacesInfo } from "@/services/cmds"; -import { alpha, Box, Button, IconButton } from "@mui/material"; import { ContentCopyRounded } from "@mui/icons-material"; +import { alpha, Box, Button, IconButton } from "@mui/material"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { showNotice } from "@/services/noticeService"; +import type { Ref } from "react"; +import { useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; import useSWR from "swr"; -export const NetworkInterfaceViewer = forwardRef((props, ref) => { +import { BaseDialog, DialogRef } from "@/components/base"; +import { getNetworkInterfacesInfo } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; + +export function NetworkInterfaceViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [isV4, setIsV4] = useState(true); @@ -98,7 +100,7 @@ export const NetworkInterfaceViewer = forwardRef((props, ref) => { ))} ); -}); +} const AddressDisplay = (props: { label: string; content: string }) => { const { t } = useTranslation(); diff --git a/clash-verge-rev/src/components/setting/mods/password-input.tsx b/clash-verge-rev/src/components/setting/mods/password-input.tsx index a42b6a45ba..ae8dd7fd8e 100644 --- a/clash-verge-rev/src/components/setting/mods/password-input.tsx +++ b/clash-verge-rev/src/components/setting/mods/password-input.tsx @@ -1,5 +1,3 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; import { Button, Dialog, @@ -8,6 +6,8 @@ import { DialogTitle, TextField, } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; interface Props { onConfirm: (passwd: string) => Promise; diff --git a/clash-verge-rev/src/components/setting/mods/setting-comp.tsx b/clash-verge-rev/src/components/setting/mods/setting-comp.tsx index 87cf3f38f1..1575e35460 100644 --- a/clash-verge-rev/src/components/setting/mods/setting-comp.tsx +++ b/clash-verge-rev/src/components/setting/mods/setting-comp.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState } from "react"; +import { ChevronRightRounded } from "@mui/icons-material"; import { Box, List, @@ -7,8 +7,9 @@ import { ListItemText, ListSubheader, } from "@mui/material"; -import { ChevronRightRounded } from "@mui/icons-material"; import CircularProgress from "@mui/material/CircularProgress"; +import React, { ReactNode, useState } from "react"; + import isAsyncFunction from "@/utils/is-async-function"; interface ItemProps { diff --git a/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx b/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx index 3c2ea89c1e..b8bc792bc2 100644 --- a/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx @@ -1,19 +1,3 @@ -import { BaseDialog, DialogRef, Switch } from "@/components/base"; -import { BaseFieldset } from "@/components/base/base-fieldset"; -import { TooltipIcon } from "@/components/base/base-tooltip-icon"; -import { EditorViewer } from "@/components/profile/editor-viewer"; -import { useVerge } from "@/hooks/use-verge"; -import { useAppData } from "@/providers/app-data-provider"; -import { getClashConfig } from "@/services/cmds"; -import { - getAutotemProxy, - getNetworkInterfacesInfo, - getSystemHostname, - getSystemProxy, - patchVergeConfig, -} from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; -import getSystem from "@/utils/get-system"; import { EditRounded } from "@mui/icons-material"; import { Autocomplete, @@ -37,6 +21,23 @@ import { import { useTranslation } from "react-i18next"; import useSWR, { mutate } from "swr"; +import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { BaseFieldset } from "@/components/base/base-fieldset"; +import { TooltipIcon } from "@/components/base/base-tooltip-icon"; +import { EditorViewer } from "@/components/profile/editor-viewer"; +import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-context"; +import { + getAutotemProxy, + getClashConfig, + getNetworkInterfacesInfo, + getSystemHostname, + getSystemProxy, + patchVergeConfig, +} from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; +import getSystem from "@/utils/get-system"; + const DEFAULT_PAC = `function FindProxyForURL(url, host) { return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;"; }`; @@ -122,16 +123,12 @@ export const SysproxyViewer = forwardRef((props, ref) => { return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,"; }; - const { data: clashConfig, mutate: mutateClash } = useSWR( - "getClashConfig", - getClashConfig, - { - revalidateOnFocus: false, - revalidateIfStale: true, - dedupingInterval: 1000, - errorRetryInterval: 5000, - }, - ); + const { data: clashConfig } = useSWR("getClashConfig", getClashConfig, { + revalidateOnFocus: false, + revalidateIfStale: true, + dedupingInterval: 1000, + errorRetryInterval: 5000, + }); const [prevMixedPort, setPrevMixedPort] = useState( clashConfig?.["mixed-port"], @@ -299,7 +296,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; const hostnameRegex = - /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; if ( !ipv4Regex.test(value.proxy_host) && @@ -443,14 +440,10 @@ export const SysproxyViewer = forwardRef((props, ref) => { {!value.pac && ( - <> - - {t("Server Addr")} - - {getSystemProxyAddress} - - - + + {t("Server Addr")} + {getSystemProxyAddress} + )} {value.pac && ( @@ -585,39 +578,37 @@ export const SysproxyViewer = forwardRef((props, ref) => { )} {value.pac && ( - <> - - - + {editorOpen && ( + { + let pac = DEFAULT_PAC; + if (curr && curr.trim().length > 0) { + pac = curr; + } + setValue((v) => ({ ...v, pac_content: pac })); }} - > - {t("Edit")} PAC - - {editorOpen && ( - { - let pac = DEFAULT_PAC; - if (curr && curr.trim().length > 0) { - pac = curr; - } - setValue((v) => ({ ...v, pac_content: pac })); - }} - onClose={() => setEditorOpen(false)} - /> - )} - - + onClose={() => setEditorOpen(false)} + /> + )} + )} diff --git a/clash-verge-rev/src/components/setting/mods/theme-mode-switch.tsx b/clash-verge-rev/src/components/setting/mods/theme-mode-switch.tsx index 29ae9ef0c6..edd3fab927 100644 --- a/clash-verge-rev/src/components/setting/mods/theme-mode-switch.tsx +++ b/clash-verge-rev/src/components/setting/mods/theme-mode-switch.tsx @@ -1,5 +1,5 @@ -import { useTranslation } from "react-i18next"; import { Button, ButtonGroup } from "@mui/material"; +import { useTranslation } from "react-i18next"; type ThemeValue = IVergeConfig["theme_mode"]; diff --git a/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx b/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx index f4e535b877..07fc1009f6 100644 --- a/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx @@ -1,6 +1,4 @@ -import { forwardRef, useImperativeHandle, useState } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; +import { EditRounded } from "@mui/icons-material"; import { Button, List, @@ -10,14 +8,18 @@ import { TextField, useTheme, } from "@mui/material"; -import { useVerge } from "@/hooks/use-verge"; -import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; +import { useLockFn } from "ahooks"; +import { useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; + import { BaseDialog, DialogRef } from "@/components/base"; import { EditorViewer } from "@/components/profile/editor-viewer"; -import { EditRounded } from "@mui/icons-material"; +import { useVerge } from "@/hooks/use-verge"; +import { defaultDarkTheme, defaultTheme } from "@/pages/_theme"; import { showNotice } from "@/services/noticeService"; -export const ThemeViewer = forwardRef((props, ref) => { +export function ThemeViewer(props: { ref?: React.Ref }) { + const { ref } = props; const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -143,7 +145,7 @@ export const ThemeViewer = forwardRef((props, ref) => { ); -}); +} const Item = styled(ListItem)(() => ({ padding: "5px 2px", diff --git a/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx b/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx index a3fbc33b06..b9241c7d79 100644 --- a/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx @@ -1,25 +1,28 @@ -import { forwardRef, useImperativeHandle, useState } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; import { + Box, + Button, List, ListItem, ListItemText, - Box, - Typography, - Button, TextField, + Typography, } from "@mui/material"; -import { useClash } from "@/hooks/use-clash"; +import { useLockFn } from "ahooks"; +import type { Ref } from "react"; +import { useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; + import { BaseDialog, DialogRef, Switch } from "@/components/base"; -import { StackModeSwitch } from "./stack-mode-switch"; +import { useClash } from "@/hooks/use-clash"; import { enhanceProfiles } from "@/services/cmds"; -import getSystem from "@/utils/get-system"; import { showNotice } from "@/services/noticeService"; +import getSystem from "@/utils/get-system"; + +import { StackModeSwitch } from "./stack-mode-switch"; const OS = getSystem(); -export const TunViewer = forwardRef((props, ref) => { +export function TunViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const { clash, mutateClash, patchClash } = useClash(); @@ -53,7 +56,7 @@ export const TunViewer = forwardRef((props, ref) => { const onSave = useLockFn(async () => { try { - let tun = { + const tun = { stack: values.stack, device: values.device === "" @@ -70,7 +73,7 @@ export const TunViewer = forwardRef((props, ref) => { await patchClash({ tun }); await mutateClash( (old) => ({ - ...(old! || {}), + ...old!, tun, }), false, @@ -97,7 +100,7 @@ export const TunViewer = forwardRef((props, ref) => { variant="outlined" size="small" onClick={async () => { - let tun = { + const tun = { stack: "gvisor", device: OS === "macos" ? "utun1024" : "Mihomo", "auto-route": true, @@ -118,7 +121,7 @@ export const TunViewer = forwardRef((props, ref) => { await patchClash({ tun }); await mutateClash( (old) => ({ - ...(old! || {}), + ...old!, tun, }), false, @@ -236,4 +239,4 @@ export const TunViewer = forwardRef((props, ref) => { ); -}); +} diff --git a/clash-verge-rev/src/components/setting/mods/update-viewer.tsx b/clash-verge-rev/src/components/setting/mods/update-viewer.tsx index 91ea4ae1d3..b65a0e643f 100644 --- a/clash-verge-rev/src/components/setting/mods/update-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/update-viewer.tsx @@ -1,26 +1,22 @@ -import useSWR from "swr"; -import { - forwardRef, - useImperativeHandle, - useState, - useMemo, - useEffect, -} from "react"; -import { useLockFn } from "ahooks"; -import { Box, LinearProgress, Button } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { relaunch } from "@tauri-apps/plugin-process"; -import { check as checkUpdate } from "@tauri-apps/plugin-updater"; -import { BaseDialog, DialogRef } from "@/components/base"; -import { useUpdateState, useSetUpdateState } from "@/services/states"; +import { Box, Button, LinearProgress } from "@mui/material"; import { Event, UnlistenFn } from "@tauri-apps/api/event"; -import { portableFlag } from "@/pages/_layout"; +import { relaunch } from "@tauri-apps/plugin-process"; import { open as openUrl } from "@tauri-apps/plugin-shell"; +import { check as checkUpdate } from "@tauri-apps/plugin-updater"; +import { useLockFn } from "ahooks"; +import type { Ref } from "react"; +import { useEffect, useImperativeHandle, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import ReactMarkdown from "react-markdown"; -import { useListen } from "@/hooks/use-listen"; -import { showNotice } from "@/services/noticeService"; +import useSWR from "swr"; -export const UpdateViewer = forwardRef((props, ref) => { +import { BaseDialog, DialogRef } from "@/components/base"; +import { useListen } from "@/hooks/use-listen"; +import { portableFlag } from "@/pages/_layout"; +import { showNotice } from "@/services/noticeService"; +import { useSetUpdateState, useUpdateState } from "@/services/states"; + +export function UpdateViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -143,7 +139,7 @@ export const UpdateViewer = forwardRef((props, ref) => { { + a: ({ ...props }) => { const { children } = props; return ( @@ -166,4 +162,4 @@ export const UpdateViewer = forwardRef((props, ref) => { )} ); -}); +} diff --git a/clash-verge-rev/src/components/setting/mods/web-ui-item.tsx b/clash-verge-rev/src/components/setting/mods/web-ui-item.tsx index 73d0681d64..577f6413d3 100644 --- a/clash-verge-rev/src/components/setting/mods/web-ui-item.tsx +++ b/clash-verge-rev/src/components/setting/mods/web-ui-item.tsx @@ -1,11 +1,3 @@ -import { useState } from "react"; -import { - Divider, - IconButton, - Stack, - TextField, - Typography, -} from "@mui/material"; import { CheckRounded, CloseRounded, @@ -13,6 +5,14 @@ import { EditRounded, OpenInNewRounded, } from "@mui/icons-material"; +import { + Divider, + IconButton, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; interface Props { diff --git a/clash-verge-rev/src/components/setting/mods/web-ui-viewer.tsx b/clash-verge-rev/src/components/setting/mods/web-ui-viewer.tsx index 34219deaf8..5b881d3530 100644 --- a/clash-verge-rev/src/components/setting/mods/web-ui-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/web-ui-viewer.tsx @@ -1,15 +1,18 @@ -import { forwardRef, useImperativeHandle, useState } from "react"; +import { Box, Button, Typography } from "@mui/material"; import { useLockFn } from "ahooks"; +import type { Ref } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Button, Box, Typography } from "@mui/material"; -import { useVerge } from "@/hooks/use-verge"; -import { openWebUrl } from "@/services/cmds"; + import { BaseDialog, BaseEmpty, DialogRef } from "@/components/base"; import { useClashInfo } from "@/hooks/use-clash"; -import { WebUIItem } from "./web-ui-item"; +import { useVerge } from "@/hooks/use-verge"; +import { openWebUrl } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; -export const WebUIViewer = forwardRef((props, ref) => { +import { WebUIItem } from "./web-ui-item"; + +export function WebUIViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const { clashInfo } = useClashInfo(); @@ -137,4 +140,4 @@ export const WebUIViewer = forwardRef((props, ref) => { )} ); -}); +} diff --git a/clash-verge-rev/src/components/setting/setting-clash.tsx b/clash-verge-rev/src/components/setting/setting-clash.tsx index 102ba1eda4..481683fa5b 100644 --- a/clash-verge-rev/src/components/setting/setting-clash.tsx +++ b/clash-verge-rev/src/components/setting/setting-clash.tsx @@ -1,27 +1,28 @@ -import { DialogRef, Switch } from "@/components/base"; -import { TooltipIcon } from "@/components/base/base-tooltip-icon"; -import { useClash } from "@/hooks/use-clash"; -import { useListen } from "@/hooks/use-listen"; -import { useVerge } from "@/hooks/use-verge"; -import { updateGeoData } from "@/services/cmds"; -import { invoke_uwp_tool } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; -import getSystem from "@/utils/get-system"; import { LanRounded, SettingsRounded } from "@mui/icons-material"; import { MenuItem, Select, TextField, Typography } from "@mui/material"; import { invoke } from "@tauri-apps/api/core"; import { useLockFn } from "ahooks"; import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; + +import { DialogRef, Switch } from "@/components/base"; +import { TooltipIcon } from "@/components/base/base-tooltip-icon"; +import { useClash } from "@/hooks/use-clash"; +import { useVerge } from "@/hooks/use-verge"; +import { invoke_uwp_tool } from "@/services/cmds"; +import { updateGeoData } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; +import getSystem from "@/utils/get-system"; + import { ClashCoreViewer } from "./mods/clash-core-viewer"; import { ClashPortViewer } from "./mods/clash-port-viewer"; import { ControllerViewer } from "./mods/controller-viewer"; import { DnsViewer } from "./mods/dns-viewer"; +import { HeaderConfiguration } from "./mods/external-controller-cors"; import { GuardState } from "./mods/guard-state"; import { NetworkInterfaceViewer } from "./mods/network-interface-viewer"; import { SettingItem, SettingList } from "./mods/setting-comp"; import { WebUIViewer } from "./mods/web-ui-viewer"; -import { HeaderConfiguration } from "./mods/external-controller-cors"; const isWIN = getSystem() === "windows"; @@ -33,25 +34,22 @@ const SettingClash = ({ onError }: Props) => { const { t } = useTranslation(); const { clash, version, mutateClash, patchClash } = useClash(); - const { verge, mutateVerge, patchVerge } = useVerge(); + const { verge, patchVerge } = useVerge(); const { ipv6, "allow-lan": allowLan, "log-level": logLevel, "unified-delay": unifiedDelay, - dns, } = clash ?? {}; - const { enable_random_port = false, verge_mixed_port } = verge ?? {}; + const { verge_mixed_port } = verge ?? {}; // 独立跟踪DNS设置开关状态 const [dnsSettingsEnabled, setDnsSettingsEnabled] = useState(() => { return verge?.enable_dns_settings ?? false; }); - const { addListener } = useListen(); - const webRef = useRef(null); const portRef = useRef(null); const ctrlRef = useRef(null); @@ -62,10 +60,7 @@ const SettingClash = ({ onError }: Props) => { const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial) => { - mutateClash((old) => ({ ...(old! || {}), ...patch }), false); - }; - const onChangeVerge = (patch: Partial) => { - mutateVerge({ ...verge, ...patch }, false); + mutateClash((old) => ({ ...old!, ...patch }), false); }; const onUpdateGeo = async () => { try { diff --git a/clash-verge-rev/src/components/setting/setting-system.tsx b/clash-verge-rev/src/components/setting/setting-system.tsx index 2e94f01f2c..fcc03a8a9e 100644 --- a/clash-verge-rev/src/components/setting/setting-system.tsx +++ b/clash-verge-rev/src/components/setting/setting-system.tsx @@ -1,29 +1,20 @@ -import { mutate } from "swr"; -import { useRef } from "react"; +import { WarningRounded } from "@mui/icons-material"; +import { Tooltip } from "@mui/material"; +import React, { useRef } from "react"; import { useTranslation } from "react-i18next"; -import { - SettingsRounded, - PlayArrowRounded, - PauseRounded, - WarningRounded, - BuildRounded, - DeleteForeverRounded, -} from "@mui/icons-material"; -import { useVerge } from "@/hooks/use-verge"; -import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; +import { mutate } from "swr"; + import { DialogRef, Switch } from "@/components/base"; -import { SettingList, SettingItem } from "./mods/setting-comp"; +import { TooltipIcon } from "@/components/base/base-tooltip-icon"; +import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches"; +import { useSystemState } from "@/hooks/use-system-state"; +import { useVerge } from "@/hooks/use-verge"; +import { showNotice } from "@/services/noticeService"; + import { GuardState } from "./mods/guard-state"; +import { SettingList, SettingItem } from "./mods/setting-comp"; import { SysproxyViewer } from "./mods/sysproxy-viewer"; import { TunViewer } from "./mods/tun-viewer"; -import { TooltipIcon } from "@/components/base/base-tooltip-icon"; -import { uninstallService, restartCore, stopCore } from "@/services/cmds"; -import { useLockFn } from "ahooks"; -import { Button, Tooltip } from "@mui/material"; -import { useSystemState } from "@/hooks/use-system-state"; - -import { showNotice } from "@/services/noticeService"; -import { useServiceInstaller } from "@/hooks/useServiceInstaller"; interface Props { onError?: (err: Error) => void; @@ -33,170 +24,30 @@ const SettingSystem = ({ onError }: Props) => { const { t } = useTranslation(); const { verge, mutateVerge, patchVerge } = useVerge(); - const { installServiceAndRestartCore } = useServiceInstaller(); - const { - actualState: systemProxyActualState, - indicator: systemProxyIndicator, - toggleSystemProxy, - } = useSystemProxyState(); - const { isAdminMode, isServiceMode, mutateRunningMode } = useSystemState(); + const { isAdminMode } = useSystemState(); - // +++ isTunAvailable 现在使用 SWR 的 isServiceMode - const isTunAvailable = isServiceMode || isAdminMode; + const { enable_auto_launch, enable_silent_start } = verge ?? {}; const sysproxyRef = useRef(null); const tunRef = useRef(null); - const { enable_tun_mode, enable_auto_launch, enable_silent_start } = - verge ?? {}; - - const onSwitchFormat = (_e: any, value: boolean) => value; + const onSwitchFormat = ( + _e: React.ChangeEvent, + value: boolean, + ) => value; const onChangeData = (patch: Partial) => { mutateVerge({ ...verge, ...patch }, false); }; - // 抽象服务操作逻辑 - const handleServiceOperation = useLockFn( - async ({ - beforeMsg, - action, - actionMsg, - successMsg, - }: { - beforeMsg: string; - action: () => Promise; - actionMsg: string; - successMsg: string; - }) => { - try { - showNotice("info", beforeMsg); - await stopCore(); - showNotice("info", actionMsg); - await action(); - showNotice("success", successMsg); - showNotice("info", t("Restarting Core...")); - await restartCore(); - await mutateRunningMode(); - } catch (err: any) { - showNotice("error", err.message || err.toString()); - try { - showNotice("info", t("Try running core as Sidecar...")); - await restartCore(); - await mutateRunningMode(); - } catch (e: any) { - showNotice("error", e?.message || e?.toString()); - } - } - }, - ); - - // 卸载系统服务 - const onUninstallService = () => - handleServiceOperation({ - beforeMsg: t("Stopping Core..."), - action: uninstallService, - actionMsg: t("Uninstalling Service..."), - successMsg: t("Service Uninstalled Successfully"), - }); - return ( - - tunRef.current?.open()} - /> - {!isTunAvailable && ( - - - - )} - {!isServiceMode && !isAdminMode && ( - - - - )} - {isServiceMode && ( - - - - )} - - } - > - { - if (!isTunAvailable) return; - onChangeData({ enable_tun_mode: e }); - }} - onGuard={(e) => { - if (!isTunAvailable) { - showNotice("error", t("TUN requires Service Mode or Admin Mode")); - return Promise.reject( - new Error(t("TUN requires Service Mode or Admin Mode")), - ); - } - return patchVerge({ enable_tun_mode: e }); - }} - > - - - - - sysproxyRef.current?.open()} - /> - {systemProxyIndicator ? ( - - ) : ( - - )} - - } - > - toggleSystemProxy(e)} - > - - - + + + void; } -const SettingVergeAdvanced = ({ onError }: Props) => { +const SettingVergeAdvanced = ({ onError: _ }: Props) => { const { t } = useTranslation(); - const { verge, patchVerge, mutateVerge } = useVerge(); const configRef = useRef(null); const hotkeyRef = useRef(null); const miscRef = useRef(null); @@ -59,13 +59,13 @@ const SettingVergeAdvanced = ({ onError }: Props) => { const onExportDiagnosticInfo = useCallback(async () => { await exportDiagnosticInfo(); showNotice("success", t("Copy Success"), 1000); - }, []); + }, [t]); const copyVersion = useCallback(() => { navigator.clipboard.writeText(`v${version}`).then(() => { showNotice("success", t("Version copied to clipboard"), 1000); }); - }, [version, t]); + }, [t]); return ( diff --git a/clash-verge-rev/src/components/setting/setting-verge-basic.tsx b/clash-verge-rev/src/components/setting/setting-verge-basic.tsx index 34660f4eca..915fb5a978 100644 --- a/clash-verge-rev/src/components/setting/setting-verge-basic.tsx +++ b/clash-verge-rev/src/components/setting/setting-verge-basic.tsx @@ -1,26 +1,28 @@ +import { ContentCopyRounded } from "@mui/icons-material"; +import { Button, MenuItem, Select, Input } from "@mui/material"; +import { open } from "@tauri-apps/plugin-dialog"; import { useCallback, useRef } from "react"; import { useTranslation } from "react-i18next"; -import { open } from "@tauri-apps/plugin-dialog"; -import { Button, MenuItem, Select, Input } from "@mui/material"; -import { copyClashEnv } from "@/services/cmds"; -import { useVerge } from "@/hooks/use-verge"; + import { DialogRef } from "@/components/base"; +import { TooltipIcon } from "@/components/base/base-tooltip-icon"; +import { useVerge } from "@/hooks/use-verge"; +import { routers } from "@/pages/_routers"; +import { copyClashEnv } from "@/services/cmds"; +import { supportedLanguages } from "@/services/i18n"; +import { showNotice } from "@/services/noticeService"; +import getSystem from "@/utils/get-system"; + +import { BackupViewer } from "./mods/backup-viewer"; +import { ConfigViewer } from "./mods/config-viewer"; +import { GuardState } from "./mods/guard-state"; +import { HotkeyViewer } from "./mods/hotkey-viewer"; +import { LayoutViewer } from "./mods/layout-viewer"; +import { MiscViewer } from "./mods/misc-viewer"; import { SettingList, SettingItem } from "./mods/setting-comp"; import { ThemeModeSwitch } from "./mods/theme-mode-switch"; -import { ConfigViewer } from "./mods/config-viewer"; -import { HotkeyViewer } from "./mods/hotkey-viewer"; -import { MiscViewer } from "./mods/misc-viewer"; import { ThemeViewer } from "./mods/theme-viewer"; -import { GuardState } from "./mods/guard-state"; -import { LayoutViewer } from "./mods/layout-viewer"; import { UpdateViewer } from "./mods/update-viewer"; -import { BackupViewer } from "./mods/backup-viewer"; -import getSystem from "@/utils/get-system"; -import { routers } from "@/pages/_routers"; -import { TooltipIcon } from "@/components/base/base-tooltip-icon"; -import { ContentCopyRounded } from "@mui/icons-material"; -import { languages } from "@/services/i18n"; -import { showNotice } from "@/services/noticeService"; interface Props { onError?: (err: Error) => void; @@ -28,7 +30,7 @@ interface Props { const OS = getSystem(); -const languageOptions = Object.entries(languages).map(([code, _]) => { +const languageOptions = supportedLanguages.map((code) => { const labels: { [key: string]: string } = { en: "English", ru: "Русский", @@ -39,8 +41,13 @@ const languageOptions = Object.entries(languages).map(([code, _]) => { ar: "العربية", ko: "한국어", tr: "Türkçe", + de: "Deutsch", + es: "Español", + jp: "日本語", + zhtw: "繁體中文", }; - return { code, label: labels[code] }; + const label = labels[code] || code; + return { code, label }; }); const SettingVergeBasic = ({ onError }: Props) => { @@ -70,7 +77,7 @@ const SettingVergeBasic = ({ onError }: Props) => { const onCopyClashEnv = useCallback(async () => { await copyClashEnv(); showNotice("success", t("Copy Success"), 1000); - }, []); + }, [t]); return ( diff --git a/clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx b/clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx index f96d5cbeb5..c775f91e54 100644 --- a/clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx +++ b/clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx @@ -1,260 +1,227 @@ -import { useRef } from "react"; -import { useTranslation } from "react-i18next"; -import useSWR from "swr"; import { SettingsRounded, PlayCircleOutlineRounded, PauseCircleOutlineRounded, BuildRounded, + DeleteForeverRounded, + WarningRounded, } from "@mui/icons-material"; -import { - Box, - Button, - Tooltip, - Typography, - alpha, - useTheme, -} from "@mui/material"; +import { Box, Typography, alpha, useTheme } from "@mui/material"; +import { useLockFn } from "ahooks"; +import React, { useRef, useCallback } from "react"; +import { useTranslation } from "react-i18next"; + import { DialogRef, Switch } from "@/components/base"; +import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { GuardState } from "@/components/setting/mods/guard-state"; import { SysproxyViewer } from "@/components/setting/mods/sysproxy-viewer"; import { TunViewer } from "@/components/setting/mods/tun-viewer"; -import { useVerge } from "@/hooks/use-verge"; import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; -import { getRunningMode } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { useSystemState } from "@/hooks/use-system-state"; +import { useVerge } from "@/hooks/use-verge"; import { useServiceInstaller } from "@/hooks/useServiceInstaller"; +import { useServiceUninstaller } from "@/hooks/useServiceUninstaller"; +import { showNotice } from "@/services/noticeService"; interface ProxySwitchProps { label?: string; onError?: (err: Error) => void; + noRightPadding?: boolean; +} + +interface SwitchRowProps { + label: string; + active: boolean; + disabled?: boolean; + infoTitle: string; + onInfoClick?: () => void; + extraIcons?: React.ReactNode; + onToggle: (value: boolean) => Promise; + onError?: (err: Error) => void; + highlight?: boolean; } /** - * 可复用的代理控制开关组件 - * 包含 Tun Mode 和 System Proxy 的开关功能 + * 抽取的子组件:统一的开关 UI */ -const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { +const SwitchRow = ({ + label, + active, + disabled, + infoTitle, + onInfoClick, + extraIcons, + onToggle, + onError, + highlight, +}: SwitchRowProps) => { + const theme = useTheme(); + return ( + + + {active ? ( + + ) : ( + + )} + + {label} + + + {extraIcons} + + + v} + onGuard={onToggle} + > + + + + ); +}; + +const ProxyControlSwitches = ({ + label, + onError, + noRightPadding = false, +}: ProxySwitchProps) => { const { t } = useTranslation(); const { verge, mutateVerge, patchVerge } = useVerge(); - const theme = useTheme(); const { installServiceAndRestartCore } = useServiceInstaller(); - + const { uninstallServiceAndRestartCore } = useServiceUninstaller(); + const { actualState: systemProxyActualState, toggleSystemProxy } = + useSystemProxyState(); const { - actualState: systemProxyActualState, - indicator: systemProxyIndicator, - toggleSystemProxy, - } = useSystemProxyState(); - - const { data: runningMode } = useSWR("getRunningMode", getRunningMode); - - // 是否以sidecar模式运行 - const isSidecarMode = runningMode === "Sidecar"; + isServiceMode, + isTunModeAvailable, + mutateRunningMode, + mutateServiceOk, + } = useSystemState(); const sysproxyRef = useRef(null); const tunRef = useRef(null); const { enable_tun_mode, enable_system_proxy } = verge ?? {}; - // 确定当前显示哪个开关 + const showErrorNotice = useCallback( + (msg: string) => showNotice("error", t(msg)), + [t], + ); + + const handleTunToggle = async (value: boolean) => { + if (!isTunModeAvailable) { + const msg = "TUN requires Service Mode or Admin Mode"; + showErrorNotice(msg); + throw new Error(t(msg)); + } + mutateVerge({ ...verge, enable_tun_mode: value }, false); + await patchVerge({ enable_tun_mode: value }); + }; + + const onInstallService = useLockFn(async () => { + try { + await installServiceAndRestartCore(); + await mutateRunningMode(); + await mutateServiceOk(); + } catch (err) { + showNotice("error", (err as Error).message || String(err)); + } + }); + + const onUninstallService = useLockFn(async () => { + try { + await uninstallServiceAndRestartCore(); + await mutateRunningMode(); + await mutateServiceOk(); + } catch (err) { + showNotice("error", (err as Error).message || String(err)); + } + }); + const isSystemProxyMode = label === t("System Proxy") || !label; const isTunMode = label === t("Tun Mode"); - const onSwitchFormat = (_e: any, value: boolean) => value; - const onChangeData = (patch: Partial) => { - mutateVerge({ ...verge, ...patch }, false); - }; - - // 安装系统服务 - const onInstallService = installServiceAndRestartCore; - return ( - - {label && ( - - {label} - - )} - - {/* 仅显示当前选中的开关 */} + {isSystemProxyMode && ( - - - {systemProxyIndicator ? ( - - ) : ( - - )} - - - - {t("System Proxy")} - - {/* - {sysproxy?.enable - ? t("Proxy is active") - : t("Enable this for most users") - } - */} - - - - - - sysproxyRef.current?.open()} - > - - - - - toggleSystemProxy(e)} - > - - - - + sysproxyRef.current?.open()} + onToggle={(value) => toggleSystemProxy(value)} + onError={onError} + highlight={enable_system_proxy} + /> )} {isTunMode && ( - - - {enable_tun_mode ? ( - - ) : ( - - )} - - - - {t("Tun Mode")} - - - - - - {isSidecarMode && ( - - - - )} - - - tunRef.current?.open()} - > - - - - - { - if (isSidecarMode) { - showNotice( - "error", - t("TUN requires Service Mode or Admin Mode"), - ); - return Promise.reject( - new Error(t("TUN requires Service Mode or Admin Mode")), - ); - } - onChangeData({ enable_tun_mode: e }); - }} - onGuard={(e) => { - if (isSidecarMode) { - showNotice( - "error", - t("TUN requires Service Mode or Admin Mode"), - ); - return Promise.reject( - new Error(t("TUN requires Service Mode or Admin Mode")), - ); - } - return patchVerge({ enable_tun_mode: e }); - }} - > - - - - + sx={{ ml: 1 }} + /> + ) : ( + + )} + + } + /> )} - {/* 引用对话框组件 */} diff --git a/clash-verge-rev/src/components/test/test-item.tsx b/clash-verge-rev/src/components/test/test-item.tsx index 50ca11f0c6..40d73014cc 100644 --- a/clash-verge-rev/src/components/test/test-item.tsx +++ b/clash-verge-rev/src/components/test/test-item.tsx @@ -1,18 +1,20 @@ -import { useEffect, useState } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { Box, Divider, MenuItem, Menu, styled, alpha } from "@mui/material"; -import { BaseLoading } from "@/components/base"; import { LanguageRounded } from "@mui/icons-material"; -import { showNotice } from "@/services/noticeService"; -import { TestBox } from "./test-box"; -import delayManager from "@/services/delay"; -import { cmdTestDelay, downloadIconCache } from "@/services/cmds"; -import { UnlistenFn } from "@tauri-apps/api/event"; +import { Box, Divider, MenuItem, Menu, styled, alpha } from "@mui/material"; import { convertFileSrc } from "@tauri-apps/api/core"; +import { UnlistenFn } from "@tauri-apps/api/event"; +import { useLockFn } from "ahooks"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { BaseLoading } from "@/components/base"; import { useListen } from "@/hooks/use-listen"; +import { cmdTestDelay, downloadIconCache } from "@/services/cmds"; +import delayManager from "@/services/delay"; +import { showNotice } from "@/services/noticeService"; + +import { TestBox } from "./test-box"; interface Props { id: string; diff --git a/clash-verge-rev/src/components/test/test-viewer.tsx b/clash-verge-rev/src/components/test/test-viewer.tsx index fbe353719e..0962c42499 100644 --- a/clash-verge-rev/src/components/test/test-viewer.tsx +++ b/clash-verge-rev/src/components/test/test-viewer.tsx @@ -1,11 +1,12 @@ -import { forwardRef, useImperativeHandle, useState } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; -import { useForm, Controller } from "react-hook-form"; import { TextField } from "@mui/material"; -import { useVerge } from "@/hooks/use-verge"; -import { BaseDialog } from "@/components/base"; +import { useLockFn } from "ahooks"; import { nanoid } from "nanoid"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +import { BaseDialog } from "@/components/base"; +import { useVerge } from "@/hooks/use-verge"; import { showNotice } from "@/services/noticeService"; interface Props { @@ -25,7 +26,12 @@ export const TestViewer = forwardRef((props, ref) => { const [loading, setLoading] = useState(false); const { verge, patchVerge } = useVerge(); const testList = verge?.test_list ?? []; - const { control, watch, register, ...formIns } = useForm({ + const { + control, + watch: _watch, + register: _register, + ...formIns + } = useForm({ defaultValues: { name: "", icon: "", diff --git a/clash-verge-rev/src/hooks/use-clash.ts b/clash-verge-rev/src/hooks/use-clash.ts index cffce52a75..6284dbcfeb 100644 --- a/clash-verge-rev/src/hooks/use-clash.ts +++ b/clash-verge-rev/src/hooks/use-clash.ts @@ -1,5 +1,6 @@ -import useSWR, { mutate } from "swr"; import { useLockFn } from "ahooks"; +import useSWR, { mutate } from "swr"; + import { getVersion } from "@/services/cmds"; import { getClashInfo, diff --git a/clash-verge-rev/src/hooks/use-current-proxy.ts b/clash-verge-rev/src/hooks/use-current-proxy.ts index 84da26e501..7d35232692 100644 --- a/clash-verge-rev/src/hooks/use-current-proxy.ts +++ b/clash-verge-rev/src/hooks/use-current-proxy.ts @@ -1,5 +1,6 @@ import { useMemo } from "react"; -import { useAppData } from "@/providers/app-data-provider"; + +import { useAppData } from "@/providers/app-data-context"; // 定义代理组类型 interface ProxyGroup { diff --git a/clash-verge-rev/src/hooks/use-i18n.ts b/clash-verge-rev/src/hooks/use-i18n.ts new file mode 100644 index 0000000000..c82b05ffaa --- /dev/null +++ b/clash-verge-rev/src/hooks/use-i18n.ts @@ -0,0 +1,47 @@ +import { useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; + +import { changeLanguage, supportedLanguages } from "@/services/i18n"; + +import { useVerge } from "./use-verge"; + +export const useI18n = () => { + const { i18n, t } = useTranslation(); + const { patchVerge } = useVerge(); + const [isLoading, setIsLoading] = useState(false); + + const switchLanguage = useCallback( + async (language: string) => { + if (!supportedLanguages.includes(language)) { + console.warn(`Unsupported language: ${language}`); + return; + } + + if (i18n.language === language) { + return; + } + + setIsLoading(true); + try { + await changeLanguage(language); + + if (patchVerge) { + await patchVerge({ language }); + } + } catch (error) { + console.error("Failed to change language:", error); + } finally { + setIsLoading(false); + } + }, + [i18n.language, patchVerge], + ); + + return { + currentLanguage: i18n.language, + supportedLanguages, + switchLanguage, + isLoading, + t, + }; +}; diff --git a/clash-verge-rev/src/hooks/use-listen.ts b/clash-verge-rev/src/hooks/use-listen.ts index 4d17b6b5a9..4cd0e7222b 100644 --- a/clash-verge-rev/src/hooks/use-listen.ts +++ b/clash-verge-rev/src/hooks/use-listen.ts @@ -1,5 +1,5 @@ -import { listen, UnlistenFn, EventCallback } from "@tauri-apps/api/event"; import { event } from "@tauri-apps/api"; +import { listen, UnlistenFn, EventCallback } from "@tauri-apps/api/event"; import { useRef } from "react"; export const useListen = () => { diff --git a/clash-verge-rev/src/hooks/use-log-data.ts b/clash-verge-rev/src/hooks/use-log-data.ts index cecf7168a7..441b3f166c 100644 --- a/clash-verge-rev/src/hooks/use-log-data.ts +++ b/clash-verge-rev/src/hooks/use-log-data.ts @@ -1,52 +1,11 @@ -import { create } from "zustand"; import { useGlobalLogData, clearGlobalLogs, LogLevel, - ILogItem, } from "@/services/global-log-service"; // 为了向后兼容,导出相同的类型 export type { LogLevel }; -export type { ILogItem }; - -const MAX_LOG_NUM = 1000; - -const buildWSUrl = (server: string, logLevel: LogLevel) => { - let baseUrl = `${server}/logs`; - - // 只处理日志级别参数 - if (logLevel && logLevel !== "info") { - const level = logLevel === "all" ? "debug" : logLevel; - baseUrl += `?level=${level}`; - } - - return baseUrl; -}; - -interface LogStore { - logs: ILogItem[]; - clearLogs: () => void; - appendLog: (log: ILogItem) => void; -} - -const useLogStore = create( - (set: (fn: (state: LogStore) => Partial) => void) => ({ - logs: [], - clearLogs: () => - set(() => ({ - logs: [], - })), - appendLog: (log: ILogItem) => - set((state: LogStore) => { - const newLogs = - state.logs.length >= MAX_LOG_NUM - ? [...state.logs.slice(1), log] - : [...state.logs, log]; - return { logs: newLogs }; - }), - }), -); export const useLogData = useGlobalLogData; diff --git a/clash-verge-rev/src/hooks/use-profiles.ts b/clash-verge-rev/src/hooks/use-profiles.ts index 111bf78c37..5484cf70b9 100644 --- a/clash-verge-rev/src/hooks/use-profiles.ts +++ b/clash-verge-rev/src/hooks/use-profiles.ts @@ -1,4 +1,5 @@ import useSWR, { mutate } from "swr"; + import { getProfiles, patchProfile, @@ -8,17 +9,28 @@ import { import { getProxies, updateProxy } from "@/services/cmds"; export const useProfiles = () => { - const { data: profiles, mutate: mutateProfiles } = useSWR( - "getProfiles", - getProfiles, - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - dedupingInterval: 2000, - errorRetryCount: 2, - errorRetryInterval: 1000, + const { + data: profiles, + mutate: mutateProfiles, + error, + isValidating, + } = useSWR("getProfiles", getProfiles, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 500, // 减少去重时间,提高响应性 + errorRetryCount: 3, + errorRetryInterval: 1000, + refreshInterval: 0, // 完全由手动控制 + onError: (error) => { + console.error("[useProfiles] SWR错误:", error); }, - ); + onSuccess: (data) => { + console.log( + "[useProfiles] 配置数据更新成功,配置数量:", + data?.items?.length || 0, + ); + }, + }); const patchProfiles = async ( value: Partial, @@ -153,5 +165,9 @@ export const useProfiles = () => { patchProfiles, patchCurrent, mutateProfiles, + // 新增故障检测状态 + isLoading: isValidating, + error, + isStale: !profiles && !error && !isValidating, // 检测是否处于异常状态 }; }; diff --git a/clash-verge-rev/src/hooks/use-proxy-selection.ts b/clash-verge-rev/src/hooks/use-proxy-selection.ts new file mode 100644 index 0000000000..47fc53d979 --- /dev/null +++ b/clash-verge-rev/src/hooks/use-proxy-selection.ts @@ -0,0 +1,144 @@ +import { useLockFn } from "ahooks"; +import { useCallback, useMemo } from "react"; + +import { useProfiles } from "@/hooks/use-profiles"; +import { useVerge } from "@/hooks/use-verge"; +import { + updateProxy, + updateProxyAndSync, + forceRefreshProxies, + syncTrayProxySelection, + getConnections, + deleteConnection, +} from "@/services/cmds"; + +// 缓存连接清理 +const cleanupConnections = async (previousProxy: string) => { + try { + const { connections } = await getConnections(); + const cleanupPromises = connections + .filter((conn) => conn.chains.includes(previousProxy)) + .map((conn) => deleteConnection(conn.id)); + + if (cleanupPromises.length > 0) { + await Promise.allSettled(cleanupPromises); + console.log(`[ProxySelection] 清理了 ${cleanupPromises.length} 个连接`); + } + } catch (error) { + console.warn("[ProxySelection] 连接清理失败:", error); + } +}; + +interface ProxySelectionOptions { + onSuccess?: () => void; + onError?: (error: any) => void; + enableConnectionCleanup?: boolean; +} + +// 代理选择 Hook +export const useProxySelection = (options: ProxySelectionOptions = {}) => { + const { current, patchCurrent } = useProfiles(); + const { verge } = useVerge(); + + const { onSuccess, onError, enableConnectionCleanup = true } = options; + + // 缓存 + const config = useMemo( + () => ({ + autoCloseConnection: verge?.auto_close_connection ?? false, + enableConnectionCleanup, + }), + [verge?.auto_close_connection, enableConnectionCleanup], + ); + + // 切换节点 + const changeProxy = useLockFn( + async ( + groupName: string, + proxyName: string, + previousProxy?: string, + skipConfigSave: boolean = false, + ) => { + console.log(`[ProxySelection] 代理切换: ${groupName} -> ${proxyName}`); + + try { + if (current && !skipConfigSave) { + if (!current.selected) current.selected = []; + + const index = current.selected.findIndex( + (item) => item.name === groupName, + ); + + if (index < 0) { + current.selected.push({ name: groupName, now: proxyName }); + } else { + current.selected[index] = { name: groupName, now: proxyName }; + } + await patchCurrent({ selected: current.selected }); + } + + await updateProxyAndSync(groupName, proxyName); + console.log( + `[ProxySelection] 代理和状态同步完成: ${groupName} -> ${proxyName}`, + ); + + onSuccess?.(); + + if ( + config.enableConnectionCleanup && + config.autoCloseConnection && + previousProxy + ) { + setTimeout(() => cleanupConnections(previousProxy), 0); + } + } catch (error) { + console.error( + `[ProxySelection] 代理切换失败: ${groupName} -> ${proxyName}`, + error, + ); + + try { + await updateProxy(groupName, proxyName); + await forceRefreshProxies(); + await syncTrayProxySelection(); + onSuccess?.(); + console.log( + `[ProxySelection] 代理切换回退成功: ${groupName} -> ${proxyName}`, + ); + } catch (fallbackError) { + console.error( + `[ProxySelection] 代理切换回退也失败: ${groupName} -> ${proxyName}`, + fallbackError, + ); + onError?.(fallbackError); + } + } + }, + ); + + const handleSelectChange = useCallback( + ( + groupName: string, + previousProxy?: string, + skipConfigSave: boolean = false, + ) => + (event: { target: { value: string } }) => { + const newProxy = event.target.value; + changeProxy(groupName, newProxy, previousProxy, skipConfigSave); + }, + [changeProxy], + ); + + const handleProxyGroupChange = useCallback( + (group: { name: string; now?: string }, proxy: { name: string }) => { + changeProxy(group.name, proxy.name, group.now); + }, + [changeProxy], + ); + + return { + changeProxy, + handleSelectChange, + handleProxyGroupChange, + }; +}; diff --git a/clash-verge-rev/src/hooks/use-system-proxy-state.ts b/clash-verge-rev/src/hooks/use-system-proxy-state.ts index 22f1c7cba1..75c6fde879 100644 --- a/clash-verge-rev/src/hooks/use-system-proxy-state.ts +++ b/clash-verge-rev/src/hooks/use-system-proxy-state.ts @@ -1,8 +1,8 @@ import useSWR, { mutate } from "swr"; + import { useVerge } from "@/hooks/use-verge"; -import { getAutotemProxy } from "@/services/cmds"; -import { useAppData } from "@/providers/app-data-provider"; -import { closeAllConnections } from "@/services/cmds"; +import { useAppData } from "@/providers/app-data-context"; +import { closeAllConnections, getAutotemProxy } from "@/services/cmds"; // 系统代理状态检测统一逻辑 export const useSystemProxyState = () => { @@ -18,13 +18,18 @@ export const useSystemProxyState = () => { const getSystemProxyActualState = () => { const userEnabled = enable_system_proxy ?? false; + // 用户配置状态应该与系统实际状态一致 + // 如果用户启用了系统代理,检查实际的系统状态 if (userEnabled) { - return true; + if (proxy_auto_config) { + return autoproxy?.enable ?? false; + } else { + return sysproxy?.enable ?? false; + } } - return autoproxy?.enable === false && sysproxy?.enable === false - ? false - : userEnabled; + // 用户没有启用时,返回 false + return false; }; const getSystemProxyIndicator = () => { @@ -53,6 +58,7 @@ export const useSystemProxyState = () => { updateProxyStatus(); } catch (error) { + console.warn("[useSystemProxyState] toggleSystemProxy failed:", error); mutateVerge({ ...verge, enable_system_proxy: !enabled }, false); } }, 0); diff --git a/clash-verge-rev/src/hooks/use-system-state.ts b/clash-verge-rev/src/hooks/use-system-state.ts index b8ed5c4afc..c638214aab 100644 --- a/clash-verge-rev/src/hooks/use-system-state.ts +++ b/clash-verge-rev/src/hooks/use-system-state.ts @@ -1,4 +1,5 @@ import useSWR from "swr"; + import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds"; /** @@ -7,39 +8,57 @@ import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds"; */ export function useSystemState() { // 获取运行模式 - const { data: runningMode = "Sidecar", mutate: mutateRunningMode } = useSWR( - "getRunningMode", - getRunningMode, - { - suspense: false, - revalidateOnFocus: false, - }, - ); - - // 获取管理员状态 - const { data: isAdminMode = false } = useSWR("isAdmin", isAdmin, { + const { + data: runningMode = "Sidecar", + mutate: mutateRunningMode, + isLoading: runningModeLoading, + } = useSWR("getRunningMode", getRunningMode, { suspense: false, revalidateOnFocus: false, }); - - // 获取系统服务状态 + const isSidecarMode = runningMode === "Sidecar"; const isServiceMode = runningMode === "Service"; - const { data: isServiceOk = false } = useSWR( - "isServiceAvailable", - isServiceAvailable, + + // 获取管理员状态 + const { data: isAdminMode = false, isLoading: isAdminLoading } = useSWR( + "isAdmin", + isAdmin, { suspense: false, revalidateOnFocus: false, - isPaused: () => !isServiceMode, // 仅在 Service 模式下请求 }, ); + const { + data: isServiceOk = false, + mutate: mutateServiceOk, + isLoading: isServiceLoading, + } = useSWR(isServiceMode ? "isServiceAvailable" : null, isServiceAvailable, { + suspense: false, + revalidateOnFocus: false, + onSuccess: (data) => { + console.log("[useSystemState] 服务状态更新:", data); + }, + onError: (error) => { + console.error("[useSystemState] 服务状态检查失败:", error); + }, + // isPaused: () => !isServiceMode, // 仅在非 Service 模式下暂停请求 + }); + + const isLoading = + runningModeLoading || isAdminLoading || (isServiceMode && isServiceLoading); + + const isTunModeAvailable = isAdminMode || isServiceOk; + return { runningMode, isAdminMode, - isSidecarMode: runningMode === "Sidecar", - isServiceMode: runningMode === "Service", + isSidecarMode, + isServiceMode, isServiceOk, + isTunModeAvailable: isTunModeAvailable, mutateRunningMode, + mutateServiceOk, + isLoading, }; } diff --git a/clash-verge-rev/src/hooks/use-traffic-monitor-enhanced.ts b/clash-verge-rev/src/hooks/use-traffic-monitor-enhanced.ts deleted file mode 100644 index 09a3a8aac0..0000000000 --- a/clash-verge-rev/src/hooks/use-traffic-monitor-enhanced.ts +++ /dev/null @@ -1,398 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from "react"; -import useSWR from "swr"; -import { useClashInfo } from "@/hooks/use-clash"; -import { useVisibility } from "@/hooks/use-visibility"; -import { getSystemMonitorOverviewSafe } from "@/services/cmds"; - -// 增强的流量数据点接口 -export interface ITrafficDataPoint { - up: number; - down: number; - timestamp: number; - name: string; -} - -// 压缩的数据点(用于长期存储) -interface ICompressedDataPoint { - up: number; - down: number; - timestamp: number; - samples: number; // 压缩了多少个原始数据点 -} - -// 数据采样器配置 -interface ISamplingConfig { - // 原始数据保持时间(分钟) - rawDataMinutes: number; - // 压缩数据保持时间(分钟) - compressedDataMinutes: number; - // 压缩比例(多少个原始点压缩成1个) - compressionRatio: number; -} - -// 引用计数管理器 -class ReferenceCounter { - private count = 0; - private callbacks: (() => void)[] = []; - - increment(): () => void { - this.count++; - console.log(`[ReferenceCounter] 引用计数增加: ${this.count}`); - - if (this.count === 1) { - // 从0到1,开始数据收集 - this.callbacks.forEach((cb) => cb()); - } - - return () => { - this.count--; - console.log(`[ReferenceCounter] 引用计数减少: ${this.count}`); - - if (this.count === 0) { - // 从1到0,停止数据收集 - this.callbacks.forEach((cb) => cb()); - } - }; - } - - onCountChange(callback: () => void) { - this.callbacks.push(callback); - } - - getCount(): number { - return this.count; - } -} - -// 智能数据采样器 -class TrafficDataSampler { - private rawBuffer: ITrafficDataPoint[] = []; - private compressedBuffer: ICompressedDataPoint[] = []; - private config: ISamplingConfig; - private compressionQueue: ITrafficDataPoint[] = []; - - constructor(config: ISamplingConfig) { - this.config = config; - } - - addDataPoint(point: ITrafficDataPoint): void { - // 添加到原始缓冲区 - this.rawBuffer.push(point); - - // 清理过期的原始数据 - const rawCutoff = Date.now() - this.config.rawDataMinutes * 60 * 1000; - this.rawBuffer = this.rawBuffer.filter((p) => p.timestamp > rawCutoff); - - // 添加到压缩队列 - this.compressionQueue.push(point); - - // 当压缩队列达到压缩比例时,执行压缩 - if (this.compressionQueue.length >= this.config.compressionRatio) { - this.compressData(); - } - - // 清理过期的压缩数据 - const compressedCutoff = - Date.now() - this.config.compressedDataMinutes * 60 * 1000; - this.compressedBuffer = this.compressedBuffer.filter( - (p) => p.timestamp > compressedCutoff, - ); - } - - private compressData(): void { - if (this.compressionQueue.length === 0) return; - - // 计算平均值进行压缩 - const totalUp = this.compressionQueue.reduce((sum, p) => sum + p.up, 0); - const totalDown = this.compressionQueue.reduce((sum, p) => sum + p.down, 0); - const avgTimestamp = - this.compressionQueue.reduce((sum, p) => sum + p.timestamp, 0) / - this.compressionQueue.length; - - const compressedPoint: ICompressedDataPoint = { - up: totalUp / this.compressionQueue.length, - down: totalDown / this.compressionQueue.length, - timestamp: avgTimestamp, - samples: this.compressionQueue.length, - }; - - this.compressedBuffer.push(compressedPoint); - this.compressionQueue = []; - - console.log(`[DataSampler] 压缩了 ${compressedPoint.samples} 个数据点`); - } - - getDataForTimeRange(minutes: number): ITrafficDataPoint[] { - const cutoff = Date.now() - minutes * 60 * 1000; - - // 如果请求的时间范围在原始数据范围内,直接返回原始数据 - if (minutes <= this.config.rawDataMinutes) { - return this.rawBuffer.filter((p) => p.timestamp > cutoff); - } - - // 否则组合原始数据和压缩数据 - const rawData = this.rawBuffer.filter((p) => p.timestamp > cutoff); - const compressedData = this.compressedBuffer - .filter( - (p) => - p.timestamp > cutoff && - p.timestamp <= Date.now() - this.config.rawDataMinutes * 60 * 1000, - ) - .map((p) => ({ - up: p.up, - down: p.down, - timestamp: p.timestamp, - name: new Date(p.timestamp).toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - })); - - return [...compressedData, ...rawData].sort( - (a, b) => a.timestamp - b.timestamp, - ); - } - - getStats() { - return { - rawBufferSize: this.rawBuffer.length, - compressedBufferSize: this.compressedBuffer.length, - compressionQueueSize: this.compressionQueue.length, - totalMemoryPoints: this.rawBuffer.length + this.compressedBuffer.length, - }; - } - - clear(): void { - this.rawBuffer = []; - this.compressedBuffer = []; - this.compressionQueue = []; - } -} - -// 全局单例 -const refCounter = new ReferenceCounter(); -let globalSampler: TrafficDataSampler | null = null; -let lastValidData: ISystemMonitorOverview | null = null; - -/** - * 增强的流量监控Hook - 支持数据压缩、采样和引用计数 - */ -export const useTrafficMonitorEnhanced = () => { - const { clashInfo } = useClashInfo(); - const pageVisible = useVisibility(); - - // 初始化采样器 - if (!globalSampler) { - globalSampler = new TrafficDataSampler({ - rawDataMinutes: 10, // 原始数据保持10分钟 - compressedDataMinutes: 60, // 压缩数据保持1小时 - compressionRatio: 5, // 每5个原始点压缩成1个 - }); - } - - const [, forceUpdate] = useState({}); - const cleanupRef = useRef<(() => void) | null>(null); - - // 强制组件更新 - const triggerUpdate = useCallback(() => { - forceUpdate({}); - }, []); - - // 注册引用计数 - useEffect(() => { - console.log("[TrafficMonitorEnhanced] 组件挂载,注册引用计数"); - const cleanup = refCounter.increment(); - cleanupRef.current = cleanup; - - return () => { - console.log("[TrafficMonitorEnhanced] 组件卸载,清理引用计数"); - cleanup(); - cleanupRef.current = null; - }; - }, []); - - // 设置引用计数变化回调 - useEffect(() => { - const handleCountChange = () => { - console.log( - `[TrafficMonitorEnhanced] 引用计数变化: ${refCounter.getCount()}`, - ); - if (refCounter.getCount() === 0) { - console.log("[TrafficMonitorEnhanced] 所有组件已卸载,暂停数据收集"); - } else { - console.log("[TrafficMonitorEnhanced] 开始数据收集"); - } - }; - - refCounter.onCountChange(handleCountChange); - }, []); - - // 只有在有引用时才启用SWR - const shouldFetch = clashInfo && pageVisible && refCounter.getCount() > 0; - - const { data: monitorData, error } = useSWR( - shouldFetch ? "getSystemMonitorOverviewSafe" : null, - getSystemMonitorOverviewSafe, - { - refreshInterval: shouldFetch ? 1000 : 0, // 只有在需要时才刷新 - keepPreviousData: true, - onSuccess: (data) => { - // console.log("[TrafficMonitorEnhanced] 获取到监控数据:", data); - - if (data?.traffic?.raw && globalSampler) { - // 保存最后有效数据 - lastValidData = data; - - // 添加到采样器 - const timestamp = Date.now(); - const dataPoint: ITrafficDataPoint = { - up: data.traffic.raw.up_rate || 0, - down: data.traffic.raw.down_rate || 0, - timestamp, - name: new Date(timestamp).toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - }; - - globalSampler.addDataPoint(dataPoint); - triggerUpdate(); - } - }, - onError: (error) => { - console.error( - "[TrafficMonitorEnhanced] 网络错误,使用最后有效数据. 错误详情:", - { - message: error?.message || "未知错误", - stack: error?.stack || "无堆栈信息", - }, - ); - // 网络错误时不清空数据,继续使用最后有效值 - // 但是添加一个错误标记的数据点(流量为0) - if (globalSampler) { - const timestamp = Date.now(); - const errorPoint: ITrafficDataPoint = { - up: 0, - down: 0, - timestamp, - name: new Date(timestamp).toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - }; - globalSampler.addDataPoint(errorPoint); - triggerUpdate(); - } - }, - }, - ); - - // 获取指定时间范围的数据 - const getDataForTimeRange = useCallback( - (minutes: number): ITrafficDataPoint[] => { - if (!globalSampler) return []; - return globalSampler.getDataForTimeRange(minutes); - }, - [], - ); - - // 清空数据 - const clearData = useCallback(() => { - if (globalSampler) { - globalSampler.clear(); - triggerUpdate(); - } - }, [triggerUpdate]); - - // 获取采样器统计信息 - const getSamplerStats = useCallback(() => { - return ( - globalSampler?.getStats() || { - rawBufferSize: 0, - compressedBufferSize: 0, - compressionQueueSize: 0, - totalMemoryPoints: 0, - } - ); - }, []); - - // 构建返回的监控数据,优先使用当前数据,fallback到最后有效数据 - const currentData = monitorData || lastValidData; - const trafficMonitorData = { - traffic: currentData?.traffic || { - raw: { up: 0, down: 0, up_rate: 0, down_rate: 0 }, - formatted: { - up_rate: "0B", - down_rate: "0B", - total_up: "0B", - total_down: "0B", - }, - is_fresh: false, - }, - memory: currentData?.memory || { - raw: { inuse: 0, oslimit: 0, usage_percent: 0 }, - formatted: { inuse: "0B", oslimit: "0B", usage_percent: 0 }, - is_fresh: false, - }, - }; - - return { - // 监控数据 - monitorData: trafficMonitorData, - - // 图表数据管理 - graphData: { - dataPoints: globalSampler?.getDataForTimeRange(60) || [], // 默认获取1小时数据 - getDataForTimeRange, - clearData, - }, - - // 状态信息 - isLoading: !currentData && !error, - error, - isDataFresh: currentData?.traffic?.is_fresh || false, - hasValidData: !!lastValidData, - - // 性能统计 - samplerStats: getSamplerStats(), - referenceCount: refCounter.getCount(), - }; -}; - -/** - * 轻量级流量数据Hook - */ -export const useTrafficDataEnhanced = () => { - const { monitorData, isLoading, error, isDataFresh, hasValidData } = - useTrafficMonitorEnhanced(); - - return { - traffic: monitorData.traffic, - memory: monitorData.memory, - isLoading, - error, - isDataFresh, - hasValidData, - }; -}; - -/** - * 图表数据Hook - */ -export const useTrafficGraphDataEnhanced = () => { - const { graphData, isDataFresh, samplerStats, referenceCount } = - useTrafficMonitorEnhanced(); - - return { - ...graphData, - isDataFresh, - samplerStats, - referenceCount, - }; -}; diff --git a/clash-verge-rev/src/hooks/use-traffic-monitor.ts b/clash-verge-rev/src/hooks/use-traffic-monitor.ts index 211da2823f..abaa56772e 100644 --- a/clash-verge-rev/src/hooks/use-traffic-monitor.ts +++ b/clash-verge-rev/src/hooks/use-traffic-monitor.ts @@ -1,10 +1,11 @@ import { useState, useEffect, useRef, useCallback } from "react"; import useSWR from "swr"; + import { useClashInfo } from "@/hooks/use-clash"; import { useVisibility } from "@/hooks/use-visibility"; -import { getSystemMonitorOverview } from "@/services/cmds"; +import { getSystemMonitorOverviewSafe } from "@/services/cmds"; -// 流量数据项接口 +// 增强的流量数据点接口 export interface ITrafficDataPoint { up: number; down: number; @@ -12,215 +13,365 @@ export interface ITrafficDataPoint { name: string; } -// 流量监控数据接口 -export interface ITrafficMonitorData { - traffic: { - raw: { up_rate: number; down_rate: number }; - formatted: { up_rate: string; down_rate: string }; - is_fresh: boolean; - }; - memory: { - raw: { inuse: number; oslimit?: number }; - formatted: { inuse: string; usage_percent?: number }; - is_fresh: boolean; - }; +// 压缩的数据点(用于长期存储) +interface ICompressedDataPoint { + up: number; + down: number; + timestamp: number; + samples: number; // 压缩了多少个原始数据点 } -// 图表数据管理接口 -export interface ITrafficGraphData { - dataPoints: ITrafficDataPoint[]; - addDataPoint: (data: { - up: number; - down: number; - timestamp?: number; - }) => void; - clearData: () => void; - getDataForTimeRange: (minutes: number) => ITrafficDataPoint[]; +// 数据采样器配置 +interface ISamplingConfig { + // 原始数据保持时间(分钟) + rawDataMinutes: number; + // 压缩数据保持时间(分钟) + compressedDataMinutes: number; + // 压缩比例(多少个原始点压缩成1个) + compressionRatio: number; } +// 引用计数管理器 +class ReferenceCounter { + private count = 0; + private callbacks: (() => void)[] = []; + + increment(): () => void { + this.count++; + console.log(`[ReferenceCounter] 引用计数增加: ${this.count}`); + + if (this.count === 1) { + // 从0到1,开始数据收集 + this.callbacks.forEach((cb) => cb()); + } + + return () => { + this.count--; + console.log(`[ReferenceCounter] 引用计数减少: ${this.count}`); + + if (this.count === 0) { + // 从1到0,停止数据收集 + this.callbacks.forEach((cb) => cb()); + } + }; + } + + onCountChange(callback: () => void) { + this.callbacks.push(callback); + } + + getCount(): number { + return this.count; + } +} + +// 智能数据采样器 +class TrafficDataSampler { + private rawBuffer: ITrafficDataPoint[] = []; + private compressedBuffer: ICompressedDataPoint[] = []; + private config: ISamplingConfig; + private compressionQueue: ITrafficDataPoint[] = []; + + constructor(config: ISamplingConfig) { + this.config = config; + } + + addDataPoint(point: ITrafficDataPoint): void { + // 添加到原始缓冲区 + this.rawBuffer.push(point); + + // 清理过期的原始数据 + const rawCutoff = Date.now() - this.config.rawDataMinutes * 60 * 1000; + this.rawBuffer = this.rawBuffer.filter((p) => p.timestamp > rawCutoff); + + // 添加到压缩队列 + this.compressionQueue.push(point); + + // 当压缩队列达到压缩比例时,执行压缩 + if (this.compressionQueue.length >= this.config.compressionRatio) { + this.compressData(); + } + + // 清理过期的压缩数据 + const compressedCutoff = + Date.now() - this.config.compressedDataMinutes * 60 * 1000; + this.compressedBuffer = this.compressedBuffer.filter( + (p) => p.timestamp > compressedCutoff, + ); + } + + private compressData(): void { + if (this.compressionQueue.length === 0) return; + + // 计算平均值进行压缩 + const totalUp = this.compressionQueue.reduce((sum, p) => sum + p.up, 0); + const totalDown = this.compressionQueue.reduce((sum, p) => sum + p.down, 0); + const avgTimestamp = + this.compressionQueue.reduce((sum, p) => sum + p.timestamp, 0) / + this.compressionQueue.length; + + const compressedPoint: ICompressedDataPoint = { + up: totalUp / this.compressionQueue.length, + down: totalDown / this.compressionQueue.length, + timestamp: avgTimestamp, + samples: this.compressionQueue.length, + }; + + this.compressedBuffer.push(compressedPoint); + this.compressionQueue = []; + + console.log(`[DataSampler] 压缩了 ${compressedPoint.samples} 个数据点`); + } + + getDataForTimeRange(minutes: number): ITrafficDataPoint[] { + const cutoff = Date.now() - minutes * 60 * 1000; + + // 如果请求的时间范围在原始数据范围内,直接返回原始数据 + if (minutes <= this.config.rawDataMinutes) { + return this.rawBuffer.filter((p) => p.timestamp > cutoff); + } + + // 否则组合原始数据和压缩数据 + const rawData = this.rawBuffer.filter((p) => p.timestamp > cutoff); + const compressedData = this.compressedBuffer + .filter( + (p) => + p.timestamp > cutoff && + p.timestamp <= Date.now() - this.config.rawDataMinutes * 60 * 1000, + ) + .map((p) => ({ + up: p.up, + down: p.down, + timestamp: p.timestamp, + name: new Date(p.timestamp).toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }), + })); + + return [...compressedData, ...rawData].sort( + (a, b) => a.timestamp - b.timestamp, + ); + } + + getStats() { + return { + rawBufferSize: this.rawBuffer.length, + compressedBufferSize: this.compressedBuffer.length, + compressionQueueSize: this.compressionQueue.length, + totalMemoryPoints: this.rawBuffer.length + this.compressedBuffer.length, + }; + } + + clear(): void { + this.rawBuffer = []; + this.compressedBuffer = []; + this.compressionQueue = []; + } +} + +// 全局单例 +const refCounter = new ReferenceCounter(); +let globalSampler: TrafficDataSampler | null = null; +let lastValidData: ISystemMonitorOverview | null = null; + /** - * 全局流量监控数据管理Hook - * 提供统一的流量数据获取和图表数据管理 + * 增强的流量监控Hook - 支持数据压缩、采样和引用计数 */ -export const useTrafficMonitor = () => { +export const useTrafficMonitorEnhanced = () => { const { clashInfo } = useClashInfo(); const pageVisible = useVisibility(); - // 图表数据缓冲区 - 使用ref保持数据持久性 - const dataBufferRef = useRef([]); - const [, forceUpdate] = useState({}); + // 初始化采样器 + if (!globalSampler) { + globalSampler = new TrafficDataSampler({ + rawDataMinutes: 10, // 原始数据保持10分钟 + compressedDataMinutes: 60, // 压缩数据保持1小时 + compressionRatio: 5, // 每5个原始点压缩成1个 + }); + } - // 强制组件更新的函数 + const [, forceUpdate] = useState({}); + const cleanupRef = useRef<(() => void) | null>(null); + + // 强制组件更新 const triggerUpdate = useCallback(() => { forceUpdate({}); }, []); - // 最大缓冲区大小 (10分钟 * 60秒 = 600个数据点) - const MAX_BUFFER_SIZE = 600; - - // 初始化数据缓冲区 + // 注册引用计数 useEffect(() => { - if (dataBufferRef.current.length === 0) { - const now = Date.now(); - const tenMinutesAgo = now - 10 * 60 * 1000; + console.log("[TrafficMonitorEnhanced] 组件挂载,注册引用计数"); + const cleanup = refCounter.increment(); + cleanupRef.current = cleanup; - const initialBuffer = Array.from( - { length: MAX_BUFFER_SIZE }, - (_, index) => { - const pointTime = - tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE); - const date = new Date(pointTime); + return () => { + console.log("[TrafficMonitorEnhanced] 组件卸载,清理引用计数"); + cleanup(); + cleanupRef.current = null; + }; + }, []); - let nameValue: string; - try { - if (isNaN(date.getTime())) { - nameValue = "??:??:??"; - } else { - nameValue = date.toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - } - } catch (e) { - nameValue = "Err:Time"; - } - - return { - up: 0, - down: 0, - timestamp: pointTime, - name: nameValue, - }; - }, + // 设置引用计数变化回调 + useEffect(() => { + const handleCountChange = () => { + console.log( + `[TrafficMonitorEnhanced] 引用计数变化: ${refCounter.getCount()}`, ); + if (refCounter.getCount() === 0) { + console.log("[TrafficMonitorEnhanced] 所有组件已卸载,暂停数据收集"); + } else { + console.log("[TrafficMonitorEnhanced] 开始数据收集"); + } + }; - dataBufferRef.current = initialBuffer; - } - }, [MAX_BUFFER_SIZE]); + refCounter.onCountChange(handleCountChange); + }, []); + + // 只有在有引用时才启用SWR + const shouldFetch = clashInfo && pageVisible && refCounter.getCount() > 0; - // 使用SWR获取监控数据 const { data: monitorData, error } = useSWR( - clashInfo && pageVisible ? "getSystemMonitorOverview" : null, - getSystemMonitorOverview, + shouldFetch ? "getSystemMonitorOverviewSafe" : null, + getSystemMonitorOverviewSafe, { - refreshInterval: 1000, // 1秒刷新一次 + refreshInterval: shouldFetch ? 1000 : 0, // 只有在需要时才刷新 keepPreviousData: true, onSuccess: (data) => { - console.log("[TrafficMonitor] 获取到监控数据:", data); + // console.log("[TrafficMonitorEnhanced] 获取到监控数据:", data); - if (data?.traffic) { - // 为图表添加新数据点 - addDataPoint({ + if (data?.traffic?.raw && globalSampler) { + // 保存最后有效数据 + lastValidData = data; + + // 添加到采样器 + const timestamp = Date.now(); + const dataPoint: ITrafficDataPoint = { up: data.traffic.raw.up_rate || 0, down: data.traffic.raw.down_rate || 0, - timestamp: Date.now(), - }); + timestamp, + name: new Date(timestamp).toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }), + }; + + globalSampler.addDataPoint(dataPoint); + triggerUpdate(); } }, onError: (error) => { - console.error("[TrafficMonitor] 获取数据错误:", error); + console.error( + "[TrafficMonitorEnhanced] 网络错误,使用最后有效数据. 错误详情:", + { + message: error?.message || "未知错误", + stack: error?.stack || "无堆栈信息", + }, + ); + // 网络错误时不清空数据,继续使用最后有效值 + // 但是添加一个错误标记的数据点(流量为0) + if (globalSampler) { + const timestamp = Date.now(); + const errorPoint: ITrafficDataPoint = { + up: 0, + down: 0, + timestamp, + name: new Date(timestamp).toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }), + }; + globalSampler.addDataPoint(errorPoint); + triggerUpdate(); + } }, }, ); - // 添加数据点到缓冲区 - const addDataPoint = useCallback( - (data: { up: number; down: number; timestamp?: number }) => { - const timestamp = data.timestamp || Date.now(); - const date = new Date(timestamp); - - let nameValue: string; - try { - if (isNaN(date.getTime())) { - nameValue = "??:??:??"; - } else { - nameValue = date.toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - } - } catch (e) { - nameValue = "Err:Time"; - } - - const newPoint: ITrafficDataPoint = { - up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0, - down: - typeof data.down === "number" && !isNaN(data.down) ? data.down : 0, - timestamp, - name: nameValue, - }; - - // 更新缓冲区,保持固定大小 - const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; - dataBufferRef.current = newBuffer; - - // 触发使用该数据的组件更新 - triggerUpdate(); - }, - [triggerUpdate], - ); - - // 清空数据 - const clearData = useCallback(() => { - dataBufferRef.current = []; - triggerUpdate(); - }, [triggerUpdate]); - - // 根据时间范围获取数据 + // 获取指定时间范围的数据 const getDataForTimeRange = useCallback( (minutes: number): ITrafficDataPoint[] => { - const pointsToShow = minutes * 60; // 每分钟60个数据点 - return dataBufferRef.current.slice(-pointsToShow); + if (!globalSampler) return []; + return globalSampler.getDataForTimeRange(minutes); }, [], ); - // 构建图表数据管理对象 - const graphData: ITrafficGraphData = { - dataPoints: dataBufferRef.current, - addDataPoint, - clearData, - getDataForTimeRange, - }; + // 清空数据 + const clearData = useCallback(() => { + if (globalSampler) { + globalSampler.clear(); + triggerUpdate(); + } + }, [triggerUpdate]); - // 构建监控数据对象 - const trafficMonitorData: ITrafficMonitorData = { - traffic: monitorData?.traffic || { - raw: { up_rate: 0, down_rate: 0 }, - formatted: { up_rate: "0B", down_rate: "0B" }, + // 获取采样器统计信息 + const getSamplerStats = useCallback(() => { + return ( + globalSampler?.getStats() || { + rawBufferSize: 0, + compressedBufferSize: 0, + compressionQueueSize: 0, + totalMemoryPoints: 0, + } + ); + }, []); + + // 构建返回的监控数据,优先使用当前数据,fallback到最后有效数据 + const currentData = monitorData || lastValidData; + const trafficMonitorData = { + traffic: currentData?.traffic || { + raw: { up: 0, down: 0, up_rate: 0, down_rate: 0 }, + formatted: { + up_rate: "0B", + down_rate: "0B", + total_up: "0B", + total_down: "0B", + }, is_fresh: false, }, - memory: monitorData?.memory || { - raw: { inuse: 0 }, - formatted: { inuse: "0B" }, + memory: currentData?.memory || { + raw: { inuse: 0, oslimit: 0, usage_percent: 0 }, + formatted: { inuse: "0B", oslimit: "0B", usage_percent: 0 }, is_fresh: false, }, }; return { - // 原始监控数据 + // 监控数据 monitorData: trafficMonitorData, + // 图表数据管理 - graphData, - // 数据获取状态 - isLoading: !monitorData && !error, + graphData: { + dataPoints: globalSampler?.getDataForTimeRange(60) || [], // 默认获取1小时数据 + getDataForTimeRange, + clearData, + }, + + // 状态信息 + isLoading: !currentData && !error, error, - // 数据新鲜度 - isDataFresh: monitorData?.overall_status === "active", + isDataFresh: currentData?.traffic?.is_fresh || false, + hasValidData: !!lastValidData, + + // 性能统计 + samplerStats: getSamplerStats(), + referenceCount: refCounter.getCount(), }; }; /** - * 仅获取流量数据的轻量级Hook - * 适用于不需要图表数据的组件 + * 轻量级流量数据Hook */ -export const useTrafficData = () => { - const { monitorData, isLoading, error, isDataFresh } = useTrafficMonitor(); +export const useTrafficDataEnhanced = () => { + const { monitorData, isLoading, error, isDataFresh, hasValidData } = + useTrafficMonitorEnhanced(); return { traffic: monitorData.traffic, @@ -228,18 +379,21 @@ export const useTrafficData = () => { isLoading, error, isDataFresh, + hasValidData, }; }; /** - * 仅获取图表数据的Hook - * 适用于图表组件 + * 图表数据Hook */ -export const useTrafficGraphData = () => { - const { graphData, isDataFresh } = useTrafficMonitor(); +export const useTrafficGraphDataEnhanced = () => { + const { graphData, isDataFresh, samplerStats, referenceCount } = + useTrafficMonitorEnhanced(); return { ...graphData, isDataFresh, + samplerStats, + referenceCount, }; }; diff --git a/clash-verge-rev/src/hooks/use-verge.ts b/clash-verge-rev/src/hooks/use-verge.ts index b6539c71a7..bbe33b8128 100644 --- a/clash-verge-rev/src/hooks/use-verge.ts +++ b/clash-verge-rev/src/hooks/use-verge.ts @@ -1,7 +1,15 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import useSWR from "swr"; + +import { useSystemState } from "@/hooks/use-system-state"; import { getVergeConfig, patchVergeConfig } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; export const useVerge = () => { + const { t } = useTranslation(); + const { isTunModeAvailable, isLoading } = useSystemState(); + const { data: verge, mutate: mutateVerge } = useSWR( "getVergeConfig", async () => { @@ -15,6 +23,28 @@ export const useVerge = () => { mutateVerge(); }; + const { enable_tun_mode } = verge ?? {}; + + // 当服务不可用且TUN模式开启时自动关闭TUN + useEffect(() => { + if (enable_tun_mode && !isTunModeAvailable && !isLoading) { + console.log("[useVerge] 检测到服务不可用,自动关闭TUN模式"); + + patchVergeConfig({ enable_tun_mode: false }) + .then(() => { + mutateVerge(); + showNotice( + "info", + t("TUN Mode automatically disabled due to service unavailable"), + ); + }) + .catch((err) => { + console.error("[useVerge] 自动关闭TUN模式失败:", err); + showNotice("error", t("Failed to disable TUN Mode automatically")); + }); + } + }, [isTunModeAvailable, isLoading, enable_tun_mode, mutateVerge, t]); + return { verge, mutateVerge, diff --git a/clash-verge-rev/src/hooks/useNotificationPermission.ts b/clash-verge-rev/src/hooks/useNotificationPermission.ts deleted file mode 100644 index 458695295f..0000000000 --- a/clash-verge-rev/src/hooks/useNotificationPermission.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { setupNotificationPermission } from "../utils/notification-permission"; -import { useEffect } from "react"; - -export function useNotificationPermission() { - useEffect(() => { - setupNotificationPermission(); - }, []); -} diff --git a/clash-verge-rev/src/hooks/useServiceInstaller.ts b/clash-verge-rev/src/hooks/useServiceInstaller.ts index 6dd5ba8c96..6422f17e89 100644 --- a/clash-verge-rev/src/hooks/useServiceInstaller.ts +++ b/clash-verge-rev/src/hooks/useServiceInstaller.ts @@ -1,121 +1,36 @@ -import { useTranslation } from "react-i18next"; -import { useLockFn } from "ahooks"; +import { t } from "i18next"; +import { useCallback } from "react"; + +import { installService, restartCore } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; -import { - installService, - isServiceAvailable, - restartCore, -} from "@/services/cmds"; -import { useSystemState } from "@/hooks/use-system-state"; -import { mutate } from "swr"; -export function useServiceInstaller() { - const { t } = useTranslation(); - const { mutateRunningMode } = useSystemState(); - - const installServiceAndRestartCore = useLockFn(async () => { - try { - showNotice("info", t("Installing Service...")); - await installService(); - showNotice("success", t("Service Installed Successfully")); - - showNotice("info", t("Waiting for service to be ready...")); - let serviceReady = false; - for (let i = 0; i < 5; i++) { - try { - // 等待1秒再检查 - await new Promise((resolve) => setTimeout(resolve, 1000)); - const isAvailable = await isServiceAvailable(); - if (isAvailable) { - serviceReady = true; - mutate("isServiceAvailable", true, false); - break; - } - // 最后一次尝试不显示重试信息 - if (i < 4) { - showNotice( - "info", - t("Service not ready, retrying attempt {count}/{total}...", { - count: i + 1, - total: 5, - }), - ); - } - } catch (error) { - console.error(t("Error checking service status:"), error); - if (i < 4) { - showNotice( - "error", - t( - "Failed to check service status, retrying attempt {count}/{total}...", - { - count: i + 1, - total: 5, - }, - ), - ); - } - } - } - - if (!serviceReady) { - showNotice( - "info", - t( - "Service did not become ready after attempts. Proceeding with core restart.", - ), - ); - } - - showNotice("info", t("Restarting Core...")); - await restartCore(); - - // 核心重启后,再次确认并更新相关状态 - await mutateRunningMode(); - const finalServiceStatus = await isServiceAvailable(); - mutate("isServiceAvailable", finalServiceStatus, false); - - if (serviceReady && finalServiceStatus) { - showNotice("success", t("Service is ready and core restarted")); - } else if (finalServiceStatus) { - showNotice("success", t("Core restarted. Service is now available.")); - } else if (serviceReady) { - showNotice( - "info", - t( - "Service was ready, but core restart might have issues or service became unavailable. Please check.", - ), - ); - } else { - showNotice( - "error", - t( - "Service installation or core restart encountered issues. Service might not be available. Please check system logs.", - ), - ); - } - return finalServiceStatus; - } catch (err: any) { - showNotice("error", err.message || err.toString()); - // 尝试性回退或最终操作 - try { - showNotice("info", t("Attempting to restart core as a fallback...")); - await restartCore(); - await mutateRunningMode(); - await isServiceAvailable().then((status) => - mutate("isServiceAvailable", status, false), - ); - } catch (recoveryError: any) { - showNotice( - "error", - t("Fallback core restart also failed: {message}", { - message: recoveryError.message, - }), - ); - } - return false; +const executeWithErrorHandling = async ( + operation: () => Promise, + loadingMessage: string, + successMessage?: string, +) => { + try { + showNotice("info", t(loadingMessage)); + await operation(); + if (successMessage) { + showNotice("success", t(successMessage)); } - }); + } catch (err) { + const msg = (err as Error)?.message || String(err); + showNotice("error", msg); + throw err; + } +}; +export const useServiceInstaller = () => { + const installServiceAndRestartCore = useCallback(async () => { + await executeWithErrorHandling( + () => installService(), + "Installing Service...", + "Service Installed Successfully", + ); + + await executeWithErrorHandling(() => restartCore(), "Restarting Core..."); + }, []); return { installServiceAndRestartCore }; -} +}; diff --git a/clash-verge-rev/src/hooks/useServiceUninstaller.ts b/clash-verge-rev/src/hooks/useServiceUninstaller.ts new file mode 100644 index 0000000000..03e525080f --- /dev/null +++ b/clash-verge-rev/src/hooks/useServiceUninstaller.ts @@ -0,0 +1,43 @@ +import { t } from "i18next"; +import { useCallback } from "react"; + +import { restartCore, stopCore, uninstallService } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; + +import { useSystemState } from "./use-system-state"; + +const executeWithErrorHandling = async ( + operation: () => Promise, + loadingMessage: string, + successMessage?: string, +) => { + try { + showNotice("info", t(loadingMessage)); + await operation(); + if (successMessage) { + showNotice("success", t(successMessage)); + } + } catch (err) { + const msg = (err as Error)?.message || String(err); + showNotice("error", msg); + throw err; + } +}; + +export const useServiceUninstaller = () => { + const { mutateRunningMode, mutateServiceOk } = useSystemState(); + + const uninstallServiceAndRestartCore = useCallback(async () => { + await executeWithErrorHandling(() => stopCore(), "Stopping Core..."); + + await executeWithErrorHandling( + () => uninstallService(), + "Uninstalling Service...", + "Service Uninstalled Successfully", + ); + + await executeWithErrorHandling(() => restartCore(), "Restarting Core..."); + }, [mutateRunningMode, mutateServiceOk]); + + return { uninstallServiceAndRestartCore }; +}; diff --git a/clash-verge-rev/src/locales/ar.json b/clash-verge-rev/src/locales/ar.json index ea131dbeb3..b7f25860fa 100644 --- a/clash-verge-rev/src/locales/ar.json +++ b/clash-verge-rev/src/locales/ar.json @@ -31,6 +31,7 @@ "rule": "قاعدة", "global": "عالمي", "direct": "مباشر", + "Chain Proxy": "🔗 بروكسي السلسلة", "script": "سكريبت", "locate": "الموقع", "Delay check": "فحص التأخير", diff --git a/clash-verge-rev/src/locales/de.json b/clash-verge-rev/src/locales/de.json index 389658d7f5..fba723cc2c 100644 --- a/clash-verge-rev/src/locales/de.json +++ b/clash-verge-rev/src/locales/de.json @@ -32,6 +32,7 @@ "rule": "Regel", "global": "Global", "direct": "Direktverbindung", + "Chain Proxy": "🔗 Ketten-Proxy", "script": "Skript", "locate": "Aktueller Knoten", "Delay check": "Latenztest", diff --git a/clash-verge-rev/src/locales/en.json b/clash-verge-rev/src/locales/en.json index 8fc5924088..6a3897ffdf 100644 --- a/clash-verge-rev/src/locales/en.json +++ b/clash-verge-rev/src/locales/en.json @@ -26,6 +26,11 @@ "Label-Settings": "Settings", "Proxies": "Proxies", "Proxy Groups": "Proxy Groups", + "Proxy Chain Mode": "Proxy Chain Mode", + "Connect": "Connect", + "Connecting...": "Connecting...", + "Disconnect": "Disconnect", + "Failed to connect to proxy chain": "Failed to connect to proxy chain", "Proxy Provider": "Proxy Provider", "Proxy Count": "Proxy Count", "Update All": "Update All", @@ -33,6 +38,15 @@ "rule": "rule", "global": "global", "direct": "direct", + "Chain Proxy": "🔗 Chain Proxy", + "Chain Proxy Config": "Chain Proxy Config", + "Proxy Rules": "Proxy Rules", + "Select Rules": "Select Rules", + "Click nodes in order to add to proxy chain": "Click nodes in order to add to proxy chain", + "No proxy chain configured": "No proxy chain configured", + "Proxy Order": "Proxy Order", + "timeout": "Timeout", + "Clear All": "Clear All", "script": "script", "locate": "locate", "Delay check": "Delay check", @@ -210,6 +224,8 @@ "Reset to Default": "Reset to Default", "Tun Mode Info": "Tun (Virtual NIC) mode: Captures all system traffic, when enabled, there is no need to enable system proxy.", "TUN requires Service Mode or Admin Mode": "TUN requires Service Mode or Admin Mode", + "TUN Mode automatically disabled due to service unavailable": "TUN Mode automatically disabled due to service unavailable", + "Failed to disable TUN Mode automatically": "Failed to disable TUN Mode automatically", "System Proxy Enabled": "System proxy is enabled, your applications will access the network through the proxy", "System Proxy Disabled": "System proxy is disabled, it is recommended for most users to turn on this option", "TUN Mode Enabled": "TUN mode is enabled, applications will access the network through the virtual network card", @@ -652,7 +668,7 @@ "Allowed Origins": "Allowed Origins", "Please enter a valid url": "Please enter a valid url", "Add": "Add", - "Development mode: Automatically includes Tauri and localhost origins": "Development mode: Automatically includes Tauri and localhost origins", + "Always included origins: {{urls}}": "Always included origins: {{urls}}", "Invalid regular expression": "Invalid regular expression", "Copy Version": "Copy Version", "Version copied to clipboard": "Version copied to clipboard", @@ -662,5 +678,7 @@ "Failed to save configuration": "Failed to save configuration", "Controller address copied to clipboard": "Controller address copied to clipboard", "Secret copied to clipboard": "Secret copied to clipboard", - "Saving...": "Saving..." + "Saving...": "Saving...", + "Proxy node already exists in chain": "Proxy node already exists in chain", + "Detection timeout or failed": "Detection timeout or failed" } diff --git a/clash-verge-rev/src/locales/es.json b/clash-verge-rev/src/locales/es.json index 5c7931c283..6ced735bf8 100644 --- a/clash-verge-rev/src/locales/es.json +++ b/clash-verge-rev/src/locales/es.json @@ -32,6 +32,7 @@ "rule": "Regla", "global": "Global", "direct": "Conexión directa", + "Chain Proxy": "🔗 Proxy en cadena", "script": "Script", "locate": "Nodo actual", "Delay check": "Prueba de latencia", diff --git a/clash-verge-rev/src/locales/fa.json b/clash-verge-rev/src/locales/fa.json index 1b12d56dd9..858bf57251 100644 --- a/clash-verge-rev/src/locales/fa.json +++ b/clash-verge-rev/src/locales/fa.json @@ -31,6 +31,7 @@ "rule": "قانون", "global": "جهانی", "direct": "مستقیم", + "Chain Proxy": "🔗 پراکسی زنجیره‌ای", "script": "اسکریپت", "locate": "موقعیت", "Delay check": "بررسی تأخیر", diff --git a/clash-verge-rev/src/locales/id.json b/clash-verge-rev/src/locales/id.json index 4aeb4fc805..9b9d91270e 100644 --- a/clash-verge-rev/src/locales/id.json +++ b/clash-verge-rev/src/locales/id.json @@ -63,6 +63,7 @@ "rule": "aturan", "global": "global", "direct": "langsung", + "Chain Proxy": "🔗 Proxy Rantai", "script": "skrip", "locate": "Lokasi", "Delay check": "Periksa Keterlambatan", diff --git a/clash-verge-rev/src/locales/jp.json b/clash-verge-rev/src/locales/jp.json index c9e4dd123f..92488c7045 100644 --- a/clash-verge-rev/src/locales/jp.json +++ b/clash-verge-rev/src/locales/jp.json @@ -32,6 +32,7 @@ "rule": "ルール", "global": "グローバル", "direct": "直接接続", + "Chain Proxy": "🔗 チェーンプロキシ", "script": "スクリプト", "locate": "現在のノード", "Delay check": "遅延テスト", diff --git a/clash-verge-rev/src/locales/ko.json b/clash-verge-rev/src/locales/ko.json index a93171f06f..ab0f98bf4e 100644 --- a/clash-verge-rev/src/locales/ko.json +++ b/clash-verge-rev/src/locales/ko.json @@ -33,6 +33,7 @@ "rule": "규칙", "global": "전역", "direct": "직접", + "Chain Proxy": "🔗 체인 프록시", "script": "스크립트", "locate": "로케이트", "Delay check": "지연 확인", @@ -126,7 +127,6 @@ "Lazy": "지연 로딩", "Timeout": "타임아웃", "Max Failed Times": "최대 실패 횟수", - "Interface Name": "인터페이스 이름", "Routing Mark": "라우팅 마크", "Include All": "모든 프록시 및 제공자 포함", "Include All Providers": "모든 제공자 포함", diff --git a/clash-verge-rev/src/locales/ru.json b/clash-verge-rev/src/locales/ru.json index 0bdeab4871..cbd6e6b6cb 100644 --- a/clash-verge-rev/src/locales/ru.json +++ b/clash-verge-rev/src/locales/ru.json @@ -32,6 +32,13 @@ "rule": "правила", "global": "глобальный", "direct": "прямой", + "Chain Proxy": "🔗 Цепной прокси", + "Chain Proxy Config": "Конфигурация цепочки прокси", + "Click nodes in order to add to proxy chain": "Нажимайте узлы по порядку, чтобы добавить в цепочку прокси", + "No proxy chain configured": "Цепочка прокси не настроена", + "Proxy Order": "Порядок прокси", + "timeout": "Тайм-аут", + "Clear All": "Очистить всё", "script": "скриптовый", "locate": "Местоположение", "Delay check": "Проверка задержки", diff --git a/clash-verge-rev/src/locales/tr.json b/clash-verge-rev/src/locales/tr.json index bc24c32d48..c4e2c8862d 100644 --- a/clash-verge-rev/src/locales/tr.json +++ b/clash-verge-rev/src/locales/tr.json @@ -33,6 +33,7 @@ "rule": "kural", "global": "küresel", "direct": "doğrudan", + "Chain Proxy": "🔗 Zincir Proxy", "script": "betik", "locate": "konum", "Delay check": "Gecikme kontrolü", diff --git a/clash-verge-rev/src/locales/tt.json b/clash-verge-rev/src/locales/tt.json index 2dbfa84a2d..088e124172 100644 --- a/clash-verge-rev/src/locales/tt.json +++ b/clash-verge-rev/src/locales/tt.json @@ -31,6 +31,7 @@ "rule": "кагыйдә", "global": "глобаль", "direct": "туры", + "Chain Proxy": "🔗 Чылбыр прокси", "script": "скриптлы", "locate": "Урын", "Delay check": "Задержканы тикшерү", diff --git a/clash-verge-rev/src/locales/zh.json b/clash-verge-rev/src/locales/zh.json index 5a86591065..9a3d174860 100644 --- a/clash-verge-rev/src/locales/zh.json +++ b/clash-verge-rev/src/locales/zh.json @@ -26,6 +26,11 @@ "Label-Settings": "设 置", "Proxies": "代理", "Proxy Groups": "代理组", + "Proxy Chain Mode": "链式代理模式", + "Connect": "连接", + "Connecting...": "连接中...", + "Disconnect": "断开", + "Failed to connect to proxy chain": "连接链式代理失败", "Proxy Provider": "代理集合", "Proxy Count": "节点数量", "Update All": "更新全部", @@ -33,6 +38,15 @@ "rule": "规则", "global": "全局", "direct": "直连", + "Chain Proxy": "🔗 链式代理", + "Chain Proxy Config": "代理链配置", + "Proxy Rules": "代理规则", + "Select Rules": "选择规则", + "Click nodes in order to add to proxy chain": "顺序点击节点添加到代理链中", + "No proxy chain configured": "暂无代理链配置", + "Proxy Order": "代理顺序", + "timeout": "超时", + "Clear All": "清除全部", "script": "脚本", "locate": "当前节点", "Delay check": "延迟测试", @@ -210,6 +224,8 @@ "Reset to Default": "重置为默认值", "Tun Mode Info": "TUN(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理", "TUN requires Service Mode or Admin Mode": "TUN 模式需要安装服务模式或管理员模式", + "TUN Mode automatically disabled due to service unavailable": "由于服务不可用,TUN 模式已自动关闭", + "Failed to disable TUN Mode automatically": "自动关闭 TUN 模式失败", "System Proxy Enabled": "系统代理已启用,您的应用将通过代理访问网络", "System Proxy Disabled": "系统代理已关闭,建议大多数用户打开此选项", "TUN Mode Enabled": "TUN 模式已启用,应用将通过虚拟网卡访问网络", @@ -652,7 +668,7 @@ "Allowed Origins": "允许的来源", "Please enter a valid url": "请输入有效的网址", "Add": "添加", - "Development mode: Automatically includes Tauri and localhost origins": "开发模式:自动包含 Tauri 和 localhost 来源", + "Always included origins: {{urls}}": "始终包含来源:{{urls}}", "Invalid regular expression": "无效的正则表达式", "Copy Version": "复制Verge版本号", "Version copied to clipboard": "Verge版本已复制到剪贴板", @@ -662,5 +678,7 @@ "Failed to save configuration": "配置保存失败", "Controller address copied to clipboard": "控制器地址已复制到剪贴板", "Secret copied to clipboard": "访问密钥已复制到剪贴板", - "Saving...": "保存中..." + "Saving...": "保存中...", + "Proxy node already exists in chain": "该节点已在链式代理表中", + "Detection timeout or failed": "检测超时或失败" } diff --git a/clash-verge-rev/src/locales/zhtw.json b/clash-verge-rev/src/locales/zhtw.json index 9933db0491..6521d8651c 100644 --- a/clash-verge-rev/src/locales/zhtw.json +++ b/clash-verge-rev/src/locales/zhtw.json @@ -25,6 +25,11 @@ "Label-Unlock": "測 試", "Label-Settings": "設 置", "Proxy Groups": "代理組", + "Proxy Chain Mode": "鏈式代理模式", + "Connect": "連接", + "Connecting...": "連接中...", + "Disconnect": "斷開", + "Failed to connect to proxy chain": "連接鏈式代理失敗", "Proxy Provider": "代理集合", "Proxy Count": "節點數量", "Update All": "更新全部", @@ -33,6 +38,7 @@ "global": "全局", "direct": "直連", "script": "腳本", + "Chain Proxy": "🔗 鏈式代理", "locate": "當前節點", "Delay check": "延遲測試", "Sort by default": "默認排序", diff --git a/clash-verge-rev/src/main.tsx b/clash-verge-rev/src/main.tsx index d1c3080641..0860aec417 100644 --- a/clash-verge-rev/src/main.tsx +++ b/clash-verge-rev/src/main.tsx @@ -7,19 +7,20 @@ if (!window.ResizeObserver) { window.ResizeObserver = ResizeObserver; } +import { ComposeContextProvider } from "foxact/compose-context-provider"; import React from "react"; import { createRoot } from "react-dom/client"; -import { ComposeContextProvider } from "foxact/compose-context-provider"; import { BrowserRouter } from "react-router-dom"; + import { BaseErrorBoundary } from "./components/base"; import Layout from "./pages/_layout"; -import "./services/i18n"; +import { AppDataProvider } from "./providers/app-data-provider"; +import { initializeLanguage } from "./services/i18n"; import { LoadingCacheProvider, ThemeModeProvider, UpdateStateProvider, } from "./services/states"; -import { AppDataProvider } from "./providers/app-data-provider"; const mainElementId = "root"; const container = document.getElementById(mainElementId); @@ -39,29 +40,47 @@ document.addEventListener("keydown", (event) => { ["F", "G", "H", "J", "P", "Q", "R", "U"].includes( event.key.toUpperCase(), )); - disabledShortcuts && event.preventDefault(); + if (disabledShortcuts) { + event.preventDefault(); + } }); -const contexts = [ - , - , - , -]; +const initializeApp = async () => { + try { + await initializeLanguage("zh"); -const root = createRoot(container); -root.render( - - - - - - - - - - - , -); + const contexts = [ + , + , + , + ]; + + const root = createRoot(container); + root.render( + + + + + + + + + + + , + ); + } catch (error) { + console.error("[main.tsx] 应用初始化失败:", error); + const root = createRoot(container); + root.render( +
+ 应用初始化失败: {error instanceof Error ? error.message : String(error)} +
, + ); + } +}; + +initializeApp(); // 错误处理 window.addEventListener("error", (event) => { diff --git a/clash-verge-rev/src/pages/_layout.tsx b/clash-verge-rev/src/pages/_layout.tsx index aca5f26b95..41cc527323 100644 --- a/clash-verge-rev/src/pages/_layout.tsx +++ b/clash-verge-rev/src/pages/_layout.tsx @@ -1,38 +1,48 @@ -import dayjs from "dayjs"; -import i18next from "i18next"; -import relativeTime from "dayjs/plugin/relativeTime"; -import { SWRConfig, mutate } from "swr"; -import { useEffect, useCallback, useState, useRef } from "react"; -import { useTranslation } from "react-i18next"; -import { useLocation, useRoutes, useNavigate } from "react-router-dom"; import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { routers } from "./_routers"; -import { getAxios } from "@/services/api"; -import { forceRefreshClashConfig } from "@/services/cmds"; -import { useVerge } from "@/hooks/use-verge"; -import LogoSvg from "@/assets/image/logo.svg?react"; -import iconLight from "@/assets/image/icon_light.svg?react"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { useLocalStorage } from "foxact/use-local-storage"; +import { useEffect, useCallback, useState, useRef } from "react"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation, useRoutes, useNavigate } from "react-router-dom"; +import { SWRConfig, mutate } from "swr"; + import iconDark from "@/assets/image/icon_dark.svg?react"; -import { useThemeMode, useEnableLog } from "@/services/states"; +import iconLight from "@/assets/image/icon_light.svg?react"; +import LogoSvg from "@/assets/image/logo.svg?react"; import { LayoutItem } from "@/components/layout/layout-item"; import { LayoutTraffic } from "@/components/layout/layout-traffic"; import { UpdateButton } from "@/components/layout/update-button"; import { useCustomTheme } from "@/components/layout/use-custom-theme"; +import { useI18n } from "@/hooks/use-i18n"; +import { useVerge } from "@/hooks/use-verge"; +import { getAxios } from "@/services/api"; +import { forceRefreshClashConfig } from "@/services/cmds"; +import { useThemeMode, useEnableLog } from "@/services/states"; import getSystem from "@/utils/get-system"; + +import { routers } from "./_routers"; + import "dayjs/locale/ru"; import "dayjs/locale/zh-cn"; -import React from "react"; + import { useListen } from "@/hooks/use-listen"; + import { listen } from "@tauri-apps/api/event"; + import { useClashInfo } from "@/hooks/use-clash"; import { initGlobalLogService } from "@/services/global-log-service"; + import { invoke } from "@tauri-apps/api/core"; + import { showNotice } from "@/services/noticeService"; import { NoticeManager } from "@/components/base/NoticeManager"; +import { LogLevel } from "@/hooks/use-log-data"; const appWindow = getCurrentWebviewWindow(); -export let portableFlag = false; +export const portableFlag = false; dayjs.extend(relativeTime); @@ -154,11 +164,13 @@ const Layout = () => { const { verge } = useVerge(); const { clashInfo } = useClashInfo(); const [enableLog] = useEnableLog(); + const [logLevel] = useLocalStorage("log:log-level", "info"); const { language, start_page } = verge ?? {}; + const { switchLanguage } = useI18n(); const navigate = useNavigate(); const location = useLocation(); const routersEles = useRoutes(routers); - const { addListener, setupCloseListener } = useListen(); + const { addListener } = useListen(); const initRef = useRef(false); const [themeReady, setThemeReady] = useState(false); @@ -183,10 +195,9 @@ const Layout = () => { // 初始化全局日志服务 useEffect(() => { if (clashInfo) { - const { server = "", secret = "" } = clashInfo; - initGlobalLogService(server, secret, enableLog, "info"); + initGlobalLogService(enableLog, logLevel); } - }, [clashInfo, enableLog]); + }, [clashInfo, enableLog, logLevel]); // 设置监听器 useEffect(() => { @@ -205,6 +216,9 @@ const Layout = () => { mutate("getVergeConfig"); mutate("getSystemProxy"); mutate("getAutotemProxy"); + // 运行模式变更时也需要刷新相关状态 + mutate("getRunningMode"); + mutate("isServiceAvailable"); }), addListener("verge://notice-message", ({ payload }) => @@ -224,7 +238,6 @@ const Layout = () => { }; }; - setupCloseListener(); const cleanupWindow = setupWindowListeners(); return () => { @@ -294,7 +307,7 @@ const Layout = () => { setTimeout(() => { try { initialOverlay.remove(); - } catch (e) { + } catch { console.log("[Layout] 加载指示器已被移除"); } }, 300); @@ -376,14 +389,6 @@ const Layout = () => { const setupEventListener = async () => { try { console.log("[Layout] 开始监听启动完成事件"); - const unlisten = await listen("verge://startup-completed", () => { - if (!hasEventTriggered) { - console.log("[Layout] 收到启动完成事件,开始初始化"); - hasEventTriggered = true; - performInitialization(); - } - }); - return unlisten; } catch (err) { console.error("[Layout] 监听启动完成事件失败:", err); return () => {}; @@ -400,7 +405,7 @@ const Layout = () => { hasEventTriggered = true; performInitialization(); } - } catch (err) { + } catch { console.log("[Layout] 后端尚未就绪,等待启动完成事件"); } }; @@ -422,14 +427,11 @@ const Layout = () => { } }, 5000); - const unlistenPromise = setupEventListener(); - setTimeout(checkImmediateInitialization, 100); return () => { clearTimeout(backupInitialization); clearTimeout(emergencyInitialization); - unlistenPromise.then((unlisten) => unlisten()); }; }, []); @@ -437,9 +439,9 @@ const Layout = () => { useEffect(() => { if (language) { dayjs.locale(language === "zh" ? "zh-cn" : language); - i18next.changeLanguage(language); + switchLanguage(language); } - }, [language]); + }, [language, switchLanguage]); useEffect(() => { if (start_page) { diff --git a/clash-verge-rev/src/pages/_routers.tsx b/clash-verge-rev/src/pages/_routers.tsx index d917ae2878..d3ccfe9de2 100644 --- a/clash-verge-rev/src/pages/_routers.tsx +++ b/clash-verge-rev/src/pages/_routers.tsx @@ -1,78 +1,78 @@ -import LogsPage from "./logs"; -import ProxiesPage from "./proxies"; -import ProfilesPage from "./profiles"; -import SettingsPage from "./settings"; -import ConnectionsPage from "./connections"; -import RulesPage from "./rules"; -import HomePage from "./home"; -import UnlockPage from "./unlock"; +import DnsRoundedIcon from "@mui/icons-material/DnsRounded"; +import ForkRightRoundedIcon from "@mui/icons-material/ForkRightRounded"; +import HomeRoundedIcon from "@mui/icons-material/HomeRounded"; +import LanguageRoundedIcon from "@mui/icons-material/LanguageRounded"; +import LockOpenRoundedIcon from "@mui/icons-material/LockOpenRounded"; +import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded"; +import SubjectRoundedIcon from "@mui/icons-material/SubjectRounded"; +import WifiRoundedIcon from "@mui/icons-material/WifiRounded"; + +import ConnectionsSvg from "@/assets/image/itemicon/connections.svg?react"; +import HomeSvg from "@/assets/image/itemicon/home.svg?react"; +import LogsSvg from "@/assets/image/itemicon/logs.svg?react"; +import ProfilesSvg from "@/assets/image/itemicon/profiles.svg?react"; +import ProxiesSvg from "@/assets/image/itemicon/proxies.svg?react"; +import RulesSvg from "@/assets/image/itemicon/rules.svg?react"; +import SettingsSvg from "@/assets/image/itemicon/settings.svg?react"; +import UnlockSvg from "@/assets/image/itemicon/unlock.svg?react"; import { BaseErrorBoundary } from "@/components/base"; -import HomeSvg from "@/assets/image/itemicon/home.svg?react"; -import ProxiesSvg from "@/assets/image/itemicon/proxies.svg?react"; -import ProfilesSvg from "@/assets/image/itemicon/profiles.svg?react"; -import ConnectionsSvg from "@/assets/image/itemicon/connections.svg?react"; -import RulesSvg from "@/assets/image/itemicon/rules.svg?react"; -import LogsSvg from "@/assets/image/itemicon/logs.svg?react"; -import UnlockSvg from "@/assets/image/itemicon/unlock.svg?react"; -import SettingsSvg from "@/assets/image/itemicon/settings.svg?react"; - -import WifiRoundedIcon from "@mui/icons-material/WifiRounded"; -import DnsRoundedIcon from "@mui/icons-material/DnsRounded"; -import LanguageRoundedIcon from "@mui/icons-material/LanguageRounded"; -import ForkRightRoundedIcon from "@mui/icons-material/ForkRightRounded"; -import SubjectRoundedIcon from "@mui/icons-material/SubjectRounded"; -import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded"; -import HomeRoundedIcon from "@mui/icons-material/HomeRounded"; -import LockOpenRoundedIcon from "@mui/icons-material/LockOpenRounded"; +import ConnectionsPage from "./connections"; +import HomePage from "./home"; +import LogsPage from "./logs"; +import ProfilesPage from "./profiles"; +import ProxiesPage from "./proxies"; +import RulesPage from "./rules"; +import SettingsPage from "./settings"; +import UnlockPage from "./unlock"; export const routers = [ { label: "Label-Home", path: "/home", - icon: [, ], + icon: [, ], element: , }, { label: "Label-Proxies", path: "/", - icon: [, ], + icon: [, ], element: , }, { label: "Label-Profiles", path: "/profile", - icon: [, ], + icon: [, ], element: , }, { label: "Label-Connections", path: "/connections", - icon: [, ], + icon: [, ], element: , }, { label: "Label-Rules", path: "/rules", - icon: [, ], + icon: [, ], element: , }, { label: "Label-Logs", path: "/logs", - icon: [, ], + icon: [, ], element: , }, { label: "Label-Unlock", path: "/unlock", - icon: [, ], + icon: [, ], element: , }, { label: "Label-Settings", path: "/settings", - icon: [, ], + icon: [, ], element: , }, ].map((router) => ({ diff --git a/clash-verge-rev/src/pages/connections.tsx b/clash-verge-rev/src/pages/connections.tsx index 4a9bc917ad..e2416d9d29 100644 --- a/clash-verge-rev/src/pages/connections.tsx +++ b/clash-verge-rev/src/pages/connections.tsx @@ -1,29 +1,29 @@ -import { useMemo, useRef, useState, useCallback } from "react"; -import { useLockFn } from "ahooks"; -import { Box, Button, IconButton, MenuItem } from "@mui/material"; -import { Virtuoso } from "react-virtuoso"; -import { useTranslation } from "react-i18next"; import { + PauseCircleOutlineRounded, + PlayCircleOutlineRounded, TableChartRounded, TableRowsRounded, - PlayCircleOutlineRounded, - PauseCircleOutlineRounded, } from "@mui/icons-material"; -import { closeAllConnections } from "@/services/cmds"; -import { useConnectionSetting } from "@/services/states"; +import { Box, Button, IconButton, MenuItem } from "@mui/material"; +import { useLockFn } from "ahooks"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Virtuoso } from "react-virtuoso"; + import { BaseEmpty, BasePage } from "@/components/base"; -import { ConnectionItem } from "@/components/connection/connection-item"; -import { ConnectionTable } from "@/components/connection/connection-table"; +import { BaseSearchBox } from "@/components/base/base-search-box"; +import { BaseStyledSelect } from "@/components/base/base-styled-select"; import { ConnectionDetail, ConnectionDetailRef, } from "@/components/connection/connection-detail"; -import parseTraffic from "@/utils/parse-traffic"; -import { BaseSearchBox } from "@/components/base/base-search-box"; -import { BaseStyledSelect } from "@/components/base/base-styled-select"; -import { useTheme } from "@mui/material/styles"; +import { ConnectionItem } from "@/components/connection/connection-item"; +import { ConnectionTable } from "@/components/connection/connection-table"; import { useVisibility } from "@/hooks/use-visibility"; -import { useAppData } from "@/providers/app-data-provider"; +import { useAppData } from "@/providers/app-data-context"; +import { closeAllConnections } from "@/services/cmds"; +import { useConnectionSetting } from "@/services/states"; +import parseTraffic from "@/utils/parse-traffic"; const initConn: IConnections = { uploadTotal: 0, @@ -36,8 +36,6 @@ type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; const ConnectionsPage = () => { const { t } = useTranslation(); const pageVisible = useVisibility(); - const theme = useTheme(); - const isDark = theme.palette.mode === "dark"; const [match, setMatch] = useState(() => (_: string) => true); const [curOrderOpt, setOrderOpt] = useState("Default"); @@ -48,17 +46,21 @@ const ConnectionsPage = () => { const isTableLayout = setting.layout === "table"; - const orderOpts: Record = { - Default: (list) => - list.sort( - (a, b) => - new Date(b.start || "0").getTime()! - - new Date(a.start || "0").getTime()!, - ), - "Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!), - "Download Speed": (list) => - list.sort((a, b) => b.curDownload! - a.curDownload!), - }; + const orderOpts = useMemo>( + () => ({ + Default: (list) => + list.sort( + (a, b) => + new Date(b.start || "0").getTime()! - + new Date(a.start || "0").getTime()!, + ), + "Upload Speed": (list) => + list.sort((a, b) => b.curUpload! - a.curUpload!), + "Download Speed": (list) => + list.sort((a, b) => b.curDownload! - a.curDownload!), + }), + [], + ); const [isPaused, setIsPaused] = useState(false); const [frozenData, setFrozenData] = useState(null); @@ -96,7 +98,7 @@ const ConnectionsPage = () => { if (orderFunc) conns = orderFunc(conns); return [conns]; - }, [displayData, match, curOrderOpt]); + }, [displayData, match, curOrderOpt, orderOpts]); const onCloseAll = useLockFn(closeAllConnections); diff --git a/clash-verge-rev/src/pages/home.tsx b/clash-verge-rev/src/pages/home.tsx index e28151b217..6632903def 100644 --- a/clash-verge-rev/src/pages/home.tsx +++ b/clash-verge-rev/src/pages/home.tsx @@ -1,52 +1,61 @@ -import { useTranslation } from "react-i18next"; +import { + DnsOutlined, + HelpOutlineRounded, + HistoryEduOutlined, + RouterOutlined, + SettingsOutlined, + SpeedOutlined, +} from "@mui/icons-material"; import { Box, Button, - IconButton, - useTheme, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - FormGroup, - FormControlLabel, Checkbox, - Tooltip, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + FormGroup, Grid, + IconButton, + Skeleton, + Tooltip, } from "@mui/material"; -import { useVerge } from "@/hooks/use-verge"; -import { useProfiles } from "@/hooks/use-profiles"; -import { - RouterOutlined, - SettingsOutlined, - DnsOutlined, - SpeedOutlined, - HelpOutlineRounded, - HistoryEduOutlined, -} from "@mui/icons-material"; -import { useNavigate } from "react-router-dom"; -import { ProxyTunCard } from "@/components/home/proxy-tun-card"; -import { ClashModeCard } from "@/components/home/clash-mode-card"; -import { EnhancedTrafficStats } from "@/components/home/enhanced-traffic-stats"; -import { useState, useEffect } from "react"; -import { HomeProfileCard } from "@/components/home/home-profile-card"; -import { EnhancedCard } from "@/components/home/enhanced-card"; -import { CurrentProxyCard } from "@/components/home/current-proxy-card"; -import { BasePage } from "@/components/base"; -import { ClashInfoCard } from "@/components/home/clash-info-card"; -import { SystemInfoCard } from "@/components/home/system-info-card"; import { useLockFn } from "ahooks"; +import { Suspense, lazy, useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { BasePage } from "@/components/base"; +import { ClashModeCard } from "@/components/home/clash-mode-card"; +import { CurrentProxyCard } from "@/components/home/current-proxy-card"; +import { EnhancedCard } from "@/components/home/enhanced-card"; +import { EnhancedTrafficStats } from "@/components/home/enhanced-traffic-stats"; +import { HomeProfileCard } from "@/components/home/home-profile-card"; +import { ProxyTunCard } from "@/components/home/proxy-tun-card"; +import { useProfiles } from "@/hooks/use-profiles"; +import { useVerge } from "@/hooks/use-verge"; import { entry_lightweight_mode, openWebUrl } from "@/services/cmds"; -import { TestCard } from "@/components/home/test-card"; -import { IpInfoCard } from "@/components/home/ip-info-card"; -import { - DragDropContext, - Droppable, - Draggable, - DropResult, - DroppableProvided, - DraggableProvided, -} from "react-beautiful-dnd"; + +const LazyTestCard = lazy(() => + import("@/components/home/test-card").then((module) => ({ + default: module.TestCard, + })), +); +const LazyIpInfoCard = lazy(() => + import("@/components/home/ip-info-card").then((module) => ({ + default: module.IpInfoCard, + })), +); +const LazyClashInfoCard = lazy(() => + import("@/components/home/clash-info-card").then((module) => ({ + default: module.ClashInfoCard, + })), +); +const LazySystemInfoCard = lazy(() => + import("@/components/home/system-info-card").then((module) => ({ + default: module.SystemInfoCard, + })), +); // 定义首页卡片设置接口 interface HomeCardsSettings { @@ -63,13 +72,6 @@ interface HomeCardsSettings { [key: string]: boolean; } -// 卡片配置接口,包含排序信息 -interface CardConfig { - id: string; - size: number; - enabled: boolean; -} - // 首页设置对话框组件接口 interface HomeSettingsDialogProps { open: boolean; @@ -78,35 +80,6 @@ interface HomeSettingsDialogProps { onSave: (cards: HomeCardsSettings) => void; } -// 确保对象符合HomeCardsSettings类型的辅助函数 -const ensureHomeCardsSettings = (obj: any): HomeCardsSettings => { - const defaultSettings: HomeCardsSettings = { - profile: true, - proxy: true, - network: true, - mode: true, - traffic: true, - info: false, - clashinfo: true, - systeminfo: true, - test: true, - ip: true, - }; - - if (!obj || typeof obj !== "object") return defaultSettings; - - // 合并默认值和传入对象,确保所有必要属性都存在 - return Object.keys(defaultSettings).reduce((acc, key) => { - return { - ...acc, - [key]: - typeof obj[key] === "boolean" - ? obj[key] - : defaultSettings[key as keyof HomeCardsSettings], - }; - }, {} as HomeCardsSettings); -}; - // 首页设置对话框组件 const HomeSettingsDialog = ({ open, @@ -118,16 +91,15 @@ const HomeSettingsDialog = ({ const [cards, setCards] = useState(homeCards); const { patchVerge } = useVerge(); - const handleToggle = (key: keyof HomeCardsSettings) => { - setCards((prev) => ({ + const handleToggle = (key: string) => { + setCards((prev: HomeCardsSettings) => ({ ...prev, [key]: !prev[key], })); }; const handleSave = async () => { - // 明确类型为HomeCardsSettings - await patchVerge({ home_cards: cards as HomeCardsSettings }); + await patchVerge({ home_cards: cards }); onSave(cards); onClose(); }; @@ -140,7 +112,7 @@ const HomeSettingsDialog = ({ handleToggle("profile")} /> } @@ -149,7 +121,7 @@ const HomeSettingsDialog = ({ handleToggle("proxy")} /> } @@ -158,7 +130,7 @@ const HomeSettingsDialog = ({ handleToggle("network")} /> } @@ -167,7 +139,7 @@ const HomeSettingsDialog = ({ handleToggle("mode")} /> } @@ -176,7 +148,7 @@ const HomeSettingsDialog = ({ handleToggle("traffic")} /> } @@ -185,7 +157,7 @@ const HomeSettingsDialog = ({ handleToggle("test")} /> } @@ -194,7 +166,7 @@ const HomeSettingsDialog = ({ handleToggle("ip")} /> } @@ -203,7 +175,7 @@ const HomeSettingsDialog = ({ handleToggle("clashinfo")} /> } @@ -212,7 +184,7 @@ const HomeSettingsDialog = ({ handleToggle("systeminfo")} /> } @@ -230,89 +202,72 @@ const HomeSettingsDialog = ({ ); }; -export const HomePage = () => { +const HomePage = () => { const { t } = useTranslation(); - const { verge, patchVerge } = useVerge(); + const { verge } = useVerge(); const { current, mutateProfiles } = useProfiles(); - const theme = useTheme(); // 设置弹窗的状态 const [settingsOpen, setSettingsOpen] = useState(false); - // 卡片显示状态 - 确保类型正确 - const [homeCards, setHomeCards] = useState( - ensureHomeCardsSettings(verge?.home_cards), + + // 卡片显示状态 + const defaultCards = useMemo( + () => ({ + info: false, + profile: true, + proxy: true, + network: true, + mode: true, + traffic: true, + clashinfo: true, + systeminfo: true, + test: true, + ip: true, + }), + [], ); - // 卡片排序配置 - 默认为初始顺序 - const [cardOrder, setCardOrder] = useState( - // 明确断言类型 - (verge?.card_order as string[]) || [ - "profile", - "proxy", - "network", - "mode", - "traffic", - "test", - "ip", - "clashinfo", - "systeminfo", - ], - ); - - // 当homeCards变化时,确保cardOrder中只包含启用的卡片 - useEffect(() => { - const enabledCards = Object.entries(homeCards) - .filter(([_, enabled]) => enabled) - .map(([id]) => id); - - // 过滤掉已禁用的卡片 - const filteredOrder = cardOrder.filter((id) => enabledCards.includes(id)); - - // 添加新启用但不在排序中的卡片 - const newCards = enabledCards.filter((id) => !filteredOrder.includes(id)); - - setCardOrder([...filteredOrder, ...newCards]); - }, [homeCards]); - - // 保存卡片排序 - const saveCardOrder = useLockFn(async (order: string[]) => { - await patchVerge({ card_order: order } as any); - setCardOrder(order); + const [homeCards, setHomeCards] = useState(() => { + return (verge?.home_cards as HomeCardsSettings) || defaultCards; }); - // 处理拖拽结束 - const handleDragEnd = (result: DropResult) => { - const { destination, source, draggableId } = result; - - // 拖拽到无效位置或原位置,不做处理 - if ( - !destination || - (destination.droppableId === source.droppableId && - destination.index === source.index) - ) { - return; - } - - // 重新排序 - const newOrder = Array.from(cardOrder); - newOrder.splice(source.index, 1); - newOrder.splice(destination.index, 0, draggableId); - - // 保存新顺序 - saveCardOrder(newOrder); - }; - // 文档链接函数 const toGithubDoc = useLockFn(() => { return openWebUrl("https://clash-verge-rev.github.io/index.html"); }); - // 卡片设置弹窗 - const openSettings = () => { + // 新增:打开设置弹窗 + const openSettings = useCallback(() => { setSettingsOpen(true); - }; + }, []); - // 保存勾选设置 + const renderCard = useCallback( + (cardKey: string, component: React.ReactNode, size: number = 6) => { + if (!homeCards[cardKey]) return null; + + return ( + + {component} + + ); + }, + [homeCards], + ); + + const criticalCards = useMemo( + () => [ + renderCard( + "profile", + , + ), + renderCard("proxy", ), + renderCard("network", ), + renderCard("mode", ), + ], + [current, mutateProfiles, renderCard], + ); + + // 新增:保存设置时用requestIdleCallback/setTimeout const handleSaveSettings = (newCards: HomeCardsSettings) => { if (window.requestIdleCallback) { window.requestIdleCallback(() => setHomeCards(newCards)); @@ -321,67 +276,46 @@ export const HomePage = () => { } }; - // 获取卡片配置信息 - const getCardConfig = (id: string): CardConfig => { - const configs: Record = { - profile: { id: "profile", size: 6, enabled: homeCards.profile }, - proxy: { id: "proxy", size: 6, enabled: homeCards.proxy }, - network: { id: "network", size: 6, enabled: homeCards.network }, - mode: { id: "mode", size: 6, enabled: homeCards.mode }, - traffic: { id: "traffic", size: 12, enabled: homeCards.traffic }, - test: { id: "test", size: 6, enabled: homeCards.test }, - ip: { id: "ip", size: 6, enabled: homeCards.ip }, - clashinfo: { id: "clashinfo", size: 6, enabled: homeCards.clashinfo }, - systeminfo: { id: "systeminfo", size: 6, enabled: homeCards.systeminfo }, - }; - - if (!configs[id]) { - console.warn(`检测到未知卡片ID: ${id},使用默认配置`); - return { id, size: 6, enabled: false }; - } - - return configs[id]; - }; - - // 渲染卡片内容 - const renderCardContent = (id: string) => { - switch (id) { - case "profile": - return ( - - ); - case "proxy": - return ; - case "network": - return ; - case "mode": - return ; - case "traffic": - return ( - } - iconColor="secondary" - > - - - ); - case "test": - return ; - case "ip": - return ; - case "clashinfo": - return ; - case "systeminfo": - return ; - default: - console.warn(`无法渲染未知卡片: ${id}`); - return null; - } - }; + const nonCriticalCards = useMemo( + () => [ + renderCard( + "traffic", + } + iconColor="secondary" + > + + , + 12, + ), + renderCard( + "test", + }> + + , + ), + renderCard( + "ip", + }> + + , + ), + renderCard( + "clashinfo", + }> + + , + ), + renderCard( + "systeminfo", + }> + + , + ), + ], + [t, renderCard], + ); return ( {
} > - {/* 拖拽上下文 */} - - - {(provided: DroppableProvided) => ( - - {cardOrder - .filter((id) => { - const config = getCardConfig(id); - return homeCards[id] && config.enabled; - }) - .map((id, index) => { - const config = getCardConfig(id); - if (!config) return null; + + {criticalCards} - return ( - - {(provided: DraggableProvided) => ( - - {renderCardContent(id)} - - )} - - ); - })} - {provided.placeholder} - - )} - - + {nonCriticalCards} + {/* 首页设置弹窗 */} { const { t } = useTranslation(); const [enableLog, setEnableLog] = useEnableLog(); - const { clashInfo } = useClashInfo(); - const theme = useTheme(); - const isDark = theme.palette.mode === "dark"; const [logLevel, setLogLevel] = useLocalStorage( "log:log-level", "info", @@ -52,37 +40,27 @@ const LogPage = () => { return []; } - const allowedTypes = LOG_LEVEL_HIERARCHY[logLevel] || []; - + // Server-side filtering handles level filtering via query parameters + // We only need to apply search filtering here return logData.filter((data) => { - const logType = data.type?.toLowerCase() || ""; - const isAllowedType = - logLevel === "all" || allowedTypes.includes(logType); - // 构建完整的搜索文本,包含时间、类型和内容 const searchText = `${data.time || ""} ${data.type} ${data.payload}`.toLowerCase(); const matchesSearch = match(searchText); - return isAllowedType && matchesSearch; + return matchesSearch; }); - }, [logData, logLevel, match]); + }, [logData, match]); const handleLogLevelChange = (newLevel: LogLevel) => { setLogLevel(newLevel); - if (clashInfo) { - const { server = "", secret = "" } = clashInfo; - changeLogLevel(newLevel, server, secret); - } + changeLogLevel(newLevel); }; - const handleToggleLog = () => { - if (clashInfo) { - const { server = "", secret = "" } = clashInfo; - toggleLogEnabled(server, secret); - setEnableLog(!enableLog); - } + const handleToggleLog = async () => { + await toggleLogEnabled(); + setEnableLog(!enableLog); }; return ( @@ -110,17 +88,15 @@ const LogPage = () => { )}
- {enableLog === true && ( - - )} + } > @@ -139,10 +115,10 @@ const LogPage = () => { onChange={(e) => handleLogLevelChange(e.target.value as LogLevel)} > ALL + DEBUG INFO WARNING ERROR - DEBUG { diff --git a/clash-verge-rev/src/pages/profiles.tsx b/clash-verge-rev/src/pages/profiles.tsx index 3c90976ee2..bd2cdd5acf 100644 --- a/clash-verge-rev/src/pages/profiles.tsx +++ b/clash-verge-rev/src/pages/profiles.tsx @@ -1,21 +1,17 @@ -import useSWR from "swr"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { useLockFn } from "ahooks"; -import { Box, Button, IconButton, Stack, Divider, Grid } from "@mui/material"; import { - DndContext, closestCenter, + DndContext, + DragEndEvent, + DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors, - DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, sortableKeyboardCoordinates, } from "@dnd-kit/sortable"; -import { LoadingButton } from "@mui/lab"; import { ClearRounded, ContentPasteRounded, @@ -23,37 +19,43 @@ import { RefreshRounded, TextSnippetOutlined, } from "@mui/icons-material"; +import { LoadingButton } from "@mui/lab"; +import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material"; +import { listen, TauriEvent } from "@tauri-apps/api/event"; +import { readText } from "@tauri-apps/plugin-clipboard-manager"; +import { readTextFile } from "@tauri-apps/plugin-fs"; +import { useLockFn } from "ahooks"; +import { throttle } from "lodash-es"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { - importProfile, - enhanceProfiles, - //restartCore, - getRuntimeLogs, - deleteProfile, - updateProfile, - reorderProfile, - createProfile, -} from "@/services/cmds"; -import { useSetLoadingCache, useThemeMode } from "@/services/states"; -import { closeAllConnections } from "@/services/cmds"; +import { useLocation } from "react-router-dom"; +import useSWR, { mutate } from "swr"; + import { BasePage, DialogRef } from "@/components/base"; +import { BaseStyledTextField } from "@/components/base/base-styled-text-field"; +import { ProfileItem } from "@/components/profile/profile-item"; +import { ProfileMore } from "@/components/profile/profile-more"; import { ProfileViewer, ProfileViewerRef, } from "@/components/profile/profile-viewer"; -import { ProfileMore } from "@/components/profile/profile-more"; -import { ProfileItem } from "@/components/profile/profile-item"; -import { useProfiles } from "@/hooks/use-profiles"; import { ConfigViewer } from "@/components/setting/mods/config-viewer"; -import { throttle } from "lodash-es"; -import { BaseStyledTextField } from "@/components/base/base-styled-text-field"; -import { readTextFile } from "@tauri-apps/plugin-fs"; -import { readText } from "@tauri-apps/plugin-clipboard-manager"; -import { useLocation } from "react-router-dom"; import { useListen } from "@/hooks/use-listen"; -import { listen } from "@tauri-apps/api/event"; -import { TauriEvent } from "@tauri-apps/api/event"; +import { useProfiles } from "@/hooks/use-profiles"; +import { + closeAllConnections, + createProfile, + deleteProfile, + enhanceProfiles, + getProfiles, + //restartCore, + getRuntimeLogs, + importProfile, + reorderProfile, + updateProfile, +} from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; +import { useSetLoadingCache, useThemeMode } from "@/services/states"; // 记录profile切换状态 const debugProfileSwitch = (action: string, profile: string, extra?: any) => { @@ -115,41 +117,44 @@ const ProfilePage = () => { const pendingRequestRef = useRef | null>(null); // 处理profile切换中断 - const handleProfileInterrupt = ( - previousSwitching: string, - newProfile: string, - ) => { - debugProfileSwitch( - "INTERRUPT_PREVIOUS", - previousSwitching, - `被 ${newProfile} 中断`, - ); + const handleProfileInterrupt = useCallback( + (previousSwitching: string, newProfile: string) => { + debugProfileSwitch( + "INTERRUPT_PREVIOUS", + previousSwitching, + `被 ${newProfile} 中断`, + ); - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - debugProfileSwitch("ABORT_CONTROLLER_TRIGGERED", previousSwitching); - } + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + debugProfileSwitch("ABORT_CONTROLLER_TRIGGERED", previousSwitching); + } - if (pendingRequestRef.current) { - debugProfileSwitch("CANCEL_PENDING_REQUEST", previousSwitching); - } + if (pendingRequestRef.current) { + debugProfileSwitch("CANCEL_PENDING_REQUEST", previousSwitching); + } - setActivatings((prev) => prev.filter((id) => id !== previousSwitching)); - showNotice( - "info", - `${t("Profile switch interrupted by new selection")}: ${previousSwitching} → ${newProfile}`, - 3000, - ); - }; + setActivatings((prev) => prev.filter((id) => id !== previousSwitching)); + showNotice( + "info", + `${t("Profile switch interrupted by new selection")}: ${previousSwitching} → ${newProfile}`, + 3000, + ); + }, + [t], + ); // 清理切换状态 - const cleanupSwitchState = (profile: string, sequence: number) => { - setActivatings((prev) => prev.filter((id) => id !== profile)); - switchingProfileRef.current = null; - abortControllerRef.current = null; - pendingRequestRef.current = null; - debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`); - }; + const cleanupSwitchState = useCallback( + (profile: string, sequence: number) => { + setActivatings((prev) => prev.filter((id) => id !== profile)); + switchingProfileRef.current = null; + abortControllerRef.current = null; + pendingRequestRef.current = null; + debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`); + }, + [], + ); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -158,6 +163,15 @@ const ProfilePage = () => { ); const { current } = location.state || {}; + const { + profiles = {}, + activateSelected, + patchProfiles, + mutateProfiles, + error, + isStale, + } = useProfiles(); + useEffect(() => { const handleFileDrop = async () => { const unlisten = await addListener( @@ -165,7 +179,7 @@ const ProfilePage = () => { async (event: any) => { const paths = event.payload.paths; - for (let file of paths) { + for (const file of paths) { if (!file.endsWith(".yaml") && !file.endsWith(".yml")) { showNotice("error", t("Only YAML Files Supported")); continue; @@ -180,7 +194,7 @@ const ProfilePage = () => { self_proxy: false, }, } as IProfileItem; - let data = await readTextFile(file); + const data = await readTextFile(file); await createProfile(item, data); await mutateProfiles(); } @@ -195,14 +209,32 @@ const ProfilePage = () => { return () => { unsubscribe.then((cleanup) => cleanup()); }; - }, []); + }, [addListener, mutateProfiles, t]); - const { - profiles = {}, - activateSelected, - patchProfiles, - mutateProfiles, - } = useProfiles(); + // 添加紧急恢复功能 + const onEmergencyRefresh = useLockFn(async () => { + console.log("[紧急刷新] 开始强制刷新所有数据"); + + try { + // 清除所有SWR缓存 + await mutate(() => true, undefined, { revalidate: false }); + + // 强制重新获取配置数据 + await mutateProfiles(undefined, { + revalidate: true, + rollbackOnError: false, + }); + + // 等待状态稳定后增强配置 + await new Promise((resolve) => setTimeout(resolve, 500)); + await onEnhance(false); + + showNotice("success", "数据已强制刷新", 2000); + } catch (error: any) { + console.error("[紧急刷新] 失败:", error); + showNotice("error", `紧急刷新失败: ${error.message}`, 4000); + } + }); const { data: chainLogs = {}, mutate: mutateLogs } = useSWR( "getRuntimeLogs", @@ -233,16 +265,20 @@ const ProfilePage = () => { return; } setLoading(true); + + // 保存导入前的配置状态用于故障恢复 + const preImportProfilesCount = profiles?.items?.length || 0; + try { // 尝试正常导入 await importProfile(url); showNotice("success", t("Profile Imported Successfully")); setUrl(""); - mutateProfiles(); - await onEnhance(false); - } catch (err: any) { + + // 增强的刷新策略 + await performRobustRefresh(preImportProfilesCount); + } catch { // 首次导入失败,尝试使用自身代理 - const errmsg = err.message || err.toString(); showNotice("info", t("Import failed, retrying with Clash proxy...")); try { // 使用自身代理尝试导入 @@ -253,8 +289,9 @@ const ProfilePage = () => { // 回退导入成功 showNotice("success", t("Profile Imported with Clash proxy")); setUrl(""); - mutateProfiles(); - await onEnhance(false); + + // 增强的刷新策略 + await performRobustRefresh(preImportProfilesCount); } catch (retryErr: any) { // 回退导入也失败 const retryErrmsg = retryErr?.message || retryErr.toString(); @@ -269,6 +306,73 @@ const ProfilePage = () => { } }; + // 强化的刷新策略 + const performRobustRefresh = async (expectedMinCount: number) => { + let retryCount = 0; + const maxRetries = 5; + const baseDelay = 200; + + while (retryCount < maxRetries) { + try { + console.log(`[导入刷新] 第${retryCount + 1}次尝试刷新配置数据`); + + // 强制刷新,绕过所有缓存 + await mutateProfiles(undefined, { + revalidate: true, + rollbackOnError: false, + }); + + // 等待状态稳定 + await new Promise((resolve) => + setTimeout(resolve, baseDelay * (retryCount + 1)), + ); + + // 验证刷新是否成功 + const currentProfiles = await getProfiles(); + const currentCount = currentProfiles?.items?.length || 0; + + if (currentCount > expectedMinCount) { + console.log( + `[导入刷新] 配置刷新成功,配置数量: ${expectedMinCount} -> ${currentCount}`, + ); + await onEnhance(false); + return; + } + + console.warn( + `[导入刷新] 配置数量未增加 (${currentCount}), 继续重试...`, + ); + retryCount++; + } catch (error) { + console.error(`[导入刷新] 第${retryCount + 1}次刷新失败:`, error); + retryCount++; + await new Promise((resolve) => + setTimeout(resolve, baseDelay * retryCount), + ); + } + } + + // 所有重试失败后的最后尝试 + console.warn(`[导入刷新] 常规刷新失败,尝试清除缓存重新获取`); + try { + // 清除SWR缓存并重新获取 + await mutate("getProfiles", getProfiles(), { revalidate: true }); + await onEnhance(false); + showNotice( + "error", + t("Profile imported but may need manual refresh"), + 3000, + ); + } catch (finalError) { + console.error(`[导入刷新] 最终刷新尝试失败:`, finalError); + showNotice( + "error", + t("Profile imported successfully, please restart if not visible"), + 5000, + ); + } + }; + const onDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (over) { @@ -279,151 +383,167 @@ const ProfilePage = () => { } }; - const executeBackgroundTasks = async ( - profile: string, - sequence: number, - abortController: AbortController, - ) => { - try { - if ( - sequence === requestSequenceRef.current && - switchingProfileRef.current === profile && - !abortController.signal.aborted - ) { - await activateSelected(); - console.log(`[Profile] 后台处理完成,序列号: ${sequence}`); - } else { - debugProfileSwitch( - "BACKGROUND_TASK_SKIPPED", - profile, - `序列号过期或被中断: ${sequence} vs ${requestSequenceRef.current}`, - ); - } - } catch (err: any) { - console.warn("Failed to activate selected proxies:", err); - } - }; - - const activateProfile = async (profile: string, notifySuccess: boolean) => { - if (profiles.current === profile && !notifySuccess) { - console.log(`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`); - return; - } - - const currentSequence = ++requestSequenceRef.current; - debugProfileSwitch("NEW_REQUEST", profile, `序列号: ${currentSequence}`); - - // 处理中断逻辑 - const previousSwitching = switchingProfileRef.current; - if (previousSwitching && previousSwitching !== profile) { - handleProfileInterrupt(previousSwitching, profile); - } - - // 防止重复切换同一个profile - if (switchingProfileRef.current === profile) { - debugProfileSwitch("DUPLICATE_SWITCH_BLOCKED", profile); - return; - } - - // 初始化切换状态 - switchingProfileRef.current = profile; - debugProfileSwitch("SWITCH_START", profile, `序列号: ${currentSequence}`); - - const currentAbortController = new AbortController(); - abortControllerRef.current = currentAbortController; - - setActivatings((prev) => { - if (prev.includes(profile)) return prev; - return [...prev, profile]; - }); - - try { - console.log( - `[Profile] 开始切换到: ${profile},序列号: ${currentSequence}`, - ); - - // 检查请求有效性 - if ( - isRequestOutdated(currentSequence, requestSequenceRef, profile) || - isOperationAborted(currentAbortController, profile) - ) { - return; - } - - // 执行切换请求 - const requestPromise = patchProfiles( - { current: profile }, - currentAbortController.signal, - ); - pendingRequestRef.current = requestPromise; - - const success = await requestPromise; - - if (pendingRequestRef.current === requestPromise) { - pendingRequestRef.current = null; - } - - // 再次检查有效性 - if ( - isRequestOutdated(currentSequence, requestSequenceRef, profile) || - isOperationAborted(currentAbortController, profile) - ) { - return; - } - - // 完成切换 - await mutateLogs(); - closeAllConnections(); - - if (notifySuccess && success) { - showNotice("success", t("Profile Switched"), 1000); - } - - console.log( - `[Profile] 切换到 ${profile} 完成,序列号: ${currentSequence},开始后台处理`, - ); - - // 延迟执行后台任务 - setTimeout( - () => - executeBackgroundTasks( + const executeBackgroundTasks = useCallback( + async ( + profile: string, + sequence: number, + abortController: AbortController, + ) => { + try { + if ( + sequence === requestSequenceRef.current && + switchingProfileRef.current === profile && + !abortController.signal.aborted + ) { + await activateSelected(); + console.log(`[Profile] 后台处理完成,序列号: ${sequence}`); + } else { + debugProfileSwitch( + "BACKGROUND_TASK_SKIPPED", profile, - currentSequence, - currentAbortController, - ), - 50, - ); - } catch (err: any) { - if (pendingRequestRef.current) { - pendingRequestRef.current = null; + `序列号过期或被中断: ${sequence} vs ${requestSequenceRef.current}`, + ); + } + } catch (err: any) { + console.warn("Failed to activate selected proxies:", err); } + }, + [activateSelected], + ); - // 检查是否因为中断或过期而出错 - if ( - isOperationAborted(currentAbortController, profile) || - isRequestOutdated(currentSequence, requestSequenceRef, profile) - ) { + const activateProfile = useCallback( + async (profile: string, notifySuccess: boolean) => { + if (profiles.current === profile && !notifySuccess) { + console.log( + `[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`, + ); return; } - console.error(`[Profile] 切换失败:`, err); - showNotice("error", err?.message || err.toString(), 4000); - } finally { - // 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态 - if ( - switchingProfileRef.current === profile && - currentSequence === requestSequenceRef.current - ) { - cleanupSwitchState(profile, currentSequence); - } else { - debugProfileSwitch( - "CLEANUP_SKIPPED", - profile, - `序列号不匹配或已被接管: ${currentSequence} vs ${requestSequenceRef.current}`, - ); + const currentSequence = ++requestSequenceRef.current; + debugProfileSwitch("NEW_REQUEST", profile, `序列号: ${currentSequence}`); + + // 处理中断逻辑 + const previousSwitching = switchingProfileRef.current; + if (previousSwitching && previousSwitching !== profile) { + handleProfileInterrupt(previousSwitching, profile); } - } - }; + + // 防止重复切换同一个profile + if (switchingProfileRef.current === profile) { + debugProfileSwitch("DUPLICATE_SWITCH_BLOCKED", profile); + return; + } + + // 初始化切换状态 + switchingProfileRef.current = profile; + debugProfileSwitch("SWITCH_START", profile, `序列号: ${currentSequence}`); + + const currentAbortController = new AbortController(); + abortControllerRef.current = currentAbortController; + + setActivatings((prev) => { + if (prev.includes(profile)) return prev; + return [...prev, profile]; + }); + + try { + console.log( + `[Profile] 开始切换到: ${profile},序列号: ${currentSequence}`, + ); + + // 检查请求有效性 + if ( + isRequestOutdated(currentSequence, requestSequenceRef, profile) || + isOperationAborted(currentAbortController, profile) + ) { + return; + } + + // 执行切换请求 + const requestPromise = patchProfiles( + { current: profile }, + currentAbortController.signal, + ); + pendingRequestRef.current = requestPromise; + + const success = await requestPromise; + + if (pendingRequestRef.current === requestPromise) { + pendingRequestRef.current = null; + } + + // 再次检查有效性 + if ( + isRequestOutdated(currentSequence, requestSequenceRef, profile) || + isOperationAborted(currentAbortController, profile) + ) { + return; + } + + // 完成切换 + await mutateLogs(); + closeAllConnections(); + + if (notifySuccess && success) { + showNotice("success", t("Profile Switched"), 1000); + } + + console.log( + `[Profile] 切换到 ${profile} 完成,序列号: ${currentSequence},开始后台处理`, + ); + + // 延迟执行后台任务 + setTimeout( + () => + executeBackgroundTasks( + profile, + currentSequence, + currentAbortController, + ), + 50, + ); + } catch (err: any) { + if (pendingRequestRef.current) { + pendingRequestRef.current = null; + } + + // 检查是否因为中断或过期而出错 + if ( + isOperationAborted(currentAbortController, profile) || + isRequestOutdated(currentSequence, requestSequenceRef, profile) + ) { + return; + } + + console.error(`[Profile] 切换失败:`, err); + showNotice("error", err?.message || err.toString(), 4000); + } finally { + // 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态 + if ( + switchingProfileRef.current === profile && + currentSequence === requestSequenceRef.current + ) { + cleanupSwitchState(profile, currentSequence); + } else { + debugProfileSwitch( + "CLEANUP_SKIPPED", + profile, + `序列号不匹配或已被接管: ${currentSequence} vs ${requestSequenceRef.current}`, + ); + } + } + }, + [ + profiles, + patchProfiles, + mutateLogs, + t, + executeBackgroundTasks, + handleProfileInterrupt, + cleanupSwitchState, + ], + ); const onSelect = async (current: string, force: boolean) => { // 阻止重复点击或已激活的profile if (switchingProfileRef.current === current) { @@ -446,7 +566,7 @@ const ProfilePage = () => { await activateProfile(current, false); } })(); - }, current); + }, [current, activateProfile, mutateProfiles]); const onEnhance = useLockFn(async (notifySuccess: boolean) => { if (switchingProfileRef.current) { @@ -482,7 +602,9 @@ const ProfilePage = () => { await deleteProfile(uid); mutateProfiles(); mutateLogs(); - current && (await onEnhance(false)); + if (current) { + await onEnhance(false); + } } catch (err: any) { showNotice("error", err?.message || err.toString()); } finally { @@ -618,6 +740,26 @@ const ProfilePage = () => { > + + {/* 故障检测和紧急恢复按钮 */} + {(error || isStale) && ( + + + + )} } > @@ -757,6 +899,7 @@ const ProfilePage = () => { + { const { t } = useTranslation(); + // 从 localStorage 恢复链式代理按钮状态 + const [isChainMode, setIsChainMode] = useState(() => { + try { + const saved = localStorage.getItem("proxy-chain-mode-enabled"); + return saved === "true"; + } catch { + return false; + } + }); + + const [chainConfigData, setChainConfigData] = useState(null); + const { data: clashConfig, mutate: mutateClash } = useSWR( "getClashConfig", getClashConfig, @@ -26,7 +44,7 @@ const ProxyPage = () => { const { verge } = useVerge(); - const modeList = ["rule", "global", "direct"]; + const modeList = useMemo(() => ["rule", "global", "direct"], []); const curMode = clashConfig?.mode?.toLowerCase(); @@ -39,17 +57,64 @@ const ProxyPage = () => { mutateClash(); }); + const onToggleChainMode = useLockFn(async () => { + const newChainMode = !isChainMode; + + if (!newChainMode) { + // 退出链式代理模式时,清除链式代理配置 + try { + console.log("Exiting chain mode, clearing chain configuration"); + await updateProxyChainConfigInRuntime(null); + console.log("Chain configuration cleared successfully"); + } catch (error) { + console.error("Failed to clear chain configuration:", error); + } + } + + setIsChainMode(newChainMode); + + // 保存链式代理按钮状态到 localStorage + localStorage.setItem("proxy-chain-mode-enabled", newChainMode.toString()); + }); + + // 当开启链式代理模式时,获取配置数据 + useEffect(() => { + if (isChainMode) { + const fetchChainConfig = async () => { + try { + const exitNode = localStorage.getItem("proxy-chain-exit-node"); + + if (!exitNode) { + console.error("No proxy chain exit node found in localStorage"); + setChainConfigData(""); + return; + } + + const configData = await getRuntimeProxyChainConfig(exitNode); + setChainConfigData(configData || ""); + } catch (error) { + console.error("Failed to get runtime proxy chain config:", error); + setChainConfigData(""); + } + }; + + fetchChainConfig(); + } else { + setChainConfigData(null); + } + }, [isChainMode]); + useEffect(() => { if (curMode && !modeList.includes(curMode)) { onChangeMode("rule"); } - }, [curMode]); + }, [curMode, modeList, onChangeMode]); return ( @@ -66,10 +131,23 @@ const ProxyPage = () => { ))} + + } > - + ); }; diff --git a/clash-verge-rev/src/pages/rules.tsx b/clash-verge-rev/src/pages/rules.tsx index 152ced35a0..c800e13c0e 100644 --- a/clash-verge-rev/src/pages/rules.tsx +++ b/clash-verge-rev/src/pages/rules.tsx @@ -1,14 +1,15 @@ -import { useState, useMemo, useRef, useEffect } from "react"; +import { Box } from "@mui/material"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; -import { Box } from "@mui/material"; + import { BaseEmpty, BasePage } from "@/components/base"; -import RuleItem from "@/components/rule/rule-item"; -import { ProviderButton } from "@/components/rule/provider-button"; import { BaseSearchBox } from "@/components/base/base-search-box"; import { ScrollTopButton } from "@/components/layout/scroll-top-button"; -import { useAppData } from "@/providers/app-data-provider"; +import { ProviderButton } from "@/components/rule/provider-button"; +import RuleItem from "@/components/rule/rule-item"; import { useVisibility } from "@/hooks/use-visibility"; +import { useAppData } from "@/providers/app-data-context"; const RulesPage = () => { const { t } = useTranslation(); diff --git a/clash-verge-rev/src/pages/settings.tsx b/clash-verge-rev/src/pages/settings.tsx index cd2ea89555..18eb8af28b 100644 --- a/clash-verge-rev/src/pages/settings.tsx +++ b/clash-verge-rev/src/pages/settings.tsx @@ -1,15 +1,16 @@ +import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material"; import { Box, ButtonGroup, IconButton, Grid } from "@mui/material"; import { useLockFn } from "ahooks"; import { useTranslation } from "react-i18next"; + import { BasePage } from "@/components/base"; -import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material"; -import { openWebUrl } from "@/services/cmds"; -import SettingVergeBasic from "@/components/setting/setting-verge-basic"; -import SettingVergeAdvanced from "@/components/setting/setting-verge-advanced"; import SettingClash from "@/components/setting/setting-clash"; import SettingSystem from "@/components/setting/setting-system"; -import { useThemeMode } from "@/services/states"; +import SettingVergeAdvanced from "@/components/setting/setting-verge-advanced"; +import SettingVergeBasic from "@/components/setting/setting-verge-basic"; +import { openWebUrl } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; +import { useThemeMode } from "@/services/states"; const SettingPage = () => { const { t } = useTranslation(); diff --git a/clash-verge-rev/src/pages/test.tsx b/clash-verge-rev/src/pages/test.tsx index b77aed37ea..66a63e7438 100644 --- a/clash-verge-rev/src/pages/test.tsx +++ b/clash-verge-rev/src/pages/test.tsx @@ -1,33 +1,32 @@ -import { useEffect, useRef, useState } from "react"; -import { useVerge } from "@/hooks/use-verge"; -import { Box, Button, Grid } from "@mui/material"; import { - DndContext, closestCenter, + DndContext, + DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors, - DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, sortableKeyboardCoordinates, } from "@dnd-kit/sortable"; - -import { useTranslation } from "react-i18next"; -import { BasePage } from "@/components/base"; -import { TestViewer, TestViewerRef } from "@/components/test/test-viewer"; -import { TestItem } from "@/components/test/test-item"; +import { Box, Button, Grid } from "@mui/material"; import { emit } from "@tauri-apps/api/event"; import { nanoid } from "nanoid"; -import { ScrollTopButton } from "@/components/layout/scroll-top-button"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; // test icons import apple from "@/assets/image/test/apple.svg?raw"; import github from "@/assets/image/test/github.svg?raw"; import google from "@/assets/image/test/google.svg?raw"; import youtube from "@/assets/image/test/youtube.svg?raw"; +import { BasePage } from "@/components/base"; +import { ScrollTopButton } from "@/components/layout/scroll-top-button"; +import { TestItem } from "@/components/test/test-item"; +import { TestViewer, TestViewerRef } from "@/components/test/test-viewer"; +import { useVerge } from "@/hooks/use-verge"; const TestPage = () => { const { t } = useTranslation(); @@ -40,32 +39,36 @@ const TestPage = () => { const { verge, mutateVerge, patchVerge } = useVerge(); // test list - const testList = verge?.test_list ?? [ - { - uid: nanoid(), - name: "Apple", - url: "https://www.apple.com", - icon: apple, - }, - { - uid: nanoid(), - name: "GitHub", - url: "https://www.github.com", - icon: github, - }, - { - uid: nanoid(), - name: "Google", - url: "https://www.google.com", - icon: google, - }, - { - uid: nanoid(), - name: "Youtube", - url: "https://www.youtube.com", - icon: youtube, - }, - ]; + const testList = useMemo( + () => + verge?.test_list ?? [ + { + uid: nanoid(), + name: "Apple", + url: "https://www.apple.com", + icon: apple, + }, + { + uid: nanoid(), + name: "GitHub", + url: "https://www.github.com", + icon: github, + }, + { + uid: nanoid(), + name: "Google", + url: "https://www.google.com", + icon: google, + }, + { + uid: nanoid(), + name: "Youtube", + url: "https://www.youtube.com", + icon: youtube, + }, + ], + [verge], + ); const onTestListItemChange = ( uid: string, @@ -101,12 +104,12 @@ const TestPage = () => { const { active, over } = event; if (over) { if (active.id !== over.id) { - let old_index = testList.findIndex((x) => x.uid === active.id); - let new_index = testList.findIndex((x) => x.uid === over.id); + const old_index = testList.findIndex((x) => x.uid === active.id); + const new_index = testList.findIndex((x) => x.uid === over.id); if (old_index < 0 || new_index < 0) { return; } - let newList = reorder(testList, old_index, new_index); + const newList = reorder(testList, old_index, new_index); await mutateVerge({ ...verge, test_list: newList }, false); await patchVerge({ test_list: newList }); } @@ -118,7 +121,7 @@ const TestPage = () => { if (!verge?.test_list) { patchVerge({ test_list: testList }); } - }, [verge]); + }, [verge, patchVerge, testList]); const viewerRef = useRef(null); const [showScrollTop, setShowScrollTop] = useState(false); diff --git a/clash-verge-rev/src/pages/unlock.tsx b/clash-verge-rev/src/pages/unlock.tsx index f62f4c97fd..562d534cfa 100644 --- a/clash-verge-rev/src/pages/unlock.tsx +++ b/clash-verge-rev/src/pages/unlock.tsx @@ -1,32 +1,32 @@ -import { useEffect, useState } from "react"; +import { + AccessTimeOutlined, + CancelOutlined, + CheckCircleOutlined, + HelpOutline, + PendingOutlined, + RefreshRounded, +} from "@mui/icons-material"; import { Box, Button, Card, - Divider, - Typography, Chip, - Tooltip, CircularProgress, + Divider, + Grid, + Tooltip, + Typography, alpha, useTheme, - Grid, } from "@mui/material"; -import { useTranslation } from "react-i18next"; import { invoke } from "@tauri-apps/api/core"; -import { BasePage, BaseEmpty } from "@/components/base"; import { useLockFn } from "ahooks"; -import { - CheckCircleOutlined, - CancelOutlined, - HelpOutline, - PendingOutlined, - RefreshRounded, - AccessTimeOutlined, -} from "@mui/icons-material"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { BaseEmpty, BasePage } from "@/components/base"; import { showNotice } from "@/services/noticeService"; -// 定义流媒体检测项类型 interface UnlockItem { name: string; status: string; @@ -34,7 +34,6 @@ interface UnlockItem { check_time?: string | null; } -// 用于存储测试结果的本地存储键名 const UNLOCK_RESULTS_STORAGE_KEY = "clash_verge_unlock_results"; const UNLOCK_RESULTS_TIME_KEY = "clash_verge_unlock_time"; @@ -42,19 +41,13 @@ const UnlockPage = () => { const { t } = useTranslation(); const theme = useTheme(); - // 保存所有流媒体检测项的状态 const [unlockItems, setUnlockItems] = useState([]); - // 是否正在执行全部检测 const [isCheckingAll, setIsCheckingAll] = useState(false); - // 记录正在检测中的项目 const [loadingItems, setLoadingItems] = useState([]); - // 最后检测时间 - const [lastCheckTime, setLastCheckTime] = useState(null); - // 按首字母排序项目 - const sortItemsByName = (items: UnlockItem[]) => { + const sortItemsByName = useCallback((items: UnlockItem[]) => { return [...items].sort((a, b) => a.name.localeCompare(b.name)); - }; + }, []); // 保存测试结果到本地存储 const saveResultsToStorage = (items: UnlockItem[], time: string | null) => { @@ -68,7 +61,6 @@ const UnlockPage = () => { } }; - // 从本地存储加载测试结果 const loadResultsFromStorage = (): { items: UnlockItem[] | null; time: string | null; @@ -90,39 +82,33 @@ const UnlockPage = () => { return { items: null, time: null }; }; - // 页面加载时获取初始检测项列表 + const getUnlockItems = useCallback( + async (updateUI: boolean = true) => { + try { + const items = await invoke("get_unlock_items"); + const sortedItems = sortItemsByName(items); + + if (updateUI) { + setUnlockItems(sortedItems); + } + } catch (err: any) { + console.error("Failed to get unlock items:", err); + } + }, + [sortItemsByName], + ); + useEffect(() => { - // 尝试从本地存储加载上次测试结果 - const { items: storedItems, time } = loadResultsFromStorage(); + const { items: storedItems } = loadResultsFromStorage(); if (storedItems && storedItems.length > 0) { - // 如果有存储的结果,优先使用 setUnlockItems(storedItems); - setLastCheckTime(time); - - // 后台同时获取最新的初始状态(但不更新UI) getUnlockItems(false); } else { - // 没有存储的结果,获取初始状态 getUnlockItems(true); } - }, []); + }, [getUnlockItems]); - // 获取所有解锁检测项列表 - const getUnlockItems = async (updateUI: boolean = true) => { - try { - const items = await invoke("get_unlock_items"); - const sortedItems = sortItemsByName(items); - - if (updateUI) { - setUnlockItems(sortedItems); - } - } catch (err: any) { - console.error("Failed to get unlock items:", err); - } - }; - - // invoke加超时,防止后端卡死 const invokeWithTimeout = async ( cmd: string, args?: any, @@ -131,7 +117,10 @@ const UnlockPage = () => { return Promise.race([ invoke(cmd, args), new Promise((_, reject) => - setTimeout(() => reject(new Error("Timeout")), timeout), + setTimeout( + () => reject(new Error(t("Detection timeout or failed"))), + timeout, + ), ), ]); }; @@ -146,15 +135,16 @@ const UnlockPage = () => { setUnlockItems(sortedItems); const currentTime = new Date().toLocaleString(); - setLastCheckTime(currentTime); saveResultsToStorage(sortedItems, currentTime); setIsCheckingAll(false); } catch (err: any) { setIsCheckingAll(false); - showNotice("error", err?.message || err?.toString() || "检测超时或失败"); - alert("检测超时或失败: " + (err?.message || err)); + showNotice( + "error", + err?.message || err?.toString() || t("Detection timeout or failed"), + ); console.error("Failed to check media unlock:", err); } }); @@ -177,7 +167,6 @@ const UnlockPage = () => { setUnlockItems(updatedItems); const currentTime = new Date().toLocaleString(); - setLastCheckTime(currentTime); saveResultsToStorage(updatedItems, currentTime); } @@ -185,13 +174,17 @@ const UnlockPage = () => { setLoadingItems((prev) => prev.filter((item) => item !== name)); } catch (err: any) { setLoadingItems((prev) => prev.filter((item) => item !== name)); - showNotice("error", err?.message || err?.toString() || `检测${name}失败`); - alert("检测超时或失败: " + (err?.message || err)); + showNotice( + "error", + err?.message || + err?.toString() || + t("Detection failed for {name}").replace("{name}", name), + ); console.error(`Failed to check ${name}:`, err); } }); - // 获取状态对应的颜色 + // 状态颜色 const getStatusColor = (status: string) => { if (status === "Pending") return "default"; if (status === "Yes") return "success"; @@ -209,7 +202,7 @@ const UnlockPage = () => { return "default"; }; - // 获取状态对应的图标 + // 状态图标 const getStatusIcon = (status: string) => { if (status === "Pending") return ; if (status === "Yes") return ; @@ -219,17 +212,7 @@ const UnlockPage = () => { return ; }; - // 获取状态对应的背景色 - const getStatusBgColor = (status: string) => { - if (status === "Yes") return alpha(theme.palette.success.main, 0.05); - if (status === "No") return alpha(theme.palette.error.main, 0.05); - if (status === "Soon") return alpha(theme.palette.warning.main, 0.05); - if (status.includes("Failed")) return alpha(theme.palette.error.main, 0.03); - if (status === "Completed") return alpha(theme.palette.info.main, 0.05); - return "transparent"; - }; - - // 获取状态对应的边框色 + // 边框色 const getStatusBorderColor = (status: string) => { if (status === "Yes") return theme.palette.success.main; if (status === "No") return theme.palette.error.main; @@ -278,7 +261,7 @@ const UnlockPage = () => { ) : ( {unlockItems.map((item) => ( - + { minWidth: "32px", width: "32px", height: "32px", - //p: 0, borderRadius: "50%", }} onClick={() => checkSingleMedia(item.name)} > - {loadingItems.includes(item.name) ? ( - - ) : ( - - )} + diff --git a/clash-verge-rev/src/polyfills/RegExp.js b/clash-verge-rev/src/polyfills/RegExp.js index 7fe6f104de..389ae27a65 100644 --- a/clash-verge-rev/src/polyfills/RegExp.js +++ b/clash-verge-rev/src/polyfills/RegExp.js @@ -11,13 +11,23 @@ } if (flags) { - if (!originalRegExp.prototype.hasOwnProperty("unicodeSets")) { + if ( + !Object.prototype.hasOwnProperty.call( + originalRegExp.prototype, + "unicodeSets", + ) + ) { if (flags.includes("v")) { flags = flags.replace("v", "u"); } } - if (!originalRegExp.prototype.hasOwnProperty("hasIndices")) { + if ( + !Object.prototype.hasOwnProperty.call( + originalRegExp.prototype, + "hasIndices", + ) + ) { if (flags.includes("d")) { flags = flags.replace("d", ""); } diff --git a/clash-verge-rev/src/providers/app-data-context.ts b/clash-verge-rev/src/providers/app-data-context.ts new file mode 100644 index 0000000000..3b97c92c85 --- /dev/null +++ b/clash-verge-rev/src/providers/app-data-context.ts @@ -0,0 +1,53 @@ +import { createContext, use } from "react"; + +export interface AppDataContextType { + proxies: any; + clashConfig: any; + rules: any[]; + sysproxy: any; + runningMode?: string; + uptime: number; + proxyProviders: any; + ruleProviders: any; + connections: { + data: ConnectionWithSpeed[]; + count: number; + uploadTotal: number; + downloadTotal: number; + }; + traffic: { up: number; down: number }; + memory: { inuse: number }; + systemProxyAddress: string; + + refreshProxy: () => Promise; + refreshClashConfig: () => Promise; + refreshRules: () => Promise; + refreshSysproxy: () => Promise; + refreshProxyProviders: () => Promise; + refreshRuleProviders: () => Promise; + refreshAll: () => Promise; +} + +export interface ConnectionWithSpeed extends IConnectionsItem { + curUpload: number; + curDownload: number; +} + +export interface ConnectionSpeedData { + id: string; + upload: number; + download: number; + timestamp: number; +} + +export const AppDataContext = createContext(null); + +export const useAppData = () => { + const context = use(AppDataContext); + + if (!context) { + throw new Error("useAppData必须在AppDataProvider内使用"); + } + + return context; +}; diff --git a/clash-verge-rev/src/providers/app-data-provider.tsx b/clash-verge-rev/src/providers/app-data-provider.tsx index 87cd588977..6f9972d51c 100644 --- a/clash-verge-rev/src/providers/app-data-provider.tsx +++ b/clash-verge-rev/src/providers/app-data-provider.tsx @@ -1,76 +1,30 @@ -import React, { - createContext, - useContext, - useEffect, - useMemo, - useRef, -} from "react"; -import { useVerge } from "@/hooks/use-verge"; +import { listen } from "@tauri-apps/api/event"; +import React, { useCallback, useEffect, useMemo, useRef } from "react"; import useSWR from "swr"; + +import { useClashInfo } from "@/hooks/use-clash"; +import { useVerge } from "@/hooks/use-verge"; +import { useVisibility } from "@/hooks/use-visibility"; import { - getProxies, - getRules, + forceRefreshProxies, + getAppUptime, getClashConfig, + getConnections, + getMemoryData, + getProxies, getProxyProviders, getRuleProviders, - getConnections, - getTrafficData, - getMemoryData, -} from "@/services/cmds"; -import { - getSystemProxy, + getRules, getRunningMode, - getAppUptime, - forceRefreshProxies, + getSystemProxy, + getTrafficData, } from "@/services/cmds"; -import { useClashInfo } from "@/hooks/use-clash"; -import { useVisibility } from "@/hooks/use-visibility"; -import { listen } from "@tauri-apps/api/event"; -// 连接速度计算接口 -interface ConnectionSpeedData { - id: string; - upload: number; - download: number; - timestamp: number; -} - -interface ConnectionWithSpeed extends IConnectionsItem { - curUpload: number; - curDownload: number; -} - -// 定义AppDataContext类型 - 使用宽松类型 -interface AppDataContextType { - proxies: any; - clashConfig: any; - rules: any[]; - sysproxy: any; - runningMode?: string; - uptime: number; - proxyProviders: any; - ruleProviders: any; - connections: { - data: ConnectionWithSpeed[]; - count: number; - uploadTotal: number; - downloadTotal: number; - }; - traffic: { up: number; down: number }; - memory: { inuse: number }; - systemProxyAddress: string; - - refreshProxy: () => Promise; - refreshClashConfig: () => Promise; - refreshRules: () => Promise; - refreshSysproxy: () => Promise; - refreshProxyProviders: () => Promise; - refreshRuleProviders: () => Promise; - refreshAll: () => Promise; -} - -// 创建上下文 -const AppDataContext = createContext(null); +import { + AppDataContext, + type ConnectionSpeedData, + type ConnectionWithSpeed, +} from "./app-data-context"; // 全局数据提供者组件 export const AppDataProvider = ({ @@ -142,107 +96,227 @@ export const AppDataProvider = ({ // 监听profile和clash配置变更事件 useEffect(() => { - let profileUnlisten: Promise<() => void> | undefined; let lastProfileId: string | null = null; let lastUpdateTime = 0; const refreshThrottle = 500; - const setupEventListeners = async () => { - try { - // 监听profile切换事件 - profileUnlisten = listen("profile-changed", (event) => { - const newProfileId = event.payload; - const now = Date.now(); + let isUnmounted = false; + const scheduledTimeouts = new Set>(); + const cleanupFns: Array<() => void> = []; + const fallbackWindowListeners: Array<[string, EventListener]> = []; - console.log(`[AppDataProvider] Profile切换事件: ${newProfileId}`); - - if ( - lastProfileId === newProfileId && - now - lastUpdateTime < refreshThrottle - ) { - console.log("[AppDataProvider] 重复事件被防抖,跳过"); - return; - } - - lastProfileId = newProfileId; - lastUpdateTime = now; - - setTimeout(() => { - // 先执行 forceRefreshProxies,完成后稍延迟再刷新前端数据,避免页面一直 loading - forceRefreshProxies() - .catch((e) => - console.warn("[AppDataProvider] forceRefreshProxies 失败:", e), - ) - .finally(() => { - setTimeout(() => { - refreshProxy().catch((e) => - console.warn("[AppDataProvider] 普通刷新也失败:", e), - ); - }, 200); // 200ms 延迟,保证后端缓存已清理 - }); - }, 0); - }); - - // 监听Clash配置刷新事件(enhance操作等) - const handleRefreshClash = () => { - const now = Date.now(); - console.log("[AppDataProvider] Clash配置刷新事件"); - - if (now - lastUpdateTime > refreshThrottle) { - lastUpdateTime = now; - - setTimeout(async () => { - try { - console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存"); - - // 添加超时保护 - const refreshPromise = Promise.race([ - forceRefreshProxies(), - new Promise((_, reject) => - setTimeout( - () => reject(new Error("forceRefreshProxies timeout")), - 8000, - ), - ), - ]); - - await refreshPromise; - await refreshProxy(); - } catch (error) { - console.error( - "[AppDataProvider] Clash刷新时强制刷新代理缓存失败:", - error, - ); - refreshProxy().catch((e) => - console.warn("[AppDataProvider] Clash刷新普通刷新也失败:", e), - ); - } - }, 0); - } - }; - - window.addEventListener( - "verge://refresh-clash-config", - handleRefreshClash, - ); - - return () => { - window.removeEventListener( - "verge://refresh-clash-config", - handleRefreshClash, - ); - }; - } catch (error) { - console.error("[AppDataProvider] 事件监听器设置失败:", error); - return () => {}; + const registerCleanup = (fn: () => void) => { + if (isUnmounted) { + fn(); + } else { + cleanupFns.push(fn); } }; - const cleanupPromise = setupEventListeners(); + const scheduleTimeout = ( + callback: () => void | Promise, + delay: number, + ) => { + const timeoutId = window.setTimeout(() => { + scheduledTimeouts.delete(timeoutId); + void callback(); + }, delay); + + scheduledTimeouts.add(timeoutId); + return timeoutId; + }; + + const clearScheduledTimeout = ( + timeoutId: ReturnType, + ) => { + if (scheduledTimeouts.has(timeoutId)) { + clearTimeout(timeoutId); + scheduledTimeouts.delete(timeoutId); + } + }; + + const clearAllTimeouts = () => { + scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId)); + scheduledTimeouts.clear(); + }; + + const withTimeout = async ( + promise: Promise, + timeoutMs: number, + label: string, + ): Promise => { + let timeoutId: ReturnType | null = null; + + const timeoutPromise = new Promise((_, reject) => { + timeoutId = scheduleTimeout(() => reject(new Error(label)), timeoutMs); + }); + + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + if (timeoutId !== null) { + clearScheduledTimeout(timeoutId); + } + } + }; + + const handleProfileChanged = (event: { payload: string }) => { + const newProfileId = event.payload; + const now = Date.now(); + + console.log(`[AppDataProvider] Profile切换事件: ${newProfileId}`); + + if ( + lastProfileId === newProfileId && + now - lastUpdateTime < refreshThrottle + ) { + console.log("[AppDataProvider] 重复事件被防抖,跳过"); + return; + } + + lastProfileId = newProfileId; + lastUpdateTime = now; + + scheduleTimeout(() => { + void forceRefreshProxies() + .catch((error) => { + console.warn("[AppDataProvider] forceRefreshProxies 失败:", error); + }) + .finally(() => { + scheduleTimeout(() => { + refreshProxy().catch((error) => { + console.warn("[AppDataProvider] 普通刷新也失败:", error); + }); + }, 200); + }); + }, 0); + }; + + const handleRefreshClash = () => { + const now = Date.now(); + console.log("[AppDataProvider] Clash配置刷新事件"); + + if (now - lastUpdateTime <= refreshThrottle) { + return; + } + + lastUpdateTime = now; + + scheduleTimeout(async () => { + try { + console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存"); + await withTimeout( + forceRefreshProxies(), + 8000, + "forceRefreshProxies timeout", + ); + await refreshProxy(); + } catch (error) { + console.error( + "[AppDataProvider] Clash刷新时强制刷新代理缓存失败:", + error, + ); + refreshProxy().catch((e) => + console.warn("[AppDataProvider] Clash刷新普通刷新也失败:", e), + ); + } + }, 0); + }; + + const handleRefreshProxy = () => { + const now = Date.now(); + console.log("[AppDataProvider] 代理配置刷新事件"); + + if (now - lastUpdateTime <= refreshThrottle) { + return; + } + + lastUpdateTime = now; + + scheduleTimeout(() => { + refreshProxy().catch((error) => + console.warn("[AppDataProvider] 代理刷新失败:", error), + ); + }, 100); + }; + + const handleForceRefreshProxies = () => { + console.log("[AppDataProvider] 强制代理刷新事件"); + + void forceRefreshProxies() + .then(() => { + console.log("[AppDataProvider] 强制刷新代理缓存完成"); + return refreshProxy(); + }) + .then(() => { + console.log("[AppDataProvider] 前端代理数据刷新完成"); + }) + .catch((error) => { + console.warn("[AppDataProvider] 强制代理刷新失败:", error); + refreshProxy().catch((fallbackError) => { + console.warn( + "[AppDataProvider] 普通代理刷新也失败:", + fallbackError, + ); + }); + }); + }; + + const initializeListeners = async () => { + try { + const unlistenProfile = await listen( + "profile-changed", + handleProfileChanged, + ); + registerCleanup(unlistenProfile); + } catch (error) { + console.error("[AppDataProvider] 监听 Profile 事件失败:", error); + } + + try { + const unlistenClash = await listen( + "verge://refresh-clash-config", + handleRefreshClash, + ); + const unlistenProxy = await listen( + "verge://refresh-proxy-config", + handleRefreshProxy, + ); + const unlistenForceRefresh = await listen( + "verge://force-refresh-proxies", + handleForceRefreshProxies, + ); + + registerCleanup(() => { + unlistenClash(); + unlistenProxy(); + unlistenForceRefresh(); + }); + } catch (error) { + console.warn("[AppDataProvider] 设置 Tauri 事件监听器失败:", error); + + const fallbackHandlers: Array<[string, EventListener]> = [ + ["verge://refresh-clash-config", handleRefreshClash], + ["verge://refresh-proxy-config", handleRefreshProxy], + ["verge://force-refresh-proxies", handleForceRefreshProxies], + ]; + + fallbackHandlers.forEach(([eventName, handler]) => { + window.addEventListener(eventName, handler); + fallbackWindowListeners.push([eventName, handler]); + }); + } + }; + + void initializeListeners(); return () => { - profileUnlisten?.then((unlisten) => unlisten()).catch(console.error); - cleanupPromise.then((cleanup) => cleanup()); + isUnmounted = true; + clearAllTimeouts(); + fallbackWindowListeners.splice(0).forEach(([eventName, handler]) => { + window.removeEventListener(eventName, handler); + }); + cleanupFns.splice(0).forEach((fn) => fn()); }; }, [refreshProxy]); @@ -351,7 +425,7 @@ export const AppDataProvider = ({ }; }, { - refreshInterval: 2000, // 2秒刷新一次 + refreshInterval: 1000, // 1秒刷新一次 fallbackData: { connections: [], uploadTotal: 0, downloadTotal: 0 }, keepPreviousData: true, onError: (error) => { @@ -368,7 +442,7 @@ export const AppDataProvider = ({ refreshInterval: 1000, // 1秒刷新一次 fallbackData: { up: 0, down: 0 }, keepPreviousData: true, - onSuccess: (data) => { + onSuccess: () => { // console.log("[Traffic][AppDataProvider] IPC 获取到流量数据:", data); }, onError: (error) => { @@ -392,7 +466,7 @@ export const AppDataProvider = ({ ); // 提供统一的刷新方法 - const refreshAll = async () => { + const refreshAll = useCallback(async () => { await Promise.all([ refreshProxy(), refreshClashConfig(), @@ -401,7 +475,14 @@ export const AppDataProvider = ({ refreshProxyProviders(), refreshRuleProviders(), ]); - }; + }, [ + refreshProxy, + refreshClashConfig, + refreshRules, + refreshSysproxy, + refreshProxyProviders, + refreshRuleProviders, + ]); // 聚合所有数据 const value = useMemo(() => { @@ -491,20 +572,8 @@ export const AppDataProvider = ({ refreshSysproxy, refreshProxyProviders, refreshRuleProviders, + refreshAll, ]); - return ( - {children} - ); -}; - -// 自定义Hook访问全局数据 -export const useAppData = () => { - const context = useContext(AppDataContext); - - if (!context) { - throw new Error("useAppData必须在AppDataProvider内使用"); - } - - return context; + return {children}; }; diff --git a/clash-verge-rev/src/providers/chain-proxy-provider.tsx b/clash-verge-rev/src/providers/chain-proxy-provider.tsx new file mode 100644 index 0000000000..c8ccce1d51 --- /dev/null +++ b/clash-verge-rev/src/providers/chain-proxy-provider.tsx @@ -0,0 +1,48 @@ +import React, { createContext, useCallback, use, useState } from "react"; + +interface ChainProxyContextType { + isChainMode: boolean; + setChainMode: (isChain: boolean) => void; + chainConfigData: string | null; + setChainConfigData: (data: string | null) => void; +} + +const ChainProxyContext = createContext(null); + +export const ChainProxyProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [isChainMode, setIsChainMode] = useState(false); + const [chainConfigData, setChainConfigData] = useState(null); + + const setChainMode = useCallback((isChain: boolean) => { + setIsChainMode(isChain); + }, []); + + const setChainConfigDataCallback = useCallback((data: string | null) => { + setChainConfigData(data); + }, []); + + return ( + + {children} + + ); +}; + +export const useChainProxy = () => { + const context = use(ChainProxyContext); + if (!context) { + throw new Error("useChainProxy must be used within a ChainProxyProvider"); + } + return context; +}; diff --git a/clash-verge-rev/src/services/api.ts b/clash-verge-rev/src/services/api.ts index 0cb54214c1..a5508509f8 100644 --- a/clash-verge-rev/src/services/api.ts +++ b/clash-verge-rev/src/services/api.ts @@ -1,5 +1,6 @@ +import { fetch } from "@tauri-apps/plugin-http"; import axios, { AxiosInstance } from "axios"; -import { invoke } from "@tauri-apps/api/core"; + import { getClashInfo } from "./cmds"; let instancePromise: Promise = null!; @@ -213,17 +214,19 @@ export const getIpInfo = async (): Promise => { timeoutController.abort(); }, service.timeout || serviceTimeout); - const response = await axios.get(service.url, { + const response = await fetch(service.url, { + method: "GET", signal: timeoutController.signal, - timeout: service.timeout || serviceTimeout, - // 移除了headers参数(默认会使用axios的默认User-Agent) + connectTimeout: service.timeout || serviceTimeout, }); + const data = await response.json(); + if (timeoutId) clearTimeout(timeoutId); - if (response.data && response.data.ip) { + if (data && data.ip) { console.log(`IP检测成功,使用服务: ${service.url}`); - return service.mapping(response.data); + return service.mapping(data); } else { throw new Error(`无效的响应格式 from ${service.url}`); } @@ -231,9 +234,9 @@ export const getIpInfo = async (): Promise => { if (timeoutId) clearTimeout(timeoutId); lastError = error; - console.log( + console.warn( `尝试 ${attempt + 1}/${maxRetries} 失败 (${service.url}):`, - error.message, + error, ); if (error.name === "AbortError") { diff --git a/clash-verge-rev/src/services/cmds.ts b/clash-verge-rev/src/services/cmds.ts index d4021c3eff..d12f79987c 100644 --- a/clash-verge-rev/src/services/cmds.ts +++ b/clash-verge-rev/src/services/cmds.ts @@ -1,4 +1,5 @@ import { invoke } from "@tauri-apps/api/core"; + import { showNotice } from "@/services/noticeService"; export async function copyClashEnv() { @@ -86,11 +87,23 @@ export async function getRuntimeLogs() { return invoke>("get_runtime_logs"); } +export async function getRuntimeProxyChainConfig(proxyChainExitNode: string) { + return invoke("get_runtime_proxy_chain_config", { + proxyChainExitNode, + }); +} + +export async function updateProxyChainConfigInRuntime(proxyChainConfig: any) { + return invoke("update_proxy_chain_config_in_runtime", { + proxyChainConfig, + }); +} + export async function patchClashConfig(payload: Partial) { return invoke("patch_clash_config", { payload }); } -export async function patchClashMode(payload: String) { +export async function patchClashMode(payload: string) { return invoke("patch_clash_mode", { payload }); } @@ -137,7 +150,18 @@ export async function getProxyDelay( } export async function updateProxy(group: string, proxy: string) { - return await invoke("update_proxy_choice", { group, proxy }); + // const start = Date.now(); + await invoke("update_proxy_choice", { group, proxy }); + // const duration = Date.now() - start; + // console.log(`[API] updateProxy 耗时: ${duration}ms`); +} + +export async function syncTrayProxySelection() { + return invoke("sync_tray_proxy_selection"); +} + +export async function updateProxyAndSync(group: string, proxy: string) { + return invoke("update_proxy_and_sync", { group, proxy }); } export async function getProxies(): Promise<{ @@ -196,20 +220,19 @@ export async function getProxies(): Promise<{ }, []); if (global?.all) { - let globalGroups: IProxyGroupItem[] = global.all.reduce( - (acc, name) => { - if (proxyRecord[name]?.all) { - acc.push({ - ...proxyRecord[name], - all: proxyRecord[name].all!.map((item) => generateItem(item)), - }); - } - return acc; - }, - [], - ); + const globalGroups: IProxyGroupItem[] = global.all.reduce< + IProxyGroupItem[] + >((acc, name) => { + if (proxyRecord[name]?.all) { + acc.push({ + ...proxyRecord[name], + all: proxyRecord[name].all!.map((item) => generateItem(item)), + }); + } + return acc; + }, []); - let globalNames = new Set(globalGroups.map((each) => each.name)); + const globalNames = new Set(globalGroups.map((each) => each.name)); groups = groups .filter((group) => { return !globalNames.has(group.name); @@ -245,7 +268,7 @@ export async function getProxyProviders() { const providers = response.providers as Record; return Object.fromEntries( - Object.entries(providers).filter(([key, item]) => { + Object.entries(providers).filter(([, item]) => { const type = item.vehicleType.toLowerCase(); return type === "http" || type === "file"; }), @@ -263,7 +286,7 @@ export async function getRuleProviders() { >; return Object.fromEntries( - Object.entries(providers).filter(([key, item]) => { + Object.entries(providers).filter(([, item]) => { const type = item.vehicleType.toLowerCase(); return type === "http" || type === "file"; }), @@ -321,7 +344,7 @@ export async function getMemoryData() { usage_percent?: number; last_updated?: number; }>("get_memory_data"); - console.log("[Memory][Service] get_memory_data 返回结果:", result); + // console.debug("[Memory][Service] get_memory_data 返回结果:", result); return result; } @@ -330,10 +353,10 @@ export async function getFormattedTrafficData() { const result = await invoke( "get_formatted_traffic_data", ); - console.log( - "[Traffic][Service] get_formatted_traffic_data 返回结果:", - result, - ); + // console.debug( + // "[Traffic][Service] get_formatted_traffic_data 返回结果:", + // result, + // ); return result; } @@ -342,7 +365,7 @@ export async function getFormattedMemoryData() { const result = await invoke( "get_formatted_memory_data", ); - console.log("[Memory][Service] get_formatted_memory_data 返回结果:", result); + // console.debug("[Memory][Service] get_formatted_memory_data 返回结果:", result); return result; } @@ -351,10 +374,10 @@ export async function getSystemMonitorOverview() { const result = await invoke( "get_system_monitor_overview", ); - console.log( - "[Monitor][Service] get_system_monitor_overview 返回结果:", - result, - ); + // console.debug( + // "[Monitor][Service] get_system_monitor_overview 返回结果:", + // result, + // ); return result; } @@ -377,7 +400,7 @@ export async function getSystemMonitorOverviewSafe() { // console.warn("[Monitor][Service] 数据验证失败,使用清理后的数据"); return systemMonitorValidator.sanitize(result); } - } catch (error) { + } catch { // console.error("[Monitor][Service] API调用失败:", error); // 返回安全的默认值 const { systemMonitorValidator } = await import("@/utils/data-validator"); @@ -412,6 +435,22 @@ export async function gc() { return invoke("clash_gc"); } +export async function getClashLogs() { + return invoke("get_clash_logs"); +} + +export async function startLogsMonitoring(level?: string) { + return invoke("start_logs_monitoring", { level }); +} + +export async function stopLogsMonitoring() { + return invoke("stop_logs_monitoring"); +} + +export async function clearLogs() { + return invoke("clear_logs"); +} + export async function getVergeConfig() { return invoke("get_verge_config"); } @@ -531,7 +570,7 @@ export async function cmdGetProxyDelay( // 返回一个有效的结果对象,但标记为超时 return { delay: 1e6 }; } - } catch (error) { + } catch { // 返回一个有效的结果对象,但标记为错误 return { delay: 1e6 }; } @@ -539,9 +578,11 @@ export async function cmdGetProxyDelay( /// 用于profile切换等场景 export async function forceRefreshProxies() { + const start = Date.now(); console.log("[API] 强制刷新代理缓存"); const result = await invoke("force_refresh_proxies"); - console.log("[API] 代理缓存刷新完成"); + const duration = Date.now() - start; + console.log(`[API] 代理缓存刷新完成,耗时: ${duration}ms`); return result; } @@ -625,7 +666,7 @@ export async function restoreWebDavBackup(filename: string) { export async function saveWebdavConfig( url: string, username: string, - password: String, + password: string, ) { return invoke("save_webdav_config", { url, @@ -635,7 +676,7 @@ export async function saveWebdavConfig( } export async function listWebDavBackup() { - let list: IWebDavFile[] = await invoke("list_webdav_backup"); + const list: IWebDavFile[] = await invoke("list_webdav_backup"); list.map((item) => { item.filename = item.href.split("/").pop() as string; }); diff --git a/clash-verge-rev/src/services/delay.ts b/clash-verge-rev/src/services/delay.ts index 2600833380..756dfddab2 100644 --- a/clash-verge-rev/src/services/delay.ts +++ b/clash-verge-rev/src/services/delay.ts @@ -1,4 +1,4 @@ -import { cmdGetProxyDelay } from "./cmds"; +import { cmdGetProxyDelay } from "@/services/cmds"; const hashKey = (name: string, group: string) => `${group ?? ""}::${name}`; @@ -74,7 +74,8 @@ class DelayManager { if (delay >= 0 || delay === -2) return delay; } - if (proxy.history.length > 0) { + // 添加 history 属性的安全检查 + if (proxy.history && proxy.history.length > 0) { // 0ms以error显示 return proxy.history[proxy.history.length - 1].delay || 1e6; } diff --git a/clash-verge-rev/src/services/global-log-service.ts b/clash-verge-rev/src/services/global-log-service.ts index 8e94832521..ebd62e76bf 100644 --- a/clash-verge-rev/src/services/global-log-service.ts +++ b/clash-verge-rev/src/services/global-log-service.ts @@ -1,14 +1,19 @@ // 全局日志服务,使应用在任何页面都能收集日志 import { create } from "zustand"; -import { createAuthSockette } from "@/utils/websocket"; -import dayjs from "dayjs"; + +import { + fetchLogsViaIPC, + startLogsStreaming, + stopLogsStreaming, + clearLogs as clearLogsIPC, +} from "@/services/ipc-log-service"; // 最大日志数量 const MAX_LOG_NUM = 1000; -export type LogLevel = "warning" | "info" | "debug" | "error" | "all"; +export type LogLevel = "debug" | "info" | "warning" | "error" | "all"; -export interface ILogItem { +interface ILogItem { time?: string; type: string; payload: string; @@ -24,6 +29,7 @@ interface GlobalLogStore { setCurrentLevel: (level: LogLevel) => void; clearLogs: () => void; appendLog: (log: ILogItem) => void; + setLogs: (logs: ILogItem[]) => void; } // 创建全局状态存储 @@ -43,132 +49,134 @@ export const useGlobalLogStore = create((set) => ({ : [...state.logs, log]; return { logs: newLogs }; }), + setLogs: (logs: ILogItem[]) => set({ logs }), })); -// 构建WebSocket URL -const buildWSUrl = (server: string, logLevel: LogLevel) => { - let baseUrl = `${server}/logs`; - - // 只处理日志级别参数 - if (logLevel && logLevel !== "info") { - const level = logLevel === "all" ? "debug" : logLevel; - baseUrl += `?level=${level}`; +// IPC 日志获取函数 +export const fetchLogsViaIPCPeriodically = async () => { + try { + const logs = await fetchLogsViaIPC(); + useGlobalLogStore.getState().setLogs(logs); + console.log(`[GlobalLog-IPC] 成功获取 ${logs.length} 条日志`); + } catch (error) { + console.error("[GlobalLog-IPC] 获取日志失败:", error); } - - return baseUrl; }; -// 初始化全局日志服务 -let globalLogSocket: any = null; +// 初始化全局日志服务 (仅IPC模式) +let ipcPollingInterval: number | null = null; +let isInitializing = false; // 添加初始化标志 export const initGlobalLogService = ( - server: string, - secret: string, enabled: boolean = false, logLevel: LogLevel = "info", ) => { - const { appendLog, setEnabled } = useGlobalLogStore.getState(); + // 防止重复初始化 + if (isInitializing) { + console.log("[GlobalLog-IPC] 正在初始化中,跳过重复调用"); + return; + } + + const { setEnabled, setCurrentLevel } = useGlobalLogStore.getState(); // 更新启用状态 setEnabled(enabled); + setCurrentLevel(logLevel); - // 如果不启用或没有服务器信息,则不初始化 - if (!enabled || !server) { - closeGlobalLogConnection(); - return; - } - - // 关闭现有连接 - closeGlobalLogConnection(); - - // 创建新的WebSocket连接,使用新的认证方法 - const wsUrl = buildWSUrl(server, logLevel); - console.log(`[GlobalLog] 正在连接日志服务: ${wsUrl}`); - - if (!server) { - console.warn("[GlobalLog] 服务器地址为空,无法建立连接"); - return; - } - - globalLogSocket = createAuthSockette(wsUrl, secret, { - timeout: 8000, // 8秒超时 - onmessage(event) { - try { - const data = JSON.parse(event.data) as ILogItem; - const time = dayjs().format("MM-DD HH:mm:ss"); - appendLog({ ...data, time }); - } catch (error) { - console.error("[GlobalLog] 解析日志数据失败:", error); - } - }, - onerror(event) { - console.error("[GlobalLog] WebSocket连接错误", event); - - // 记录错误状态但不关闭连接,让重连机制起作用 - useGlobalLogStore.setState({ isConnected: false }); - - // 只有在重试彻底失败后才关闭连接 - if ( - event && - typeof event === "object" && - "type" in event && - event.type === "error" - ) { - console.error("[GlobalLog] 连接已彻底失败,关闭连接"); - closeGlobalLogConnection(); - } - }, - onclose(event) { - console.log("[GlobalLog] WebSocket连接关闭", event); - useGlobalLogStore.setState({ isConnected: false }); - }, - onopen(event) { - console.log("[GlobalLog] WebSocket连接已建立", event); - useGlobalLogStore.setState({ isConnected: true }); - }, - }); -}; - -// 关闭全局日志连接 -export const closeGlobalLogConnection = () => { - if (globalLogSocket) { - globalLogSocket.close(); - globalLogSocket = null; + // 如果不启用,则不初始化 + if (!enabled) { + clearIpcPolling(); useGlobalLogStore.setState({ isConnected: false }); + return; + } + + isInitializing = true; + + // 使用IPC流式模式 + console.log("[GlobalLog-IPC] 启用IPC流式日志服务"); + + // 启动流式监控 + startLogsStreaming(logLevel); + + // 立即获取一次日志 + fetchLogsViaIPCPeriodically(); + + // 设置定期轮询来同步流式缓存的数据 + clearIpcPolling(); + ipcPollingInterval = setInterval(() => { + fetchLogsViaIPCPeriodically(); + }, 1000); // 每1秒同步一次流式缓存 + + // 设置连接状态 + useGlobalLogStore.setState({ isConnected: true }); + + isInitializing = false; +}; + +// 清除IPC轮询 +const clearIpcPolling = () => { + if (ipcPollingInterval) { + clearInterval(ipcPollingInterval); + ipcPollingInterval = null; + console.log("[GlobalLog-IPC] 轮询已停止"); } }; -// 切换日志级别 -export const changeLogLevel = ( - level: LogLevel, - server: string, - secret: string, -) => { +// 停止日志监控 (仅IPC模式) +export const stopGlobalLogMonitoring = async () => { + clearIpcPolling(); + isInitializing = false; // 重置初始化标志 + + // 调用后端停止监控 + await stopLogsStreaming(); + + useGlobalLogStore.setState({ isConnected: false }); + console.log("[GlobalLog-IPC] 日志监控已停止"); +}; + +// 关闭全局日志连接 (仅IPC模式) - 保持向后兼容 +export const closeGlobalLogConnection = async () => { + await stopGlobalLogMonitoring(); +}; + +// 切换日志级别 (仅IPC模式) +export const changeLogLevel = (level: LogLevel) => { const { enabled } = useGlobalLogStore.getState(); useGlobalLogStore.setState({ currentLevel: level }); - if (enabled && server) { - initGlobalLogService(server, secret, enabled, level); + // 如果正在初始化,则跳过,避免重复启动 + if (isInitializing) { + console.log("[GlobalLog-IPC] 正在初始化中,跳过级别变更流启动"); + return; + } + + if (enabled) { + // IPC流式模式下重新启动监控 + startLogsStreaming(level); + fetchLogsViaIPCPeriodically(); } }; -// 切换启用状态 -export const toggleLogEnabled = (server: string, secret: string) => { +// 切换启用状态 (仅IPC模式) +export const toggleLogEnabled = async () => { const { enabled, currentLevel } = useGlobalLogStore.getState(); const newEnabled = !enabled; useGlobalLogStore.setState({ enabled: newEnabled }); - if (newEnabled && server) { - initGlobalLogService(server, secret, newEnabled, currentLevel); + if (newEnabled) { + // IPC模式下直接启动 + initGlobalLogService(newEnabled, currentLevel); } else { - closeGlobalLogConnection(); + await stopGlobalLogMonitoring(); } }; -// 获取日志清理函数 +// 获取日志清理函数 - 只清理前端日志,不停止监控 export const clearGlobalLogs = () => { useGlobalLogStore.getState().clearLogs(); + // 同时清理后端缓存的日志,但不停止监控 + clearLogsIPC(); }; // 自定义钩子,用于获取过滤后的日志数据 diff --git a/clash-verge-rev/src/services/i18n.ts b/clash-verge-rev/src/services/i18n.ts index d3947ad4ee..7b6c910e2f 100644 --- a/clash-verge-rev/src/services/i18n.ts +++ b/clash-verge-rev/src/services/i18n.ts @@ -1,29 +1,61 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; -import en from "@/locales/en.json"; -import ru from "@/locales/ru.json"; -import zh from "@/locales/zh.json"; -import fa from "@/locales/fa.json"; -import tt from "@/locales/tt.json"; -import id from "@/locales/id.json"; -import ar from "@/locales/ar.json"; -import ko from "@/locales/ko.json"; -import tr from "@/locales/tr.json"; -export const languages = { en, ru, zh, fa, tt, id, ar, ko, tr }; +export const supportedLanguages = [ + "en", + "ru", + "zh", + "fa", + "tt", + "id", + "ar", + "ko", + "tr", + "de", + "es", + "jp", + "zhtw", +]; -const resources = Object.fromEntries( - Object.entries(languages).map(([key, value]) => [ - key, - { translation: value }, - ]), +export const languages: Record = supportedLanguages.reduce( + (acc, lang) => { + acc[lang] = {}; + return acc; + }, + {} as Record, ); +export const loadLanguage = async (language: string) => { + try { + const module = await import(`@/locales/${language}.json`); + return module.default; + } catch (error) { + console.warn( + `Failed to load language ${language}, fallback to zh, ${error}`, + ); + const fallback = await import("@/locales/zh.json"); + return fallback.default; + } +}; + i18n.use(initReactI18next).init({ - resources, + resources: {}, lng: "zh", fallbackLng: "zh", interpolation: { escapeValue: false, }, }); + +export const changeLanguage = async (language: string) => { + if (!i18n.hasResourceBundle(language, "translation")) { + const resources = await loadLanguage(language); + i18n.addResourceBundle(language, "translation", resources); + } + + await i18n.changeLanguage(language); +}; + +export const initializeLanguage = async (initialLanguage: string = "zh") => { + await changeLanguage(initialLanguage); +}; diff --git a/clash-verge-rev/src/services/ipc-log-service.ts b/clash-verge-rev/src/services/ipc-log-service.ts new file mode 100644 index 0000000000..6d5e29ccca --- /dev/null +++ b/clash-verge-rev/src/services/ipc-log-service.ts @@ -0,0 +1,74 @@ +// IPC-based log service using Tauri commands with streaming support +import dayjs from "dayjs"; + +import { + getClashLogs, + startLogsMonitoring, + stopLogsMonitoring, + clearLogs as clearLogsCmd, +} from "@/services/cmds"; + +type LogLevel = "debug" | "info" | "warning" | "error" | "all"; + +interface ILogItem { + time?: string; + type: string; + payload: string; + [key: string]: any; +} + +// Start logs monitoring with specified level +export const startLogsStreaming = async (logLevel: LogLevel = "info") => { + try { + const level = logLevel === "all" ? undefined : logLevel; + await startLogsMonitoring(level); + console.log( + `[IPC-LogService] Started logs monitoring with level: ${logLevel}`, + ); + } catch (error) { + console.error("[IPC-LogService] Failed to start logs monitoring:", error); + } +}; + +// Stop logs monitoring +export const stopLogsStreaming = async () => { + try { + await stopLogsMonitoring(); + console.log("[IPC-LogService] Stopped logs monitoring"); + } catch (error) { + console.error("[IPC-LogService] Failed to stop logs monitoring:", error); + } +}; + +// Fetch logs using IPC command (now from streaming cache) +export const fetchLogsViaIPC = async (): Promise => { + try { + // Server-side filtering handles the level via /logs?level={level} + // We just fetch all cached logs regardless of the logLevel parameter + const response = await getClashLogs(); + + // The response should be in the format expected by the frontend + // Transform the logs to match the expected format + if (Array.isArray(response)) { + return response.map((log: any) => ({ + ...log, + time: log.time || dayjs().format("HH:mm:ss"), + })); + } + + return []; + } catch (error) { + console.error("[IPC-LogService] Failed to fetch logs:", error); + return []; + } +}; + +// Clear logs +export const clearLogs = async () => { + try { + await clearLogsCmd(); + console.log("[IPC-LogService] Logs cleared"); + } catch (error) { + console.error("[IPC-LogService] Failed to clear logs:", error); + } +}; diff --git a/clash-verge-rev/src/services/noticeService.ts b/clash-verge-rev/src/services/noticeService.ts index f5ef04ca62..0a3505dac0 100644 --- a/clash-verge-rev/src/services/noticeService.ts +++ b/clash-verge-rev/src/services/noticeService.ts @@ -1,6 +1,6 @@ import { ReactNode } from "react"; -export interface NoticeItem { +interface NoticeItem { id: number; type: "success" | "error" | "info"; message: ReactNode; diff --git a/clash-verge-rev/src/services/types.d.ts b/clash-verge-rev/src/services/types.d.ts index b0c27d9c49..254f75904c 100644 --- a/clash-verge-rev/src/services/types.d.ts +++ b/clash-verge-rev/src/services/types.d.ts @@ -739,7 +739,6 @@ interface IProxySnellConfig extends IProxyBaseConfig { psk?: string; udp?: boolean; version?: number; - "obfs-opts"?: {}; } interface IProxyConfig extends IProxyBaseConfig, @@ -779,6 +778,8 @@ interface IProxyConfig interface IVergeConfig { app_log_level?: "trace" | "debug" | "info" | "warn" | "error" | string; + app_log_max_size?: number; // KB + app_log_max_count?: number; language?: string; tray_event?: | "main_window" @@ -822,7 +823,6 @@ interface IVergeConfig { verge_tproxy_enabled?: boolean; verge_socks_enabled?: boolean; verge_http_enabled?: boolean; - card_order?: string[]; enable_proxy_guard?: boolean; enable_bypass_check?: boolean; use_default_bypass?: boolean; @@ -861,12 +861,6 @@ interface IVergeConfig { enable_external_controller?: boolean; } -interface CardConfig { - id: string; - size: number; - enabled: boolean; -} - interface IWebDavFile { filename: string; href: string; diff --git a/clash-verge-rev/src/utils/is-async-function.ts b/clash-verge-rev/src/utils/is-async-function.ts index e865115221..4ef11c3df3 100644 --- a/clash-verge-rev/src/utils/is-async-function.ts +++ b/clash-verge-rev/src/utils/is-async-function.ts @@ -1,3 +1,3 @@ -export default function isAsyncFunction(fn: Function): boolean { +export default function isAsyncFunction(fn: (...args: any[]) => any): boolean { return fn.constructor.name === "AsyncFunction"; } diff --git a/clash-verge-rev/src/utils/notification-permission.ts b/clash-verge-rev/src/utils/notification-permission.ts deleted file mode 100644 index 226f3025e9..0000000000 --- a/clash-verge-rev/src/utils/notification-permission.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - isPermissionGranted, - requestPermission, -} from "@tauri-apps/plugin-notification"; - -export async function setupNotificationPermission() { - let permission = await isPermissionGranted(); - if (!permission) { - const result = await requestPermission(); - permission = result === "granted"; - } - if (permission) { - console.log("通知权限已授予"); - } else { - console.log("通知权限被拒绝"); - } -} diff --git a/clash-verge-rev/src/utils/traffic-diagnostics.ts b/clash-verge-rev/src/utils/traffic-diagnostics.ts index 26af3939f8..2046c6c30f 100644 --- a/clash-verge-rev/src/utils/traffic-diagnostics.ts +++ b/clash-verge-rev/src/utils/traffic-diagnostics.ts @@ -39,7 +39,7 @@ export function recordTrafficError(error: Error, component: string) { */ function getMemoryUsage(): number { if ("memory" in performance) { - // @ts-ignore - 某些浏览器支持 + // @@ts-expect-error - 某些浏览器支持 const memory = (performance as any).memory; if (memory && memory.usedJSHeapSize) { return memory.usedJSHeapSize / 1024 / 1024; // 转换为MB diff --git a/clash-verge-rev/src/utils/uri-parser.ts b/clash-verge-rev/src/utils/uri-parser.ts index 7aba40a5a5..c3a622c5fd 100644 --- a/clash-verge-rev/src/utils/uri-parser.ts +++ b/clash-verge-rev/src/utils/uri-parser.ts @@ -53,10 +53,6 @@ function trimStr(str: string | undefined): string | undefined { return str ? str.trim() : str; } -function isNotBlank(name: string) { - return name.trim().length !== 0; -} - function isIPv4(address: string): boolean { // Check if the address is IPv4 const ipv4Regex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/; @@ -64,9 +60,9 @@ function isIPv4(address: string): boolean { } function isIPv6(address: string): boolean { - // Check if the address is IPv6 + // Check if the address is IPv6 - simplified regex to avoid backreference issues const ipv6Regex = - /^((?=.*(::))(?!.*\3.+)(::)?)([0-9A-Fa-f]{1,4}(\3|:\b)|\3){7}[0-9A-Fa-f]{1,4}$/; + /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::$|^::1$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$/; return ipv6Regex.test(address); } @@ -176,7 +172,7 @@ function URI_SS(line: string): IProxyShadowsocksConfig { if (query) { if (/(&|\?)v2ray-plugin=/.test(query)) { const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/); - let v2rayPlugin = parsed![2]; + const v2rayPlugin = parsed![2]; if (v2rayPlugin) { proxy.plugin = "v2ray-plugin"; proxy["plugin-opts"] = JSON.parse( @@ -255,7 +251,7 @@ function URI_SSR(line: string): IProxyshadowsocksRConfig { serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1), ); - let params = line + const params = line .substring(splitIdx + 1) .split("/?")[0] .split(":"); @@ -352,9 +348,13 @@ function URI_VMESS(line: string): IProxyVmessConfig { params = JSON.parse(content); } catch (e) { // Shadowrocket URI format + console.warn( + "[URI_VMESS] JSON.parse(content) failed, falling back to Shadowrocket parsing:", + e, + ); const match = /(^[^?]+?)\/?\?(.*)$/.exec(line); if (match) { - let [_, base64Line, qs] = match; + const [_, base64Line, qs] = match; content = decodeBase64OrOriginal(base64Line); for (const addon of qs.split("&")) { @@ -370,7 +370,7 @@ function URI_VMESS(line: string): IProxyVmessConfig { const contentMatch = /(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content); if (contentMatch) { - let [__, cipher, uuid, server, port] = contentMatch; + const [__, cipher, uuid, server, port] = contentMatch; params.scy = cipher; params.id = uuid; @@ -432,6 +432,7 @@ function URI_VMESS(line: string): IProxyVmessConfig { transportHost = parsedHost; } } catch (e) { + console.warn("[URI_VMESS] transportHost JSON.parse failed:", e); // ignore JSON parse errors } @@ -500,14 +501,16 @@ function URI_VLESS(line: string): IProxyVlessConfig { let isShadowrocket; let parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; if (!parsed) { - let [_, base64, other] = /^(.*?)(\?.*?$)/.exec(line)!; + const [_, base64, other] = /^(.*?)(\?.*?$)/.exec(line)!; line = `${atob(base64)}${other}`; parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; isShadowrocket = true; } - let [__, uuid, server, portStr, ___, addons = "", name] = parsed; + const [, uuidRaw, server, portStr, , addons = "", nameRaw] = parsed; + let uuid = uuidRaw; + let name = nameRaw; if (isShadowrocket) { - uuid = uuid.replace(/^.*?:/g, ""); + uuid = uuidRaw.replace(/^.*?:/g, ""); } const port = parseInt(portStr, 10); @@ -597,6 +600,7 @@ function URI_VLESS(line: string): IProxyVlessConfig { proxy.network = "grpc"; break; default: { + break; } } } @@ -612,6 +616,7 @@ function URI_VLESS(line: string): IProxyVlessConfig { const parsed = JSON.parse(host); opts.headers = parsed; } catch (e) { + console.warn("[URI_VLESS] host JSON.parse failed:", e); opts.headers = { Host: host }; } } else { @@ -634,7 +639,7 @@ function URI_VLESS(line: string): IProxyVlessConfig { if (proxy.network === "ws") { proxy.servername = proxy["ws-opts"]?.headers?.Host; } else if (proxy.network === "http") { - let httpHost = proxy["http-opts"]?.headers?.Host; + const httpHost = proxy["http-opts"]?.headers?.Host; proxy.servername = Array.isArray(httpHost) ? httpHost[0] : httpHost; } } @@ -643,7 +648,7 @@ function URI_VLESS(line: string): IProxyVlessConfig { function URI_Trojan(line: string): IProxyTrojanConfig { line = line.split("trojan://")[1]; - let [__, password, server, ___, port, ____, addons = "", name] = + const [, passwordRaw, server, , port, , addons = "", nameRaw] = /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; let portNum = parseInt(`${port}`, 10); @@ -651,9 +656,11 @@ function URI_Trojan(line: string): IProxyTrojanConfig { portNum = 443; } + let password = passwordRaw; password = decodeURIComponent(password); - let decodedName = trimStr(decodeURIComponent(name)); + let name = nameRaw; + const decodedName = trimStr(decodeURIComponent(name)); name = decodedName ?? `Trojan ${server}:${portNum}`; const proxy: IProxyTrojanConfig = { @@ -667,8 +674,8 @@ function URI_Trojan(line: string): IProxyTrojanConfig { let path = ""; for (const addon of addons.split("&")) { - let [key, value] = addon.split("="); - value = decodeURIComponent(value); + const [key, valueRaw] = addon.split("="); + const value = decodeURIComponent(valueRaw); switch (key) { case "type": if (["ws", "h2"].includes(value)) { @@ -699,14 +706,17 @@ function URI_Trojan(line: string): IProxyTrojanConfig { proxy["fingerprint"] = value; break; case "encryption": - let encryption = value.split(";"); - if (encryption.length === 3) { - proxy["ss-opts"] = { - enabled: true, - method: encryption[1], - password: encryption[2], - }; + { + const encryption = value.split(";"); + if (encryption.length === 3) { + proxy["ss-opts"] = { + enabled: true, + method: encryption[1], + password: encryption[2], + }; + } } + break; case "client-fingerprint": proxy["client-fingerprint"] = value as ClientFingerprint; break; @@ -731,17 +741,17 @@ function URI_Trojan(line: string): IProxyTrojanConfig { function URI_Hysteria2(line: string): IProxyHysteria2Config { line = line.split(/(hysteria2|hy2):\/\//)[2]; - let [__, password, server, ___, port, ____, addons = "", name] = + const [, passwordRaw, server, , port, , addons = "", nameRaw] = /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; let portNum = parseInt(`${port}`, 10); if (isNaN(portNum)) { portNum = 443; } - password = decodeURIComponent(password); + const password = decodeURIComponent(passwordRaw); - let decodedName = trimStr(decodeURIComponent(name)); + const decodedName = trimStr(decodeURIComponent(nameRaw)); - name = decodedName ?? `Hysteria2 ${server}:${port}`; + const name = decodedName ?? `Hysteria2 ${server}:${port}`; const proxy: IProxyHysteria2Config = { type: "hysteria2", @@ -778,15 +788,15 @@ function URI_Hysteria2(line: string): IProxyHysteria2Config { function URI_Hysteria(line: string): IProxyHysteriaConfig { line = line.split(/(hysteria|hy):\/\//)[2]; - let [__, server, ___, port, ____, addons = "", name] = + const [, server, , port, , addons = "", nameRaw] = /^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; let portNum = parseInt(`${port}`, 10); if (isNaN(portNum)) { portNum = 443; } - let decodedName = trimStr(decodeURIComponent(name)); + const decodedName = trimStr(decodeURIComponent(nameRaw)); - name = decodedName ?? `Hysteria ${server}:${port}`; + const name = decodedName ?? `Hysteria ${server}:${port}`; const proxy: IProxyHysteriaConfig = { type: "hysteria", @@ -851,8 +861,10 @@ function URI_Hysteria(line: string): IProxyHysteriaConfig { break; case "protocol": proxy["protocol"] = value; + break; case "sni": proxy["sni"] = value; + break; default: break; } @@ -874,17 +886,17 @@ function URI_Hysteria(line: string): IProxyHysteriaConfig { function URI_TUIC(line: string): IProxyTuicConfig { line = line.split(/tuic:\/\//)[1]; - let [__, uuid, password, server, ___, port, ____, addons = "", name] = + const [, uuid, passwordRaw, server, , port, , addons = "", nameRaw] = /^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; let portNum = parseInt(`${port}`, 10); if (isNaN(portNum)) { portNum = 443; } - password = decodeURIComponent(password); - let decodedName = trimStr(decodeURIComponent(name)); + const password = decodeURIComponent(passwordRaw); + const decodedName = trimStr(decodeURIComponent(nameRaw)); - name = decodedName ?? `TUIC ${server}:${port}`; + const name = decodedName ?? `TUIC ${server}:${port}`; const proxy: IProxyTuicConfig = { type: "tuic", @@ -953,17 +965,17 @@ function URI_TUIC(line: string): IProxyTuicConfig { function URI_Wireguard(line: string): IProxyWireguardConfig { line = line.split(/(wireguard|wg):\/\//)[2]; - let [__, ___, privateKey, server, ____, port, _____, addons = "", name] = + const [, , privateKeyRaw, server, , port, , addons = "", nameRaw] = /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; let portNum = parseInt(`${port}`, 10); if (isNaN(portNum)) { portNum = 443; } - privateKey = decodeURIComponent(privateKey); - let decodedName = trimStr(decodeURIComponent(name)); + const privateKey = decodeURIComponent(privateKeyRaw); + const decodedName = trimStr(decodeURIComponent(nameRaw)); - name = decodedName ?? `WireGuard ${server}:${port}`; + const name = decodedName ?? `WireGuard ${server}:${port}`; const proxy: IProxyWireguardConfig = { type: "wireguard", name, @@ -1002,12 +1014,14 @@ function URI_Wireguard(line: string): IProxyWireguardConfig { proxy["pre-shared-key"] = value; break; case "reserved": - const parsed = value - .split(",") - .map((i) => parseInt(i.trim(), 10)) - .filter((i) => Number.isInteger(i)); - if (parsed.length === 3) { - proxy["reserved"] = parsed; + { + const parsed = value + .split(",") + .map((i) => parseInt(i.trim(), 10)) + .filter((i) => Number.isInteger(i)); + if (parsed.length === 3) { + proxy["reserved"] = parsed; + } } break; case "udp": @@ -1035,19 +1049,21 @@ function URI_Wireguard(line: string): IProxyWireguardConfig { function URI_HTTP(line: string): IProxyHttpConfig { line = line.split(/(http|https):\/\//)[2]; - let [__, ___, auth, server, ____, port, _____, addons = "", name] = + const [, , authRaw, server, , port, , addons = "", nameRaw] = /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; let portNum = parseInt(`${port}`, 10); if (isNaN(portNum)) { portNum = 443; } + let auth = authRaw; + if (auth) { auth = decodeURIComponent(auth); } - let decodedName = trimStr(decodeURIComponent(name)); + const decodedName = trimStr(decodeURIComponent(nameRaw)); - name = decodedName ?? `HTTP ${server}:${portNum}`; + const name = decodedName ?? `HTTP ${server}:${portNum}`; const proxy: IProxyHttpConfig = { type: "http", name, @@ -1099,18 +1115,20 @@ function URI_HTTP(line: string): IProxyHttpConfig { function URI_SOCKS(line: string): IProxySocks5Config { line = line.split(/socks5:\/\//)[1]; - let [__, ___, auth, server, ____, port, _____, addons = "", name] = + const [, , authRaw, server, , port, , addons = "", nameRaw] = /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; let portNum = parseInt(`${port}`, 10); if (isNaN(portNum)) { portNum = 443; } + + let auth = authRaw; if (auth) { auth = decodeURIComponent(auth); } - let decodedName = trimStr(decodeURIComponent(name)); - name = decodedName ?? `SOCKS5 ${server}:${portNum}`; + const decodedName = trimStr(decodeURIComponent(nameRaw)); + const name = decodedName ?? `SOCKS5 ${server}:${portNum}`; const proxy: IProxySocks5Config = { type: "socks5", name, diff --git a/clash-verge-rev/src/utils/websocket.ts b/clash-verge-rev/src/utils/websocket.ts deleted file mode 100644 index 624fbb54a4..0000000000 --- a/clash-verge-rev/src/utils/websocket.ts +++ /dev/null @@ -1,262 +0,0 @@ -import Sockette, { type SocketteOptions } from "sockette"; - -/** - * A wrapper of Sockette that will automatically reconnect up to `maxError` before emitting an error event. - */ -export const createSockette = ( - url: string, - opt: SocketteOptions, - maxError = 10, -) => { - let remainRetryCount = maxError; - - return new Sockette(url, { - ...opt, - // Sockette has a built-in reconnect when ECONNREFUSED feature - // Use maxError if opt.maxAttempts is not specified - maxAttempts: opt.maxAttempts ?? maxError, - onmessage(this: Sockette, ev) { - remainRetryCount = maxError; // reset counter - opt.onmessage?.call(this, ev); - }, - onerror(this: Sockette, ev) { - remainRetryCount -= 1; - - if (remainRetryCount >= 0) { - if (this instanceof Sockette) { - this.close(); - this.reconnect(); - } - } else { - opt.onerror?.call(this, ev); - } - }, - onmaximum(this: Sockette, ev) { - opt.onmaximum?.call(this, ev); - // onmaximum will be fired when Sockette reaches built-in reconnect limit, - // We will also set remainRetryCount to 0 to prevent further reconnect. - remainRetryCount = 0; - }, - }); -}; - -/** - * 创建一个支持认证的WebSocket连接 - * 使用标准的URL参数方式添加token - * - * 注意:mihomo服务器对WebSocket的认证支持不佳,使用URL参数方式传递token - */ -export const createAuthSockette = ( - baseUrl: string, - secret: string, - opt: SocketteOptions, - maxError = 10, -) => { - // 确保baseUrl格式正确 - let url = baseUrl; - if (!url.startsWith("ws://") && !url.startsWith("wss://")) { - url = `ws://${url}`; - } - - // 重试控制 - let reconnectAttempts = 0; - const MAX_RECONNECT = maxError; - let reconnectTimeout: any = null; - let ws: WebSocket | null = null; - - // 使用URL API解析和构建URL - try { - const urlObj = new URL(url); - - // 添加token参数(如果有secret) - if (secret) { - urlObj.searchParams.delete("token"); - urlObj.searchParams.append("token", secret); - } - - url = urlObj.toString(); - console.log(`[WebSocket] 创建连接: ${url.replace(secret || "", "***")}`); - } catch (e) { - console.error(`[WebSocket] URL格式错误: ${url}`, e); - if (opt.onerror) { - // 使用任意类型避免类型错误 - const anyOpt = opt as any; - anyOpt.onerror( - new ErrorEvent("error", { message: `URL格式错误: ${e}` } as any), - ); - } - return createDummySocket(); - } - - function connect() { - try { - ws = new WebSocket(url); - - ws.onopen = function (event) { - console.log( - `[WebSocket] 连接成功: ${url.replace(secret || "", "***")}`, - ); - reconnectAttempts = 0; // 重置重连计数 - if (opt.onopen) { - // 使用任意类型避免类型错误 - const anyOpt = opt as any; - anyOpt.onopen(event); - } - }; - - ws.onmessage = function (event) { - if (opt.onmessage) { - // 使用任意类型避免类型错误 - const anyOpt = opt as any; - anyOpt.onmessage(event); - } - }; - - ws.onerror = function (event) { - console.error( - `[WebSocket] 连接错误: ${url.replace(secret || "", "***")}`, - ); - // 错误处理 - if (reconnectAttempts < MAX_RECONNECT) { - scheduleReconnect(); - } else if (opt.onerror) { - // 使用任意类型避免类型错误 - const anyOpt = opt as any; - anyOpt.onerror(event); - } - }; - - ws.onclose = function (event) { - console.log( - `[WebSocket] 连接关闭: ${url.replace(secret || "", "***")}, 代码: ${event.code}`, - ); - - // 如果不是正常关闭(1000, 1001),尝试重连 - if ( - event.code !== 1000 && - event.code !== 1001 && - reconnectAttempts < MAX_RECONNECT - ) { - scheduleReconnect(); - } else { - if (opt.onclose) { - // 使用任意类型避免类型错误 - const anyOpt = opt as any; - anyOpt.onclose(event); - } - - // 如果已达到最大重试次数 - if (reconnectAttempts >= MAX_RECONNECT && opt.onmaximum) { - console.error( - `[WebSocket] 达到最大重试次数: ${url.replace(secret || "", "***")}`, - ); - const anyOpt = opt as any; - anyOpt.onmaximum(event); - } - } - }; - } catch (error) { - console.error(`[WebSocket] 创建连接失败:`, error); - if (opt.onerror) { - // 使用任意类型避免类型错误 - const anyOpt = opt as any; - anyOpt.onerror( - new ErrorEvent("error", { message: `创建连接失败: ${error}` } as any), - ); - } - } - } - - function scheduleReconnect() { - if (reconnectTimeout) { - clearTimeout(reconnectTimeout); - } - - reconnectAttempts++; - const delay = Math.min(1000 * Math.pow(1.5, reconnectAttempts), 10000); // 指数退避,最大10秒 - - console.log( - `[WebSocket] 计划重连 (${reconnectAttempts}/${MAX_RECONNECT}) 延迟: ${delay}ms`, - ); - - reconnectTimeout = setTimeout(() => { - console.log( - `[WebSocket] 尝试重连 (${reconnectAttempts}/${MAX_RECONNECT})`, - ); - cleanup(); - connect(); - }, delay); - } - - function cleanup() { - if (ws) { - // 移除所有事件监听器 - ws.onopen = null; - ws.onmessage = null; - ws.onerror = null; - ws.onclose = null; - - // 如果连接仍然打开,关闭它 - if ( - ws.readyState === WebSocket.OPEN || - ws.readyState === WebSocket.CONNECTING - ) { - try { - ws.close(); - } catch (e) { - console.error("[WebSocket] 关闭连接时出错:", e); - } - } - - ws = null; - } - - // 清除重连计时器 - if (reconnectTimeout) { - clearTimeout(reconnectTimeout); - reconnectTimeout = null; - } - } - - // 创建一个类似Sockette的接口对象 - const socketLike = { - ws, - close: () => { - console.log( - `[WebSocket] 手动关闭连接: ${url.replace(secret || "", "***")}`, - ); - cleanup(); - }, - reconnect: () => { - cleanup(); - connect(); - }, - json: (data: any) => { - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify(data)); - } - }, - send: (data: string) => { - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(data); - } - }, - open: connect, - }; - - // 立即连接 - connect(); - - return socketLike; -}; - -// 创建一个空的WebSocket对象 -function createDummySocket() { - return { - close: () => {}, - reconnect: () => {}, - json: () => {}, - send: () => {}, - open: () => {}, - }; -} diff --git a/clash-verge-rev/vite.config.mts b/clash-verge-rev/vite.config.mts index 66099eb9a6..b33edbca13 100644 --- a/clash-verge-rev/vite.config.mts +++ b/clash-verge-rev/vite.config.mts @@ -17,8 +17,8 @@ export default defineConfig({ svgr(), react(), legacy({ + targets: ["edge>=109", "safari>=13"], renderLegacyChunks: false, - modernTargets: ["edge>=109", "safari>=13"], modernPolyfills: true, additionalModernPolyfills: [ "core-js/modules/es.object.has-own.js", @@ -42,40 +42,81 @@ export default defineConfig({ build: { outDir: "../dist", emptyOutDir: true, - target: "es2020", minify: "terser", chunkSizeWarningLimit: 4000, reportCompressedSize: false, sourcemap: false, cssCodeSplit: true, cssMinify: true, + terserOptions: { + compress: { + drop_console: false, + drop_debugger: true, + pure_funcs: ["console.debug", "console.trace"], + dead_code: true, + unused: true, + }, + mangle: { + safari10: true, + }, + }, rollupOptions: { treeshake: { preset: "recommended", - moduleSideEffects: (id) => !/\.css$/.test(id), + moduleSideEffects: (id) => !id.endsWith(".css"), tryCatchDeoptimization: false, }, output: { compact: true, - experimentalMinChunkSize: 30000, + experimentalMinChunkSize: 100000, dynamicImportInCjs: true, manualChunks(id) { if (id.includes("node_modules")) { // Monaco Editor should be a separate chunk if (id.includes("monaco-editor")) return "monaco-editor"; - // React-related libraries (react, react-dom, react-router-dom, etc.) + // React core libraries if ( id.includes("react") || id.includes("react-dom") || - id.includes("react-router-dom") || + id.includes("react-router-dom") + ) { + return "react-core"; + } + + // React UI libraries + if ( id.includes("react-transition-group") || id.includes("react-error-boundary") || id.includes("react-hook-form") || id.includes("react-markdown") || id.includes("react-virtuoso") ) { - return "react"; + return "react-ui"; + } + + // Material UI libraries (grouped together) + if ( + id.includes("@mui/material") || + id.includes("@mui/icons-material") || + id.includes("@mui/lab") || + id.includes("@mui/x-data-grid") + ) { + return "mui"; + } + + // Tauri-related plugins: grouping together Tauri plugins + if ( + id.includes("@tauri-apps/api") || + id.includes("@tauri-apps/plugin-clipboard-manager") || + id.includes("@tauri-apps/plugin-dialog") || + id.includes("@tauri-apps/plugin-fs") || + id.includes("@tauri-apps/plugin-global-shortcut") || + id.includes("@tauri-apps/plugin-process") || + id.includes("@tauri-apps/plugin-shell") || + id.includes("@tauri-apps/plugin-updater") + ) { + return "tauri-plugins"; } // Utilities chunk: group commonly used utility libraries @@ -91,37 +132,22 @@ export default defineConfig({ return "utils"; } - // Tauri-related plugins: grouping together Tauri plugins - if ( - id.includes("@tauri-apps/api") || - id.includes("@tauri-apps/plugin-clipboard-manager") || - id.includes("@tauri-apps/plugin-dialog") || - id.includes("@tauri-apps/plugin-fs") || - id.includes("@tauri-apps/plugin-global-shortcut") || - id.includes("@tauri-apps/plugin-notification") || - id.includes("@tauri-apps/plugin-process") || - id.includes("@tauri-apps/plugin-shell") || - id.includes("@tauri-apps/plugin-updater") - ) { - return "tauri-plugins"; + // Group other vendor packages together to reduce small chunks + const pkg = id.match(/node_modules\/([^/]+)/)?.[1]; + if (pkg) { + // Large packages get their own chunks + if ( + pkg.includes("monaco") || + pkg.includes("lodash") || + pkg.includes("antd") || + pkg.includes("emotion") + ) { + return `vendor-${pkg}`; + } + + // Group all other packages together + return "vendor"; } - - // Material UI libraries (grouped together) - if ( - id.includes("@mui/material") || - id.includes("@mui/icons-material") || - id.includes("@mui/lab") || - id.includes("@mui/x-data-grid") - ) { - return "mui"; - } - - // Small vendor packages - const pkg = id.match(/node_modules\/([^\/]+)/)?.[1]; - if (pkg && pkg.length < 8) return "small-vendors"; - - // Large vendor packages - return "large-vendor"; } }, }, @@ -133,13 +159,7 @@ export default defineConfig({ "@root": path.resolve("."), }, }, - css: { - preprocessorOptions: { - scss: { - api: "modern-compiler", - }, - }, - }, + define: { OS_PLATFORM: `"${process.platform}"`, }, diff --git a/lede/target/linux/generic/backport-6.6/703-v6.10-flow_offload-add-control-flag-checking-helpers.patch b/lede/target/linux/generic/backport-6.6/703-v6.10-flow_offload-add-control-flag-checking-helpers.patch new file mode 100644 index 0000000000..457dac16d4 --- /dev/null +++ b/lede/target/linux/generic/backport-6.6/703-v6.10-flow_offload-add-control-flag-checking-helpers.patch @@ -0,0 +1,101 @@ +From d11e63119432bdb55065d094cb6fd37e9147c70d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Asbj=C3=B8rn=20Sloth=20T=C3=B8nnesen?= +Date: Thu, 11 Apr 2024 10:52:54 +0000 +Subject: [PATCH] flow_offload: add control flag checking helpers +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +These helpers aim to help drivers, with checking +for the presence of unsupported control flags. + +For drivers supporting at least one control flag: + flow_rule_is_supp_control_flags() + +For drivers using flow_rule_match_control(), but not using flags: + flow_rule_has_control_flags() + +For drivers not using flow_rule_match_control(): + flow_rule_match_has_control_flags() + +While primarily aimed at FLOW_DISSECTOR_KEY_CONTROL +and flow_rule_match_control(), then the first two +can also be used with FLOW_DISSECTOR_KEY_ENC_CONTROL +and flow_rule_match_enc_control(). + +These helpers mirrors the existing check done in sfc: + drivers/net/ethernet/sfc/tc.c +276 + +Only compile-tested. + +Signed-off-by: Asbjørn Sloth Tønnesen +Reviewed-by: Louis Peens +Signed-off-by: David S. Miller +--- + include/net/flow_offload.h | 55 ++++++++++++++++++++++++++++++++++++++ + 1 file changed, 55 insertions(+) + +--- a/include/net/flow_offload.h ++++ b/include/net/flow_offload.h +@@ -449,6 +449,61 @@ static inline bool flow_rule_match_key(c + return dissector_uses_key(rule->match.dissector, key); + } + ++/** ++ * flow_rule_is_supp_control_flags() - check for supported control flags ++ * @supp_flags: control flags supported by driver ++ * @ctrl_flags: control flags present in rule ++ * @extack: The netlink extended ACK for reporting errors. ++ * ++ * Return: true if only supported control flags are set, false otherwise. ++ */ ++static inline bool flow_rule_is_supp_control_flags(const u32 supp_flags, ++ const u32 ctrl_flags, ++ struct netlink_ext_ack *extack) ++{ ++ if (likely((ctrl_flags & ~supp_flags) == 0)) ++ return true; ++ ++ NL_SET_ERR_MSG_FMT_MOD(extack, ++ "Unsupported match on control.flags %#x", ++ ctrl_flags); ++ ++ return false; ++} ++ ++/** ++ * flow_rule_has_control_flags() - check for presence of any control flags ++ * @ctrl_flags: control flags present in rule ++ * @extack: The netlink extended ACK for reporting errors. ++ * ++ * Return: true if control flags are set, false otherwise. ++ */ ++static inline bool flow_rule_has_control_flags(const u32 ctrl_flags, ++ struct netlink_ext_ack *extack) ++{ ++ return !flow_rule_is_supp_control_flags(0, ctrl_flags, extack); ++} ++ ++/** ++ * flow_rule_match_has_control_flags() - match and check for any control flags ++ * @rule: The flow_rule under evaluation. ++ * @extack: The netlink extended ACK for reporting errors. ++ * ++ * Return: true if control flags are set, false otherwise. ++ */ ++static inline bool flow_rule_match_has_control_flags(struct flow_rule *rule, ++ struct netlink_ext_ack *extack) ++{ ++ struct flow_match_control match; ++ ++ if (!flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_CONTROL)) ++ return false; ++ ++ flow_rule_match_control(rule, &match); ++ ++ return flow_rule_has_control_flags(match.mask->flags, extack); ++} ++ + struct flow_stats { + u64 pkts; + u64 bytes; diff --git a/lede/target/linux/generic/backport-6.6/704-v6.12-ipv6-Add-ipv6_addr_-cpu_to_be32-be32_to_cpu-helpers.patch b/lede/target/linux/generic/backport-6.6/704-v6.12-ipv6-Add-ipv6_addr_-cpu_to_be32-be32_to_cpu-helpers.patch new file mode 100644 index 0000000000..2f4eec6953 --- /dev/null +++ b/lede/target/linux/generic/backport-6.6/704-v6.12-ipv6-Add-ipv6_addr_-cpu_to_be32-be32_to_cpu-helpers.patch @@ -0,0 +1,40 @@ +From f40a455d01f80c6638be382d75cb1c4e7748d8af Mon Sep 17 00:00:00 2001 +From: Simon Horman +Date: Tue, 13 Aug 2024 14:33:47 +0100 +Subject: [PATCH] ipv6: Add ipv6_addr_{cpu_to_be32,be32_to_cpu} helpers + +Add helpers to convert an ipv6 addr, expressed as an array +of words, from CPU to big-endian byte order, and vice versa. + +No functional change intended. +Compile tested only. + +Suggested-by: Andrew Lunn +Link: https://lore.kernel.org/netdev/c7684349-535c-45a4-9a74-d47479a50020@lunn.ch/ +Reviewed-by: Andrew Lunn +Signed-off-by: Simon Horman +Link: https://patch.msgid.link/20240813-ipv6_addr-helpers-v2-1-5c974f8cca3e@kernel.org +Signed-off-by: Jakub Kicinski +--- + include/net/ipv6.h | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +--- a/include/net/ipv6.h ++++ b/include/net/ipv6.h +@@ -1382,4 +1382,16 @@ static inline void ip6_sock_set_recvpkti + release_sock(sk); + } + ++#define IPV6_ADDR_WORDS 4 ++ ++static inline void ipv6_addr_cpu_to_be32(__be32 *dst, const u32 *src) ++{ ++ cpu_to_be32_array(dst, src, IPV6_ADDR_WORDS); ++} ++ ++static inline void ipv6_addr_be32_to_cpu(u32 *dst, const __be32 *src) ++{ ++ be32_to_cpu_array(dst, src, IPV6_ADDR_WORDS); ++} ++ + #endif /* _NET_IPV6_H */ diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index d3b16f760b..ba9cdfa226 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -7,7 +7,6 @@ local has_xray = api.finded_com("xray") local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist") local has_chnlist = fs.access("/usr/share/passwall/rules/chnlist") local has_chnroute = fs.access("/usr/share/passwall/rules/chnroute") -local chinadns_tls = os.execute("chinadns-ng -V | grep -i wolfssl >/dev/null") m = Map(appname) api.set_apply_on_parse(m) @@ -91,33 +90,6 @@ local doh_validate = function(self, value, t) return nil, translatef("%s request address","DoH") .. " " .. translate("Format must be:") .. " URL,IP" end -local chinadns_dot_validate = function(self, value, t) - local function isValidDoTString(s) - if s:sub(1, 6) ~= "tls://" then return false end - local address = s:sub(7) - local at_index = address:find("@") - local hash_index = address:find("#") - local ip, port - local domain = at_index and address:sub(1, at_index - 1) or nil - ip = at_index and address:sub(at_index + 1, (hash_index or 0) - 1) or address:sub(1, (hash_index or 0) - 1) - port = hash_index and address:sub(hash_index + 1) or nil - local num_port = tonumber(port) - if (port and (not num_port or num_port <= 0 or num_port >= 65536)) or - (domain and domain == "") or - (not datatypes.ipaddr(ip) and not datatypes.ip6addr(ip)) then - return false - end - return true - end - value = value:gsub("%s+", "") - if value ~= "" then - if isValidDoTString(value) then - return value - end - end - return nil, translatef("%s request address","DoT") .. " " .. translate("Format must be:") .. " tls://" .. translate("Domain") .. "@IP[#Port] | tls://IP[#Port]" -end - m:append(Template(appname .. "/global/status")) s = m:section(TypedSection, "global") @@ -331,11 +303,6 @@ o = s:taboption("DNS", ListValue, "direct_dns_mode", translate("Direct DNS") .. o:value("", translate("Auto")) o:value("udp", translatef("Requery DNS By %s", "UDP")) o:value("tcp", translatef("Requery DNS By %s", "TCP")) -if chinadns_tls == 0 then - o:value("dot", translatef("Requery DNS By %s", "DoT")) -end ---TO DO ---o:value("doh", "DoH") o:depends({dns_shunt = "dnsmasq"}) o:depends({dns_shunt = "chinadns-ng"}) @@ -364,19 +331,6 @@ o:value("114.114.115.115") o:value("119.28.28.28") o:depends("direct_dns_mode", "tcp") -o = s:taboption("DNS", Value, "direct_dns_dot", translate("Direct DNS DoT")) -o.default = "tls://dot.pub@1.12.12.12" -o:value("tls://dot.pub@1.12.12.12") -o:value("tls://dot.pub@120.53.53.53") -o:value("tls://dot.360.cn@36.99.170.86") -o:value("tls://dot.360.cn@101.198.191.4") -o:value("tls://dns.alidns.com@223.5.5.5") -o:value("tls://dns.alidns.com@223.6.6.6") -o:value("tls://dns.alidns.com@2400:3200::1") -o:value("tls://dns.alidns.com@2400:3200:baba::1") -o.validate = chinadns_dot_validate -o:depends("direct_dns_mode", "dot") - o = s:taboption("DNS", Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature.")) o.default = "0" @@ -387,9 +341,6 @@ o = s:taboption("DNS", ListValue, "dns_mode", translate("Filter Mode"), "") o:value("udp", translatef("Requery DNS By %s", "UDP")) o:value("tcp", translatef("Requery DNS By %s", "TCP")) -if chinadns_tls == 0 then - o:value("dot", translatef("Requery DNS By %s", "DoT")) -end if api.is_finded("dns2socks") then o:value("dns2socks", "dns2socks") end @@ -519,22 +470,6 @@ o:depends({xray_dns_mode = "tcp"}) o:depends({xray_dns_mode = "tcp+doh"}) o:depends({singbox_dns_mode = "tcp"}) ----- DoT -o = s:taboption("DNS", Value, "remote_dns_dot", translate("Remote DNS DoT")) -o.default = "tls://one.one.one.one@1.1.1.1" -o:value("tls://one.one.one.one@1.0.0.1", "1.0.0.1 (CloudFlare)") -o:value("tls://one.one.one.one@1.1.1.1", "1.1.1.1 (CloudFlare)") -o:value("tls://dns.google@8.8.4.4", "8.8.4.4 (Google)") -o:value("tls://dns.google@8.8.8.8", "8.8.8.8 (Google)") -o:value("tls://dns.quad9.net@9.9.9.9", "9.9.9.9 (Quad9)") -o:value("tls://dns.quad9.net@149.112.112.112", "149.112.112.112 (Quad9)") -o:value("tls://dns.adguard.com@94.140.14.14", "94.140.14.14 (AdGuard)") -o:value("tls://dns.adguard.com@94.140.15.15", "94.140.15.15 (AdGuard)") -o:value("tls://dns.opendns.com@208.67.222.222", "208.67.222.222 (OpenDNS)") -o:value("tls://dns.opendns.com@208.67.220.220", "208.67.220.220 (OpenDNS)") -o.validate = chinadns_dot_validate -o:depends("dns_mode", "dot") - ---- DoH o = s:taboption("DNS", Value, "remote_dns_doh", translate("Remote DNS DoH")) o.default = "https://1.1.1.1/dns-query" @@ -617,11 +552,6 @@ if api.is_finded("smartdns") then o:depends({dns_shunt = "smartdns"}) end -o = s:taboption("DNS", Flag, "chinadns_ng_cert_verify", translate("DoT Cert verify"), translate("Verify DoT SSL cert. (May fail on some platforms!)")) -o.default = "0" -o:depends({direct_dns_mode = "dot"}) -o:depends({dns_mode = "dot"}) - o = s:taboption("DNS", Flag, "dns_redirect", translate("DNS Redirect"), translate("Force special DNS server to need proxy devices.")) o.default = "1" o.rmempty = false diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 131b771687..d84cc5a573 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -909,7 +909,6 @@ function gen_config(var) local direct_dns_port = var["-direct_dns_port"] local direct_dns_udp_server = var["-direct_dns_udp_server"] local direct_dns_tcp_server = var["-direct_dns_tcp_server"] - local direct_dns_dot_server = var["-direct_dns_dot_server"] local direct_dns_query_strategy = var["-direct_dns_query_strategy"] local remote_dns_server = var["-remote_dns_server"] local remote_dns_port = var["-remote_dns_port"] @@ -1639,7 +1638,7 @@ function gen_config(var) end local direct_strategy = "prefer_ipv6" - if direct_dns_udp_server or direct_dns_tcp_server or direct_dns_dot_server then + if direct_dns_udp_server or direct_dns_tcp_server then if direct_dns_query_strategy == "UseIPv4" then direct_strategy = "ipv4_only" elseif direct_dns_query_strategy == "UseIPv6" then @@ -1667,13 +1666,6 @@ function gen_config(var) elseif direct_dns_tcp_server then port = tonumber(direct_dns_port) or 53 direct_dns_server = "tcp://" .. direct_dns_tcp_server .. ":" .. port - elseif direct_dns_dot_server then - port = tonumber(direct_dns_port) or 853 - if direct_dns_dot_server:find(":") == nil then - direct_dns_server = "tls://" .. direct_dns_dot_server .. ":" .. port - else - direct_dns_server = "tls://[" .. direct_dns_dot_server .. "]:" .. port - end end table.insert(dns.servers, { @@ -1693,10 +1685,6 @@ function gen_config(var) port = tonumber(direct_dns_port) or 53 direct_dns_server = direct_dns_tcp_server type = "tcp" - elseif direct_dns_dot_server then - port = tonumber(direct_dns_port) or 853 - direct_dns_server = direct_dns_dot_server - type = "tls" end table.insert(dns.servers, { diff --git a/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po b/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po index 83cc18e5c8..b9d9c37c34 100644 --- a/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po +++ b/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po @@ -139,9 +139,6 @@ msgstr "TCP 节点必须是 '%s' 类型才能使用 FakeDNS。" msgid "Direct DNS" msgstr "直连 DNS" -msgid "Direct DNS DoT" -msgstr "直连 DNS DoT" - msgid "Remote DNS" msgstr "远程 DNS" @@ -172,9 +169,6 @@ msgstr "请求协议" msgid "Remote DNS DoH" msgstr "远程 DNS DoH" -msgid "Remote DNS DoT" -msgstr "远程 DNS DoT" - msgid "Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address)." msgstr "用于 DNS 查询时通知 DNS 服务器,客户端所在的地理位置(不能是私有 IP 地址)。" @@ -241,12 +235,6 @@ msgstr "清空 IPSET" msgid "Clear NFTSET" msgstr "清空 NFTSET" -msgid "DoT Cert verify" -msgstr "DoT 证书验证" - -msgid "Verify DoT SSL cert. (May fail on some platforms!)" -msgstr "验证 DoT SSL 证书。(在某些平台可能无法验证,谨慎开启!)" - msgid "Try this feature if the rule modification does not take effect." msgstr "如果修改规则后没有生效,请尝试此功能。" diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh index 2bd4245fc8..7db4a4bb5c 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh @@ -425,7 +425,7 @@ run_ipt2socks() { run_singbox() { local flag type node tcp_redir_port tcp_proxy_way udp_redir_port socks_address socks_port socks_username socks_password http_address http_port http_username http_password - local dns_listen_port direct_dns_query_strategy direct_dns_port direct_dns_udp_server direct_dns_tcp_server direct_dns_dot_server remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy dns_cache dns_socks_address dns_socks_port + local dns_listen_port direct_dns_query_strategy direct_dns_port direct_dns_udp_server direct_dns_tcp_server remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy dns_cache dns_socks_address dns_socks_port local loglevel log_file config_file server_host server_port no_run local _extra_param="" eval_set_val $@ @@ -471,9 +471,6 @@ run_singbox() { elif [ -n "$direct_dns_tcp_server" ]; then direct_dns_port=$(echo ${direct_dns_tcp_server} | awk -F '#' '{print $2}') _extra_param="${_extra_param} -direct_dns_tcp_server $(echo ${direct_dns_tcp_server} | awk -F '#' '{print $1}')" - elif [ -n "$direct_dns_dot_server" ]; then - direct_dns_port=$(echo ${direct_dns_dot_server} | awk -F '#' '{print $2}') - _extra_param="${_extra_param} -direct_dns_dot_server $(echo ${direct_dns_dot_server} | awk -F '#' '{print $1}')" else local local_dns=$(echo -n $(echo "${LOCAL_DNS}" | sed "s/,/\n/g" | head -n1) | tr " " ",") _extra_param="${_extra_param} -direct_dns_udp_server $(echo ${local_dns} | awk -F '#' '{print $1}')" @@ -958,12 +955,6 @@ run_redir() { tcp) _args="${_args} direct_dns_tcp_server=$(config_t_get global direct_dns_tcp 223.5.5.5 | sed 's/:/#/g')" ;; - dot) - local tmp_dot_dns=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12") - local tmp_dot_ip=$(echo "$tmp_dot_dns" | sed -n 's/.*:\/\/\([^@#]*@\)*\([^@#]*\).*/\2/p') - local tmp_dot_port=$(echo "$tmp_dot_dns" | sed -n 's/.*#\([0-9]\+\).*/\1/p') - _args="${_args} direct_dns_dot_server=$tmp_dot_ip#${tmp_dot_port:-853}" - ;; esac _args="${_args} remote_dns_protocol=${v2ray_dns_mode}" @@ -1420,7 +1411,6 @@ stop_crontab() { start_dns() { echolog "DNS域名解析:" - local chinadns_tls=$($(first_type chinadns-ng) -V | grep -i wolfssl) local china_ng_local_dns=$(IFS=','; set -- $LOCAL_DNS; [ "${1%%[#:]*}" = "127.0.0.1" ] && echo "$1" || ([ -n "$2" ] && echo "$1,$2" || echo "$1")) local sing_box_local_dns= local direct_dns_mode=$(config_t_get global direct_dns_mode "auto") @@ -1456,29 +1446,6 @@ start_dns() { NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1) } ;; - dot) - if [ "$chinadns_tls" != "nil" ]; then - local DIRECT_DNS=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12") - local cert_verify=$([ "$(config_t_get global chinadns_ng_cert_verify 0)" = "1" ] && echo "--cert-verify") - china_ng_local_dns=${DIRECT_DNS} - - #当全局(包括访问控制节点)开启chinadns-ng时,不启动新进程。 - [ "$DNS_SHUNT" != "chinadns-ng" ] || [ "$ACL_RULE_DNSMASQ" = "1" ] && { - LOCAL_DNS="127.0.0.1#${NEXT_DNS_LISTEN_PORT}" - ln_run "$(first_type chinadns-ng)" chinadns-ng "/dev/null" -b :: -l ${NEXT_DNS_LISTEN_PORT} -c ${DIRECT_DNS} -d chn ${cert_verify} - echolog " - ChinaDNS-NG(${LOCAL_DNS}) -> ${DIRECT_DNS}" - echolog " * 请确保上游直连 DNS 支持 DoT 查询。" - NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1) - } - - local tmp_dot_ip=$(echo "$DIRECT_DNS" | sed -n 's/.*:\/\/\([^@#]*@\)*\([^@#]*\).*/\2/p') - local tmp_dot_port=$(echo "$DIRECT_DNS" | sed -n 's/.*#\([0-9]\+\).*/\1/p') - DIRECT_DNS=$tmp_dot_ip#${tmp_dot_port:-853} - sing_box_local_dns="direct_dns_dot_server=${DIRECT_DNS}" - else - echolog " - 你的ChinaDNS-NG版本不支持DoT,直连DNS将使用默认地址。" - fi - ;; auto) #Automatic logic is already done by default : @@ -1577,32 +1544,6 @@ start_dns() { run_xray ${_args} } ;; - dot) - TCP_PROXY_DNS=1 - if [ "$chinadns_tls" != "nil" ]; then - local china_ng_listen_port=${NEXT_DNS_LISTEN_PORT} - local china_ng_trust_dns=$(config_t_get global remote_dns_dot "tls://one.one.one.one@1.1.1.1") - local cert_verify=$([ "$(config_t_get global chinadns_ng_cert_verify 0)" = "1" ] && echo "--cert-verify") - local tmp_dot_ip=$(echo "$china_ng_trust_dns" | sed -n 's/.*:\/\/\([^@#]*@\)*\([^@#]*\).*/\2/p') - local tmp_dot_port=$(echo "$china_ng_trust_dns" | sed -n 's/.*#\([0-9]\+\).*/\1/p') - REMOTE_DNS="$tmp_dot_ip#${tmp_dot_port:-853}" - [ "$DNS_SHUNT" != "chinadns-ng" ] && { - [ "$FILTER_PROXY_IPV6" = "1" ] && DNSMASQ_FILTER_PROXY_IPV6=0 && local no_ipv6_trust="-N" - ln_run "$(first_type chinadns-ng)" chinadns-ng "/dev/null" -b :: -l ${china_ng_listen_port} -t ${china_ng_trust_dns} -d gfw ${no_ipv6_trust} ${cert_verify} - echolog " - ChinaDNS-NG(${TUN_DNS}) -> ${china_ng_trust_dns}" - } - else - echolog " - 你的ChinaDNS-NG版本不支持DoT,远程DNS将默认使用tcp://1.1.1.1" - REMOTE_DNS="1.1.1.1" - local china_ng_listen_port=${NEXT_DNS_LISTEN_PORT} - local china_ng_trust_dns="tcp://${REMOTE_DNS}" - [ "$DNS_SHUNT" != "chinadns-ng" ] && { - [ "$FILTER_PROXY_IPV6" = "1" ] && DNSMASQ_FILTER_PROXY_IPV6=0 && local no_ipv6_trust="-N" - ln_run "$(first_type chinadns-ng)" chinadns-ng "/dev/null" -b :: -l ${china_ng_listen_port} -t ${china_ng_trust_dns} -d gfw ${no_ipv6_trust} - echolog " - ChinaDNS-NG(${TUN_DNS}) -> ${china_ng_trust_dns}" - } - fi - ;; udp) UDP_PROXY_DNS=1 local china_ng_listen_port=${NEXT_DNS_LISTEN_PORT} @@ -1631,7 +1572,7 @@ start_dns() { [ -n "${resolve_dns_log}" ] && echolog " - ${resolve_dns_log}" - [ -n "${TCP_PROXY_DNS}" ] && echolog " * 请确认上游 DNS 支持 TCP/DoT/DoH 查询,如非直连地址,确保 TCP 代理打开,并且已经正确转发!" + [ -n "${TCP_PROXY_DNS}" ] && echolog " * 请确认上游 DNS 支持 TCP/DoH 查询,如非直连地址,确保 TCP 代理打开,并且已经正确转发!" [ -n "${UDP_PROXY_DNS}" ] && echolog " * 请确认上游 DNS 支持 UDP 查询并已使用 UDP 节点,如上游 DNS 非直连地址,确保 UDP 代理打开,并且已经正确转发!" [ "${DNS_SHUNT}" = "smartdns" ] && { @@ -1929,11 +1870,6 @@ acl_app() { tcp) _chinadns_local_dns="tcp://$(config_t_get global direct_dns_tcp 223.5.5.5 | sed 's/:/#/g')" ;; - dot) - if [ "$($(first_type chinadns-ng) -V | grep -i wolfssl)" != "nil" ]; then - _chinadns_local_dns=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12") - fi - ;; esac run_chinadns_ng \ diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua index 2ea4164e76..c17dde63d2 100644 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua @@ -504,11 +504,6 @@ end table.insert(config_lines, "hosts") -local cert_verify = uci:get(appname, "@global[0]", "chinadns_ng_cert_verify") or 0 -if tonumber(cert_verify) == 1 then - table.insert(config_lines, "cert-verify") -end - if DEFAULT_TAG == "chn" then log(string.format(" - 默认 DNS :%s", DNS_LOCAL)) elseif DEFAULT_TAG == "gfw" then diff --git a/sing-box/dns/transport/https.go b/sing-box/dns/transport/https.go index ea2998eb4b..da065505b9 100644 --- a/sing-box/dns/transport/https.go +++ b/sing-box/dns/transport/https.go @@ -25,7 +25,6 @@ import ( "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - aTLS "github.com/sagernet/sing/common/tls" sHTTP "github.com/sagernet/sing/protocol/http" mDNS "github.com/miekg/dns" @@ -126,19 +125,11 @@ func NewHTTPSRaw( ) *HTTPSTransport { var transport *http.Transport if tlsConfig != nil { + tlsDialer := tls.NewDialer(dialer, tlsConfig) transport = &http.Transport{ ForceAttemptHTTP2: true, DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - tcpConn, hErr := dialer.DialContext(ctx, network, serverAddr) - if hErr != nil { - return nil, hErr - } - tlsConn, hErr := aTLS.ClientHandshake(ctx, tcpConn, tlsConfig) - if hErr != nil { - tcpConn.Close() - return nil, hErr - } - return tlsConn, nil + return tlsDialer.DialContext(ctx, network, serverAddr) }, } } else { diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js index b448e03061..6ec831ba9d 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js @@ -1471,13 +1471,13 @@ return view.extend({ } so.modalonly = false; - so = ss.option(form.ListValue, 'chain_head_sub', _('Chain head')); + so = ss.option(form.ListValue, 'chain_head_sub', _('Chain head') + _(' (Destination)')); so.load = L.bind(hm.loadProviderLabel, so, [['', _('-- Please choose --')]]); so.rmempty = false; so.depends('type', 'provider'); so.modalonly = true; - so = ss.option(form.ListValue, 'chain_head', _('Chain head'), + so = ss.option(form.ListValue, 'chain_head', _('Chain head') + _(' (Destination)'), _('Recommended to use UoT node.
such as %s.') .format('ss|ssr|vmess|vless|trojan|tuic')); so.load = L.bind(hm.loadNodeLabel, so, [['', _('-- Please choose --')]]); @@ -1493,13 +1493,13 @@ return view.extend({ so.depends('type', 'node'); so.modalonly = true; - so = ss.option(form.ListValue, 'chain_tail_group', _('Chain tail')); + so = ss.option(form.ListValue, 'chain_tail_group', _('Chain tail') + _(' (Transit)')); so.load = L.bind(hm.loadProxyGroupLabel, so, [['', _('-- Please choose --')]]); so.rmempty = false; so.depends({chain_tail: /.+/, '!reverse': true}); so.modalonly = true; - so = ss.option(form.ListValue, 'chain_tail', _('Chain tail'), + so = ss.option(form.ListValue, 'chain_tail', _('Chain tail') + _(' (Transit)'), _('Recommended to use UoT node.
such as %s.') .format('ss|ssr|vmess|vless|trojan|tuic')); so.load = L.bind(hm.loadNodeLabel, so, [['', _('-- Please choose --')]]); diff --git a/small/luci-app-fchomo/po/templates/fchomo.pot b/small/luci-app-fchomo/po/templates/fchomo.pot index 6946a973d5..3d516f2eb9 100644 --- a/small/luci-app-fchomo/po/templates/fchomo.pot +++ b/small/luci-app-fchomo/po/templates/fchomo.pot @@ -5,6 +5,11 @@ msgstr "Content-Type: text/plain; charset=UTF-8" msgid "%s log" msgstr "" +#: htdocs/luci-static/resources/view/fchomo/node.js:1474 +#: htdocs/luci-static/resources/view/fchomo/node.js:1480 +msgid "(Destination)" +msgstr "" + #: htdocs/luci-static/resources/fchomo.js:582 #: htdocs/luci-static/resources/view/fchomo/client.js:237 #: htdocs/luci-static/resources/view/fchomo/client.js:267 @@ -12,6 +17,11 @@ msgstr "" msgid "(Imported)" msgstr "" +#: htdocs/luci-static/resources/view/fchomo/node.js:1496 +#: htdocs/luci-static/resources/view/fchomo/node.js:1502 +msgid "(Transit)" +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/global.js:549 #: htdocs/luci-static/resources/view/fchomo/global.js:555 #: htdocs/luci-static/resources/view/fchomo/node.js:767 diff --git a/small/luci-app-fchomo/po/zh_Hans/fchomo.po b/small/luci-app-fchomo/po/zh_Hans/fchomo.po index 26568a14f1..24720025f0 100644 --- a/small/luci-app-fchomo/po/zh_Hans/fchomo.po +++ b/small/luci-app-fchomo/po/zh_Hans/fchomo.po @@ -12,6 +12,11 @@ msgstr "" msgid "%s log" msgstr "%s 日志" +#: htdocs/luci-static/resources/view/fchomo/node.js:1474 +#: htdocs/luci-static/resources/view/fchomo/node.js:1480 +msgid "(Destination)" +msgstr "(落地)" + #: htdocs/luci-static/resources/fchomo.js:582 #: htdocs/luci-static/resources/view/fchomo/client.js:237 #: htdocs/luci-static/resources/view/fchomo/client.js:267 @@ -19,6 +24,11 @@ msgstr "%s 日志" msgid "(Imported)" msgstr "(已导入)" +#: htdocs/luci-static/resources/view/fchomo/node.js:1496 +#: htdocs/luci-static/resources/view/fchomo/node.js:1502 +msgid "(Transit)" +msgstr "(中转)" + #: htdocs/luci-static/resources/view/fchomo/global.js:549 #: htdocs/luci-static/resources/view/fchomo/global.js:555 #: htdocs/luci-static/resources/view/fchomo/node.js:767 diff --git a/small/luci-app-fchomo/po/zh_Hant/fchomo.po b/small/luci-app-fchomo/po/zh_Hant/fchomo.po index 5fa67dc57e..fe1aa1e429 100644 --- a/small/luci-app-fchomo/po/zh_Hant/fchomo.po +++ b/small/luci-app-fchomo/po/zh_Hant/fchomo.po @@ -12,6 +12,11 @@ msgstr "" msgid "%s log" msgstr "%s 日誌" +#: htdocs/luci-static/resources/view/fchomo/node.js:1474 +#: htdocs/luci-static/resources/view/fchomo/node.js:1480 +msgid "(Destination)" +msgstr "(落地)" + #: htdocs/luci-static/resources/fchomo.js:582 #: htdocs/luci-static/resources/view/fchomo/client.js:237 #: htdocs/luci-static/resources/view/fchomo/client.js:267 @@ -19,6 +24,11 @@ msgstr "%s 日誌" msgid "(Imported)" msgstr "(已導入)" +#: htdocs/luci-static/resources/view/fchomo/node.js:1496 +#: htdocs/luci-static/resources/view/fchomo/node.js:1502 +msgid "(Transit)" +msgstr "(中轉)" + #: htdocs/luci-static/resources/view/fchomo/global.js:549 #: htdocs/luci-static/resources/view/fchomo/global.js:555 #: htdocs/luci-static/resources/view/fchomo/node.js:767 diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index d3b16f760b..ba9cdfa226 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -7,7 +7,6 @@ local has_xray = api.finded_com("xray") local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist") local has_chnlist = fs.access("/usr/share/passwall/rules/chnlist") local has_chnroute = fs.access("/usr/share/passwall/rules/chnroute") -local chinadns_tls = os.execute("chinadns-ng -V | grep -i wolfssl >/dev/null") m = Map(appname) api.set_apply_on_parse(m) @@ -91,33 +90,6 @@ local doh_validate = function(self, value, t) return nil, translatef("%s request address","DoH") .. " " .. translate("Format must be:") .. " URL,IP" end -local chinadns_dot_validate = function(self, value, t) - local function isValidDoTString(s) - if s:sub(1, 6) ~= "tls://" then return false end - local address = s:sub(7) - local at_index = address:find("@") - local hash_index = address:find("#") - local ip, port - local domain = at_index and address:sub(1, at_index - 1) or nil - ip = at_index and address:sub(at_index + 1, (hash_index or 0) - 1) or address:sub(1, (hash_index or 0) - 1) - port = hash_index and address:sub(hash_index + 1) or nil - local num_port = tonumber(port) - if (port and (not num_port or num_port <= 0 or num_port >= 65536)) or - (domain and domain == "") or - (not datatypes.ipaddr(ip) and not datatypes.ip6addr(ip)) then - return false - end - return true - end - value = value:gsub("%s+", "") - if value ~= "" then - if isValidDoTString(value) then - return value - end - end - return nil, translatef("%s request address","DoT") .. " " .. translate("Format must be:") .. " tls://" .. translate("Domain") .. "@IP[#Port] | tls://IP[#Port]" -end - m:append(Template(appname .. "/global/status")) s = m:section(TypedSection, "global") @@ -331,11 +303,6 @@ o = s:taboption("DNS", ListValue, "direct_dns_mode", translate("Direct DNS") .. o:value("", translate("Auto")) o:value("udp", translatef("Requery DNS By %s", "UDP")) o:value("tcp", translatef("Requery DNS By %s", "TCP")) -if chinadns_tls == 0 then - o:value("dot", translatef("Requery DNS By %s", "DoT")) -end ---TO DO ---o:value("doh", "DoH") o:depends({dns_shunt = "dnsmasq"}) o:depends({dns_shunt = "chinadns-ng"}) @@ -364,19 +331,6 @@ o:value("114.114.115.115") o:value("119.28.28.28") o:depends("direct_dns_mode", "tcp") -o = s:taboption("DNS", Value, "direct_dns_dot", translate("Direct DNS DoT")) -o.default = "tls://dot.pub@1.12.12.12" -o:value("tls://dot.pub@1.12.12.12") -o:value("tls://dot.pub@120.53.53.53") -o:value("tls://dot.360.cn@36.99.170.86") -o:value("tls://dot.360.cn@101.198.191.4") -o:value("tls://dns.alidns.com@223.5.5.5") -o:value("tls://dns.alidns.com@223.6.6.6") -o:value("tls://dns.alidns.com@2400:3200::1") -o:value("tls://dns.alidns.com@2400:3200:baba::1") -o.validate = chinadns_dot_validate -o:depends("direct_dns_mode", "dot") - o = s:taboption("DNS", Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature.")) o.default = "0" @@ -387,9 +341,6 @@ o = s:taboption("DNS", ListValue, "dns_mode", translate("Filter Mode"), "") o:value("udp", translatef("Requery DNS By %s", "UDP")) o:value("tcp", translatef("Requery DNS By %s", "TCP")) -if chinadns_tls == 0 then - o:value("dot", translatef("Requery DNS By %s", "DoT")) -end if api.is_finded("dns2socks") then o:value("dns2socks", "dns2socks") end @@ -519,22 +470,6 @@ o:depends({xray_dns_mode = "tcp"}) o:depends({xray_dns_mode = "tcp+doh"}) o:depends({singbox_dns_mode = "tcp"}) ----- DoT -o = s:taboption("DNS", Value, "remote_dns_dot", translate("Remote DNS DoT")) -o.default = "tls://one.one.one.one@1.1.1.1" -o:value("tls://one.one.one.one@1.0.0.1", "1.0.0.1 (CloudFlare)") -o:value("tls://one.one.one.one@1.1.1.1", "1.1.1.1 (CloudFlare)") -o:value("tls://dns.google@8.8.4.4", "8.8.4.4 (Google)") -o:value("tls://dns.google@8.8.8.8", "8.8.8.8 (Google)") -o:value("tls://dns.quad9.net@9.9.9.9", "9.9.9.9 (Quad9)") -o:value("tls://dns.quad9.net@149.112.112.112", "149.112.112.112 (Quad9)") -o:value("tls://dns.adguard.com@94.140.14.14", "94.140.14.14 (AdGuard)") -o:value("tls://dns.adguard.com@94.140.15.15", "94.140.15.15 (AdGuard)") -o:value("tls://dns.opendns.com@208.67.222.222", "208.67.222.222 (OpenDNS)") -o:value("tls://dns.opendns.com@208.67.220.220", "208.67.220.220 (OpenDNS)") -o.validate = chinadns_dot_validate -o:depends("dns_mode", "dot") - ---- DoH o = s:taboption("DNS", Value, "remote_dns_doh", translate("Remote DNS DoH")) o.default = "https://1.1.1.1/dns-query" @@ -617,11 +552,6 @@ if api.is_finded("smartdns") then o:depends({dns_shunt = "smartdns"}) end -o = s:taboption("DNS", Flag, "chinadns_ng_cert_verify", translate("DoT Cert verify"), translate("Verify DoT SSL cert. (May fail on some platforms!)")) -o.default = "0" -o:depends({direct_dns_mode = "dot"}) -o:depends({dns_mode = "dot"}) - o = s:taboption("DNS", Flag, "dns_redirect", translate("DNS Redirect"), translate("Force special DNS server to need proxy devices.")) o.default = "1" o.rmempty = false diff --git a/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 131b771687..d84cc5a573 100644 --- a/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -909,7 +909,6 @@ function gen_config(var) local direct_dns_port = var["-direct_dns_port"] local direct_dns_udp_server = var["-direct_dns_udp_server"] local direct_dns_tcp_server = var["-direct_dns_tcp_server"] - local direct_dns_dot_server = var["-direct_dns_dot_server"] local direct_dns_query_strategy = var["-direct_dns_query_strategy"] local remote_dns_server = var["-remote_dns_server"] local remote_dns_port = var["-remote_dns_port"] @@ -1639,7 +1638,7 @@ function gen_config(var) end local direct_strategy = "prefer_ipv6" - if direct_dns_udp_server or direct_dns_tcp_server or direct_dns_dot_server then + if direct_dns_udp_server or direct_dns_tcp_server then if direct_dns_query_strategy == "UseIPv4" then direct_strategy = "ipv4_only" elseif direct_dns_query_strategy == "UseIPv6" then @@ -1667,13 +1666,6 @@ function gen_config(var) elseif direct_dns_tcp_server then port = tonumber(direct_dns_port) or 53 direct_dns_server = "tcp://" .. direct_dns_tcp_server .. ":" .. port - elseif direct_dns_dot_server then - port = tonumber(direct_dns_port) or 853 - if direct_dns_dot_server:find(":") == nil then - direct_dns_server = "tls://" .. direct_dns_dot_server .. ":" .. port - else - direct_dns_server = "tls://[" .. direct_dns_dot_server .. "]:" .. port - end end table.insert(dns.servers, { @@ -1693,10 +1685,6 @@ function gen_config(var) port = tonumber(direct_dns_port) or 53 direct_dns_server = direct_dns_tcp_server type = "tcp" - elseif direct_dns_dot_server then - port = tonumber(direct_dns_port) or 853 - direct_dns_server = direct_dns_dot_server - type = "tls" end table.insert(dns.servers, { diff --git a/small/luci-app-passwall/po/zh-cn/passwall.po b/small/luci-app-passwall/po/zh-cn/passwall.po index 83cc18e5c8..b9d9c37c34 100644 --- a/small/luci-app-passwall/po/zh-cn/passwall.po +++ b/small/luci-app-passwall/po/zh-cn/passwall.po @@ -139,9 +139,6 @@ msgstr "TCP 节点必须是 '%s' 类型才能使用 FakeDNS。" msgid "Direct DNS" msgstr "直连 DNS" -msgid "Direct DNS DoT" -msgstr "直连 DNS DoT" - msgid "Remote DNS" msgstr "远程 DNS" @@ -172,9 +169,6 @@ msgstr "请求协议" msgid "Remote DNS DoH" msgstr "远程 DNS DoH" -msgid "Remote DNS DoT" -msgstr "远程 DNS DoT" - msgid "Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address)." msgstr "用于 DNS 查询时通知 DNS 服务器,客户端所在的地理位置(不能是私有 IP 地址)。" @@ -241,12 +235,6 @@ msgstr "清空 IPSET" msgid "Clear NFTSET" msgstr "清空 NFTSET" -msgid "DoT Cert verify" -msgstr "DoT 证书验证" - -msgid "Verify DoT SSL cert. (May fail on some platforms!)" -msgstr "验证 DoT SSL 证书。(在某些平台可能无法验证,谨慎开启!)" - msgid "Try this feature if the rule modification does not take effect." msgstr "如果修改规则后没有生效,请尝试此功能。" diff --git a/small/luci-app-passwall/root/usr/share/passwall/app.sh b/small/luci-app-passwall/root/usr/share/passwall/app.sh index 2bd4245fc8..7db4a4bb5c 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/small/luci-app-passwall/root/usr/share/passwall/app.sh @@ -425,7 +425,7 @@ run_ipt2socks() { run_singbox() { local flag type node tcp_redir_port tcp_proxy_way udp_redir_port socks_address socks_port socks_username socks_password http_address http_port http_username http_password - local dns_listen_port direct_dns_query_strategy direct_dns_port direct_dns_udp_server direct_dns_tcp_server direct_dns_dot_server remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy dns_cache dns_socks_address dns_socks_port + local dns_listen_port direct_dns_query_strategy direct_dns_port direct_dns_udp_server direct_dns_tcp_server remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy dns_cache dns_socks_address dns_socks_port local loglevel log_file config_file server_host server_port no_run local _extra_param="" eval_set_val $@ @@ -471,9 +471,6 @@ run_singbox() { elif [ -n "$direct_dns_tcp_server" ]; then direct_dns_port=$(echo ${direct_dns_tcp_server} | awk -F '#' '{print $2}') _extra_param="${_extra_param} -direct_dns_tcp_server $(echo ${direct_dns_tcp_server} | awk -F '#' '{print $1}')" - elif [ -n "$direct_dns_dot_server" ]; then - direct_dns_port=$(echo ${direct_dns_dot_server} | awk -F '#' '{print $2}') - _extra_param="${_extra_param} -direct_dns_dot_server $(echo ${direct_dns_dot_server} | awk -F '#' '{print $1}')" else local local_dns=$(echo -n $(echo "${LOCAL_DNS}" | sed "s/,/\n/g" | head -n1) | tr " " ",") _extra_param="${_extra_param} -direct_dns_udp_server $(echo ${local_dns} | awk -F '#' '{print $1}')" @@ -958,12 +955,6 @@ run_redir() { tcp) _args="${_args} direct_dns_tcp_server=$(config_t_get global direct_dns_tcp 223.5.5.5 | sed 's/:/#/g')" ;; - dot) - local tmp_dot_dns=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12") - local tmp_dot_ip=$(echo "$tmp_dot_dns" | sed -n 's/.*:\/\/\([^@#]*@\)*\([^@#]*\).*/\2/p') - local tmp_dot_port=$(echo "$tmp_dot_dns" | sed -n 's/.*#\([0-9]\+\).*/\1/p') - _args="${_args} direct_dns_dot_server=$tmp_dot_ip#${tmp_dot_port:-853}" - ;; esac _args="${_args} remote_dns_protocol=${v2ray_dns_mode}" @@ -1420,7 +1411,6 @@ stop_crontab() { start_dns() { echolog "DNS域名解析:" - local chinadns_tls=$($(first_type chinadns-ng) -V | grep -i wolfssl) local china_ng_local_dns=$(IFS=','; set -- $LOCAL_DNS; [ "${1%%[#:]*}" = "127.0.0.1" ] && echo "$1" || ([ -n "$2" ] && echo "$1,$2" || echo "$1")) local sing_box_local_dns= local direct_dns_mode=$(config_t_get global direct_dns_mode "auto") @@ -1456,29 +1446,6 @@ start_dns() { NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1) } ;; - dot) - if [ "$chinadns_tls" != "nil" ]; then - local DIRECT_DNS=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12") - local cert_verify=$([ "$(config_t_get global chinadns_ng_cert_verify 0)" = "1" ] && echo "--cert-verify") - china_ng_local_dns=${DIRECT_DNS} - - #当全局(包括访问控制节点)开启chinadns-ng时,不启动新进程。 - [ "$DNS_SHUNT" != "chinadns-ng" ] || [ "$ACL_RULE_DNSMASQ" = "1" ] && { - LOCAL_DNS="127.0.0.1#${NEXT_DNS_LISTEN_PORT}" - ln_run "$(first_type chinadns-ng)" chinadns-ng "/dev/null" -b :: -l ${NEXT_DNS_LISTEN_PORT} -c ${DIRECT_DNS} -d chn ${cert_verify} - echolog " - ChinaDNS-NG(${LOCAL_DNS}) -> ${DIRECT_DNS}" - echolog " * 请确保上游直连 DNS 支持 DoT 查询。" - NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1) - } - - local tmp_dot_ip=$(echo "$DIRECT_DNS" | sed -n 's/.*:\/\/\([^@#]*@\)*\([^@#]*\).*/\2/p') - local tmp_dot_port=$(echo "$DIRECT_DNS" | sed -n 's/.*#\([0-9]\+\).*/\1/p') - DIRECT_DNS=$tmp_dot_ip#${tmp_dot_port:-853} - sing_box_local_dns="direct_dns_dot_server=${DIRECT_DNS}" - else - echolog " - 你的ChinaDNS-NG版本不支持DoT,直连DNS将使用默认地址。" - fi - ;; auto) #Automatic logic is already done by default : @@ -1577,32 +1544,6 @@ start_dns() { run_xray ${_args} } ;; - dot) - TCP_PROXY_DNS=1 - if [ "$chinadns_tls" != "nil" ]; then - local china_ng_listen_port=${NEXT_DNS_LISTEN_PORT} - local china_ng_trust_dns=$(config_t_get global remote_dns_dot "tls://one.one.one.one@1.1.1.1") - local cert_verify=$([ "$(config_t_get global chinadns_ng_cert_verify 0)" = "1" ] && echo "--cert-verify") - local tmp_dot_ip=$(echo "$china_ng_trust_dns" | sed -n 's/.*:\/\/\([^@#]*@\)*\([^@#]*\).*/\2/p') - local tmp_dot_port=$(echo "$china_ng_trust_dns" | sed -n 's/.*#\([0-9]\+\).*/\1/p') - REMOTE_DNS="$tmp_dot_ip#${tmp_dot_port:-853}" - [ "$DNS_SHUNT" != "chinadns-ng" ] && { - [ "$FILTER_PROXY_IPV6" = "1" ] && DNSMASQ_FILTER_PROXY_IPV6=0 && local no_ipv6_trust="-N" - ln_run "$(first_type chinadns-ng)" chinadns-ng "/dev/null" -b :: -l ${china_ng_listen_port} -t ${china_ng_trust_dns} -d gfw ${no_ipv6_trust} ${cert_verify} - echolog " - ChinaDNS-NG(${TUN_DNS}) -> ${china_ng_trust_dns}" - } - else - echolog " - 你的ChinaDNS-NG版本不支持DoT,远程DNS将默认使用tcp://1.1.1.1" - REMOTE_DNS="1.1.1.1" - local china_ng_listen_port=${NEXT_DNS_LISTEN_PORT} - local china_ng_trust_dns="tcp://${REMOTE_DNS}" - [ "$DNS_SHUNT" != "chinadns-ng" ] && { - [ "$FILTER_PROXY_IPV6" = "1" ] && DNSMASQ_FILTER_PROXY_IPV6=0 && local no_ipv6_trust="-N" - ln_run "$(first_type chinadns-ng)" chinadns-ng "/dev/null" -b :: -l ${china_ng_listen_port} -t ${china_ng_trust_dns} -d gfw ${no_ipv6_trust} - echolog " - ChinaDNS-NG(${TUN_DNS}) -> ${china_ng_trust_dns}" - } - fi - ;; udp) UDP_PROXY_DNS=1 local china_ng_listen_port=${NEXT_DNS_LISTEN_PORT} @@ -1631,7 +1572,7 @@ start_dns() { [ -n "${resolve_dns_log}" ] && echolog " - ${resolve_dns_log}" - [ -n "${TCP_PROXY_DNS}" ] && echolog " * 请确认上游 DNS 支持 TCP/DoT/DoH 查询,如非直连地址,确保 TCP 代理打开,并且已经正确转发!" + [ -n "${TCP_PROXY_DNS}" ] && echolog " * 请确认上游 DNS 支持 TCP/DoH 查询,如非直连地址,确保 TCP 代理打开,并且已经正确转发!" [ -n "${UDP_PROXY_DNS}" ] && echolog " * 请确认上游 DNS 支持 UDP 查询并已使用 UDP 节点,如上游 DNS 非直连地址,确保 UDP 代理打开,并且已经正确转发!" [ "${DNS_SHUNT}" = "smartdns" ] && { @@ -1929,11 +1870,6 @@ acl_app() { tcp) _chinadns_local_dns="tcp://$(config_t_get global direct_dns_tcp 223.5.5.5 | sed 's/:/#/g')" ;; - dot) - if [ "$($(first_type chinadns-ng) -V | grep -i wolfssl)" != "nil" ]; then - _chinadns_local_dns=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12") - fi - ;; esac run_chinadns_ng \ diff --git a/small/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua b/small/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua index 2ea4164e76..c17dde63d2 100644 --- a/small/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua +++ b/small/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua @@ -504,11 +504,6 @@ end table.insert(config_lines, "hosts") -local cert_verify = uci:get(appname, "@global[0]", "chinadns_ng_cert_verify") or 0 -if tonumber(cert_verify) == 1 then - table.insert(config_lines, "cert-verify") -end - if DEFAULT_TAG == "chn" then log(string.format(" - 默认 DNS :%s", DNS_LOCAL)) elseif DEFAULT_TAG == "gfw" then diff --git a/small/sing-box/Makefile b/small/sing-box/Makefile index d104555497..68943f11e2 100644 --- a/small/sing-box/Makefile +++ b/small/sing-box/Makefile @@ -5,12 +5,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=sing-box -PKG_VERSION:=1.12.8 +PKG_VERSION:=1.12.9 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)? -PKG_HASH:=4d3da19e0819fdc12740539512dc3455ea030747c2fa04171967715fb26ff775 +PKG_HASH:=7db58b28e93d1f7dde0565fd2a2d82979d18a82df48623a0a99455278cd5c372 PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE_FILES:=LICENSE diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile index dc0a9769a3..d5bf4eae85 100644 --- a/small/v2ray-geodata/Makefile +++ b/small/v2ray-geodata/Makefile @@ -12,13 +12,13 @@ PKG_MAINTAINER:=Tianling Shen include $(INCLUDE_DIR)/package.mk -GEOIP_VER:=202509050142 +GEOIP_VER:=202510050144 GEOIP_FILE:=geoip.dat.$(GEOIP_VER) define Download/geoip URL:=https://github.com/v2fly/geoip/releases/download/$(GEOIP_VER)/ URL_FILE:=geoip.dat FILE:=$(GEOIP_FILE) - HASH:=a01e09150b456cb2f3819d29d6e6c34572420aaee3ff9ef23977c4e9596c20ec + HASH:=c23ac8343e9796f8cc8b670c3aeb6df6d03d4e8914437a409961477f6b226098 endef GEOSITE_VER:=20250916122507 diff --git a/small/v2raya/Makefile b/small/v2raya/Makefile index 59d144385c..ca0578ee8c 100644 --- a/small/v2raya/Makefile +++ b/small/v2raya/Makefile @@ -5,12 +5,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=v2rayA -PKG_VERSION:=2.2.7.1 +PKG_VERSION:=2.2.7.2 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/v2rayA/v2rayA/tar.gz/v$(PKG_VERSION)? -PKG_HASH:=8996ce3ac42f4998a433ab4f8968c7da656baae40b34c154705ecba4274f012d +PKG_HASH:=368d57206b26ee53d59861961e7eca311f32654a4e76eab24e00d716a7ae7cd0 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/service PKG_LICENSE:=AGPL-3.0-only @@ -60,7 +60,7 @@ define Download/v2raya-web URL:=https://github.com/v2rayA/v2rayA/releases/download/v$(PKG_VERSION)/ URL_FILE:=web.tar.gz FILE:=$(WEB_FILE) - HASH:=26eaea7b367b36b844c98c0b537fb05482595329ac5fe0ea2293f77bc9d1aac9 + HASH:=cbb9e01e80fd89f9a78459e416304160f16b764383b9cad2dcc5a6318b8543f9 endef define Build/Prepare diff --git a/v2raya/gui/src/components/modalSubcription.vue b/v2raya/gui/src/components/modalSubcription.vue index e611c7c454..2df703c45d 100644 --- a/v2raya/gui/src/components/modalSubcription.vue +++ b/v2raya/gui/src/components/modalSubcription.vue @@ -17,6 +17,12 @@ :placeholder="$t('subscription.remarks')" /> + + {{ $t("subscription.autoSelect") }} + +