diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index d9b308bd..65178f41 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -103,7 +103,7 @@ jobs: - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.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_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.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) diff --git a/UPDATELOG.md b/UPDATELOG.md index c1d12deb..c393f5e0 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -87,6 +87,7 @@ - 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题 - Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单 - 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书` +- 允许设置 Mihomo 端口范围 1000(含) - 65536(含) diff --git a/package.json b/package.json index ee047041..06b40301 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "react-dom": "19.2.0", "react-error-boundary": "6.0.0", "react-hook-form": "^7.66.0", - "react-i18next": "16.2.3", + "react-i18next": "16.2.4", "react-markdown": "10.1.0", "react-monaco-editor": "0.59.0", "react-router": "^7.9.5", @@ -81,7 +81,7 @@ "devDependencies": { "@actions/github": "^6.0.1", "@eslint-react/eslint-plugin": "^2.3.1", - "@eslint/js": "^9.39.0", + "@eslint/js": "^9.39.1", "@tauri-apps/cli": "2.9.2", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", @@ -94,7 +94,7 @@ "cli-color": "^2.0.4", "commander": "^14.0.2", "cross-env": "^10.1.0", - "eslint": "^9.39.0", + "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import-x": "^4.16.1", @@ -115,7 +115,7 @@ "tar": "^7.5.2", "terser": "^5.44.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.2", + "typescript-eslint": "^8.46.3", "vite": "^7.1.12", "vite-plugin-monaco-editor-esm": "^2.0.2", "vite-plugin-svgr": "^4.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77cce79d..2b6f49d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: ^7.66.0 version: 7.66.0(react@19.2.0) react-i18next: - specifier: 16.2.3 - version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + specifier: 16.2.4 + version: 16.2.4(i18next@25.6.0(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.2.2)(react@19.2.0) @@ -140,10 +140,10 @@ importers: version: 6.0.1 '@eslint-react/eslint-plugin': specifier: ^2.3.1 - version: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + version: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint/js': - specifier: ^9.39.0 - version: 9.39.0 + specifier: ^9.39.1 + version: 9.39.1 '@tauri-apps/cli': specifier: 2.9.2 version: 2.9.2 @@ -181,29 +181,29 @@ importers: specifier: ^10.1.0 version: 10.1.0 eslint: - specifier: ^9.39.0 - version: 9.39.0(jiti@2.6.1) + specifier: ^9.39.1 + version: 9.39.1(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.0(jiti@2.6.1)) + version: 10.1.8(eslint@9.39.1(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.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.0(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-import-x: specifier: ^4.16.1 - version: 4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)) + version: 4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.5.4 - version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))(prettier@3.6.2) + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) eslint-plugin-react-hooks: specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.0(jiti@2.6.1)) + version: 7.0.1(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-refresh: specifier: ^0.4.24 - version: 0.4.24(eslint@9.39.0(jiti@2.6.1)) + version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-unused-imports: specifier: ^4.3.0 - version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)) + version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) glob: specifier: ^11.0.3 version: 11.0.3 @@ -244,8 +244,8 @@ importers: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.46.2 - version: 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.46.3 + version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.1.12 version: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) @@ -1053,8 +1053,8 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.0': - resolution: {integrity: sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==} + '@eslint/js@9.39.1': + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -1849,63 +1849,63 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.46.2': - resolution: {integrity: sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==} + '@typescript-eslint/eslint-plugin@8.46.3': + resolution: {integrity: sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.46.2 + '@typescript-eslint/parser': ^8.46.3 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.46.2': - resolution: {integrity: sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==} + '@typescript-eslint/parser@8.46.3': + resolution: {integrity: sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==} 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.46.2': - resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==} + '@typescript-eslint/project-service@8.46.3': + resolution: {integrity: sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.46.2': - resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} + '@typescript-eslint/scope-manager@8.46.3': + resolution: {integrity: sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.2': - resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==} + '@typescript-eslint/tsconfig-utils@8.46.3': + resolution: {integrity: sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.46.2': - resolution: {integrity: sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==} + '@typescript-eslint/type-utils@8.46.3': + resolution: {integrity: sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==} 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.46.2': - resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} + '@typescript-eslint/types@8.46.3': + resolution: {integrity: sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.46.2': - resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==} + '@typescript-eslint/typescript-estree@8.46.3': + resolution: {integrity: sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.46.2': - resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==} + '@typescript-eslint/utils@8.46.3': + resolution: {integrity: sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==} 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.46.2': - resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} + '@typescript-eslint/visitor-keys@8.46.3': + resolution: {integrity: sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -2670,8 +2670,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.39.0: - resolution: {integrity: sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==} + eslint@9.39.1: + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3624,8 +3624,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@16.2.3: - resolution: {integrity: sha512-O0t2zvmIz7nHWKNfIL+O/NTIbpTaOPY0vZov779hegbep3IZ+xcqkeVPKWBSXwzdkiv77q8zmq9toKIUys1x3A==} + react-i18next@16.2.4: + resolution: {integrity: sha512-pvbcPQ+YuQQoRkKBA4VCU9aO8dOgP/vdKEizIYXcAk3+AmI8yQKSJaCzxQQu4Kgg2zWZm3ax9KqHv8ItUlRY0A==} peerDependencies: i18next: '>= 25.5.2' react: '>= 16.8.0' @@ -4059,8 +4059,8 @@ packages: types-pac@1.0.3: resolution: {integrity: sha512-MF2UAZGvGMOM+vHi9Zj/LvQqdNN1m1xSB+PjAW9B/GvFqaB4GwR18YaIbGIGDRTW/J8iqFXQHLZd5eJVtho46w==} - typescript-eslint@8.46.2: - resolution: {integrity: sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==} + typescript-eslint@8.46.3: + resolution: {integrity: sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -5199,34 +5199,34 @@ snapshots: '@esbuild/win32-x64@0.25.4': optional: true - '@eslint-community/eslint-utils@4.8.0(eslint@9.39.0(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.8.0(eslint@9.39.1(jiti@2.6.1))': dependencies: - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint-react/ast@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/ast@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-react/eff': 2.3.1 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) string-ts: 2.2.1 transitivePeerDependencies: - eslint - supports-color - typescript - '@eslint-react/core@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/core@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) birecord: 0.1.1 ts-pattern: 5.9.0 transitivePeerDependencies: @@ -5236,29 +5236,29 @@ snapshots: '@eslint-react/eff@2.3.1': {} - '@eslint-react/eslint-plugin@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/eslint-plugin@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.0(jiti@2.6.1) - eslint-plugin-react-dom: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-hooks-extra: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-naming-convention: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-web-api: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-x: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) + eslint-plugin-react-dom: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-hooks-extra: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-naming-convention: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-web-api: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-x: 2.3.1(eslint@9.39.1(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/shared@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/shared@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-react/eff': 2.3.1 - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) ts-pattern: 5.9.0 zod: 4.1.12 transitivePeerDependencies: @@ -5266,13 +5266,13 @@ snapshots: - supports-color - typescript - '@eslint-react/var@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/var@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/eff': 2.3.1 - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) ts-pattern: 5.9.0 transitivePeerDependencies: - eslint @@ -5309,7 +5309,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.0': {} + '@eslint/js@9.39.1': {} '@eslint/object-schema@2.1.7': {} @@ -6006,15 +6006,15 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.2 - eslint: 9.39.0(jiti@2.6.1) + '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.3 + eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -6023,56 +6023,56 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.3 debug: 4.4.3 - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.46.2(typescript@5.9.3)': + '@typescript-eslint/project-service@8.46.3(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.46.2': + '@typescript-eslint/scope-manager@8.46.3': dependencies: - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/visitor-keys': 8.46.3 - '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.46.3(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(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.46.2': {} + '@typescript-eslint/types@8.46.3': {} - '@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.46.3(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.46.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/project-service': 8.46.3(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/visitor-keys': 8.46.3 debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -6083,20 +6083,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - eslint: 9.39.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.46.2': + '@typescript-eslint/visitor-keys@8.46.3': dependencies: - '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/types': 8.46.3 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} @@ -6805,9 +6805,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.39.0(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)): dependencies: - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(jiti@2.6.1) eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: @@ -6825,10 +6825,10 @@ snapshots: - supports-color optional: true - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.0(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(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 @@ -6836,29 +6836,29 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.0(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.39.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.39.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.0(jiti@2.6.1) + '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(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.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color optional: true - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): dependencies: - '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/types': 8.46.3 comment-parser: 1.4.1 debug: 4.4.3 - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.0.3 @@ -6866,12 +6866,12 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(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.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -6880,9 +6880,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.39.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.39.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -6894,122 +6894,122 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(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.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))(prettier@3.6.2): + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2): dependencies: - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(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.39.0(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-react-dom@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-dom@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks-extra@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-hooks-extra@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.0(jiti@2.6.1) + '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@7.0.1(eslint@9.39.0(jiti@2.6.1)): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: '@babel/core': 7.28.4 '@babel/parser': 7.28.4 - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(jiti@2.6.1) hermes-parser: 0.25.1 zod: 4.1.12 zod-validation-error: 4.0.2(zod@4.1.12) transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-naming-convention@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.0(jiti@2.6.1) + '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.4.24(eslint@9.39.0(jiti@2.6.1)): + eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@2.6.1)): dependencies: - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(jiti@2.6.1) - eslint-plugin-react-web-api@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-web-api@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.0(jiti@2.6.1) + '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-x@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 - eslint: 9.39.0(jiti@2.6.1) - is-immutable-type: 5.0.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) + is-immutable-type: 5.0.1(eslint@9.39.1(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.9.0 @@ -7017,11 +7017,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)): + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: - eslint: 9.39.0(jiti@2.6.1) + eslint: 9.39.1(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -7032,15 +7032,15 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.0(jiti@2.6.1): + eslint@9.39.1(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.1(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.39.0 + '@eslint/js': 9.39.1 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 @@ -7478,10 +7478,10 @@ snapshots: is-hexadecimal@2.0.1: {} - is-immutable-type@5.0.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): + is-immutable-type@5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.0(jiti@2.6.1) + '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(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 @@ -8183,7 +8183,7 @@ snapshots: dependencies: react: 19.2.0 - react-i18next@16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): + react-i18next@16.2.4(i18next@25.6.0(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.28.4 html-parse-stringify: 3.0.1 @@ -8719,13 +8719,13 @@ snapshots: types-pac@1.0.3: {} - typescript-eslint@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.0(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color diff --git a/src-tauri/benches/draft_benchmark.rs b/src-tauri/benches/draft_benchmark.rs index 3942382f..c8a07c53 100644 --- a/src-tauri/benches/draft_benchmark.rs +++ b/src-tauri/benches/draft_benchmark.rs @@ -7,13 +7,13 @@ use app_lib::config::IVerge; use app_lib::utils::Draft as DraftNew; /// 创建测试数据 -fn make_draft() -> DraftNew> { - let verge = Box::new(IVerge { +fn make_draft() -> DraftNew { + let verge = IVerge { enable_auto_launch: Some(true), enable_tun_mode: Some(false), ..Default::default() - }); - DraftNew::from(verge) + }; + DraftNew::new(verge) } pub fn bench_draft(c: &mut Criterion) { @@ -30,18 +30,17 @@ pub fn bench_draft(c: &mut Criterion) { group.bench_function("data_mut", |b| { b.iter(|| { let draft = black_box(make_draft()); - let mut data = draft.data_mut(); - data.enable_tun_mode = Some(true); - black_box(&data.enable_tun_mode); + draft.edit_draft(|d| d.enable_tun_mode = Some(true)); + black_box(&draft.latest_arc().enable_tun_mode); }); }); group.bench_function("draft_mut_first", |b| { b.iter(|| { let draft = black_box(make_draft()); - let mut d = draft.draft_mut(); - d.enable_auto_launch = Some(false); - black_box(&d.enable_auto_launch); + draft.edit_draft(|d| d.enable_auto_launch = Some(false)); + let latest = draft.latest_arc(); + black_box(&latest.enable_auto_launch); }); }); @@ -49,20 +48,24 @@ pub fn bench_draft(c: &mut Criterion) { b.iter(|| { let draft = black_box(make_draft()); { - let mut first = draft.draft_mut(); - first.enable_tun_mode = Some(true); - black_box(&first.enable_tun_mode); + draft.edit_draft(|d| { + d.enable_tun_mode = Some(true); + }); + let latest1 = draft.latest_arc(); + black_box(&latest1.enable_tun_mode); } - let mut second = draft.draft_mut(); - second.enable_tun_mode = Some(false); - black_box(&second.enable_tun_mode); + draft.edit_draft(|d| { + d.enable_tun_mode = Some(false); + }); + let latest2 = draft.latest_arc(); + black_box(&latest2.enable_tun_mode); }); }); - group.bench_function("latest_ref", |b| { + group.bench_function("latest_arc", |b| { b.iter(|| { let draft = black_box(make_draft()); - let latest = draft.latest_ref(); + let latest = draft.latest_arc(); black_box(&latest.enable_auto_launch); }); }); @@ -71,8 +74,9 @@ pub fn bench_draft(c: &mut Criterion) { b.iter(|| { let draft = black_box(make_draft()); { - let mut d = draft.draft_mut(); - d.enable_auto_launch = Some(false); + draft.edit_draft(|d| { + d.enable_auto_launch = Some(false); + }); } draft.apply(); black_box(&draft); @@ -83,8 +87,9 @@ pub fn bench_draft(c: &mut Criterion) { b.iter(|| { let draft = black_box(make_draft()); { - let mut d = draft.draft_mut(); - d.enable_auto_launch = Some(false); + draft.edit_draft(|d| { + d.enable_auto_launch = Some(false); + }); } draft.discard(); black_box(&draft); @@ -95,7 +100,7 @@ pub fn bench_draft(c: &mut Criterion) { b.to_async(&rt).iter(|| async { let draft = black_box(make_draft()); let _: Result<(), anyhow::Error> = draft - .with_data_modify::<_, _, _, anyhow::Error>(|mut box_data| async move { + .with_data_modify::<_, _, _>(|mut box_data| async move { box_data.enable_auto_launch = Some(!box_data.enable_auto_launch.unwrap_or(false)); Ok((box_data, ())) diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 1bcadea9..b51afce5 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -2,11 +2,11 @@ use super::CmdResult; use crate::utils::dirs; use crate::{ cmd::StringifyErr, - config::Config, + config::{ClashInfo, Config}, constants, core::{CoreManager, handle, validate::CoreConfigValidator}, }; -use crate::{config::*, feat, logging, utils::logging::Type}; +use crate::{feat, logging, utils::logging::Type}; use compact_str::CompactString; use serde_yaml_ng::Mapping; use smartstring::alias::String; @@ -22,7 +22,7 @@ pub async fn copy_clash_env() -> CmdResult { /// 获取Clash信息 #[tauri::command] pub async fn get_clash_info() -> CmdResult { - Ok(Config::clash().await.latest_ref().get_client_info()) + Ok(Config::clash().await.latest_arc().get_client_info()) } /// 修改Clash配置 @@ -141,12 +141,6 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult { /// 应用或撤销DNS配置 #[tauri::command] pub async fn apply_dns_config(apply: bool) -> CmdResult { - use crate::{ - config::Config, - core::{CoreManager, handle}, - utils::dirs, - }; - if apply { // 读取DNS配置文件 let dns_path = dirs::app_home_dir() @@ -175,7 +169,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult { patch.insert("dns".into(), patch_config.into()); // 应用DNS配置到运行时配置 - Config::runtime().await.draft_mut().patch_config(patch); + Config::runtime().await.edit_draft(|d| { + d.patch_config(patch); + }); // 重新生成配置 Config::generate().await.stringify_err_log(|err| { diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index c6a04fd6..c16672e2 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -1,5 +1,6 @@ use super::CmdResult; use super::StringifyErr; +use crate::utils::draft::SharedBox; use crate::{ config::{ Config, IProfiles, PrfItem, PrfOption, @@ -23,11 +24,11 @@ use std::time::Duration; static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false); #[tauri::command] -pub async fn get_profiles() -> CmdResult { +pub async fn get_profiles() -> CmdResult> { logging!(debug, Type::Cmd, "获取配置文件列表"); let draft = Config::profiles().await; - let latest = draft.latest_ref(); - Ok((**latest).clone()) + let data = draft.data_arc(); + Ok(data) } /// 增强配置文件 @@ -99,9 +100,11 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult { match profiles_reorder_safe(&active_id, &over_id).await { Ok(_) => { logging!(info, Type::Cmd, "重新排序配置文件"); + Config::profiles().await.apply(); Ok(()) } Err(err) => { + Config::profiles().await.discard(); logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err); Err(format!("重新排序配置文件失败: {}", err).into()) } @@ -119,12 +122,16 @@ pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResu logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid); handle::Handle::notify_profile_changed(uid.clone()); } + Config::profiles().await.apply(); 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}").into()), - }, + Err(err) => { + Config::profiles().await.discard(); + match err.to_string().as_str() { + "the file already exists" => Err("the file already exists".into()), + _ => Err(format!("add profile error: {err}").into()), + } + } } } @@ -132,8 +139,12 @@ pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResu #[tauri::command] pub async fn update_profile(index: String, option: Option) -> CmdResult { match feat::update_profile(&index, option.as_ref(), true, true).await { - Ok(_) => Ok(()), + Ok(_) => { + let _: () = Config::profiles().await.apply(); + Ok(()) + } Err(e) => { + Config::profiles().await.discard(); logging!(error, Type::Cmd, "{}", e); Err(e.to_string().into()) } @@ -143,12 +154,11 @@ pub async fn update_profile(index: String, option: Option) -> CmdResu /// 删除配置文件 #[tauri::command] pub async fn delete_profile(index: String) -> CmdResult { - println!("delete_profile: {}", index); // 使用Send-safe helper函数 let should_update = profiles_delete_item_safe(&index).await.stringify_err()?; profiles_save_file_safe().await.stringify_err()?; - if should_update { + Config::profiles().await.apply(); match CoreManager::global().update_config().await { Ok(_) => { handle::Handle::refresh_clash(); @@ -172,7 +182,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> { // 获取目标配置文件路径 let config_file_result = { let profiles_config = Config::profiles().await; - let profiles_data = profiles_config.latest_ref(); + let profiles_data = profiles_config.latest_arc(); match profiles_data.get_item(new_profile) { Ok(item) => { if let Some(file) = &item.file { @@ -274,16 +284,15 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> { } /// 执行配置更新并处理结果 -async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> { +async fn restore_previous_profile(prev_profile: &String) -> CmdResult<()> { logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile); let restore_profiles = IProfiles { - current: Some(prev_profile), + current: Some(prev_profile.to_owned()), items: None, }; Config::profiles() .await - .draft_mut() - .patch_config(&restore_profiles) + .edit_draft(|d| d.patch_config(&restore_profiles)) .stringify_err()?; Config::profiles().await.apply(); crate::process::AsyncHandler::spawn(|| async move { @@ -295,7 +304,7 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> { Ok(()) } -async fn handle_success(current_value: Option) -> CmdResult { +async fn handle_success(current_value: Option<&String>) -> CmdResult { Config::profiles().await.apply(); handle::Handle::refresh_clash(); @@ -311,9 +320,9 @@ async fn handle_success(current_value: Option) -> CmdResult { logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}"); } - if let Some(current) = ¤t_value { - logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current,); - handle::Handle::notify_profile_changed(current.clone()); + if let Some(current) = current_value { + logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current); + handle::Handle::notify_profile_changed(current.to_owned()); } Ok(true) @@ -321,7 +330,7 @@ async fn handle_success(current_value: Option) -> CmdResult { async fn handle_validation_failure( error_msg: String, - current_profile: Option, + current_profile: Option<&String>, ) -> CmdResult { logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg); Config::profiles().await.discard(); @@ -339,7 +348,7 @@ async fn handle_update_error(e: E) -> CmdResult { Ok(false) } -async fn handle_timeout(current_profile: Option) -> CmdResult { +async fn handle_timeout(current_profile: Option<&String>) -> CmdResult { let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞"; logging!(error, Type::Cmd, "{}", timeout_msg); Config::profiles().await.discard(); @@ -351,8 +360,8 @@ async fn handle_timeout(current_profile: Option) -> CmdResult { } async fn perform_config_update( - current_value: Option, - current_profile: Option, + current_value: Option<&String>, + current_profile: Option<&String>, ) -> CmdResult { let update_result = tokio::time::timeout( Duration::from_secs(30), @@ -376,13 +385,14 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { .is_err() { logging!(info, Type::Cmd, "当前正在切换配置,放弃请求"); - return Err("switch_in_progress".into()); + return Ok(false); } defer! { CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release); } - let target_profile = profiles.current.clone(); + + let target_profile = profiles.current.as_ref(); logging!( info, @@ -392,21 +402,21 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { ); // 保存当前配置,以便在验证失败时恢复 - let current_profile = Config::profiles().await.latest_ref().current.clone(); - logging!(info, Type::Cmd, "当前配置: {:?}", current_profile); + let previous_profile = Config::profiles().await.data_arc().current.clone(); + logging!(info, Type::Cmd, "当前配置: {:?}", previous_profile); // 如果要切换配置,先检查目标配置文件是否有语法错误 - if let Some(new_profile) = profiles.current.as_ref() - && current_profile.as_ref() != Some(new_profile) - && validate_new_profile(new_profile).await.is_err() + if let Some(switch_to_profile) = target_profile + && previous_profile.as_ref() != Some(switch_to_profile) + && validate_new_profile(switch_to_profile).await.is_err() { return Ok(false); } + let _ = Config::profiles() + .await + .edit_draft(|d| d.patch_config(&profiles)); - let _ = Config::profiles().await.draft_mut().patch_config(&profiles); - let current_value = profiles.current.clone(); - - perform_config_update(current_value, current_profile).await + perform_config_update(target_profile, previous_profile.as_ref()).await } /// 根据profile name修改profiles @@ -426,7 +436,7 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { // 保存修改前检查是否有更新 update_interval let profiles = Config::profiles().await; - let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) + let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index) && let Some(new_option) = profile.option.as_ref() { let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval); @@ -465,7 +475,7 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { #[tauri::command] pub async fn view_profile(index: String) -> CmdResult { let profiles = Config::profiles().await; - let profiles_ref = profiles.latest_ref(); + let profiles_ref = profiles.latest_arc(); let file = profiles_ref .get_item(&index) .stringify_err()? @@ -488,7 +498,7 @@ pub async fn view_profile(index: String) -> CmdResult { pub async fn read_profile_file(index: String) -> CmdResult { let item = { let profiles = Config::profiles().await; - let profiles_ref = profiles.latest_ref(); + let profiles_ref = profiles.latest_arc(); PrfItem { file: profiles_ref.get_item(&index).stringify_err()?.file.clone(), ..Default::default() diff --git a/src-tauri/src/cmd/runtime.rs b/src-tauri/src/cmd/runtime.rs index 05ae1252..5ecfe954 100644 --- a/src-tauri/src/cmd/runtime.rs +++ b/src-tauri/src/cmd/runtime.rs @@ -8,14 +8,14 @@ use std::collections::HashMap; /// 获取运行时配置 #[tauri::command] pub async fn get_runtime_config() -> CmdResult> { - Ok(Config::runtime().await.latest_ref().config.clone()) + Ok(Config::runtime().await.latest_arc().config.clone()) } /// 获取运行时YAML配置 #[tauri::command] pub async fn get_runtime_yaml() -> CmdResult { let runtime = Config::runtime().await; - let runtime = runtime.latest_ref(); + let runtime = runtime.latest_arc(); let config = runtime.config.as_ref(); config @@ -31,19 +31,19 @@ pub async fn get_runtime_yaml() -> CmdResult { /// 获取运行时存在的键 #[tauri::command] pub async fn get_runtime_exists() -> CmdResult> { - Ok(Config::runtime().await.latest_ref().exists_keys.clone()) + Ok(Config::runtime().await.latest_arc().exists_keys.clone()) } /// 获取运行时日志 #[tauri::command] pub async fn get_runtime_logs() -> CmdResult>> { - Ok(Config::runtime().await.latest_ref().chain_logs.clone()) + Ok(Config::runtime().await.latest_arc().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 runtime = runtime.latest_arc(); let config = runtime .config @@ -98,9 +98,7 @@ pub async fn update_proxy_chain_config_in_runtime( ) -> CmdResult<()> { { let runtime = Config::runtime().await; - let mut draft = runtime.draft_mut(); - draft.update_proxy_chain_config(proxy_chain_config); - drop(draft); + runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config)); runtime.apply(); } diff --git a/src-tauri/src/cmd/save_profile.rs b/src-tauri/src/cmd/save_profile.rs index 1e905e63..a5246295 100644 --- a/src-tauri/src/cmd/save_profile.rs +++ b/src-tauri/src/cmd/save_profile.rs @@ -20,7 +20,7 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR // 在异步操作前获取必要元数据并释放锁 let (rel_path, is_merge_file) = { let profiles = Config::profiles().await; - let profiles_guard = profiles.latest_ref(); + let profiles_guard = profiles.latest_arc(); let item = profiles_guard.get_item(&index).stringify_err()?; let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge"); let path = item.file.clone().ok_or("file field is null")?; diff --git a/src-tauri/src/cmd/verge.rs b/src-tauri/src/cmd/verge.rs index cce4043d..c7b05fe5 100644 --- a/src-tauri/src/cmd/verge.rs +++ b/src-tauri/src/cmd/verge.rs @@ -1,16 +1,10 @@ use super::CmdResult; -use crate::{cmd::StringifyErr, config::*, feat}; +use crate::{cmd::StringifyErr, config::IVerge, feat, utils::draft::SharedBox}; /// 获取Verge配置 #[tauri::command] -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) +pub async fn get_verge_config() -> CmdResult> { + feat::fetch_verge_config().await.stringify_err() } /// 修改Verge配置 diff --git a/src-tauri/src/cmd/webdav.rs b/src-tauri/src/cmd/webdav.rs index 1de27b70..209f39e7 100644 --- a/src-tauri/src/cmd/webdav.rs +++ b/src-tauri/src/cmd/webdav.rs @@ -1,5 +1,9 @@ use super::CmdResult; -use crate::{cmd::StringifyErr, config::*, core, feat}; +use crate::{ + cmd::StringifyErr, + config::{Config, IVerge}, + core, feat, +}; use reqwest_dav::list_cmd::ListFile; use smartstring::alias::String; @@ -12,15 +16,11 @@ pub async fn save_webdav_config(url: String, username: String, password: String) webdav_password: Some(password), ..IVerge::default() }; - Config::verge().await.draft_mut().patch_config(&patch); + Config::verge().await.edit_draft(|e| e.patch_config(&patch)); Config::verge().await.apply(); - // 分离数据获取和异步调用 - let verge_data = Config::verge().await.latest_ref().clone(); - verge_data - .save_file() - .await - .map_err(|err| err.to_string())?; + let verge_data = Config::verge().await.latest_arc(); + verge_data.save_file().await.stringify_err()?; core::backup::WebDavClient::global().reset(); Ok(()) } diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs index 8b1c072a..195ec6c9 100644 --- a/src-tauri/src/config/clash.rs +++ b/src-tauri/src/config/clash.rs @@ -300,7 +300,7 @@ impl IClashTemp { // 检查 enable_external_controller 设置,用于运行时配置生成 let enable_external_controller = Config::verge() .await - .latest_ref() + .latest_arc() .enable_external_controller .unwrap_or(false); diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index f9bbcf0a..4cdc5ac7 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -15,10 +15,10 @@ use tokio::sync::OnceCell; use tokio::time::sleep; pub struct Config { - clash_config: Draft>, - verge_config: Draft>, - profiles_config: Draft>, - runtime_config: Draft>, + clash_config: Draft, + verge_config: Draft, + profiles_config: Draft, + runtime_config: Draft, } impl Config { @@ -27,28 +27,28 @@ impl Config { 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())), + clash_config: Draft::new(IClashTemp::new().await), + verge_config: Draft::new(IVerge::new().await), + profiles_config: Draft::new(IProfiles::new().await), + runtime_config: Draft::new(IRuntime::new()), } }) .await } - pub async fn clash() -> Draft> { + pub async fn clash() -> Draft { Self::global().await.clash_config.clone() } - pub async fn verge() -> Draft> { + pub async fn verge() -> Draft { Self::global().await.verge_config.clone() } - pub async fn profiles() -> Draft> { + pub async fn profiles() -> Draft { Self::global().await.profiles_config.clone() } - pub async fn runtime() -> Draft> { + pub async fn runtime() -> Draft { Self::global().await.runtime_config.clone() } @@ -61,12 +61,14 @@ impl Config { && service::is_service_available().await.is_err() { let verge = Config::verge().await; - verge.draft_mut().enable_tun_mode = Some(false); + verge.edit_draft(|d| { + d.enable_tun_mode = Some(false); + }); verge.apply(); - let _ = tray::Tray::global().update_tray_display().await; + let _ = tray::Tray::global().update_menu().await; // 分离数据获取和异步调用避免Send问题 - let verge_data = Config::verge().await.latest_ref().clone(); + let verge_data = Config::verge().await.latest_arc(); logging_error!(Type::Core, verge_data.save_file().await); } @@ -83,11 +85,11 @@ impl Config { // Ensure "Merge" and "Script" profile items exist, adding them if missing. async fn ensure_default_profile_items() -> Result<()> { let profiles = Self::profiles().await; - if profiles.latest_ref().get_item("Merge").is_err() { + if profiles.latest_arc().get_item("Merge").is_err() { let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?; profiles_append_item_safe(merge_item).await?; } - if profiles.latest_ref().get_item("Script").is_err() { + if profiles.latest_arc().get_item("Script").is_err() { let script_item = &mut PrfItem::from_script(Some("Script".into()))?; profiles_append_item_safe(script_item).await?; } @@ -154,7 +156,7 @@ impl Config { let runtime = Config::runtime().await; let config = runtime - .latest_ref() + .latest_arc() .config .as_ref() .ok_or_else(|| anyhow!("failed to get runtime config"))? @@ -168,11 +170,13 @@ impl Config { pub async fn generate() -> Result<()> { let (config, exists_keys, logs) = enhance::enhance().await; - **Config::runtime().await.draft_mut() = IRuntime { - config: Some(config), - exists_keys, - chain_logs: logs, - }; + Config::runtime().await.edit_draft(|d| { + *d = IRuntime { + config: Some(config), + exists_keys, + chain_logs: logs, + } + }); Ok(()) } @@ -187,7 +191,7 @@ impl Config { }; let operation = || async { - if Config::runtime().await.latest_ref().config.is_some() { + if Config::runtime().await.latest_arc().config.is_some() { return Ok::<(), BackoffError>(()); } @@ -228,7 +232,7 @@ mod tests { #[test] #[allow(unused_variables)] fn test_draft_size_non_boxed() { - let draft = Draft::from(IRuntime::new()); + let draft = Draft::new(IRuntime::new()); let iruntime_size = std::mem::size_of_val(&draft); assert_eq!(iruntime_size, std::mem::size_of::>()); } @@ -236,7 +240,7 @@ mod tests { #[test] #[allow(unused_variables)] fn test_draft_size_boxed() { - let draft = Draft::from(Box::new(IRuntime::new())); + let draft = Draft::new(Box::new(IRuntime::new())); let box_iruntime_size = std::mem::size_of_val(&draft); assert_eq!( box_iruntime_size, diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index d6b54b56..a649333b 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -584,6 +584,33 @@ impl PrfItem { } } +impl PrfItem { + /// 获取current指向的订阅的merge + pub fn current_merge(&self) -> Option { + self.option.as_ref().and_then(|o| o.merge.clone()) + } + + /// 获取current指向的订阅的script + pub fn current_script(&self) -> Option { + self.option.as_ref().and_then(|o| o.script.clone()) + } + + /// 获取current指向的订阅的rules + pub fn current_rules(&self) -> Option { + self.option.as_ref().and_then(|o| o.rules.clone()) + } + + /// 获取current指向的订阅的proxies + pub fn current_proxies(&self) -> Option { + self.option.as_ref().and_then(|o| o.proxies.clone()) + } + + /// 获取current指向的订阅的groups + pub fn current_groups(&self) -> Option { + self.option.as_ref().and_then(|o| o.groups.clone()) + } +} + // 向前兼容,默认为订阅启用自动更新 fn default_allow_auto_update() -> Option { Some(true) diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index cc7f5c80..51dd6172 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -8,7 +8,7 @@ use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; use serde_yaml_ng::Mapping; use smartstring::alias::String; -use std::collections::HashSet; +use std::{collections::HashSet, sync::Arc}; use tokio::fs; /// Define the `profiles.yaml` schema @@ -32,7 +32,7 @@ pub struct CleanupResult { macro_rules! patch { ($lv: expr, $rv: expr, $key: tt) => { if ($rv.$key).is_some() { - $lv.$key = $rv.$key.clone(); + $lv.$key = $rv.$key.to_owned(); } }; } @@ -50,28 +50,26 @@ impl IProfiles { } None } + 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![]); + let path = match dirs::profiles_path() { + Ok(p) => p, + Err(err) => { + logging!(error, Type::Config, "{err}"); + return Self::default(); + } + }; + + match help::read_yaml::(&path).await { + Ok(mut profiles) => { + let items = profiles.items.get_or_insert_with(Vec::new); + for item in items.iter_mut() { + if item.uid.is_none() { + item.uid = Some(help::get_uid("d").into()); } - // 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").into()); - } - } - } - profiles } - Err(err) => { - logging!(error, Type::Config, "{err}"); - Self::default() - } - }, + profiles + } Err(err) => { logging!(error, Type::Config, "{err}"); Self::default() @@ -106,8 +104,8 @@ impl IProfiles { Ok(()) } - pub fn get_current(&self) -> Option { - self.current.clone() + pub fn get_current(&self) -> Option<&String> { + self.current.as_ref() } /// get items ref @@ -132,6 +130,15 @@ impl IProfiles { bail!("failed to get the profile item \"uid:{}\"", uid_str); } + pub fn get_item_arc(&self, uid: &str) -> Option> { + self.items.as_ref().and_then(|items| { + items + .iter() + .find(|it| it.uid.as_deref() == Some(uid)) + .map(|it| Arc::new(it.clone())) + }) + } + /// append new item /// if the file_data is some /// then should save the data to file @@ -357,88 +364,18 @@ impl IProfiles { } } - /// 获取current指向的订阅的merge - pub fn current_merge(&self) -> Option { - 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)) { - let merge = item.option.as_ref().and_then(|e| e.merge.clone()); - return merge; - } - None - } - _ => None, - } - } - - /// 获取current指向的订阅的script - pub fn current_script(&self) -> Option { - 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)) { - let script = item.option.as_ref().and_then(|e| e.script.clone()); - return script; - } - None - } - _ => None, - } - } - - /// 获取current指向的订阅的rules - pub fn current_rules(&self) -> Option { - 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)) { - let rules = item.option.as_ref().and_then(|e| e.rules.clone()); - return rules; - } - None - } - _ => None, - } - } - - /// 获取current指向的订阅的proxies - pub fn current_proxies(&self) -> Option { - 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)) { - let proxies = item.option.as_ref().and_then(|e| e.proxies.clone()); - return proxies; - } - None - } - _ => None, - } - } - - /// 获取current指向的订阅的groups - pub fn current_groups(&self) -> Option { - 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)) { - let groups = item.option.as_ref().and_then(|e| e.groups.clone()); - return groups; - } - None - } - _ => None, - } - } - /// 判断profile是否是current指向的 pub fn is_current_profile_index(&self, index: &String) -> bool { self.current.as_ref() == Some(index) } /// 获取所有的profiles(uid,名称) - pub fn all_profile_uid_and_name(&self) -> Option> { + pub fn all_profile_uid_and_name(&self) -> Option> { self.items.as_ref().map(|items| { items .iter() .filter_map(|e| { - if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) { + if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) { Some((uid, name)) } else { None @@ -449,11 +386,11 @@ impl IProfiles { } /// 通过 uid 获取名称 - pub fn get_name_by_uid(&self, uid: &String) -> Option { + pub fn get_name_by_uid(&self, uid: &String) -> Option<&String> { if let Some(items) = &self.items { for item in items { if item.uid.as_ref() == Some(uid) { - return item.name.clone(); + return item.name.as_ref(); } } } @@ -551,14 +488,14 @@ impl IProfiles { } /// 获取所有 active profile 关联的文件名 - fn get_all_active_files(&self) -> HashSet { - let mut active_files = HashSet::new(); + fn get_all_active_files(&self) -> HashSet<&str> { + let mut active_files: HashSet<&str> = HashSet::new(); if let Some(items) = &self.items { for item in items { // 收集所有类型 profile 的文件 if let Some(file) = &item.file { - active_files.insert(file.clone()); + active_files.insert(file); } // 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件 @@ -571,35 +508,35 @@ impl IProfiles { && let Ok(merge_item) = self.get_item(merge_uid) && let Some(file) = &merge_item.file { - active_files.insert(file.clone()); + active_files.insert(file); } 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()); + active_files.insert(file); } 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()); + active_files.insert(file); } 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()); + active_files.insert(file); } 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()); + active_files.insert(file); } } } diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index 1e37cee4..5de30e9f 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -46,19 +46,24 @@ pub struct IVerge { pub enable_memory_usage: Option, /// enable group icon + #[serde(skip_serializing_if = "Option::is_none")] pub enable_group_icon: Option, /// common tray icon + #[serde(skip_serializing_if = "Option::is_none")] pub common_tray_icon: Option, /// tray icon #[cfg(target_os = "macos")] + #[serde(skip_serializing_if = "Option::is_none")] pub tray_icon: Option, /// menu icon + #[serde(skip_serializing_if = "Option::is_none")] pub menu_icon: Option, /// menu order + #[serde(skip_serializing_if = "Option::is_none")] pub menu_order: Option>, /// sysproxy tray icon @@ -115,6 +120,7 @@ pub struct IVerge { /// hotkey map /// format: {func},{key} + #[serde(skip_serializing_if = "Option::is_none")] pub hotkeys: Option>, /// enable global hotkey @@ -134,7 +140,7 @@ pub struct IVerge { pub default_latency_test: Option, /// 默认的延迟测试超时时间 - pub default_latency_timeout: Option, + pub default_latency_timeout: Option, /// 是否自动检测当前节点延迟 pub enable_auto_delay_detection: Option, @@ -143,7 +149,7 @@ pub struct IVerge { pub enable_builtin_enhanced: Option, /// proxy 页面布局 列数 - pub proxy_layout_column: Option, + pub proxy_layout_column: Option, /// 测试站列表 pub test_list: Option>, @@ -202,6 +208,7 @@ pub struct IVerge { )] pub webdav_password: Option, + #[serde(skip)] pub enable_tray_speed: Option, // pub enable_tray_icon: Option, @@ -313,7 +320,9 @@ impl IVerge { ); let config_draft = Config::verge().await; - **config_draft.draft_mut() = updated_config; + config_draft.edit_draft(|d| { + *d = updated_config; + }); config_draft.apply(); Ok(()) @@ -584,10 +593,10 @@ pub struct IVergeResponse { pub auto_close_connection: Option, pub auto_check_update: Option, pub default_latency_test: Option, - pub default_latency_timeout: Option, + pub default_latency_timeout: Option, pub enable_auto_delay_detection: Option, pub enable_builtin_enhanced: Option, - pub proxy_layout_column: Option, + pub proxy_layout_column: Option, pub test_list: Option>, pub auto_log_clean: Option, #[cfg(not(target_os = "windows"))] @@ -696,9 +705,3 @@ impl From for IVergeResponse { } } } - -impl From> for IVergeResponse { - fn from(verge: Box) -> Self { - IVergeResponse::from(*verge) - } -} diff --git a/src-tauri/src/core/backup.rs b/src-tauri/src/core/backup.rs index 6e48a137..3e94a733 100644 --- a/src-tauri/src/core/backup.rs +++ b/src-tauri/src/core/backup.rs @@ -87,7 +87,7 @@ impl WebDavClient { (*cfg_arc).clone() } else { // 释放锁后获取异步配置 - let verge = Config::verge().await.latest_ref().clone(); + let verge = Config::verge().await.latest_arc(); if verge.webdav_url.is_none() || verge.webdav_username.is_none() || verge.webdav_password.is_none() @@ -99,11 +99,13 @@ impl WebDavClient { let config = WebDavConfig { url: verge .webdav_url + .as_ref() + .cloned() .unwrap_or_default() .trim_end_matches('/') .into(), - username: verge.webdav_username.unwrap_or_default(), - password: verge.webdav_password.unwrap_or_default(), + username: verge.webdav_username.as_ref().cloned().unwrap_or_default(), + password: verge.webdav_password.as_ref().cloned().unwrap_or_default(), }; // 存储配置到 ArcSwapOption diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs index 18a6f155..c55593b9 100644 --- a/src-tauri/src/core/event_driven_proxy.rs +++ b/src-tauri/src/core/event_driven_proxy.rs @@ -387,7 +387,7 @@ impl EventDrivenProxyManager { async fn get_proxy_config() -> ProxyConfig { let (sys_enabled, pac_enabled, guard_enabled, guard_duration) = { let verge_config = Config::verge().await; - let verge = verge_config.latest_ref(); + let verge = verge_config.latest_arc(); ( verge.enable_system_proxy.unwrap_or(false), verge.proxy_auto_config.unwrap_or(false), @@ -406,7 +406,7 @@ impl EventDrivenProxyManager { async fn get_expected_pac_config() -> Autoproxy { let proxy_host = { let verge_config = Config::verge().await; - let verge = verge_config.latest_ref(); + let verge = verge_config.latest_arc(); verge .proxy_host .clone() @@ -424,13 +424,13 @@ impl EventDrivenProxyManager { let (verge_mixed_port, proxy_host) = { let verge_config = Config::verge().await; - let verge_ref = verge_config.latest_ref(); + let verge_ref = verge_config.latest_arc(); (verge_ref.verge_mixed_port, verge_ref.proxy_host.clone()) }; let default_port = { let clash_config = Config::clash().await; - clash_config.latest_ref().get_mixed_port() + clash_config.latest_arc().get_mixed_port() }; let port = verge_mixed_port.unwrap_or(default_port); @@ -450,7 +450,7 @@ impl EventDrivenProxyManager { use crate::constants::bypass; let verge_config = Config::verge().await; - let verge = verge_config.latest_ref(); + let verge = verge_config.latest_arc(); let use_default = verge.use_default_bypass.unwrap_or(true); let custom = verge.system_proxy_bypass.as_deref().unwrap_or(""); diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index ef868f59..f15cc0fb 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -102,6 +102,7 @@ impl Handle { Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid }); } + // TODO 利用 &str 等缩短 Clone pub fn notice_message, M: Into>(status: S, msg: M) { let handle = Self::global(); let status_str = status.into(); diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index f33a2cbc..0735c9e4 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -237,7 +237,7 @@ impl Hotkey { let is_enable_global_hotkey = Config::verge() .await - .latest_ref() + .latest_arc() .enable_global_hotkey .unwrap_or(true); @@ -274,7 +274,7 @@ singleton_with_logging!(Hotkey, INSTANCE, "Hotkey"); impl Hotkey { pub async fn init(&self, skip: bool) -> Result<()> { let verge = Config::verge().await; - let enable_global_hotkey = !skip && verge.latest_ref().enable_global_hotkey.unwrap_or(true); + let enable_global_hotkey = !skip && verge.latest_arc().enable_global_hotkey.unwrap_or(true); logging!( debug, @@ -284,7 +284,7 @@ impl Hotkey { ); // Extract hotkeys data before async operations - let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned(); + let hotkeys = verge.latest_arc().hotkeys.as_ref().cloned(); if let Some(hotkeys) = hotkeys { logging!( diff --git a/src-tauri/src/core/manager/config.rs b/src-tauri/src/core/manager/config.rs index b0e79bca..e0f9bd51 100644 --- a/src-tauri/src/core/manager/config.rs +++ b/src-tauri/src/core/manager/config.rs @@ -17,13 +17,15 @@ impl CoreManager { use crate::constants::files::RUNTIME_CONFIG; let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG); - let clash_config = Config::clash().await.latest_ref().0.clone(); + let clash_config = &Config::clash().await.latest_arc().0; - **Config::runtime().await.draft_mut() = IRuntime { - config: Some(clash_config.clone()), - exists_keys: vec![], - chain_logs: Default::default(), - }; + Config::runtime().await.edit_draft(|d| { + *d = IRuntime { + config: Some(clash_config.to_owned()), + exists_keys: vec![], + chain_logs: Default::default(), + } + }); help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?; handle::Handle::notice_message(error_key, error_msg); diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index 8f0c6a88..c299c1ef 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -47,10 +47,12 @@ impl CoreManager { return Err(format!("Invalid clash core: {}", clash_core).into()); } - Config::verge().await.draft_mut().clash_core = clash_core.to_owned().into(); + Config::verge().await.edit_draft(|d| { + d.clash_core = Some(clash_core.to_owned()); + }); Config::verge().await.apply(); - let verge_data = Config::verge().await.latest_ref().clone(); + let verge_data = Config::verge().await.latest_arc(); verge_data.save_file().await.map_err(|e| e.to_string())?; let run_path = Config::generate_file(ConfigType::Run) @@ -82,7 +84,7 @@ impl CoreManager { let needs_service = Config::verge() .await - .latest_ref() + .latest_arc() .enable_tun_mode .unwrap_or(false); diff --git a/src-tauri/src/core/manager/state.rs b/src-tauri/src/core/manager/state.rs index af92ce18..f13bc543 100644 --- a/src-tauri/src/core/manager/state.rs +++ b/src-tauri/src/core/manager/state.rs @@ -32,7 +32,7 @@ impl CoreManager { let config_file = Config::generate_file(crate::config::ConfigType::Run).await?; let app_handle = handle::Handle::app_handle(); - let clash_core = Config::verge().await.latest_ref().get_valid_clash_core(); + let clash_core = Config::verge().await.latest_arc().get_valid_clash_core(); let config_dir = dirs::app_home_dir()?; let (mut rx, child) = app_handle diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 04ded18a..427015d8 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -353,7 +353,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result logging!(info, Type::Service, "尝试使用现有服务启动核心"); let verge_config = Config::verge().await; - let clash_core = verge_config.latest_ref().get_valid_clash_core(); + let clash_core = verge_config.latest_arc().get_valid_clash_core(); drop(verge_config); let bin_ext = if cfg!(windows) { ".exe" } else { "" }; @@ -532,7 +532,7 @@ impl ServiceManager { return Err(anyhow::anyhow!("服务不可用: {}", reason)); } } - let _ = tray::Tray::global().update_tray_display().await; + let _ = tray::Tray::global().update_menu().await; Ok(()) } } diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index c24d8be9..bc280578 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -31,12 +31,12 @@ static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12 async fn get_bypass() -> String { let use_default = Config::verge() .await - .latest_ref() + .latest_arc() .use_default_bypass .unwrap_or(true); let res = { let verge = Config::verge().await; - let verge = verge.latest_ref(); + let verge = verge.latest_arc(); verge.system_proxy_bypass.clone() }; let custom_bypass = match res { @@ -124,17 +124,17 @@ impl Sysopt { } let port = { - let verge_port = Config::verge().await.latest_ref().verge_mixed_port; + let verge_port = Config::verge().await.latest_arc().verge_mixed_port; match verge_port { Some(port) => port, - None => Config::clash().await.latest_ref().get_mixed_port(), + None => Config::clash().await.latest_arc().get_mixed_port(), } }; let pac_port = IVerge::get_singleton_port(); let (sys_enable, pac_enable, proxy_host) = { let verge = Config::verge().await; - let verge = verge.latest_ref(); + let verge = verge.latest_arc(); ( verge.enable_system_proxy.unwrap_or(false), verge.proxy_auto_config.unwrap_or(false), @@ -266,7 +266,7 @@ impl Sysopt { /// update the startup pub async fn update_launch(&self) -> Result<()> { - let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch }; + let enable_auto_launch = { Config::verge().await.latest_arc().enable_auto_launch }; let is_enable = enable_auto_launch.unwrap_or(false); logging!( info, diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index e8506df1..a3eb9791 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -100,7 +100,7 @@ impl Timer { // Collect profiles that need immediate update let profiles_to_update = - if let Some(items) = Config::profiles().await.latest_ref().get_items() { + if let Some(items) = Config::profiles().await.latest_arc().get_items() { items .iter() .filter_map(|item| { @@ -161,9 +161,7 @@ impl Timer { .set_frequency_count_down_by_seconds(3, 3) .spawn_async_routine(|| async move { logging!(debug, Type::Timer, "Updating tray menu"); - crate::core::tray::Tray::global() - .update_tray_display() - .await + crate::core::tray::Tray::global().update_menu().await }) .context("failed to create update tray menu timer task")?; delay_timer @@ -273,7 +271,7 @@ impl Timer { async fn gen_map(&self) -> HashMap { let mut new_map = HashMap::new(); - if let Some(items) = Config::profiles().await.latest_ref().get_items() { + if let Some(items) = Config::profiles().await.latest_arc().get_items() { for item in items.iter() { if let Some(option) = item.option.as_ref() && let Some(allow_auto_update) = option.allow_auto_update @@ -427,7 +425,7 @@ impl Timer { // Get the profile updated timestamp - now safe to await let items = { let profiles = Config::profiles().await; - let profiles_guard = profiles.latest_ref(); + let profiles_guard = profiles.latest_arc(); match profiles_guard.get_items() { Some(i) => i.clone(), None => { @@ -489,7 +487,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().await.latest_ref().current.as_ref() == Some(uid); + let is_current = Config::profiles().await.latest_arc().current.as_ref() == Some(uid); logging!( info, Type::Timer, diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index ef300bbe..6b91f06a 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -1,5 +1,4 @@ use once_cell::sync::OnceCell; -use tauri::Emitter; use tauri::tray::TrayIconBuilder; use tauri_plugin_mihomo::models::Proxies; use tokio::fs; @@ -86,7 +85,7 @@ pub struct Tray { impl TrayState { pub async fn get_common_tray_icon() -> (bool, Vec) { - let verge = Config::verge().await.latest_ref().clone(); + let verge = Config::verge().await.latest_arc(); let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false); if is_common_tray_icon && let Ok(Some(common_icon_path)) = find_target_icons("common") @@ -123,7 +122,7 @@ impl TrayState { } pub async fn get_sysproxy_tray_icon() -> (bool, Vec) { - let verge = Config::verge().await.latest_ref().clone(); + let verge = Config::verge().await.latest_arc(); let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false); if is_sysproxy_tray_icon && let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") @@ -160,7 +159,7 @@ impl TrayState { } pub async fn get_tun_tray_icon() -> (bool, Vec) { - let verge = Config::verge().await.latest_ref().clone(); + let verge = Config::verge().await.latest_arc(); let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false); if is_tun_tray_icon && let Ok(Some(tun_icon_path)) = find_target_icons("tun") @@ -243,7 +242,7 @@ impl Tray { } let app_handle = handle::Handle::app_handle(); - let tray_event = { Config::verge().await.latest_ref().tray_event.clone() }; + let tray_event = { Config::verge().await.latest_arc().tray_event.clone() }; let tray_event = tray_event.unwrap_or_else(|| "main_window".into()); let tray = app_handle .tray_by_id("main") @@ -303,7 +302,7 @@ impl Tray { } async fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> { - let verge = Config::verge().await.latest_ref().clone(); + let verge = Config::verge().await.latest_arc(); 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 tun_mode_available = cmd::system::is_admin().unwrap_or_default() @@ -311,18 +310,16 @@ impl Tray { let mode = { Config::clash() .await - .latest_ref() + .latest_arc() .0 .get("mode") .map(|val| val.as_str().unwrap_or("rule")) .unwrap_or("rule") .to_owned() }; - let profile_uid_and_name = Config::profiles() - .await - .data_mut() - .all_profile_uid_and_name() - .unwrap_or_default(); + let profiles_config = Config::profiles().await; + let profiles_arc = profiles_config.latest_arc(); + let profile_uid_and_name = profiles_arc.all_profile_uid_and_name().unwrap_or_default(); let is_lightweight_mode = is_in_lightweight_mode(); match app_handle.tray_by_id("main") { @@ -375,7 +372,7 @@ impl Tray { } }; - let verge = Config::verge().await.latest_ref().clone(); + let verge = Config::verge().await.latest_arc(); let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); @@ -418,7 +415,7 @@ impl Tray { } }; - let verge = Config::verge().await.latest_ref().clone(); + let verge = Config::verge().await.latest_arc(); let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); @@ -433,24 +430,6 @@ impl Tray { Ok(()) } - /// 更新托盘显示状态的函数 - pub async fn update_tray_display(&self) -> Result<()> { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Tray, "应用正在退出,跳过托盘显示状态更新"); - return Ok(()); - } - - let app_handle = handle::Handle::app_handle(); - let _tray = app_handle - .tray_by_id("main") - .ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?; - - // 更新菜单 - self.update_menu().await?; - - Ok(()) - } - /// 更新托盘提示 pub async fn update_tooltip(&self) -> Result<()> { if handle::Handle::global().is_exiting() { @@ -460,7 +439,7 @@ impl Tray { let app_handle = handle::Handle::app_handle(); - let verge = Config::verge().await.latest_ref().clone(); + let verge = Config::verge().await.latest_arc(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); @@ -474,9 +453,9 @@ impl Tray { let mut current_profile_name = "None".into(); { let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); + let profiles = profiles.latest_arc(); if let Some(current_profile_uid) = profiles.get_current() - && let Ok(profile) = profiles.get_item(¤t_profile_uid) + && let Ok(profile) = profiles.get_item(current_profile_uid) { current_profile_name = match &profile.name { Some(profile_name) => profile_name.to_string(), @@ -525,9 +504,7 @@ impl Tray { logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新"); return Ok(()); } - // self.update_menu().await?; - // 更新轻量模式显示状态 - self.update_tray_display().await?; + self.update_menu().await?; self.update_icon().await?; self.update_tooltip().await?; Ok(()) @@ -552,7 +529,7 @@ impl Tray { #[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 = { Config::verge().await.latest_arc().tray_event.clone() }; let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into()); tray_event.as_str() == "tray_menu" }; @@ -583,7 +560,7 @@ impl Tray { } AsyncHandler::spawn(|| async move { - let tray_event = { Config::verge().await.latest_ref().tray_event.clone() }; + let tray_event = { Config::verge().await.latest_arc().tray_event.clone() }; let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into()); logging!(debug, Type::Tray, "tray event: {tray_event:?}"); @@ -622,22 +599,6 @@ impl Tray { tray.on_menu_event(on_menu_event); Ok(()) } - - // 托盘统一的状态更新函数 - pub async fn update_all_states(&self) -> Result<()> { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Tray, "应用正在退出,跳过托盘状态更新"); - return Ok(()); - } - - // 确保所有状态更新完成 - self.update_tray_display().await?; - // self.update_menu().await?; - self.update_icon().await?; - self.update_tooltip().await?; - - Ok(()) - } } fn create_hotkeys(hotkeys: &Option>) -> HashMap { @@ -666,7 +627,7 @@ fn create_hotkeys(hotkeys: &Option>) -> HashMap { async fn create_profile_menu_item( app_handle: &AppHandle, - profile_uid_and_name: Vec<(String, String)>, + profile_uid_and_name: Vec<(&String, &String)>, ) -> Result>> { let futures = profile_uid_and_name .iter() @@ -675,7 +636,7 @@ async fn create_profile_menu_item( async move { let is_current_profile = Config::profiles() .await - .latest_ref() + .latest_arc() .is_current_profile_index(profile_uid); CheckMenuItem::with_id( &app_handle, @@ -870,7 +831,7 @@ async fn create_tray_menu( system_proxy_enabled: bool, tun_mode_enabled: bool, tun_mode_available: bool, - profile_uid_and_name: Vec<(String, String)>, + profile_uid_and_name: Vec<(&String, &String)>, is_lightweight_mode: bool, ) -> Result> { let current_proxy_mode = mode.unwrap_or(""); @@ -878,10 +839,10 @@ async fn create_tray_menu( // 获取当前配置文件的选中代理组信息 let current_profile_selected = { let profiles_config = Config::profiles().await; - let profiles_ref = profiles_config.latest_ref(); + let profiles_ref = profiles_config.latest_arc(); profiles_ref .get_current() - .and_then(|uid| profiles_ref.get_item(&uid).ok()) + .and_then(|uid| profiles_ref.get_item(uid).ok()) .and_then(|profile| profile.selected.clone()) .unwrap_or_default() }; @@ -924,7 +885,7 @@ async fn create_tray_menu( .collect::>() }); - let verge_settings = Config::verge().await.latest_ref().clone(); + let verge_settings = Config::verge().await.latest_arc(); let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false); let version = env!("CARGO_PKG_VERSION"); @@ -1255,66 +1216,20 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { } id if id.starts_with("proxy_") => { // proxy_{group_name}_{proxy_name} - let parts: Vec<&str> = id.splitn(3, '_').collect(); - - if parts.len() == 3 && parts[0] == "proxy" { - let group_name = parts[1]; - let proxy_name = parts[2]; - - match handle::Handle::mihomo() - .await - .select_node_for_group(group_name, proxy_name) - .await - { - Ok(_) => { - logging!( - info, - Type::Tray, - "切换代理成功: {} -> {}", - group_name, - proxy_name - ); - let _ = handle::Handle::app_handle() - .emit("verge://refresh-proxy-config", ()); - } - Err(e) => { - logging!( - error, - Type::Tray, - "切换代理失败: {} -> {}, 错误: {:?}", - group_name, - proxy_name, - e - ); - - // Fallback to IPC update - if (handle::Handle::mihomo() - .await - .select_node_for_group(group_name, proxy_name) - .await) - .is_ok() - { - logging!( - info, - Type::Tray, - "代理切换回退成功: {} -> {}", - group_name, - proxy_name - ); - - let app_handle = handle::Handle::app_handle(); - let _ = app_handle.emit("verge://force-refresh-proxies", ()); - } - } - } - } + let rest = match id.strip_prefix("proxy_") { + Some(r) => r, + None => return, + }; + let (group_name, proxy_name) = match rest.split_once('_') { + Some((g, p)) => (g, p), + None => return, + }; + feat::switch_proxy_node(group_name, proxy_name).await; } _ => {} } - // Ensure tray state update is awaited and properly handled - if let Err(e) = Tray::global().update_all_states().await { - logging!(warn, Type::Tray, "Failed to update tray state: {e}"); - } + // We dont expected to refresh tray state here + // as the inner handle function (SHOULD) already takes care of it }); } diff --git a/src-tauri/src/core/validate.rs b/src-tauri/src/core/validate.rs index 204497a0..562ebd24 100644 --- a/src-tauri/src/core/validate.rs +++ b/src-tauri/src/core/validate.rs @@ -266,7 +266,7 @@ impl CoreConfigValidator { logging!(info, Type::Validate, "开始验证配置文件: {}", config_path); - let clash_core = Config::verge().await.latest_ref().get_valid_clash_core(); + let clash_core = Config::verge().await.latest_arc().get_valid_clash_core(); logging!(info, Type::Validate, "使用内核: {}", clash_core); let app_handle = handle::Handle::app_handle(); diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index e9183170..f2e5a191 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -44,12 +44,49 @@ struct ProfileItems { profile_name: String, } +impl Default for ProfileItems { + fn default() -> Self { + Self { + config: Default::default(), + profile_name: Default::default(), + merge_item: ChainItem { + uid: "".into(), + data: ChainType::Merge(Mapping::new()), + }, + script_item: ChainItem { + uid: "".into(), + data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), + }, + rules_item: ChainItem { + uid: "".into(), + data: ChainType::Rules(SeqMap::default()), + }, + proxies_item: ChainItem { + uid: "".into(), + data: ChainType::Proxies(SeqMap::default()), + }, + groups_item: ChainItem { + uid: "".into(), + data: ChainType::Groups(SeqMap::default()), + }, + global_merge: ChainItem { + uid: "Merge".into(), + data: ChainType::Merge(Mapping::new()), + }, + global_script: ChainItem { + uid: "Script".into(), + data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), + }, + } + } +} + async fn get_config_values() -> ConfigValues { - let clash_config = { Config::clash().await.latest_ref().0.clone() }; + let clash_config = { Config::clash().await.latest_arc().0.clone() }; let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = { let verge = Config::verge().await; - let verge = verge.latest_ref(); + let verge = verge.latest_arc(); ( Some(verge.get_valid_clash_core()), verge.enable_tun_mode.unwrap_or(false), @@ -63,14 +100,14 @@ async fn get_config_values() -> ConfigValues { #[cfg(not(target_os = "windows"))] let redir_enabled = { let verge = Config::verge().await; - let verge = verge.latest_ref(); + let verge = verge.latest_arc(); verge.verge_redir_enabled.unwrap_or(false) }; #[cfg(target_os = "linux")] let tproxy_enabled = { let verge = Config::verge().await; - let verge = verge.latest_ref(); + let verge = verge.latest_arc(); verge.verge_tproxy_enabled.unwrap_or(false) }; @@ -89,33 +126,43 @@ async fn get_config_values() -> ConfigValues { } } +#[allow(clippy::cognitive_complexity)] async fn collect_profile_items() -> ProfileItems { // 从profiles里拿东西 - 先收集需要的数据,然后释放锁 - let ( - current, - merge_uid, - script_uid, - rules_uid, - proxies_uid, - groups_uid, - _current_profile_uid, - name, - ) = { + let (current, merge_uid, script_uid, rules_uid, proxies_uid, groups_uid, name) = { let current = { let profiles = Config::profiles().await; - let profiles_clone = profiles.latest_ref().clone(); + let profiles_clone = profiles.latest_arc(); profiles_clone.current_mapping().await.unwrap_or_default() }; let profiles = Config::profiles().await; - let profiles_ref = profiles.latest_ref(); + let profiles_ref = profiles.latest_arc(); + let current_profile_uid = match profiles_ref.get_current() { + Some(uid) => uid.clone(), + None => return ProfileItems::default(), + }; - 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 current_item = match profiles_ref.get_item_arc(¤t_profile_uid) { + Some(item) => item, + None => return ProfileItems::default(), + }; + + let merge_uid = current_item + .current_merge() + .unwrap_or_else(|| "Merge".into()); + let script_uid = current_item + .current_script() + .unwrap_or_else(|| "Script".into()); + let rules_uid = current_item + .current_rules() + .unwrap_or_else(|| "Rules".into()); + let proxies_uid = current_item + .current_proxies() + .unwrap_or_else(|| "Proxies".into()); + let groups_uid = current_item + .current_groups() + .unwrap_or_else(|| "Groups".into()); let name = profiles_ref .get_item(¤t_profile_uid) @@ -130,7 +177,6 @@ async fn collect_profile_items() -> ProfileItems { rules_uid, proxies_uid, groups_uid, - current_profile_uid, name, ) }; @@ -139,8 +185,8 @@ async fn collect_profile_items() -> ProfileItems { let merge_item = { let item = { let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(merge_uid).ok().cloned() + let profiles = profiles.latest_arc(); + profiles.get_item(&merge_uid).ok().cloned() }; if let Some(item) = item { >::from_async(&item).await @@ -156,8 +202,8 @@ async fn collect_profile_items() -> ProfileItems { let script_item = { let item = { let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(script_uid).ok().cloned() + let profiles = profiles.latest_arc(); + profiles.get_item(&script_uid).ok().cloned() }; if let Some(item) = item { >::from_async(&item).await @@ -173,8 +219,8 @@ async fn collect_profile_items() -> ProfileItems { let rules_item = { let item = { let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(rules_uid).ok().cloned() + let profiles = profiles.latest_arc(); + profiles.get_item(&rules_uid).ok().cloned() }; if let Some(item) = item { >::from_async(&item).await @@ -190,8 +236,8 @@ async fn collect_profile_items() -> ProfileItems { let proxies_item = { let item = { let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(proxies_uid).ok().cloned() + let profiles = profiles.latest_arc(); + profiles.get_item(&proxies_uid).ok().cloned() }; if let Some(item) = item { >::from_async(&item).await @@ -207,8 +253,8 @@ async fn collect_profile_items() -> ProfileItems { let groups_item = { let item = { let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(groups_uid).ok().cloned() + let profiles = profiles.latest_arc(); + profiles.get_item(&groups_uid).ok().cloned() }; if let Some(item) = item { >::from_async(&item).await @@ -224,7 +270,7 @@ async fn collect_profile_items() -> ProfileItems { let global_merge = { let item = { let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); + let profiles = profiles.latest_arc(); profiles.get_item("Merge").ok().cloned() }; if let Some(item) = item { @@ -241,7 +287,7 @@ async fn collect_profile_items() -> ProfileItems { let global_script = { let item = { let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); + let profiles = profiles.latest_arc(); profiles.get_item("Script").ok().cloned() }; if let Some(item) = item { @@ -394,7 +440,7 @@ async fn merge_default_config( if key.as_str() == Some("external-controller") { let enable_external_controller = Config::verge() .await - .latest_ref() + .latest_arc() .enable_external_controller .unwrap_or(false); diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index 4ccdecab..2aac5505 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -78,7 +78,7 @@ 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().await; - let verge_data = verge.latest_ref().clone(); + let verge_data = verge.latest_arc(); let webdav_url = verge_data.webdav_url.clone(); let webdav_username = verge_data.webdav_username.clone(); let webdav_password = verge_data.webdav_password.clone(); @@ -243,7 +243,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> { let (webdav_url, webdav_username, webdav_password) = { let verge = Config::verge().await; - let verge = verge.latest_ref(); + let verge = verge.latest_arc(); ( verge.webdav_url.clone(), verge.webdav_username.clone(), diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index cf031d78..6551d440 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -72,10 +72,12 @@ pub async fn change_clash_mode(mode: String) { { Ok(_) => { // 更新订阅 - Config::clash().await.data_mut().patch_config(mapping); + Config::clash() + .await + .edit_draft(|d| d.patch_config(mapping)); // 分离数据获取和异步调用 - let clash_data = Config::clash().await.data_mut().clone(); + let clash_data = Config::clash().await.data_arc(); if clash_data.save_config().await.is_ok() { handle::Handle::refresh_clash(); logging_error!(Type::Tray, tray::Tray::global().update_menu().await); @@ -84,7 +86,7 @@ pub async fn change_clash_mode(mode: String) { let is_auto_close_connection = Config::verge() .await - .data_mut() + .data_arc() .auto_close_connection .unwrap_or(false); if is_auto_close_connection { @@ -102,7 +104,7 @@ pub async fn test_delay(url: String) -> anyhow::Result { let tun_mode = Config::verge() .await - .latest_ref() + .latest_arc() .enable_tun_mode .unwrap_or(false); diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index 1ca9669c..2bd2cfa8 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -3,7 +3,7 @@ use crate::{ core::{CoreManager, handle, hotkey, sysopt, tray}, logging_error, module::lightweight, - utils::logging::Type, + utils::{draft::SharedBox, logging::Type}, }; use anyhow::Result; use serde_yaml_ng::Mapping; @@ -12,8 +12,7 @@ use serde_yaml_ng::Mapping; pub async fn patch_clash(patch: Mapping) -> Result<()> { Config::clash() .await - .draft_mut() - .patch_config(patch.clone()); + .edit_draft(|d| d.patch_config(patch.clone())); let res = { // 激活订阅 @@ -25,7 +24,9 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> { logging_error!(Type::Tray, tray::Tray::global().update_menu().await); logging_error!(Type::Tray, tray::Tray::global().update_icon().await); } - Config::runtime().await.draft_mut().patch_config(patch); + Config::runtime() + .await + .edit_draft(|d| d.patch_config(patch)); CoreManager::global().update_config().await?; } handle::Handle::refresh_clash(); @@ -35,7 +36,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> { Ok(()) => { Config::clash().await.apply(); // 分离数据获取和异步调用 - let clash_data = Config::clash().await.data_mut().clone(); + let clash_data = Config::clash().await.data_arc(); clash_data.save_config().await?; Ok(()) } @@ -190,7 +191,9 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<( handle::Handle::refresh_clash(); } if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 { - Config::verge().await.draft_mut().enable_global_hotkey = patch.enable_global_hotkey; + Config::verge() + .await + .edit_draft(|d| d.enable_global_hotkey = patch.enable_global_hotkey); handle::Handle::refresh_verge(); } if (update_flags & (UpdateFlags::Launch as i32)) != 0 { @@ -227,7 +230,7 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<( } pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> { - Config::verge().await.draft_mut().patch_config(patch); + Config::verge().await.edit_draft(|d| d.patch_config(patch)); let update_flags = determine_update_flags(patch); let process_flag_result: std::result::Result<(), anyhow::Error> = { @@ -242,8 +245,14 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> { Config::verge().await.apply(); if !not_save_file { // 分离数据获取和异步调用 - let verge_data = Config::verge().await.data_ref().clone(); + let verge_data = Config::verge().await.data_arc(); verge_data.save_file().await?; } Ok(()) } + +pub async fn fetch_verge_config() -> Result> { + let draft = Config::verge().await; + let data = draft.data_arc(); + Ok(data) +} diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index f3832e38..b8c93655 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -7,6 +7,7 @@ use crate::{ }; use anyhow::{Result, bail}; use smartstring::alias::String; +use tauri::Emitter; /// Toggle proxy profile pub async fn toggle_proxy_profile(profile_index: String) { @@ -23,12 +24,72 @@ pub async fn toggle_proxy_profile(profile_index: String) { } } +pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) { + match handle::Handle::mihomo() + .await + .select_node_for_group(group_name, proxy_name) + .await + { + Ok(_) => { + logging!( + info, + Type::Tray, + "切换代理成功: {} -> {}", + group_name, + proxy_name + ); + let _ = handle::Handle::app_handle().emit("verge://refresh-proxy-config", ()); + let _ = tray::Tray::global().update_menu().await; + return; + } + Err(err) => { + logging!( + error, + Type::Tray, + "切换代理失败: {} -> {}, 错误: {:?}", + group_name, + proxy_name, + err + ); + } + } + + match handle::Handle::mihomo() + .await + .select_node_for_group(group_name, proxy_name) + .await + { + Ok(_) => { + logging!( + info, + Type::Tray, + "代理切换回退成功: {} -> {}", + group_name, + proxy_name + ); + let _ = handle::Handle::app_handle().emit("verge://force-refresh-proxies", ()); + let _ = tray::Tray::global().update_menu().await; + } + Err(err) => { + logging!( + error, + Type::Tray, + "代理切换最终失败: {} -> {}, 错误: {:?}", + group_name, + proxy_name, + err + ); + let _ = handle::Handle::app_handle().emit("verge://force-refresh-proxies", ()); + } + } +} + async fn should_update_profile( uid: &String, ignore_auto_update: bool, ) -> Result)>> { let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); + let profiles = profiles.latest_arc(); let item = profiles.get_item(uid)?; let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote"); @@ -89,15 +150,15 @@ async fn perform_profile_update( let mut merged_opt = PrfOption::merge(opt, option); let is_current = { let profiles = Config::profiles().await; - profiles.latest_ref().is_current_profile_index(uid) - }; - let profile_name = { - let profiles = Config::profiles().await; - profiles - .latest_ref() - .get_name_by_uid(uid) - .unwrap_or_default() + profiles.latest_arc().is_current_profile_index(uid) }; + let profiles = Config::profiles().await; + let profiles_arc = profiles.latest_arc(); + let profile_name = profiles_arc + .get_name_by_uid(uid) + .cloned() + .unwrap_or_else(|| String::from("UnKown Profile")); + let mut last_err; match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await { diff --git a/src-tauri/src/feat/proxy.rs b/src-tauri/src/feat/proxy.rs index 731cec20..9f7d513e 100644 --- a/src-tauri/src/feat/proxy.rs +++ b/src-tauri/src/feat/proxy.rs @@ -10,8 +10,8 @@ use tauri_plugin_clipboard_manager::ClipboardExt; /// Toggle system proxy on/off pub async fn toggle_system_proxy() { let verge = Config::verge().await; - let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false); - let auto_close_connection = verge.latest_ref().auto_close_connection.unwrap_or(false); + let enable = verge.latest_arc().enable_system_proxy.unwrap_or(false); + let auto_close_connection = verge.latest_arc().auto_close_connection.unwrap_or(false); // 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接 if enable @@ -42,7 +42,7 @@ pub async fn toggle_system_proxy() { /// Toggle TUN mode on/off pub async fn toggle_tun_mode(not_save_file: Option) { - let enable = Config::verge().await.data_mut().enable_tun_mode; + let enable = Config::verge().await.latest_arc().enable_tun_mode; let enable = enable.unwrap_or(false); match super::patch_verge( @@ -66,7 +66,7 @@ pub async fn copy_clash_env() { Ok(ip) => ip.into(), Err(_) => Config::verge() .await - .latest_ref() + .latest_arc() .proxy_host .clone() .unwrap_or_else(|| "127.0.0.1".into()), @@ -76,7 +76,7 @@ pub async fn copy_clash_env() { let port = { Config::verge() .await - .latest_ref() + .latest_arc() .verge_mixed_port .unwrap_or(7897) }; @@ -84,7 +84,7 @@ pub async fn copy_clash_env() { let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}"); let cliboard = app_handle.clipboard(); - let env_type = { Config::verge().await.latest_ref().env_type.clone() }; + let env_type = { Config::verge().await.latest_arc().env_type.clone() }; let env_type = match env_type { Some(env_type) => env_type, None => { diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index a23a41f2..8352fea1 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -47,7 +47,7 @@ pub async fn clean_async() -> bool { let tun_task = async { let tun_enabled = Config::verge() .await - .latest_ref() + .latest_arc() .enable_tun_mode .unwrap_or(false); @@ -100,7 +100,7 @@ pub async fn clean_async() -> bool { // 检查系统代理是否开启 let sys_proxy_enabled = Config::verge() .await - .latest_ref() + .latest_arc() .enable_system_proxy .unwrap_or(false); @@ -176,7 +176,7 @@ pub async fn clean_async() -> bool { { let sys_proxy_enabled = Config::verge() .await - .latest_ref() + .latest_arc() .enable_system_proxy .unwrap_or(false); @@ -316,7 +316,7 @@ pub async fn hide() { let enable_auto_light_weight_mode = Config::verge() .await - .data_mut() + .latest_arc() .enable_auto_light_weight_mode .unwrap_or(false); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index da6c5710..7555e477 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -319,7 +319,7 @@ pub fn run() { AsyncHandler::spawn(move || async move { let is_enable_global_hotkey = Config::verge() .await - .latest_ref() + .latest_arc() .enable_global_hotkey .unwrap_or(true); @@ -360,7 +360,7 @@ pub fn run() { let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW); let is_enable_global_hotkey = Config::verge() .await - .latest_ref() + .latest_arc() .enable_global_hotkey .unwrap_or(true); if !is_enable_global_hotkey { diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index 9390639f..86017df7 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -79,7 +79,7 @@ pub fn is_in_lightweight_mode() -> bool { } async fn refresh_lightweight_tray_state() { - if let Err(err) = Tray::global().update_tray_display().await { + if let Err(err) = Tray::global().update_menu().await { logging!(warn, Type::Lightweight, "更新托盘轻量模式状态失败: {err}"); } } @@ -87,11 +87,11 @@ async fn refresh_lightweight_tray_state() { pub async fn auto_lightweight_boot() -> Result<()> { let verge_config = Config::verge().await; let enable_auto = verge_config - .data_mut() + .latest_arc() .enable_auto_light_weight_mode .unwrap_or(false); let is_silent_start = verge_config - .latest_ref() + .latest_arc() .enable_silent_start .unwrap_or(false); @@ -236,7 +236,7 @@ async fn setup_light_weight_timer() -> Result<()> { let once_by_minutes = Config::verge() .await - .latest_ref() + .latest_arc() .auto_light_weight_minutes .unwrap_or(10); diff --git a/src-tauri/src/utils/draft.rs b/src-tauri/src/utils/draft.rs index c34bbfb6..52d9119d 100644 --- a/src-tauri/src/utils/draft.rs +++ b/src-tauri/src/utils/draft.rs @@ -1,179 +1,368 @@ +use parking_lot::RwLock; use std::sync::Arc; -use parking_lot::{ - MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard, - RwLockUpgradableReadGuard, RwLockWriteGuard, -}; +pub type SharedBox = Arc>; +type DraftInner = (SharedBox, Option>); +/// Draft 管理:committed 与 optional draft 都以 Arc> 存储, +// (committed_snapshot, optional_draft_snapshot) #[derive(Debug, Clone)] -pub struct Draft { - inner: Arc)>>, +pub struct Draft { + inner: Arc>>, } -impl From for Draft { - fn from(data: T) -> Self { +impl Draft { + pub fn new(data: T) -> Self { Self { - inner: Arc::new(RwLock::new((data, None))), + inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))), } } -} - -/// Implements draft management for `Box`, allowing for safe concurrent editing and committing of draft data. -/// # Type Parameters -/// - `T`: The underlying data type, which must implement `Clone` and `ToOwned`. -/// -/// # Methods -/// - `data_mut`: Returns a mutable reference to the committed data. -/// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists. -/// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data. -/// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed. -/// - `discard`: Discards the draft data and returns it if it existed. -impl Draft> { - /// 正式数据视图 - pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box> { - RwLockReadGuard::map(self.inner.read(), |inner| &inner.0) + /// 以 Arc> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc) + pub fn data_arc(&self) -> SharedBox { + let guard = self.inner.read(); + Arc::clone(&guard.0) } - /// 可写正式数据 - pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box> { - RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0) + /// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照 + /// 这也是零拷贝:只 clone Arc,不 clone T + pub fn latest_arc(&self) -> SharedBox { + let guard = self.inner.read(); + guard + .1 + .as_ref() + .cloned() + .unwrap_or_else(|| Arc::clone(&guard.0)) } - /// 创建或获取草稿并返回可写引用 - pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box> { - let guard = self.inner.upgradable_read(); + /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T) + /// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T; + /// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。 + pub fn edit_draft(&self, f: F) -> R + where + F: FnOnce(&mut T) -> R, + { + // 先获得写锁以创建或取出草稿 Arc 的可变引用位置 + let mut guard = self.inner.write(); 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_or_else(|| { - unreachable!("Draft was just created above, this should never fail") - }) - }); + // 创建草稿 snapshot(Arc clone,cheap) + guard.1 = Some(Arc::clone(&guard.0)); } - // 已存在草稿,升级为写锁映射 - RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| { - inner - .1 - .as_mut() - .unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()")) - }) + // 此时 guaranteed: guard.1 is Some(Arc>) + #[allow(clippy::unwrap_used)] + let arc_box = guard.1.as_mut().unwrap(); + // Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box(要求 T: Clone) + let boxed = Arc::make_mut(arc_box); // &mut Box + // 对 Box 解引用得到 &mut T + + f(&mut **boxed) } - /// 零拷贝只读视图:返回草稿(若存在)或正式值 - pub fn latest_ref(&self) -> MappedRwLockReadGuard<'_, Box> { - RwLockReadGuard::map(self.inner.read(), |inner| { - inner.1.as_ref().unwrap_or(&inner.0) - }) - } - - /// 提交草稿,返回旧正式数据 + /// 将草稿提交到已提交位置(替换),并清除草稿 pub fn apply(&self) { - let guard = self.inner.upgradable_read(); - if guard.1.is_none() { - return; - } - - let mut guard = RwLockUpgradableReadGuard::upgrade(guard); - if let Some(draft) = guard.1.take() { - guard.0 = draft; + let mut guard = self.inner.write(); + if let Some(d) = guard.1.take() { + guard.0 = d; } } - /// 丢弃草稿,返回被丢弃的草稿 + /// 丢弃草稿(如果存在) pub fn discard(&self) { - self.inner.write().1.take(); + let mut guard = self.inner.write(); + guard.1 = None; } - /// 异步修改正式数据,闭包直接获得 Box 所有权 - pub async fn with_data_modify(&self, f: F) -> Result + /// 异步地以拥有 Box 的方式修改已提交数据:将克隆一次已提交数据到本地, + /// 异步闭包返回新的 Box(替换已提交数据)和业务返回值 R。 + pub async fn with_data_modify(&self, f: F) -> Result where T: Send + Sync + 'static, F: FnOnce(Box) -> Fut + Send, - Fut: std::future::Future, R), E>> + Send, - E: From, + Fut: std::future::Future, R), anyhow::Error>> + Send, { - // 克隆正式数据 - let local = { + // 读取已提交快照(cheap Arc clone, 然后得到 Box 所有权 via clone) + // 注意:为了让闭包接收 Box 所有权,我们需要 clone 底层 T(不可避免) + let local: Box = { let guard = self.inner.read(); - guard.0.clone() + // 将 Arc> 的 Box clone 出来(会调用 T: Clone) + (*guard.0).clone() }; - // 异步闭包执行,返回修改后的 Box 和业务结果 R let (new_local, res) = f(local).await?; - // 写回正式数据 + // 将新的 Box 放到已提交位置(包进 Arc) let mut guard = self.inner.write(); - guard.0 = new_local; + guard.0 = Arc::new(new_local); Ok(res) } } -#[test] -fn test_draft_box() { - use crate::config::IVerge; +#[cfg(test)] +mod tests { + use super::*; + use anyhow::anyhow; + use std::future::Future; + use std::pin::Pin; + use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; - // 1. 创建 Draft> - let verge = Box::new(IVerge { - enable_auto_launch: Some(true), - enable_tun_mode: Some(false), - ..IVerge::default() - }); - let draft = Draft::from(verge); - - // 2. 读取正式数据(data_mut) - { - let data = draft.data_mut(); - assert_eq!(data.enable_auto_launch, Some(true)); - assert_eq!(data.enable_tun_mode, Some(false)); + #[derive(Clone, Debug, Default, PartialEq)] + struct IVerge { + enable_auto_launch: Option, + enable_tun_mode: Option, } - // 3. 初次获取草稿(draft_mut 会自动 clone 一份) - { - let draft_view = draft.draft_mut(); - assert_eq!(draft_view.enable_auto_launch, Some(true)); - assert_eq!(draft_view.enable_tun_mode, Some(false)); + // Minimal single-threaded executor for immediately-ready futures + fn block_on_ready(fut: F) -> F::Output { + fn no_op_raw_waker() -> RawWaker { + fn clone(_: *const ()) -> RawWaker { + no_op_raw_waker() + } + fn wake(_: *const ()) {} + fn wake_by_ref(_: *const ()) {} + fn drop(_: *const ()) {} + static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); + RawWaker::new(std::ptr::null(), &VTABLE) + } + + let waker = unsafe { Waker::from_raw(no_op_raw_waker()) }; + let mut cx = Context::from_waker(&waker); + let mut fut = Box::pin(fut); + loop { + match Pin::as_mut(&mut fut).poll(&mut cx) { + Poll::Ready(v) => return v, + Poll::Pending => std::thread::yield_now(), + } + } } - // 4. 修改草稿 - { - let mut d = draft.draft_mut(); - d.enable_auto_launch = Some(false); - d.enable_tun_mode = Some(true); + #[test] + fn test_draft_basic_flow() { + let verge = IVerge { + enable_auto_launch: Some(true), + enable_tun_mode: Some(false), + }; + let draft = Draft::new(verge); + + // 读取正式数据(data_arc) + { + let data = draft.data_arc(); + assert_eq!(data.enable_auto_launch, Some(true)); + assert_eq!(data.enable_tun_mode, Some(false)); + } + + // 修改草稿(使用 edit_draft) + draft.edit_draft(|d| { + d.enable_auto_launch = Some(false); + d.enable_tun_mode = Some(true); + }); + + // 正式数据未变 + { + let data = draft.data_arc(); + assert_eq!(data.enable_auto_launch, Some(true)); + assert_eq!(data.enable_tun_mode, Some(false)); + } + + // 草稿已变 + { + let latest = draft.latest_arc(); + assert_eq!(latest.enable_auto_launch, Some(false)); + assert_eq!(latest.enable_tun_mode, Some(true)); + } + + // 提交草稿 + draft.apply(); + + // 正式数据已更新 + { + let data = draft.data_arc(); + assert_eq!(data.enable_auto_launch, Some(false)); + assert_eq!(data.enable_tun_mode, Some(true)); + } + + // 新一轮草稿并修改 + draft.edit_draft(|d| { + d.enable_auto_launch = Some(true); + }); + { + let latest = draft.latest_arc(); + assert_eq!(latest.enable_auto_launch, Some(true)); + assert_eq!(latest.enable_tun_mode, Some(true)); + } + + // 丢弃草稿 + draft.discard(); + + // 丢弃后再次创建草稿,会从已提交重新 clone + { + draft.edit_draft(|d| { + // 原 committed 是 enable_auto_launch = Some(false) + assert_eq!(d.enable_auto_launch, Some(false)); + // 再修改一下 + d.enable_tun_mode = Some(false); + }); + // 草稿中值已修改,但正式数据仍是 apply 后的值 + let data = draft.data_arc(); + assert_eq!(data.enable_auto_launch, Some(false)); + assert_eq!(data.enable_tun_mode, Some(true)); + } } - // 正式数据未变 - assert_eq!(draft.data_mut().enable_auto_launch, Some(true)); - assert_eq!(draft.data_mut().enable_tun_mode, Some(false)); + #[test] + fn test_arc_pointer_behavior_on_edit_and_apply() { + let draft = Draft::new(IVerge { + enable_auto_launch: Some(true), + enable_tun_mode: Some(false), + }); - // 草稿已变 - { - let latest = draft.latest_ref(); - assert_eq!(latest.enable_auto_launch, Some(false)); + // 初始 latest == committed + let committed = draft.data_arc(); + let latest = draft.latest_arc(); + assert!(std::sync::Arc::ptr_eq(&committed, &latest)); + + // 第一次 edit:由于与 committed 共享,Arc::make_mut 会克隆 + draft.edit_draft(|d| d.enable_tun_mode = Some(true)); + let committed_after_first_edit = draft.data_arc(); + let draft_after_first_edit = draft.latest_arc(); + assert!(!std::sync::Arc::ptr_eq( + &committed_after_first_edit, + &draft_after_first_edit + )); + // 提交会把 committed 指向草稿的 Arc + let prev_draft_ptr = std::sync::Arc::as_ptr(&draft_after_first_edit); + draft.apply(); + let committed_after_apply = draft.data_arc(); + assert_eq!( + std::sync::Arc::as_ptr(&committed_after_apply), + prev_draft_ptr + ); + + // 第二次编辑:此时草稿唯一持有(无其它引用),不应再克隆 + // 获取草稿 Arc 的指针并立即丢弃本地引用,避免增加 strong_count + draft.edit_draft(|d| d.enable_auto_launch = Some(false)); + let latest1 = draft.latest_arc(); + let latest1_ptr = std::sync::Arc::as_ptr(&latest1); + drop(latest1); // 确保只有 Draft 内部持有草稿 Arc + + // 再次编辑(unique,Arc::make_mut 不应克隆) + draft.edit_draft(|d| d.enable_tun_mode = Some(false)); + let latest2 = draft.latest_arc(); + let latest2_ptr = std::sync::Arc::as_ptr(&latest2); + + assert_eq!(latest1_ptr, latest2_ptr, "Unique edit should not clone Arc"); + assert_eq!(latest2.enable_auto_launch, Some(false)); + assert_eq!(latest2.enable_tun_mode, Some(false)); + } + + #[test] + fn test_discard_restores_latest_to_committed() { + let draft = Draft::new(IVerge { + enable_auto_launch: Some(false), + enable_tun_mode: Some(false), + }); + + // 创建草稿并修改 + draft.edit_draft(|d| d.enable_auto_launch = Some(true)); + let committed = draft.data_arc(); + let latest = draft.latest_arc(); + assert!(!std::sync::Arc::ptr_eq(&committed, &latest)); + + // 丢弃草稿后 latest 应回到 committed + draft.discard(); + let committed2 = draft.data_arc(); + let latest2 = draft.latest_arc(); + assert!(std::sync::Arc::ptr_eq(&committed2, &latest2)); + assert_eq!(latest2.enable_auto_launch, Some(false)); + } + + #[test] + fn test_edit_draft_returns_closure_result() { + let draft = Draft::new(IVerge::default()); + let ret = draft.edit_draft(|d| { + d.enable_tun_mode = Some(true); + 123usize + }); + assert_eq!(ret, 123); + let latest = draft.latest_arc(); assert_eq!(latest.enable_tun_mode, Some(true)); } - // 5. 提交草稿 - draft.apply(); + #[test] + fn test_with_data_modify_ok_and_replaces_committed() { + let draft = Draft::new(IVerge { + enable_auto_launch: Some(false), + enable_tun_mode: Some(false), + }); - // 正式数据已更新 - { - let data = draft.data_mut(); - assert_eq!(data.enable_auto_launch, Some(false)); - assert_eq!(data.enable_tun_mode, Some(true)); + // 使用 with_data_modify 异步(立即就绪)地更新 committed + let res = block_on_ready(draft.with_data_modify(|mut v| async move { + v.enable_auto_launch = Some(true); + Ok((Box::new(*v), "done")) // Dereference v to get Box + })); + assert_eq!( + { + #[allow(clippy::unwrap_used)] + res.unwrap() + }, + "done" + ); + + let committed = draft.data_arc(); + assert_eq!(committed.enable_auto_launch, Some(true)); + assert_eq!(committed.enable_tun_mode, Some(false)); } - // 6. 新建并修改下一轮草稿 - { - let mut d = draft.draft_mut(); - d.enable_auto_launch = Some(true); + #[test] + fn test_with_data_modify_error_propagation() { + let draft = Draft::new(IVerge::default()); + + #[allow(clippy::unwrap_used)] + let err = block_on_ready(draft.with_data_modify(|v| async move { + drop(v); + Err::<(Box, ()), _>(anyhow!("boom")) + })) + .unwrap_err(); + + assert_eq!(format!("{err}"), "boom"); } - assert_eq!(draft.draft_mut().enable_auto_launch, Some(true)); - // 7. 丢弃草稿 - draft.discard(); + #[test] + fn test_with_data_modify_does_not_touch_existing_draft() { + let draft = Draft::new(IVerge { + enable_auto_launch: Some(false), + enable_tun_mode: Some(false), + }); - // 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone - assert_eq!(draft.draft_mut().enable_auto_launch, Some(false)); + // 创建草稿并修改 + draft.edit_draft(|d| { + d.enable_auto_launch = Some(true); + d.enable_tun_mode = Some(true); + }); + let draft_before = draft.latest_arc(); + let draft_before_ptr = std::sync::Arc::as_ptr(&draft_before); + + // 同时通过 with_data_modify 修改 committed + #[allow(clippy::unwrap_used)] + block_on_ready(draft.with_data_modify(|mut v| async move { + v.enable_auto_launch = Some(false); // 与草稿不同 + Ok((Box::new(*v), ())) // Dereference v to get Box + })) + .unwrap(); + + // 草稿应保持不变 + let draft_after = draft.latest_arc(); + assert_eq!( + std::sync::Arc::as_ptr(&draft_after), + draft_before_ptr, + "Existing draft should not be replaced by with_data_modify" + ); + assert_eq!(draft_after.enable_auto_launch, Some(true)); + assert_eq!(draft_after.enable_tun_mode, Some(true)); + + // 丢弃草稿后 latest == committed,且 committed 为异步修改结果 + draft.discard(); + let latest = draft.latest_arc(); + assert_eq!(latest.enable_auto_launch, Some(false)); + assert_eq!(latest.enable_tun_mode, Some(false)); + } } diff --git a/src-tauri/src/utils/i18n.rs b/src-tauri/src/utils/i18n.rs index 83f6140e..b1c55232 100644 --- a/src-tauri/src/utils/i18n.rs +++ b/src-tauri/src/utils/i18n.rs @@ -43,7 +43,7 @@ pub fn get_supported_languages() -> Vec { pub async fn current_language() -> String { Config::verge() .await - .latest_ref() + .latest_arc() .language .as_deref() .map(String::from) diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index 97211210..ff29d937 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -31,7 +31,7 @@ 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(); + let verge = verge_guard.latest_arc(); ( verge.get_log_level(), verge.app_log_max_size.unwrap_or(128), @@ -81,7 +81,7 @@ pub async fn init_logger() -> Result<()> { pub async fn sidecar_writer() -> Result { let (log_max_size, log_max_count) = { let verge_guard = Config::verge().await; - let verge = verge_guard.latest_ref(); + let verge = verge_guard.latest_arc(); ( verge.app_log_max_size.unwrap_or(128), verge.app_log_max_count.unwrap_or(8), @@ -109,7 +109,7 @@ pub async fn sidecar_writer() -> Result { pub async fn service_writer_config() -> Result { let (log_max_size, log_max_count) = { let verge_guard = Config::verge().await; - let verge = verge_guard.latest_ref(); + let verge = verge_guard.latest_arc(); ( verge.app_log_max_size.unwrap_or(128), verge.app_log_max_count.unwrap_or(8), @@ -134,7 +134,7 @@ pub async fn delete_log() -> Result<()> { let auto_log_clean = { let verge = Config::verge().await; - let verge = verge.latest_ref(); + let verge = verge.latest_arc(); verge.auto_log_clean.unwrap_or(0) }; @@ -509,7 +509,7 @@ pub async fn startup_script() -> Result<()> { let app_handle = handle::Handle::app_handle(); let script_path = { let verge = Config::verge().await; - let verge = verge.latest_ref(); + let verge = verge.latest_arc(); verge.startup_script.clone().unwrap_or_else(|| "".into()) }; diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index 65ef63a2..d2353673 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -165,10 +165,10 @@ impl NetworkManager { ProxyType::None => None, ProxyType::Localhost => { let port = { - let verge_port = Config::verge().await.latest_ref().verge_mixed_port; + let verge_port = Config::verge().await.latest_arc().verge_mixed_port; match verge_port { Some(port) => port, - None => Config::clash().await.latest_ref().get_mixed_port(), + None => Config::clash().await.latest_arc().get_mixed_port(), } }; let proxy_scheme = format!("http://127.0.0.1:{port}"); diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 4055ecd0..5a355616 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -179,7 +179,7 @@ pub(super) async fn refresh_tray_menu() { pub(super) async fn init_window() { let is_silent_start = Config::verge() .await - .latest_ref() + .latest_arc() .enable_silent_start .unwrap_or(false); #[cfg(target_os = "macos")] diff --git a/src-tauri/src/utils/resolve/window.rs b/src-tauri/src/utils/resolve/window.rs index 387145ec..cf8cecd5 100644 --- a/src-tauri/src/utils/resolve/window.rs +++ b/src-tauri/src/utils/resolve/window.rs @@ -22,7 +22,7 @@ pub async fn build_new_window() -> Result { let app_handle = handle::Handle::app_handle(); let config = Config::verge().await; - let latest = config.latest_ref(); + let latest = config.latest_arc(); let start_page = latest.start_page.as_deref().unwrap_or("/"); match tauri::WebviewWindowBuilder::new( diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index c5062a8c..ef2f951b 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -89,15 +89,15 @@ pub fn embed_server() { let clash_config = Config::clash().await; let pac_content = verge_config - .latest_ref() + .latest_arc() .pac_file_content .clone() .unwrap_or_else(|| DEFAULT_PAC.into()); let pac_port = verge_config - .latest_ref() + .latest_arc() .verge_mixed_port - .unwrap_or_else(|| clash_config.latest_ref().get_mixed_port()); + .unwrap_or_else(|| clash_config.latest_arc().get_mixed_port()); let pac = warp::path!("commands" / "pac").map(move || { let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}")); diff --git a/src/components/setting/mods/clash-port-viewer.tsx b/src/components/setting/mods/clash-port-viewer.tsx index b8bebfca..8cc54855 100644 --- a/src/components/setting/mods/clash-port-viewer.tsx +++ b/src/components/setting/mods/clash-port-viewer.tsx @@ -71,8 +71,8 @@ export const ClashPortViewer = forwardRef((_, ref) => { setOpen(false); showNotice.success("settings.clash.port.messages.saved"); }, - onError: () => { - showNotice.error("settings.clash.port.messages.saveFailed"); + onError: (error) => { + showNotice.error("settings.clash.port.messages.saveFailed", error); }, }, ); diff --git a/src/hooks/use-clash.ts b/src/hooks/use-clash.ts index 483f197d..85b3faed 100644 --- a/src/hooks/use-clash.ts +++ b/src/hooks/use-clash.ts @@ -70,8 +70,8 @@ export const useClashInfo = () => { if (patch["redir-port"]) { const port = patch["redir-port"]; - if (port < 1111) { - throw new Error("The port should not < 1111"); + if (port < 1000) { + throw new Error("The port should not < 1000"); } if (port > 65536) { throw new Error("The port should not > 65536"); @@ -80,8 +80,8 @@ export const useClashInfo = () => { if (patch["tproxy-port"]) { const port = patch["tproxy-port"]; - if (port < 1111) { - throw new Error("The port should not < 1111"); + if (port < 1000) { + throw new Error("The port should not < 1000"); } if (port > 65536) { throw new Error("The port should not > 65536"); @@ -90,8 +90,8 @@ export const useClashInfo = () => { if (patch["mixed-port"]) { const port = patch["mixed-port"]; - if (port < 1111) { - throw new Error("The port should not < 1111"); + if (port < 1000) { + throw new Error("The port should not < 1000"); } if (port > 65536) { throw new Error("The port should not > 65536"); @@ -100,8 +100,8 @@ export const useClashInfo = () => { if (patch["socks-port"]) { const port = patch["socks-port"]; - if (port < 1111) { - throw new Error("The port should not < 1111"); + if (port < 1000) { + throw new Error("The port should not < 1000"); } if (port > 65536) { throw new Error("The port should not > 65536"); @@ -110,8 +110,8 @@ export const useClashInfo = () => { if (patch["port"]) { const port = patch["port"]; - if (port < 1111) { - throw new Error("The port should not < 1111"); + if (port < 1000) { + throw new Error("The port should not < 1000"); } if (port > 65536) { throw new Error("The port should not > 65536"); diff --git a/src/types/types.d.ts b/src/types/types.d.ts index b5be4647..cc9eaac3 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -801,7 +801,7 @@ interface IVergeConfig { common_tray_icon?: boolean; sysproxy_tray_icon?: boolean; tun_tray_icon?: boolean; - enable_tray_speed?: boolean; + // enable_tray_speed?: boolean; // enable_tray_icon?: boolean; tray_inline_proxy_groups?: boolean; enable_tun_mode?: boolean;