Compare commits
39 Commits
chore/upda
...
renovate/n
40
.github/workflows/autobuild.yml
vendored
40
.github/workflows/autobuild.yml
vendored
@@ -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)
|
||||
@@ -493,43 +493,6 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-updater-manifests:
|
||||
name: Publish Updater Manifests
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
update_tag,
|
||||
autobuild-x86-windows-macos-linux,
|
||||
autobuild-arm-linux,
|
||||
autobuild-x86-arm-windows_webview2,
|
||||
]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
- name: Publish updater manifests
|
||||
run: pnpm updater
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish WebView2 updater manifests
|
||||
run: pnpm updater-fixed-webview2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
notify-telegram:
|
||||
name: Notify Telegram
|
||||
runs-on: ubuntu-latest
|
||||
@@ -539,7 +502,6 @@ jobs:
|
||||
autobuild-x86-windows-macos-linux,
|
||||
autobuild-arm-linux,
|
||||
autobuild-x86-arm-windows_webview2,
|
||||
publish-updater-manifests,
|
||||
]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
|
||||
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
|
||||
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
|
||||
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
28
package.json
28
package.json
@@ -40,10 +40,10 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/lab": "7.0.0-beta.17",
|
||||
"@mui/material": "^7.3.4",
|
||||
"@mui/x-data-grid": "^8.16.0",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/x-data-grid": "^8.17.0",
|
||||
"@tauri-apps/api": "2.9.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
@@ -54,10 +54,10 @@
|
||||
"@tauri-apps/plugin-updater": "2.9.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.9.6",
|
||||
"axios": "^1.13.1",
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "1.11.19",
|
||||
"foxact": "^0.2.49",
|
||||
"i18next": "^25.6.0",
|
||||
"i18next": "^25.6.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -68,7 +68,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",
|
||||
@@ -80,20 +80,20 @@
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
"@eslint-react/eslint-plugin": "^2.3.1",
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@tauri-apps/cli": "2.9.2",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tauri-apps/cli": "2.9.3",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.2.0",
|
||||
"@vitejs/plugin-react-swc": "^4.2.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"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",
|
||||
@@ -112,13 +112,13 @@
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.93.3",
|
||||
"tar": "^7.5.2",
|
||||
"terser": "^5.44.0",
|
||||
"terser": "^5.44.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"vite": "^7.1.12",
|
||||
"typescript-eslint": "^8.46.3",
|
||||
"vite": "^7.2.1",
|
||||
"vite-plugin-monaco-editor-esm": "^2.0.2",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vitest": "^4.0.6"
|
||||
"vitest": "^4.0.7"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": [
|
||||
|
||||
822
pnpm-lock.yaml
generated
822
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,12 @@
|
||||
* pnpm release-version <version>
|
||||
*
|
||||
* <version> can be:
|
||||
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3-rc.1)
|
||||
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
|
||||
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
|
||||
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
||||
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3-autobuild.1022.r2+cc39b2)
|
||||
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3-autobuild.1022.r2+a1b2c3d)
|
||||
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3-deploytest.1022.r2+cc39b2)
|
||||
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
|
||||
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3+autobuild.0614.a1b2c3d)
|
||||
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3+deploytest.2406101530)
|
||||
*
|
||||
* Examples:
|
||||
* pnpm release-version 1.2.3
|
||||
@@ -30,11 +30,9 @@
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs/promises";
|
||||
import process from "node:process";
|
||||
import path from "path";
|
||||
|
||||
import { program } from "commander";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* 获取当前 git 短 commit hash
|
||||
@@ -75,118 +73,41 @@ function getLatestTauriCommit() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Asia/Shanghai 时区的日期片段
|
||||
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
|
||||
* 使用 Asia/Shanghai 时区
|
||||
* @param {boolean} withCommit 是否带 commit
|
||||
* @param {boolean} useTauriCommit 是否使用 Tauri 相关的 commit(仅当 withCommit 为 true 时有效)
|
||||
* @returns {string}
|
||||
*/
|
||||
function getLocalDatePart() {
|
||||
function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
|
||||
const now = new Date();
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat("en-CA", {
|
||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "Asia/Shanghai",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
const dateParts = Object.fromEntries(
|
||||
dateFormatter.formatToParts(now).map((part) => [part.type, part.value]),
|
||||
);
|
||||
|
||||
const month = dateParts.month ?? "00";
|
||||
const day = dateParts.day ?? "00";
|
||||
const parts = formatter.formatToParts(now);
|
||||
const month = parts.find((part) => part.type === "month").value;
|
||||
const day = parts.find((part) => part.type === "day").value;
|
||||
|
||||
if (withCommit) {
|
||||
const gitShort = useTauriCommit
|
||||
? getLatestTauriCommit()
|
||||
: getGitShortCommit();
|
||||
return `${month}${day}.${gitShort}`;
|
||||
}
|
||||
return `${month}${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 GitHub Actions 运行编号(若存在)
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function getRunIdentifier() {
|
||||
const runNumber = process.env.GITHUB_RUN_NUMBER;
|
||||
if (runNumber && /^[0-9]+$/.test(runNumber)) {
|
||||
const runNum = Number.parseInt(runNumber, 10);
|
||||
if (!Number.isNaN(runNum)) {
|
||||
const base = `r${runNum.toString(36)}`;
|
||||
const attempt = process.env.GITHUB_RUN_ATTEMPT;
|
||||
if (attempt && /^[0-9]+$/.test(attempt)) {
|
||||
const attemptNumber = Number.parseInt(attempt, 10);
|
||||
if (!Number.isNaN(attemptNumber) && attemptNumber > 1) {
|
||||
return `${base}${attemptNumber.toString(36)}`;
|
||||
}
|
||||
}
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
const attempt = process.env.GITHUB_RUN_ATTEMPT;
|
||||
if (attempt && /^[0-9]+$/.test(attempt)) {
|
||||
const attemptNumber = Number.parseInt(attempt, 10);
|
||||
if (!Number.isNaN(attemptNumber)) {
|
||||
return `r${attemptNumber.toString(36)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成用于自动构建类渠道的版本后缀
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.includeCommit=false]
|
||||
* @param {"current"|"tauri"} [options.commitSource="current"]
|
||||
* @param {boolean} [options.includeRun=true]
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateChannelSuffix({
|
||||
includeCommit = false,
|
||||
commitSource = "current",
|
||||
includeRun = true,
|
||||
} = {}) {
|
||||
const segments = [];
|
||||
const date = getLocalDatePart();
|
||||
segments.push(date);
|
||||
|
||||
if (includeCommit) {
|
||||
const commit =
|
||||
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
|
||||
segments.push(commit);
|
||||
}
|
||||
|
||||
if (includeRun) {
|
||||
const run = getRunIdentifier();
|
||||
if (run) {
|
||||
segments.push(run);
|
||||
}
|
||||
}
|
||||
|
||||
return segments.join(".");
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 autobuild 渠道构建版本片段
|
||||
* @param {Object} options
|
||||
* @param {"current"|"tauri"} [options.commitSource="current"]
|
||||
* @returns {{date: string, run: string, metadata: string}}
|
||||
*/
|
||||
function generateAutobuildComponents({ commitSource = "current" } = {}) {
|
||||
const date = getLocalDatePart();
|
||||
const run = getRunIdentifier() ?? `manual${Date.now().toString(36)}`;
|
||||
const commitHash =
|
||||
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
|
||||
|
||||
return {
|
||||
date,
|
||||
run,
|
||||
metadata: commitHash || "nogit",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证版本号格式
|
||||
* @param {string} version
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidVersion(version) {
|
||||
return /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
|
||||
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
|
||||
version,
|
||||
);
|
||||
}
|
||||
@@ -201,14 +122,13 @@ function normalizeVersion(version) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取基础版本号(去掉所有 pre-release 和 build metadata)
|
||||
* 提取基础版本号(去掉所有 -tag 和 +build 部分)
|
||||
* @param {string} version
|
||||
* @returns {string}
|
||||
*/
|
||||
function getBaseVersion(version) {
|
||||
const cleaned = version.startsWith("v") ? version.slice(1) : version;
|
||||
const withoutBuild = cleaned.split("+")[0];
|
||||
const [base] = withoutBuild.split("-");
|
||||
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
|
||||
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
|
||||
return base;
|
||||
}
|
||||
|
||||
@@ -353,17 +273,17 @@ async function main(versionArg) {
|
||||
const baseVersion = getBaseVersion(currentVersion);
|
||||
|
||||
if (versionArg.toLowerCase() === "autobuild") {
|
||||
// 格式: 2.3.0-autobuild.1022.r2+cc39b2
|
||||
const parts = generateAutobuildComponents({ commitSource: "tauri" });
|
||||
newVersion = `${baseVersion}-autobuild.${parts.date}.${parts.run}+${parts.metadata}`;
|
||||
// 格式: 2.3.0+autobuild.1004.cc39b27
|
||||
// 使用 Tauri 相关的最新 commit hash
|
||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`;
|
||||
} else if (versionArg.toLowerCase() === "autobuild-latest") {
|
||||
// 格式: 2.3.0-autobuild.1022.r2+a1b2c3d (使用最新 Tauri 提交)
|
||||
const parts = generateAutobuildComponents({ commitSource: "tauri" });
|
||||
newVersion = `${baseVersion}-autobuild.${parts.date}.${parts.run}+${parts.metadata}`;
|
||||
// 格式: 2.3.0+autobuild.1004.a1b2c3d (使用最新 Tauri 提交)
|
||||
const latestTauriCommit = getLatestTauriCommit();
|
||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
|
||||
} else if (versionArg.toLowerCase() === "deploytest") {
|
||||
// 格式: 2.3.0-deploytest.1022.r2+cc39b2
|
||||
const parts = generateAutobuildComponents({ commitSource: "tauri" });
|
||||
newVersion = `${baseVersion}-deploytest.${parts.date}.${parts.run}+${parts.metadata}`;
|
||||
// 格式: 2.3.0+deploytest.1004.cc39b27
|
||||
// 使用 Tauri 相关的最新 commit hash
|
||||
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`;
|
||||
} else {
|
||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||
}
|
||||
|
||||
@@ -1,105 +1,7 @@
|
||||
import process from "node:process";
|
||||
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||
|
||||
const SEMVER_REGEX =
|
||||
/v?\d+(?:\.\d+){2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/g;
|
||||
const STRICT_SEMVER_REGEX =
|
||||
/^\d+(?:\.\d+){2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
|
||||
const stripLeadingV = (version) =>
|
||||
typeof version === "string" && version.startsWith("v")
|
||||
? version.slice(1)
|
||||
: version;
|
||||
|
||||
const preferCandidate = (current, candidate) => {
|
||||
if (!candidate) return current;
|
||||
if (!current) return candidate;
|
||||
|
||||
const candidateHasPre = /[-+]/.test(candidate);
|
||||
const currentHasPre = /[-+]/.test(current);
|
||||
|
||||
if (candidateHasPre && !currentHasPre) return candidate;
|
||||
if (candidateHasPre === currentHasPre && candidate.length > current.length) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
const extractBestSemver = (input) => {
|
||||
if (typeof input !== "string") return null;
|
||||
const matches = input.match(SEMVER_REGEX);
|
||||
if (!matches) return null;
|
||||
|
||||
return matches
|
||||
.map(stripLeadingV)
|
||||
.reduce((best, candidate) => preferCandidate(best, candidate), null);
|
||||
};
|
||||
|
||||
const splitIdentifiers = (segment) =>
|
||||
segment
|
||||
.split(/[^0-9A-Za-z-]+/)
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const sanitizeSuffix = (value, fallbackLabel) => {
|
||||
if (!value) return fallbackLabel;
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return fallbackLabel;
|
||||
|
||||
const [preRelease = "", metadata] = trimmed.split("+", 2);
|
||||
const normalizedPre = splitIdentifiers(preRelease).join(".") || fallbackLabel;
|
||||
const normalizedMeta = metadata ? splitIdentifiers(metadata).join(".") : "";
|
||||
|
||||
return normalizedMeta ? `${normalizedPre}+${normalizedMeta}` : normalizedPre;
|
||||
};
|
||||
|
||||
const ensureSemverCompatibleVersion = (
|
||||
version,
|
||||
{ channel, releaseTag, fallbackLabel },
|
||||
) => {
|
||||
const trimmed = stripLeadingV(version ?? "").trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
if (STRICT_SEMVER_REGEX.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (channel === "autobuild") {
|
||||
const normalizedSuffix = sanitizeSuffix(trimmed, fallbackLabel ?? channel);
|
||||
const fallback = `0.0.0-${normalizedSuffix}`;
|
||||
console.warn(
|
||||
`[${channel}] Normalized non-semver version "${trimmed}" from release "${releaseTag}" to "${fallback}"`,
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`[${channel}] Derived version "${trimmed}" is not semver compatible for release "${releaseTag}"`,
|
||||
);
|
||||
};
|
||||
|
||||
const resolveReleaseVersion = (release) => {
|
||||
const sources = [
|
||||
release?.name,
|
||||
release?.tag_name,
|
||||
release?.body,
|
||||
...(Array.isArray(release?.assets)
|
||||
? release.assets.map((asset) => asset?.name)
|
||||
: []),
|
||||
];
|
||||
|
||||
return sources.reduce((best, source) => {
|
||||
const candidate = extractBestSemver(source);
|
||||
return preferCandidate(best, candidate);
|
||||
}, null);
|
||||
};
|
||||
|
||||
// Add stable update JSON filenames
|
||||
const UPDATE_TAG_NAME = "updater";
|
||||
const UPDATE_JSON_FILE = "update.json";
|
||||
@@ -108,11 +10,6 @@ const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
const ALPHA_TAG_NAME = "updater-alpha";
|
||||
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
||||
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
// Add autobuild update JSON filenames
|
||||
const AUTOBUILD_SOURCE_TAG_NAME = "autobuild";
|
||||
const AUTOBUILD_TAG_NAME = "updater-autobuild";
|
||||
const AUTOBUILD_UPDATE_JSON_FILE = "update.json";
|
||||
const AUTOBUILD_UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
|
||||
/// generate update.json
|
||||
/// upload to update tag's release asset
|
||||
@@ -151,12 +48,12 @@ async function resolveUpdater() {
|
||||
|
||||
// More flexible tag detection with regex patterns
|
||||
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
|
||||
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
|
||||
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
|
||||
|
||||
// Get tags for known channels
|
||||
// Get the latest stable tag and pre-release tag
|
||||
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
|
||||
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
|
||||
const autobuildTag = tags.find((t) => t.name === AUTOBUILD_SOURCE_TAG_NAME);
|
||||
|
||||
console.log("All tags:", tags.map((t) => t.name).join(", "));
|
||||
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
|
||||
@@ -164,106 +61,32 @@ async function resolveUpdater() {
|
||||
"Pre-release tag:",
|
||||
preReleaseTag ? preReleaseTag.name : "None found",
|
||||
);
|
||||
console.log(
|
||||
"Autobuild tag:",
|
||||
autobuildTag ? autobuildTag.name : "None found",
|
||||
);
|
||||
console.log();
|
||||
|
||||
const channels = [
|
||||
{
|
||||
name: "stable",
|
||||
tagName: stableTag?.name,
|
||||
updateReleaseTag: UPDATE_TAG_NAME,
|
||||
jsonFile: UPDATE_JSON_FILE,
|
||||
proxyFile: UPDATE_JSON_PROXY,
|
||||
prerelease: false,
|
||||
},
|
||||
{
|
||||
name: "alpha",
|
||||
tagName: preReleaseTag?.name,
|
||||
updateReleaseTag: ALPHA_TAG_NAME,
|
||||
jsonFile: ALPHA_UPDATE_JSON_FILE,
|
||||
proxyFile: ALPHA_UPDATE_JSON_PROXY,
|
||||
prerelease: true,
|
||||
},
|
||||
{
|
||||
name: "autobuild",
|
||||
tagName: autobuildTag?.name ?? AUTOBUILD_SOURCE_TAG_NAME,
|
||||
updateReleaseTag: AUTOBUILD_TAG_NAME,
|
||||
jsonFile: AUTOBUILD_UPDATE_JSON_FILE,
|
||||
proxyFile: AUTOBUILD_UPDATE_JSON_PROXY,
|
||||
prerelease: true,
|
||||
},
|
||||
];
|
||||
// Process stable release
|
||||
if (stableTag) {
|
||||
await processRelease(github, options, stableTag, false);
|
||||
}
|
||||
|
||||
for (const channel of channels) {
|
||||
if (!channel.tagName) {
|
||||
console.log(`[${channel.name}] tag not found, skipping...`);
|
||||
continue;
|
||||
}
|
||||
await processRelease(github, options, channel);
|
||||
// Process pre-release if found
|
||||
if (preReleaseTag) {
|
||||
await processRelease(github, options, preReleaseTag, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Process a release and generate update files for the specified channel
|
||||
async function processRelease(github, options, channelConfig) {
|
||||
if (!channelConfig) return;
|
||||
|
||||
const {
|
||||
tagName,
|
||||
name: channelName,
|
||||
updateReleaseTag,
|
||||
jsonFile,
|
||||
proxyFile,
|
||||
prerelease,
|
||||
} = channelConfig;
|
||||
|
||||
const channelLabel =
|
||||
channelName.charAt(0).toUpperCase() + channelName.slice(1);
|
||||
// Process a release (stable or alpha) and generate update files
|
||||
async function processRelease(github, options, tag, isAlpha) {
|
||||
if (!tag) return;
|
||||
|
||||
try {
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: tagName,
|
||||
tag: tag.name,
|
||||
});
|
||||
|
||||
const releaseTagName = release.tag_name ?? tagName;
|
||||
const resolvedVersion = resolveReleaseVersion(release);
|
||||
|
||||
if (!resolvedVersion) {
|
||||
throw new Error(
|
||||
`[${channelName}] Failed to determine semver version from release "${releaseTagName}"`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${channelName}] Preparing update metadata from release "${releaseTagName}"`,
|
||||
);
|
||||
console.log(
|
||||
`[${channelName}] Resolved release version: ${resolvedVersion}`,
|
||||
);
|
||||
|
||||
const semverCompatibleVersion = ensureSemverCompatibleVersion(
|
||||
resolvedVersion,
|
||||
{
|
||||
channel: channelName,
|
||||
releaseTag: releaseTagName,
|
||||
fallbackLabel: channelName,
|
||||
},
|
||||
);
|
||||
|
||||
if (semverCompatibleVersion !== resolvedVersion) {
|
||||
console.log(
|
||||
`[${channelName}] Normalized updater version: ${semverCompatibleVersion}`,
|
||||
);
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
version: semverCompatibleVersion,
|
||||
original_version: resolvedVersion,
|
||||
tag_name: releaseTagName,
|
||||
notes: await resolveUpdateLog(releaseTagName).catch(() =>
|
||||
name: tag.name,
|
||||
notes: await resolveUpdateLog(tag.name).catch(() =>
|
||||
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
||||
),
|
||||
pub_date: new Date().toISOString(),
|
||||
@@ -363,15 +186,13 @@ async function processRelease(github, options, channelConfig) {
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
console.log(`[${channelName}] Update data snapshot:`, updateData);
|
||||
console.log(updateData);
|
||||
|
||||
// maybe should test the signature as well
|
||||
// delete the null field
|
||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||
if (!value.url) {
|
||||
console.log(
|
||||
`[${channelName}] [Error]: failed to parse release for "${key}"`,
|
||||
);
|
||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
||||
delete updateData.platforms[key];
|
||||
}
|
||||
});
|
||||
@@ -384,14 +205,15 @@ async function processRelease(github, options, channelConfig) {
|
||||
updateDataNew.platforms[key].url =
|
||||
"https://download.clashverge.dev/" + value.url;
|
||||
} else {
|
||||
console.log(
|
||||
`[${channelName}] [Error]: updateDataNew.platforms.${key} is null`,
|
||||
);
|
||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||
}
|
||||
});
|
||||
|
||||
// Get the appropriate updater release based on isAlpha flag
|
||||
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
|
||||
console.log(
|
||||
`[${channelName}] Processing update release target "${updateReleaseTag}"`,
|
||||
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
|
||||
releaseTag,
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -401,28 +223,30 @@ async function processRelease(github, options, channelConfig) {
|
||||
// Try to get the existing release
|
||||
const response = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: updateReleaseTag,
|
||||
tag: releaseTag,
|
||||
});
|
||||
updateRelease = response.data;
|
||||
console.log(
|
||||
`[${channelName}] Found existing ${updateReleaseTag} release with ID: ${updateRelease.id}`,
|
||||
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||
);
|
||||
} catch (error) {
|
||||
// If release doesn't exist, create it
|
||||
if (error.status === 404) {
|
||||
console.log(
|
||||
`[${channelName}] Release with tag ${updateReleaseTag} not found, creating new release...`,
|
||||
`Release with tag ${releaseTag} not found, creating new release...`,
|
||||
);
|
||||
const createResponse = await github.rest.repos.createRelease({
|
||||
...options,
|
||||
tag_name: updateReleaseTag,
|
||||
name: `Auto-update ${channelLabel} Channel`,
|
||||
body: `This release contains the update information for the ${channelName} channel.`,
|
||||
prerelease,
|
||||
tag_name: releaseTag,
|
||||
name: isAlpha
|
||||
? "Auto-update Alpha Channel"
|
||||
: "Auto-update Stable Channel",
|
||||
body: `This release contains the update information for ${isAlpha ? "alpha" : "stable"} channel.`,
|
||||
prerelease: isAlpha,
|
||||
});
|
||||
updateRelease = createResponse.data;
|
||||
console.log(
|
||||
`[${channelName}] Created new ${updateReleaseTag} release with ID: ${updateRelease.id}`,
|
||||
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||
);
|
||||
} else {
|
||||
// If it's another error, throw it
|
||||
@@ -431,8 +255,11 @@ async function processRelease(github, options, channelConfig) {
|
||||
}
|
||||
|
||||
// File names based on release type
|
||||
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
|
||||
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
||||
|
||||
// Delete existing assets with these names
|
||||
for (const asset of updateRelease.assets) {
|
||||
for (let asset of updateRelease.assets) {
|
||||
if (asset.name === jsonFile) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
@@ -443,12 +270,7 @@ async function processRelease(github, options, channelConfig) {
|
||||
if (asset.name === proxyFile) {
|
||||
await github.rest.repos
|
||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||
.catch((deleteError) =>
|
||||
console.error(
|
||||
`[${channelName}] Failed to delete existing proxy asset:`,
|
||||
deleteError.message,
|
||||
),
|
||||
); // do not break the pipeline
|
||||
.catch(console.error); // do not break the pipeline
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,22 +290,20 @@ async function processRelease(github, options, channelConfig) {
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[${channelName}] Successfully uploaded update files to ${updateReleaseTag}`,
|
||||
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[${channelName}] Failed to process update release:`,
|
||||
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log(
|
||||
`[${channelName}] Release not found for tag: ${tagName}, skipping...`,
|
||||
);
|
||||
console.log(`Release not found for tag: ${tag.name}, skipping...`);
|
||||
} else {
|
||||
console.error(
|
||||
`[${channelName}] Failed to get release for tag: ${tagName}`,
|
||||
`Failed to get release for tag: ${tag.name}`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
|
||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -1158,7 +1158,6 @@ dependencies = [
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
"users",
|
||||
"warp",
|
||||
"winapi",
|
||||
|
||||
@@ -87,7 +87,6 @@ clash_verge_service_ipc = { version = "2.0.21", features = [
|
||||
"client",
|
||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||
arc-swap = "1.7.1"
|
||||
url = "2.5.4"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
@@ -227,3 +226,23 @@ needless_raw_string_hashes = "deny" # Too many in existing code
|
||||
|
||||
or_fun_call = "deny"
|
||||
cognitive_complexity = "deny"
|
||||
useless_let_if_seq = "deny"
|
||||
use_self = "deny"
|
||||
tuple_array_conversions = "deny"
|
||||
trait_duplication_in_bounds = "deny"
|
||||
suspicious_operation_groupings = "deny"
|
||||
string_lit_as_bytes = "deny"
|
||||
significant_drop_tightening = "deny"
|
||||
significant_drop_in_scrutinee = "deny"
|
||||
redundant_clone = "deny"
|
||||
# option_if_let_else = "deny" // 过于激进,暂时不开启
|
||||
needless_pass_by_ref_mut = "deny"
|
||||
needless_collect = "deny"
|
||||
missing_const_for_fn = "deny"
|
||||
iter_with_drain = "deny"
|
||||
iter_on_single_items = "deny"
|
||||
iter_on_empty_collections = "deny"
|
||||
# fallible_impl_from = "deny" // 过于激进,暂时不开启
|
||||
equatable_if_let = "deny"
|
||||
collection_is_never_read = "deny"
|
||||
branches_sharing_code = "deny"
|
||||
|
||||
@@ -7,13 +7,13 @@ use app_lib::config::IVerge;
|
||||
use app_lib::utils::Draft as DraftNew;
|
||||
|
||||
/// 创建测试数据
|
||||
fn make_draft() -> DraftNew<Box<IVerge>> {
|
||||
let verge = Box::new(IVerge {
|
||||
fn make_draft() -> DraftNew<IVerge> {
|
||||
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, ()))
|
||||
|
||||
@@ -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<ClashInfo> {
|
||||
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| {
|
||||
@@ -193,7 +189,6 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
})?;
|
||||
|
||||
logging!(info, Type::Config, "DNS config successfully applied");
|
||||
handle::Handle::refresh_clash();
|
||||
} else {
|
||||
// 当关闭DNS设置时,重新生成配置(不加载DNS配置文件)
|
||||
logging!(
|
||||
@@ -216,9 +211,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
})?;
|
||||
|
||||
logging!(info, Type::Config, "Config regenerated successfully");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -13,19 +13,23 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let body_lower = body.to_lowercase();
|
||||
let mut status = "Failed";
|
||||
let mut region = None;
|
||||
|
||||
if body_lower.contains("youtube premium is not available in your country") {
|
||||
return UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if body_lower.contains("ad-free") {
|
||||
let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
|
||||
Ok(re) => re,
|
||||
status = "No";
|
||||
} else if body_lower.contains("ad-free") {
|
||||
match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
|
||||
Ok(re) => {
|
||||
if let Some(caps) = re.captures(&body)
|
||||
&& let Some(m) = caps.get(1)
|
||||
{
|
||||
let country_code = m.as_str().trim();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
region = Some(format!("{emoji}{country_code}"));
|
||||
status = "Yes";
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
@@ -33,34 +37,14 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
"Failed to compile YouTube Premium regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let region = re.captures(&body).and_then(|caps| {
|
||||
caps.get(1).map(|m| {
|
||||
let country_code = m.as_str().trim();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{emoji}{country_code}")
|
||||
})
|
||||
});
|
||||
|
||||
return UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -16,7 +16,6 @@ pub mod runtime;
|
||||
pub mod save_profile;
|
||||
pub mod service;
|
||||
pub mod system;
|
||||
pub mod updater;
|
||||
pub mod uwp;
|
||||
pub mod validate;
|
||||
pub mod verge;
|
||||
@@ -35,7 +34,6 @@ pub use runtime::*;
|
||||
pub use save_profile::*;
|
||||
pub use service::*;
|
||||
pub use system::*;
|
||||
pub use updater::*;
|
||||
pub use uwp::*;
|
||||
pub use validate::*;
|
||||
pub use verge::*;
|
||||
|
||||
@@ -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<IProfiles> {
|
||||
pub async fn get_profiles() -> CmdResult<SharedBox<IProfiles>> {
|
||||
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<String>) -> 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<String>) -> CmdResu
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> 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<PrfOption>) -> 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 {
|
||||
@@ -234,7 +244,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_syntax_error",
|
||||
error_msg.clone(),
|
||||
error_msg,
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
@@ -243,7 +253,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_parse_error",
|
||||
error_msg.clone(),
|
||||
error_msg,
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
@@ -252,19 +262,13 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_error",
|
||||
error_msg.clone(),
|
||||
);
|
||||
handle::Handle::notice_message("config_validate::file_read_error", error_msg);
|
||||
Err(())
|
||||
}
|
||||
Err(_) => {
|
||||
let error_msg = "读取配置文件超时(5秒)".to_string();
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_timeout",
|
||||
error_msg.clone(),
|
||||
);
|
||||
handle::Handle::notice_message("config_validate::file_read_timeout", error_msg);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
@@ -274,17 +278,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)
|
||||
.stringify_err()?;
|
||||
.edit_draft(|d| d.patch_config(&restore_profiles));
|
||||
Config::profiles().await.apply();
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
@@ -295,7 +297,7 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
|
||||
async fn handle_success(current_value: Option<&String>) -> CmdResult<bool> {
|
||||
Config::profiles().await.apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
@@ -311,9 +313,9 @@ async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
|
||||
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 +323,7 @@ async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
|
||||
|
||||
async fn handle_validation_failure(
|
||||
error_msg: String,
|
||||
current_profile: Option<String>,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<bool> {
|
||||
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
|
||||
Config::profiles().await.discard();
|
||||
@@ -339,7 +341,7 @@ async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
|
||||
async fn handle_timeout(current_profile: Option<&String>) -> CmdResult<bool> {
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(error, Type::Cmd, "{}", timeout_msg);
|
||||
Config::profiles().await.discard();
|
||||
@@ -351,9 +353,12 @@ async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
|
||||
}
|
||||
|
||||
async fn perform_config_update(
|
||||
current_value: Option<String>,
|
||||
current_profile: Option<String>,
|
||||
current_value: Option<&String>,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<bool> {
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
CoreManager::global().update_config(),
|
||||
@@ -376,13 +381,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
.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 +394,22 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
);
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
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()
|
||||
{
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
return Ok(false);
|
||||
}
|
||||
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 +429,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 +468,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 +491,7 @@ pub async fn view_profile(index: String) -> CmdResult {
|
||||
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
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()
|
||||
|
||||
@@ -8,14 +8,14 @@ use std::collections::HashMap;
|
||||
/// 获取运行时配置
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
/// 获取运行时存在的键
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
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<HashMap<String, Vec<(String, String)>>> {
|
||||
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<String> {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> 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")?;
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{CoreManager, handle},
|
||||
core::{CoreManager, handle, manager::RunningMode},
|
||||
logging,
|
||||
module::sysinfo::PlatformSpecification,
|
||||
utils::logging::Type,
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
sync::atomic::{AtomicI64, Ordering},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tokio::time::Instant;
|
||||
|
||||
// 存储应用启动时间的全局变量
|
||||
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
||||
// 获取当前系统时间,转换为毫秒级时间戳
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
AtomicI64::new(now)
|
||||
static APP_START_TIME: Lazy<Instant> = Lazy::new(Instant::now);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| unsafe { libc::geteuid() } == 0);
|
||||
#[cfg(target_os = "windows")]
|
||||
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| {
|
||||
Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
@@ -45,52 +47,18 @@ pub async fn get_system_info() -> CmdResult<String> {
|
||||
|
||||
/// 获取当前内核运行模式
|
||||
#[tauri::command]
|
||||
pub async fn get_running_mode() -> Result<String, String> {
|
||||
Ok(CoreManager::global().get_running_mode().to_string())
|
||||
pub async fn get_running_mode() -> Result<Arc<RunningMode>, String> {
|
||||
Ok(CoreManager::global().get_running_mode())
|
||||
}
|
||||
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
#[tauri::command]
|
||||
pub fn get_app_uptime() -> CmdResult<i64> {
|
||||
let start_time = APP_START_TIME.load(Ordering::Relaxed);
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
Ok(now - start_time)
|
||||
pub fn get_app_uptime() -> CmdResult<u128> {
|
||||
Ok(APP_START_TIME.elapsed().as_millis())
|
||||
}
|
||||
|
||||
/// 检查应用是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
|
||||
let result = Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 非Windows平台检测是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
Ok(false)
|
||||
}
|
||||
Ok(*APPS_RUN_AS_ADMIN)
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
use serde::Serialize;
|
||||
use tauri::{Manager, ResourceId, Runtime, webview::Webview};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
use url::Url;
|
||||
|
||||
use super::{CmdResult, String};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateMetadata {
|
||||
rid: ResourceId,
|
||||
current_version: String,
|
||||
version: String,
|
||||
date: Option<String>,
|
||||
body: Option<String>,
|
||||
raw_json: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum UpdateChannel {
|
||||
Stable,
|
||||
Autobuild,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for UpdateChannel {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
"stable" => Ok(Self::Stable),
|
||||
"autobuild" => Ok(Self::Autobuild),
|
||||
other => Err(String::from(format!("Unsupported channel \"{other}\""))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CHANNEL_RELEASE_TAGS: &[(UpdateChannel, &str)] = &[
|
||||
(UpdateChannel::Stable, "updater"),
|
||||
(UpdateChannel::Autobuild, "updater-autobuild"),
|
||||
];
|
||||
|
||||
const CHANNEL_ENDPOINT_TEMPLATES: &[&str] = &[
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update-proxy.json",
|
||||
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update.json",
|
||||
];
|
||||
|
||||
fn resolve_release_tag(channel: UpdateChannel) -> CmdResult<&'static str> {
|
||||
CHANNEL_RELEASE_TAGS
|
||||
.iter()
|
||||
.find_map(|(entry_channel, tag)| (*entry_channel == channel).then_some(*tag))
|
||||
.ok_or_else(|| {
|
||||
String::from(format!(
|
||||
"No release tag registered for update channel \"{channel:?}\""
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_channel_endpoints(channel: UpdateChannel) -> CmdResult<Vec<Url>> {
|
||||
let release_tag = resolve_release_tag(channel)?;
|
||||
CHANNEL_ENDPOINT_TEMPLATES
|
||||
.iter()
|
||||
.map(|template| {
|
||||
let endpoint = template.replace("{release}", release_tag);
|
||||
Url::parse(&endpoint).map_err(|err| {
|
||||
String::from(format!(
|
||||
"Failed to parse updater endpoint \"{endpoint}\": {err}"
|
||||
))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tauri::command]
|
||||
pub async fn check_update_channel<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
channel: String,
|
||||
headers: Option<Vec<(String, String)>>,
|
||||
timeout: Option<u64>,
|
||||
proxy: Option<String>,
|
||||
target: Option<String>,
|
||||
allow_downgrades: Option<bool>,
|
||||
) -> CmdResult<Option<UpdateMetadata>> {
|
||||
let channel_enum = UpdateChannel::try_from(channel.as_str())?;
|
||||
let endpoints = resolve_channel_endpoints(channel_enum)?;
|
||||
|
||||
let mut builder = webview
|
||||
.updater_builder()
|
||||
.endpoints(endpoints)
|
||||
.map_err(|err| String::from(err.to_string()))?;
|
||||
|
||||
if let Some(headers) = headers {
|
||||
for (key, value) in headers {
|
||||
builder = builder
|
||||
.header(key.as_str(), value.as_str())
|
||||
.map_err(|err| String::from(err.to_string()))?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(timeout) = timeout {
|
||||
builder = builder.timeout(std::time::Duration::from_millis(timeout));
|
||||
}
|
||||
|
||||
if let Some(proxy) = proxy {
|
||||
let proxy_url = Url::parse(&proxy)
|
||||
.map_err(|err| String::from(format!("Invalid proxy URL \"{proxy}\": {err}")))?;
|
||||
builder = builder.proxy(proxy_url);
|
||||
}
|
||||
|
||||
if let Some(target) = target {
|
||||
builder = builder.target(target);
|
||||
}
|
||||
|
||||
let allow_downgrades = allow_downgrades.unwrap_or(channel_enum != UpdateChannel::Stable);
|
||||
|
||||
if allow_downgrades {
|
||||
builder = builder.version_comparator(|current, update| update.version != current);
|
||||
}
|
||||
|
||||
let updater = builder
|
||||
.build()
|
||||
.map_err(|err| String::from(err.to_string()))?;
|
||||
|
||||
let update = updater
|
||||
.check()
|
||||
.await
|
||||
.map_err(|err| String::from(err.to_string()))?;
|
||||
|
||||
let Some(update) = update else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let formatted_date = update
|
||||
.date
|
||||
.as_ref()
|
||||
.map(|date| String::from(date.to_string()));
|
||||
|
||||
let metadata = UpdateMetadata {
|
||||
rid: webview.resources_table().add(update.clone()),
|
||||
current_version: String::from(update.current_version.clone()),
|
||||
version: String::from(update.version.clone()),
|
||||
date: formatted_date,
|
||||
body: update.body.clone().map(Into::into),
|
||||
raw_json: update.raw_json.clone(),
|
||||
};
|
||||
|
||||
Ok(Some(metadata))
|
||||
}
|
||||
@@ -17,7 +17,7 @@ mod platform {
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
|
||||
pub fn invoke_uwp_tool() -> CmdResult {
|
||||
pub const fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IVergeResponse> {
|
||||
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<SharedBox<IVerge>> {
|
||||
feat::fetch_verge_config().await.stringify_err()
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -15,40 +15,40 @@ use tokio::sync::OnceCell;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub struct Config {
|
||||
clash_config: Draft<Box<IClashTemp>>,
|
||||
verge_config: Draft<Box<IVerge>>,
|
||||
profiles_config: Draft<Box<IProfiles>>,
|
||||
runtime_config: Draft<Box<IRuntime>>,
|
||||
clash_config: Draft<IClashTemp>,
|
||||
verge_config: Draft<IVerge>,
|
||||
profiles_config: Draft<IProfiles>,
|
||||
runtime_config: Draft<IRuntime>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn global() -> &'static Config {
|
||||
pub async fn global() -> &'static Self {
|
||||
static CONFIG: OnceCell<Config> = OnceCell::const_new();
|
||||
CONFIG
|
||||
.get_or_init(|| async {
|
||||
Config {
|
||||
clash_config: Draft::from(Box::new(IClashTemp::new().await)),
|
||||
verge_config: Draft::from(Box::new(IVerge::new().await)),
|
||||
profiles_config: Draft::from(Box::new(IProfiles::new().await)),
|
||||
runtime_config: Draft::from(Box::new(IRuntime::new())),
|
||||
Self {
|
||||
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<Box<IClashTemp>> {
|
||||
pub async fn clash() -> Draft<IClashTemp> {
|
||||
Self::global().await.clash_config.clone()
|
||||
}
|
||||
|
||||
pub async fn verge() -> Draft<Box<IVerge>> {
|
||||
pub async fn verge() -> Draft<IVerge> {
|
||||
Self::global().await.verge_config.clone()
|
||||
}
|
||||
|
||||
pub async fn profiles() -> Draft<Box<IProfiles>> {
|
||||
pub async fn profiles() -> Draft<IProfiles> {
|
||||
Self::global().await.profiles_config.clone()
|
||||
}
|
||||
|
||||
pub async fn runtime() -> Draft<Box<IRuntime>> {
|
||||
pub async fn runtime() -> Draft<IRuntime> {
|
||||
Self::global().await.runtime_config.clone()
|
||||
}
|
||||
|
||||
@@ -60,13 +60,15 @@ impl Config {
|
||||
if !cmd::system::is_admin().unwrap_or_default()
|
||||
&& service::is_service_available().await.is_err()
|
||||
{
|
||||
let verge = Config::verge().await;
|
||||
verge.draft_mut().enable_tun_mode = Some(false);
|
||||
let verge = Self::verge().await;
|
||||
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 = Self::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?;
|
||||
}
|
||||
@@ -152,27 +154,27 @@ impl Config {
|
||||
ConfigType::Check => dirs::app_home_dir()?.join(files::CHECK_CONFIG),
|
||||
};
|
||||
|
||||
let runtime = Config::runtime().await;
|
||||
let config = runtime
|
||||
.latest_ref()
|
||||
let runtime = Self::runtime().await;
|
||||
let runtime_arc = runtime.latest_arc();
|
||||
let config = runtime_arc
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("failed to get runtime config"))?
|
||||
.clone();
|
||||
drop(runtime); // 显式释放锁
|
||||
.ok_or_else(|| anyhow!("failed to get runtime config"))?;
|
||||
|
||||
help::save_yaml(&path, &config, Some("# Generated by Clash Verge")).await?;
|
||||
help::save_yaml(&path, config, Some("# Generated by Clash Verge")).await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
Self::runtime().await.edit_draft(|d| {
|
||||
*d = IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -187,11 +189,11 @@ impl Config {
|
||||
};
|
||||
|
||||
let operation = || async {
|
||||
if Config::runtime().await.latest_ref().config.is_some() {
|
||||
if Self::runtime().await.latest_arc().config.is_some() {
|
||||
return Ok::<(), BackoffError<anyhow::Error>>(());
|
||||
}
|
||||
|
||||
Config::generate().await.map_err(BackoffError::transient)
|
||||
Self::generate().await.map_err(BackoffError::transient)
|
||||
};
|
||||
|
||||
if let Err(e) = backoff::future::retry(backoff_strategy, operation).await {
|
||||
@@ -228,7 +230,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::<Draft<IRuntime>>());
|
||||
}
|
||||
@@ -236,7 +238,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,
|
||||
|
||||
@@ -5,9 +5,16 @@ use aes_gcm::{
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::cell::Cell;
|
||||
use std::future::Future;
|
||||
|
||||
const NONCE_LENGTH: usize = 12;
|
||||
|
||||
// Use task-local context so the flag follows the async task across threads
|
||||
tokio::task_local! {
|
||||
static ENCRYPTION_ACTIVE: Cell<bool>;
|
||||
}
|
||||
|
||||
/// Encrypt data
|
||||
#[allow(deprecated)]
|
||||
pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
@@ -59,39 +66,45 @@ where
|
||||
T: Serialize,
|
||||
S: Serializer,
|
||||
{
|
||||
// 如果序列化失败,返回 None
|
||||
let json = match serde_json::to_string(value) {
|
||||
Ok(j) => j,
|
||||
Err(_) => return serializer.serialize_none(),
|
||||
};
|
||||
|
||||
// 如果加密失败,返回 None
|
||||
match encrypt_data(&json) {
|
||||
Ok(encrypted) => serializer.serialize_str(&encrypted),
|
||||
Err(_) => serializer.serialize_none(),
|
||||
if is_encryption_active() {
|
||||
let json = serde_json::to_string(value).map_err(serde::ser::Error::custom)?;
|
||||
let encrypted = encrypt_data(&json).map_err(serde::ser::Error::custom)?;
|
||||
serializer.serialize_str(&encrypted)
|
||||
} else {
|
||||
value.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize decrypted function
|
||||
pub fn deserialize_encrypted<'a, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
pub fn deserialize_encrypted<'a, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de> + Default,
|
||||
D: Deserializer<'a>,
|
||||
{
|
||||
// 如果反序列化字符串失败,返回默认值
|
||||
let encrypted = match String::deserialize(deserializer) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Ok(T::default()),
|
||||
};
|
||||
if is_encryption_active() {
|
||||
let encrypted_opt: Option<String> = Option::deserialize(deserializer)?;
|
||||
|
||||
// 如果解密失败,返回默认值
|
||||
let decrypted_string = match decrypt_data(&encrypted) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Ok(T::default()),
|
||||
};
|
||||
// 如果 JSON 解析失败,返回默认值
|
||||
match serde_json::from_str(&decrypted_string) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => Ok(T::default()),
|
||||
match encrypted_opt {
|
||||
Some(encrypted) if !encrypted.is_empty() => {
|
||||
let decrypted_string =
|
||||
decrypt_data(&encrypted).map_err(serde::de::Error::custom)?;
|
||||
serde_json::from_str(&decrypted_string).map_err(serde::de::Error::custom)
|
||||
}
|
||||
_ => Ok(T::default()),
|
||||
}
|
||||
} else {
|
||||
T::deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn with_encryption<F, Fut, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = R>,
|
||||
{
|
||||
ENCRYPTION_ACTIVE.scope(Cell::new(true), f()).await
|
||||
}
|
||||
|
||||
fn is_encryption_active() -> bool {
|
||||
ENCRYPTION_ACTIVE.try_with(|c| c.get()).unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ impl PrfOption {
|
||||
impl PrfItem {
|
||||
/// From partial item
|
||||
/// must contain `itype`
|
||||
pub async fn from(item: &PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
||||
pub async fn from(item: &Self, file_data: Option<String>) -> Result<Self> {
|
||||
if item.itype.is_none() {
|
||||
bail!("type should not be null");
|
||||
}
|
||||
@@ -170,13 +170,13 @@ impl PrfItem {
|
||||
let name = item.name.as_ref();
|
||||
let desc = item.desc.as_ref();
|
||||
let option = item.option.as_ref();
|
||||
PrfItem::from_url(url, name, desc, option).await
|
||||
Self::from_url(url, name, desc, option).await
|
||||
}
|
||||
"local" => {
|
||||
let name = item.name.clone().unwrap_or_else(|| "Local File".into());
|
||||
let desc = item.desc.clone().unwrap_or_else(|| "".into());
|
||||
let option = item.option.as_ref();
|
||||
PrfItem::from_local(name, desc, file_data, option).await
|
||||
Self::from_local(name, desc, file_data, option).await
|
||||
}
|
||||
typ => bail!("invalid profile item type \"{typ}\""),
|
||||
}
|
||||
@@ -189,7 +189,7 @@ impl PrfItem {
|
||||
desc: String,
|
||||
file_data: Option<String>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
) -> Result<Self> {
|
||||
let uid = help::get_uid("L").into();
|
||||
let file = format!("{uid}.yaml").into();
|
||||
let opt_ref = option.as_ref();
|
||||
@@ -201,31 +201,31 @@ impl PrfItem {
|
||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = &mut PrfItem::from_merge(None)?;
|
||||
let merge_item = &mut Self::from_merge(None)?;
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = &mut PrfItem::from_script(None)?;
|
||||
let script_item = &mut Self::from_script(None)?;
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut PrfItem::from_rules()?;
|
||||
let rules_item = &mut Self::from_rules()?;
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut PrfItem::from_proxies()?;
|
||||
let proxies_item = &mut Self::from_proxies()?;
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut PrfItem::from_groups()?;
|
||||
let groups_item = &mut Self::from_groups()?;
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("local".into()),
|
||||
name: Some(name),
|
||||
@@ -256,7 +256,7 @@ impl PrfItem {
|
||||
name: Option<&String>,
|
||||
desc: Option<&String>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
) -> Result<Self> {
|
||||
let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||
let accept_invalid_certs =
|
||||
@@ -393,32 +393,32 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = &mut PrfItem::from_merge(None)?;
|
||||
let merge_item = &mut Self::from_merge(None)?;
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = &mut PrfItem::from_script(None)?;
|
||||
let script_item = &mut Self::from_script(None)?;
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut PrfItem::from_rules()?;
|
||||
let rules_item = &mut Self::from_rules()?;
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut PrfItem::from_proxies()?;
|
||||
let proxies_item = &mut Self::from_proxies()?;
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut PrfItem::from_groups()?;
|
||||
let groups_item = &mut Self::from_groups()?;
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("remote".into()),
|
||||
name: Some(name),
|
||||
@@ -445,16 +445,15 @@ impl PrfItem {
|
||||
|
||||
/// ## Merge type (enhance)
|
||||
/// create the enhanced item by using `merge` rule
|
||||
pub fn from_merge(uid: Option<String>) -> Result<PrfItem> {
|
||||
let mut id = help::get_uid("m").into();
|
||||
let mut template = tmpl::ITEM_MERGE_EMPTY.into();
|
||||
if let Some(uid) = uid {
|
||||
id = uid;
|
||||
template = tmpl::ITEM_MERGE.into();
|
||||
}
|
||||
pub fn from_merge(uid: Option<String>) -> Result<Self> {
|
||||
let (id, template) = if let Some(uid) = uid {
|
||||
(uid, tmpl::ITEM_MERGE.into())
|
||||
} else {
|
||||
(help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
|
||||
};
|
||||
let file = format!("{id}.yaml").into();
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(id),
|
||||
itype: Some("merge".into()),
|
||||
name: None,
|
||||
@@ -472,14 +471,14 @@ impl PrfItem {
|
||||
|
||||
/// ## Script type (enhance)
|
||||
/// create the enhanced item by using javascript quick.js
|
||||
pub fn from_script(uid: Option<String>) -> Result<PrfItem> {
|
||||
let mut id = help::get_uid("s").into();
|
||||
if let Some(uid) = uid {
|
||||
id = uid;
|
||||
}
|
||||
pub fn from_script(uid: Option<String>) -> Result<Self> {
|
||||
let id = if let Some(uid) = uid {
|
||||
uid
|
||||
} else {
|
||||
help::get_uid("s").into()
|
||||
};
|
||||
let file = format!("{id}.js").into(); // js ext
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(id),
|
||||
itype: Some("script".into()),
|
||||
name: None,
|
||||
@@ -496,11 +495,11 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
/// ## Rules type (enhance)
|
||||
pub fn from_rules() -> Result<PrfItem> {
|
||||
pub fn from_rules() -> Result<Self> {
|
||||
let uid = help::get_uid("r").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("rules".into()),
|
||||
name: None,
|
||||
@@ -517,11 +516,11 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
/// ## Proxies type (enhance)
|
||||
pub fn from_proxies() -> Result<PrfItem> {
|
||||
pub fn from_proxies() -> Result<Self> {
|
||||
let uid = help::get_uid("p").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("proxies".into()),
|
||||
name: None,
|
||||
@@ -538,11 +537,11 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
/// ## Groups type (enhance)
|
||||
pub fn from_groups() -> Result<PrfItem> {
|
||||
pub fn from_groups() -> Result<Self> {
|
||||
let uid = help::get_uid("g").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("groups".into()),
|
||||
name: None,
|
||||
@@ -584,7 +583,34 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
impl PrfItem {
|
||||
/// 获取current指向的订阅的merge
|
||||
pub fn current_merge(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.merge.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的script
|
||||
pub fn current_script(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.script.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的rules
|
||||
pub fn current_rules(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.rules.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的proxies
|
||||
pub fn current_proxies(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.proxies.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的groups
|
||||
pub fn current_groups(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.groups.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// 向前兼容,默认为订阅启用自动更新
|
||||
fn default_allow_auto_update() -> Option<bool> {
|
||||
const fn default_allow_auto_update() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
@@ -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::<Self>(&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::<Self>(&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()
|
||||
@@ -89,7 +87,7 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// 只修改current,valid和chain
|
||||
pub fn patch_config(&mut self, patch: &IProfiles) -> Result<()> {
|
||||
pub fn patch_config(&mut self, patch: &Self) {
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
@@ -102,16 +100,14 @@ impl IProfiles {
|
||||
self.current = some_uid.cloned();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_current(&self) -> Option<String> {
|
||||
self.current.clone()
|
||||
pub const fn get_current(&self) -> Option<&String> {
|
||||
self.current.as_ref()
|
||||
}
|
||||
|
||||
/// get items ref
|
||||
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
|
||||
pub const fn get_items(&self) -> Option<&Vec<PrfItem>> {
|
||||
self.items.as_ref()
|
||||
}
|
||||
|
||||
@@ -132,6 +128,15 @@ impl IProfiles {
|
||||
bail!("failed to get the profile item \"uid:{}\"", uid_str);
|
||||
}
|
||||
|
||||
pub fn get_item_arc(&self, uid: &str) -> Option<Arc<PrfItem>> {
|
||||
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 +362,18 @@ impl IProfiles {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的merge
|
||||
pub fn current_merge(&self) -> Option<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<Vec<(String, String)>> {
|
||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(&String, &String)>> {
|
||||
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 +384,11 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// 通过 uid 获取名称
|
||||
pub fn get_name_by_uid(&self, uid: &String) -> Option<String> {
|
||||
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 +486,14 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// 获取所有 active profile 关联的文件名
|
||||
fn get_all_active_files(&self) -> HashSet<String> {
|
||||
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 +506,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,19 +46,24 @@ pub struct IVerge {
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
|
||||
/// enable group icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_group_icon: Option<bool>,
|
||||
|
||||
/// common tray icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub common_tray_icon: Option<bool>,
|
||||
|
||||
/// tray icon
|
||||
#[cfg(target_os = "macos")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_icon: Option<String>,
|
||||
|
||||
/// menu icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub menu_icon: Option<String>,
|
||||
|
||||
/// menu order
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub menu_order: Option<Vec<String>>,
|
||||
|
||||
/// 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<Vec<String>>,
|
||||
|
||||
/// enable global hotkey
|
||||
@@ -134,7 +140,7 @@ pub struct IVerge {
|
||||
pub default_latency_test: Option<String>,
|
||||
|
||||
/// 默认的延迟测试超时时间
|
||||
pub default_latency_timeout: Option<i32>,
|
||||
pub default_latency_timeout: Option<i16>,
|
||||
|
||||
/// 是否自动检测当前节点延迟
|
||||
pub enable_auto_delay_detection: Option<bool>,
|
||||
@@ -143,7 +149,7 @@ pub struct IVerge {
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
|
||||
/// proxy 页面布局 列数
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
pub proxy_layout_column: Option<u8>,
|
||||
|
||||
/// 测试站列表
|
||||
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||
@@ -202,6 +208,7 @@ pub struct IVerge {
|
||||
)]
|
||||
pub webdav_password: Option<String>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
|
||||
// pub enable_tray_icon: Option<bool>,
|
||||
@@ -255,7 +262,7 @@ impl IVerge {
|
||||
/// 验证并修正配置文件中的clash_core值
|
||||
pub async fn validate_and_fix_config() -> Result<()> {
|
||||
let config_path = dirs::verge_path()?;
|
||||
let mut config = match help::read_yaml::<IVerge>(&config_path).await {
|
||||
let mut config = match help::read_yaml::<Self>(&config_path).await {
|
||||
Ok(config) => config,
|
||||
Err(_) => Self::template(),
|
||||
};
|
||||
@@ -304,7 +311,7 @@ impl IVerge {
|
||||
}
|
||||
|
||||
/// 配置修正后重新加载配置
|
||||
async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
|
||||
async fn reload_config_after_fix(updated_config: Self) -> Result<()> {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
@@ -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(())
|
||||
@@ -342,7 +351,7 @@ impl IVerge {
|
||||
|
||||
pub async fn new() -> Self {
|
||||
match dirs::verge_path() {
|
||||
Ok(path) => match help::read_yaml::<IVerge>(&path).await {
|
||||
Ok(path) => match help::read_yaml::<Self>(&path).await {
|
||||
Ok(mut config) => {
|
||||
// compatibility
|
||||
if let Some(start_page) = config.start_page.clone()
|
||||
@@ -437,7 +446,7 @@ impl IVerge {
|
||||
/// patch verge config
|
||||
/// only save to file
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn patch_config(&mut self, patch: &IVerge) {
|
||||
pub fn patch_config(&mut self, patch: &Self) {
|
||||
macro_rules! patch {
|
||||
($key: tt) => {
|
||||
if patch.$key.is_some() {
|
||||
@@ -522,7 +531,7 @@ impl IVerge {
|
||||
patch!(enable_external_controller);
|
||||
}
|
||||
|
||||
pub fn get_singleton_port() -> u16 {
|
||||
pub const fn get_singleton_port() -> u16 {
|
||||
crate::constants::network::ports::SINGLETON_SERVER
|
||||
}
|
||||
|
||||
@@ -543,162 +552,3 @@ impl IVerge {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct IVergeResponse {
|
||||
pub app_log_level: Option<String>,
|
||||
pub app_log_max_size: Option<u64>,
|
||||
pub app_log_max_count: Option<usize>,
|
||||
pub language: Option<String>,
|
||||
pub theme_mode: Option<String>,
|
||||
pub tray_event: Option<String>,
|
||||
pub env_type: Option<String>,
|
||||
pub start_page: Option<String>,
|
||||
pub startup_script: Option<String>,
|
||||
pub traffic_graph: Option<bool>,
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
pub enable_group_icon: Option<bool>,
|
||||
pub common_tray_icon: Option<bool>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub tray_icon: Option<String>,
|
||||
pub menu_icon: Option<String>,
|
||||
pub menu_order: Option<Vec<String>>,
|
||||
pub sysproxy_tray_icon: Option<bool>,
|
||||
pub tun_tray_icon: Option<bool>,
|
||||
pub enable_tun_mode: Option<bool>,
|
||||
pub enable_auto_launch: Option<bool>,
|
||||
pub enable_silent_start: Option<bool>,
|
||||
pub enable_system_proxy: Option<bool>,
|
||||
pub enable_proxy_guard: Option<bool>,
|
||||
pub enable_global_hotkey: Option<bool>,
|
||||
pub use_default_bypass: Option<bool>,
|
||||
pub system_proxy_bypass: Option<String>,
|
||||
pub proxy_guard_duration: Option<u64>,
|
||||
pub proxy_auto_config: Option<bool>,
|
||||
pub pac_file_content: Option<String>,
|
||||
pub proxy_host: Option<String>,
|
||||
pub theme_setting: Option<IVergeTheme>,
|
||||
pub web_ui_list: Option<Vec<String>>,
|
||||
pub clash_core: Option<String>,
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
pub auto_close_connection: Option<bool>,
|
||||
pub auto_check_update: Option<bool>,
|
||||
pub default_latency_test: Option<String>,
|
||||
pub default_latency_timeout: Option<i32>,
|
||||
pub enable_auto_delay_detection: Option<bool>,
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||
pub auto_log_clean: Option<i32>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub verge_redir_port: Option<u16>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub verge_redir_enabled: Option<bool>,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub verge_tproxy_port: Option<u16>,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub verge_tproxy_enabled: Option<bool>,
|
||||
pub verge_mixed_port: Option<u16>,
|
||||
pub verge_socks_port: Option<u16>,
|
||||
pub verge_socks_enabled: Option<bool>,
|
||||
pub verge_port: Option<u16>,
|
||||
pub verge_http_enabled: Option<bool>,
|
||||
pub webdav_url: Option<String>,
|
||||
pub webdav_username: Option<String>,
|
||||
pub webdav_password: Option<String>,
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
// pub enable_tray_icon: Option<bool>,
|
||||
pub tray_inline_proxy_groups: Option<bool>,
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
pub enable_hover_jump_navigator: Option<bool>,
|
||||
pub hover_jump_navigator_delay: Option<u64>,
|
||||
pub enable_external_controller: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<IVerge> for IVergeResponse {
|
||||
fn from(verge: IVerge) -> Self {
|
||||
// 先获取验证后的clash_core值,避免后续借用冲突
|
||||
let valid_clash_core = verge.get_valid_clash_core();
|
||||
Self {
|
||||
app_log_level: verge.app_log_level,
|
||||
app_log_max_size: verge.app_log_max_size,
|
||||
app_log_max_count: verge.app_log_max_count,
|
||||
language: verge.language,
|
||||
theme_mode: verge.theme_mode,
|
||||
tray_event: verge.tray_event,
|
||||
env_type: verge.env_type,
|
||||
start_page: verge.start_page,
|
||||
startup_script: verge.startup_script,
|
||||
traffic_graph: verge.traffic_graph,
|
||||
enable_memory_usage: verge.enable_memory_usage,
|
||||
enable_group_icon: verge.enable_group_icon,
|
||||
common_tray_icon: verge.common_tray_icon,
|
||||
#[cfg(target_os = "macos")]
|
||||
tray_icon: verge.tray_icon,
|
||||
menu_icon: verge.menu_icon,
|
||||
menu_order: verge.menu_order,
|
||||
sysproxy_tray_icon: verge.sysproxy_tray_icon,
|
||||
tun_tray_icon: verge.tun_tray_icon,
|
||||
enable_tun_mode: verge.enable_tun_mode,
|
||||
enable_auto_launch: verge.enable_auto_launch,
|
||||
enable_silent_start: verge.enable_silent_start,
|
||||
enable_system_proxy: verge.enable_system_proxy,
|
||||
enable_proxy_guard: verge.enable_proxy_guard,
|
||||
enable_global_hotkey: verge.enable_global_hotkey,
|
||||
use_default_bypass: verge.use_default_bypass,
|
||||
system_proxy_bypass: verge.system_proxy_bypass,
|
||||
proxy_guard_duration: verge.proxy_guard_duration,
|
||||
proxy_auto_config: verge.proxy_auto_config,
|
||||
pac_file_content: verge.pac_file_content,
|
||||
proxy_host: verge.proxy_host,
|
||||
theme_setting: verge.theme_setting,
|
||||
web_ui_list: verge.web_ui_list,
|
||||
clash_core: Some(valid_clash_core),
|
||||
hotkeys: verge.hotkeys,
|
||||
auto_close_connection: verge.auto_close_connection,
|
||||
auto_check_update: verge.auto_check_update,
|
||||
default_latency_test: verge.default_latency_test,
|
||||
default_latency_timeout: verge.default_latency_timeout,
|
||||
enable_auto_delay_detection: verge.enable_auto_delay_detection,
|
||||
enable_builtin_enhanced: verge.enable_builtin_enhanced,
|
||||
proxy_layout_column: verge.proxy_layout_column,
|
||||
test_list: verge.test_list,
|
||||
auto_log_clean: verge.auto_log_clean,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
verge_redir_port: verge.verge_redir_port,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
verge_redir_enabled: verge.verge_redir_enabled,
|
||||
#[cfg(target_os = "linux")]
|
||||
verge_tproxy_port: verge.verge_tproxy_port,
|
||||
#[cfg(target_os = "linux")]
|
||||
verge_tproxy_enabled: verge.verge_tproxy_enabled,
|
||||
verge_mixed_port: verge.verge_mixed_port,
|
||||
verge_socks_port: verge.verge_socks_port,
|
||||
verge_socks_enabled: verge.verge_socks_enabled,
|
||||
verge_port: verge.verge_port,
|
||||
verge_http_enabled: verge.verge_http_enabled,
|
||||
webdav_url: verge.webdav_url,
|
||||
webdav_username: verge.webdav_username,
|
||||
webdav_password: verge.webdav_password,
|
||||
enable_tray_speed: verge.enable_tray_speed,
|
||||
// enable_tray_icon: verge.enable_tray_icon,
|
||||
tray_inline_proxy_groups: verge.tray_inline_proxy_groups,
|
||||
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||
enable_dns_settings: verge.enable_dns_settings,
|
||||
home_cards: verge.home_cards,
|
||||
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
||||
hover_jump_navigator_delay: verge.hover_jump_navigator_delay,
|
||||
enable_external_controller: verge.enable_external_controller,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<IVerge>> for IVergeResponse {
|
||||
fn from(verge: Box<IVerge>) -> Self {
|
||||
IVergeResponse::from(*verge)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,11 +347,12 @@ impl AsyncProxyQuery {
|
||||
&mut buffer_size,
|
||||
);
|
||||
|
||||
let mut proxy_server = String::new();
|
||||
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
||||
let proxy_server = if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
||||
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
|
||||
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
|
||||
}
|
||||
String::from_utf16_lossy(&buffer[..end_pos])
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// 读取代理绕过列表
|
||||
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
|
||||
@@ -368,14 +369,16 @@ impl AsyncProxyQuery {
|
||||
&mut bypass_buffer_size,
|
||||
);
|
||||
|
||||
let mut bypass_list = String::new();
|
||||
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
|
||||
let end_pos = bypass_buffer
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(bypass_buffer.len());
|
||||
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
|
||||
}
|
||||
let bypass_list =
|
||||
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
|
||||
let end_pos = bypass_buffer
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(bypass_buffer.len());
|
||||
String::from_utf16_lossy(&bypass_buffer[..end_pos])
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
RegCloseKey(hkey);
|
||||
|
||||
|
||||
@@ -45,12 +45,12 @@ enum Operation {
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
fn timeout(&self) -> u64 {
|
||||
const fn timeout(&self) -> u64 {
|
||||
match self {
|
||||
Operation::Upload => TIMEOUT_UPLOAD,
|
||||
Operation::Download => TIMEOUT_DOWNLOAD,
|
||||
Operation::List => TIMEOUT_LIST,
|
||||
Operation::Delete => TIMEOUT_DELETE,
|
||||
Self::Upload => TIMEOUT_UPLOAD,
|
||||
Self::Download => TIMEOUT_DOWNLOAD,
|
||||
Self::List => TIMEOUT_LIST,
|
||||
Self::Delete => TIMEOUT_DELETE,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,9 +61,9 @@ pub struct WebDavClient {
|
||||
}
|
||||
|
||||
impl WebDavClient {
|
||||
pub fn global() -> &'static WebDavClient {
|
||||
pub fn global() -> &'static Self {
|
||||
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
|
||||
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
|
||||
WEBDAV_CLIENT.get_or_init(|| Self {
|
||||
config: Arc::new(ArcSwapOption::new(None)),
|
||||
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
|
||||
})
|
||||
@@ -87,7 +87,7 @@ impl WebDavClient {
|
||||
(*cfg_arc).clone()
|
||||
} else {
|
||||
// 释放锁后获取异步配置
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
let verge = Config::verge().await.data_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
|
||||
|
||||
@@ -75,7 +75,7 @@ struct ProxyConfig {
|
||||
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
|
||||
|
||||
impl EventDrivenProxyManager {
|
||||
pub fn global() -> &'static EventDrivenProxyManager {
|
||||
pub fn global() -> &'static Self {
|
||||
&PROXY_MANAGER
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
|
||||
|
||||
@@ -102,14 +102,14 @@ impl Handle {
|
||||
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
|
||||
}
|
||||
|
||||
// TODO 利用 &str 等缩短 Clone
|
||||
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
||||
let handle = Self::global();
|
||||
let status_str = status.into();
|
||||
let msg_str = msg.into();
|
||||
|
||||
if !*handle.startup_completed.read() {
|
||||
let mut errors = handle.startup_errors.write();
|
||||
errors.push(ErrorMessage {
|
||||
handle.startup_errors.write().push(ErrorMessage {
|
||||
status: status_str,
|
||||
message: msg_str,
|
||||
});
|
||||
@@ -158,7 +158,7 @@ impl Handle {
|
||||
.spawn(move || {
|
||||
thread::sleep(timing::STARTUP_ERROR_DELAY);
|
||||
|
||||
let handle = Handle::global();
|
||||
let handle = Self::global();
|
||||
if handle.is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,16 +28,16 @@ pub enum HotkeyFunction {
|
||||
impl fmt::Display for HotkeyFunction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard",
|
||||
HotkeyFunction::ClashModeRule => "clash_mode_rule",
|
||||
HotkeyFunction::ClashModeGlobal => "clash_mode_global",
|
||||
HotkeyFunction::ClashModeDirect => "clash_mode_direct",
|
||||
HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy",
|
||||
HotkeyFunction::ToggleTunMode => "toggle_tun_mode",
|
||||
HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode",
|
||||
HotkeyFunction::Quit => "quit",
|
||||
Self::OpenOrCloseDashboard => "open_or_close_dashboard",
|
||||
Self::ClashModeRule => "clash_mode_rule",
|
||||
Self::ClashModeGlobal => "clash_mode_global",
|
||||
Self::ClashModeDirect => "clash_mode_direct",
|
||||
Self::ToggleSystemProxy => "toggle_system_proxy",
|
||||
Self::ToggleTunMode => "toggle_tun_mode",
|
||||
Self::EntryLightweightMode => "entry_lightweight_mode",
|
||||
Self::Quit => "quit",
|
||||
#[cfg(target_os = "macos")]
|
||||
HotkeyFunction::Hide => "hide",
|
||||
Self::Hide => "hide",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
@@ -48,16 +48,16 @@ impl FromStr for HotkeyFunction {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim() {
|
||||
"open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard),
|
||||
"clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule),
|
||||
"clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal),
|
||||
"clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect),
|
||||
"toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy),
|
||||
"toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode),
|
||||
"entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode),
|
||||
"quit" => Ok(HotkeyFunction::Quit),
|
||||
"open_or_close_dashboard" => Ok(Self::OpenOrCloseDashboard),
|
||||
"clash_mode_rule" => Ok(Self::ClashModeRule),
|
||||
"clash_mode_global" => Ok(Self::ClashModeGlobal),
|
||||
"clash_mode_direct" => Ok(Self::ClashModeDirect),
|
||||
"toggle_system_proxy" => Ok(Self::ToggleSystemProxy),
|
||||
"toggle_tun_mode" => Ok(Self::ToggleTunMode),
|
||||
"entry_lightweight_mode" => Ok(Self::EntryLightweightMode),
|
||||
"quit" => Ok(Self::Quit),
|
||||
#[cfg(target_os = "macos")]
|
||||
"hide" => Ok(HotkeyFunction::Hide),
|
||||
"hide" => Ok(Self::Hide),
|
||||
_ => bail!("invalid hotkey function: {}", s),
|
||||
}
|
||||
}
|
||||
@@ -75,8 +75,8 @@ pub enum SystemHotkey {
|
||||
impl fmt::Display for SystemHotkey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
SystemHotkey::CmdQ => "CMD+Q",
|
||||
SystemHotkey::CmdW => "CMD+W",
|
||||
Self::CmdQ => "CMD+Q",
|
||||
Self::CmdW => "CMD+W",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
@@ -84,10 +84,10 @@ impl fmt::Display for SystemHotkey {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl SystemHotkey {
|
||||
pub fn function(self) -> HotkeyFunction {
|
||||
pub const fn function(self) -> HotkeyFunction {
|
||||
match self {
|
||||
SystemHotkey::CmdQ => HotkeyFunction::Quit,
|
||||
SystemHotkey::CmdW => HotkeyFunction::Hide,
|
||||
Self::CmdQ => HotkeyFunction::Quit,
|
||||
Self::CmdW => HotkeyFunction::Hide,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!(
|
||||
|
||||
@@ -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);
|
||||
@@ -135,7 +137,7 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_connection_io_error(kind: std::io::ErrorKind) -> bool {
|
||||
const fn is_connection_io_error(kind: std::io::ErrorKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
std::io::ErrorKind::ConnectionAborted
|
||||
|
||||
@@ -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)
|
||||
@@ -66,7 +68,8 @@ impl CoreManager {
|
||||
#[cfg(target_os = "windows")]
|
||||
self.wait_for_service_if_needed().await;
|
||||
|
||||
let mode = match SERVICE_MANAGER.lock().await.current() {
|
||||
let value = SERVICE_MANAGER.lock().await.current();
|
||||
let mode = match value {
|
||||
ServiceStatus::Ready => RunningMode::Service,
|
||||
_ => RunningMode::Sidecar,
|
||||
};
|
||||
@@ -82,7 +85,7 @@ impl CoreManager {
|
||||
|
||||
let needs_service = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.latest_arc()
|
||||
.enable_tun_mode
|
||||
.unwrap_or(false);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -62,8 +62,12 @@ impl CoreManager {
|
||||
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
|
||||
let mut now = DeferredNow::default();
|
||||
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
|
||||
let w = shared_writer.lock().await;
|
||||
write_sidecar_log(w, &mut now, Level::Error, &message);
|
||||
write_sidecar_log(
|
||||
shared_writer.lock().await,
|
||||
&mut now,
|
||||
Level::Error,
|
||||
&message,
|
||||
);
|
||||
CLASH_LOGGER.append_log(message).await;
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
|
||||
@@ -75,8 +79,12 @@ impl CoreManager {
|
||||
} else {
|
||||
CompactString::from("Process terminated")
|
||||
};
|
||||
let w = shared_writer.lock().await;
|
||||
write_sidecar_log(w, &mut now, Level::Info, &message);
|
||||
write_sidecar_log(
|
||||
shared_writer.lock().await,
|
||||
&mut now,
|
||||
Level::Info,
|
||||
&message,
|
||||
);
|
||||
CLASH_LOGGER.clear_logs().await;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::handle::Handle;
|
||||
use crate::{
|
||||
constants::{retry, timing},
|
||||
logging,
|
||||
@@ -11,7 +12,7 @@ use std::{
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
time::Instant,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri::{Emitter, WebviewWindow};
|
||||
|
||||
@@ -91,30 +92,26 @@ impl NotificationSystem {
|
||||
}
|
||||
|
||||
fn worker_loop(rx: mpsc::Receiver<FrontendEvent>) {
|
||||
use super::handle::Handle;
|
||||
|
||||
let handle = Handle::global();
|
||||
|
||||
while !handle.is_exiting() {
|
||||
match rx.recv() {
|
||||
loop {
|
||||
let handle = Handle::global();
|
||||
if handle.is_exiting() {
|
||||
break;
|
||||
}
|
||||
match rx.recv_timeout(Duration::from_millis(1_000)) {
|
||||
Ok(event) => Self::process_event(handle, event),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::System,
|
||||
"receive event error, stop notification worker: {}",
|
||||
e
|
||||
);
|
||||
break;
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => (),
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clippy 似乎对 parking lot 的 RwLock 有误报,这里禁用相关警告
|
||||
#[allow(clippy::significant_drop_tightening)]
|
||||
fn process_event(handle: &super::handle::Handle, event: FrontendEvent) {
|
||||
let system_guard = handle.notification_system.read();
|
||||
let Some(system) = system_guard.as_ref() else {
|
||||
return;
|
||||
let binding = handle.notification_system.read();
|
||||
let system = match binding.as_ref() {
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if system.should_skip_event(&event) {
|
||||
|
||||
@@ -136,7 +136,7 @@ async fn uninstall_service() -> Result<()> {
|
||||
let elevator = crate::utils::help::linux_elevator();
|
||||
let status = match get_effective_uid() {
|
||||
0 => StdCommand::new(uninstall_shell).status()?,
|
||||
_ => StdCommand::new(elevator.clone())
|
||||
_ => StdCommand::new(elevator)
|
||||
.arg("sh")
|
||||
.arg("-c")
|
||||
.arg(uninstall_shell)
|
||||
@@ -177,7 +177,7 @@ async fn install_service() -> Result<()> {
|
||||
let elevator = crate::utils::help::linux_elevator();
|
||||
let status = match get_effective_uid() {
|
||||
0 => StdCommand::new(install_shell).status()?,
|
||||
_ => StdCommand::new(elevator.clone())
|
||||
_ => StdCommand::new(elevator)
|
||||
.arg("sh")
|
||||
.arg("-c")
|
||||
.arg(install_shell)
|
||||
@@ -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 { "" };
|
||||
@@ -449,7 +449,7 @@ impl ServiceManager {
|
||||
Self(ServiceStatus::Unavailable("Need Checks".into()))
|
||||
}
|
||||
|
||||
pub fn config() -> Option<clash_verge_service_ipc::IpcConfig> {
|
||||
pub const fn config() -> Option<clash_verge_service_ipc::IpcConfig> {
|
||||
Some(clash_verge_service_ipc::IpcConfig {
|
||||
default_timeout: Duration::from_millis(30),
|
||||
retry_delay: Duration::from_millis(250),
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -84,7 +84,7 @@ async fn execute_sysproxy_command(args: Vec<std::string::String>) -> Result<()>
|
||||
|
||||
impl Default for Sysopt {
|
||||
fn default() -> Self {
|
||||
Sysopt {
|
||||
Self {
|
||||
initialed: AtomicBool::new(false),
|
||||
update_sysproxy: AtomicBool::new(false),
|
||||
reset_sysproxy: AtomicBool::new(false),
|
||||
@@ -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,
|
||||
|
||||
@@ -46,7 +46,7 @@ singleton!(Timer, TIMER_INSTANCE);
|
||||
|
||||
impl Timer {
|
||||
fn new() -> Self {
|
||||
Timer {
|
||||
Self {
|
||||
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
|
||||
timer_map: Arc::new(RwLock::new(HashMap::new())),
|
||||
timer_count: AtomicU64::new(1),
|
||||
@@ -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| {
|
||||
@@ -154,19 +154,17 @@ impl Timer {
|
||||
/// 每 3 秒更新系统托盘菜单,总共执行 3 次
|
||||
pub fn add_update_tray_menu_task(&self) -> Result<()> {
|
||||
let tid = self.timer_count.fetch_add(1, Ordering::SeqCst);
|
||||
let delay_timer = self.delay_timer.write();
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(tid)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.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
|
||||
self.delay_timer
|
||||
.write()
|
||||
.add_task(task)
|
||||
.context("failed to add update tray menu timer task")?;
|
||||
Ok(())
|
||||
@@ -195,14 +193,12 @@ impl Timer {
|
||||
|
||||
// Perform sync operations while holding locks
|
||||
{
|
||||
let mut timer_map = self.timer_map.write();
|
||||
let delay_timer = self.delay_timer.write();
|
||||
|
||||
for (uid, diff) in diff_map {
|
||||
match diff {
|
||||
DiffFlag::Del(tid) => {
|
||||
timer_map.remove(&uid);
|
||||
if let Err(e) = delay_timer.remove_task(tid) {
|
||||
self.timer_map.write().remove(&uid);
|
||||
let value = self.delay_timer.write().remove_task(tid);
|
||||
if let Err(e) = value {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
@@ -222,12 +218,13 @@ impl Timer {
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(uid.clone(), task);
|
||||
self.timer_map.write().insert(uid.clone(), task);
|
||||
operations_to_add.push((uid, tid, interval));
|
||||
}
|
||||
DiffFlag::Mod(tid, interval) => {
|
||||
// Remove old task first
|
||||
if let Err(e) = delay_timer.remove_task(tid) {
|
||||
let value = self.delay_timer.write().remove_task(tid);
|
||||
if let Err(e) = value {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
@@ -245,7 +242,7 @@ impl Timer {
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(uid.clone(), task);
|
||||
self.timer_map.write().insert(uid.clone(), task);
|
||||
operations_to_add.push((uid, tid, interval));
|
||||
}
|
||||
}
|
||||
@@ -255,8 +252,8 @@ impl Timer {
|
||||
// Now perform async operations without holding locks
|
||||
for (uid, tid, interval) in operations_to_add {
|
||||
// Re-acquire locks for individual operations
|
||||
let mut delay_timer = self.delay_timer.write();
|
||||
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||
let delay_timer = self.delay_timer.write();
|
||||
if let Err(e) = self.add_task(&delay_timer, uid.clone(), tid, interval) {
|
||||
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
|
||||
|
||||
// Rollback on failure - remove from timer_map
|
||||
@@ -273,7 +270,7 @@ impl Timer {
|
||||
async fn gen_map(&self) -> HashMap<String, u64> {
|
||||
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
|
||||
@@ -373,7 +370,7 @@ impl Timer {
|
||||
/// Add a timer task with better error handling
|
||||
fn add_task(
|
||||
&self,
|
||||
delay_timer: &mut DelayTimer,
|
||||
delay_timer: &DelayTimer,
|
||||
uid: String,
|
||||
tid: TaskID,
|
||||
minutes: u64,
|
||||
@@ -427,7 +424,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 +486,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,
|
||||
|
||||
@@ -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;
|
||||
@@ -25,6 +24,8 @@ use futures::future::join_all;
|
||||
use parking_lot::Mutex;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
@@ -55,18 +56,17 @@ fn get_tray_click_debounce() -> &'static Mutex<Instant> {
|
||||
|
||||
fn should_handle_tray_click() -> bool {
|
||||
let debounce_lock = get_tray_click_debounce();
|
||||
let mut last_click = debounce_lock.lock();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
|
||||
*last_click = now;
|
||||
if now.duration_since(*debounce_lock.lock()) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
|
||||
*debounce_lock.lock() = now;
|
||||
true
|
||||
} else {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Tray,
|
||||
"托盘点击被防抖机制忽略,距离上次点击 {}ms",
|
||||
now.duration_since(*last_click).as_millis()
|
||||
now.duration_since(*debounce_lock.lock()).as_millis()
|
||||
);
|
||||
false
|
||||
}
|
||||
@@ -86,7 +86,7 @@ pub struct Tray {
|
||||
|
||||
impl TrayState {
|
||||
pub async fn get_common_tray_icon() -> (bool, Vec<u8>) {
|
||||
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 +123,7 @@ impl TrayState {
|
||||
}
|
||||
|
||||
pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
|
||||
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 +160,7 @@ impl TrayState {
|
||||
}
|
||||
|
||||
pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) {
|
||||
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")
|
||||
@@ -198,7 +198,7 @@ impl TrayState {
|
||||
|
||||
impl Default for Tray {
|
||||
fn default() -> Self {
|
||||
Tray {
|
||||
Self {
|
||||
last_menu_update: Mutex::new(None),
|
||||
menu_updating: AtomicBool::new(false),
|
||||
}
|
||||
@@ -243,7 +243,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 +303,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 +311,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 +373,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 +416,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 +431,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 +440,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 +454,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 +505,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 +530,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 +561,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:?}");
|
||||
|
||||
@@ -598,9 +576,6 @@ impl Tray {
|
||||
return;
|
||||
}
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
let fut: Pin<Box<dyn Future<Output = ()> + Send>> = match tray_event.as_str() {
|
||||
"system_proxy" => Box::pin(async move {
|
||||
feat::toggle_system_proxy().await;
|
||||
@@ -622,22 +597,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<Vec<String>>) -> HashMap<String, String> {
|
||||
@@ -666,7 +625,7 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
|
||||
|
||||
async fn create_profile_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
profile_uid_and_name: Vec<(String, String)>,
|
||||
profile_uid_and_name: Vec<(&String, &String)>,
|
||||
) -> Result<Vec<CheckMenuItem<Wry>>> {
|
||||
let futures = profile_uid_and_name
|
||||
.iter()
|
||||
@@ -675,7 +634,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 +829,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<tauri::menu::Menu<Wry>> {
|
||||
let current_proxy_mode = mode.unwrap_or("");
|
||||
@@ -878,10 +837,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 +883,7 @@ async fn create_tray_menu(
|
||||
.collect::<HashMap<String, usize>>()
|
||||
});
|
||||
|
||||
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 +1214,27 @@ 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;
|
||||
}
|
||||
_ => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Tray,
|
||||
"Unhandled tray menu event: {:?}",
|
||||
event.id
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct CoreConfigValidator {
|
||||
}
|
||||
|
||||
impl CoreConfigValidator {
|
||||
pub fn new() -> Self {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
is_processing: AtomicBool::new(false),
|
||||
}
|
||||
@@ -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();
|
||||
@@ -275,41 +275,43 @@ impl CoreConfigValidator {
|
||||
logging!(info, Type::Validate, "验证目录: {}", app_dir_str);
|
||||
|
||||
// 使用子进程运行clash验证配置
|
||||
let output = app_handle
|
||||
.shell()
|
||||
.sidecar(clash_core.as_str())?
|
||||
.args(["-t", "-d", app_dir_str, "-f", config_path])
|
||||
.output()
|
||||
.await?;
|
||||
let command = app_handle.shell().sidecar(clash_core.as_str())?.args([
|
||||
"-t",
|
||||
"-d",
|
||||
app_dir_str,
|
||||
"-f",
|
||||
config_path,
|
||||
]);
|
||||
let output = command.output().await?;
|
||||
|
||||
let stderr = std::string::String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
|
||||
let status = &output.status;
|
||||
let stderr = &output.stderr;
|
||||
let stdout = &output.stdout;
|
||||
|
||||
// 检查进程退出状态和错误输出
|
||||
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
|
||||
let has_error =
|
||||
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||
let has_error = !status.success() || contains_any_keyword(stderr, &error_keywords);
|
||||
|
||||
logging!(info, Type::Validate, "-------- 验证结果 --------");
|
||||
|
||||
if !stderr.is_empty() {
|
||||
logging!(info, Type::Validate, "stderr输出:\n{}", stderr);
|
||||
logging!(info, Type::Validate, "stderr输出:\n{:?}", stderr);
|
||||
}
|
||||
|
||||
if has_error {
|
||||
logging!(info, Type::Validate, "发现错误,开始处理错误信息");
|
||||
let error_msg = if !stdout.is_empty() {
|
||||
stdout.into()
|
||||
let error_msg: String = if !stdout.is_empty() {
|
||||
str::from_utf8(stdout).unwrap_or_default().into()
|
||||
} else if !stderr.is_empty() {
|
||||
stderr.into()
|
||||
} else if let Some(code) = output.status.code() {
|
||||
format!("验证进程异常退出,退出码: {code}")
|
||||
str::from_utf8(stderr).unwrap_or_default().into()
|
||||
} else if let Some(code) = status.code() {
|
||||
format!("验证进程异常退出,退出码: {code}").into()
|
||||
} else {
|
||||
"验证进程被终止".into()
|
||||
};
|
||||
|
||||
logging!(info, Type::Validate, "-------- 验证结束 --------");
|
||||
Ok((false, error_msg.into())) // 返回错误消息给调用者处理
|
||||
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
||||
} else {
|
||||
logging!(info, Type::Validate, "验证成功");
|
||||
logging!(info, Type::Validate, "-------- 验证结束 --------");
|
||||
@@ -342,6 +344,23 @@ fn has_ext<P: AsRef<std::path::Path>>(path: P, ext: &str) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn contains_any_keyword<'a>(buf: &'a [u8], keywords: &'a [&str]) -> bool {
|
||||
for &kw in keywords {
|
||||
let needle = kw.as_bytes();
|
||||
if needle.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut i = 0;
|
||||
while i + needle.len() <= buf.len() {
|
||||
if &buf[i..i + needle.len()] == needle {
|
||||
return true;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
singleton_lazy!(
|
||||
CoreConfigValidator,
|
||||
CORECONFIGVALIDATOR,
|
||||
|
||||
@@ -70,7 +70,7 @@ pub trait AsyncChainItemFrom {
|
||||
}
|
||||
|
||||
impl AsyncChainItemFrom for Option<ChainItem> {
|
||||
async fn from_async(item: &PrfItem) -> Option<ChainItem> {
|
||||
async fn from_async(item: &PrfItem) -> Self {
|
||||
let itype = item.itype.as_ref()?.as_str();
|
||||
let file = item.file.clone()?;
|
||||
let uid = item.uid.clone().unwrap_or_else(|| "".into());
|
||||
@@ -116,22 +116,21 @@ impl AsyncChainItemFrom for Option<ChainItem> {
|
||||
}
|
||||
impl ChainItem {
|
||||
/// 内建支持一些脚本
|
||||
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
|
||||
pub fn builtin() -> Vec<(ChainSupport, Self)> {
|
||||
// meta 的一些处理
|
||||
let meta_guard =
|
||||
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn =
|
||||
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
|
||||
// meta 的一些处理
|
||||
let meta_guard_alpha =
|
||||
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn_alpha =
|
||||
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
|
||||
vec![
|
||||
(ChainSupport::ClashMeta, hy_alpn),
|
||||
@@ -154,8 +153,7 @@ impl ChainSupport {
|
||||
match core {
|
||||
Some(core) => matches!(
|
||||
(self, core.as_str()),
|
||||
(ChainSupport::ClashMeta, "verge-mihomo")
|
||||
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
|
||||
(Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha")
|
||||
),
|
||||
None => true,
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ fn deep_merge(a: &mut Value, b: &Value) {
|
||||
|
||||
pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
|
||||
let mut config = Value::from(config);
|
||||
let merge = use_lowercase(merge.clone());
|
||||
let merge = use_lowercase(merge);
|
||||
|
||||
deep_merge(&mut config, &Value::from(merge));
|
||||
|
||||
|
||||
@@ -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 {
|
||||
<Option<ChainItem>>::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 {
|
||||
<Option<ChainItem>>::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 {
|
||||
<Option<ChainItem>>::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 {
|
||||
<Option<ChainItem>>::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 {
|
||||
<Option<ChainItem>>::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 {
|
||||
@@ -284,7 +330,7 @@ fn process_global_items(
|
||||
|
||||
if let ChainType::Script(script) = global_script.data {
|
||||
let mut logs = vec![];
|
||||
match use_script(script, config.to_owned(), profile_name.to_owned()) {
|
||||
match use_script(script, config.to_owned(), profile_name) {
|
||||
Ok((res_config, res_logs)) => {
|
||||
exists_keys.extend(use_keys(&res_config));
|
||||
config = res_config;
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ pub fn use_script(
|
||||
});"#,
|
||||
));
|
||||
|
||||
let config = use_lowercase(config.clone());
|
||||
let config = use_lowercase(config);
|
||||
let config_str = serde_json::to_string(&config)?;
|
||||
|
||||
// 仅处理 name 参数中的特殊字符
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -47,6 +47,7 @@ fn after_change_clash_mode() {
|
||||
for connection in connections_array {
|
||||
let _ = mihomo.close_connection(&connection.id).await;
|
||||
}
|
||||
drop(mihomo);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -72,10 +73,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 +87,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 +105,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
|
||||
let tun_mode = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.latest_arc()
|
||||
.enable_tun_mode
|
||||
.unwrap_or(false);
|
||||
|
||||
|
||||
@@ -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<SharedBox<IVerge>> {
|
||||
let draft = Config::verge().await;
|
||||
let data = draft.data_arc();
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
@@ -2,23 +2,75 @@ use crate::{
|
||||
cmd,
|
||||
config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
|
||||
core::{CoreManager, handle, tray},
|
||||
logging,
|
||||
logging, logging_error,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use smartstring::alias::String;
|
||||
use tauri::Emitter;
|
||||
|
||||
/// Toggle proxy profile
|
||||
pub async fn toggle_proxy_profile(profile_index: String) {
|
||||
match cmd::patch_profiles_config_by_profile_index(profile_index).await {
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
cmd::patch_profiles_config_by_profile_index(profile_index).await
|
||||
);
|
||||
}
|
||||
|
||||
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(_) => {
|
||||
let result = tray::Tray::global().update_menu().await;
|
||||
if let Err(err) = result {
|
||||
logging!(error, Type::Tray, "更新菜单失败: {}", err);
|
||||
}
|
||||
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, "{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 _ = tray::Tray::global().update_menu().await;
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Tray,
|
||||
"代理切换最终失败: {} -> {}, 错误: {:?}",
|
||||
group_name,
|
||||
proxy_name,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +80,7 @@ async fn should_update_profile(
|
||||
ignore_auto_update: bool,
|
||||
) -> Result<Option<(String, Option<PrfOption>)>> {
|
||||
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 +141,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 {
|
||||
|
||||
@@ -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<bool>) {
|
||||
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 => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -174,7 +174,6 @@ mod app_init {
|
||||
cmd::get_runtime_logs,
|
||||
cmd::get_runtime_proxy_chain_config,
|
||||
cmd::update_proxy_chain_config_in_runtime,
|
||||
cmd::check_update_channel,
|
||||
cmd::invoke_uwp_tool,
|
||||
cmd::copy_clash_env,
|
||||
cmd::sync_tray_proxy_selection,
|
||||
@@ -320,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);
|
||||
|
||||
@@ -352,23 +351,21 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn handle_window_destroyed() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use crate::core::hotkey::SystemHotkey;
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
let _ = hotkey::Hotkey::global().reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
use crate::core::hotkey::SystemHotkey;
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.await
|
||||
.latest_arc()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
let _ = hotkey::Hotkey::global().reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +445,7 @@ pub fn run() {
|
||||
tauri::WindowEvent::Focused(focused) => {
|
||||
event_handlers::handle_window_focus(focused);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
tauri::WindowEvent::Destroyed => {
|
||||
event_handlers::handle_window_destroyed();
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ fn main() {
|
||||
console_subscriber::init();
|
||||
|
||||
// Check for --no-tray command line argument
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.contains(&"--no-tray".into()) {
|
||||
#[cfg(target_os = "linux")]
|
||||
if std::env::args().any(|x| x == "--no-tray") {
|
||||
unsafe {
|
||||
std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1");
|
||||
}
|
||||
|
||||
@@ -28,15 +28,15 @@ enum LightweightState {
|
||||
impl From<u8> for LightweightState {
|
||||
fn from(v: u8) -> Self {
|
||||
match v {
|
||||
1 => LightweightState::In,
|
||||
2 => LightweightState::Exiting,
|
||||
_ => LightweightState::Normal,
|
||||
1 => Self::In,
|
||||
2 => Self::Exiting,
|
||||
_ => Self::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LightweightState {
|
||||
fn as_u8(self) -> u8 {
|
||||
const fn as_u8(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
@@ -79,40 +79,24 @@ 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}");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn auto_lightweight_boot() -> Result<()> {
|
||||
let verge_config = Config::verge().await;
|
||||
let enable_auto = verge_config
|
||||
.data_mut()
|
||||
let is_enable_auto = verge_config
|
||||
.data_arc()
|
||||
.enable_auto_light_weight_mode
|
||||
.unwrap_or(false);
|
||||
let is_silent_start = verge_config
|
||||
.latest_ref()
|
||||
.enable_silent_start
|
||||
.unwrap_or(false);
|
||||
|
||||
let is_silent_start = verge_config.data_arc().enable_silent_start.unwrap_or(false);
|
||||
if is_enable_auto {
|
||||
enable_auto_light_weight_mode().await;
|
||||
}
|
||||
if is_silent_start {
|
||||
logging!(info, Type::Lightweight, "静默启动:直接进入轻量模式");
|
||||
let _ = entry_lightweight_mode().await;
|
||||
return Ok(());
|
||||
entry_lightweight_mode().await;
|
||||
}
|
||||
|
||||
if !enable_auto {
|
||||
logging!(info, Type::Lightweight, "未开启自动轻量模式,跳过初始化");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
"非静默启动:注册自动轻量模式监听器"
|
||||
);
|
||||
enable_auto_light_weight_mode().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -135,7 +119,7 @@ pub fn disable_auto_light_weight_mode() {
|
||||
|
||||
pub async fn entry_lightweight_mode() -> bool {
|
||||
if !try_transition(LightweightState::Normal, LightweightState::In) {
|
||||
logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用");
|
||||
logging!(debug, Type::Lightweight, "无需进入轻量模式,跳过调用");
|
||||
refresh_lightweight_tray_state().await;
|
||||
return false;
|
||||
}
|
||||
@@ -149,7 +133,7 @@ pub async fn entry_lightweight_mode() -> bool {
|
||||
pub async fn exit_lightweight_mode() -> bool {
|
||||
if !try_transition(LightweightState::In, LightweightState::Exiting) {
|
||||
logging!(
|
||||
info,
|
||||
debug,
|
||||
Type::Lightweight,
|
||||
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
|
||||
);
|
||||
@@ -171,10 +155,6 @@ pub async fn add_light_weight_timer() {
|
||||
|
||||
fn setup_window_close_listener() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let old_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if old_id != 0 {
|
||||
window.unlisten(old_id);
|
||||
}
|
||||
let handler_id = window.listen("tauri://close-requested", move |_event| {
|
||||
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||
if let Err(e) = setup_light_weight_timer().await {
|
||||
@@ -196,21 +176,17 @@ fn cancel_window_close_listener() {
|
||||
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
window.unlisten(id);
|
||||
logging!(info, Type::Lightweight, "取消了窗口关闭监听");
|
||||
logging!(debug, Type::Lightweight, "取消了窗口关闭监听");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_webview_focus_listener() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let old_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if old_id != 0 {
|
||||
window.unlisten(old_id);
|
||||
}
|
||||
let handler_id = window.listen("tauri://focus", move |_event| {
|
||||
log_err!(cancel_light_weight_timer());
|
||||
logging!(
|
||||
info,
|
||||
debug,
|
||||
Type::Lightweight,
|
||||
"监听到窗口获得焦点,取消轻量模式计时"
|
||||
);
|
||||
@@ -224,7 +200,7 @@ fn cancel_webview_focus_listener() {
|
||||
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
window.unlisten(id);
|
||||
logging!(info, Type::Lightweight, "取消了窗口焦点监听");
|
||||
logging!(debug, Type::Lightweight, "取消了窗口焦点监听");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,14 +212,14 @@ async fn setup_light_weight_timer() -> Result<()> {
|
||||
|
||||
let once_by_minutes = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.data_arc()
|
||||
.auto_light_weight_minutes
|
||||
.unwrap_or(10);
|
||||
|
||||
{
|
||||
let timer_map = Timer::global().timer_map.read();
|
||||
if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) {
|
||||
logging!(warn, Type::Timer, "轻量模式计时器已存在,跳过创建");
|
||||
logging!(debug, Type::Timer, "轻量模式计时器已存在,跳过创建");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -292,14 +268,17 @@ async fn setup_light_weight_timer() -> Result<()> {
|
||||
}
|
||||
|
||||
fn cancel_light_weight_timer() -> Result<()> {
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
|
||||
if let Some(task) = timer_map.remove(LIGHT_WEIGHT_TASK_UID) {
|
||||
delay_timer
|
||||
let value = Timer::global()
|
||||
.timer_map
|
||||
.write()
|
||||
.remove(LIGHT_WEIGHT_TASK_UID);
|
||||
if let Some(task) = value {
|
||||
Timer::global()
|
||||
.delay_timer
|
||||
.write()
|
||||
.remove_task(task.task_id)
|
||||
.context("failed to remove timer task")?;
|
||||
logging!(info, Type::Timer, "计时器已取消");
|
||||
logging!(debug, Type::Timer, "计时器已取消");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -20,7 +20,7 @@ impl Drop for CommandChildGuard {
|
||||
}
|
||||
|
||||
impl CommandChildGuard {
|
||||
pub fn new(child: CommandChild) -> Self {
|
||||
pub const fn new(child: CommandChild) -> Self {
|
||||
Self(Some(child))
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use once_cell::sync::OnceCell;
|
||||
#[cfg(unix)]
|
||||
use std::iter;
|
||||
use std::{fs, path::PathBuf};
|
||||
use tauri::Manager;
|
||||
|
||||
@@ -226,8 +228,7 @@ pub fn get_encryption_key() -> Result<Vec<u8>> {
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> {
|
||||
["/tmp"]
|
||||
.iter()
|
||||
iter::once("/tmp")
|
||||
.map(PathBuf::from)
|
||||
.find(|path| path.exists())
|
||||
.or_else(|| {
|
||||
|
||||
@@ -1,179 +1,369 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::{
|
||||
MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard,
|
||||
RwLockUpgradableReadGuard, RwLockWriteGuard,
|
||||
};
|
||||
pub type SharedBox<T> = Arc<Box<T>>;
|
||||
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
|
||||
|
||||
/// Draft 管理:committed 与 optional draft 都以 Arc<Box<T>> 存储,
|
||||
// (committed_snapshot, optional_draft_snapshot)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Draft<T: Clone + ToOwned> {
|
||||
inner: Arc<RwLock<(T, Option<T>)>>,
|
||||
pub struct Draft<T: Clone> {
|
||||
inner: Arc<RwLock<DraftInner<T>>>,
|
||||
}
|
||||
|
||||
impl<T: Clone + ToOwned> From<T> for Draft<T> {
|
||||
fn from(data: T) -> Self {
|
||||
impl<T: Clone> Draft<T> {
|
||||
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<T>`, 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<T: Clone + ToOwned> Draft<Box<T>> {
|
||||
/// 正式数据视图
|
||||
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
|
||||
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc)
|
||||
pub fn data_arc(&self) -> SharedBox<T> {
|
||||
let guard = self.inner.read();
|
||||
Arc::clone(&guard.0)
|
||||
}
|
||||
|
||||
/// 可写正式数据
|
||||
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
|
||||
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
|
||||
/// 这也是零拷贝:只 clone Arc,不 clone T
|
||||
pub fn latest_arc(&self) -> SharedBox<T> {
|
||||
let guard = self.inner.read();
|
||||
guard
|
||||
.1
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Arc::clone(&guard.0))
|
||||
}
|
||||
|
||||
/// 创建或获取草稿并返回可写引用
|
||||
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
let guard = self.inner.upgradable_read();
|
||||
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")
|
||||
})
|
||||
});
|
||||
}
|
||||
// 已存在草稿,升级为写锁映射
|
||||
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
|
||||
inner
|
||||
.1
|
||||
.as_mut()
|
||||
.unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()"))
|
||||
})
|
||||
/// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T)
|
||||
/// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T;
|
||||
/// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。
|
||||
pub fn edit_draft<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut T) -> R,
|
||||
{
|
||||
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
|
||||
let mut guard = self.inner.write();
|
||||
let mut draft_arc = if guard.1.is_none() {
|
||||
Arc::clone(&guard.0)
|
||||
} else {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
guard.1.take().unwrap()
|
||||
};
|
||||
drop(guard);
|
||||
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
|
||||
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
|
||||
// 对 Box<T> 解引用得到 &mut T
|
||||
let result = f(&mut **boxed);
|
||||
// 恢复修改后的草稿 Arc
|
||||
self.inner.write().1 = Some(draft_arc);
|
||||
result
|
||||
}
|
||||
|
||||
/// 零拷贝只读视图:返回草稿(若存在)或正式值
|
||||
pub fn latest_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
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<T> 所有权
|
||||
pub async fn with_data_modify<F, Fut, R, E>(&self, f: F) -> Result<R, E>
|
||||
/// 异步地以拥有 Box<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
|
||||
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
|
||||
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
F: FnOnce(Box<T>) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<(Box<T>, R), E>> + Send,
|
||||
E: From<anyhow::Error>,
|
||||
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
|
||||
{
|
||||
// 克隆正式数据
|
||||
let local = {
|
||||
// 读取已提交快照(cheap Arc clone, 然后得到 Box<T> 所有权 via clone)
|
||||
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T(不可避免)
|
||||
let local: Box<T> = {
|
||||
let guard = self.inner.read();
|
||||
guard.0.clone()
|
||||
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone)
|
||||
(*guard.0).clone()
|
||||
};
|
||||
|
||||
// 异步闭包执行,返回修改后的 Box<T> 和业务结果 R
|
||||
let (new_local, res) = f(local).await?;
|
||||
|
||||
// 写回正式数据
|
||||
let mut guard = self.inner.write();
|
||||
guard.0 = new_local;
|
||||
// 将新的 Box<T> 放到已提交位置(包进 Arc)
|
||||
self.inner.write().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<Box<IVerge>>
|
||||
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<bool>,
|
||||
enable_tun_mode: Option<bool>,
|
||||
}
|
||||
|
||||
// 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<F: Future>(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<T>
|
||||
}));
|
||||
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<IVerge>, ()), _>(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<T>
|
||||
}))
|
||||
.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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
|
||||
use crate::{config::with_encryption, enhance::seq::SeqMap, logging, utils::logging::Type};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use nanoid::nanoid;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
@@ -13,7 +13,7 @@ pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
|
||||
let yaml_str = tokio::fs::read_to_string(path).await?;
|
||||
|
||||
Ok(serde_yaml_ng::from_str::<T>(&yaml_str)?)
|
||||
Ok(with_encryption(|| async { serde_yaml_ng::from_str::<T>(&yaml_str) }).await?)
|
||||
}
|
||||
|
||||
/// read mapping from yaml
|
||||
@@ -65,7 +65,7 @@ pub async fn save_yaml<T: Serialize + Sync>(
|
||||
data: &T,
|
||||
prefix: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let data_str = serde_yaml_ng::to_string(data)?;
|
||||
let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?;
|
||||
|
||||
let yaml_str = match prefix {
|
||||
Some(prefix) => format!("{prefix}\n\n{data_str}"),
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn get_supported_languages() -> Vec<String> {
|
||||
pub async fn current_language() -> String {
|
||||
Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.latest_arc()
|
||||
.language
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
// #[cfg(not(feature = "tracing"))]
|
||||
#[cfg(not(feature = "tauri-dev"))]
|
||||
use crate::utils::logging::NoModuleFilter;
|
||||
use crate::{
|
||||
@@ -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),
|
||||
@@ -49,7 +49,9 @@ pub async fn init_logger() -> Result<()> {
|
||||
#[cfg(feature = "tracing")]
|
||||
spec.module("tauri", log::LevelFilter::Debug);
|
||||
#[cfg(feature = "tracing")]
|
||||
spec.module("wry", log::LevelFilter::Debug);
|
||||
spec.module("wry", log::LevelFilter::Off);
|
||||
#[cfg(feature = "tracing")]
|
||||
spec.module("tauri_plugin_mihomo", log::LevelFilter::Off);
|
||||
let spec = spec.build();
|
||||
|
||||
let logger = Logger::with(spec)
|
||||
@@ -67,6 +69,12 @@ pub async fn init_logger() -> Result<()> {
|
||||
);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
let logger = logger.filter(Box::new(NoModuleFilter(&["wry", "tauri"])));
|
||||
#[cfg(feature = "tracing")]
|
||||
let logger = logger.filter(Box::new(NoModuleFilter(&[
|
||||
"wry",
|
||||
"tauri_plugin_mihomo",
|
||||
"kode_bridge",
|
||||
])));
|
||||
|
||||
let _handle = logger.start()?;
|
||||
|
||||
@@ -81,7 +89,7 @@ pub async fn init_logger() -> Result<()> {
|
||||
pub async fn sidecar_writer() -> Result<FileLogWriter> {
|
||||
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 +117,7 @@ pub async fn sidecar_writer() -> Result<FileLogWriter> {
|
||||
pub async fn service_writer_config() -> Result<WriterConfig> {
|
||||
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 +142,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)
|
||||
};
|
||||
|
||||
@@ -498,7 +506,7 @@ pub fn init_scheme() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
pub const fn init_scheme() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -509,7 +517,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())
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ struct IntelGpuDetection {
|
||||
}
|
||||
|
||||
impl IntelGpuDetection {
|
||||
fn should_disable_dmabuf(&self) -> bool {
|
||||
const fn should_disable_dmabuf(&self) -> bool {
|
||||
self.intel_is_primary || self.inconclusive
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ enum NvidiaDmabufDisableReason {
|
||||
}
|
||||
|
||||
impl NvidiaGpuDetection {
|
||||
fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> {
|
||||
const fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> {
|
||||
if !session.is_wayland {
|
||||
return None;
|
||||
}
|
||||
@@ -144,11 +144,11 @@ impl DmabufOverrides {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_env_override(&self) -> bool {
|
||||
const fn has_env_override(&self) -> bool {
|
||||
self.dmabuf_override.is_some()
|
||||
}
|
||||
|
||||
fn should_override_env(&self, decision: &DmabufDecision) -> bool {
|
||||
const fn should_override_env(&self, decision: &DmabufDecision) -> bool {
|
||||
if self.user_preference.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -36,24 +36,24 @@ pub enum Type {
|
||||
impl fmt::Display for Type {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Type::Cmd => write!(f, "[Cmd]"),
|
||||
Type::Core => write!(f, "[Core]"),
|
||||
Type::Config => write!(f, "[Config]"),
|
||||
Type::Setup => write!(f, "[Setup]"),
|
||||
Type::System => write!(f, "[System]"),
|
||||
Type::Service => write!(f, "[Service]"),
|
||||
Type::Hotkey => write!(f, "[Hotkey]"),
|
||||
Type::Window => write!(f, "[Window]"),
|
||||
Type::Tray => write!(f, "[Tray]"),
|
||||
Type::Timer => write!(f, "[Timer]"),
|
||||
Type::Frontend => write!(f, "[Frontend]"),
|
||||
Type::Backup => write!(f, "[Backup]"),
|
||||
Type::File => write!(f, "[File]"),
|
||||
Type::Lightweight => write!(f, "[Lightweight]"),
|
||||
Type::Network => write!(f, "[Network]"),
|
||||
Type::ProxyMode => write!(f, "[ProxMode]"),
|
||||
Type::Validate => write!(f, "[Validate]"),
|
||||
Type::ClashVergeRev => write!(f, "[ClashVergeRev]"),
|
||||
Self::Cmd => write!(f, "[Cmd]"),
|
||||
Self::Core => write!(f, "[Core]"),
|
||||
Self::Config => write!(f, "[Config]"),
|
||||
Self::Setup => write!(f, "[Setup]"),
|
||||
Self::System => write!(f, "[System]"),
|
||||
Self::Service => write!(f, "[Service]"),
|
||||
Self::Hotkey => write!(f, "[Hotkey]"),
|
||||
Self::Window => write!(f, "[Window]"),
|
||||
Self::Tray => write!(f, "[Tray]"),
|
||||
Self::Timer => write!(f, "[Timer]"),
|
||||
Self::Frontend => write!(f, "[Frontend]"),
|
||||
Self::Backup => write!(f, "[Backup]"),
|
||||
Self::File => write!(f, "[File]"),
|
||||
Self::Lightweight => write!(f, "[Lightweight]"),
|
||||
Self::Network => write!(f, "[Network]"),
|
||||
Self::ProxyMode => write!(f, "[ProxMode]"),
|
||||
Self::Validate => write!(f, "[Validate]"),
|
||||
Self::ClashVergeRev => write!(f, "[ClashVergeRev]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ pub struct HttpResponse {
|
||||
}
|
||||
|
||||
impl HttpResponse {
|
||||
pub fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
|
||||
pub const fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
|
||||
Self {
|
||||
status,
|
||||
headers,
|
||||
@@ -34,11 +34,11 @@ impl HttpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> StatusCode {
|
||||
pub const fn status(&self) -> StatusCode {
|
||||
self.status
|
||||
}
|
||||
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
pub const fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
@@ -80,8 +80,7 @@ impl NetworkManager {
|
||||
}
|
||||
|
||||
async fn record_connection_error(&self, error: &str) {
|
||||
let mut last_error = self.last_connection_error.lock().await;
|
||||
*last_error = Some((Instant::now(), error.into()));
|
||||
*self.last_connection_error.lock().await = Some((Instant::now(), error.into()));
|
||||
|
||||
let mut count = self.connection_error_count.lock().await;
|
||||
*count += 1;
|
||||
@@ -89,13 +88,11 @@ impl NetworkManager {
|
||||
|
||||
async fn should_reset_clients(&self) -> bool {
|
||||
let count = *self.connection_error_count.lock().await;
|
||||
let last_error_guard = self.last_connection_error.lock().await;
|
||||
|
||||
if count > 5 {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some((time, _)) = &*last_error_guard
|
||||
if let Some((time, _)) = &*self.last_connection_error.lock().await
|
||||
&& time.elapsed() < Duration::from_secs(30)
|
||||
&& count > 2
|
||||
{
|
||||
@@ -119,18 +116,15 @@ impl NetworkManager {
|
||||
accept_invalid_certs: bool,
|
||||
timeout_secs: Option<u64>,
|
||||
) -> Result<HttpClient> {
|
||||
let proxy_uri_clone = proxy_uri.clone();
|
||||
let headers_clone = default_headers.clone();
|
||||
|
||||
{
|
||||
let mut builder = HttpClient::builder();
|
||||
|
||||
builder = match proxy_uri_clone {
|
||||
builder = match proxy_uri {
|
||||
Some(uri) => builder.proxy(Some(uri)),
|
||||
None => builder.proxy(None),
|
||||
};
|
||||
|
||||
for (name, value) in headers_clone.iter() {
|
||||
for (name, value) in default_headers.iter() {
|
||||
builder = builder.default_header(name, value);
|
||||
}
|
||||
|
||||
@@ -165,10 +159,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}");
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use percent_encoding::percent_decode_str;
|
||||
use smartstring::alias::String;
|
||||
use tauri::Url;
|
||||
|
||||
use crate::{
|
||||
config::{PrfItem, profiles},
|
||||
config::{Config, PrfItem, profiles},
|
||||
core::handle,
|
||||
logging, logging_error,
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
|
||||
@@ -29,55 +31,79 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
|
||||
let name_owned: Option<String> = link_parsed
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "name")
|
||||
.map(|(_, value)| value.into_owned().into());
|
||||
let name = name_owned.as_ref();
|
||||
let (url_param, name) =
|
||||
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
|
||||
let name_owned: Option<String> = link_parsed
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "name")
|
||||
.map(|(_, value)| value.into_owned().into());
|
||||
|
||||
let url_param = if let Some(query) = link_parsed.query() {
|
||||
let prefix = "url=";
|
||||
if let Some(pos) = query.find(prefix) {
|
||||
let raw_url = &query[pos + prefix.len()..];
|
||||
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
|
||||
let url_param = if let Some(query) = link_parsed.query() {
|
||||
let prefix = "url=";
|
||||
if let Some(pos) = query.find(prefix) {
|
||||
let raw_url = &query[pos + prefix.len()..];
|
||||
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
(url_param, name_owned)
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
match url_param {
|
||||
Some(ref url) => {
|
||||
logging!(info, Type::Config, "decoded subscription url: {url}");
|
||||
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
||||
Ok(mut item) => {
|
||||
let uid = match item.uid.clone() {
|
||||
Some(uid) => uid,
|
||||
None => {
|
||||
logging!(error, Type::Config, "Profile item missing UID");
|
||||
handle::Handle::notice_message(
|
||||
"import_sub_url::error",
|
||||
"Profile item missing UID".to_string(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let result = profiles::profiles_append_item_safe(&mut item).await;
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
"failed to import subscription url: {:?}",
|
||||
result
|
||||
);
|
||||
handle::Handle::notice_message("import_sub_url::ok", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None => bail!("failed to get profile url"),
|
||||
let url = if let Some(ref url) = url_param {
|
||||
url
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"missing url parameter in deep link: {}",
|
||||
param_str
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut item = match PrfItem::from_url(url, name.as_ref(), None, None).await {
|
||||
Ok(item) => item,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"failed to parse profile from url: {:?}",
|
||||
e
|
||||
);
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let uid = item.uid.clone().unwrap_or_default();
|
||||
match profiles::profiles_append_item_safe(&mut item).await {
|
||||
Ok(_) => {
|
||||
Config::profiles().await.apply();
|
||||
let _ = Config::profiles().await.data_arc().save_file().await;
|
||||
handle::Handle::notice_message(
|
||||
"import_sub_url::ok",
|
||||
"", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
);
|
||||
handle::Handle::refresh_verge();
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"failed to import subscription url: {:?}",
|
||||
e
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,9 +54,7 @@ fn get_ui_ready_notify() -> &'static Arc<Notify> {
|
||||
// 更新UI准备阶段
|
||||
pub fn update_ui_ready_stage(stage: UiReadyStage) {
|
||||
let state = get_ui_ready_state();
|
||||
let mut stage_lock = state.stage.write();
|
||||
|
||||
*stage_lock = stage;
|
||||
*state.stage.write() = stage;
|
||||
// 如果是最终阶段,标记UI完全就绪
|
||||
if stage == UiReadyStage::Ready {
|
||||
mark_ui_ready();
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn build_new_window() -> Result<WebviewWindow, String> {
|
||||
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(
|
||||
|
||||
@@ -31,6 +31,8 @@ pub async fn check_singleton() -> Result<()> {
|
||||
let client = ClientBuilder::new()
|
||||
.timeout(Duration::from_millis(500))
|
||||
.build()?;
|
||||
// 需要确保 Send
|
||||
#[allow(clippy::needless_collect)]
|
||||
let argvs: Vec<std::string::String> = std::env::args().collect();
|
||||
if argvs.len() > 1 {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -89,15 +91,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}"));
|
||||
|
||||
@@ -81,6 +81,7 @@ fn should_handle_window_operation() -> bool {
|
||||
|
||||
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
|
||||
*last_operation = now;
|
||||
drop(last_operation);
|
||||
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
|
||||
logging!(info, Type::Window, "[防抖] 窗口操作被允许执行");
|
||||
true
|
||||
|
||||
@@ -26,7 +26,6 @@ import { useServiceInstaller } from "@/hooks/useServiceInstaller";
|
||||
import { getSystemInfo } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||
import { useUpdateChannel } from "@/services/updateChannel";
|
||||
import { version as appVersion } from "@root/package.json";
|
||||
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
@@ -60,7 +59,6 @@ export const SystemInfoCard = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isAdminMode, isSidecarMode } = useSystemState();
|
||||
const { installServiceAndRestartCore } = useServiceInstaller();
|
||||
const [updateChannel] = useUpdateChannel();
|
||||
|
||||
// 系统信息状态
|
||||
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
|
||||
@@ -119,7 +117,7 @@ export const SystemInfoCard = () => {
|
||||
|
||||
timeoutId = window.setTimeout(() => {
|
||||
if (verge?.auto_check_update) {
|
||||
checkUpdate(updateChannel).catch(console.error);
|
||||
checkUpdate().catch(console.error);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
@@ -128,11 +126,11 @@ export const SystemInfoCard = () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [verge?.auto_check_update, dispatchSystemState, updateChannel]);
|
||||
}, [verge?.auto_check_update, dispatchSystemState]);
|
||||
|
||||
// 自动检查更新逻辑
|
||||
useSWR(
|
||||
verge?.auto_check_update ? ["checkUpdate", updateChannel] : null,
|
||||
verge?.auto_check_update ? "checkUpdate" : null,
|
||||
async () => {
|
||||
const now = Date.now();
|
||||
localStorage.setItem("last_check_update", now.toString());
|
||||
@@ -140,7 +138,7 @@ export const SystemInfoCard = () => {
|
||||
type: "set-last-check-update",
|
||||
payload: new Date(now).toLocaleString(),
|
||||
});
|
||||
return await checkUpdate(updateChannel);
|
||||
return await checkUpdate();
|
||||
},
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
@@ -174,7 +172,7 @@ export const SystemInfoCard = () => {
|
||||
// 检查更新
|
||||
const onCheckUpdate = useLockFn(async () => {
|
||||
try {
|
||||
const info = await checkUpdate(updateChannel);
|
||||
const info = await checkUpdate();
|
||||
if (!info?.available) {
|
||||
showNotice("success", t("Currently on the Latest Version"));
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,6 @@ import useSWR from "swr";
|
||||
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { checkUpdateSafe } from "@/services/update";
|
||||
import { useUpdateChannel } from "@/services/updateChannel";
|
||||
|
||||
import { DialogRef } from "../base";
|
||||
import { UpdateViewer } from "../setting/mods/update-viewer";
|
||||
@@ -17,14 +16,12 @@ export const UpdateButton = (props: Props) => {
|
||||
const { className } = props;
|
||||
const { verge } = useVerge();
|
||||
const { auto_check_update } = verge || {};
|
||||
const [updateChannel] = useUpdateChannel();
|
||||
|
||||
const viewerRef = useRef<DialogRef>(null);
|
||||
|
||||
const shouldCheck = auto_check_update || auto_check_update === null;
|
||||
const { data: updateInfo } = useSWR(
|
||||
shouldCheck ? ["checkUpdate", updateChannel] : null,
|
||||
() => checkUpdateSafe(updateChannel),
|
||||
auto_check_update || auto_check_update === null ? "checkUpdate" : null,
|
||||
checkUpdateSafe,
|
||||
{
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
|
||||
@@ -71,8 +71,9 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
setOpen(false);
|
||||
showNotice("success", t("Port settings saved"));
|
||||
},
|
||||
onError: () => {
|
||||
showNotice("error", t("Failed to save port settings"));
|
||||
onError: (e) => {
|
||||
showNotice("error", e.message || t("Failed to save port settings"));
|
||||
// showNotice("error", t("Failed to save port settings"));
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -15,7 +15,6 @@ import { portableFlag } from "@/pages/_layout";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { useSetUpdateState, useUpdateState } from "@/services/states";
|
||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||
import { useUpdateChannel } from "@/services/updateChannel";
|
||||
|
||||
export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -27,17 +26,12 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const updateState = useUpdateState();
|
||||
const setUpdateState = useSetUpdateState();
|
||||
const { addListener } = useListen();
|
||||
const [updateChannel] = useUpdateChannel();
|
||||
|
||||
const { data: updateInfo } = useSWR(
|
||||
["checkUpdate", updateChannel],
|
||||
() => checkUpdate(updateChannel),
|
||||
{
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
focusThrottleInterval: 36e5, // 1 hour
|
||||
},
|
||||
);
|
||||
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
focusThrottleInterval: 36e5, // 1 hour
|
||||
});
|
||||
|
||||
const [downloaded, setDownloaded] = useState(0);
|
||||
const [buffer, setBuffer] = useState(0);
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||
import { useUpdateChannel } from "@/services/updateChannel";
|
||||
import { version } from "@root/package.json";
|
||||
|
||||
import { BackupViewer } from "./mods/backup-viewer";
|
||||
@@ -43,11 +42,10 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
|
||||
const updateRef = useRef<DialogRef>(null);
|
||||
const backupRef = useRef<DialogRef>(null);
|
||||
const liteModeRef = useRef<DialogRef>(null);
|
||||
const [updateChannel] = useUpdateChannel();
|
||||
|
||||
const onCheckUpdate = async () => {
|
||||
try {
|
||||
const info = await checkUpdate(updateChannel);
|
||||
const info = await checkUpdate();
|
||||
if (!info?.available) {
|
||||
showNotice("success", t("Currently on the Latest Version"));
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { ContentCopyRounded } from "@mui/icons-material";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
} from "@mui/material";
|
||||
import { Button, Input, MenuItem, Select } from "@mui/material";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -17,11 +11,6 @@ import { navItems } from "@/pages/_routers";
|
||||
import { copyClashEnv } from "@/services/cmds";
|
||||
import { supportedLanguages } from "@/services/i18n";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import {
|
||||
UPDATE_CHANNEL_OPTIONS,
|
||||
type UpdateChannel,
|
||||
useUpdateChannel,
|
||||
} from "@/services/updateChannel";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
import { BackupViewer } from "./mods/backup-viewer";
|
||||
@@ -80,7 +69,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
||||
const layoutRef = useRef<DialogRef>(null);
|
||||
const updateRef = useRef<DialogRef>(null);
|
||||
const backupRef = useRef<DialogRef>(null);
|
||||
const [updateChannel, setUpdateChannel] = useUpdateChannel();
|
||||
|
||||
const onChangeData = (patch: any) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
@@ -91,14 +79,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
||||
showNotice("success", t("Copy Success"), 1000);
|
||||
}, [t]);
|
||||
|
||||
const onUpdateChannelChange = useCallback(
|
||||
(event: SelectChangeEvent<UpdateChannel>) => {
|
||||
const nextChannel = event.target.value as UpdateChannel;
|
||||
setUpdateChannel(nextChannel);
|
||||
},
|
||||
[setUpdateChannel],
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingList title={t("Verge Basic Setting")}>
|
||||
<ThemeViewer ref={themeRef} />
|
||||
@@ -109,21 +89,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
||||
<UpdateViewer ref={updateRef} />
|
||||
<BackupViewer ref={backupRef} />
|
||||
|
||||
<SettingItem label={t("Update Channel")}>
|
||||
<Select
|
||||
size="small"
|
||||
value={updateChannel}
|
||||
onChange={onUpdateChannelChange}
|
||||
sx={{ width: 160, "> div": { py: "7.5px" } }}
|
||||
>
|
||||
{UPDATE_CHANNEL_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{t(option.labelKey)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Language")}>
|
||||
<GuardState
|
||||
value={language ?? "en"}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -43,9 +43,6 @@
|
||||
"Proxy detail": "تفاصيل الوكيل",
|
||||
"Profiles": "الملفات الشخصية",
|
||||
"Update All Profiles": "تحديث جميع الملفات الشخصية",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "عرض تكوين وقت التشغيل",
|
||||
"Reactivate Profiles": "إعادة تنشيط الملفات الشخصية",
|
||||
"Paste": "لصق",
|
||||
|
||||
@@ -45,9 +45,6 @@
|
||||
"Proxy detail": "Knotendetails anzeigen",
|
||||
"Profiles": "Abonnement",
|
||||
"Update All Profiles": "Alle Abonnements aktualisieren",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Laufzeit-Abonnement anzeigen",
|
||||
"Reactivate Profiles": "Abonnement erneut aktivieren",
|
||||
"Paste": "Einfügen",
|
||||
|
||||
@@ -59,9 +59,6 @@
|
||||
"Proxy detail": "Proxy detail",
|
||||
"Profiles": "Profiles",
|
||||
"Update All Profiles": "Update All Profiles",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "View Runtime Config",
|
||||
"Reactivate Profiles": "Reactivate Profiles",
|
||||
"Paste": "Paste",
|
||||
|
||||
@@ -45,9 +45,6 @@
|
||||
"Proxy detail": "Mostrar detalles del nodo",
|
||||
"Profiles": "Suscripciones",
|
||||
"Update All Profiles": "Actualizar todas las suscripciones",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Ver configuración en tiempo de ejecución",
|
||||
"Reactivate Profiles": "Reactivar suscripciones",
|
||||
"Paste": "Pegar",
|
||||
|
||||
@@ -43,9 +43,6 @@
|
||||
"Proxy detail": "جزئیات پراکسی",
|
||||
"Profiles": "پروفایلها",
|
||||
"Update All Profiles": "بهروزرسانی همه پروفایلها",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "مشاهده پیکربندی زمان اجرا",
|
||||
"Reactivate Profiles": "فعالسازی مجدد پروفایلها",
|
||||
"Paste": "چسباندن",
|
||||
|
||||
@@ -43,9 +43,6 @@
|
||||
"Proxy detail": "Detail Proksi",
|
||||
"Profiles": "Profil",
|
||||
"Update All Profiles": "Perbarui Semua Profil",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Lihat Konfigurasi Runtime",
|
||||
"Reactivate Profiles": "Reaktivasi Profil",
|
||||
"Paste": "Tempel",
|
||||
|
||||
@@ -45,9 +45,6 @@
|
||||
"Proxy detail": "ノードの詳細を表示する",
|
||||
"Profiles": "プロファイル",
|
||||
"Update All Profiles": "すべてのプロファイルを更新",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "実行時のプロファイルを表示",
|
||||
"Reactivate Profiles": "プロファイルを再アクティブ化",
|
||||
"Paste": "貼り付け",
|
||||
|
||||
@@ -46,9 +46,6 @@
|
||||
"Proxy detail": "프록시 상세",
|
||||
"Profiles": "프로필",
|
||||
"Update All Profiles": "모든 프로필 업데이트",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "런타임 설정 보기",
|
||||
"Reactivate Profiles": "프로필 재활성화",
|
||||
"Paste": "붙여넣기",
|
||||
|
||||
@@ -51,9 +51,6 @@
|
||||
"Proxy detail": "Отображать больше сведений о прокси",
|
||||
"Profiles": "Профили",
|
||||
"Update All Profiles": "Обновить все профили",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Просмотреть используемый конфиг",
|
||||
"Reactivate Profiles": "Перезапустить профиль",
|
||||
"Paste": "Вставить",
|
||||
|
||||
@@ -46,9 +46,6 @@
|
||||
"Proxy detail": "Vekil detayı",
|
||||
"Profiles": "Profiller",
|
||||
"Update All Profiles": "Tüm Profilleri Güncelle",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Çalışma Zamanı Yapılandırmasını Görüntüle",
|
||||
"Reactivate Profiles": "Profilleri Yeniden Etkinleştir",
|
||||
"Paste": "Yapıştır",
|
||||
|
||||
@@ -43,9 +43,6 @@
|
||||
"Proxy detail": "Прокси турында тулы мәгълүмат",
|
||||
"Profiles": "Профильләр",
|
||||
"Update All Profiles": "Барлык профильләрне яңарту",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Кулланылган конфигурацияне карау",
|
||||
"Reactivate Profiles": "Профильләрне янәдән активлаштыру",
|
||||
"Paste": "Кую",
|
||||
|
||||
@@ -59,9 +59,6 @@
|
||||
"Proxy detail": "展示节点细节",
|
||||
"Profiles": "订阅",
|
||||
"Update All Profiles": "更新所有订阅",
|
||||
"Update Channel": "更新通道",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "查看运行时订阅",
|
||||
"Reactivate Profiles": "重新激活订阅",
|
||||
"Paste": "粘贴",
|
||||
|
||||
@@ -59,9 +59,6 @@
|
||||
"Proxy detail": "展示節點細節",
|
||||
"Profiles": "訂閱",
|
||||
"Update All Profiles": "更新所有訂閱",
|
||||
"Update Channel": "更新頻道",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "查看執行時訂閱",
|
||||
"Reactivate Profiles": "重新啟用訂閱",
|
||||
"Paste": "貼上",
|
||||
@@ -220,7 +217,6 @@
|
||||
"Settings": "設定",
|
||||
"System Setting": "系統設定",
|
||||
"Tun Mode": "虛擬網路介面卡模式",
|
||||
"TUN requires Service Mode or Admin Mode": "虛擬網路介面卡模式需要服務模式或管理員模式",
|
||||
"Install Service": "安裝服務",
|
||||
"Install Service failed": "安裝服務失敗",
|
||||
"Uninstall Service": "解除安裝服務",
|
||||
@@ -308,7 +304,6 @@
|
||||
"Socks Port": "SOCKS 代理連接埠",
|
||||
"Http Port": "HTTP(S) 代理連接埠",
|
||||
"Redir Port": "Redir 透明代理連接埠",
|
||||
"TPROXY Port": "TPROXY 透明代理連接埠",
|
||||
"Port settings saved": "連結埠設定已儲存",
|
||||
"Failed to save port settings": "連結埠設定儲存失敗",
|
||||
"External": "外部控制",
|
||||
@@ -429,6 +424,8 @@
|
||||
"Uninstalling Service...": "服務解除安裝中...",
|
||||
"Service Installed Successfully": "已成功安裝服務",
|
||||
"Service Uninstalled Successfully": "已成功解除安裝服務",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守護間隔時間不得低於 1 秒",
|
||||
"Invalid Bypass Format": "無效的代理繞過格式",
|
||||
"Waiting for service to be ready...": "等待服務就緒...",
|
||||
"Service not ready, retrying attempt {count}/{total}...": "服務未就緒,正在重試 {{count}}/{{total}} 次...",
|
||||
"Failed to check service status, retrying attempt {count}/{total}...": "檢查服務狀態失敗,正在重試 {{count}}/{{total}} 次...",
|
||||
@@ -439,8 +436,6 @@
|
||||
"Fallback core restart also failed: {message}": "被園內核重新啟動也失敗了:{{message}}",
|
||||
"Service is ready and core restarted": "服務已就緒,內核已重啟",
|
||||
"Core restarted. Service is now available.": "內核已重啟,服務已就緒",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守護間隔時間不得低於 1 秒",
|
||||
"Invalid Bypass Format": "無效的代理繞過格式",
|
||||
"Clash Port Modified": "Clash 連結埠已修改",
|
||||
"Port Conflict": "連結埠衝突",
|
||||
"Restart Application to Apply Modifications": "重新啟動 Verge 以套用修改",
|
||||
@@ -716,6 +711,5 @@
|
||||
"Menu reorder mode": "選單排序模式",
|
||||
"Unlock menu order": "解鎖選單排序",
|
||||
"Lock menu order": "鎖定選單排序",
|
||||
"Open App Log": "應用程式日誌",
|
||||
"Open Core Log": "內核日誌"
|
||||
"TPROXY Port": "TPROXY 透明代理連接埠"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ export const handleNoticeMessage = (
|
||||
) => {
|
||||
const handlers: Record<string, () => void> = {
|
||||
"import_sub_url::ok": () => {
|
||||
navigate("/profile", { state: { current: msg } });
|
||||
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
|
||||
// navigate("/profile", { state: { current: msg } });
|
||||
navigate("/profile");
|
||||
showNotice("success", t("Import Subscription Successful"));
|
||||
},
|
||||
"import_sub_url::error": () => {
|
||||
|
||||
@@ -13,15 +13,15 @@ import {
|
||||
sortableKeyboardCoordinates,
|
||||
} from "@dnd-kit/sortable";
|
||||
import {
|
||||
CheckBoxOutlineBlankRounded,
|
||||
CheckBoxRounded,
|
||||
ClearRounded,
|
||||
ContentPasteRounded,
|
||||
DeleteRounded,
|
||||
IndeterminateCheckBoxRounded,
|
||||
LocalFireDepartmentRounded,
|
||||
RefreshRounded,
|
||||
TextSnippetOutlined,
|
||||
CheckBoxOutlineBlankRounded,
|
||||
CheckBoxRounded,
|
||||
IndeterminateCheckBoxRounded,
|
||||
DeleteRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material";
|
||||
|
||||
2
src/services/types.d.ts
vendored
2
src/services/types.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -5,10 +5,8 @@ import {
|
||||
compareVersions,
|
||||
ensureSemver,
|
||||
extractSemver,
|
||||
isPrereleaseVersion,
|
||||
normalizeVersion,
|
||||
resolveRemoteVersion,
|
||||
shouldRejectUpdate,
|
||||
splitVersion,
|
||||
} from "@/services/update";
|
||||
import type { VersionParts } from "@/services/update";
|
||||
@@ -140,53 +138,3 @@ describe("resolveRemoteVersion", () => {
|
||||
expect(resolveRemoteVersion(update)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPrereleaseVersion", () => {
|
||||
it("returns true when version has prerelease identifiers", () => {
|
||||
expect(isPrereleaseVersion("1.2.3-beta.1")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for release versions or missing input", () => {
|
||||
expect(isPrereleaseVersion("1.2.3")).toBe(false);
|
||||
expect(isPrereleaseVersion(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldRejectUpdate", () => {
|
||||
const localStable = "2.4.3";
|
||||
const remoteAutobuild = "2.4.3-autobuild.1122.qwerty.r1a";
|
||||
|
||||
it("rejects when comparison cannot proceed in downgrade-safe way on stable channel", () => {
|
||||
expect(shouldRejectUpdate("stable", -1, "2.4.2", localStable)).toBe(true);
|
||||
expect(shouldRejectUpdate("stable", 0, "2.4.3", localStable)).toBe(true);
|
||||
});
|
||||
|
||||
it("allows prerelease downgrade on autobuild channel", () => {
|
||||
expect(
|
||||
shouldRejectUpdate("autobuild", -1, remoteAutobuild, localStable),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects prerelease downgrade when base version is older", () => {
|
||||
expect(
|
||||
shouldRejectUpdate("autobuild", -1, "2.3.0-autobuild.1", localStable),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects downgrade when both versions are prereleases", () => {
|
||||
expect(
|
||||
shouldRejectUpdate(
|
||||
"autobuild",
|
||||
-1,
|
||||
"2.4.3-autobuild.1122.qwerty.r1a",
|
||||
"2.4.3-autobuild.1127.qwerty.r1a",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects downgrade when remote release is older even on autobuild channel", () => {
|
||||
expect(shouldRejectUpdate("autobuild", -1, "2.4.2", localStable)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Update, type CheckOptions } from "@tauri-apps/plugin-updater";
|
||||
|
||||
import {
|
||||
DEFAULT_UPDATE_CHANNEL,
|
||||
getStoredUpdateChannel,
|
||||
type UpdateChannel,
|
||||
} from "@/services/updateChannel";
|
||||
import { version as appVersion } from "@root/package.json";
|
||||
check,
|
||||
type CheckOptions,
|
||||
type Update,
|
||||
} from "@tauri-apps/plugin-updater";
|
||||
|
||||
type NativeUpdateMetadata = {
|
||||
rid: number;
|
||||
currentVersion: string;
|
||||
version: string;
|
||||
date?: string;
|
||||
body?: string | null;
|
||||
rawJson: Record<string, unknown>;
|
||||
};
|
||||
import { version as appVersion } from "@root/package.json";
|
||||
|
||||
export type VersionParts = {
|
||||
main: number[];
|
||||
@@ -142,92 +131,16 @@ export const resolveRemoteVersion = (update: Update): string | null => {
|
||||
|
||||
const localVersionNormalized = normalizeVersion(appVersion);
|
||||
|
||||
export const isPrereleaseVersion = (version: string | null): boolean => {
|
||||
const parts = splitVersion(version);
|
||||
return Boolean(parts?.pre.length);
|
||||
};
|
||||
|
||||
export const shouldRejectUpdate = (
|
||||
channel: UpdateChannel,
|
||||
comparison: number | null,
|
||||
remoteVersion: string | null,
|
||||
localVersion: string | null,
|
||||
): boolean => {
|
||||
if (comparison === null) return false;
|
||||
if (comparison === 0) return true;
|
||||
if (comparison > 0) return false;
|
||||
|
||||
if (channel !== "stable") {
|
||||
const remoteIsPrerelease = isPrereleaseVersion(remoteVersion);
|
||||
const localIsPrerelease = isPrereleaseVersion(localVersion);
|
||||
if (remoteIsPrerelease && !localIsPrerelease) {
|
||||
const remoteParts = splitVersion(remoteVersion);
|
||||
const localParts = splitVersion(localVersion);
|
||||
if (!remoteParts || !localParts) return true;
|
||||
|
||||
const mainComparison = compareVersionParts(
|
||||
{ main: remoteParts.main, pre: [] },
|
||||
{ main: localParts.main, pre: [] },
|
||||
);
|
||||
|
||||
if (mainComparison < 0) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const normalizeHeaders = (
|
||||
headers?: HeadersInit,
|
||||
): Array<[string, string]> | undefined => {
|
||||
if (!headers) return undefined;
|
||||
const pairs = Array.from(new Headers(headers).entries());
|
||||
return pairs.length > 0 ? pairs : undefined;
|
||||
};
|
||||
|
||||
export const checkUpdateForChannel = async (
|
||||
channel: UpdateChannel = DEFAULT_UPDATE_CHANNEL,
|
||||
export const checkUpdateSafe = async (
|
||||
options?: CheckOptions,
|
||||
): Promise<Update | null> => {
|
||||
const allowDowngrades = channel !== "stable";
|
||||
|
||||
const metadata = await invoke<NativeUpdateMetadata | null>(
|
||||
"check_update_channel",
|
||||
{
|
||||
channel,
|
||||
headers: normalizeHeaders(options?.headers),
|
||||
timeout: options?.timeout,
|
||||
proxy: options?.proxy,
|
||||
target: options?.target,
|
||||
allowDowngrades,
|
||||
},
|
||||
);
|
||||
|
||||
if (!metadata) return null;
|
||||
|
||||
const result = new Update({
|
||||
...metadata,
|
||||
body:
|
||||
typeof metadata.body === "string"
|
||||
? metadata.body
|
||||
: metadata.body === null
|
||||
? undefined
|
||||
: metadata.body,
|
||||
});
|
||||
|
||||
const result = await check({ ...(options ?? {}), allowDowngrades: false });
|
||||
if (!result) return null;
|
||||
|
||||
const remoteVersion = resolveRemoteVersion(result);
|
||||
const comparison = compareVersions(remoteVersion, localVersionNormalized);
|
||||
if (
|
||||
shouldRejectUpdate(
|
||||
channel,
|
||||
comparison,
|
||||
remoteVersion,
|
||||
localVersionNormalized,
|
||||
)
|
||||
) {
|
||||
|
||||
if (comparison !== null && comparison <= 0) {
|
||||
try {
|
||||
await result.close();
|
||||
} catch (err) {
|
||||
@@ -239,13 +152,4 @@ export const checkUpdateForChannel = async (
|
||||
return result;
|
||||
};
|
||||
|
||||
export const checkUpdateSafe = async (
|
||||
channel?: UpdateChannel,
|
||||
options?: CheckOptions,
|
||||
): Promise<Update | null> => {
|
||||
const resolvedChannel = channel ?? getStoredUpdateChannel();
|
||||
return checkUpdateForChannel(resolvedChannel, options);
|
||||
};
|
||||
|
||||
export type { CheckOptions };
|
||||
export type { UpdateChannel } from "@/services/updateChannel";
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
|
||||
export type UpdateChannel = "stable" | "autobuild";
|
||||
|
||||
export const UPDATE_CHANNEL_STORAGE_KEY = "update-channel";
|
||||
|
||||
export const DEFAULT_UPDATE_CHANNEL: UpdateChannel = "stable";
|
||||
|
||||
export const UPDATE_CHANNEL_OPTIONS: Array<{
|
||||
value: UpdateChannel;
|
||||
labelKey: string;
|
||||
}> = [
|
||||
{ value: "stable", labelKey: "Update Channel Stable" },
|
||||
{ value: "autobuild", labelKey: "Update Channel Autobuild" },
|
||||
];
|
||||
|
||||
const isValidChannel = (value: unknown): value is UpdateChannel => {
|
||||
return value === "stable" || value === "autobuild";
|
||||
};
|
||||
|
||||
export const useUpdateChannel = () =>
|
||||
useLocalStorage<UpdateChannel>(
|
||||
UPDATE_CHANNEL_STORAGE_KEY,
|
||||
DEFAULT_UPDATE_CHANNEL,
|
||||
{
|
||||
serializer: JSON.stringify,
|
||||
deserializer: (value) => {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return isValidChannel(parsed) ? parsed : DEFAULT_UPDATE_CHANNEL;
|
||||
} catch (ignoreErr) {
|
||||
return DEFAULT_UPDATE_CHANNEL;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const getStoredUpdateChannel = (): UpdateChannel => {
|
||||
if (
|
||||
typeof window === "undefined" ||
|
||||
typeof window.localStorage === "undefined"
|
||||
) {
|
||||
return DEFAULT_UPDATE_CHANNEL;
|
||||
}
|
||||
|
||||
const raw = window.localStorage.getItem(UPDATE_CHANNEL_STORAGE_KEY);
|
||||
if (raw === null) {
|
||||
return DEFAULT_UPDATE_CHANNEL;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return isValidChannel(parsed) ? parsed : DEFAULT_UPDATE_CHANNEL;
|
||||
} catch (ignoreErr) {
|
||||
return DEFAULT_UPDATE_CHANNEL;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user