mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
chore: i18n (#5276)
* chore: notice i18n * feat: add script to clean up unused i18n keys * chore: cleanup i18n keys * refactor(i18n/proxies): migrate proxies UI to structured locale keys * chore: i18n for rule module * chore: i18n for profile module * chore: i18n for connections module * chore: i18n for settings module * chore: i18n for verge settings * chore: i18n for theme settings * chore: i18n for theme * chore(i18n): components.home.* * chore(i18n): remove unused i18n keys * chore(i18n): components.profile.* * chore(i18n): components.connection * chore(i18n): pages.logs.* * chore(i18n): pages.*.provider * chore(i18n): components.settings.externalCors.* * chore(i18n): components.settings.clash.* * chore(i18n): components.settings.liteMode.* * chore(i18n): components.settings.backup.* * chore(i18n): components.settings.clash.port.* * chore(i18n): components.settings.misc.* * chore(i18n): components.settings.update.* * chore(i18n): components.settings.sysproxy.* * chore(i18n): components.settings.sysproxy.* * chore(i18n): pages.profiles.notices/components.providers.notices * refactor(notice): unify showNotice usage * refactor(notice): add typed showNotice shortcuts, centralize defaults, and simplify subscriptions * refactor: unify showNotice usage * refactor(notice): unify showNotice API * refactor(notice): unify showNotice usage * chore(i18n): components.test.* * chore(i18n): components.settings.dns.* * chore(i18n): components.home.clashInfo.* * chore(i18n): components.home.systemInfo.* * chore(i18n): components.home.ipInfo/traffic.* * chore(i18n): navigation.* * refactor(i18n): remove pages.* namespace and migrate route texts under module-level page keys * chore(i18n): common.* * chore(i18n): common.* * fix: change error handling in patch_profiles_config to return false when a switch is in progress * fix: improve error handling in patch_profiles_config to prevent requests during profile switching * fix: change error handling in patch_profiles_config to return false when a switch is in progress fix: ensure CURRENT_SWITCHING_PROFILE is reset after config updates in perform_config_update and patch_profiles_config * chore(i18n): restructure root-level locale keys into namespaces * chore(i18n): add missing i18n keys * docs: i18n guide * chore: adjust i18n * refactor(i18n): align UI actions and status labels with common keys * refactor(i18n): unify two-name locale namespaces * refactor(i18n/components): unify locale keys and update component references * chore(i18n): add shared and entities namespaces to all locale files * refactor(i18n): consolidate shared and entity namespaces across features * chore(deps): update npm dependencies to ^7.3.5 (#5310) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * refactor(i18n): migrate shared editor modes and consolidate entities namespaces * tmp * refactor(i18n): flatten locales and move theme/validation strings * docs: CONTRIBUTING_i18n.md * refactor(i18n): restructure feedback and profile namespaces for better organization * refactor(i18n): unify settings locale structure and update references * refactor(i18n): reorganize locale keys for home, proxies, rules, connections, logs, unlock, and tests * refactor(i18n/feedback/layout): unify shared toasts & normalize layout namespace * refactor(i18n): centralize common UI strings in shared * refactor(i18n): flatten headers and unify locale schema * refactor(i18n): consolidate duplicate per-feature translations into shared namespace * refactor(i18n): split locales into per-namespace files * style: lint * refactor(i18n): unify unlock UI translations under tests namespace * feat(i18n): add type-checked translation keys * style: eslint import order * feat(i18n): replace ad-hoc loader with rust-i18n backend bundles * chore(prebuild): remove locale-copy step * fix(i18n, notice): propagate runtime params and update cleanup script path * fix(i18n,notice): make locale formatting idempotent and guard early notice translations * fix(i18n): resolve locale aliases and match OS codes correctly * fix(unlock): use i18next-compatible double-brace interpolation in failure notice * fix(i18n): route unlock error notices through translation keys * fix(i18n): i18n types * feat(i18n): localize upgrade notice for Clash core viewer * fix(notice): ensure runtime overrides apply to prefix translations * chore(i18n): replace literal notices with translation keys * chore(i18n): types * chore(i18n): regen typings before formatting to keep keys in sync * chore(i18n): simply labels * chore(i18n): adjust translation * chore: remove eslint-plugin-i18next * chore(i18n): add/refine Korean translations across frontend scopes and Rust backend (#5341) * chore(i18n): translate settings.json (missed in previous pass) (#5343) * chore(i18n): add/refine Korean translations across frontend scopes and Rust backend * chore(i18n): add/refine Korean translations across frontend scopes and Rust backend * fix(i18n-tauri): quote placeholder-leading value in ko.yml to prevent rust_i18n parse panic * chore(i18n): translate settings.json (forgot to include previously) --------- Co-authored-by: rozan <34974262+thelojan@users.noreply.github.com>
This commit is contained in:
co-authored by
renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
rozan
parent
c1a2e99f63
commit
c8aa72186e
@@ -9,6 +9,19 @@ if ! command -v pnpm >/dev/null 2>&1; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
LOCALE_DIFF="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src/locales/' || true)"
|
||||||
|
if [ -n "$LOCALE_DIFF" ]; then
|
||||||
|
echo "[pre-commit] Locale changes detected. Regenerating i18n types..."
|
||||||
|
pnpm i18n:types
|
||||||
|
if [ -d src/types/generated ]; then
|
||||||
|
echo "[pre-commit] Staging regenerated i18n type artifacts..."
|
||||||
|
git add src/types/generated
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[pre-commit] Running pnpm format before lint..."
|
||||||
|
pnpm format
|
||||||
|
|
||||||
echo "[pre-commit] Running lint-staged for JS/TS files..."
|
echo "[pre-commit] Running lint-staged for JS/TS files..."
|
||||||
pnpm exec lint-staged
|
pnpm exec lint-staged
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
files:
|
|
||||||
- source: /src/locales/en.json
|
|
||||||
translation: /src/locales
|
|
||||||
multilingual: 1
|
|
||||||
+57
-59
@@ -1,81 +1,79 @@
|
|||||||
# CONTRIBUTING — i18n
|
# CONTRIBUTING — i18n
|
||||||
|
|
||||||
Thank you for considering contributing to our localization work — your help is appreciated.
|
Thanks for helping localize Clash Verge Rev. This guide reflects the current architecture, where the React frontend and the Tauri backend keep their translation bundles separate. Follow the steps below to keep both sides in sync without stepping on each other.
|
||||||
|
|
||||||
Quick overview
|
## Quick workflow
|
||||||
|
|
||||||
- cvr-i18 is a CLI that helps manage simple top-level JSON locale files:
|
- Update the language folder under `src/locales/<lang>/`; use `src/locales/en/` as the canonical reference for keys and intent.
|
||||||
- Detect duplicated top-level keys
|
- Run `pnpm format:i18n` to align structure and `pnpm i18n:types` to refresh generated typings.
|
||||||
- Find keys missing versus a base file (default: en.json)
|
- If you touch backend copy, edit the matching YAML file in `src-tauri/locales/<lang>.yml`.
|
||||||
- Export missing entries for translators
|
- Preview UI changes with `pnpm dev` (desktop shell) or `pnpm web:dev` (web only).
|
||||||
- Reorder keys to match the base file for predictable diffs
|
- Keep PRs focused and add screenshots whenever layout could be affected by text length.
|
||||||
- Operate on a directory or a single file
|
|
||||||
|
|
||||||
Get the CLI (No binary provided yet)
|
## Frontend locale structure
|
||||||
|
|
||||||
```bash
|
Each locale folder mirrors the namespaces under `src/locales/en/`:
|
||||||
git clone https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
|
||||||
cd clash-verge-rev-i18n-cli
|
```
|
||||||
cargo install --path .
|
src/locales/
|
||||||
# or
|
en/
|
||||||
cargo install --git https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
connections.json
|
||||||
|
home.json
|
||||||
|
shared.json
|
||||||
|
...
|
||||||
|
index.ts
|
||||||
|
zh/
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
Common commands
|
- JSON files map to namespaces (for example `home.json` → `home.*`). Keep keys scoped to the file they belong to.
|
||||||
|
- `shared.json` stores reusable vocabulary (buttons, validations, etc.); feature-specific wording should live in the relevant namespace.
|
||||||
|
- `index.ts` re-exports a `resources` object that aggregates the namespace JSON files. When adding or removing namespaces, mirror the pattern from `src/locales/en/index.ts`.
|
||||||
|
- Frontend bundles are lazy-loaded by `src/services/i18n.ts`. Only languages listed in `supportedLanguages` are fetched at runtime, so append new codes there when you add a locale.
|
||||||
|
|
||||||
- Show help: `cvr-i18`
|
Because backend translations now live in their own directory, you no longer need to run `pnpm prebuild` just to sync locales—the frontend folder is the sole source of truth for web bundles.
|
||||||
- Directory (auto-detects `./locales` or `./src/locales`): `cvr-i18 -d /path/to/locales`
|
|
||||||
- Check duplicates: `cvr-i18 -k`
|
|
||||||
- Check missing keys: `cvr-i18 -m`
|
|
||||||
- Export missing keys: `cvr-i18 -m -e ./exports`
|
|
||||||
- Sort keys to base file: `cvr-i18 -s`
|
|
||||||
- Use a base file: `cvr-i18 -b base.json`
|
|
||||||
- Single file: `cvr-i18 -f locales/zh.json`
|
|
||||||
|
|
||||||
Options (short)
|
## Tooling for frontend contributors
|
||||||
|
|
||||||
- `-d, --directory <DIR>`
|
- `pnpm format:i18n` → `node scripts/cleanup-unused-i18n.mjs --align --apply`. It aligns key ordering, removes unused entries, and keeps all locales in lock-step with English.
|
||||||
- `-f, --file <FILE>`
|
- `pnpm node scripts/cleanup-unused-i18n.mjs` (without flags) performs a dry-run audit. Use it to inspect missing or extra keys before committing.
|
||||||
- `-k, --duplicated-key`
|
- `pnpm i18n:types` regenerates `src/types/generated/i18n-keys.ts` and `src/types/generated/i18n-resources.ts`, ensuring TypeScript catches invalid key usage.
|
||||||
- `-m, --missing-key`
|
- For dynamic keys that the analyzer cannot statically detect, add explicit references in code or update the script whitelist to avoid false positives.
|
||||||
- `-e, --export <DIR>`
|
|
||||||
- `-s, --sort`
|
|
||||||
- `-b, --base <FILE>`
|
|
||||||
|
|
||||||
Exit codes
|
## Backend (Tauri) locale bundles
|
||||||
|
|
||||||
- `0` — success (no issues)
|
Native UI strings (tray menu, notifications, dialogs) use `rust-i18n` with YAML bundles stored in `src-tauri/locales/<lang>.yml`. These files are completely independent from the frontend JSON modules.
|
||||||
- `1` — issues found (duplicates/missing)
|
|
||||||
- `2` — error (IO/parse/runtime)
|
|
||||||
|
|
||||||
How to contribute (recommended steps)
|
- Keep `en.yml` semantically aligned with the Simplified Chinese baseline (`zh.yml`). Other locales may temporarily copy English if no translation is available yet.
|
||||||
|
- When a backend feature introduces new strings, update every YAML file to keep the key set consistent. Missing keys fall back to the default language (`zh`), so catching gaps early avoids mixed-language output.
|
||||||
|
- Rust code resolves the active language through `src-tauri/src/utils/i18n.rs`. No additional build step is required after editing YAML files; `tauri dev` and `tauri build` pick them up automatically.
|
||||||
|
|
||||||
- Start small: fix typos, improve phrasing, or refine tone and consistency.
|
## Adding a new language
|
||||||
- Run the CLI against your locale files to detect duplicates or missing keys.
|
|
||||||
- Export starter JSONs for translators with `-m -e <DIR>`.
|
|
||||||
- Prefer incremental PRs or draft PRs; leave a comment on the issue if you want guidance.
|
|
||||||
- Open an issue to report missing strings, UI context, or localization bugs.
|
|
||||||
- Add or improve docs and tests to make future contributions easier.
|
|
||||||
|
|
||||||
PR checklist
|
1. Duplicate `src/locales/en/` into `src/locales/<new-lang>/` and translate the JSON files while preserving key structure.
|
||||||
|
2. Update the locale’s `index.ts` to import every namespace. Matching the English file is the easiest way to avoid missing exports.
|
||||||
|
3. Append the language code to `supportedLanguages` in `src/services/i18n.ts`.
|
||||||
|
4. If the backend should expose the language, create `src-tauri/locales/<new-lang>.yml` and translate the keys used in existing YAML files.
|
||||||
|
5. Adjust `crowdin.yml` if the locale requires a special mapping for Crowdin.
|
||||||
|
6. Run `pnpm format:i18n`, `pnpm i18n:types`, and (optionally) `pnpm node scripts/cleanup-unused-i18n.mjs` in dry-run mode to confirm structure.
|
||||||
|
|
||||||
- Keep JSON files UTF-8 encoded.
|
## Authoring guidelines
|
||||||
- Follow the repo’s locale file structure and naming conventions.
|
|
||||||
- Reorder keys to match the base file (`-s`) for minimal diffs.
|
|
||||||
- Test translations in a local dev build before opening a PR.
|
|
||||||
- Reference related issues and explain any context for translations or changes.
|
|
||||||
|
|
||||||
Notes
|
- **Reuse shared vocabulary** before introducing new phrases—check `shared.json` for common actions, statuses, and labels.
|
||||||
|
- **Prefer semantic keys** (`systemProxy`, `updateInterval`, `autoRefresh`) over positional ones (`item1`, `dialogTitle2`).
|
||||||
|
- **Document placeholders** using `{{placeholder}}` and ensure components supply the required values.
|
||||||
|
- **Group keys by UI responsibility** inside each namespace (`page`, `sections`, `forms`, `actions`, `tooltips`, `notifications`, `errors`, `tables`, `statuses`, etc.).
|
||||||
|
- **Keep strings concise** to avoid layout issues. If a translation needs more context, leave a PR note so reviewers can verify the UI.
|
||||||
|
|
||||||
- The tool expects simple top-level JSON key/value maps.
|
## Testing & QA
|
||||||
- Exported JSONs are starter files for translators (fill in values, keep keys).
|
|
||||||
- Sorting keeps diffs consistent and reviewable.
|
|
||||||
|
|
||||||
Repository
|
- Launch the desktop shell with `pnpm dev` (or `pnpm web:dev`) and navigate through the affected views to confirm translations load and layouts behave.
|
||||||
https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
- Run `pnpm test` if you touched code that consumes translations or adjusts formatting logic.
|
||||||
|
- For backend changes, trigger the relevant tray actions or notifications to verify the updated copy.
|
||||||
|
- Note any remaining untranslated sections or layout concerns in your PR description so maintainers can follow up.
|
||||||
|
|
||||||
## Feedback & Contributions
|
## Feedback & support
|
||||||
|
|
||||||
- For tool usage issues or feedback: please open an Issue in the [repository](https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli) so it can be tracked and addressed.
|
- File an issue for missing context, tooling bugs, or localization gaps so we can track them.
|
||||||
- For localization contributions (translations, fixes, context notes, etc.): submit a PR or Issue in this repository and include examples, context, and testing instructions when possible.
|
- PRs that touch UI should include screenshots or GIFs whenever text length may affect layout.
|
||||||
- If you need help or a review, leave a comment on your submission requesting assistance.
|
- Mention the commands you ran (formatting, type generation, tests) in the PR checklist. If you need extra context or review help, request it via a PR comment.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default defineConfig([
|
|||||||
|
|
||||||
plugins: {
|
plugins: {
|
||||||
js: eslintJS,
|
js: eslintJS,
|
||||||
|
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
|
||||||
"react-hooks": pluginReactHooks,
|
"react-hooks": pluginReactHooks,
|
||||||
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
|
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
|
||||||
"import-x": pluginImportX,
|
"import-x": pluginImportX,
|
||||||
@@ -132,4 +133,14 @@ export default defineConfig([
|
|||||||
"prettier/prettier": "warn",
|
"prettier/prettier": "warn",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ["scripts/**/*.{js,mjs,cjs}", "scripts-workflow/**/*.{js,mjs,cjs}"],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
|
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
|
"format:i18n": "node scripts/cleanup-unused-i18n.mjs --align --apply",
|
||||||
|
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
|
|
||||||
const SRC_DIRS = [
|
|
||||||
path.resolve(__dirname, "../src"),
|
|
||||||
path.resolve(__dirname, "../src-tauri"),
|
|
||||||
];
|
|
||||||
const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
|
|
||||||
|
|
||||||
// 递归获取所有文件
|
|
||||||
function getAllFiles(dir, exts) {
|
|
||||||
let files = [];
|
|
||||||
fs.readdirSync(dir).forEach((file) => {
|
|
||||||
const full = path.join(dir, file);
|
|
||||||
if (fs.statSync(full).isDirectory()) {
|
|
||||||
files = files.concat(getAllFiles(full, exts));
|
|
||||||
} else if (exts.includes(path.extname(full))) {
|
|
||||||
files.push(full);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取所有源码内容为一个大字符串
|
|
||||||
function getAllSourceContent() {
|
|
||||||
const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
|
|
||||||
return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 白名单 key,不检查这些 key 是否被使用
|
|
||||||
const WHITELIST_KEYS = [
|
|
||||||
"theme.light",
|
|
||||||
"theme.dark",
|
|
||||||
"theme.system",
|
|
||||||
"Already Using Latest Core Version",
|
|
||||||
];
|
|
||||||
|
|
||||||
// 主流程
|
|
||||||
function processI18nFile(i18nPath, lang, allSource) {
|
|
||||||
const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
|
|
||||||
const keys = Object.keys(i18n);
|
|
||||||
|
|
||||||
const used = {};
|
|
||||||
const unused = [];
|
|
||||||
|
|
||||||
let checked = 0;
|
|
||||||
const total = keys.length;
|
|
||||||
keys.forEach((key) => {
|
|
||||||
if (WHITELIST_KEYS.includes(key)) {
|
|
||||||
used[key] = i18n[key];
|
|
||||||
} else {
|
|
||||||
// 只查找一次
|
|
||||||
const regex = new RegExp(`["'\`]${key}["'\`]`);
|
|
||||||
if (regex.test(allSource)) {
|
|
||||||
used[key] = i18n[key];
|
|
||||||
} else {
|
|
||||||
unused.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checked++;
|
|
||||||
if (checked % 20 === 0 || checked === total) {
|
|
||||||
const percent = ((checked / total) * 100).toFixed(1);
|
|
||||||
process.stdout.write(
|
|
||||||
`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
|
|
||||||
);
|
|
||||||
if (checked === total) process.stdout.write("\n");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 输出未使用的 key
|
|
||||||
console.log(`\n[${lang}] Unused keys:`, unused);
|
|
||||||
|
|
||||||
// 备份原文件
|
|
||||||
const oldPath = i18nPath + ".old";
|
|
||||||
fs.renameSync(i18nPath, oldPath);
|
|
||||||
|
|
||||||
// 写入精简后的 i18n 文件(保留原文件名)
|
|
||||||
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
|
|
||||||
console.log(
|
|
||||||
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
|
|
||||||
);
|
|
||||||
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等
|
|
||||||
const files = fs
|
|
||||||
.readdirSync(LOCALES_DIR)
|
|
||||||
.filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
|
|
||||||
const allSource = getAllSourceContent();
|
|
||||||
files.forEach((file) => {
|
|
||||||
const lang = path.basename(file, ".json");
|
|
||||||
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { promisify } from "util";
|
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为Alpha版本重命名版本号
|
* 为Alpha版本重命名版本号
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const ROOT_DIR = path.resolve(__dirname, "..");
|
||||||
|
const LOCALE_DIR = path.resolve(ROOT_DIR, "src/locales/en");
|
||||||
|
const KEY_OUTPUT = path.resolve(ROOT_DIR, "src/types/generated/i18n-keys.ts");
|
||||||
|
const RESOURCE_OUTPUT = path.resolve(
|
||||||
|
ROOT_DIR,
|
||||||
|
"src/types/generated/i18n-resources.ts",
|
||||||
|
);
|
||||||
|
|
||||||
|
const isPlainObject = (value) =>
|
||||||
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
|
||||||
|
const flattenKeys = (data, prefix = "") => {
|
||||||
|
const keys = [];
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
||||||
|
if (isPlainObject(value)) {
|
||||||
|
keys.push(...flattenKeys(value, nextPrefix));
|
||||||
|
} else {
|
||||||
|
keys.push(nextPrefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildType = (data, indent = 0) => {
|
||||||
|
if (!isPlainObject(data)) {
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Object.entries(data).sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
const pad = " ".repeat(indent);
|
||||||
|
const inner = entries
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const typeStr = buildType(value, indent + 2);
|
||||||
|
return `${" ".repeat(indent + 2)}${JSON.stringify(key)}: ${typeStr};`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return entries.length
|
||||||
|
? `{
|
||||||
|
${inner}
|
||||||
|
${pad}}`
|
||||||
|
: "{}";
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadNamespaceJson = async () => {
|
||||||
|
const dirents = await fs.readdir(LOCALE_DIR, { withFileTypes: true });
|
||||||
|
const namespaces = [];
|
||||||
|
for (const dirent of dirents) {
|
||||||
|
if (!dirent.isFile() || !dirent.name.endsWith(".json")) continue;
|
||||||
|
const name = dirent.name.replace(/\.json$/, "");
|
||||||
|
const filePath = path.join(LOCALE_DIR, dirent.name);
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
const json = JSON.parse(raw);
|
||||||
|
namespaces.push({ name, json });
|
||||||
|
}
|
||||||
|
namespaces.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
return namespaces;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildKeysFile = (keys) => {
|
||||||
|
const arrayLiteral = keys.map((key) => ` "${key}"`).join(",\n");
|
||||||
|
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport const translationKeys = [\n${arrayLiteral}\n] as const;\n\nexport type TranslationKey = typeof translationKeys[number];\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildResourcesFile = (namespaces) => {
|
||||||
|
const namespaceEntries = namespaces
|
||||||
|
.map(({ name, json }) => {
|
||||||
|
const typeStr = buildType(json, 4);
|
||||||
|
return ` ${JSON.stringify(name)}: ${typeStr};`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport interface TranslationResources {\n translation: {\n${namespaceEntries}\n };\n}\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
const namespaces = await loadNamespaceJson();
|
||||||
|
const keys = namespaces.flatMap(({ name, json }) => flattenKeys(json, name));
|
||||||
|
const keysContent = buildKeysFile(keys);
|
||||||
|
const resourcesContent = buildResourcesFile(namespaces);
|
||||||
|
await fs.mkdir(path.dirname(KEY_OUTPUT), { recursive: true });
|
||||||
|
await fs.writeFile(KEY_OUTPUT, keysContent, "utf8");
|
||||||
|
await fs.writeFile(RESOURCE_OUTPUT, resourcesContent, "utf8");
|
||||||
|
console.log(`Generated ${keys.length} translation keys.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Failed to generate i18n metadata:", error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import fsp from "fs/promises";
|
import fsp from "fs/promises";
|
||||||
import path from "path";
|
|
||||||
import AdmZip from "adm-zip";
|
|
||||||
import { createRequire } from "module";
|
import { createRequire } from "module";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
import { getOctokit, context } from "@actions/github";
|
import { getOctokit, context } from "@actions/github";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
|
||||||
const target = process.argv.slice(2)[0];
|
const target = process.argv.slice(2)[0];
|
||||||
const alpha = process.argv.slice(2)[1];
|
const alpha = process.argv.slice(2)[1];
|
||||||
@@ -79,11 +80,11 @@ async function resolvePortable() {
|
|||||||
tag,
|
tag,
|
||||||
});
|
});
|
||||||
|
|
||||||
let assets = release.assets.filter((x) => {
|
const assets = release.assets.filter((x) => {
|
||||||
return x.name === zipFile;
|
return x.name === zipFile;
|
||||||
});
|
});
|
||||||
if (assets.length > 0) {
|
if (assets.length > 0) {
|
||||||
let id = assets[0].id;
|
const id = assets[0].id;
|
||||||
await github.rest.repos.deleteReleaseAsset({
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
asset_id: id,
|
asset_id: id,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
|
||||||
import AdmZip from "adm-zip";
|
|
||||||
import { createRequire } from "module";
|
|
||||||
import fsp from "fs/promises";
|
import fsp from "fs/promises";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
|
||||||
const target = process.argv.slice(2)[0];
|
const target = process.argv.slice(2)[0];
|
||||||
const ARCH_MAP = {
|
const ARCH_MAP = {
|
||||||
|
|||||||
+15
-37
@@ -1,14 +1,16 @@
|
|||||||
import AdmZip from "adm-zip";
|
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import fsp from "fs/promises";
|
import fsp from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import zlib from "zlib";
|
||||||
|
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
import { glob } from "glob";
|
import { glob } from "glob";
|
||||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import path from "path";
|
|
||||||
import { extract } from "tar";
|
import { extract } from "tar";
|
||||||
import zlib from "zlib";
|
|
||||||
import { log_debug, log_error, log_info, log_success } from "./utils.mjs";
|
import { log_debug, log_error, log_info, log_success } from "./utils.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +57,7 @@ const ARCH_MAP = {
|
|||||||
|
|
||||||
const arg1 = process.argv.slice(2)[0];
|
const arg1 = process.argv.slice(2)[0];
|
||||||
const arg2 = process.argv.slice(2)[1];
|
const arg2 = process.argv.slice(2)[1];
|
||||||
let target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
|
const target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
|
||||||
const { platform, arch } = target
|
const { platform, arch } = target
|
||||||
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
|
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
|
||||||
: process;
|
: process;
|
||||||
@@ -113,7 +115,7 @@ async function calculateFileHash(filePath) {
|
|||||||
const hashSum = createHash("sha256");
|
const hashSum = createHash("sha256");
|
||||||
hashSum.update(fileBuffer);
|
hashSum.update(fileBuffer);
|
||||||
return hashSum.digest("hex");
|
return hashSum.digest("hex");
|
||||||
} catch (err) {
|
} catch (ignoreErr) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,9 +549,9 @@ const resolveServicePermission = async () => {
|
|||||||
const hashCache = await loadHashCache();
|
const hashCache = await loadHashCache();
|
||||||
let hasChanges = false;
|
let hasChanges = false;
|
||||||
|
|
||||||
for (let f of serviceExecutables) {
|
for (const f of serviceExecutables) {
|
||||||
const files = glob.sync(path.join(resDir, f));
|
const files = glob.sync(path.join(resDir, f));
|
||||||
for (let filePath of files) {
|
for (const filePath of files) {
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
const currentHash = await calculateFileHash(filePath);
|
const currentHash = await calculateFileHash(filePath);
|
||||||
const cacheKey = `${filePath}_chmod`;
|
const cacheKey = `${filePath}_chmod`;
|
||||||
@@ -573,52 +575,29 @@ const resolveServicePermission = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// resolve locales (从 src/locales 复制到 resources/locales,并使用 hash 检查)
|
|
||||||
async function resolveLocales() {
|
|
||||||
const srcLocalesDir = path.join(cwd, "src/locales");
|
|
||||||
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fsp.mkdir(targetLocalesDir, { recursive: true });
|
|
||||||
const files = await fsp.readdir(srcLocalesDir);
|
|
||||||
for (const file of files) {
|
|
||||||
const srcPath = path.join(srcLocalesDir, file);
|
|
||||||
const targetPath = path.join(targetLocalesDir, file);
|
|
||||||
if (!(await hasFileChanged(srcPath, targetPath))) continue;
|
|
||||||
await fsp.copyFile(srcPath, targetPath);
|
|
||||||
await updateHashCache(targetPath);
|
|
||||||
log_success(`Copied locale file: ${file}`);
|
|
||||||
}
|
|
||||||
log_success("All locale files processed successfully");
|
|
||||||
} catch (err) {
|
|
||||||
log_error("Error copying locale files:", err.message);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =======================
|
// =======================
|
||||||
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
|
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
|
||||||
// =======================
|
// =======================
|
||||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
|
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
|
||||||
const resolveService = () => {
|
const resolveService = () => {
|
||||||
let ext = platform === "win32" ? ".exe" : "";
|
const ext = platform === "win32" ? ".exe" : "";
|
||||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||||
return resolveResource({
|
return resolveResource({
|
||||||
file: "clash-verge-service" + suffix + ext,
|
file: "clash-verge-service" + suffix + ext,
|
||||||
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const resolveInstall = () => {
|
const resolveInstall = () => {
|
||||||
let ext = platform === "win32" ? ".exe" : "";
|
const ext = platform === "win32" ? ".exe" : "";
|
||||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||||
return resolveResource({
|
return resolveResource({
|
||||||
file: "clash-verge-service-install" + suffix + ext,
|
file: "clash-verge-service-install" + suffix + ext,
|
||||||
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
|
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const resolveUninstall = () => {
|
const resolveUninstall = () => {
|
||||||
let ext = platform === "win32" ? ".exe" : "";
|
const ext = platform === "win32" ? ".exe" : "";
|
||||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||||
return resolveResource({
|
return resolveResource({
|
||||||
file: "clash-verge-service-uninstall" + suffix + ext,
|
file: "clash-verge-service-uninstall" + suffix + ext,
|
||||||
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
|
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
|
||||||
@@ -715,7 +694,6 @@ const tasks = [
|
|||||||
retry: 5,
|
retry: 5,
|
||||||
macosOnly: true,
|
macosOnly: true,
|
||||||
},
|
},
|
||||||
{ name: "locales", func: resolveLocales, retry: 2 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
async function runTask() {
|
async function runTask() {
|
||||||
|
|||||||
@@ -30,10 +30,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import { program } from "commander";
|
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
import { program } from "commander";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前 git 短 commit hash
|
* 获取当前 git 短 commit hash
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import axios from "axios";
|
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
import { log_error, log_info, log_success } from "./utils.mjs";
|
import { log_error, log_info, log_success } from "./utils.mjs";
|
||||||
|
|
||||||
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
|
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export async function resolveUpdateLogDefault() {
|
|||||||
const reEnd = /^---/;
|
const reEnd = /^---/;
|
||||||
|
|
||||||
let isCapturing = false;
|
let isCapturing = false;
|
||||||
let content = [];
|
const content = [];
|
||||||
let firstTag = "";
|
let firstTag = "";
|
||||||
|
|
||||||
for (const line of data.split("\n")) {
|
for (const line of data.split("\n")) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
import { getOctokit, context } from "@actions/github";
|
import { getOctokit, context } from "@actions/github";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
import { resolveUpdateLog } from "./updatelog.mjs";
|
||||||
|
|
||||||
const UPDATE_TAG_NAME = "updater";
|
const UPDATE_TAG_NAME = "updater";
|
||||||
@@ -113,7 +114,7 @@ async function resolveUpdater() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// delete the old assets
|
// delete the old assets
|
||||||
for (let asset of updateRelease.assets) {
|
for (const asset of updateRelease.assets) {
|
||||||
if (asset.name === UPDATE_JSON_FILE) {
|
if (asset.name === UPDATE_JSON_FILE) {
|
||||||
await github.rest.repos.deleteReleaseAsset({
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
import { getOctokit, context } from "@actions/github";
|
import { getOctokit, context } from "@actions/github";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||||
|
|
||||||
// Add stable update JSON filenames
|
// Add stable update JSON filenames
|
||||||
@@ -259,7 +260,7 @@ async function processRelease(github, options, tag, isAlpha) {
|
|||||||
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
||||||
|
|
||||||
// Delete existing assets with these names
|
// Delete existing assets with these names
|
||||||
for (let asset of updateRelease.assets) {
|
for (const asset of updateRelease.assets) {
|
||||||
if (asset.name === jsonFile) {
|
if (asset.name === jsonFile) {
|
||||||
await github.rest.repos.deleteReleaseAsset({
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
Generated
+184
-14
@@ -572,6 +572,12 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base62"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
@@ -822,6 +828,16 @@ dependencies = [
|
|||||||
"alloc-stdlib",
|
"alloc-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.0"
|
version = "3.19.0"
|
||||||
@@ -1131,6 +1147,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest_dav",
|
"reqwest_dav",
|
||||||
"runas",
|
"runas",
|
||||||
|
"rust-i18n",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -2847,7 +2864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
|
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"proc-macro-crate 2.0.2",
|
"proc-macro-crate 2.0.0",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2888,6 +2905,30 @@ dependencies = [
|
|||||||
"xkeysym",
|
"xkeysym",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globset"
|
||||||
|
version = "0.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"bstr",
|
||||||
|
"log",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globwalk"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"ignore",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gobject-sys"
|
name = "gobject-sys"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
@@ -3512,6 +3553,22 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ignore"
|
||||||
|
version = "0.4.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"globset",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"same-file",
|
||||||
|
"walkdir",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.8"
|
version = "0.25.8"
|
||||||
@@ -3701,6 +3758,15 @@ dependencies = [
|
|||||||
"waker-fn",
|
"waker-fn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -4432,6 +4498,15 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "normpath"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify-rust"
|
name = "notify-rust"
|
||||||
version = "4.11.7"
|
version = "4.11.7"
|
||||||
@@ -5554,11 +5629,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "2.0.2"
|
version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
|
checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"toml_datetime 0.6.3",
|
|
||||||
"toml_edit 0.20.2",
|
"toml_edit 0.20.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6230,6 +6304,60 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-i18n"
|
||||||
|
version = "3.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332"
|
||||||
|
dependencies = [
|
||||||
|
"globwalk",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"rust-i18n-macro",
|
||||||
|
"rust-i18n-support",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-i18n-macro"
|
||||||
|
version = "3.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-i18n-support",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"syn 2.0.108",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-i18n-support"
|
||||||
|
version = "3.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19"
|
||||||
|
dependencies = [
|
||||||
|
"arc-swap",
|
||||||
|
"base62",
|
||||||
|
"globwalk",
|
||||||
|
"itertools 0.11.0",
|
||||||
|
"lazy_static",
|
||||||
|
"normpath",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"siphasher 1.0.1",
|
||||||
|
"toml 0.8.23",
|
||||||
|
"triomphe",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.21.3"
|
version = "0.21.3"
|
||||||
@@ -6672,6 +6800,19 @@ dependencies = [
|
|||||||
"syn 2.0.108",
|
"syn 2.0.108",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.34+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap 2.12.0",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_yaml_ng"
|
name = "serde_yaml_ng"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -7185,7 +7326,7 @@ dependencies = [
|
|||||||
"cfg-expr",
|
"cfg-expr",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"toml 0.8.2",
|
"toml 0.8.23",
|
||||||
"version-compare",
|
"version-compare",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8126,14 +8267,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.2"
|
version = "0.8.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned 0.6.9",
|
"serde_spanned 0.6.9",
|
||||||
"toml_datetime 0.6.3",
|
"toml_datetime 0.6.11",
|
||||||
"toml_edit 0.20.2",
|
"toml_edit 0.22.27",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8153,9 +8294,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.3"
|
version = "0.6.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -8176,7 +8317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.12.0",
|
"indexmap 2.12.0",
|
||||||
"toml_datetime 0.6.3",
|
"toml_datetime 0.6.11",
|
||||||
"winnow 0.5.40",
|
"winnow 0.5.40",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8185,12 +8326,24 @@ name = "toml_edit"
|
|||||||
version = "0.20.2"
|
version = "0.20.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap 2.12.0",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"winnow 0.5.40",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.12.0",
|
"indexmap 2.12.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned 0.6.9",
|
"serde_spanned 0.6.9",
|
||||||
"toml_datetime 0.6.3",
|
"toml_datetime 0.6.11",
|
||||||
"winnow 0.5.40",
|
"toml_write",
|
||||||
|
"winnow 0.7.13",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8214,6 +8367,12 @@ dependencies = [
|
|||||||
"winnow 0.7.13",
|
"winnow 0.7.13",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -8513,6 +8672,17 @@ dependencies = [
|
|||||||
"petgraph",
|
"petgraph",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "triomphe"
|
||||||
|
version = "0.1.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39"
|
||||||
|
dependencies = [
|
||||||
|
"arc-swap",
|
||||||
|
"serde",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ clash_verge_service_ipc = { version = "2.0.21", features = [
|
|||||||
"client",
|
"client",
|
||||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||||
arc-swap = "1.7.1"
|
arc-swap = "1.7.1"
|
||||||
|
rust-i18n = "3.1.5"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.2.0"
|
runas = "=1.2.0"
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Dashboard
|
||||||
|
body: Dashboard visibility has been updated.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Mode Switch
|
||||||
|
body: Switched to {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: System Proxy
|
||||||
|
body: System proxy status has been updated.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN Mode
|
||||||
|
body: TUN mode status has been updated.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Lightweight Mode
|
||||||
|
body: Entered lightweight mode.
|
||||||
|
appQuit:
|
||||||
|
title: About to Exit
|
||||||
|
body: Clash Verge is about to exit.
|
||||||
|
appHidden:
|
||||||
|
title: Application Hidden
|
||||||
|
body: Clash Verge is running in the background.
|
||||||
|
service:
|
||||||
|
adminPrompt: Installing the service requires administrator privileges.
|
||||||
|
tray:
|
||||||
|
dashboard: Dashboard
|
||||||
|
ruleMode: Rule Mode
|
||||||
|
globalMode: Global Mode
|
||||||
|
directMode: Direct Mode
|
||||||
|
profiles: Profiles
|
||||||
|
proxies: Proxies
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tunMode: TUN Mode
|
||||||
|
closeAllConnections: Close All Connections
|
||||||
|
lightweightMode: Lightweight Mode
|
||||||
|
copyEnv: Copy Environment Variables
|
||||||
|
confDir: Configuration Directory
|
||||||
|
coreDir: Core Directory
|
||||||
|
logsDir: Log Directory
|
||||||
|
openDir: Open Directory
|
||||||
|
appLog: Application Log
|
||||||
|
coreLog: Core Log
|
||||||
|
restartClash: Restart Clash Core
|
||||||
|
restartApp: Restart Application
|
||||||
|
vergeVersion: Verge Version
|
||||||
|
more: More
|
||||||
|
exit: Exit
|
||||||
|
tooltip:
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tun: TUN
|
||||||
|
profile: Profile
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Dashboard
|
||||||
|
body: Dashboard visibility has been updated.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Mode Switch
|
||||||
|
body: Switched to {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: System Proxy
|
||||||
|
body: System proxy status has been updated.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN Mode
|
||||||
|
body: TUN mode status has been updated.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Lightweight Mode
|
||||||
|
body: Entered lightweight mode.
|
||||||
|
appQuit:
|
||||||
|
title: About to Exit
|
||||||
|
body: Clash Verge is about to exit.
|
||||||
|
appHidden:
|
||||||
|
title: Application Hidden
|
||||||
|
body: Clash Verge is running in the background.
|
||||||
|
service:
|
||||||
|
adminPrompt: Installing the service requires administrator privileges.
|
||||||
|
tray:
|
||||||
|
dashboard: Dashboard
|
||||||
|
ruleMode: Rule Mode
|
||||||
|
globalMode: Global Mode
|
||||||
|
directMode: Direct Mode
|
||||||
|
profiles: Profiles
|
||||||
|
proxies: Proxies
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tunMode: TUN Mode
|
||||||
|
closeAllConnections: Close All Connections
|
||||||
|
lightweightMode: Lightweight Mode
|
||||||
|
copyEnv: Copy Environment Variables
|
||||||
|
confDir: Configuration Directory
|
||||||
|
coreDir: Core Directory
|
||||||
|
logsDir: Log Directory
|
||||||
|
openDir: Open Directory
|
||||||
|
appLog: Application Log
|
||||||
|
coreLog: Core Log
|
||||||
|
restartClash: Restart Clash Core
|
||||||
|
restartApp: Restart Application
|
||||||
|
vergeVersion: Verge Version
|
||||||
|
more: More
|
||||||
|
exit: Exit
|
||||||
|
tooltip:
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tun: TUN
|
||||||
|
profile: Profile
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Dashboard
|
||||||
|
body: Dashboard visibility has been updated.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Mode Switch
|
||||||
|
body: Switched to {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: System Proxy
|
||||||
|
body: System proxy status has been updated.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN Mode
|
||||||
|
body: TUN mode status has been updated.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Lightweight Mode
|
||||||
|
body: Entered lightweight mode.
|
||||||
|
appQuit:
|
||||||
|
title: About to Exit
|
||||||
|
body: Clash Verge is about to exit.
|
||||||
|
appHidden:
|
||||||
|
title: Application Hidden
|
||||||
|
body: Clash Verge is running in the background.
|
||||||
|
service:
|
||||||
|
adminPrompt: Installing the service requires administrator privileges.
|
||||||
|
tray:
|
||||||
|
dashboard: Dashboard
|
||||||
|
ruleMode: Rule Mode
|
||||||
|
globalMode: Global Mode
|
||||||
|
directMode: Direct Mode
|
||||||
|
profiles: Profiles
|
||||||
|
proxies: Proxies
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tunMode: TUN Mode
|
||||||
|
closeAllConnections: Close All Connections
|
||||||
|
lightweightMode: Lightweight Mode
|
||||||
|
copyEnv: Copy Environment Variables
|
||||||
|
confDir: Configuration Directory
|
||||||
|
coreDir: Core Directory
|
||||||
|
logsDir: Log Directory
|
||||||
|
openDir: Open Directory
|
||||||
|
appLog: Application Log
|
||||||
|
coreLog: Core Log
|
||||||
|
restartClash: Restart Clash Core
|
||||||
|
restartApp: Restart Application
|
||||||
|
vergeVersion: Verge Version
|
||||||
|
more: More
|
||||||
|
exit: Exit
|
||||||
|
tooltip:
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tun: TUN
|
||||||
|
profile: Profile
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Dashboard
|
||||||
|
body: Dashboard visibility has been updated.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Mode Switch
|
||||||
|
body: Switched to {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: System Proxy
|
||||||
|
body: System proxy status has been updated.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN Mode
|
||||||
|
body: TUN mode status has been updated.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Lightweight Mode
|
||||||
|
body: Entered lightweight mode.
|
||||||
|
appQuit:
|
||||||
|
title: About to Exit
|
||||||
|
body: Clash Verge is about to exit.
|
||||||
|
appHidden:
|
||||||
|
title: Application Hidden
|
||||||
|
body: Clash Verge is running in the background.
|
||||||
|
service:
|
||||||
|
adminPrompt: Installing the service requires administrator privileges.
|
||||||
|
tray:
|
||||||
|
dashboard: Dashboard
|
||||||
|
ruleMode: Rule Mode
|
||||||
|
globalMode: Global Mode
|
||||||
|
directMode: Direct Mode
|
||||||
|
profiles: Profiles
|
||||||
|
proxies: Proxies
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tunMode: TUN Mode
|
||||||
|
closeAllConnections: Close All Connections
|
||||||
|
lightweightMode: Lightweight Mode
|
||||||
|
copyEnv: Copy Environment Variables
|
||||||
|
confDir: Configuration Directory
|
||||||
|
coreDir: Core Directory
|
||||||
|
logsDir: Log Directory
|
||||||
|
openDir: Open Directory
|
||||||
|
appLog: Application Log
|
||||||
|
coreLog: Core Log
|
||||||
|
restartClash: Restart Clash Core
|
||||||
|
restartApp: Restart Application
|
||||||
|
vergeVersion: Verge Version
|
||||||
|
more: More
|
||||||
|
exit: Exit
|
||||||
|
tooltip:
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tun: TUN
|
||||||
|
profile: Profile
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Dashboard
|
||||||
|
body: Dashboard visibility has been updated.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Mode Switch
|
||||||
|
body: Switched to {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: System Proxy
|
||||||
|
body: System proxy status has been updated.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN Mode
|
||||||
|
body: TUN mode status has been updated.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Lightweight Mode
|
||||||
|
body: Entered lightweight mode.
|
||||||
|
appQuit:
|
||||||
|
title: About to Exit
|
||||||
|
body: Clash Verge is about to exit.
|
||||||
|
appHidden:
|
||||||
|
title: Application Hidden
|
||||||
|
body: Clash Verge is running in the background.
|
||||||
|
service:
|
||||||
|
adminPrompt: Installing the service requires administrator privileges.
|
||||||
|
tray:
|
||||||
|
dashboard: Dashboard
|
||||||
|
ruleMode: Rule Mode
|
||||||
|
globalMode: Global Mode
|
||||||
|
directMode: Direct Mode
|
||||||
|
profiles: Profiles
|
||||||
|
proxies: Proxies
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tunMode: TUN Mode
|
||||||
|
closeAllConnections: Close All Connections
|
||||||
|
lightweightMode: Lightweight Mode
|
||||||
|
copyEnv: Copy Environment Variables
|
||||||
|
confDir: Configuration Directory
|
||||||
|
coreDir: Core Directory
|
||||||
|
logsDir: Log Directory
|
||||||
|
openDir: Open Directory
|
||||||
|
appLog: Application Log
|
||||||
|
coreLog: Core Log
|
||||||
|
restartClash: Restart Clash Core
|
||||||
|
restartApp: Restart Application
|
||||||
|
vergeVersion: Verge Version
|
||||||
|
more: More
|
||||||
|
exit: Exit
|
||||||
|
tooltip:
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tun: TUN
|
||||||
|
profile: Profile
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Dashboard
|
||||||
|
body: Dashboard visibility has been updated.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Mode Switch
|
||||||
|
body: Switched to {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: System Proxy
|
||||||
|
body: System proxy status has been updated.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN Mode
|
||||||
|
body: TUN mode status has been updated.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Lightweight Mode
|
||||||
|
body: Entered lightweight mode.
|
||||||
|
appQuit:
|
||||||
|
title: About to Exit
|
||||||
|
body: Clash Verge is about to exit.
|
||||||
|
appHidden:
|
||||||
|
title: Application Hidden
|
||||||
|
body: Clash Verge is running in the background.
|
||||||
|
service:
|
||||||
|
adminPrompt: Installing the service requires administrator privileges.
|
||||||
|
tray:
|
||||||
|
dashboard: Dashboard
|
||||||
|
ruleMode: Rule Mode
|
||||||
|
globalMode: Global Mode
|
||||||
|
directMode: Direct Mode
|
||||||
|
profiles: Profiles
|
||||||
|
proxies: Proxies
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tunMode: TUN Mode
|
||||||
|
closeAllConnections: Close All Connections
|
||||||
|
lightweightMode: Lightweight Mode
|
||||||
|
copyEnv: Copy Environment Variables
|
||||||
|
confDir: Configuration Directory
|
||||||
|
coreDir: Core Directory
|
||||||
|
logsDir: Log Directory
|
||||||
|
openDir: Open Directory
|
||||||
|
appLog: Application Log
|
||||||
|
coreLog: Core Log
|
||||||
|
restartClash: Restart Clash Core
|
||||||
|
restartApp: Restart Application
|
||||||
|
vergeVersion: Verge Version
|
||||||
|
more: More
|
||||||
|
exit: Exit
|
||||||
|
tooltip:
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tun: TUN
|
||||||
|
profile: Profile
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Dashboard
|
||||||
|
body: Dashboard visibility has been updated.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Mode Switch
|
||||||
|
body: Switched to {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: System Proxy
|
||||||
|
body: System proxy status has been updated.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN Mode
|
||||||
|
body: TUN mode status has been updated.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Lightweight Mode
|
||||||
|
body: Entered lightweight mode.
|
||||||
|
appQuit:
|
||||||
|
title: About to Exit
|
||||||
|
body: Clash Verge is about to exit.
|
||||||
|
appHidden:
|
||||||
|
title: Application Hidden
|
||||||
|
body: Clash Verge is running in the background.
|
||||||
|
service:
|
||||||
|
adminPrompt: Installing the service requires administrator privileges.
|
||||||
|
tray:
|
||||||
|
dashboard: Dashboard
|
||||||
|
ruleMode: Rule Mode
|
||||||
|
globalMode: Global Mode
|
||||||
|
directMode: Direct Mode
|
||||||
|
profiles: Profiles
|
||||||
|
proxies: Proxies
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tunMode: TUN Mode
|
||||||
|
closeAllConnections: Close All Connections
|
||||||
|
lightweightMode: Lightweight Mode
|
||||||
|
copyEnv: Copy Environment Variables
|
||||||
|
confDir: Configuration Directory
|
||||||
|
coreDir: Core Directory
|
||||||
|
logsDir: Log Directory
|
||||||
|
openDir: Open Directory
|
||||||
|
appLog: Application Log
|
||||||
|
coreLog: Core Log
|
||||||
|
restartClash: Restart Clash Core
|
||||||
|
restartApp: Restart Application
|
||||||
|
vergeVersion: Verge Version
|
||||||
|
more: More
|
||||||
|
exit: Exit
|
||||||
|
tooltip:
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tun: TUN
|
||||||
|
profile: Profile
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: 대시보드
|
||||||
|
body: 대시보드 표시 상태가 업데이트되었습니다.
|
||||||
|
clashModeChanged:
|
||||||
|
title: 모드 전환
|
||||||
|
body: "{mode}(으)로 전환되었습니다."
|
||||||
|
systemProxyToggled:
|
||||||
|
title: 시스템 프록시
|
||||||
|
body: 시스템 프록시 상태가 업데이트되었습니다.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN 모드
|
||||||
|
body: TUN 모드 상태가 업데이트되었습니다.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: 경량 모드
|
||||||
|
body: 경량 모드에 진입했습니다.
|
||||||
|
appQuit:
|
||||||
|
title: 곧 종료
|
||||||
|
body: Clash Verge가 곧 종료됩니다.
|
||||||
|
appHidden:
|
||||||
|
title: 앱이 숨겨짐
|
||||||
|
body: Clash Verge가 백그라운드에서 실행 중입니다.
|
||||||
|
service:
|
||||||
|
adminPrompt: 서비스를 설치하려면 관리자 권한이 필요합니다.
|
||||||
|
tray:
|
||||||
|
dashboard: 대시보드
|
||||||
|
ruleMode: 규칙 모드
|
||||||
|
globalMode: 전역 모드
|
||||||
|
directMode: 직접 모드
|
||||||
|
profiles: 프로필
|
||||||
|
proxies: 프록시
|
||||||
|
systemProxy: 시스템 프록시
|
||||||
|
tunMode: TUN 모드
|
||||||
|
closeAllConnections: 모든 연결 닫기
|
||||||
|
lightweightMode: 경량 모드
|
||||||
|
copyEnv: 환경 변수 복사
|
||||||
|
confDir: 구성 디렉터리
|
||||||
|
coreDir: 코어 디렉터리
|
||||||
|
logsDir: 로그 디렉터리
|
||||||
|
openDir: 디렉터리 열기
|
||||||
|
appLog: 애플리케이션 로그
|
||||||
|
coreLog: 코어 로그
|
||||||
|
restartClash: Clash 코어 재시작
|
||||||
|
restartApp: 애플리케이션 재시작
|
||||||
|
vergeVersion: Verge 버전
|
||||||
|
more: 더 보기
|
||||||
|
exit: 종료
|
||||||
|
tooltip:
|
||||||
|
systemProxy: 시스템 프록시
|
||||||
|
tun: TUN
|
||||||
|
profile: 프로필
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Dashboard
|
||||||
|
body: Dashboard visibility has been updated.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Mode Switch
|
||||||
|
body: Switched to {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: System Proxy
|
||||||
|
body: System proxy status has been updated.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN Mode
|
||||||
|
body: TUN mode status has been updated.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Lightweight Mode
|
||||||
|
body: Entered lightweight mode.
|
||||||
|
appQuit:
|
||||||
|
title: About to Exit
|
||||||
|
body: Clash Verge is about to exit.
|
||||||
|
appHidden:
|
||||||
|
title: Application Hidden
|
||||||
|
body: Clash Verge is running in the background.
|
||||||
|
service:
|
||||||
|
adminPrompt: Installing the service requires administrator privileges.
|
||||||
|
tray:
|
||||||
|
dashboard: Dashboard
|
||||||
|
ruleMode: Rule Mode
|
||||||
|
globalMode: Global Mode
|
||||||
|
directMode: Direct Mode
|
||||||
|
profiles: Profiles
|
||||||
|
proxies: Proxies
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tunMode: TUN Mode
|
||||||
|
closeAllConnections: Close All Connections
|
||||||
|
lightweightMode: Lightweight Mode
|
||||||
|
copyEnv: Copy Environment Variables
|
||||||
|
confDir: Configuration Directory
|
||||||
|
coreDir: Core Directory
|
||||||
|
logsDir: Log Directory
|
||||||
|
openDir: Open Directory
|
||||||
|
appLog: Application Log
|
||||||
|
coreLog: Core Log
|
||||||
|
restartClash: Restart Clash Core
|
||||||
|
restartApp: Restart Application
|
||||||
|
vergeVersion: Verge Version
|
||||||
|
more: More
|
||||||
|
exit: Exit
|
||||||
|
tooltip:
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tun: TUN
|
||||||
|
profile: Profile
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Dashboard
|
||||||
|
body: Dashboard visibility has been updated.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Mode Switch
|
||||||
|
body: Switched to {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: System Proxy
|
||||||
|
body: System proxy status has been updated.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN Mode
|
||||||
|
body: TUN mode status has been updated.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Lightweight Mode
|
||||||
|
body: Entered lightweight mode.
|
||||||
|
appQuit:
|
||||||
|
title: About to Exit
|
||||||
|
body: Clash Verge is about to exit.
|
||||||
|
appHidden:
|
||||||
|
title: Application Hidden
|
||||||
|
body: Clash Verge is running in the background.
|
||||||
|
service:
|
||||||
|
adminPrompt: Installing the service requires administrator privileges.
|
||||||
|
tray:
|
||||||
|
dashboard: Dashboard
|
||||||
|
ruleMode: Rule Mode
|
||||||
|
globalMode: Global Mode
|
||||||
|
directMode: Direct Mode
|
||||||
|
profiles: Profiles
|
||||||
|
proxies: Proxies
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tunMode: TUN Mode
|
||||||
|
closeAllConnections: Close All Connections
|
||||||
|
lightweightMode: Lightweight Mode
|
||||||
|
copyEnv: Copy Environment Variables
|
||||||
|
confDir: Configuration Directory
|
||||||
|
coreDir: Core Directory
|
||||||
|
logsDir: Log Directory
|
||||||
|
openDir: Open Directory
|
||||||
|
appLog: Application Log
|
||||||
|
coreLog: Core Log
|
||||||
|
restartClash: Restart Clash Core
|
||||||
|
restartApp: Restart Application
|
||||||
|
vergeVersion: Verge Version
|
||||||
|
more: More
|
||||||
|
exit: Exit
|
||||||
|
tooltip:
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tun: TUN
|
||||||
|
profile: Profile
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Dashboard
|
||||||
|
body: Dashboard visibility has been updated.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Mode Switch
|
||||||
|
body: Switched to {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: System Proxy
|
||||||
|
body: System proxy status has been updated.
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN Mode
|
||||||
|
body: TUN mode status has been updated.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Lightweight Mode
|
||||||
|
body: Entered lightweight mode.
|
||||||
|
appQuit:
|
||||||
|
title: About to Exit
|
||||||
|
body: Clash Verge is about to exit.
|
||||||
|
appHidden:
|
||||||
|
title: Application Hidden
|
||||||
|
body: Clash Verge is running in the background.
|
||||||
|
service:
|
||||||
|
adminPrompt: Installing the service requires administrator privileges.
|
||||||
|
tray:
|
||||||
|
dashboard: Dashboard
|
||||||
|
ruleMode: Rule Mode
|
||||||
|
globalMode: Global Mode
|
||||||
|
directMode: Direct Mode
|
||||||
|
profiles: Profiles
|
||||||
|
proxies: Proxies
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tunMode: TUN Mode
|
||||||
|
closeAllConnections: Close All Connections
|
||||||
|
lightweightMode: Lightweight Mode
|
||||||
|
copyEnv: Copy Environment Variables
|
||||||
|
confDir: Configuration Directory
|
||||||
|
coreDir: Core Directory
|
||||||
|
logsDir: Log Directory
|
||||||
|
openDir: Open Directory
|
||||||
|
appLog: Application Log
|
||||||
|
coreLog: Core Log
|
||||||
|
restartClash: Restart Clash Core
|
||||||
|
restartApp: Restart Application
|
||||||
|
vergeVersion: Verge Version
|
||||||
|
more: More
|
||||||
|
exit: Exit
|
||||||
|
tooltip:
|
||||||
|
systemProxy: System Proxy
|
||||||
|
tun: TUN
|
||||||
|
profile: Profile
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: 仪表板
|
||||||
|
body: 仪表板显示状态已更新。
|
||||||
|
clashModeChanged:
|
||||||
|
title: 模式切换
|
||||||
|
body: 已切换至 {mode}。
|
||||||
|
systemProxyToggled:
|
||||||
|
title: 系统代理
|
||||||
|
body: 系统代理状态已更新。
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN 模式
|
||||||
|
body: TUN 模式状态已更新。
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: 轻量模式
|
||||||
|
body: 已进入轻量模式。
|
||||||
|
appQuit:
|
||||||
|
title: 即将退出
|
||||||
|
body: Clash Verge 即将退出。
|
||||||
|
appHidden:
|
||||||
|
title: 应用已隐藏
|
||||||
|
body: Clash Verge 正在后台运行。
|
||||||
|
service:
|
||||||
|
adminPrompt: 安装服务需要管理员权限
|
||||||
|
tray:
|
||||||
|
dashboard: 仪表板
|
||||||
|
ruleMode: 规则模式
|
||||||
|
globalMode: 全局模式
|
||||||
|
directMode: 直连模式
|
||||||
|
profiles: 订阅
|
||||||
|
proxies: 代理
|
||||||
|
systemProxy: 系统代理
|
||||||
|
tunMode: TUN 模式
|
||||||
|
closeAllConnections: 关闭所有连接
|
||||||
|
lightweightMode: 轻量模式
|
||||||
|
copyEnv: 复制环境变量
|
||||||
|
confDir: 配置目录
|
||||||
|
coreDir: 内核目录
|
||||||
|
logsDir: 日志目录
|
||||||
|
openDir: 打开目录
|
||||||
|
appLog: 应用日志
|
||||||
|
coreLog: 内核日志
|
||||||
|
restartClash: 重启 Clash 内核
|
||||||
|
restartApp: 重启应用
|
||||||
|
vergeVersion: Verge 版本
|
||||||
|
more: 更多
|
||||||
|
exit: 退出
|
||||||
|
tooltip:
|
||||||
|
systemProxy: 系统代理
|
||||||
|
tun: TUN
|
||||||
|
profile: 订阅
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: 儀表板
|
||||||
|
body: 儀表板顯示狀態已更新。
|
||||||
|
clashModeChanged:
|
||||||
|
title: 模式切換
|
||||||
|
body: 已切換至 {mode}。
|
||||||
|
systemProxyToggled:
|
||||||
|
title: 系統代理
|
||||||
|
body: 系統代理狀態已更新。
|
||||||
|
tunModeToggled:
|
||||||
|
title: TUN 模式
|
||||||
|
body: TUN 模式狀態已更新。
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: 輕量模式
|
||||||
|
body: 已進入輕量模式。
|
||||||
|
appQuit:
|
||||||
|
title: 即將退出
|
||||||
|
body: Clash Verge 即將退出。
|
||||||
|
appHidden:
|
||||||
|
title: 應用已隱藏
|
||||||
|
body: Clash Verge 正在背景執行。
|
||||||
|
service:
|
||||||
|
adminPrompt: 安裝服務需要管理員權限
|
||||||
|
tray:
|
||||||
|
dashboard: 儀表板
|
||||||
|
ruleMode: 規則模式
|
||||||
|
globalMode: 全域模式
|
||||||
|
directMode: 直連模式
|
||||||
|
profiles: 訂閱
|
||||||
|
proxies: 代理
|
||||||
|
systemProxy: 系統代理
|
||||||
|
tunMode: TUN 模式
|
||||||
|
closeAllConnections: 關閉所有連線
|
||||||
|
lightweightMode: 輕量模式
|
||||||
|
copyEnv: 複製環境變數
|
||||||
|
confDir: 設定目錄
|
||||||
|
coreDir: 核心目錄
|
||||||
|
logsDir: 日誌目錄
|
||||||
|
openDir: 開啟目錄
|
||||||
|
appLog: 應用程式日誌
|
||||||
|
coreLog: 核心日誌
|
||||||
|
restartClash: 重新啟動 Clash 核心
|
||||||
|
restartApp: 重新啟動應用程式
|
||||||
|
vergeVersion: Verge 版本
|
||||||
|
more: 更多
|
||||||
|
exit: 離開
|
||||||
|
tooltip:
|
||||||
|
systemProxy: 系統代理
|
||||||
|
tun: TUN
|
||||||
|
profile: 訂閱
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
use super::{CmdResult, StringifyErr};
|
use super::{CmdResult, StringifyErr};
|
||||||
use crate::{
|
use crate::core::service::{self, SERVICE_MANAGER, ServiceStatus};
|
||||||
core::service::{self, SERVICE_MANAGER, ServiceStatus},
|
|
||||||
utils::i18n::t,
|
|
||||||
};
|
|
||||||
use smartstring::SmartString;
|
use smartstring::SmartString;
|
||||||
|
|
||||||
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
|
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
|
||||||
@@ -13,7 +10,7 @@ async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) ->
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let emsg = format!("{} Service failed: {}", op_type, e);
|
let emsg = format!("{} Service failed: {}", op_type, e);
|
||||||
return Err(SmartString::from(&*t(emsg.as_str()).await));
|
return Err(SmartString::from(emsg));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,8 +227,6 @@ fn linux_running_as_root() -> bool {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
async fn uninstall_service() -> Result<()> {
|
async fn uninstall_service() -> Result<()> {
|
||||||
use crate::utils::i18n::t;
|
|
||||||
|
|
||||||
logging!(info, Type::Service, "uninstall service");
|
logging!(info, Type::Service, "uninstall service");
|
||||||
|
|
||||||
let binary_path = dirs::service_path()?;
|
let binary_path = dirs::service_path()?;
|
||||||
@@ -240,7 +238,9 @@ async fn uninstall_service() -> Result<()> {
|
|||||||
|
|
||||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||||
|
|
||||||
let prompt = t("Service Administrator Prompt").await;
|
crate::utils::i18n::sync_locale().await;
|
||||||
|
|
||||||
|
let prompt = rust_i18n::t!("service.adminPrompt").to_string();
|
||||||
let command = format!(
|
let command = format!(
|
||||||
r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#
|
r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||||
);
|
);
|
||||||
@@ -263,8 +263,6 @@ async fn uninstall_service() -> Result<()> {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
async fn install_service() -> Result<()> {
|
async fn install_service() -> Result<()> {
|
||||||
use crate::utils::i18n::t;
|
|
||||||
|
|
||||||
logging!(info, Type::Service, "install service");
|
logging!(info, Type::Service, "install service");
|
||||||
|
|
||||||
let binary_path = dirs::service_path()?;
|
let binary_path = dirs::service_path()?;
|
||||||
@@ -276,7 +274,9 @@ async fn install_service() -> Result<()> {
|
|||||||
|
|
||||||
let install_shell: String = install_path.to_string_lossy().into_owned();
|
let install_shell: String = install_path.to_string_lossy().into_owned();
|
||||||
|
|
||||||
let prompt = t("Service Administrator Prompt").await;
|
crate::utils::i18n::sync_locale().await;
|
||||||
|
|
||||||
|
let prompt = rust_i18n::t!("service.adminPrompt").to_string();
|
||||||
let command = format!(
|
let command = format!(
|
||||||
r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
|
r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
use crate::utils::i18n::t;
|
use rust_i18n::t;
|
||||||
use std::sync::Arc;
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
|
fn to_arc_str(value: Cow<'static, str>) -> Arc<str> {
|
||||||
|
match value {
|
||||||
|
Cow::Borrowed(s) => Arc::from(s),
|
||||||
|
Cow::Owned(s) => Arc::from(s.into_boxed_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! define_menu {
|
macro_rules! define_menu {
|
||||||
($($field:ident => $const_name:ident, $id:expr, $text:expr),+ $(,)?) => {
|
($($field:ident => $const_name:ident, $id:expr, $text:expr),+ $(,)?) => {
|
||||||
@@ -11,9 +18,10 @@ macro_rules! define_menu {
|
|||||||
pub struct MenuIds;
|
pub struct MenuIds;
|
||||||
|
|
||||||
impl MenuTexts {
|
impl MenuTexts {
|
||||||
pub async fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let ($($field,)+) = futures::join!($(t($text),)+);
|
Self {
|
||||||
Self { $($field,)+ }
|
$($field: to_arc_str(t!($text)),)+
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,26 +32,26 @@ macro_rules! define_menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
define_menu! {
|
define_menu! {
|
||||||
dashboard => DASHBOARD, "tray_dashboard", "Dashboard",
|
dashboard => DASHBOARD, "tray_dashboard", "tray.dashboard",
|
||||||
rule_mode => RULE_MODE, "tray_rule_mode", "Rule Mode",
|
rule_mode => RULE_MODE, "tray_rule_mode", "tray.ruleMode",
|
||||||
global_mode => GLOBAL_MODE, "tray_global_mode", "Global Mode",
|
global_mode => GLOBAL_MODE, "tray_global_mode", "tray.globalMode",
|
||||||
direct_mode => DIRECT_MODE, "tray_direct_mode", "Direct Mode",
|
direct_mode => DIRECT_MODE, "tray_direct_mode", "tray.directMode",
|
||||||
profiles => PROFILES, "tray_profiles", "Profiles",
|
profiles => PROFILES, "tray_profiles", "tray.profiles",
|
||||||
proxies => PROXIES, "tray_proxies", "Proxies",
|
proxies => PROXIES, "tray_proxies", "tray.proxies",
|
||||||
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "System Proxy",
|
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy",
|
||||||
tun_mode => TUN_MODE, "tray_tun_mode", "TUN Mode",
|
tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode",
|
||||||
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "Close All Connections",
|
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections",
|
||||||
lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "LightWeight Mode",
|
lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "tray.lightweightMode",
|
||||||
copy_env => COPY_ENV, "tray_copy_env", "Copy Env",
|
copy_env => COPY_ENV, "tray_copy_env", "tray.copyEnv",
|
||||||
conf_dir => CONF_DIR, "tray_conf_dir", "Conf Dir",
|
conf_dir => CONF_DIR, "tray_conf_dir", "tray.confDir",
|
||||||
core_dir => CORE_DIR, "tray_core_dir", "Core Dir",
|
core_dir => CORE_DIR, "tray_core_dir", "tray.coreDir",
|
||||||
logs_dir => LOGS_DIR, "tray_logs_dir", "Logs Dir",
|
logs_dir => LOGS_DIR, "tray_logs_dir", "tray.logsDir",
|
||||||
open_dir => OPEN_DIR, "tray_open_dir", "Open Dir",
|
open_dir => OPEN_DIR, "tray_open_dir", "tray.openDir",
|
||||||
app_log => APP_LOG, "tray_app_log", "Open App Log",
|
app_log => APP_LOG, "tray_app_log", "tray.appLog",
|
||||||
core_log => CORE_LOG, "tray_core_log", "Open Core Log",
|
core_log => CORE_LOG, "tray_core_log", "tray.coreLog",
|
||||||
restart_clash => RESTART_CLASH, "tray_restart_clash", "Restart Clash Core",
|
restart_clash => RESTART_CLASH, "tray_restart_clash", "tray.restartClash",
|
||||||
restart_app => RESTART_APP, "tray_restart_app", "Restart App",
|
restart_app => RESTART_APP, "tray_restart_app", "tray.restartApp",
|
||||||
verge_version => VERGE_VERSION, "tray_verge_version", "Verge Version",
|
verge_version => VERGE_VERSION, "tray_verge_version", "tray.vergeVersion",
|
||||||
more => MORE, "tray_more", "More",
|
more => MORE, "tray_more", "tray.more",
|
||||||
exit => EXIT, "tray_exit", "Exit",
|
exit => EXIT, "tray_exit", "tray.exit",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::{
|
|||||||
feat, logging,
|
feat, logging,
|
||||||
module::lightweight::is_in_lightweight_mode,
|
module::lightweight::is_in_lightweight_mode,
|
||||||
singleton_lazy,
|
singleton_lazy,
|
||||||
utils::{dirs::find_target_icons, i18n::t},
|
utils::{dirs::find_target_icons, i18n},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::handle;
|
use super::handle;
|
||||||
@@ -438,6 +438,8 @@ impl Tray {
|
|||||||
|
|
||||||
let app_handle = handle::Handle::app_handle();
|
let app_handle = handle::Handle::app_handle();
|
||||||
|
|
||||||
|
i18n::sync_locale().await;
|
||||||
|
|
||||||
let verge = Config::verge().await.latest_arc();
|
let verge = Config::verge().await.latest_arc();
|
||||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
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 = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||||
@@ -464,9 +466,9 @@ impl Tray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get localized strings before using them
|
// Get localized strings before using them
|
||||||
let sys_proxy_text = t("SysProxy").await;
|
let sys_proxy_text = rust_i18n::t!("tray.tooltip.systemProxy");
|
||||||
let tun_text = t("TUN").await;
|
let tun_text = rust_i18n::t!("tray.tooltip.tun");
|
||||||
let profile_text = t("Profile").await;
|
let profile_text = rust_i18n::t!("tray.tooltip.profile");
|
||||||
|
|
||||||
let v = env!("CARGO_PKG_VERSION");
|
let v = env!("CARGO_PKG_VERSION");
|
||||||
let reassembled_version = v.split_once('+').map_or_else(
|
let reassembled_version = v.split_once('+').map_or_else(
|
||||||
@@ -622,7 +624,7 @@ async fn create_profile_menu_item(
|
|||||||
CheckMenuItem::with_id(
|
CheckMenuItem::with_id(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
format!("profiles_{profile_uid}"),
|
format!("profiles_{profile_uid}"),
|
||||||
t(profile_name).await,
|
profile_name.as_str(),
|
||||||
true,
|
true,
|
||||||
is_current_profile,
|
is_current_profile,
|
||||||
None::<&str>,
|
None::<&str>,
|
||||||
@@ -817,6 +819,8 @@ async fn create_tray_menu(
|
|||||||
) -> Result<tauri::menu::Menu<Wry>> {
|
) -> Result<tauri::menu::Menu<Wry>> {
|
||||||
let current_proxy_mode = mode.unwrap_or("");
|
let current_proxy_mode = mode.unwrap_or("");
|
||||||
|
|
||||||
|
i18n::sync_locale().await;
|
||||||
|
|
||||||
// 获取当前配置文件的选中代理组信息
|
// 获取当前配置文件的选中代理组信息
|
||||||
let current_profile_selected = {
|
let current_profile_selected = {
|
||||||
let profiles_config = Config::profiles().await;
|
let profiles_config = Config::profiles().await;
|
||||||
@@ -877,7 +881,7 @@ async fn create_tray_menu(
|
|||||||
create_profile_menu_item(app_handle, profile_uid_and_name).await?;
|
create_profile_menu_item(app_handle, profile_uid_and_name).await?;
|
||||||
|
|
||||||
// Pre-fetch all localized strings
|
// Pre-fetch all localized strings
|
||||||
let texts = &MenuTexts::new().await;
|
let texts = MenuTexts::new();
|
||||||
// Convert to references only when needed
|
// Convert to references only when needed
|
||||||
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -25,12 +25,15 @@ use crate::{
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use rust_i18n::i18n;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
use utils::logging::Type;
|
use utils::logging::Type;
|
||||||
|
|
||||||
|
i18n!("locales", fallback = "zh");
|
||||||
|
|
||||||
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
|
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
|
||||||
/// Application initialization helper functions
|
/// Application initialization helper functions
|
||||||
mod app_init {
|
mod app_init {
|
||||||
|
|||||||
+78
-93
@@ -1,43 +1,80 @@
|
|||||||
use crate::{config::Config, utils::dirs};
|
use crate::config::Config;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use smartstring::alias::String;
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
fs,
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
use sys_locale;
|
use sys_locale;
|
||||||
|
|
||||||
const DEFAULT_LANGUAGE: &str = "zh";
|
const DEFAULT_LANGUAGE: &str = "zh";
|
||||||
|
|
||||||
type TranslationMap = (String, HashMap<String, Arc<str>>);
|
fn supported_languages_internal() -> Vec<&'static str> {
|
||||||
|
rust_i18n::available_locales!()
|
||||||
|
}
|
||||||
|
|
||||||
fn get_locales_dir() -> Option<PathBuf> {
|
const fn fallback_language() -> &'static str {
|
||||||
dirs::app_resources_dir()
|
DEFAULT_LANGUAGE
|
||||||
.map(|resource_path| resource_path.join("locales"))
|
}
|
||||||
.ok()
|
|
||||||
|
fn locale_alias(locale: &str) -> Option<&'static str> {
|
||||||
|
match locale {
|
||||||
|
"ja" | "ja-jp" | "jp" => Some("jp"),
|
||||||
|
"zh" | "zh-cn" | "zh-hans" | "zh-sg" | "zh-my" | "zh-chs" => Some("zh"),
|
||||||
|
"zh-tw" | "zh-hk" | "zh-hant" | "zh-mo" | "zh-cht" => Some("zhtw"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_supported_language(language: &str) -> Option<String> {
|
||||||
|
if language.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = language.to_lowercase().replace('_', "-");
|
||||||
|
|
||||||
|
let mut candidates: Vec<String> = Vec::new();
|
||||||
|
let mut push_candidate = |candidate: String| {
|
||||||
|
if !candidate.is_empty()
|
||||||
|
&& !candidates
|
||||||
|
.iter()
|
||||||
|
.any(|existing| existing.eq_ignore_ascii_case(&candidate))
|
||||||
|
{
|
||||||
|
candidates.push(candidate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let segments: Vec<&str> = normalized.split('-').collect();
|
||||||
|
|
||||||
|
for i in (1..=segments.len()).rev() {
|
||||||
|
let prefix = segments[..i].join("-");
|
||||||
|
if let Some(alias) = locale_alias(&prefix) {
|
||||||
|
push_candidate(alias.to_string());
|
||||||
|
}
|
||||||
|
push_candidate(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
let supported = supported_languages_internal();
|
||||||
|
|
||||||
|
candidates.into_iter().find(|candidate| {
|
||||||
|
supported
|
||||||
|
.iter()
|
||||||
|
.any(|&lang| lang.eq_ignore_ascii_case(candidate))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn system_language() -> String {
|
||||||
|
sys_locale::get_locale()
|
||||||
|
.as_deref()
|
||||||
|
.and_then(resolve_supported_language)
|
||||||
|
.unwrap_or_else(|| fallback_language().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_supported_languages() -> Vec<String> {
|
pub fn get_supported_languages() -> Vec<String> {
|
||||||
let mut languages = Vec::new();
|
supported_languages_internal()
|
||||||
|
.into_iter()
|
||||||
|
.map(|lang| lang.to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(locales_dir) = get_locales_dir()
|
pub fn set_locale(language: &str) {
|
||||||
&& let Ok(entries) = fs::read_dir(locales_dir)
|
let lang =
|
||||||
{
|
resolve_supported_language(language).unwrap_or_else(|| fallback_language().to_string());
|
||||||
for entry in entries.flatten() {
|
rust_i18n::set_locale(&lang);
|
||||||
if let Some(file_name) = entry.file_name().to_str()
|
|
||||||
&& let Some(lang) = file_name.strip_suffix(".json")
|
|
||||||
{
|
|
||||||
languages.push(lang.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if languages.is_empty() {
|
|
||||||
languages.push(DEFAULT_LANGUAGE.into());
|
|
||||||
}
|
|
||||||
languages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn current_language() -> String {
|
pub async fn current_language() -> String {
|
||||||
@@ -45,70 +82,18 @@ pub async fn current_language() -> String {
|
|||||||
.await
|
.await
|
||||||
.latest_arc()
|
.latest_arc()
|
||||||
.language
|
.language
|
||||||
.as_deref()
|
.clone()
|
||||||
.map(String::from)
|
.filter(|lang| !lang.is_empty())
|
||||||
.unwrap_or_else(get_system_language)
|
.and_then(|lang| resolve_supported_language(&lang))
|
||||||
|
.unwrap_or_else(system_language)
|
||||||
}
|
}
|
||||||
|
|
||||||
static TRANSLATIONS: Lazy<RwLock<TranslationMap>> = Lazy::new(|| {
|
pub async fn sync_locale() -> String {
|
||||||
let lang = get_system_language();
|
let language = current_language().await;
|
||||||
let map = load_lang_file(&lang).unwrap_or_default();
|
set_locale(&language);
|
||||||
RwLock::new((lang, map))
|
language
|
||||||
});
|
|
||||||
|
|
||||||
fn load_lang_file(lang: &str) -> Option<HashMap<String, Arc<str>>> {
|
|
||||||
let locales_dir = get_locales_dir()?;
|
|
||||||
let file_path = locales_dir.join(format!("{lang}.json"));
|
|
||||||
fs::read_to_string(file_path)
|
|
||||||
.ok()
|
|
||||||
.and_then(|content| serde_json::from_str::<HashMap<String, String>>(&content).ok())
|
|
||||||
.map(|map| {
|
|
||||||
map.into_iter()
|
|
||||||
.map(|(k, v)| (k, Arc::from(v.as_str())))
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_system_language() -> String {
|
pub const fn default_language() -> &'static str {
|
||||||
sys_locale::get_locale()
|
fallback_language()
|
||||||
.map(|locale| locale.to_lowercase())
|
|
||||||
.and_then(|locale| locale.split(['_', '-']).next().map(String::from))
|
|
||||||
.filter(|lang| get_supported_languages().contains(lang))
|
|
||||||
.unwrap_or_else(|| DEFAULT_LANGUAGE.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn t(key: &str) -> Arc<str> {
|
|
||||||
let current_lang = current_language().await;
|
|
||||||
|
|
||||||
{
|
|
||||||
if let Ok(cache) = TRANSLATIONS.read()
|
|
||||||
&& cache.0 == current_lang
|
|
||||||
&& let Some(text) = cache.1.get(key)
|
|
||||||
{
|
|
||||||
return Arc::clone(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(new_map) = load_lang_file(¤t_lang)
|
|
||||||
&& let Ok(mut cache) = TRANSLATIONS.write()
|
|
||||||
{
|
|
||||||
*cache = (current_lang.clone(), new_map);
|
|
||||||
|
|
||||||
if let Some(text) = cache.1.get(key) {
|
|
||||||
return Arc::clone(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_lang != DEFAULT_LANGUAGE
|
|
||||||
&& let Some(default_map) = load_lang_file(DEFAULT_LANGUAGE)
|
|
||||||
&& let Ok(mut cache) = TRANSLATIONS.write()
|
|
||||||
{
|
|
||||||
*cache = (DEFAULT_LANGUAGE.into(), default_map);
|
|
||||||
|
|
||||||
if let Some(text) = cache.1.get(key) {
|
|
||||||
return Arc::clone(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Arc::from(key)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::{core::handle, utils::i18n::t};
|
use crate::{core::handle, utils::i18n};
|
||||||
|
|
||||||
use tauri_plugin_notification::NotificationExt;
|
use tauri_plugin_notification::NotificationExt;
|
||||||
|
|
||||||
pub enum NotificationEvent<'a> {
|
pub enum NotificationEvent<'a> {
|
||||||
@@ -27,48 +26,44 @@ fn notify(title: &str, body: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn notify_event<'a>(event: NotificationEvent<'a>) {
|
pub async fn notify_event<'a>(event: NotificationEvent<'a>) {
|
||||||
|
i18n::sync_locale().await;
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
NotificationEvent::DashboardToggled => {
|
NotificationEvent::DashboardToggled => {
|
||||||
notify(
|
let title = rust_i18n::t!("notifications.dashboardToggled.title").to_string();
|
||||||
&t("DashboardToggledTitle").await,
|
let body = rust_i18n::t!("notifications.dashboardToggled.body").to_string();
|
||||||
&t("DashboardToggledBody").await,
|
notify(&title, &body);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
NotificationEvent::ClashModeChanged { mode } => {
|
NotificationEvent::ClashModeChanged { mode } => {
|
||||||
notify(
|
let title = rust_i18n::t!("notifications.clashModeChanged.title").to_string();
|
||||||
&t("ClashModeChangedTitle").await,
|
let body = rust_i18n::t!("notifications.clashModeChanged.body").replace("{mode}", mode);
|
||||||
&t_with_args("ClashModeChangedBody", mode).await,
|
notify(&title, &body);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
NotificationEvent::SystemProxyToggled => {
|
NotificationEvent::SystemProxyToggled => {
|
||||||
notify(
|
let title = rust_i18n::t!("notifications.systemProxyToggled.title").to_string();
|
||||||
&t("SystemProxyToggledTitle").await,
|
let body = rust_i18n::t!("notifications.systemProxyToggled.body").to_string();
|
||||||
&t("SystemProxyToggledBody").await,
|
notify(&title, &body);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
NotificationEvent::TunModeToggled => {
|
NotificationEvent::TunModeToggled => {
|
||||||
notify(
|
let title = rust_i18n::t!("notifications.tunModeToggled.title").to_string();
|
||||||
&t("TunModeToggledTitle").await,
|
let body = rust_i18n::t!("notifications.tunModeToggled.body").to_string();
|
||||||
&t("TunModeToggledBody").await,
|
notify(&title, &body);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
NotificationEvent::LightweightModeEntered => {
|
NotificationEvent::LightweightModeEntered => {
|
||||||
notify(
|
let title = rust_i18n::t!("notifications.lightweightModeEntered.title").to_string();
|
||||||
&t("LightweightModeEnteredTitle").await,
|
let body = rust_i18n::t!("notifications.lightweightModeEntered.body").to_string();
|
||||||
&t("LightweightModeEnteredBody").await,
|
notify(&title, &body);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
NotificationEvent::AppQuit => {
|
NotificationEvent::AppQuit => {
|
||||||
notify(&t("AppQuitTitle").await, &t("AppQuitBody").await);
|
let title = rust_i18n::t!("notifications.appQuit.title").to_string();
|
||||||
|
let body = rust_i18n::t!("notifications.appQuit.body").to_string();
|
||||||
|
notify(&title, &body);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
NotificationEvent::AppHidden => {
|
NotificationEvent::AppHidden => {
|
||||||
notify(&t("AppHiddenTitle").await, &t("AppHiddenBody").await);
|
let title = rust_i18n::t!("notifications.appHidden.title").to_string();
|
||||||
|
let body = rust_i18n::t!("notifications.appHidden.body").to_string();
|
||||||
|
notify(&title, &body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数,带参数的i18n
|
|
||||||
async fn t_with_args(key: &str, mode: &str) -> String {
|
|
||||||
t(key).await.replace("{mode}", mode)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"resources": ["resources", "resources/locales/*"],
|
"resources": ["resources"],
|
||||||
"publisher": "Clash Verge Rev",
|
"publisher": "Clash Verge Rev",
|
||||||
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
|
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
|
||||||
"copyright": "GNU General Public License v3.0",
|
"copyright": "GNU General Public License v3.0",
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { CloseRounded } from "@mui/icons-material";
|
import { CloseRounded } from "@mui/icons-material";
|
||||||
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
|
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
|
||||||
import React, { useSyncExternalStore } from "react";
|
import React, { useSyncExternalStore } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
subscribeNotices,
|
subscribeNotices,
|
||||||
hideNotice,
|
hideNotice,
|
||||||
getSnapshotNotices,
|
getSnapshotNotices,
|
||||||
} from "@/services/noticeService";
|
} from "@/services/noticeService";
|
||||||
|
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||||
|
|
||||||
export const NoticeManager: React.FC = () => {
|
export const NoticeManager: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const currentNotices = useSyncExternalStore(
|
const currentNotices = useSyncExternalStore(
|
||||||
subscribeNotices,
|
subscribeNotices,
|
||||||
getSnapshotNotices,
|
getSnapshotNotices,
|
||||||
@@ -60,7 +63,61 @@ export const NoticeManager: React.FC = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{notice.message}
|
{notice.i18n
|
||||||
|
? (() => {
|
||||||
|
const params = (notice.i18n.params ?? {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
const {
|
||||||
|
prefixKey,
|
||||||
|
prefixParams,
|
||||||
|
prefix,
|
||||||
|
message,
|
||||||
|
...restParams
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const prefixKeyParams =
|
||||||
|
prefixParams &&
|
||||||
|
typeof prefixParams === "object" &&
|
||||||
|
prefixParams !== null
|
||||||
|
? (prefixParams as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const resolvedPrefix =
|
||||||
|
typeof prefixKey === "string"
|
||||||
|
? t(prefixKey as TranslationKey, {
|
||||||
|
defaultValue: prefixKey,
|
||||||
|
...(prefixKeyParams ?? {}),
|
||||||
|
...restParams,
|
||||||
|
})
|
||||||
|
: typeof prefix === "string"
|
||||||
|
? prefix
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const finalParams: Record<string, unknown> = {
|
||||||
|
...restParams,
|
||||||
|
};
|
||||||
|
if (resolvedPrefix !== undefined) {
|
||||||
|
finalParams.prefix = resolvedPrefix;
|
||||||
|
}
|
||||||
|
if (typeof message === "string") {
|
||||||
|
finalParams.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue =
|
||||||
|
resolvedPrefix && typeof message === "string"
|
||||||
|
? `${resolvedPrefix} ${message}`
|
||||||
|
: typeof message === "string"
|
||||||
|
? message
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return t(notice.i18n.key as TranslationKey, {
|
||||||
|
defaultValue,
|
||||||
|
...finalParams,
|
||||||
|
});
|
||||||
|
})()
|
||||||
|
: notice.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import { InboxRounded } from "@mui/icons-material";
|
import { InboxRounded } from "@mui/icons-material";
|
||||||
import { alpha, Box, Typography } from "@mui/material";
|
import { alpha, Box, Typography } from "@mui/material";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text?: React.ReactNode;
|
text?: ReactNode;
|
||||||
extra?: React.ReactNode;
|
textKey?: TranslationKey;
|
||||||
|
extra?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseEmpty = (props: Props) => {
|
export const BaseEmpty = ({
|
||||||
const { text = "Empty", extra } = props;
|
text,
|
||||||
|
textKey = "shared.statuses.empty",
|
||||||
|
extra,
|
||||||
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const resolvedText: ReactNode = text !== undefined ? text : t(textKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={({ palette }) => ({
|
sx={({ palette }) => ({
|
||||||
@@ -24,7 +33,7 @@ export const BaseEmpty = (props: Props) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<InboxRounded sx={{ fontSize: "4em" }} />
|
<InboxRounded sx={{ fontSize: "4em" }} />
|
||||||
<Typography sx={{ fontSize: "1.25em" }}>{t(`${text}`)}</Typography>
|
<Typography sx={{ fontSize: "1.25em" }}>{resolvedText}</Typography>
|
||||||
{extra}
|
{extra}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export const BaseSearchBox = ({
|
|||||||
if (useRegularExpression && value) {
|
if (useRegularExpression && value) {
|
||||||
const isValid = validateRegex(value);
|
const isValid = validateRegex(value);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
setErrorMessage(t("Invalid regular expression"));
|
setErrorMessage(t("shared.validation.invalidRegex"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ export const BaseSearchBox = ({
|
|||||||
} else {
|
} else {
|
||||||
const value = inputRef.current?.value ?? "";
|
const value = inputRef.current?.value ?? "";
|
||||||
if (value && !validateRegex(value)) {
|
if (value && !validateRegex(value)) {
|
||||||
setErrorMessage(t("Invalid regular expression"));
|
setErrorMessage(t("shared.validation.invalidRegex"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
@@ -173,7 +173,7 @@ export const BaseSearchBox = ({
|
|||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder={placeholder ?? t("Filter conditions")}
|
placeholder={placeholder ?? t("shared.placeholders.filter")}
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
error={!!errorMessage}
|
error={!!errorMessage}
|
||||||
@@ -182,7 +182,7 @@ export const BaseSearchBox = ({
|
|||||||
sx: { pr: 1 },
|
sx: { pr: 1 },
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<Box display="flex">
|
<Box display="flex">
|
||||||
<Tooltip title={t("Match Case")}>
|
<Tooltip title={t("shared.placeholders.matchCase")}>
|
||||||
<div>
|
<div>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
component={matchCaseIcon}
|
component={matchCaseIcon}
|
||||||
@@ -192,7 +192,7 @@ export const BaseSearchBox = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("Match Whole Word")}>
|
<Tooltip title={t("shared.placeholders.matchWholeWord")}>
|
||||||
<div>
|
<div>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
component={matchWholeWordIcon}
|
component={matchWholeWordIcon}
|
||||||
@@ -202,7 +202,7 @@ export const BaseSearchBox = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("Use Regular Expression")}>
|
<Tooltip title={t("shared.placeholders.useRegex")}>
|
||||||
<div>
|
<div>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
component={useRegularExpressionIcon}
|
component={useRegularExpressionIcon}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const BaseStyledTextField = styled((props: TextFieldProps) => {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder={t("Filter conditions")}
|
placeholder={t("shared.placeholders.filter")}
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
|||||||
<ErrorOutlineRounded sx={{ fontSize: 48, mb: 2, color: "error.main" }} />
|
<ErrorOutlineRounded sx={{ fontSize: 48, mb: 2, color: "error.main" }} />
|
||||||
|
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
{t("Traffic Statistics Error")}
|
{t("shared.feedback.errors.trafficStats")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
@@ -186,18 +186,17 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
|||||||
textAlign="center"
|
textAlign="center"
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
>
|
>
|
||||||
{t(
|
{t("shared.feedback.errors.trafficStatsDescription")}
|
||||||
"The traffic statistics component encountered an error and has been disabled to prevent crashes.",
|
|
||||||
)}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
|
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
<strong>Error:</strong> {error?.message || "Unknown error"}
|
<strong>Error:</strong>{" "}
|
||||||
|
{error instanceof Error ? error.message : "Unknown error"}
|
||||||
</Typography>
|
</Typography>
|
||||||
{retryCount > 0 && (
|
{retryCount > 0 && (
|
||||||
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
||||||
{t("Retry attempts")}: {retryCount}/{maxRetries}
|
{t("shared.labels.retryAttempts")}: {retryCount}/{maxRetries}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -211,12 +210,12 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
|||||||
onClick={onRetry}
|
onClick={onRetry}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
{t("Retry")}
|
{t("shared.actions.retry")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button variant="outlined" onClick={onRefresh} size="small">
|
<Button variant="outlined" onClick={onRefresh} size="small">
|
||||||
{t("Refresh Page")}
|
{t("shared.actions.refreshPage")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -225,7 +224,9 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
|||||||
onClick={onToggleDetails}
|
onClick={onToggleDetails}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
{showDetails ? t("Hide Details") : t("Show Details")}
|
{showDetails
|
||||||
|
? t("shared.actions.hideDetails")
|
||||||
|
: t("shared.actions.showDetails")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Box, Button, Snackbar, useTheme } from "@mui/material";
|
import { Box, Button, Snackbar, useTheme } from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { t } from "i18next";
|
|
||||||
import { useImperativeHandle, useState, type Ref } from "react";
|
import { useImperativeHandle, useState, type Ref } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { closeConnections } from "tauri-plugin-mihomo-api";
|
import { closeConnections } from "tauri-plugin-mihomo-api";
|
||||||
|
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
@@ -55,6 +55,7 @@ interface InnerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { metadata, rulePayload } = data;
|
const { metadata, rulePayload } = data;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const chains = [...data.chains].reverse().join(" / ");
|
const chains = [...data.chains].reverse().join(" / ");
|
||||||
@@ -67,34 +68,52 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
|||||||
: metadata.remoteDestination;
|
: metadata.remoteDestination;
|
||||||
|
|
||||||
const information = [
|
const information = [
|
||||||
{ label: t("Host"), value: host },
|
{ label: t("connections.components.fields.host"), value: host },
|
||||||
{ label: t("Downloaded"), value: parseTraffic(data.download).join(" ") },
|
|
||||||
{ label: t("Uploaded"), value: parseTraffic(data.upload).join(" ") },
|
|
||||||
{
|
{
|
||||||
label: t("DL Speed"),
|
label: t("shared.labels.downloaded"),
|
||||||
|
value: parseTraffic(data.download).join(" "),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("shared.labels.uploaded"),
|
||||||
|
value: parseTraffic(data.upload).join(" "),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("connections.components.fields.dlSpeed"),
|
||||||
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
|
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("UL Speed"),
|
label: t("connections.components.fields.ulSpeed"),
|
||||||
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
|
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("Chains"),
|
label: t("connections.components.fields.chains"),
|
||||||
value: chains,
|
value: chains,
|
||||||
},
|
},
|
||||||
{ label: t("Rule"), value: rule },
|
{ label: t("connections.components.fields.rule"), value: rule },
|
||||||
{
|
{
|
||||||
label: t("Process"),
|
label: t("connections.components.fields.process"),
|
||||||
value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ""}`,
|
value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ""}`,
|
||||||
},
|
},
|
||||||
{ label: t("Time"), value: dayjs(data.start).fromNow() },
|
|
||||||
{
|
{
|
||||||
label: t("Source"),
|
label: t("connections.components.fields.time"),
|
||||||
|
value: dayjs(data.start).fromNow(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("connections.components.fields.source"),
|
||||||
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||||
},
|
},
|
||||||
{ label: t("Destination"), value: Destination },
|
{
|
||||||
{ label: t("DestinationPort"), value: `${metadata.destinationPort}` },
|
label: t("connections.components.fields.destination"),
|
||||||
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
|
value: Destination,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("connections.components.fields.destinationPort"),
|
||||||
|
value: `${metadata.destinationPort}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("connections.components.fields.type"),
|
||||||
|
value: `${metadata.type}(${metadata.network})`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const onDelete = useLockFn(async () => closeConnections(data.id));
|
const onDelete = useLockFn(async () => closeConnections(data.id));
|
||||||
@@ -118,13 +137,13 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
|||||||
<Box sx={{ textAlign: "right" }}>
|
<Box sx={{ textAlign: "right" }}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
title={t("Close Connection")}
|
title={t("connections.components.actions.closeConnection")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onDelete();
|
onDelete();
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Close Connection")}
|
{t("connections.components.actions.closeConnection")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { closeConnections } from "tauri-plugin-mihomo-api";
|
import { closeConnections } from "tauri-plugin-mihomo-api";
|
||||||
|
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
@@ -33,6 +34,7 @@ export const ConnectionItem = (props: Props) => {
|
|||||||
const { value, onShowDetail } = props;
|
const { value, onShowDetail } = props;
|
||||||
|
|
||||||
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onDelete = useLockFn(async () => closeConnections(id));
|
const onDelete = useLockFn(async () => closeConnections(id));
|
||||||
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
|
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
|
||||||
@@ -42,7 +44,13 @@ export const ConnectionItem = (props: Props) => {
|
|||||||
dense
|
dense
|
||||||
sx={{ borderBottom: "1px solid var(--divider-color)" }}
|
sx={{ borderBottom: "1px solid var(--divider-color)" }}
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
<IconButton edge="end" color="inherit" onClick={onDelete}>
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
color="inherit"
|
||||||
|
onClick={onDelete}
|
||||||
|
title={t("connections.components.actions.closeConnection")}
|
||||||
|
aria-label={t("connections.components.actions.closeConnection")}
|
||||||
|
>
|
||||||
<CloseRounded />
|
<CloseRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,13 +162,13 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: "host",
|
field: "host",
|
||||||
headerName: t("Host"),
|
headerName: t("connections.components.fields.host"),
|
||||||
width: columnWidths["host"] || 220,
|
width: columnWidths["host"] || 220,
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "download",
|
field: "download",
|
||||||
headerName: t("Downloaded"),
|
headerName: t("shared.labels.downloaded"),
|
||||||
width: columnWidths["download"] || 88,
|
width: columnWidths["download"] || 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
@@ -176,7 +176,7 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "upload",
|
field: "upload",
|
||||||
headerName: t("Uploaded"),
|
headerName: t("shared.labels.uploaded"),
|
||||||
width: columnWidths["upload"] || 88,
|
width: columnWidths["upload"] || 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
@@ -184,7 +184,7 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "dlSpeed",
|
field: "dlSpeed",
|
||||||
headerName: t("DL Speed"),
|
headerName: t("connections.components.fields.dlSpeed"),
|
||||||
width: columnWidths["dlSpeed"] || 88,
|
width: columnWidths["dlSpeed"] || 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
@@ -192,7 +192,7 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "ulSpeed",
|
field: "ulSpeed",
|
||||||
headerName: t("UL Speed"),
|
headerName: t("connections.components.fields.ulSpeed"),
|
||||||
width: columnWidths["ulSpeed"] || 88,
|
width: columnWidths["ulSpeed"] || 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
@@ -200,25 +200,25 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "chains",
|
field: "chains",
|
||||||
headerName: t("Chains"),
|
headerName: t("connections.components.fields.chains"),
|
||||||
width: columnWidths["chains"] || 340,
|
width: columnWidths["chains"] || 340,
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "rule",
|
field: "rule",
|
||||||
headerName: t("Rule"),
|
headerName: t("connections.components.fields.rule"),
|
||||||
width: columnWidths["rule"] || 280,
|
width: columnWidths["rule"] || 280,
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "process",
|
field: "process",
|
||||||
headerName: t("Process"),
|
headerName: t("connections.components.fields.process"),
|
||||||
width: columnWidths["process"] || 220,
|
width: columnWidths["process"] || 220,
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "time",
|
field: "time",
|
||||||
headerName: t("Time"),
|
headerName: t("connections.components.fields.time"),
|
||||||
width: columnWidths["time"] || 120,
|
width: columnWidths["time"] || 120,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
align: "right",
|
align: "right",
|
||||||
@@ -229,19 +229,19 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "source",
|
field: "source",
|
||||||
headerName: t("Source"),
|
headerName: t("connections.components.fields.source"),
|
||||||
width: columnWidths["source"] || 200,
|
width: columnWidths["source"] || 200,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "remoteDestination",
|
field: "remoteDestination",
|
||||||
headerName: t("Destination"),
|
headerName: t("connections.components.fields.destination"),
|
||||||
width: columnWidths["remoteDestination"] || 200,
|
width: columnWidths["remoteDestination"] || 200,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "type",
|
field: "type",
|
||||||
headerName: t("Type"),
|
headerName: t("connections.components.fields.type"),
|
||||||
width: columnWidths["type"] || 160,
|
width: columnWidths["type"] || 160,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
},
|
},
|
||||||
@@ -250,7 +250,6 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
|
|
||||||
const handleColumnResize = (params: GridColumnResizeParams) => {
|
const handleColumnResize = (params: GridColumnResizeParams) => {
|
||||||
const { colDef, width } = params;
|
const { colDef, width } = params;
|
||||||
console.log("Column resize:", colDef.field, width);
|
|
||||||
setColumnWidths((prev) => ({
|
setColumnWidths((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[colDef.field]: width,
|
[colDef.field]: width,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const ClashInfoCard = () => {
|
|||||||
<Stack spacing={1.5}>
|
<Stack spacing={1.5}>
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Core Version")}
|
{t("home.components.clashInfo.fields.coreVersion")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{clashVersion || "-"}
|
{clashVersion || "-"}
|
||||||
@@ -41,7 +41,7 @@ export const ClashInfoCard = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("System Proxy Address")}
|
{t("home.components.clashInfo.fields.systemProxyAddress")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{systemProxyAddress}
|
{systemProxyAddress}
|
||||||
@@ -50,7 +50,7 @@ export const ClashInfoCard = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Mixed Port")}
|
{t("home.components.clashInfo.fields.mixedPort")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{clashConfig.mixedPort || "-"}
|
{clashConfig.mixedPort || "-"}
|
||||||
@@ -59,7 +59,7 @@ export const ClashInfoCard = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Uptime")}
|
{t("home.components.clashInfo.fields.uptime")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{formattedUptime}
|
{formattedUptime}
|
||||||
@@ -68,7 +68,7 @@ export const ClashInfoCard = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Rules Count")}
|
{t("home.components.clashInfo.fields.rulesCount")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{rules.length}
|
{rules.length}
|
||||||
@@ -87,7 +87,7 @@ export const ClashInfoCard = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("Clash Info")}
|
title={t("home.components.clashInfo.title")}
|
||||||
icon={<DeveloperBoardOutlined />}
|
icon={<DeveloperBoardOutlined />}
|
||||||
iconColor="warning"
|
iconColor="warning"
|
||||||
action={null}
|
action={null}
|
||||||
|
|||||||
@@ -12,6 +12,31 @@ import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
|||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { useAppData } from "@/providers/app-data-context";
|
import { useAppData } from "@/providers/app-data-context";
|
||||||
import { patchClashMode } from "@/services/cmds";
|
import { patchClashMode } from "@/services/cmds";
|
||||||
|
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||||
|
|
||||||
|
const CLASH_MODES = ["rule", "global", "direct"] as const;
|
||||||
|
type ClashMode = (typeof CLASH_MODES)[number];
|
||||||
|
|
||||||
|
const isClashMode = (mode: string): mode is ClashMode =>
|
||||||
|
(CLASH_MODES as readonly string[]).includes(mode);
|
||||||
|
|
||||||
|
const MODE_META: Record<
|
||||||
|
ClashMode,
|
||||||
|
{ label: TranslationKey; description: TranslationKey }
|
||||||
|
> = {
|
||||||
|
rule: {
|
||||||
|
label: "home.components.clashMode.labels.rule",
|
||||||
|
description: "home.components.clashMode.descriptions.rule",
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
label: "home.components.clashMode.labels.global",
|
||||||
|
description: "home.components.clashMode.descriptions.global",
|
||||||
|
},
|
||||||
|
direct: {
|
||||||
|
label: "home.components.clashMode.labels.direct",
|
||||||
|
description: "home.components.clashMode.descriptions.direct",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const ClashModeCard = () => {
|
export const ClashModeCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -19,19 +44,21 @@ export const ClashModeCard = () => {
|
|||||||
const { clashConfig, refreshClashConfig } = useAppData();
|
const { clashConfig, refreshClashConfig } = useAppData();
|
||||||
|
|
||||||
// 支持的模式列表
|
// 支持的模式列表
|
||||||
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
const modeList = CLASH_MODES;
|
||||||
|
|
||||||
// 直接使用API返回的模式,不维护本地状态
|
// 直接使用API返回的模式,不维护本地状态
|
||||||
const currentMode = clashConfig?.mode?.toLowerCase();
|
const currentMode = clashConfig?.mode?.toLowerCase();
|
||||||
|
const currentModeKey =
|
||||||
|
typeof currentMode === "string" && isClashMode(currentMode)
|
||||||
|
? currentMode
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const modeDescription = useMemo(() => {
|
const modeDescription = useMemo(() => {
|
||||||
if (typeof currentMode === "string" && currentMode.length > 0) {
|
if (currentModeKey) {
|
||||||
return t(
|
return t(MODE_META[currentModeKey].description);
|
||||||
`${currentMode[0].toLocaleUpperCase()}${currentMode.slice(1)} Mode Description`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return t("Core communication error");
|
return t("home.components.clashMode.errors.communication");
|
||||||
}, [currentMode, t]);
|
}, [currentModeKey, t]);
|
||||||
|
|
||||||
// 模式图标映射
|
// 模式图标映射
|
||||||
const modeIcons = useMemo(
|
const modeIcons = useMemo(
|
||||||
@@ -44,8 +71,8 @@ export const ClashModeCard = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 切换模式的处理函数
|
// 切换模式的处理函数
|
||||||
const onChangeMode = useLockFn(async (mode: string) => {
|
const onChangeMode = useLockFn(async (mode: ClashMode) => {
|
||||||
if (mode === currentMode) return;
|
if (mode === currentModeKey) return;
|
||||||
if (verge?.auto_close_connection) {
|
if (verge?.auto_close_connection) {
|
||||||
closeAllConnections();
|
closeAllConnections();
|
||||||
}
|
}
|
||||||
@@ -60,7 +87,7 @@ export const ClashModeCard = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 按钮样式
|
// 按钮样式
|
||||||
const buttonStyles = (mode: string) => ({
|
const buttonStyles = (mode: ClashMode) => ({
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
px: 2,
|
px: 2,
|
||||||
py: 1.2,
|
py: 1.2,
|
||||||
@@ -68,8 +95,8 @@ export const ClashModeCard = () => {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
gap: 1,
|
gap: 1,
|
||||||
bgcolor: mode === currentMode ? "primary.main" : "background.paper",
|
bgcolor: mode === currentModeKey ? "primary.main" : "background.paper",
|
||||||
color: mode === currentMode ? "primary.contrastText" : "text.primary",
|
color: mode === currentModeKey ? "primary.contrastText" : "text.primary",
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
transition: "all 0.2s ease-in-out",
|
transition: "all 0.2s ease-in-out",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
@@ -82,7 +109,7 @@ export const ClashModeCard = () => {
|
|||||||
transform: "translateY(1px)",
|
transform: "translateY(1px)",
|
||||||
},
|
},
|
||||||
"&::after":
|
"&::after":
|
||||||
mode === currentMode
|
mode === currentModeKey
|
||||||
? {
|
? {
|
||||||
content: '""',
|
content: '""',
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -128,7 +155,7 @@ export const ClashModeCard = () => {
|
|||||||
{modeList.map((mode) => (
|
{modeList.map((mode) => (
|
||||||
<Paper
|
<Paper
|
||||||
key={mode}
|
key={mode}
|
||||||
elevation={mode === currentMode ? 2 : 0}
|
elevation={mode === currentModeKey ? 2 : 0}
|
||||||
onClick={() => onChangeMode(mode)}
|
onClick={() => onChangeMode(mode)}
|
||||||
sx={buttonStyles(mode)}
|
sx={buttonStyles(mode)}
|
||||||
>
|
>
|
||||||
@@ -137,10 +164,10 @@ export const ClashModeCard = () => {
|
|||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: "capitalize",
|
textTransform: "capitalize",
|
||||||
fontWeight: mode === currentMode ? 600 : 400,
|
fontWeight: mode === currentModeKey ? 600 : 400,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t(mode)}
|
{t(MODE_META[mode].label)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -811,11 +811,11 @@ export const CurrentProxyCard = () => {
|
|||||||
const getSortTooltip = (): string => {
|
const getSortTooltip = (): string => {
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case 0:
|
case 0:
|
||||||
return t("Sort by default");
|
return t("proxies.page.tooltips.sortDefault");
|
||||||
case 1:
|
case 1:
|
||||||
return t("Sort by delay");
|
return t("proxies.page.tooltips.sortDelay");
|
||||||
case 2:
|
case 2:
|
||||||
return t("Sort by name");
|
return t("proxies.page.tooltips.sortName");
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -823,7 +823,7 @@ export const CurrentProxyCard = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("Current Node")}
|
title={t("home.components.currentProxy.title")}
|
||||||
icon={
|
icon={
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
@@ -840,7 +840,9 @@ export const CurrentProxyCard = () => {
|
|||||||
iconColor={currentProxy ? "primary" : undefined}
|
iconColor={currentProxy ? "primary" : undefined}
|
||||||
action={
|
action={
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
<Tooltip title={t("Delay check")}>
|
<Tooltip
|
||||||
|
title={t("home.components.currentProxy.actions.refreshDelay")}
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
@@ -868,7 +870,7 @@ export const CurrentProxyCard = () => {
|
|||||||
sx={{ borderRadius: 1.5 }}
|
sx={{ borderRadius: 1.5 }}
|
||||||
endIcon={<ChevronRight fontSize="small" />}
|
endIcon={<ChevronRight fontSize="small" />}
|
||||||
>
|
>
|
||||||
{t("Label-Proxies")}
|
{t("layout.components.navigation.tabs.proxies")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
@@ -906,7 +908,7 @@ export const CurrentProxyCard = () => {
|
|||||||
{isGlobalMode && (
|
{isGlobalMode && (
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={t("Global Mode")}
|
label={t("home.components.currentProxy.labels.globalMode")}
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ mr: 0.5 }}
|
sx={{ mr: 0.5 }}
|
||||||
/>
|
/>
|
||||||
@@ -914,7 +916,7 @@ export const CurrentProxyCard = () => {
|
|||||||
{isDirectMode && (
|
{isDirectMode && (
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={t("Direct Mode")}
|
label={t("home.components.currentProxy.labels.directMode")}
|
||||||
color="success"
|
color="success"
|
||||||
sx={{ mr: 0.5 }}
|
sx={{ mr: 0.5 }}
|
||||||
/>
|
/>
|
||||||
@@ -954,12 +956,14 @@ export const CurrentProxyCard = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
sx={{ mb: 1.5 }}
|
sx={{ mb: 1.5 }}
|
||||||
>
|
>
|
||||||
<InputLabel id="proxy-group-select-label">{t("Group")}</InputLabel>
|
<InputLabel id="proxy-group-select-label">
|
||||||
|
{t("home.components.currentProxy.labels.group")}
|
||||||
|
</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="proxy-group-select-label"
|
labelId="proxy-group-select-label"
|
||||||
value={state.selection.group}
|
value={state.selection.group}
|
||||||
onChange={handleGroupChange}
|
onChange={handleGroupChange}
|
||||||
label={t("Group")}
|
label={t("home.components.currentProxy.labels.group")}
|
||||||
disabled={isGlobalMode || isDirectMode}
|
disabled={isGlobalMode || isDirectMode}
|
||||||
>
|
>
|
||||||
{state.proxyData.groups.map((group) => (
|
{state.proxyData.groups.map((group) => (
|
||||||
@@ -972,12 +976,14 @@ export const CurrentProxyCard = () => {
|
|||||||
|
|
||||||
{/* 代理节点选择器 */}
|
{/* 代理节点选择器 */}
|
||||||
<FormControl fullWidth variant="outlined" size="small" sx={{ mb: 0 }}>
|
<FormControl fullWidth variant="outlined" size="small" sx={{ mb: 0 }}>
|
||||||
<InputLabel id="proxy-select-label">{t("Proxy")}</InputLabel>
|
<InputLabel id="proxy-select-label">
|
||||||
|
{t("home.components.currentProxy.labels.proxy")}
|
||||||
|
</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="proxy-select-label"
|
labelId="proxy-select-label"
|
||||||
value={state.selection.proxy}
|
value={state.selection.proxy}
|
||||||
onChange={handleProxyChange}
|
onChange={handleProxyChange}
|
||||||
label={t("Proxy")}
|
label={t("home.components.currentProxy.labels.proxy")}
|
||||||
disabled={isDirectMode}
|
disabled={isDirectMode}
|
||||||
renderValue={renderProxyValue}
|
renderValue={renderProxyValue}
|
||||||
MenuProps={{
|
MenuProps={{
|
||||||
@@ -1033,7 +1039,7 @@ export const CurrentProxyCard = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Box sx={{ textAlign: "center", py: 4 }}>
|
<Box sx={{ textAlign: "center", py: 4 }}>
|
||||||
<Typography variant="body1" color="text.secondary">
|
<Typography variant="body1" color="text.secondary">
|
||||||
{t("No active proxy node")}
|
{t("home.components.currentProxy.labels.noActiveNode")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -851,7 +851,9 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
|
|
||||||
// 获取时间范围文本
|
// 获取时间范围文本
|
||||||
const getTimeRangeText = useCallback(() => {
|
const getTimeRangeText = useCallback(() => {
|
||||||
return t("{{time}} Minutes", { time: timeRange });
|
return t("home.components.traffic.patterns.minutes", {
|
||||||
|
time: timeRange,
|
||||||
|
});
|
||||||
}, [timeRange, t]);
|
}, [timeRange, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -934,7 +936,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Upload")}
|
{t("home.components.traffic.legends.upload")}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -944,7 +946,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Download")}
|
{t("home.components.traffic.legends.download")}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -219,42 +219,42 @@ export const EnhancedTrafficStats = () => {
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
icon: <ArrowUpwardRounded fontSize="small" />,
|
icon: <ArrowUpwardRounded fontSize="small" />,
|
||||||
title: t("Upload Speed"),
|
title: t("home.components.traffic.metrics.uploadSpeed"),
|
||||||
value: parsedData.up,
|
value: parsedData.up,
|
||||||
unit: `${parsedData.upUnit}/s`,
|
unit: `${parsedData.upUnit}/s`,
|
||||||
color: "secondary" as const,
|
color: "secondary" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ArrowDownwardRounded fontSize="small" />,
|
icon: <ArrowDownwardRounded fontSize="small" />,
|
||||||
title: t("Download Speed"),
|
title: t("home.components.traffic.metrics.downloadSpeed"),
|
||||||
value: parsedData.down,
|
value: parsedData.down,
|
||||||
unit: `${parsedData.downUnit}/s`,
|
unit: `${parsedData.downUnit}/s`,
|
||||||
color: "primary" as const,
|
color: "primary" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <LinkRounded fontSize="small" />,
|
icon: <LinkRounded fontSize="small" />,
|
||||||
title: t("Active Connections"),
|
title: t("home.components.traffic.metrics.activeConnections"),
|
||||||
value: parsedData.connectionsCount,
|
value: parsedData.connectionsCount,
|
||||||
unit: "",
|
unit: "",
|
||||||
color: "success" as const,
|
color: "success" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <CloudUploadRounded fontSize="small" />,
|
icon: <CloudUploadRounded fontSize="small" />,
|
||||||
title: t("Uploaded"),
|
title: t("shared.labels.uploaded"),
|
||||||
value: parsedData.uploadTotal,
|
value: parsedData.uploadTotal,
|
||||||
unit: parsedData.uploadTotalUnit,
|
unit: parsedData.uploadTotalUnit,
|
||||||
color: "secondary" as const,
|
color: "secondary" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <CloudDownloadRounded fontSize="small" />,
|
icon: <CloudDownloadRounded fontSize="small" />,
|
||||||
title: t("Downloaded"),
|
title: t("shared.labels.downloaded"),
|
||||||
value: parsedData.downloadTotal,
|
value: parsedData.downloadTotal,
|
||||||
unit: parsedData.downloadTotalUnit,
|
unit: parsedData.downloadTotalUnit,
|
||||||
color: "primary" as const,
|
color: "primary" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <MemoryRounded fontSize="small" />,
|
icon: <MemoryRounded fontSize="small" />,
|
||||||
title: t("Memory Usage"),
|
title: t("home.components.traffic.metrics.memoryUsage"),
|
||||||
value: parsedData.inuse,
|
value: parsedData.inuse,
|
||||||
unit: parsedData.inuseUnit,
|
unit: parsedData.inuseUnit,
|
||||||
color: "error" as const,
|
color: "error" as const,
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ const ProfileDetails = ({
|
|||||||
noWrap
|
noWrap
|
||||||
sx={{ display: "flex", alignItems: "center" }}
|
sx={{ display: "flex", alignItems: "center" }}
|
||||||
>
|
>
|
||||||
<span style={{ flexShrink: 0 }}>{t("From")}: </span>
|
<span style={{ flexShrink: 0 }}>{t("shared.labels.from")}: </span>
|
||||||
{current.home ? (
|
{current.home ? (
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
@@ -186,7 +186,7 @@ const ProfileDetails = ({
|
|||||||
sx={{ cursor: "pointer" }}
|
sx={{ cursor: "pointer" }}
|
||||||
onClick={onUpdateProfile}
|
onClick={onUpdateProfile}
|
||||||
>
|
>
|
||||||
{t("Update Time")}:{" "}
|
{t("shared.labels.updateTime")}:{" "}
|
||||||
<Box component="span" fontWeight="medium">
|
<Box component="span" fontWeight="medium">
|
||||||
{dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")}
|
{dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -199,7 +199,7 @@ const ProfileDetails = ({
|
|||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<SpeedOutlined fontSize="small" color="action" />
|
<SpeedOutlined fontSize="small" color="action" />
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Used / Total")}:{" "}
|
{t("shared.labels.usedTotal")}:{" "}
|
||||||
<Box component="span" fontWeight="medium">
|
<Box component="span" fontWeight="medium">
|
||||||
{parseTraffic(usedTraffic)} /{" "}
|
{parseTraffic(usedTraffic)} /{" "}
|
||||||
{parseTraffic(current.extra.total)}
|
{parseTraffic(current.extra.total)}
|
||||||
@@ -211,7 +211,7 @@ const ProfileDetails = ({
|
|||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<EventOutlined fontSize="small" color="action" />
|
<EventOutlined fontSize="small" color="action" />
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Expire Time")}:{" "}
|
{t("shared.labels.expireTime")}:{" "}
|
||||||
<Box component="span" fontWeight="medium">
|
<Box component="span" fontWeight="medium">
|
||||||
{parseExpire(current.extra.expire)}
|
{parseExpire(current.extra.expire)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -266,10 +266,10 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
|||||||
sx={{ fontSize: 60, color: "primary.main", mb: 2 }}
|
sx={{ fontSize: 60, color: "primary.main", mb: 2 }}
|
||||||
/>
|
/>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
{t("Import")} {t("Profiles")}
|
{t("profiles.page.actions.import")} {t("profiles.page.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Click to import subscription")}
|
{t("profiles.components.card.labels.clickToImport")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -296,8 +296,8 @@ export const HomeProfileCard = ({
|
|||||||
|
|
||||||
// 刷新首页数据
|
// 刷新首页数据
|
||||||
refreshAll();
|
refreshAll();
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString(), 3000);
|
showNotice.error(err, 3000);
|
||||||
} finally {
|
} finally {
|
||||||
setUpdating(false);
|
setUpdating(false);
|
||||||
}
|
}
|
||||||
@@ -310,7 +310,7 @@ export const HomeProfileCard = ({
|
|||||||
|
|
||||||
// 卡片标题
|
// 卡片标题
|
||||||
const cardTitle = useMemo(() => {
|
const cardTitle = useMemo(() => {
|
||||||
if (!current) return t("Profiles");
|
if (!current) return t("profiles.page.title");
|
||||||
|
|
||||||
if (!current.home) return current.name;
|
if (!current.home) return current.name;
|
||||||
|
|
||||||
@@ -363,7 +363,7 @@ export const HomeProfileCard = ({
|
|||||||
endIcon={<StorageOutlined fontSize="small" />}
|
endIcon={<StorageOutlined fontSize="small" />}
|
||||||
sx={{ borderRadius: 1.5 }}
|
sx={{ borderRadius: 1.5 }}
|
||||||
>
|
>
|
||||||
{t("Label-Profiles")}
|
{t("layout.components.navigation.tabs.proxies")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}, [current, goToProfiles, t]);
|
}, [current, goToProfiles, t]);
|
||||||
|
|||||||
@@ -68,8 +68,12 @@ export const IpInfoCard = () => {
|
|||||||
const data = await getIpInfo();
|
const data = await getIpInfo();
|
||||||
setIpInfo(data);
|
setIpInfo(data);
|
||||||
setCountdown(IP_REFRESH_SECONDS);
|
setCountdown(IP_REFRESH_SECONDS);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
setError(err.message || t("Failed to get IP info"));
|
setError(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: t("home.components.ipInfo.errors.load"),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -114,7 +118,7 @@ export const IpInfoCard = () => {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("IP Information")}
|
title={t("home.components.ipInfo.title")}
|
||||||
icon={<LocationOnOutlined />}
|
icon={<LocationOnOutlined />}
|
||||||
iconColor="info"
|
iconColor="info"
|
||||||
action={
|
action={
|
||||||
@@ -137,7 +141,7 @@ export const IpInfoCard = () => {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("IP Information")}
|
title={t("home.components.ipInfo.title")}
|
||||||
icon={<LocationOnOutlined />}
|
icon={<LocationOnOutlined />}
|
||||||
iconColor="info"
|
iconColor="info"
|
||||||
action={
|
action={
|
||||||
@@ -160,7 +164,7 @@ export const IpInfoCard = () => {
|
|||||||
{error}
|
{error}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button onClick={fetchIpInfo} sx={{ mt: 2 }}>
|
<Button onClick={fetchIpInfo} sx={{ mt: 2 }}>
|
||||||
{t("Retry")}
|
{t("shared.actions.retry")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
@@ -170,7 +174,7 @@ export const IpInfoCard = () => {
|
|||||||
// 渲染正常数据
|
// 渲染正常数据
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("IP Information")}
|
title={t("home.components.ipInfo.title")}
|
||||||
icon={<LocationOnOutlined />}
|
icon={<LocationOnOutlined />}
|
||||||
iconColor="info"
|
iconColor="info"
|
||||||
action={
|
action={
|
||||||
@@ -222,7 +226,7 @@ export const IpInfoCard = () => {
|
|||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ipInfo?.country || t("Unknown")}
|
{ipInfo?.country || t("home.components.ipInfo.labels.unknown")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -232,7 +236,7 @@ export const IpInfoCard = () => {
|
|||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ flexShrink: 0 }}
|
sx={{ flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
{t("IP")}:
|
{t("home.components.ipInfo.labels.ip")}:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -266,20 +270,29 @@ export const IpInfoCard = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<InfoItem
|
<InfoItem
|
||||||
label={t("ASN")}
|
label={t("home.components.ipInfo.labels.asn")}
|
||||||
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
|
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 右侧:组织、ISP和位置信息 */}
|
{/* 右侧:组织、ISP和位置信息 */}
|
||||||
<Box sx={{ width: "60%", overflow: "auto" }}>
|
<Box sx={{ width: "60%", overflow: "auto" }}>
|
||||||
<InfoItem label={t("ISP")} value={ipInfo?.isp} />
|
|
||||||
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
|
|
||||||
<InfoItem
|
<InfoItem
|
||||||
label={t("Location")}
|
label={t("home.components.ipInfo.labels.isp")}
|
||||||
|
value={ipInfo?.isp}
|
||||||
|
/>
|
||||||
|
<InfoItem
|
||||||
|
label={t("home.components.ipInfo.labels.org")}
|
||||||
|
value={ipInfo?.asn_organization}
|
||||||
|
/>
|
||||||
|
<InfoItem
|
||||||
|
label={t("home.components.ipInfo.labels.location")}
|
||||||
value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
|
value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
|
||||||
/>
|
/>
|
||||||
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
|
<InfoItem
|
||||||
|
label={t("home.components.ipInfo.labels.timezone")}
|
||||||
|
value={ipInfo?.timezone}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -297,7 +310,7 @@ export const IpInfoCard = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="caption">
|
<Typography variant="caption">
|
||||||
{t("Auto refresh")}: {countdown}s
|
{t("home.components.ipInfo.labels.autoRefresh")}: {countdown}s
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
|
|||||||
@@ -147,8 +147,8 @@ export const ProxyTunCard: FC = () => {
|
|||||||
|
|
||||||
const { enable_tun_mode } = verge ?? {};
|
const { enable_tun_mode } = verge ?? {};
|
||||||
|
|
||||||
const handleError = (err: Error) => {
|
const handleError = (err: unknown) => {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabChange = (tab: string) => {
|
const handleTabChange = (tab: string) => {
|
||||||
@@ -160,18 +160,18 @@ export const ProxyTunCard: FC = () => {
|
|||||||
if (activeTab === "system") {
|
if (activeTab === "system") {
|
||||||
return {
|
return {
|
||||||
text: systemProxyActualState
|
text: systemProxyActualState
|
||||||
? t("System Proxy Enabled")
|
? t("home.components.proxyTun.status.systemProxyEnabled")
|
||||||
: t("System Proxy Disabled"),
|
: t("home.components.proxyTun.status.systemProxyDisabled"),
|
||||||
tooltip: t("System Proxy Info"),
|
tooltip: t("home.components.proxyTun.tooltips.systemProxy"),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
text: !isTunModeAvailable
|
text: !isTunModeAvailable
|
||||||
? t("TUN Mode Service Required")
|
? t("home.components.proxyTun.status.tunModeServiceRequired")
|
||||||
: enable_tun_mode
|
: enable_tun_mode
|
||||||
? t("TUN Mode Enabled")
|
? t("home.components.proxyTun.status.tunModeEnabled")
|
||||||
: t("TUN Mode Disabled"),
|
: t("home.components.proxyTun.status.tunModeDisabled"),
|
||||||
tooltip: t("TUN Mode Intercept Info"),
|
tooltip: t("home.components.proxyTun.tooltips.tunMode"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -198,14 +198,14 @@ export const ProxyTunCard: FC = () => {
|
|||||||
isActive={activeTab === "system"}
|
isActive={activeTab === "system"}
|
||||||
onClick={() => handleTabChange("system")}
|
onClick={() => handleTabChange("system")}
|
||||||
icon={ComputerRounded}
|
icon={ComputerRounded}
|
||||||
label={t("System Proxy")}
|
label={t("settings.sections.system.toggles.systemProxy")}
|
||||||
hasIndicator={systemProxyActualState}
|
hasIndicator={systemProxyActualState}
|
||||||
/>
|
/>
|
||||||
<TabButton
|
<TabButton
|
||||||
isActive={activeTab === "tun"}
|
isActive={activeTab === "tun"}
|
||||||
onClick={() => handleTabChange("tun")}
|
onClick={() => handleTabChange("tun")}
|
||||||
icon={TroubleshootRounded}
|
icon={TroubleshootRounded}
|
||||||
label={t("Tun Mode")}
|
label={t("settings.sections.system.toggles.tunMode")}
|
||||||
hasIndicator={enable_tun_mode && isTunModeAvailable}
|
hasIndicator={enable_tun_mode && isTunModeAvailable}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -236,7 +236,11 @@ export const ProxyTunCard: FC = () => {
|
|||||||
>
|
>
|
||||||
<ProxyControlSwitches
|
<ProxyControlSwitches
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
label={activeTab === "system" ? t("System Proxy") : t("Tun Mode")}
|
label={
|
||||||
|
activeTab === "system"
|
||||||
|
? t("settings.sections.system.toggles.systemProxy")
|
||||||
|
: t("settings.sections.system.toggles.tunMode")
|
||||||
|
}
|
||||||
noRightPadding={true}
|
noRightPadding={true}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -174,13 +174,15 @@ export const SystemInfoCard = () => {
|
|||||||
try {
|
try {
|
||||||
const info = await checkUpdate();
|
const info = await checkUpdate();
|
||||||
if (!info?.available) {
|
if (!info?.available) {
|
||||||
showNotice("success", t("Currently on the Latest Version"));
|
showNotice.success(
|
||||||
|
"settings.components.verge.advanced.notifications.latestVersion",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showNotice("info", t("Update Available"), 2000);
|
showNotice.info("shared.feedback.notifications.updateAvailable", 2000);
|
||||||
goToSettings();
|
goToSettings();
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -217,11 +219,11 @@ export const SystemInfoCard = () => {
|
|||||||
<>
|
<>
|
||||||
<AdminPanelSettingsOutlined
|
<AdminPanelSettingsOutlined
|
||||||
sx={{ color: "primary.main", fontSize: 16 }}
|
sx={{ color: "primary.main", fontSize: 16 }}
|
||||||
titleAccess={t("Administrator Mode")}
|
titleAccess={t("home.components.systemInfo.badges.adminMode")}
|
||||||
/>
|
/>
|
||||||
<DnsOutlined
|
<DnsOutlined
|
||||||
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
|
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
|
||||||
titleAccess={t("Service Mode")}
|
titleAccess={t("home.components.systemInfo.badges.serviceMode")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -229,21 +231,21 @@ export const SystemInfoCard = () => {
|
|||||||
return (
|
return (
|
||||||
<AdminPanelSettingsOutlined
|
<AdminPanelSettingsOutlined
|
||||||
sx={{ color: "primary.main", fontSize: 16 }}
|
sx={{ color: "primary.main", fontSize: 16 }}
|
||||||
titleAccess={t("Administrator Mode")}
|
titleAccess={t("home.components.systemInfo.badges.adminMode")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (isSidecarMode) {
|
} else if (isSidecarMode) {
|
||||||
return (
|
return (
|
||||||
<ExtensionOutlined
|
<ExtensionOutlined
|
||||||
sx={{ color: "info.main", fontSize: 16 }}
|
sx={{ color: "info.main", fontSize: 16 }}
|
||||||
titleAccess={t("Sidecar Mode")}
|
titleAccess={t("home.components.systemInfo.badges.sidecarMode")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<DnsOutlined
|
<DnsOutlined
|
||||||
sx={{ color: "success.main", fontSize: 16 }}
|
sx={{ color: "success.main", fontSize: 16 }}
|
||||||
titleAccess={t("Service Mode")}
|
titleAccess={t("home.components.systemInfo.badges.serviceMode")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -254,13 +256,13 @@ export const SystemInfoCard = () => {
|
|||||||
if (isAdminMode) {
|
if (isAdminMode) {
|
||||||
// 判断是否同时处于服务模式
|
// 判断是否同时处于服务模式
|
||||||
if (!isSidecarMode) {
|
if (!isSidecarMode) {
|
||||||
return t("Administrator + Service Mode");
|
return t("home.components.systemInfo.badges.adminServiceMode");
|
||||||
}
|
}
|
||||||
return t("Administrator Mode");
|
return t("home.components.systemInfo.badges.adminMode");
|
||||||
} else if (isSidecarMode) {
|
} else if (isSidecarMode) {
|
||||||
return t("Sidecar Mode");
|
return t("home.components.systemInfo.badges.sidecarMode");
|
||||||
} else {
|
} else {
|
||||||
return t("Service Mode");
|
return t("home.components.systemInfo.badges.serviceMode");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,11 +271,15 @@ export const SystemInfoCard = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("System Info")}
|
title={t("home.components.systemInfo.title")}
|
||||||
icon={<InfoOutlined />}
|
icon={<InfoOutlined />}
|
||||||
iconColor="error"
|
iconColor="error"
|
||||||
action={
|
action={
|
||||||
<IconButton size="small" onClick={goToSettings} title={t("Settings")}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={goToSettings}
|
||||||
|
title={t("home.components.systemInfo.actions.settings")}
|
||||||
|
>
|
||||||
<SettingsOutlined fontSize="small" />
|
<SettingsOutlined fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
@@ -281,7 +287,7 @@ export const SystemInfoCard = () => {
|
|||||||
<Stack spacing={1.5}>
|
<Stack spacing={1.5}>
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("OS Info")}
|
{t("home.components.systemInfo.fields.osInfo")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{systemState.osInfo}
|
{systemState.osInfo}
|
||||||
@@ -294,19 +300,23 @@ export const SystemInfoCard = () => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Auto Launch")}
|
{t("home.components.systemInfo.fields.autoLaunch")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
{isAdminMode && (
|
{isAdminMode && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={t("Administrator mode may not support auto launch")}
|
title={t("home.components.systemInfo.tooltips.autoLaunchAdmin")}
|
||||||
>
|
>
|
||||||
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
|
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={autoLaunchEnabled ? t("Enabled") : t("Disabled")}
|
label={
|
||||||
|
autoLaunchEnabled
|
||||||
|
? t("shared.statuses.enabled")
|
||||||
|
: t("shared.statuses.disabled")
|
||||||
|
}
|
||||||
color={autoLaunchEnabled ? "success" : "default"}
|
color={autoLaunchEnabled ? "success" : "default"}
|
||||||
variant={autoLaunchEnabled ? "filled" : "outlined"}
|
variant={autoLaunchEnabled ? "filled" : "outlined"}
|
||||||
onClick={toggleAutoLaunch}
|
onClick={toggleAutoLaunch}
|
||||||
@@ -321,7 +331,7 @@ export const SystemInfoCard = () => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Running Mode")}
|
{t("home.components.systemInfo.fields.runningMode")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
@@ -336,7 +346,7 @@ export const SystemInfoCard = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Last Check Update")}
|
{t("home.components.systemInfo.fields.lastCheckUpdate")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
@@ -354,7 +364,7 @@ export const SystemInfoCard = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Verge Version")}
|
{t("home.components.systemInfo.fields.vergeVersion")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
v{appVersion}
|
v{appVersion}
|
||||||
|
|||||||
@@ -173,16 +173,16 @@ export const TestCard = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("Website Tests")}
|
title={t("home.components.tests.title")}
|
||||||
icon={<NetworkCheck />}
|
icon={<NetworkCheck />}
|
||||||
action={
|
action={
|
||||||
<Box sx={{ display: "flex", gap: 1 }}>
|
<Box sx={{ display: "flex", gap: 1 }}>
|
||||||
<Tooltip title={t("Test All")} arrow>
|
<Tooltip title={t("tests.page.actions.testAll")} arrow>
|
||||||
<IconButton size="small" onClick={handleTestAll}>
|
<IconButton size="small" onClick={handleTestAll}>
|
||||||
<NetworkCheck fontSize="small" />
|
<NetworkCheck fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("Create Test")} arrow>
|
<Tooltip title={t("tests.modals.test.title.create")} arrow>
|
||||||
<IconButton size="small" onClick={handleCreateTest}>
|
<IconButton size="small" onClick={handleCreateTest}>
|
||||||
<Add fontSize="small" />
|
<Add fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const LayoutTraffic = () => {
|
|||||||
|
|
||||||
<Box display="flex" flexDirection="column" gap={0.75}>
|
<Box display="flex" flexDirection="column" gap={0.75}>
|
||||||
<Box
|
<Box
|
||||||
title={`${t("Upload Speed")}`}
|
title={`${t("home.components.traffic.metrics.uploadSpeed")}`}
|
||||||
{...boxStyle}
|
{...boxStyle}
|
||||||
sx={{
|
sx={{
|
||||||
...boxStyle.sx,
|
...boxStyle.sx,
|
||||||
@@ -105,7 +105,7 @@ export const LayoutTraffic = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
title={`${t("Download Speed")}`}
|
title={`${t("home.components.traffic.metrics.downloadSpeed")}`}
|
||||||
{...boxStyle}
|
{...boxStyle}
|
||||||
sx={{
|
sx={{
|
||||||
...boxStyle.sx,
|
...boxStyle.sx,
|
||||||
@@ -124,7 +124,7 @@ export const LayoutTraffic = () => {
|
|||||||
|
|
||||||
{displayMemory && (
|
{displayMemory && (
|
||||||
<Box
|
<Box
|
||||||
title={`${t("Memory Usage")} `}
|
title={`${t("home.components.traffic.metrics.memoryUsage")} `}
|
||||||
{...boxStyle}
|
{...boxStyle}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "auto",
|
cursor: "auto",
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ export const ConfirmViewer = (props: Props) => {
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} variant="outlined">
|
<Button onClick={onClose} variant="outlined">
|
||||||
{t("Cancel")}
|
{t("shared.actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onConfirm} variant="contained">
|
<Button onClick={onConfirm} variant="contained">
|
||||||
{t("Confirm")}
|
{t("shared.actions.confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
onClose,
|
onClose,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const resolvedTitle = title ?? t("Edit File");
|
const resolvedTitle = title ?? t("profiles.components.menu.editFile");
|
||||||
const resolvedInitialData = useMemo(
|
const resolvedInitialData = useMemo(
|
||||||
() => initialData ?? Promise.resolve(""),
|
() => initialData ?? Promise.resolve(""),
|
||||||
[initialData],
|
[initialData],
|
||||||
@@ -130,8 +130,8 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
try {
|
try {
|
||||||
currData.current = value;
|
currData.current = value;
|
||||||
onChange?.(prevData.current, currData.current);
|
onChange?.(prevData.current, currData.current);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,16 +141,16 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
onSave?.(prevData.current, currData.current);
|
onSave?.(prevData.current, currData.current);
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClose = useLockFn(async () => {
|
const handleClose = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,7 +200,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
},
|
},
|
||||||
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
|
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
|
||||||
readOnly: readOnly, // 只读模式
|
readOnly: readOnly, // 只读模式
|
||||||
readOnlyMessage: { value: t("ReadOnlyMessage") }, // 只读模式尝试编辑时的提示信息
|
readOnlyMessage: {
|
||||||
|
value: t("profiles.modals.editor.messages.readOnly"),
|
||||||
|
}, // 只读模式尝试编辑时的提示信息
|
||||||
renderValidationDecorations: "on", // 只读模式下显示校验信息
|
renderValidationDecorations: "on", // 只读模式下显示校验信息
|
||||||
quickSuggestions: {
|
quickSuggestions: {
|
||||||
strings: true, // 字符串类型的建议
|
strings: true, // 字符串类型的建议
|
||||||
@@ -229,7 +231,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
size="medium"
|
size="medium"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
sx={{ display: readOnly ? "none" : "" }}
|
sx={{ display: readOnly ? "none" : "" }}
|
||||||
title={t("Format document")}
|
title={t("profiles.modals.editor.actions.format")}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
editorRef.current
|
editorRef.current
|
||||||
?.getAction("editor.action.formatDocument")
|
?.getAction("editor.action.formatDocument")
|
||||||
@@ -241,7 +243,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="medium"
|
size="medium"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t(isMaximized ? "Minimize" : "Maximize")}
|
title={t(
|
||||||
|
isMaximized ? "shared.window.minimize" : "shared.window.maximize",
|
||||||
|
)}
|
||||||
onClick={() => appWindow.toggleMaximize().then(editorResize)}
|
onClick={() => appWindow.toggleMaximize().then(editorResize)}
|
||||||
>
|
>
|
||||||
{isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
|
{isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
|
||||||
@@ -251,11 +255,11 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} variant="outlined">
|
<Button onClick={handleClose} variant="outlined">
|
||||||
{t(readOnly ? "Close" : "Cancel")}
|
{t(readOnly ? "shared.actions.close" : "shared.actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<Button onClick={handleSave} variant="contained">
|
<Button onClick={handleSave} variant="contained">
|
||||||
{t("Save")}
|
{t("shared.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const FileInput = (props: Props) => {
|
|||||||
sx={{ flex: "none" }}
|
sx={{ flex: "none" }}
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
>
|
>
|
||||||
{t("Choose File")}
|
{t("profiles.components.fileInput.chooseFile")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
} from "@/services/cmds";
|
} from "@/services/cmds";
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import { useThemeMode } from "@/services/states";
|
import { useThemeMode } from "@/services/states";
|
||||||
|
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from "@/utils/get-system";
|
||||||
|
|
||||||
import { BaseSearchBox } from "../base/base-search-box";
|
import { BaseSearchBox } from "../base/base-search-box";
|
||||||
@@ -73,6 +74,24 @@ interface Props {
|
|||||||
|
|
||||||
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
||||||
|
|
||||||
|
const PROXY_STRATEGY_LABEL_KEYS: Record<string, TranslationKey> = {
|
||||||
|
select: "proxies.components.enums.strategies.select",
|
||||||
|
"url-test": "proxies.components.enums.strategies.url-test",
|
||||||
|
fallback: "proxies.components.enums.strategies.fallback",
|
||||||
|
"load-balance": "proxies.components.enums.strategies.load-balance",
|
||||||
|
relay: "proxies.components.enums.strategies.relay",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROXY_POLICY_LABEL_KEYS: Record<string, TranslationKey> =
|
||||||
|
builtinProxyPolicies.reduce(
|
||||||
|
(acc, policy) => {
|
||||||
|
acc[policy] =
|
||||||
|
`proxies.components.enums.policies.${policy}` as TranslationKey;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, TranslationKey>,
|
||||||
|
);
|
||||||
|
|
||||||
const normalizeDeleteSeq = (input?: unknown): string[] => {
|
const normalizeDeleteSeq = (input?: unknown): string[] => {
|
||||||
if (!Array.isArray(input)) {
|
if (!Array.isArray(input)) {
|
||||||
return [];
|
return [];
|
||||||
@@ -121,6 +140,20 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
|
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
|
||||||
props;
|
props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const translateStrategy = useCallback(
|
||||||
|
(value: string) =>
|
||||||
|
PROXY_STRATEGY_LABEL_KEYS[value]
|
||||||
|
? t(PROXY_STRATEGY_LABEL_KEYS[value])
|
||||||
|
: value,
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
const translatePolicy = useCallback(
|
||||||
|
(value: string) =>
|
||||||
|
PROXY_POLICY_LABEL_KEYS[value]
|
||||||
|
? t(PROXY_POLICY_LABEL_KEYS[value])
|
||||||
|
: value,
|
||||||
|
[t],
|
||||||
|
);
|
||||||
const themeMode = useThemeMode();
|
const themeMode = useThemeMode();
|
||||||
const [prevData, setPrevData] = useState("");
|
const [prevData, setPrevData] = useState("");
|
||||||
const [currData, setCurrData] = useState("");
|
const [currData, setCurrData] = useState("");
|
||||||
@@ -369,7 +402,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
const validateGroup = () => {
|
const validateGroup = () => {
|
||||||
const group = formIns.getValues();
|
const group = formIns.getValues();
|
||||||
if (group.name === "") {
|
if (group.name === "") {
|
||||||
throw new Error(t("Group Name Required"));
|
throw new Error(t("profiles.modals.groupsEditor.errors.nameRequired"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -384,12 +417,12 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await saveProfileFile(property, nextData);
|
await saveProfileFile(property, nextData);
|
||||||
showNotice("success", t("Saved Successfully"));
|
showNotice.success("shared.feedback.notifications.saved");
|
||||||
setPrevData(nextData);
|
setPrevData(nextData);
|
||||||
onSave?.(prevData, nextData);
|
onSave?.(prevData, nextData);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -398,7 +431,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{
|
{
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
{t("Edit Groups")}
|
{t("profiles.modals.groupsEditor.title")}
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -407,7 +440,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
setVisualization((prev) => !prev);
|
setVisualization((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{visualization ? t("Advanced") : t("Visualization")}
|
{visualization
|
||||||
|
? t("shared.editorModes.advanced")
|
||||||
|
: t("shared.editorModes.visualization")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -436,7 +471,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Group Type")} />
|
<ListItemText
|
||||||
|
primary={t("profiles.modals.groupsEditor.fields.type")}
|
||||||
|
/>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: "calc(100% - 150px)" }}
|
sx={{ width: "calc(100% - 150px)" }}
|
||||||
@@ -448,9 +485,10 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
"relay",
|
"relay",
|
||||||
]}
|
]}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
|
getOptionLabel={translateStrategy}
|
||||||
renderOption={(props, option) => (
|
renderOption={(props, option) => (
|
||||||
<li {...props} title={t(option)}>
|
<li {...props} title={translateStrategy(option)}>
|
||||||
{option}
|
{translateStrategy(option)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
onChange={(_, value) => value && field.onChange(value)}
|
onChange={(_, value) => value && field.onChange(value)}
|
||||||
@@ -464,7 +502,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Group Name")} />
|
<ListItemText
|
||||||
|
primary={t("profiles.modals.groupsEditor.fields.name")}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -481,7 +521,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Proxy Group Icon")} />
|
<ListItemText
|
||||||
|
primary={t("profiles.modals.groupsEditor.fields.icon")}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -496,7 +538,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Use Proxies")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.proxies",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -508,10 +554,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
onChange={(_, value) => value && field.onChange(value)}
|
onChange={(_, value) => value && field.onChange(value)}
|
||||||
renderInput={(params) => <TextField {...params} />}
|
renderInput={(params) => <TextField {...params} />}
|
||||||
renderOption={(props, option) => (
|
renderOption={(props, option) => (
|
||||||
<li {...props} title={t(option)}>
|
<li {...props} title={translatePolicy(option)}>
|
||||||
{option}
|
{translatePolicy(option)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
getOptionLabel={translatePolicy}
|
||||||
/>
|
/>
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
@@ -521,7 +568,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Use Provider")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.provider",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: "calc(100% - 150px)" }}
|
sx={{ width: "calc(100% - 150px)" }}
|
||||||
@@ -539,7 +590,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Health Check Url")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.healthCheckUrl",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="https://cp.cloudflare.com/generate_204"
|
placeholder="https://cp.cloudflare.com/generate_204"
|
||||||
@@ -555,7 +610,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Expected Status")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.expectedStatus",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="*"
|
placeholder="*"
|
||||||
@@ -573,7 +632,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Interval")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.interval",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="300"
|
placeholder="300"
|
||||||
@@ -587,7 +650,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
{t("seconds")}
|
{t("shared.units.seconds")}
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -601,7 +664,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Timeout")} />
|
<ListItemText primary={t("shared.labels.timeout")} />
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="5000"
|
placeholder="5000"
|
||||||
@@ -615,7 +678,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
{t("millis")}
|
{t("shared.units.milliseconds")}
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -629,7 +692,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Max Failed Times")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.maxFailedTimes",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="5"
|
placeholder="5"
|
||||||
@@ -648,7 +715,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Interface Name")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.interfaceName",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: "calc(100% - 150px)" }}
|
sx={{ width: "calc(100% - 150px)" }}
|
||||||
@@ -665,7 +736,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Routing Mark")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.routingMark",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -683,7 +758,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Filter")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.filter",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -698,7 +777,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Exclude Filter")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.excludeFilter",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -713,7 +796,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Exclude Type")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.excludeType",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
multiple
|
multiple
|
||||||
options={[
|
options={[
|
||||||
@@ -759,7 +846,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Include All")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.includeAll",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Switch checked={field.value} {...field} />
|
<Switch checked={field.value} {...field} />
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
@@ -769,7 +860,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Include All Proxies")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.includeAllProxies",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Switch checked={field.value} {...field} />
|
<Switch checked={field.value} {...field} />
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
@@ -779,7 +874,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Include All Providers")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.fields.includeAllProviders",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Switch checked={field.value} {...field} />
|
<Switch checked={field.value} {...field} />
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
@@ -789,7 +888,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Lazy")} />
|
<ListItemText
|
||||||
|
primary={t("profiles.modals.groupsEditor.toggles.lazy")}
|
||||||
|
/>
|
||||||
<Switch checked={field.value} {...field} />
|
<Switch checked={field.value} {...field} />
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
@@ -799,7 +900,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Disable UDP")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.toggles.disableUdp",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Switch checked={field.value} {...field} />
|
<Switch checked={field.value} {...field} />
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
@@ -809,7 +914,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Hidden")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"profiles.modals.groupsEditor.toggles.hidden",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Switch checked={field.value} {...field} />
|
<Switch checked={field.value} {...field} />
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
@@ -825,16 +934,18 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
validateGroup();
|
validateGroup();
|
||||||
for (const item of [...prependSeq, ...groupList]) {
|
for (const item of [...prependSeq, ...groupList]) {
|
||||||
if (item.name === formIns.getValues().name) {
|
if (item.name === formIns.getValues().name) {
|
||||||
throw new Error(t("Group Name Already Exists"));
|
throw new Error(
|
||||||
|
t("profiles.modals.groupsEditor.errors.nameExists"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setPrependSeq([formIns.getValues(), ...prependSeq]);
|
setPrependSeq([formIns.getValues(), ...prependSeq]);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Prepend Group")}
|
{t("profiles.modals.groupsEditor.actions.prepend")}
|
||||||
</Button>
|
</Button>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>
|
<Item>
|
||||||
@@ -847,16 +958,18 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
validateGroup();
|
validateGroup();
|
||||||
for (const item of [...appendSeq, ...groupList]) {
|
for (const item of [...appendSeq, ...groupList]) {
|
||||||
if (item.name === formIns.getValues().name) {
|
if (item.name === formIns.getValues().name) {
|
||||||
throw new Error(t("Group Name Already Exists"));
|
throw new Error(
|
||||||
|
t("profiles.modals.groupsEditor.errors.nameExists"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setAppendSeq([...appendSeq, formIns.getValues()]);
|
setAppendSeq([...appendSeq, formIns.getValues()]);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Append Group")}
|
{t("profiles.modals.groupsEditor.actions.append")}
|
||||||
</Button>
|
</Button>
|
||||||
</Item>
|
</Item>
|
||||||
</List>
|
</List>
|
||||||
@@ -1007,11 +1120,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} variant="outlined">
|
<Button onClick={onClose} variant="outlined">
|
||||||
{t("Cancel")}
|
{t("shared.actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={handleSave} variant="contained">
|
<Button onClick={handleSave} variant="contained">
|
||||||
{t("Save")}
|
{t("shared.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const LogViewer = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose}>
|
<Dialog open={open} onClose={onClose}>
|
||||||
<DialogTitle>{t("Script Console")}</DialogTitle>
|
<DialogTitle>{t("profiles.modals.logViewer.title")}</DialogTitle>
|
||||||
|
|
||||||
<DialogContent
|
<DialogContent
|
||||||
sx={{
|
sx={{
|
||||||
@@ -62,7 +62,7 @@ export const LogViewer = (props: Props) => {
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} variant="outlined">
|
<Button onClick={onClose} variant="outlined">
|
||||||
{t("Close")}
|
{t("shared.actions.close")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
} from "@/services/cmds";
|
} from "@/services/cmds";
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import { useLoadingCache, useSetLoadingCache } from "@/services/states";
|
import { useLoadingCache, useSetLoadingCache } from "@/services/states";
|
||||||
|
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
|
||||||
import { ProfileBox } from "./profile-box";
|
import { ProfileBox } from "./profile-box";
|
||||||
@@ -120,34 +121,46 @@ export const ProfileItem = (props: Props) => {
|
|||||||
|
|
||||||
// 如果已经过期,显示"更新失败"
|
// 如果已经过期,显示"更新失败"
|
||||||
if (nextUpdateDate.isBefore(now)) {
|
if (nextUpdateDate.isBefore(now)) {
|
||||||
setNextUpdateTime(t("Last Update failed"));
|
setNextUpdateTime(
|
||||||
|
t("profiles.components.profileItem.status.lastUpdateFailed"),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// 否则显示剩余时间
|
// 否则显示剩余时间
|
||||||
const diffMinutes = nextUpdateDate.diff(now, "minute");
|
const diffMinutes = nextUpdateDate.diff(now, "minute");
|
||||||
|
|
||||||
if (diffMinutes < 60) {
|
if (diffMinutes < 60) {
|
||||||
if (diffMinutes <= 0) {
|
if (diffMinutes <= 0) {
|
||||||
setNextUpdateTime(`${t("Next Up")} <1m`);
|
setNextUpdateTime(
|
||||||
|
`${t("profiles.components.profileItem.status.nextUp")} <1m`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setNextUpdateTime(`${t("Next Up")} ${diffMinutes}m`);
|
setNextUpdateTime(
|
||||||
|
`${t("profiles.components.profileItem.status.nextUp")} ${diffMinutes}m`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const hours = Math.floor(diffMinutes / 60);
|
const hours = Math.floor(diffMinutes / 60);
|
||||||
const mins = diffMinutes % 60;
|
const mins = diffMinutes % 60;
|
||||||
setNextUpdateTime(`${t("Next Up")} ${hours}h ${mins}m`);
|
setNextUpdateTime(
|
||||||
|
`${t("profiles.components.profileItem.status.nextUp")} ${hours}h ${mins}m`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`返回的下次更新时间为空`);
|
console.log(`返回的下次更新时间为空`);
|
||||||
setNextUpdateTime(t("No schedule"));
|
setNextUpdateTime(
|
||||||
|
t("profiles.components.profileItem.status.noSchedule"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`获取下次更新时间出错:`, err);
|
console.error(`获取下次更新时间出错:`, err);
|
||||||
setNextUpdateTime(t("Unknown"));
|
setNextUpdateTime(t("profiles.components.profileItem.status.unknown"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`该配置未设置更新间隔或间隔为0`);
|
console.log(`该配置未设置更新间隔或间隔为0`);
|
||||||
setNextUpdateTime(t("Auto update disabled"));
|
setNextUpdateTime(
|
||||||
|
t("profiles.components.profileItem.status.autoUpdateDisabled"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -312,8 +325,8 @@ export const ProfileItem = (props: Props) => {
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
try {
|
try {
|
||||||
await viewProfile(itemData.uid);
|
await viewProfile(itemData.uid);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err?.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -354,42 +367,95 @@ export const ProfileItem = (props: Props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const urlModeMenu = (
|
type ContextMenuItem = {
|
||||||
hasHome ? [{ label: "Home", handler: onOpenHome, disabled: false }] : []
|
label: string;
|
||||||
).concat([
|
handler: () => void;
|
||||||
{ label: "Select", handler: onForceSelect, disabled: false },
|
disabled: boolean;
|
||||||
{ label: "Edit Info", handler: onEditInfo, disabled: false },
|
};
|
||||||
{ label: "Edit File", handler: onEditFile, disabled: false },
|
|
||||||
|
const menuLabels: Record<string, TranslationKey> = {
|
||||||
|
home: "profiles.components.menu.home",
|
||||||
|
select: "profiles.components.menu.select",
|
||||||
|
editInfo: "profiles.components.menu.editInfo",
|
||||||
|
editFile: "profiles.components.menu.editFile",
|
||||||
|
editRules: "profiles.components.menu.editRules",
|
||||||
|
editProxies: "profiles.components.menu.editProxies",
|
||||||
|
editGroups: "profiles.components.menu.editGroups",
|
||||||
|
extendConfig: "profiles.components.menu.extendConfig",
|
||||||
|
extendScript: "profiles.components.menu.extendScript",
|
||||||
|
openFile: "profiles.components.menu.openFile",
|
||||||
|
update: "profiles.components.menu.update",
|
||||||
|
updateViaProxy: "profiles.components.menu.updateViaProxy",
|
||||||
|
delete: "shared.actions.delete",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const urlModeMenu: ContextMenuItem[] = [
|
||||||
|
...(hasHome
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: menuLabels.home,
|
||||||
|
handler: onOpenHome,
|
||||||
|
disabled: false,
|
||||||
|
} satisfies ContextMenuItem,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
label: "Edit Rules",
|
label: menuLabels.select,
|
||||||
|
handler: onForceSelect,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: menuLabels.editInfo,
|
||||||
|
handler: onEditInfo,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: menuLabels.editFile,
|
||||||
|
handler: onEditFile,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: menuLabels.editRules,
|
||||||
handler: onEditRules,
|
handler: onEditRules,
|
||||||
disabled: !option?.rules,
|
disabled: !option?.rules,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Edit Proxies",
|
label: menuLabels.editProxies,
|
||||||
handler: onEditProxies,
|
handler: onEditProxies,
|
||||||
disabled: !option?.proxies,
|
disabled: !option?.proxies,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Edit Groups",
|
label: menuLabels.editGroups,
|
||||||
handler: onEditGroups,
|
handler: onEditGroups,
|
||||||
disabled: !option?.groups,
|
disabled: !option?.groups,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Extend Config",
|
label: menuLabels.extendConfig,
|
||||||
handler: onEditMerge,
|
handler: onEditMerge,
|
||||||
disabled: !option?.merge,
|
disabled: !option?.merge,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Extend Script",
|
label: menuLabels.extendScript,
|
||||||
handler: onEditScript,
|
handler: onEditScript,
|
||||||
disabled: !option?.script,
|
disabled: !option?.script,
|
||||||
},
|
},
|
||||||
{ label: "Open File", handler: onOpenFile, disabled: false },
|
|
||||||
{ label: "Update", handler: () => onUpdate(0), disabled: false },
|
|
||||||
{ label: "Update via proxy", handler: () => onUpdate(2), disabled: false },
|
|
||||||
{
|
{
|
||||||
label: "Delete",
|
label: menuLabels.openFile,
|
||||||
|
handler: onOpenFile,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: menuLabels.update,
|
||||||
|
handler: () => onUpdate(0),
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: menuLabels.updateViaProxy,
|
||||||
|
handler: () => onUpdate(2),
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: menuLabels.delete,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
if (batchMode) {
|
if (batchMode) {
|
||||||
@@ -403,39 +469,55 @@ export const ProfileItem = (props: Props) => {
|
|||||||
},
|
},
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
const fileModeMenu = [
|
const fileModeMenu: ContextMenuItem[] = [
|
||||||
{ label: "Select", handler: onForceSelect, disabled: false },
|
|
||||||
{ label: "Edit Info", handler: onEditInfo, disabled: false },
|
|
||||||
{ label: "Edit File", handler: onEditFile, disabled: false },
|
|
||||||
{
|
{
|
||||||
label: "Edit Rules",
|
label: menuLabels.select,
|
||||||
|
handler: onForceSelect,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: menuLabels.editInfo,
|
||||||
|
handler: onEditInfo,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: menuLabels.editFile,
|
||||||
|
handler: onEditFile,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: menuLabels.editRules,
|
||||||
handler: onEditRules,
|
handler: onEditRules,
|
||||||
disabled: !option?.rules,
|
disabled: !option?.rules,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Edit Proxies",
|
label: menuLabels.editProxies,
|
||||||
handler: onEditProxies,
|
handler: onEditProxies,
|
||||||
disabled: !option?.proxies,
|
disabled: !option?.proxies,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Edit Groups",
|
label: menuLabels.editGroups,
|
||||||
handler: onEditGroups,
|
handler: onEditGroups,
|
||||||
disabled: !option?.groups,
|
disabled: !option?.groups,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Extend Config",
|
label: menuLabels.extendConfig,
|
||||||
handler: onEditMerge,
|
handler: onEditMerge,
|
||||||
disabled: !option?.merge,
|
disabled: !option?.merge,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Extend Script",
|
label: menuLabels.extendScript,
|
||||||
handler: onEditScript,
|
handler: onEditScript,
|
||||||
disabled: !option?.script,
|
disabled: !option?.script,
|
||||||
},
|
},
|
||||||
{ label: "Open File", handler: onOpenFile, disabled: false },
|
|
||||||
{
|
{
|
||||||
label: "Delete",
|
label: menuLabels.openFile,
|
||||||
|
handler: onOpenFile,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: menuLabels.delete,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
if (batchMode) {
|
if (batchMode) {
|
||||||
@@ -599,7 +681,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
{/* only if has url can it be updated */}
|
{/* only if has url can it be updated */}
|
||||||
{hasUrl && (
|
{hasUrl && (
|
||||||
<IconButton
|
<IconButton
|
||||||
title={t("Refresh")}
|
title={t("shared.actions.refresh")}
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
p: "3px",
|
p: "3px",
|
||||||
@@ -637,7 +719,10 @@ export const ProfileItem = (props: Props) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
hasUrl && (
|
hasUrl && (
|
||||||
<Typography noWrap title={`${t("From")} ${from}`}>
|
<Typography
|
||||||
|
noWrap
|
||||||
|
title={`${t("shared.labels.from")} ${from}`}
|
||||||
|
>
|
||||||
{from}
|
{from}
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)
|
||||||
@@ -657,8 +742,8 @@ export const ProfileItem = (props: Props) => {
|
|||||||
textAlign="right"
|
textAlign="right"
|
||||||
title={
|
title={
|
||||||
showNextUpdate
|
showNextUpdate
|
||||||
? t("Click to show last update time")
|
? t("profiles.components.profileItem.tooltips.showLast")
|
||||||
: `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`
|
: `${t("shared.labels.updateTime")}: ${parseExpire(updated)}\n${t("profiles.components.profileItem.tooltips.showNext")}`
|
||||||
}
|
}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
@@ -686,14 +771,16 @@ export const ProfileItem = (props: Props) => {
|
|||||||
{/* the third line show extra info or last updated time */}
|
{/* the third line show extra info or last updated time */}
|
||||||
{hasExtra ? (
|
{hasExtra ? (
|
||||||
<Box sx={{ ...boxStyle, fontSize: 14 }}>
|
<Box sx={{ ...boxStyle, fontSize: 14 }}>
|
||||||
<span title={t("Used / Total")}>
|
<span title={t("shared.labels.usedTotal")}>
|
||||||
{parseTraffic(upload + download)} / {parseTraffic(total)}
|
{parseTraffic(upload + download)} / {parseTraffic(total)}
|
||||||
</span>
|
</span>
|
||||||
<span title={t("Expire Time")}>{expire}</span>
|
<span title={t("shared.labels.expireTime")}>{expire}</span>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: "flex-end" }}>
|
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: "flex-end" }}>
|
||||||
<span title={t("Update Time")}>{parseExpire(updated)}</span>
|
<span title={t("shared.labels.updateTime")}>
|
||||||
|
{parseExpire(updated)}
|
||||||
|
</span>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
@@ -728,7 +815,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
(theme) => {
|
(theme) => {
|
||||||
return {
|
return {
|
||||||
color:
|
color:
|
||||||
item.label === "Delete"
|
item.label === menuLabels.delete
|
||||||
? theme.palette.error.main
|
? theme.palette.error.main
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
@@ -813,8 +900,8 @@ export const ProfileItem = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<ConfirmViewer
|
<ConfirmViewer
|
||||||
title={t("Confirm deletion")}
|
title={t("profiles.modals.confirmDelete.title")}
|
||||||
message={t("This operation is not reversible")}
|
message={t("profiles.modals.confirmDelete.message")}
|
||||||
open={confirmOpen}
|
open={confirmOpen}
|
||||||
onClose={() => setConfirmOpen(false)}
|
onClose={() => setConfirmOpen(false)}
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
|
|||||||
@@ -47,16 +47,26 @@ export const ProfileMore = (props: Props) => {
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
try {
|
try {
|
||||||
await viewProfile(id);
|
await viewProfile(id);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err?.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasError = entries.some(([level]) => level === "exception");
|
const hasError = entries.some(([level]) => level === "exception");
|
||||||
|
|
||||||
|
const globalTitles: Record<Props["id"], string> = {
|
||||||
|
Merge: "profiles.components.more.global.merge",
|
||||||
|
Script: "profiles.components.more.global.script",
|
||||||
|
};
|
||||||
|
|
||||||
|
const chipLabels: Record<Props["id"], string> = {
|
||||||
|
Merge: "profiles.components.more.chips.merge",
|
||||||
|
Script: "profiles.components.more.chips.script",
|
||||||
|
};
|
||||||
|
|
||||||
const itemMenu = [
|
const itemMenu = [
|
||||||
{ label: "Edit File", handler: onEditFile },
|
{ label: "profiles.components.menu.editFile", handler: onEditFile },
|
||||||
{ label: "Open File", handler: onOpenFile },
|
{ label: "profiles.components.menu.openFile", handler: onOpenFile },
|
||||||
];
|
];
|
||||||
|
|
||||||
const boxStyle = {
|
const boxStyle = {
|
||||||
@@ -89,13 +99,13 @@ export const ProfileMore = (props: Props) => {
|
|||||||
variant="h6"
|
variant="h6"
|
||||||
component="h2"
|
component="h2"
|
||||||
noWrap
|
noWrap
|
||||||
title={t(`Global ${id}`)}
|
title={t(globalTitles[id])}
|
||||||
>
|
>
|
||||||
{t(`Global ${id}`)}
|
{t(globalTitles[id])}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Chip
|
<Chip
|
||||||
label={id}
|
label={t(chipLabels[id])}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -111,7 +121,7 @@ export const ProfileMore = (props: Props) => {
|
|||||||
size="small"
|
size="small"
|
||||||
edge="start"
|
edge="start"
|
||||||
color="error"
|
color="error"
|
||||||
title={t("Script Console")}
|
title={t("profiles.modals.logViewer.title")}
|
||||||
onClick={() => setLogOpen(true)}
|
onClick={() => setLogOpen(true)}
|
||||||
>
|
>
|
||||||
<FeaturedPlayListRounded fontSize="inherit" />
|
<FeaturedPlayListRounded fontSize="inherit" />
|
||||||
@@ -122,7 +132,7 @@ export const ProfileMore = (props: Props) => {
|
|||||||
size="small"
|
size="small"
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("Script Console")}
|
title={t("profiles.modals.logViewer.title")}
|
||||||
onClick={() => setLogOpen(true)}
|
onClick={() => setLogOpen(true)}
|
||||||
>
|
>
|
||||||
<FeaturedPlayListRounded fontSize="inherit" />
|
<FeaturedPlayListRounded fontSize="inherit" />
|
||||||
@@ -170,7 +180,7 @@ export const ProfileMore = (props: Props) => {
|
|||||||
{fileOpen && (
|
{fileOpen && (
|
||||||
<EditorViewer
|
<EditorViewer
|
||||||
open={true}
|
open={true}
|
||||||
title={`${t("Global " + id)}`}
|
title={t(globalTitles[id])}
|
||||||
initialData={readProfileFile(id)}
|
initialData={readProfileFile(id)}
|
||||||
language={id === "Merge" ? "yaml" : "javascript"}
|
language={id === "Merge" ? "yaml" : "javascript"}
|
||||||
schema={id === "Merge" ? "clash" : undefined}
|
schema={id === "Merge" ? "clash" : undefined}
|
||||||
|
|||||||
@@ -144,9 +144,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 首次创建/更新失败,尝试使用自身代理
|
// 首次创建/更新失败,尝试使用自身代理
|
||||||
showNotice(
|
showNotice.info(
|
||||||
"info",
|
"profiles.modals.profileForm.feedback.notifications.creationRetry",
|
||||||
t("Profile creation failed, retrying with Clash proxy..."),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 使用自身代理的配置
|
// 使用自身代理的配置
|
||||||
@@ -170,9 +169,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
await patchProfile(form.uid, { option: originalOptions });
|
await patchProfile(form.uid, { option: originalOptions });
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotice(
|
showNotice.success(
|
||||||
"success",
|
"profiles.modals.profileForm.feedback.notifications.creationSuccess",
|
||||||
t("Profile creation succeeded with Clash proxy"),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,8 +184,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onChange(isActivating);
|
onChange(isActivating);
|
||||||
}, 0);
|
}, 0);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -220,10 +218,14 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
|
title={
|
||||||
|
openType === "new"
|
||||||
|
? t("profiles.modals.profileForm.title.create")
|
||||||
|
: t("profiles.modals.profileForm.title.edit")
|
||||||
|
}
|
||||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
||||||
okBtn={t("Save")}
|
okBtn={t("shared.actions.save")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onCancel={handleClose}
|
onCancel={handleClose}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
@@ -234,8 +236,14 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
|
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
|
||||||
<InputLabel>{t("Type")}</InputLabel>
|
<InputLabel>
|
||||||
<Select {...field} autoFocus label={t("Type")}>
|
{t("profiles.modals.profileForm.fields.type")}
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
autoFocus
|
||||||
|
label={t("profiles.modals.profileForm.fields.type")}
|
||||||
|
>
|
||||||
<MenuItem value="remote">Remote</MenuItem>
|
<MenuItem value="remote">Remote</MenuItem>
|
||||||
<MenuItem value="local">Local</MenuItem>
|
<MenuItem value="local">Local</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -247,7 +255,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField {...text} {...field} label={t("Name")} />
|
<TextField {...text} {...field} label={t("shared.labels.name")} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -255,7 +263,11 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
name="desc"
|
name="desc"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField {...text} {...field} label={t("Descriptions")} />
|
<TextField
|
||||||
|
{...text}
|
||||||
|
{...field}
|
||||||
|
label={t("profiles.modals.profileForm.fields.description")}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -269,7 +281,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
{...text}
|
{...text}
|
||||||
{...field}
|
{...field}
|
||||||
multiline
|
multiline
|
||||||
label={t("Subscription URL")}
|
label={t("profiles.modals.profileForm.fields.subscriptionUrl")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -296,12 +308,12 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="60"
|
placeholder="60"
|
||||||
label={t("HTTP Request Timeout")}
|
label={t("profiles.modals.profileForm.fields.httpTimeout")}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
{t("seconds")}
|
{t("shared.units.seconds")}
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -321,11 +333,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
{...text}
|
{...text}
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
label={t("Update Interval")}
|
label={t("profiles.modals.profileForm.fields.updateInterval")}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">{t("mins")}</InputAdornment>
|
<InputAdornment position="end">
|
||||||
|
{t("shared.units.minutes")}
|
||||||
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -350,7 +364,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<InputLabel>{t("Use System Proxy")}</InputLabel>
|
<InputLabel>
|
||||||
|
{t("profiles.modals.profileForm.fields.useSystemProxy")}
|
||||||
|
</InputLabel>
|
||||||
<Switch checked={field.value} {...field} color="primary" />
|
<Switch checked={field.value} {...field} color="primary" />
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
)}
|
)}
|
||||||
@@ -361,7 +377,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<InputLabel>{t("Use Clash Proxy")}</InputLabel>
|
<InputLabel>
|
||||||
|
{t("profiles.modals.profileForm.fields.useClashProxy")}
|
||||||
|
</InputLabel>
|
||||||
<Switch checked={field.value} {...field} color="primary" />
|
<Switch checked={field.value} {...field} color="primary" />
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
)}
|
)}
|
||||||
@@ -372,7 +390,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<InputLabel>{t("Accept Invalid Certs (Danger)")}</InputLabel>
|
<InputLabel>
|
||||||
|
{t("profiles.modals.profileForm.fields.acceptInvalidCerts")}
|
||||||
|
</InputLabel>
|
||||||
<Switch checked={field.value} {...field} color="primary" />
|
<Switch checked={field.value} {...field} color="primary" />
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
)}
|
)}
|
||||||
@@ -383,7 +403,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<InputLabel>{t("Allow Auto Update")}</InputLabel>
|
<InputLabel>
|
||||||
|
{t("profiles.modals.profileForm.fields.allowAutoUpdate")}
|
||||||
|
</InputLabel>
|
||||||
<Switch checked={field.value} {...field} color="primary" />
|
<Switch checked={field.value} {...field} color="primary" />
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -163,11 +163,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
proxies.push(proxy);
|
proxies.push(proxy);
|
||||||
names.push(proxy.name);
|
names.push(proxy.name);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[ProxiesEditorViewer] parseUri failed for line:",
|
"[ProxiesEditorViewer] parseUri failed for line:",
|
||||||
uri,
|
uri,
|
||||||
err?.message || err,
|
err,
|
||||||
);
|
);
|
||||||
// 不阻塞主流程
|
// 不阻塞主流程
|
||||||
}
|
}
|
||||||
@@ -263,11 +263,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await saveProfileFile(property, currData);
|
await saveProfileFile(property, currData);
|
||||||
showNotice("success", t("Saved Successfully"));
|
showNotice.success("shared.feedback.notifications.saved");
|
||||||
onSave?.(prevData, currData);
|
onSave?.(prevData, currData);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{
|
{
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
{t("Edit Proxies")}
|
{t("profiles.modals.proxiesEditor.title")}
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -285,7 +285,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
setVisualization((prev) => !prev);
|
setVisualization((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{visualization ? t("Advanced") : t("Visualization")}
|
{visualization
|
||||||
|
? t("shared.editorModes.advanced")
|
||||||
|
: t("shared.editorModes.visualization")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -312,7 +314,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
<Item>
|
<Item>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder={t("Use newlines for multiple uri")}
|
placeholder={t(
|
||||||
|
"profiles.modals.proxiesEditor.placeholders.multiUri",
|
||||||
|
)}
|
||||||
fullWidth
|
fullWidth
|
||||||
rows={9}
|
rows={9}
|
||||||
multiline
|
multiline
|
||||||
@@ -332,7 +336,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Prepend Proxy")}
|
{t("profiles.modals.proxiesEditor.actions.prepend")}
|
||||||
</Button>
|
</Button>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>
|
<Item>
|
||||||
@@ -346,7 +350,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Append Proxy")}
|
{t("profiles.modals.proxiesEditor.actions.append")}
|
||||||
</Button>
|
</Button>
|
||||||
</Item>
|
</Item>
|
||||||
</List>
|
</List>
|
||||||
@@ -497,11 +501,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} variant="outlined">
|
<Button onClick={onClose} variant="outlined">
|
||||||
{t("Cancel")}
|
{t("shared.actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={handleSave} variant="contained">
|
<Button onClick={handleSave} variant="contained">
|
||||||
{t("Save")}
|
{t("shared.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -240,8 +240,24 @@ const rules: {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const RULE_TYPE_LABEL_KEYS: Record<string, string> = Object.fromEntries(
|
||||||
|
rules.map((rule) => [
|
||||||
|
rule.name,
|
||||||
|
`rules.modals.editor.ruleTypes.${rule.name}`,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
||||||
|
|
||||||
|
const PROXY_POLICY_LABEL_KEYS: Record<string, string> =
|
||||||
|
builtinProxyPolicies.reduce(
|
||||||
|
(acc, policy) => {
|
||||||
|
acc[policy] = `proxy.policies.${policy}`;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
|
|
||||||
export const RulesEditorViewer = (props: Props) => {
|
export const RulesEditorViewer = (props: Props) => {
|
||||||
const { groupsUid, mergeUid, profileUid, property, open, onClose, onSave } =
|
const { groupsUid, mergeUid, profileUid, property, open, onClose, onSave } =
|
||||||
props;
|
props;
|
||||||
@@ -350,8 +366,8 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
{ forceQuotes: true },
|
{ forceQuotes: true },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (error) {
|
||||||
showNotice("error", e?.message || e?.toString() || "YAML dump error");
|
showNotice.error(error ?? "YAML dump error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let idleId: number | undefined;
|
let idleId: number | undefined;
|
||||||
@@ -462,10 +478,12 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
const validateRule = () => {
|
const validateRule = () => {
|
||||||
if ((ruleType.required ?? true) && !ruleContent) {
|
if ((ruleType.required ?? true) && !ruleContent) {
|
||||||
throw new Error(t("Rule Condition Required"));
|
throw new Error(
|
||||||
|
t("rules.modals.editor.form.validation.conditionRequired"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (ruleType.validator && !ruleType.validator(ruleContent)) {
|
if (ruleType.validator && !ruleType.validator(ruleContent)) {
|
||||||
throw new Error(t("Invalid Rule"));
|
throw new Error(t("rules.modals.editor.form.validation.invalidRule"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const condition = (ruleType.required ?? true) ? ruleContent : "";
|
const condition = (ruleType.required ?? true) ? ruleContent : "";
|
||||||
@@ -477,11 +495,11 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await saveProfileFile(property, currData);
|
await saveProfileFile(property, currData);
|
||||||
showNotice("success", t("Saved Successfully"));
|
showNotice.success("shared.feedback.notifications.saved");
|
||||||
onSave?.(prevData, currData);
|
onSave?.(prevData, currData);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice("error", err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -490,7 +508,7 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{
|
{
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
{t("Edit Rules")}
|
{t("rules.modals.editor.title")}
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -499,7 +517,9 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
setVisualization((prev) => !prev);
|
setVisualization((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{visualization ? t("Advanced") : t("Visualization")}
|
{visualization
|
||||||
|
? t("shared.editorModes.advanced")
|
||||||
|
: t("shared.editorModes.visualization")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -518,26 +538,37 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Rule Type")} />
|
<ListItemText
|
||||||
|
primary={t("rules.modals.editor.form.labels.type")}
|
||||||
|
/>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ minWidth: "240px" }}
|
sx={{ minWidth: "240px" }}
|
||||||
renderInput={(params) => <TextField {...params} />}
|
renderInput={(params) => <TextField {...params} />}
|
||||||
options={rules}
|
options={rules}
|
||||||
value={ruleType}
|
value={ruleType}
|
||||||
getOptionLabel={(option) => option.name}
|
getOptionLabel={(option) =>
|
||||||
renderOption={(props, option) => (
|
t(RULE_TYPE_LABEL_KEYS[option.name] ?? option.name)
|
||||||
<li {...props} title={t(option.name)}>
|
}
|
||||||
{option.name}
|
renderOption={(props, option) => {
|
||||||
</li>
|
const label = t(
|
||||||
)}
|
RULE_TYPE_LABEL_KEYS[option.name] ?? option.name,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<li {...props} title={label}>
|
||||||
|
{label}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}}
|
||||||
onChange={(_, value) => value && setRuleType(value)}
|
onChange={(_, value) => value && setRuleType(value)}
|
||||||
/>
|
/>
|
||||||
</Item>
|
</Item>
|
||||||
<Item
|
<Item
|
||||||
sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}
|
sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}
|
||||||
>
|
>
|
||||||
<ListItemText primary={t("Rule Content")} />
|
<ListItemText
|
||||||
|
primary={t("rules.modals.editor.form.labels.content")}
|
||||||
|
/>
|
||||||
|
|
||||||
{ruleType.name === "RULE-SET" && (
|
{ruleType.name === "RULE-SET" && (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
@@ -574,24 +605,34 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
</Item>
|
</Item>
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Proxy Policy")} />
|
<ListItemText
|
||||||
|
primary={t("rules.modals.editor.form.labels.proxyPolicy")}
|
||||||
|
/>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ minWidth: "240px" }}
|
sx={{ minWidth: "240px" }}
|
||||||
renderInput={(params) => <TextField {...params} />}
|
renderInput={(params) => <TextField {...params} />}
|
||||||
options={proxyPolicyList}
|
options={proxyPolicyList}
|
||||||
value={proxyPolicy}
|
value={proxyPolicy}
|
||||||
renderOption={(props, option) => (
|
getOptionLabel={(option) =>
|
||||||
<li {...props} title={t(option)}>
|
t(PROXY_POLICY_LABEL_KEYS[option] ?? option)
|
||||||
{option}
|
}
|
||||||
</li>
|
renderOption={(props, option) => {
|
||||||
)}
|
const label = t(PROXY_POLICY_LABEL_KEYS[option] ?? option);
|
||||||
|
return (
|
||||||
|
<li {...props} title={label}>
|
||||||
|
{label}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}}
|
||||||
onChange={(_, value) => value && setProxyPolicy(value)}
|
onChange={(_, value) => value && setProxyPolicy(value)}
|
||||||
/>
|
/>
|
||||||
</Item>
|
</Item>
|
||||||
{ruleType.noResolve && (
|
{ruleType.noResolve && (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("No Resolve")} />
|
<ListItemText
|
||||||
|
primary={t("rules.modals.editor.form.toggles.noResolve")}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
checked={noResolve}
|
checked={noResolve}
|
||||||
onChange={() => setNoResolve(!noResolve)}
|
onChange={() => setNoResolve(!noResolve)}
|
||||||
@@ -609,11 +650,11 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
if (prependSeq.includes(raw)) return;
|
if (prependSeq.includes(raw)) return;
|
||||||
setPrependSeq([raw, ...prependSeq]);
|
setPrependSeq([raw, ...prependSeq]);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Prepend Rule")}
|
{t("rules.modals.editor.form.actions.prependRule")}
|
||||||
</Button>
|
</Button>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>
|
<Item>
|
||||||
@@ -627,11 +668,11 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
if (appendSeq.includes(raw)) return;
|
if (appendSeq.includes(raw)) return;
|
||||||
setAppendSeq([...appendSeq, raw]);
|
setAppendSeq([...appendSeq, raw]);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Append Rule")}
|
{t("rules.modals.editor.form.actions.appendRule")}
|
||||||
</Button>
|
</Button>
|
||||||
</Item>
|
</Item>
|
||||||
</List>
|
</List>
|
||||||
@@ -776,11 +817,11 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} variant="outlined">
|
<Button onClick={onClose} variant="outlined">
|
||||||
{t("Cancel")}
|
{t("shared.actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={handleSave} variant="contained">
|
<Button onClick={handleSave} variant="contained">
|
||||||
{t("Save")}
|
{t("shared.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -66,12 +66,17 @@ export const ProviderButton = () => {
|
|||||||
await refreshProxy();
|
await refreshProxy();
|
||||||
await refreshProxyProviders();
|
await refreshProxyProviders();
|
||||||
|
|
||||||
showNotice("success", `${name} 更新成功`);
|
showNotice.success(
|
||||||
} catch (err: any) {
|
"proxies.feedback.notifications.provider.updateSuccess",
|
||||||
showNotice(
|
{
|
||||||
"error",
|
name,
|
||||||
`${name} 更新失败: ${err?.message || err.toString()}`,
|
},
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
showNotice.error("proxies.feedback.notifications.provider.updateFailed", {
|
||||||
|
name,
|
||||||
|
message: String(err),
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// 清除更新状态
|
// 清除更新状态
|
||||||
setUpdating((prev) => ({ ...prev, [name]: false }));
|
setUpdating((prev) => ({ ...prev, [name]: false }));
|
||||||
@@ -84,7 +89,7 @@ export const ProviderButton = () => {
|
|||||||
// 获取所有provider的名称
|
// 获取所有provider的名称
|
||||||
const allProviders = Object.keys(proxyProviders || {});
|
const allProviders = Object.keys(proxyProviders || {});
|
||||||
if (allProviders.length === 0) {
|
if (allProviders.length === 0) {
|
||||||
showNotice("info", "没有可更新的代理提供者");
|
showNotice.info("proxies.feedback.notifications.provider.none");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,9 +119,11 @@ export const ProviderButton = () => {
|
|||||||
await refreshProxy();
|
await refreshProxy();
|
||||||
await refreshProxyProviders();
|
await refreshProxyProviders();
|
||||||
|
|
||||||
showNotice("success", "全部代理提供者更新成功");
|
showNotice.success("proxies.feedback.notifications.provider.allUpdated");
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
|
showNotice.error("proxies.feedback.notifications.provider.genericError", {
|
||||||
|
message: String(err),
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// 清除所有更新状态
|
// 清除所有更新状态
|
||||||
setUpdating({});
|
setUpdating({});
|
||||||
@@ -138,7 +145,7 @@ export const ProviderButton = () => {
|
|||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
>
|
>
|
||||||
{t("Proxy Provider")}
|
{t("proxies.page.provider.title")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||||
@@ -148,14 +155,17 @@ export const ProviderButton = () => {
|
|||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Typography variant="h6">{t("Proxy Provider")}</Typography>
|
<Typography variant="h6">
|
||||||
|
{t("proxies.page.provider.title")}
|
||||||
|
</Typography>
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={updateAllProviders}
|
onClick={updateAllProviders}
|
||||||
|
aria-label={t("proxies.page.provider.actions.updateAll")}
|
||||||
>
|
>
|
||||||
{t("Update All")}
|
{t("proxies.page.provider.actions.updateAll")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -246,7 +256,7 @@ export const ProviderButton = () => {
|
|||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
noWrap
|
noWrap
|
||||||
>
|
>
|
||||||
<small>{t("Update At")}: </small>
|
<small>{t("shared.labels.updateAt")}: </small>
|
||||||
{time.fromNow()}
|
{time.fromNow()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -264,11 +274,17 @@ export const ProviderButton = () => {
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span title={t("Used / Total") as string}>
|
<span
|
||||||
|
title={t("shared.labels.usedTotal") as string}
|
||||||
|
>
|
||||||
{parseTraffic(upload + download)} /{" "}
|
{parseTraffic(upload + download)} /{" "}
|
||||||
{parseTraffic(total)}
|
{parseTraffic(total)}
|
||||||
</span>
|
</span>
|
||||||
<span title={t("Expire Time") as string}>
|
<span
|
||||||
|
title={
|
||||||
|
t("shared.labels.expireTime") as string
|
||||||
|
}
|
||||||
|
>
|
||||||
{parseExpire(expire)}
|
{parseExpire(expire)}
|
||||||
</span>
|
</span>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -313,7 +329,8 @@ export const ProviderButton = () => {
|
|||||||
"100%": { transform: "rotate(360deg)" },
|
"100%": { transform: "rotate(360deg)" },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
title={t("Update Provider") as string}
|
title={t("proxies.page.provider.actions.update")}
|
||||||
|
aria-label={t("proxies.page.provider.actions.update")}
|
||||||
>
|
>
|
||||||
<RefreshRounded />
|
<RefreshRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -326,7 +343,7 @@ export const ProviderButton = () => {
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} variant="outlined">
|
<Button onClick={handleClose} variant="outlined">
|
||||||
{t("Close")}
|
{t("shared.actions.close")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -156,7 +156,11 @@ const SortableItem = ({ proxy, index, onRemove }: SortableItemProps) => {
|
|||||||
|
|
||||||
{proxy.delay !== undefined && (
|
{proxy.delay !== undefined && (
|
||||||
<Chip
|
<Chip
|
||||||
label={proxy.delay > 0 ? `${proxy.delay}ms` : t("timeout") || "超时"}
|
label={
|
||||||
|
proxy.delay > 0
|
||||||
|
? `${proxy.delay}ms`
|
||||||
|
: t("shared.labels.timeout") || "超时"
|
||||||
|
}
|
||||||
size="small"
|
size="small"
|
||||||
color={
|
color={
|
||||||
proxy.delay > 0 && proxy.delay < 200
|
proxy.delay > 0 && proxy.delay < 200
|
||||||
@@ -299,7 +303,7 @@ export const ProxyChain = ({
|
|||||||
// onUpdateChain([]);
|
// onUpdateChain([]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to disconnect from proxy chain:", error);
|
console.error("Failed to disconnect from proxy chain:", error);
|
||||||
alert(t("Failed to disconnect from proxy chain") || "断开链式代理失败");
|
alert(t("proxies.page.chain.disconnectFailed") || "断开链式代理失败");
|
||||||
} finally {
|
} finally {
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
}
|
}
|
||||||
@@ -307,9 +311,7 @@ export const ProxyChain = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (proxyChain.length < 2) {
|
if (proxyChain.length < 2) {
|
||||||
alert(
|
alert(t("proxies.page.chain.minimumNodes") || "链式代理至少需要2个节点");
|
||||||
t("Chain proxy requires at least 2 nodes") || "链式代理至少需要2个节点",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +343,7 @@ export const ProxyChain = ({
|
|||||||
console.log("Successfully connected to proxy chain");
|
console.log("Successfully connected to proxy chain");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to connect to proxy chain:", error);
|
console.error("Failed to connect to proxy chain:", error);
|
||||||
alert(t("Failed to connect to proxy chain") || "连接链式代理失败");
|
alert(t("proxies.page.chain.connectFailed") || "连接链式代理失败");
|
||||||
} finally {
|
} finally {
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
}
|
}
|
||||||
@@ -471,7 +473,7 @@ export const ProxyChain = ({
|
|||||||
mb: 2,
|
mb: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6">{t("Chain Proxy Config")}</Typography>
|
<Typography variant="h6">{t("proxies.page.chain.header")}</Typography>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
{proxyChain.length > 0 && (
|
{proxyChain.length > 0 && (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -486,7 +488,9 @@ export const ProxyChain = ({
|
|||||||
backgroundColor: theme.palette.error.light + "20",
|
backgroundColor: theme.palette.error.light + "20",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
title={t("Delete Chain Config") || "删除链式配置"}
|
title={
|
||||||
|
t("proxies.page.actions.clearChainConfig") || "删除链式配置"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -507,16 +511,16 @@ export const ProxyChain = ({
|
|||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
proxyChain.length < 2
|
proxyChain.length < 2
|
||||||
? t("Chain proxy requires at least 2 nodes") ||
|
? t("proxies.page.chain.minimumNodes") ||
|
||||||
"链式代理至少需要2个节点"
|
"链式代理至少需要2个节点"
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isConnecting
|
{isConnecting
|
||||||
? t("Connecting...") || "连接中..."
|
? t("proxies.page.actions.connecting") || "连接中..."
|
||||||
: isConnected
|
: isConnected
|
||||||
? t("Disconnect") || "断开"
|
? t("proxies.page.actions.disconnect") || "断开"
|
||||||
: t("Connect") || "连接"}
|
: t("proxies.page.actions.connect") || "连接"}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -526,10 +530,9 @@ export const ProxyChain = ({
|
|||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
>
|
>
|
||||||
{proxyChain.length === 1
|
{proxyChain.length === 1
|
||||||
? t(
|
? t("proxies.page.chain.minimumNodesHint") ||
|
||||||
"Chain proxy requires at least 2 nodes. Please add one more node.",
|
"链式代理至少需要2个节点,请再添加一个节点。"
|
||||||
) || "链式代理至少需要2个节点,请再添加一个节点。"
|
: t("proxies.page.chain.instruction") ||
|
||||||
: t("Click nodes in order to add to proxy chain") ||
|
|
||||||
"按顺序点击节点添加到代理链中"}
|
"按顺序点击节点添加到代理链中"}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
@@ -544,7 +547,7 @@ export const ProxyChain = ({
|
|||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography>{t("No proxy chain configured")}</Typography>
|
<Typography>{t("proxies.page.chain.empty")}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<DndContext
|
<DndContext
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
setProxyChain((prev) => {
|
setProxyChain((prev) => {
|
||||||
// 检查是否已经存在相同名称的代理,防止重复添加
|
// 检查是否已经存在相同名称的代理,防止重复添加
|
||||||
if (prev.some((item) => item.name === proxy.name)) {
|
if (prev.some((item) => item.name === proxy.name)) {
|
||||||
const warningMessage = t("Proxy node already exists in chain");
|
const warningMessage = t("proxies.page.chain.duplicateNode");
|
||||||
setDuplicateWarning({
|
setDuplicateWarning({
|
||||||
open: true,
|
open: true,
|
||||||
message: warningMessage,
|
message: warningMessage,
|
||||||
@@ -372,7 +372,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
}, [renderList]);
|
}, [renderList]);
|
||||||
|
|
||||||
if (mode === "direct") {
|
if (mode === "direct") {
|
||||||
return <BaseEmpty text={t("clash_mode_direct")} />;
|
return <BaseEmpty textKey="proxies.page.messages.directMode" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChainMode) {
|
if (isChainMode) {
|
||||||
@@ -403,7 +403,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
variant="h6"
|
variant="h6"
|
||||||
sx={{ fontWeight: 600, fontSize: "16px" }}
|
sx={{ fontWeight: 600, fontSize: "16px" }}
|
||||||
>
|
>
|
||||||
{t("Proxy Rules")}
|
{t("proxies.page.rules.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
{currentGroup && (
|
{currentGroup && (
|
||||||
<Box
|
<Box
|
||||||
@@ -442,7 +442,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ mr: 0.5, fontSize: "12px" }}
|
sx={{ mr: 0.5, fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
{t("Select Rules")}
|
{t("proxies.page.rules.select")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<ExpandMoreRounded fontSize="small" />
|
<ExpandMoreRounded fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const ProxyHead = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("locate")}
|
title={t("proxies.page.tooltips.locate")}
|
||||||
onClick={onLocation}
|
onClick={onLocation}
|
||||||
>
|
>
|
||||||
<MyLocationRounded />
|
<MyLocationRounded />
|
||||||
@@ -76,7 +76,7 @@ export const ProxyHead = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("Delay check")}
|
title={t("proxies.page.tooltips.delayCheck")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log(`[ProxyHead] 点击延迟测试按钮,组: ${groupName}`);
|
console.log(`[ProxyHead] 点击延迟测试按钮,组: ${groupName}`);
|
||||||
// Remind the user that it is custom test url
|
// Remind the user that it is custom test url
|
||||||
@@ -94,9 +94,11 @@ export const ProxyHead = ({
|
|||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={
|
title={
|
||||||
[t("Sort by default"), t("Sort by delay"), t("Sort by name")][
|
[
|
||||||
sortType
|
t("proxies.page.tooltips.sortDefault"),
|
||||||
]
|
t("proxies.page.tooltips.sortDelay"),
|
||||||
|
t("proxies.page.tooltips.sortName"),
|
||||||
|
][sortType]
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onHeadState({ sortType: ((sortType + 1) % 3) as ProxySortType })
|
onHeadState({ sortType: ((sortType + 1) % 3) as ProxySortType })
|
||||||
@@ -110,7 +112,7 @@ export const ProxyHead = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("Delay check URL")}
|
title={t("proxies.page.tooltips.delayCheckUrl")}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onHeadState({ textState: textState === "url" ? null : "url" })
|
onHeadState({ textState: textState === "url" ? null : "url" })
|
||||||
}
|
}
|
||||||
@@ -125,7 +127,11 @@ export const ProxyHead = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={showType ? t("Proxy basic") : t("Proxy detail")}
|
title={
|
||||||
|
showType
|
||||||
|
? t("proxies.page.tooltips.showBasic")
|
||||||
|
: t("proxies.page.tooltips.showDetail")
|
||||||
|
}
|
||||||
onClick={() => onHeadState({ showType: !showType })}
|
onClick={() => onHeadState({ showType: !showType })}
|
||||||
>
|
>
|
||||||
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
|
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
|
||||||
@@ -134,7 +140,7 @@ export const ProxyHead = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("Filter")}
|
title={t("proxies.page.tooltips.filter")}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onHeadState({ textState: textState === "filter" ? null : "filter" })
|
onHeadState({ textState: textState === "filter" ? null : "filter" })
|
||||||
}
|
}
|
||||||
@@ -154,7 +160,7 @@ export const ProxyHead = ({
|
|||||||
value={filterText}
|
value={filterText}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder={t("Filter conditions")}
|
placeholder={t("shared.placeholders.filter")}
|
||||||
onChange={(e) => onHeadState({ filterText: e.target.value })}
|
onChange={(e) => onHeadState({ filterText: e.target.value })}
|
||||||
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
|
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
|
||||||
/>
|
/>
|
||||||
@@ -169,7 +175,7 @@ export const ProxyHead = ({
|
|||||||
value={testUrl}
|
value={testUrl}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder={t("Delay check URL")}
|
placeholder={t("proxies.page.placeholders.delayCheckUrl")}
|
||||||
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
||||||
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
|
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -271,7 +271,9 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
<span
|
<span
|
||||||
className={proxy.name === group.now ? "the-pin" : "the-unpin"}
|
className={proxy.name === group.now ? "the-pin" : "the-unpin"}
|
||||||
title={
|
title={
|
||||||
group.type === "URLTest" ? t("Delay check to cancel fixed") : ""
|
group.type === "URLTest"
|
||||||
|
? t("proxies.page.labels.delayCheckReset")
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
📌
|
📌
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
<Tooltip title={t("Proxy Count")} arrow>
|
<Tooltip title={t("proxies.page.labels.proxyCount")} arrow>
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={`${group.all.length}`}
|
label={`${group.all.length}`}
|
||||||
|
|||||||
@@ -58,12 +58,17 @@ export const ProviderButton = () => {
|
|||||||
await refreshRules();
|
await refreshRules();
|
||||||
await refreshRuleProviders();
|
await refreshRuleProviders();
|
||||||
|
|
||||||
showNotice("success", `${name} 更新成功`);
|
showNotice.success(
|
||||||
} catch (err: any) {
|
"rules.feedback.notifications.provider.updateSuccess",
|
||||||
showNotice(
|
{
|
||||||
"error",
|
name,
|
||||||
`${name} 更新失败: ${err?.message || err.toString()}`,
|
},
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
showNotice.error("rules.feedback.notifications.provider.updateFailed", {
|
||||||
|
name,
|
||||||
|
message: String(err),
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// 清除更新状态
|
// 清除更新状态
|
||||||
setUpdating((prev) => ({ ...prev, [name]: false }));
|
setUpdating((prev) => ({ ...prev, [name]: false }));
|
||||||
@@ -76,7 +81,7 @@ export const ProviderButton = () => {
|
|||||||
// 获取所有provider的名称
|
// 获取所有provider的名称
|
||||||
const allProviders = Object.keys(ruleProviders || {});
|
const allProviders = Object.keys(ruleProviders || {});
|
||||||
if (allProviders.length === 0) {
|
if (allProviders.length === 0) {
|
||||||
showNotice("info", "没有可更新的规则提供者");
|
showNotice.info("rules.feedback.notifications.provider.none");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,9 +111,11 @@ export const ProviderButton = () => {
|
|||||||
await refreshRules();
|
await refreshRules();
|
||||||
await refreshRuleProviders();
|
await refreshRuleProviders();
|
||||||
|
|
||||||
showNotice("success", "全部规则提供者更新成功");
|
showNotice.success("rules.feedback.notifications.provider.allUpdated");
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
|
showNotice.error("rules.feedback.notifications.provider.genericError", {
|
||||||
|
message: String(err),
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// 清除所有更新状态
|
// 清除所有更新状态
|
||||||
setUpdating({});
|
setUpdating({});
|
||||||
@@ -129,7 +136,7 @@ export const ProviderButton = () => {
|
|||||||
startIcon={<StorageOutlined />}
|
startIcon={<StorageOutlined />}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
{t("Rule Provider")}
|
{t("rules.page.provider.trigger")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||||
@@ -139,13 +146,15 @@ export const ProviderButton = () => {
|
|||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Typography variant="h6">{t("Rule Providers")}</Typography>
|
<Typography variant="h6">
|
||||||
|
{t("rules.page.provider.dialogTitle")}
|
||||||
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={updateAllProviders}
|
onClick={updateAllProviders}
|
||||||
>
|
>
|
||||||
{t("Update All")}
|
{t("rules.page.provider.actions.updateAll")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
@@ -216,7 +225,7 @@ export const ProviderButton = () => {
|
|||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
noWrap
|
noWrap
|
||||||
>
|
>
|
||||||
<small>{t("Update At")}: </small>
|
<small>{t("shared.labels.updateAt")}: </small>
|
||||||
{time.fromNow()}
|
{time.fromNow()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -246,6 +255,7 @@ export const ProviderButton = () => {
|
|||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => updateProvider(key)}
|
onClick={() => updateProvider(key)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
|
aria-label={t("rules.page.provider.actions.update")}
|
||||||
sx={{
|
sx={{
|
||||||
animation: isUpdating
|
animation: isUpdating
|
||||||
? "spin 1s linear infinite"
|
? "spin 1s linear infinite"
|
||||||
@@ -255,7 +265,7 @@ export const ProviderButton = () => {
|
|||||||
"100%": { transform: "rotate(360deg)" },
|
"100%": { transform: "rotate(360deg)" },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
title={t("Update Provider") as string}
|
title={t("rules.page.provider.actions.update")}
|
||||||
>
|
>
|
||||||
<RefreshRounded />
|
<RefreshRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -268,7 +278,7 @@ export const ProviderButton = () => {
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} variant="outlined">
|
<Button onClick={handleClose} variant="outlined">
|
||||||
{t("Close")}
|
{t("shared.actions.close")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -84,22 +84,22 @@ export const BackupConfigViewer = memo(
|
|||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
urlRef.current?.focus();
|
urlRef.current?.focus();
|
||||||
showNotice("error", t("WebDAV URL Required"));
|
showNotice.error("settings.modals.backup.messages.webdavUrlRequired");
|
||||||
throw new Error(t("WebDAV URL Required"));
|
throw new Error(t("settings.modals.backup.messages.webdavUrlRequired"));
|
||||||
} else if (!isValidUrl(url)) {
|
} else if (!isValidUrl(url)) {
|
||||||
urlRef.current?.focus();
|
urlRef.current?.focus();
|
||||||
showNotice("error", t("Invalid WebDAV URL"));
|
showNotice.error("settings.modals.backup.messages.invalidWebdavUrl");
|
||||||
throw new Error(t("Invalid WebDAV URL"));
|
throw new Error(t("settings.modals.backup.messages.invalidWebdavUrl"));
|
||||||
}
|
}
|
||||||
if (!username) {
|
if (!username) {
|
||||||
usernameRef.current?.focus();
|
usernameRef.current?.focus();
|
||||||
showNotice("error", t("WebDAV URL Required"));
|
showNotice.error("settings.modals.backup.messages.usernameRequired");
|
||||||
throw new Error(t("Username Required"));
|
throw new Error(t("settings.modals.backup.messages.usernameRequired"));
|
||||||
}
|
}
|
||||||
if (!password) {
|
if (!password) {
|
||||||
passwordRef.current?.focus();
|
passwordRef.current?.focus();
|
||||||
showNotice("error", t("WebDAV URL Required"));
|
showNotice.error("settings.modals.backup.messages.passwordRequired");
|
||||||
throw new Error(t("Password Required"));
|
throw new Error(t("settings.modals.backup.messages.passwordRequired"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,11 +112,17 @@ export const BackupConfigViewer = memo(
|
|||||||
data.username.trim(),
|
data.username.trim(),
|
||||||
data.password,
|
data.password,
|
||||||
).then(() => {
|
).then(() => {
|
||||||
showNotice("success", t("WebDAV Config Saved"));
|
showNotice.success(
|
||||||
|
"settings.modals.backup.messages.webdavConfigSaved",
|
||||||
|
);
|
||||||
onSaveSuccess();
|
onSaveSuccess();
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
|
showNotice.error(
|
||||||
|
"settings.modals.backup.messages.webdavConfigSaveFailed",
|
||||||
|
{ error },
|
||||||
|
3000,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -127,11 +133,13 @@ export const BackupConfigViewer = memo(
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await createWebdavBackup().then(async () => {
|
await createWebdavBackup().then(async () => {
|
||||||
showNotice("success", t("Backup Created"));
|
showNotice.success("settings.modals.backup.messages.backupCreated");
|
||||||
await onBackupSuccess();
|
await onBackupSuccess();
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice("error", t("Backup Failed", { error }));
|
showNotice.error("settings.modals.backup.messages.backupFailed", {
|
||||||
|
error,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -145,7 +153,7 @@ export const BackupConfigViewer = memo(
|
|||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label={t("WebDAV Server URL")}
|
label={t("settings.modals.backup.fields.webdavUrl")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
{...register("url")}
|
{...register("url")}
|
||||||
@@ -157,7 +165,7 @@ export const BackupConfigViewer = memo(
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6 }}>
|
<Grid size={{ xs: 6 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label={t("Username")}
|
label={t("settings.modals.backup.fields.username")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
{...register("username")}
|
{...register("username")}
|
||||||
@@ -169,7 +177,7 @@ export const BackupConfigViewer = memo(
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6 }}>
|
<Grid size={{ xs: 6 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label={t("Password")}
|
label={t("shared.labels.password")}
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -214,7 +222,7 @@ export const BackupConfigViewer = memo(
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleSubmit(save)}
|
onClick={handleSubmit(save)}
|
||||||
>
|
>
|
||||||
{t("Save")}
|
{t("shared.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -225,7 +233,7 @@ export const BackupConfigViewer = memo(
|
|||||||
type="button"
|
type="button"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
{t("Backup")}
|
{t("settings.modals.backup.actions.backup")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -233,7 +241,7 @@ export const BackupConfigViewer = memo(
|
|||||||
type="button"
|
type="button"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
{t("Refresh")}
|
{t("shared.actions.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const BackupTableViewer = memo(
|
|||||||
|
|
||||||
const handleRestore = useLockFn(async (filename: string) => {
|
const handleRestore = useLockFn(async (filename: string) => {
|
||||||
await onRestore(filename).then(() => {
|
await onRestore(filename).then(() => {
|
||||||
showNotice("success", t("Restore Success, App will restart in 1s"));
|
showNotice.success("settings.modals.backup.messages.restoreSuccess");
|
||||||
});
|
});
|
||||||
await restartApp();
|
await restartApp();
|
||||||
});
|
});
|
||||||
@@ -92,10 +92,14 @@ export const BackupTableViewer = memo(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await onExport(filename, savePath);
|
await onExport(filename, savePath);
|
||||||
showNotice("success", t("Local Backup Exported"));
|
showNotice.success(
|
||||||
|
"settings.modals.backup.messages.localBackupExported",
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
showNotice("error", t("Local Backup Export Failed"));
|
showNotice.error(
|
||||||
|
"settings.modals.backup.messages.localBackupExportFailed",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,9 +108,15 @@ export const BackupTableViewer = memo(
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{t("Filename")}</TableCell>
|
<TableCell>
|
||||||
<TableCell>{t("Backup Time")}</TableCell>
|
{t("settings.modals.backup.table.filename")}
|
||||||
<TableCell align="right">{t("Actions")}</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{t("settings.modals.backup.table.backupTime")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{t("settings.modals.backup.table.actions")}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -140,9 +150,13 @@ export const BackupTableViewer = memo(
|
|||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
aria-label={t("Export")}
|
aria-label={t(
|
||||||
|
"settings.modals.backup.actions.export",
|
||||||
|
)}
|
||||||
size="small"
|
size="small"
|
||||||
title={t("Export Backup")}
|
title={t(
|
||||||
|
"settings.modals.backup.actions.exportBackup",
|
||||||
|
)}
|
||||||
onClick={async (e: React.MouseEvent) => {
|
onClick={async (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await handleExport(file.filename);
|
await handleExport(file.filename);
|
||||||
@@ -159,13 +173,17 @@ export const BackupTableViewer = memo(
|
|||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
aria-label={t("Delete")}
|
aria-label={t("shared.actions.delete")}
|
||||||
size="small"
|
size="small"
|
||||||
title={t("Delete Backup")}
|
title={t(
|
||||||
|
"settings.modals.backup.actions.deleteBackup",
|
||||||
|
)}
|
||||||
onClick={async (e: React.MouseEvent) => {
|
onClick={async (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const confirmed = await confirmAsync(
|
const confirmed = await confirmAsync(
|
||||||
t("Confirm to delete this backup file?"),
|
t(
|
||||||
|
"settings.modals.backup.messages.confirmDelete",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
await handleDelete(file.filename);
|
await handleDelete(file.filename);
|
||||||
@@ -181,14 +199,20 @@ export const BackupTableViewer = memo(
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
aria-label={t("Restore")}
|
aria-label={t(
|
||||||
|
"settings.modals.backup.actions.restore",
|
||||||
|
)}
|
||||||
size="small"
|
size="small"
|
||||||
title={t("Restore Backup")}
|
title={t(
|
||||||
|
"settings.modals.backup.actions.restoreBackup",
|
||||||
|
)}
|
||||||
disabled={!file.allow_apply}
|
disabled={!file.allow_apply}
|
||||||
onClick={async (e: React.MouseEvent) => {
|
onClick={async (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const confirmed = await confirmAsync(
|
const confirmed = await confirmAsync(
|
||||||
t("Confirm to restore this backup file?"),
|
t(
|
||||||
|
"settings.modals.backup.messages.confirmRestore",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
await handleRestore(file.filename);
|
await handleRestore(file.filename);
|
||||||
@@ -219,7 +243,7 @@ export const BackupTableViewer = memo(
|
|||||||
color="textSecondary"
|
color="textSecondary"
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
{t("No Backups")}
|
{t("settings.modals.backup.table.noBackups")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -234,7 +258,7 @@ export const BackupTableViewer = memo(
|
|||||||
rowsPerPage={DEFAULT_ROWS_PER_PAGE}
|
rowsPerPage={DEFAULT_ROWS_PER_PAGE}
|
||||||
page={page}
|
page={page}
|
||||||
onPageChange={onPageChange}
|
onPageChange={onPageChange}
|
||||||
labelRowsPerPage={t("Rows per page")}
|
labelRowsPerPage={t("settings.modals.backup.table.rowsPerPage")}
|
||||||
/>
|
/>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
setBackupFiles([]);
|
setBackupFiles([]);
|
||||||
setTotal(0);
|
setTotal(0);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
// Notice.error(t("Failed to fetch backup files"));
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -247,7 +246,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("Backup Setting")}
|
title={t("settings.modals.backup.title")}
|
||||||
contentSx={{
|
contentSx={{
|
||||||
minWidth: { xs: 320, sm: 620 },
|
minWidth: { xs: 320, sm: 620 },
|
||||||
maxWidth: "unset",
|
maxWidth: "unset",
|
||||||
@@ -278,11 +277,14 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
<Tabs
|
<Tabs
|
||||||
value={source}
|
value={source}
|
||||||
onChange={handleChangeSource}
|
onChange={handleChangeSource}
|
||||||
aria-label={t("Select Backup Target")}
|
aria-label={t("settings.modals.backup.actions.selectTarget")}
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
>
|
>
|
||||||
<Tab value="local" label={t("Local Backup")} />
|
<Tab value="local" label={t("settings.modals.backup.tabs.local")} />
|
||||||
<Tab value="webdav" label={t("WebDAV Backup")} />
|
<Tab
|
||||||
|
value="webdav"
|
||||||
|
label={t("settings.modals.backup.tabs.webdav")}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{source === "local" ? (
|
{source === "local" ? (
|
||||||
<LocalBackupActions
|
<LocalBackupActions
|
||||||
@@ -342,7 +344,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
backgroundColor: (theme) => theme.palette.background.paper,
|
backgroundColor: (theme) => theme.palette.background.paper,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Close")}
|
{t("shared.actions.close")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>,
|
</Box>,
|
||||||
dialogPaper,
|
dialogPaper,
|
||||||
|
|||||||
@@ -24,8 +24,16 @@ import { changeClashCore, restartCore } from "@/services/cmds";
|
|||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
|
|
||||||
const VALID_CORE = [
|
const VALID_CORE = [
|
||||||
{ name: "Mihomo", core: "verge-mihomo", chip: "Release Version" },
|
{
|
||||||
{ name: "Mihomo Alpha", core: "verge-mihomo-alpha", chip: "Alpha Version" },
|
name: "Mihomo",
|
||||||
|
core: "verge-mihomo",
|
||||||
|
chipKey: "settings.modals.clashCore.variants.release",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mihomo Alpha",
|
||||||
|
core: "verge-mihomo-alpha",
|
||||||
|
chipKey: "settings.modals.clashCore.variants.alpha",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||||
@@ -54,7 +62,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
const errorMsg = await changeClashCore(core);
|
const errorMsg = await changeClashCore(core);
|
||||||
|
|
||||||
if (errorMsg) {
|
if (errorMsg) {
|
||||||
showNotice("error", errorMsg);
|
showNotice.error(errorMsg);
|
||||||
setChangingCore(null);
|
setChangingCore(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -65,9 +73,9 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
mutate("getVersion");
|
mutate("getVersion");
|
||||||
setChangingCore(null);
|
setChangingCore(null);
|
||||||
}, 500);
|
}, 500);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
setChangingCore(null);
|
setChangingCore(null);
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,11 +83,13 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
try {
|
try {
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
await restartCore();
|
await restartCore();
|
||||||
showNotice("success", t(`Clash Core Restarted`));
|
showNotice.success(
|
||||||
|
t("settings.feedback.notifications.clash.restartSuccess"),
|
||||||
|
);
|
||||||
setRestarting(false);
|
setRestarting(false);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
setRestarting(false);
|
setRestarting(false);
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,14 +98,16 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
setUpgrading(true);
|
setUpgrading(true);
|
||||||
await upgradeCore();
|
await upgradeCore();
|
||||||
setUpgrading(false);
|
setUpgrading(false);
|
||||||
showNotice("success", t(`Core Version Updated`));
|
showNotice.success(
|
||||||
|
t("settings.feedback.notifications.clash.versionUpdated"),
|
||||||
|
);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setUpgrading(false);
|
setUpgrading(false);
|
||||||
const errMsg = err.response?.data?.message || err.toString();
|
const errMsg = err?.response?.data?.message ?? String(err);
|
||||||
const showMsg = errMsg.includes("already using latest version")
|
const showMsg = errMsg.includes("already using latest version")
|
||||||
? "Already Using Latest Core Version"
|
? t("settings.feedback.notifications.clash.alreadyLatestVersion")
|
||||||
: errMsg;
|
: errMsg;
|
||||||
showNotice("error", t(showMsg));
|
showNotice.info(showMsg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,7 +116,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
open={open}
|
open={open}
|
||||||
title={
|
title={
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
{t("Clash Core")}
|
{t("settings.sections.clash.form.fields.clashCore")}
|
||||||
<Box>
|
<Box>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -116,7 +128,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
sx={{ marginRight: "8px" }}
|
sx={{ marginRight: "8px" }}
|
||||||
onClick={onUpgrade}
|
onClick={onUpgrade}
|
||||||
>
|
>
|
||||||
{t("Upgrade")}
|
{t("shared.actions.upgrade")}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -127,7 +139,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
disabled={upgrading}
|
disabled={upgrading}
|
||||||
onClick={onRestart}
|
onClick={onRestart}
|
||||||
>
|
>
|
||||||
{t("Restart")}
|
{t("shared.actions.restart")}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -141,7 +153,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
marginTop: "-8px",
|
marginTop: "-8px",
|
||||||
}}
|
}}
|
||||||
disableOk
|
disableOk
|
||||||
cancelBtn={t("Close")}
|
cancelBtn={t("shared.actions.close")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
@@ -157,7 +169,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
{changingCore === each.core ? (
|
{changingCore === each.core ? (
|
||||||
<CircularProgress size={20} sx={{ mr: 1 }} />
|
<CircularProgress size={20} sx={{ mr: 1 }} />
|
||||||
) : (
|
) : (
|
||||||
<Chip label={t(`${each.chip}`)} size="small" />
|
<Chip label={t(each.chipKey)} size="small" />
|
||||||
)}
|
)}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -69,11 +69,13 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
manual: true,
|
manual: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
showNotice("success", t("Port settings saved"));
|
showNotice.success("settings.modals.clashPort.messages.saved");
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (error) => {
|
||||||
showNotice("error", e.message || t("Failed to save port settings"));
|
showNotice.error(
|
||||||
// showNotice("error", t("Failed to save port settings"));
|
"settings.modals.clashPort.messages.saveFailed",
|
||||||
|
error,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -150,7 +152,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("Port Config")}
|
title={t("settings.modals.clashPort.title")}
|
||||||
contentSx={{
|
contentSx={{
|
||||||
width: 400,
|
width: 400,
|
||||||
}}
|
}}
|
||||||
@@ -158,13 +160,13 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
loading ? (
|
loading ? (
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<CircularProgress size={20} />
|
<CircularProgress size={20} />
|
||||||
{t("Saving...")}
|
{t("shared.statuses.saving")}
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
t("Save")
|
t("shared.actions.save")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
@@ -172,7 +174,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
<List sx={{ width: "100%" }}>
|
<List sx={{ width: "100%" }}>
|
||||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Mixed Port")}
|
primary={t("settings.modals.clashPort.fields.mixed")}
|
||||||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
@@ -188,7 +190,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setMixedPort(generateRandomPort())}
|
onClick={() => setMixedPort(generateRandomPort())}
|
||||||
title={t("Random Port")}
|
title={t("settings.modals.clashPort.actions.random")}
|
||||||
sx={{ mr: 0.5 }}
|
sx={{ mr: 0.5 }}
|
||||||
>
|
>
|
||||||
<Shuffle fontSize="small" />
|
<Shuffle fontSize="small" />
|
||||||
@@ -204,7 +206,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
|
|
||||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Socks Port")}
|
primary={t("settings.modals.clashPort.fields.socks")}
|
||||||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
@@ -221,7 +223,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setSocksPort(generateRandomPort())}
|
onClick={() => setSocksPort(generateRandomPort())}
|
||||||
title={t("Random Port")}
|
title={t("settings.modals.clashPort.actions.random")}
|
||||||
disabled={!socksEnabled}
|
disabled={!socksEnabled}
|
||||||
sx={{ mr: 0.5 }}
|
sx={{ mr: 0.5 }}
|
||||||
>
|
>
|
||||||
@@ -238,7 +240,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
|
|
||||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Http Port")}
|
primary={t("settings.modals.clashPort.fields.http")}
|
||||||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
@@ -255,7 +257,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setHttpPort(generateRandomPort())}
|
onClick={() => setHttpPort(generateRandomPort())}
|
||||||
title={t("Random Port")}
|
title={t("settings.modals.clashPort.actions.random")}
|
||||||
disabled={!httpEnabled}
|
disabled={!httpEnabled}
|
||||||
sx={{ mr: 0.5 }}
|
sx={{ mr: 0.5 }}
|
||||||
>
|
>
|
||||||
@@ -273,7 +275,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
{OS !== "windows" && (
|
{OS !== "windows" && (
|
||||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Redir Port")}
|
primary={t("settings.modals.clashPort.fields.redir")}
|
||||||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
@@ -290,7 +292,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setRedirPort(generateRandomPort())}
|
onClick={() => setRedirPort(generateRandomPort())}
|
||||||
title={t("Random Port")}
|
title={t("settings.modals.clashPort.actions.random")}
|
||||||
disabled={!redirEnabled}
|
disabled={!redirEnabled}
|
||||||
sx={{ mr: 0.5 }}
|
sx={{ mr: 0.5 }}
|
||||||
>
|
>
|
||||||
@@ -309,7 +311,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
{OS === "linux" && (
|
{OS === "linux" && (
|
||||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Tproxy Port")}
|
primary={t("settings.modals.clashPort.fields.tproxy")}
|
||||||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
@@ -326,7 +328,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setTproxyPort(generateRandomPort())}
|
onClick={() => setTproxyPort(generateRandomPort())}
|
||||||
title={t("Random Port")}
|
title={t("settings.modals.clashPort.actions.random")}
|
||||||
disabled={!tproxyEnabled}
|
disabled={!tproxyEnabled}
|
||||||
sx={{ mr: 0.5 }}
|
sx={{ mr: 0.5 }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export const ConfigViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
open={true}
|
open={true}
|
||||||
title={
|
title={
|
||||||
<Box display="flex" alignItems="center" gap={2}>
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
{t("Runtime Config")}
|
{t("settings.components.verge.advanced.fields.runtimeConfig")}
|
||||||
<Chip label={t("ReadOnly")} size="small" />
|
<Chip label={t("shared.labels.readOnly")} size="small" />
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
initialData={Promise.resolve(runtimeConfig)}
|
initialData={Promise.resolve(runtimeConfig)}
|
||||||
|
|||||||
@@ -56,12 +56,16 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
// 如果启用了外部控制器,则保存控制器地址和密钥
|
// 如果启用了外部控制器,则保存控制器地址和密钥
|
||||||
if (enableController) {
|
if (enableController) {
|
||||||
if (!controller.trim()) {
|
if (!controller.trim()) {
|
||||||
showNotice("error", t("Controller address cannot be empty"));
|
showNotice.error(
|
||||||
|
"settings.sections.externalController.messages.addressRequired",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!secret.trim()) {
|
if (!secret.trim()) {
|
||||||
showNotice("error", t("Secret cannot be empty"));
|
showNotice.error(
|
||||||
|
"settings.sections.externalController.messages.secretRequired",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,12 +75,12 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
await patchInfo({ "external-controller": "" });
|
await patchInfo({ "external-controller": "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotice("success", t("Configuration saved successfully"));
|
showNotice.success("shared.feedback.notifications.common.saveSuccess");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice(
|
showNotice.error(
|
||||||
"error",
|
"shared.feedback.notifications.common.saveFailed",
|
||||||
err.message || t("Failed to save configuration"),
|
err,
|
||||||
4000,
|
4000,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -93,7 +97,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
setTimeout(() => setCopySuccess(null));
|
setTimeout(() => setCopySuccess(null));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[ControllerViewer] copy to clipboard failed:", err);
|
console.warn("[ControllerViewer] copy to clipboard failed:", err);
|
||||||
showNotice("error", t("Failed to copy"));
|
showNotice.error(
|
||||||
|
"settings.sections.externalController.messages.copyFailed",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -101,19 +107,19 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("External Controller")}
|
title={t("settings.sections.externalController.title")}
|
||||||
contentSx={{ width: 400 }}
|
contentSx={{ width: 400 }}
|
||||||
okBtn={
|
okBtn={
|
||||||
isSaving ? (
|
isSaving ? (
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<CircularProgress size={16} color="inherit" />
|
<CircularProgress size={16} color="inherit" />
|
||||||
{t("Saving...")}
|
{t("shared.statuses.saving")}
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
t("Save")
|
t("shared.actions.save")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
@@ -126,7 +132,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemText primary={t("Enable External Controller")} />
|
<ListItemText
|
||||||
|
primary={t("settings.sections.externalController.fields.enable")}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
checked={enableController}
|
checked={enableController}
|
||||||
@@ -142,7 +150,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemText primary={t("External Controller")} />
|
<ListItemText
|
||||||
|
primary={t("settings.sections.externalController.fields.address")}
|
||||||
|
/>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -152,11 +162,15 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
pointerEvents: enableController ? "auto" : "none",
|
pointerEvents: enableController ? "auto" : "none",
|
||||||
}}
|
}}
|
||||||
value={controller}
|
value={controller}
|
||||||
placeholder="Required"
|
placeholder={t(
|
||||||
|
"settings.sections.externalController.placeholders.address",
|
||||||
|
)}
|
||||||
onChange={(e) => setController(e.target.value)}
|
onChange={(e) => setController(e.target.value)}
|
||||||
disabled={isSaving || !enableController}
|
disabled={isSaving || !enableController}
|
||||||
/>
|
/>
|
||||||
<Tooltip title={t("Copy to clipboard")}>
|
<Tooltip
|
||||||
|
title={t("settings.sections.externalController.tooltips.copy")}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleCopyToClipboard(controller, "controller")}
|
onClick={() => handleCopyToClipboard(controller, "controller")}
|
||||||
@@ -176,7 +190,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemText primary={t("Core Secret")} />
|
<ListItemText
|
||||||
|
primary={t("settings.sections.externalController.fields.secret")}
|
||||||
|
/>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -186,11 +202,15 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
pointerEvents: enableController ? "auto" : "none",
|
pointerEvents: enableController ? "auto" : "none",
|
||||||
}}
|
}}
|
||||||
value={secret}
|
value={secret}
|
||||||
placeholder={t("Recommended")}
|
placeholder={t(
|
||||||
|
"settings.sections.externalController.placeholders.secret",
|
||||||
|
)}
|
||||||
onChange={(e) => setSecret(e.target.value)}
|
onChange={(e) => setSecret(e.target.value)}
|
||||||
disabled={isSaving || !enableController}
|
disabled={isSaving || !enableController}
|
||||||
/>
|
/>
|
||||||
<Tooltip title={t("Copy to clipboard")}>
|
<Tooltip
|
||||||
|
title={t("settings.sections.externalController.tooltips.copy")}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleCopyToClipboard(secret, "secret")}
|
onClick={() => handleCopyToClipboard(secret, "secret")}
|
||||||
@@ -211,8 +231,10 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
>
|
>
|
||||||
<Alert severity="success">
|
<Alert severity="success">
|
||||||
{copySuccess === "controller"
|
{copySuccess === "controller"
|
||||||
? t("Controller address copied to clipboard")
|
? t(
|
||||||
: t("Secret copied to clipboard")}
|
"settings.sections.externalController.messages.controllerCopied",
|
||||||
|
)
|
||||||
|
: t("settings.sections.externalController.messages.secretCopied")}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|||||||
@@ -424,9 +424,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
skipYamlSyncRef.current = true;
|
skipYamlSyncRef.current = true;
|
||||||
updateValuesFromConfig(parsedYaml);
|
updateValuesFromConfig(parsedYaml);
|
||||||
} catch {
|
} catch {
|
||||||
showNotice("error", t("Invalid YAML format"));
|
showNotice.error("settings.modals.dns.errors.invalidYaml");
|
||||||
}
|
}
|
||||||
}, [yamlContent, t, updateValuesFromConfig]);
|
}, [yamlContent, updateValuesFromConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (skipYamlSyncRef.current) {
|
if (skipYamlSyncRef.current) {
|
||||||
@@ -509,7 +509,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
// 使用YAML编辑器的值
|
// 使用YAML编辑器的值
|
||||||
const parsedConfig = yaml.load(yamlContent);
|
const parsedConfig = yaml.load(yamlContent);
|
||||||
if (typeof parsedConfig !== "object" || parsedConfig === null) {
|
if (typeof parsedConfig !== "object" || parsedConfig === null) {
|
||||||
throw new Error(t("Invalid configuration"));
|
throw new Error(t("settings.modals.dns.errors.invalid"));
|
||||||
}
|
}
|
||||||
config = parsedConfig as Record<string, any>;
|
config = parsedConfig as Record<string, any>;
|
||||||
}
|
}
|
||||||
@@ -547,9 +547,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotice(
|
showNotice.error(
|
||||||
"error",
|
"settings.modals.dns.messages.configError",
|
||||||
t("DNS configuration error") + ": " + cleanErrorMsg,
|
cleanErrorMsg,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -561,9 +561,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
showNotice("success", t("DNS settings saved"));
|
showNotice.success("settings.modals.dns.messages.saved");
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -613,7 +613,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
open={open}
|
open={open}
|
||||||
title={
|
title={
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
{t("DNS Overwrite")}
|
{t("settings.modals.dns.dialog.title")}
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -622,7 +622,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
startIcon={<RestartAltRounded />}
|
startIcon={<RestartAltRounded />}
|
||||||
onClick={resetToDefaults}
|
onClick={resetToDefaults}
|
||||||
>
|
>
|
||||||
{t("Reset to Default")}
|
{t("shared.actions.resetToDefault")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -631,7 +631,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
setVisualization((prev) => !prev);
|
setVisualization((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{visualization ? t("Advanced") : t("Visualization")}
|
{visualization
|
||||||
|
? t("shared.editorModes.advanced")
|
||||||
|
: t("shared.editorModes.visualization")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -643,8 +645,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
? {}
|
? {}
|
||||||
: { padding: "0 24px", display: "flex", flexDirection: "column" }),
|
: { padding: "0 24px", display: "flex", flexDirection: "column" }),
|
||||||
}}
|
}}
|
||||||
okBtn={t("Save")}
|
okBtn={t("shared.actions.save")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
@@ -655,7 +657,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
color="warning.main"
|
color="warning.main"
|
||||||
sx={{ mb: 2, mt: 0, fontStyle: "italic" }}
|
sx={{ mb: 2, mt: 0, fontStyle: "italic" }}
|
||||||
>
|
>
|
||||||
{t("DNS Settings Warning")}
|
{t("settings.modals.dns.dialog.warning")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{visualization ? (
|
{visualization ? (
|
||||||
@@ -664,11 +666,11 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
sx={{ mt: 1, mb: 1, fontWeight: "bold" }}
|
sx={{ mt: 1, mb: 1, fontWeight: "bold" }}
|
||||||
>
|
>
|
||||||
{t("DNS Settings")}
|
{t("settings.modals.dns.sections.general")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Enable DNS")} />
|
<ListItemText primary={t("settings.modals.dns.fields.enable")} />
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
checked={values.enable}
|
checked={values.enable}
|
||||||
@@ -677,7 +679,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("DNS Listen")} />
|
<ListItemText primary={t("settings.modals.dns.fields.listen")} />
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -689,7 +691,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Enhanced Mode")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.dns.fields.enhancedMode")}
|
||||||
|
/>
|
||||||
<FormControl size="small" sx={{ width: 150 }}>
|
<FormControl size="small" sx={{ width: 150 }}>
|
||||||
<Select
|
<Select
|
||||||
value={values.enhancedMode}
|
value={values.enhancedMode}
|
||||||
@@ -702,7 +706,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Fake IP Range")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.dns.fields.fakeIpRange")}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -714,7 +720,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Fake IP Filter Mode")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.dns.fields.fakeIpFilterMode")}
|
||||||
|
/>
|
||||||
<FormControl size="small" sx={{ width: 150 }}>
|
<FormControl size="small" sx={{ width: 150 }}>
|
||||||
<Select
|
<Select
|
||||||
value={values.fakeIpFilterMode}
|
value={values.fakeIpFilterMode}
|
||||||
@@ -728,8 +736,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("IPv6")}
|
primary={t("settings.modals.dns.fields.ipv6.label")}
|
||||||
secondary={t("Enable IPv6 DNS resolution")}
|
secondary={t("settings.modals.dns.fields.ipv6.description")}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -740,8 +748,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Prefer H3")}
|
primary={t("settings.modals.dns.fields.preferH3.label")}
|
||||||
secondary={t("DNS DOH使用HTTP/3")}
|
secondary={t("settings.modals.dns.fields.preferH3.description")}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -752,8 +760,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Respect Rules")}
|
primary={t("settings.modals.dns.fields.respectRules.label")}
|
||||||
secondary={t("DNS connections follow routing rules")}
|
secondary={t(
|
||||||
|
"settings.modals.dns.fields.respectRules.description",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -764,8 +774,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Use Hosts")}
|
primary={t("settings.modals.dns.fields.useHosts.label")}
|
||||||
secondary={t("Enable to resolve hosts through hosts file")}
|
secondary={t("settings.modals.dns.fields.useHosts.description")}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -776,8 +786,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Use System Hosts")}
|
primary={t("settings.modals.dns.fields.useSystemHosts.label")}
|
||||||
secondary={t("Enable to resolve hosts through system hosts file")}
|
secondary={t(
|
||||||
|
"settings.modals.dns.fields.useSystemHosts.description",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -788,8 +800,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Direct Nameserver Follow Policy")}
|
primary={t("settings.modals.dns.fields.directPolicy.label")}
|
||||||
secondary={t("Whether to follow nameserver policy")}
|
secondary={t(
|
||||||
|
"settings.modals.dns.fields.directPolicy.description",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -800,8 +814,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Default Nameserver")}
|
primary={t("settings.modals.dns.fields.defaultNameserver.label")}
|
||||||
secondary={t("Default DNS servers used to resolve DNS servers")}
|
secondary={t(
|
||||||
|
"settings.modals.dns.fields.defaultNameserver.description",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -817,8 +833,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Nameserver")}
|
primary={t("settings.modals.dns.fields.nameserver.label")}
|
||||||
secondary={t("List of DNS servers")}
|
secondary={t("settings.modals.dns.fields.nameserver.description")}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -834,8 +850,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Fallback")}
|
primary={t("settings.modals.dns.fields.fallback.label")}
|
||||||
secondary={t("List of fallback DNS servers")}
|
secondary={t("settings.modals.dns.fields.fallback.description")}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -851,8 +867,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Proxy Server Nameserver")}
|
primary={t("settings.modals.dns.fields.proxy.label")}
|
||||||
secondary={t("Proxy Node Nameserver")}
|
secondary={t("settings.modals.dns.fields.proxy.description")}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -868,8 +884,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Direct Nameserver")}
|
primary={t("settings.modals.dns.fields.directNameserver.label")}
|
||||||
secondary={t("Direct outbound Nameserver")}
|
secondary={t(
|
||||||
|
"settings.modals.dns.fields.directNameserver.description",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -885,8 +903,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Fake IP Filter")}
|
primary={t("settings.modals.dns.fields.fakeIpFilter.label")}
|
||||||
secondary={t("Domains that skip fake IP resolution")}
|
secondary={t(
|
||||||
|
"settings.modals.dns.fields.fakeIpFilter.description",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -902,8 +922,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Nameserver Policy")}
|
primary={t("settings.modals.dns.fields.nameserverPolicy.label")}
|
||||||
secondary={t("Domain-specific DNS server")}
|
secondary={t(
|
||||||
|
"settings.modals.dns.fields.nameserverPolicy.description",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -921,13 +943,15 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
sx={{ mt: 2, mb: 1, fontWeight: "bold" }}
|
sx={{ mt: 2, mb: 1, fontWeight: "bold" }}
|
||||||
>
|
>
|
||||||
{t("Fallback Filter Settings")}
|
{t("settings.modals.dns.sections.fallbackFilter")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("GeoIP Filtering")}
|
primary={t("settings.modals.dns.fields.geoipFiltering.label")}
|
||||||
secondary={t("Enable GeoIP filtering for fallback")}
|
secondary={t(
|
||||||
|
"settings.modals.dns.fields.geoipFiltering.description",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -937,7 +961,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("GeoIP Code")} />
|
<ListItemText primary={t("settings.modals.dns.fields.geoipCode")} />
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -950,8 +974,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Fallback IP CIDR")}
|
primary={t("settings.modals.dns.fields.fallbackIpCidr.label")}
|
||||||
secondary={t("IP CIDRs not using fallback servers")}
|
secondary={t(
|
||||||
|
"settings.modals.dns.fields.fallbackIpCidr.description",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -967,8 +993,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Fallback Domain")}
|
primary={t("settings.modals.dns.fields.fallbackDomain.label")}
|
||||||
secondary={t("Domains using fallback servers")}
|
secondary={t(
|
||||||
|
"settings.modals.dns.fields.fallbackDomain.description",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -987,13 +1015,13 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
sx={{ mt: 3, mb: 0, fontWeight: "bold" }}
|
sx={{ mt: 3, mb: 0, fontWeight: "bold" }}
|
||||||
>
|
>
|
||||||
{t("Hosts Settings")}
|
{t("settings.modals.dns.sections.hosts")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Hosts")}
|
primary={t("settings.modals.dns.fields.hosts.label")}
|
||||||
secondary={t("Custom domain to IP or domain mapping")}
|
secondary={t("settings.modals.dns.fields.hosts.description")}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -140,10 +140,12 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|||||||
manual: true,
|
manual: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
showNotice("success", t("Configuration saved successfully"));
|
showNotice.success(
|
||||||
|
"shared.feedback.notifications.common.saveSuccess",
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showNotice("error", t("Failed to save configuration"));
|
showNotice.error("shared.feedback.notifications.common.saveFailed");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -181,10 +183,10 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("External Cors Configuration")}
|
title={t("settings.sections.externalCors.title")}
|
||||||
contentSx={{ width: 500 }}
|
contentSx={{ width: 500 }}
|
||||||
okBtn={loading ? t("Saving...") : t("Save")}
|
okBtn={loading ? t("shared.statuses.saving") : t("shared.actions.save")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={handleSave}
|
onOk={handleSave}
|
||||||
@@ -198,7 +200,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|||||||
width="100%"
|
width="100%"
|
||||||
>
|
>
|
||||||
<span style={{ fontWeight: "normal" }}>
|
<span style={{ fontWeight: "normal" }}>
|
||||||
{t("Allow private network access")}
|
{t("settings.sections.externalCors.fields.allowPrivateNetwork")}
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -218,7 +220,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|||||||
<ListItem sx={{ padding: "8px 0" }}>
|
<ListItem sx={{ padding: "8px 0" }}>
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
|
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
|
||||||
{t("Allowed Origins")}
|
{t("settings.sections.externalCors.fields.allowedOrigins")}
|
||||||
</div>
|
</div>
|
||||||
{originEntries.map(({ origin, index, key }) => (
|
{originEntries.map(({ origin, index, key }) => (
|
||||||
<div
|
<div
|
||||||
@@ -235,7 +237,9 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|||||||
sx={{ fontSize: 14, marginRight: 2 }}
|
sx={{ fontSize: 14, marginRight: 2 }}
|
||||||
value={origin}
|
value={origin}
|
||||||
onChange={(e) => handleUpdateOrigin(index, e.target.value)}
|
onChange={(e) => handleUpdateOrigin(index, e.target.value)}
|
||||||
placeholder={t("Please enter a valid url")}
|
placeholder={t(
|
||||||
|
"settings.sections.externalCors.placeholders.origin",
|
||||||
|
)}
|
||||||
inputProps={{ style: { fontSize: 14 } }}
|
inputProps={{ style: { fontSize: 14 } }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -256,7 +260,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|||||||
onClick={handleAddOrigin}
|
onClick={handleAddOrigin}
|
||||||
sx={addButtonStyle}
|
sx={addButtonStyle}
|
||||||
>
|
>
|
||||||
{t("Add")}
|
{t("settings.sections.externalCors.actions.add")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -270,7 +274,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|||||||
<div
|
<div
|
||||||
style={{ color: "#666", fontSize: 12, fontStyle: "italic" }}
|
style={{ color: "#666", fontSize: 12, fontStyle: "italic" }}
|
||||||
>
|
>
|
||||||
{t("Always included origins: {{urls}}", {
|
{t("settings.sections.externalCors.messages.alwaysIncluded", {
|
||||||
urls: DEV_URLS.join(", "),
|
urls: DEV_URLS.join(", "),
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export const HotkeyInput = (props: Props) => {
|
|||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title={t("Delete")}
|
title={t("shared.actions.delete")}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange([]);
|
onChange([]);
|
||||||
|
|||||||
@@ -24,7 +24,19 @@ const HOTKEY_FUNC = [
|
|||||||
"toggle_system_proxy",
|
"toggle_system_proxy",
|
||||||
"toggle_tun_mode",
|
"toggle_tun_mode",
|
||||||
"entry_lightweight_mode",
|
"entry_lightweight_mode",
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
|
const HOTKEY_FUNC_LABELS: Record<(typeof HOTKEY_FUNC)[number], string> = {
|
||||||
|
open_or_close_dashboard:
|
||||||
|
"settings.modals.hotkey.functions.openOrCloseDashboard",
|
||||||
|
clash_mode_rule: "settings.modals.hotkey.functions.rule",
|
||||||
|
clash_mode_global: "settings.modals.hotkey.functions.global",
|
||||||
|
clash_mode_direct: "settings.modals.hotkey.functions.direct",
|
||||||
|
toggle_system_proxy: "settings.modals.hotkey.functions.toggleSystemProxy",
|
||||||
|
toggle_tun_mode: "settings.modals.hotkey.functions.toggleTunMode",
|
||||||
|
entry_lightweight_mode:
|
||||||
|
"settings.modals.hotkey.functions.entryLightweightMode",
|
||||||
|
};
|
||||||
|
|
||||||
export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -81,24 +93,26 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
enable_global_hotkey: enableGlobalHotkey,
|
enable_global_hotkey: enableGlobalHotkey,
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("Hotkey Setting")}
|
title={t("settings.modals.hotkey.title")}
|
||||||
contentSx={{ width: 450, maxHeight: 380 }}
|
contentSx={{ width: 450, maxHeight: 380 }}
|
||||||
okBtn={t("Save")}
|
okBtn={t("shared.actions.save")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
>
|
>
|
||||||
<ItemWrapper style={{ marginBottom: 16 }}>
|
<ItemWrapper style={{ marginBottom: 16 }}>
|
||||||
<Typography>{t("Enable Global Hotkey")}</Typography>
|
<Typography>
|
||||||
|
{t("settings.modals.hotkey.toggles.enableGlobal")}
|
||||||
|
</Typography>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
checked={enableGlobalHotkey}
|
checked={enableGlobalHotkey}
|
||||||
@@ -108,7 +122,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
{HOTKEY_FUNC.map((func) => (
|
{HOTKEY_FUNC.map((func) => (
|
||||||
<ItemWrapper key={func}>
|
<ItemWrapper key={func}>
|
||||||
<Typography>{t(func)}</Typography>
|
<Typography>{t(HOTKEY_FUNC_LABELS[func])}</Typography>
|
||||||
<HotkeyInput
|
<HotkeyInput
|
||||||
value={hotkeyMap[func] ?? []}
|
value={hotkeyMap[func] ?? []}
|
||||||
onChange={(v) => setHotkeyMap((m) => ({ ...m, [func]: v }))}
|
onChange={(v) => setHotkeyMap((m) => ({ ...m, [func]: v }))}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
|
|
||||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||||
const onError = (err: any) => {
|
const onError = (err: any) => {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
};
|
};
|
||||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false);
|
mutateVerge({ ...verge, ...patch }, false);
|
||||||
@@ -113,16 +113,20 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("Layout Setting")}
|
title={t("settings.components.verge.layout.title")}
|
||||||
contentSx={{ width: 450 }}
|
contentSx={{ width: 450 }}
|
||||||
disableOk
|
disableOk
|
||||||
cancelBtn={t("Close")}
|
cancelBtn={t("shared.actions.close")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Prefer System Titlebar")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"settings.components.verge.layout.fields.preferSystemTitlebar",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={decorated}
|
value={decorated}
|
||||||
valueProps="checked"
|
valueProps="checked"
|
||||||
@@ -137,7 +141,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Traffic Graph")} />
|
<ListItemText
|
||||||
|
primary={t("settings.components.verge.layout.fields.trafficGraph")}
|
||||||
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.traffic_graph ?? true}
|
value={verge?.traffic_graph ?? true}
|
||||||
valueProps="checked"
|
valueProps="checked"
|
||||||
@@ -151,7 +157,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Memory Usage")} />
|
<ListItemText
|
||||||
|
primary={t("settings.components.verge.layout.fields.memoryUsage")}
|
||||||
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.enable_memory_usage ?? true}
|
value={verge?.enable_memory_usage ?? true}
|
||||||
valueProps="checked"
|
valueProps="checked"
|
||||||
@@ -165,7 +173,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Proxy Group Icon")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"settings.components.verge.layout.fields.proxyGroupIcon",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.enable_group_icon ?? true}
|
value={verge?.enable_group_icon ?? true}
|
||||||
valueProps="checked"
|
valueProps="checked"
|
||||||
@@ -182,9 +194,13 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
<ListItemText
|
<ListItemText
|
||||||
primary={
|
primary={
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||||
<span>{t("Hover Jump Navigator")}</span>
|
<span>
|
||||||
|
{t("settings.components.verge.layout.fields.hoverNavigator")}
|
||||||
|
</span>
|
||||||
<TooltipIcon
|
<TooltipIcon
|
||||||
title={t("Hover Jump Navigator Info")}
|
title={t(
|
||||||
|
"settings.components.verge.layout.tooltips.hoverNavigator",
|
||||||
|
)}
|
||||||
sx={{ opacity: "0.7" }}
|
sx={{ opacity: "0.7" }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -206,9 +222,15 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
<ListItemText
|
<ListItemText
|
||||||
primary={
|
primary={
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||||
<span>{t("Hover Jump Navigator Delay")}</span>
|
<span>
|
||||||
|
{t(
|
||||||
|
"settings.components.verge.layout.fields.hoverNavigatorDelay",
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
<TooltipIcon
|
<TooltipIcon
|
||||||
title={t("Hover Jump Navigator Delay Info")}
|
title={t(
|
||||||
|
"settings.components.verge.layout.tooltips.hoverNavigatorDelay",
|
||||||
|
)}
|
||||||
sx={{ opacity: "0.7" }}
|
sx={{ opacity: "0.7" }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -241,7 +263,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
{t("millis")}
|
{t("shared.units.milliseconds")}
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -256,7 +278,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Nav Icon")} />
|
<ListItemText
|
||||||
|
primary={t("settings.components.verge.layout.fields.navIcon")}
|
||||||
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.menu_icon ?? "monochrome"}
|
value={verge?.menu_icon ?? "monochrome"}
|
||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
@@ -265,16 +289,24 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
onGuard={(value) => patchVerge({ menu_icon: value })}
|
onGuard={(value) => patchVerge({ menu_icon: value })}
|
||||||
>
|
>
|
||||||
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
||||||
<MenuItem value="monochrome">{t("Monochrome")}</MenuItem>
|
<MenuItem value="monochrome">
|
||||||
<MenuItem value="colorful">{t("Colorful")}</MenuItem>
|
{t("settings.components.verge.layout.options.icon.monochrome")}
|
||||||
<MenuItem value="disable">{t("Disable")}</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem value="colorful">
|
||||||
|
{t("settings.components.verge.layout.options.icon.colorful")}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="disable">
|
||||||
|
{t("settings.components.verge.layout.options.icon.disable")}
|
||||||
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</GuardState>
|
</GuardState>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
{OS === "macos" && (
|
{OS === "macos" && (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Tray Icon")} />
|
<ListItemText
|
||||||
|
primary={t("settings.components.verge.layout.fields.trayIcon")}
|
||||||
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.tray_icon ?? "monochrome"}
|
value={verge?.tray_icon ?? "monochrome"}
|
||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
@@ -286,15 +318,21 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
size="small"
|
size="small"
|
||||||
sx={{ width: 140, "> div": { py: "7.5px" } }}
|
sx={{ width: 140, "> div": { py: "7.5px" } }}
|
||||||
>
|
>
|
||||||
<MenuItem value="monochrome">{t("Monochrome")}</MenuItem>
|
<MenuItem value="monochrome">
|
||||||
<MenuItem value="colorful">{t("Colorful")}</MenuItem>
|
{t(
|
||||||
|
"settings.components.verge.layout.options.icon.monochrome",
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="colorful">
|
||||||
|
{t("settings.components.verge.layout.options.icon.colorful")}
|
||||||
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</GuardState>
|
</GuardState>
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
{/* {OS === "macos" && (
|
{/* {OS === "macos" && (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Enable Tray Speed")} />
|
<ListItemText primary={t("settings.components.verge.layout.fields.enableTraySpeed")} />
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.enable_tray_speed ?? false}
|
value={verge?.enable_tray_speed ?? false}
|
||||||
valueProps="checked"
|
valueProps="checked"
|
||||||
@@ -309,7 +347,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
)} */}
|
)} */}
|
||||||
{/* {OS === "macos" && (
|
{/* {OS === "macos" && (
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Enable Tray Icon")} />
|
<ListItemText primary={t("settings.components.verge.layout.fields.enableTrayIcon")} />
|
||||||
<GuardState
|
<GuardState
|
||||||
value={
|
value={
|
||||||
verge?.enable_tray_icon === false &&
|
verge?.enable_tray_icon === false &&
|
||||||
@@ -328,7 +366,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
</Item>
|
</Item>
|
||||||
)} */}
|
)} */}
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Show Proxy Groups Inline")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"settings.components.verge.layout.fields.showProxyGroupsInline",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.tray_inline_proxy_groups ?? false}
|
value={verge?.tray_inline_proxy_groups ?? false}
|
||||||
valueProps="checked"
|
valueProps="checked"
|
||||||
@@ -342,7 +384,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Common Tray Icon")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"settings.components.verge.layout.fields.commonTrayIcon",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.common_tray_icon}
|
value={verge?.common_tray_icon}
|
||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
@@ -384,13 +430,19 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{verge?.common_tray_icon ? t("Clear") : t("Browse")}
|
{verge?.common_tray_icon
|
||||||
|
? t("shared.actions.clear")
|
||||||
|
: t("settings.components.verge.basic.actions.browse")}
|
||||||
</Button>
|
</Button>
|
||||||
</GuardState>
|
</GuardState>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("System Proxy Tray Icon")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"settings.components.verge.layout.fields.systemProxyTrayIcon",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.sysproxy_tray_icon}
|
value={verge?.sysproxy_tray_icon}
|
||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
@@ -430,13 +482,17 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{verge?.sysproxy_tray_icon ? t("Clear") : t("Browse")}
|
{verge?.sysproxy_tray_icon
|
||||||
|
? t("shared.actions.clear")
|
||||||
|
: t("settings.components.verge.basic.actions.browse")}
|
||||||
</Button>
|
</Button>
|
||||||
</GuardState>
|
</GuardState>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Tun Tray Icon")} />
|
<ListItemText
|
||||||
|
primary={t("settings.components.verge.layout.fields.tunTrayIcon")}
|
||||||
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.tun_tray_icon}
|
value={verge?.tun_tray_icon}
|
||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
@@ -474,7 +530,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{verge?.tun_tray_icon ? t("Clear") : t("Browse")}
|
{verge?.tun_tray_icon
|
||||||
|
? t("shared.actions.clear")
|
||||||
|
: t("settings.components.verge.basic.actions.browse")}
|
||||||
</Button>
|
</Button>
|
||||||
</GuardState>
|
</GuardState>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
@@ -45,25 +45,27 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
auto_light_weight_minutes: values.autoEnterLiteModeDelay,
|
auto_light_weight_minutes: values.autoEnterLiteModeDelay,
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("LightWeight Mode Settings")}
|
title={t("settings.modals.liteMode.title")}
|
||||||
contentSx={{ width: 450 }}
|
contentSx={{ width: 450 }}
|
||||||
okBtn={t("Save")}
|
okBtn={t("shared.actions.save")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Enter LightWeight Mode Now")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.liteMode.actions.enterNow")}
|
||||||
|
/>
|
||||||
<Typography
|
<Typography
|
||||||
variant="button"
|
variant="button"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -73,17 +75,17 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
}}
|
}}
|
||||||
onClick={async () => await entry_lightweight_mode()}
|
onClick={async () => await entry_lightweight_mode()}
|
||||||
>
|
>
|
||||||
{t("Enable")}
|
{t("shared.actions.enable")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Auto Enter LightWeight Mode")}
|
primary={t("settings.modals.liteMode.toggles.autoEnter")}
|
||||||
sx={{ maxWidth: "fit-content" }}
|
sx={{ maxWidth: "fit-content" }}
|
||||||
/>
|
/>
|
||||||
<TooltipIcon
|
<TooltipIcon
|
||||||
title={t("Auto Enter LightWeight Mode Info")}
|
title={t("settings.modals.liteMode.tooltips.autoEnter")}
|
||||||
sx={{ opacity: "0.7" }}
|
sx={{ opacity: "0.7" }}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -99,7 +101,9 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
{values.autoEnterLiteMode && (
|
{values.autoEnterLiteMode && (
|
||||||
<>
|
<>
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Auto Enter LightWeight Mode Delay")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.liteMode.fields.delay")}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -119,7 +123,7 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
{t("mins")}
|
{t("shared.units.minutes")}
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -133,10 +137,9 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ fontStyle: "italic" }}
|
sx={{ fontStyle: "italic" }}
|
||||||
>
|
>
|
||||||
{t(
|
{t("settings.modals.liteMode.messages.autoEnterHint", {
|
||||||
"When closing the window, LightWeight Mode will be automatically activated after _n minutes",
|
n: values.autoEnterLiteModeDelay,
|
||||||
{ n: values.autoEnterLiteModeDelay },
|
})}
|
||||||
)}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ export const LocalBackupActions = memo(
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await createLocalBackup();
|
await createLocalBackup();
|
||||||
showNotice("success", t("Local Backup Created"));
|
showNotice.success(
|
||||||
|
"settings.modals.backup.messages.localBackupCreated",
|
||||||
|
);
|
||||||
await onBackupSuccess();
|
await onBackupSuccess();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
showNotice("error", t("Local Backup Failed"));
|
showNotice.error("settings.modals.backup.messages.localBackupFailed");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -43,7 +45,7 @@ export const LocalBackupActions = memo(
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid size={{ xs: 12, sm: 9 }}>
|
<Grid size={{ xs: 12, sm: 9 }}>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Local Backup Info")}
|
{t("settings.modals.backup.fields.info")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 3 }}>
|
<Grid size={{ xs: 12, sm: 3 }}>
|
||||||
@@ -60,7 +62,7 @@ export const LocalBackupActions = memo(
|
|||||||
type="button"
|
type="button"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
{t("Backup")}
|
{t("settings.modals.backup.actions.backup")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -68,7 +70,7 @@ export const LocalBackupActions = memo(
|
|||||||
type="button"
|
type="button"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
{t("Refresh")}
|
{t("shared.actions.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -69,25 +69,27 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
auto_log_clean: values.autoLogClean as any,
|
auto_log_clean: values.autoLogClean as any,
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("Miscellaneous")}
|
title={t("settings.modals.misc.title")}
|
||||||
contentSx={{ width: 450 }}
|
contentSx={{ width: 450 }}
|
||||||
okBtn={t("Save")}
|
okBtn={t("shared.actions.save")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("App Log Level")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.misc.fields.appLogLevel")}
|
||||||
|
/>
|
||||||
<Select
|
<Select
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: 100, "> div": { py: "7.5px" } }}
|
sx={{ width: 100, "> div": { py: "7.5px" } }}
|
||||||
@@ -109,7 +111,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("App Log Max Size")}
|
primary={t("settings.modals.misc.fields.appLogMaxSize")}
|
||||||
sx={{ maxWidth: "fit-content" }}
|
sx={{ maxWidth: "fit-content" }}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -130,7 +132,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">{t("KB")}</InputAdornment>
|
<InputAdornment position="end">
|
||||||
|
{t("shared.units.kilobytes")}
|
||||||
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -139,7 +143,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("App Log Max Count")}
|
primary={t("settings.modals.misc.fields.appLogMaxCount")}
|
||||||
sx={{ maxWidth: "fit-content" }}
|
sx={{ maxWidth: "fit-content" }}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -160,7 +164,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">{t("Files")}</InputAdornment>
|
<InputAdornment position="end">
|
||||||
|
{t("shared.units.files")}
|
||||||
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -169,11 +175,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Auto Close Connections")}
|
primary={t("settings.modals.misc.fields.autoCloseConnections")}
|
||||||
sx={{ maxWidth: "fit-content" }}
|
sx={{ maxWidth: "fit-content" }}
|
||||||
/>
|
/>
|
||||||
<TooltipIcon
|
<TooltipIcon
|
||||||
title={t("Auto Close Connections Info")}
|
title={t("settings.modals.misc.tooltips.autoCloseConnections")}
|
||||||
sx={{ opacity: "0.7" }}
|
sx={{ opacity: "0.7" }}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -187,7 +193,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Auto Check Update")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.misc.fields.autoCheckUpdate")}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
checked={values.autoCheckUpdate}
|
checked={values.autoCheckUpdate}
|
||||||
@@ -199,11 +207,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Enable Builtin Enhanced")}
|
primary={t("settings.modals.misc.fields.enableBuiltinEnhanced")}
|
||||||
sx={{ maxWidth: "fit-content" }}
|
sx={{ maxWidth: "fit-content" }}
|
||||||
/>
|
/>
|
||||||
<TooltipIcon
|
<TooltipIcon
|
||||||
title={t("Enable Builtin Enhanced Info")}
|
title={t("settings.modals.misc.tooltips.enableBuiltinEnhanced")}
|
||||||
sx={{ opacity: "0.7" }}
|
sx={{ opacity: "0.7" }}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -217,7 +225,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Proxy Layout Columns")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.misc.fields.proxyLayoutColumns")}
|
||||||
|
/>
|
||||||
<Select
|
<Select
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: 160, "> div": { py: "7.5px" } }}
|
sx={{ width: 160, "> div": { py: "7.5px" } }}
|
||||||
@@ -230,7 +240,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuItem value={6} key={6}>
|
<MenuItem value={6} key={6}>
|
||||||
{t("Auto Columns")}
|
{t("settings.modals.misc.options.proxyLayoutColumns.auto")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
<MenuItem value={i} key={i}>
|
<MenuItem value={i} key={i}>
|
||||||
@@ -241,7 +251,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Auto Log Clean")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.misc.fields.autoLogClean")}
|
||||||
|
/>
|
||||||
<Select
|
<Select
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: 160, "> div": { py: "7.5px" } }}
|
sx={{ width: 160, "> div": { py: "7.5px" } }}
|
||||||
@@ -255,11 +267,34 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
>
|
>
|
||||||
{/* 1: 1天, 2: 7天, 3: 30天, 4: 90天*/}
|
{/* 1: 1天, 2: 7天, 3: 30天, 4: 90天*/}
|
||||||
{[
|
{[
|
||||||
{ key: t("Never Clean"), value: 0 },
|
{
|
||||||
{ key: t("Retain _n Days", { n: 1 }), value: 1 },
|
key: t("settings.modals.misc.options.autoLogClean.never"),
|
||||||
{ key: t("Retain _n Days", { n: 7 }), value: 2 },
|
value: 0,
|
||||||
{ key: t("Retain _n Days", { n: 30 }), value: 3 },
|
},
|
||||||
{ key: t("Retain _n Days", { n: 90 }), value: 4 },
|
{
|
||||||
|
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
|
||||||
|
n: 1,
|
||||||
|
}),
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
|
||||||
|
n: 7,
|
||||||
|
}),
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
|
||||||
|
n: 30,
|
||||||
|
}),
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
|
||||||
|
n: 90,
|
||||||
|
}),
|
||||||
|
value: 4,
|
||||||
|
},
|
||||||
].map((i) => (
|
].map((i) => (
|
||||||
<MenuItem key={i.value} value={i.value}>
|
<MenuItem key={i.value} value={i.value}>
|
||||||
{i.key}
|
{i.key}
|
||||||
@@ -270,11 +305,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Auto Delay Detection")}
|
primary={t("settings.modals.misc.fields.autoDelayDetection")}
|
||||||
sx={{ maxWidth: "fit-content" }}
|
sx={{ maxWidth: "fit-content" }}
|
||||||
/>
|
/>
|
||||||
<TooltipIcon
|
<TooltipIcon
|
||||||
title={t("Auto Delay Detection Info")}
|
title={t("settings.modals.misc.tooltips.autoDelayDetection")}
|
||||||
sx={{ opacity: "0.7" }}
|
sx={{ opacity: "0.7" }}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -289,11 +324,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Default Latency Test")}
|
primary={t("settings.modals.misc.fields.defaultLatencyTest")}
|
||||||
sx={{ maxWidth: "fit-content" }}
|
sx={{ maxWidth: "fit-content" }}
|
||||||
/>
|
/>
|
||||||
<TooltipIcon
|
<TooltipIcon
|
||||||
title={t("Default Latency Test Info")}
|
title={t("settings.modals.misc.tooltips.defaultLatencyTest")}
|
||||||
sx={{ opacity: "0.7" }}
|
sx={{ opacity: "0.7" }}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -312,7 +347,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Default Latency Timeout")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.misc.fields.defaultLatencyTimeout")}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -332,7 +369,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">{t("millis")}</InputAdornment>
|
<InputAdornment position="end">
|
||||||
|
{t("shared.units.milliseconds")}
|
||||||
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
open={open}
|
open={open}
|
||||||
title={
|
title={
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
{t("Network Interface")}
|
{t("settings.modals.networkInterface.title")}
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -51,7 +51,7 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
}
|
}
|
||||||
contentSx={{ width: 450 }}
|
contentSx={{ width: 450 }}
|
||||||
disableOk
|
disableOk
|
||||||
cancelBtn={t("Close")}
|
cancelBtn={t("shared.actions.close")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
@@ -66,13 +66,17 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
address.V4 && (
|
address.V4 && (
|
||||||
<AddressDisplay
|
<AddressDisplay
|
||||||
key={address.V4.ip}
|
key={address.V4.ip}
|
||||||
label={t("Ip Address")}
|
label={t(
|
||||||
|
"settings.modals.networkInterface.fields.ipAddress",
|
||||||
|
)}
|
||||||
content={address.V4.ip}
|
content={address.V4.ip}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
<AddressDisplay
|
<AddressDisplay
|
||||||
label={t("Mac Address")}
|
label={t(
|
||||||
|
"settings.modals.networkInterface.fields.macAddress",
|
||||||
|
)}
|
||||||
content={item.mac_addr ?? ""}
|
content={item.mac_addr ?? ""}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -84,13 +88,17 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
address.V6 && (
|
address.V6 && (
|
||||||
<AddressDisplay
|
<AddressDisplay
|
||||||
key={address.V6.ip}
|
key={address.V6.ip}
|
||||||
label={t("Ip Address")}
|
label={t(
|
||||||
|
"settings.modals.networkInterface.fields.ipAddress",
|
||||||
|
)}
|
||||||
content={address.V6.ip}
|
content={address.V6.ip}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
<AddressDisplay
|
<AddressDisplay
|
||||||
label={t("Mac Address")}
|
label={t(
|
||||||
|
"settings.modals.networkInterface.fields.macAddress",
|
||||||
|
)}
|
||||||
content={item.mac_addr ?? ""}
|
content={item.mac_addr ?? ""}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -109,8 +117,6 @@ const AddressDisplay = ({
|
|||||||
label: string;
|
label: string;
|
||||||
content: string;
|
content: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -135,7 +141,9 @@ const AddressDisplay = ({
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await writeText(content);
|
await writeText(content);
|
||||||
showNotice("success", t("Copy Success"));
|
showNotice.success(
|
||||||
|
"shared.feedback.notifications.common.copySuccess",
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ContentCopyRounded sx={{ fontSize: "18px" }} />
|
<ContentCopyRounded sx={{ fontSize: "18px" }} />
|
||||||
|
|||||||
@@ -21,13 +21,15 @@ export const PasswordInput = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} maxWidth="xs" fullWidth>
|
<Dialog open={true} maxWidth="xs" fullWidth>
|
||||||
<DialogTitle>{t("Please enter your root password")}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{t("settings.modals.password.prompts.enterRoot")}
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
sx={{ mt: 1 }}
|
sx={{ mt: 1 }}
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t("Password")}
|
label={t("shared.labels.password")}
|
||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -42,7 +44,7 @@ export const PasswordInput = (props: Props) => {
|
|||||||
onClick={async () => await onConfirm(passwd)}
|
onClick={async () => await onConfirm(passwd)}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
{t("Confirm")}
|
{t("shared.actions.confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -160,8 +160,8 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
mutate("getAutotemProxy"),
|
mutate("getAutotemProxy"),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -277,14 +277,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
const onSave = useLockFn(async () => {
|
const onSave = useLockFn(async () => {
|
||||||
if (value.duration < 1) {
|
if (value.duration < 1) {
|
||||||
showNotice(
|
showNotice.error("settings.modals.sysproxy.messages.durationTooShort");
|
||||||
"error",
|
|
||||||
t("Proxy Daemon Duration Cannot be Less than 1 Second"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (value.bypass && !validReg.test(value.bypass)) {
|
if (value.bypass && !validReg.test(value.bypass)) {
|
||||||
showNotice("error", t("Invalid Bypass Format"));
|
showNotice.error("settings.modals.sysproxy.messages.invalidBypass");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +298,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
!ipv6Regex.test(value.proxy_host) &&
|
!ipv6Regex.test(value.proxy_host) &&
|
||||||
!hostnameRegex.test(value.proxy_host)
|
!hostnameRegex.test(value.proxy_host)
|
||||||
) {
|
) {
|
||||||
showNotice("error", t("Invalid Proxy Host Format"));
|
showNotice.error("settings.modals.sysproxy.messages.invalidProxyHost");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,10 +398,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
console.warn("代理状态更新失败:", err);
|
console.warn("代理状态更新失败:", err);
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
console.error("配置保存失败:", err);
|
console.error("配置保存失败:", err);
|
||||||
mutateVerge();
|
mutateVerge();
|
||||||
showNotice("error", err.toString());
|
showNotice.error(err);
|
||||||
// setOpen(true);
|
// setOpen(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -413,10 +410,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("System Proxy Setting")}
|
title={t("settings.modals.sysproxy.title")}
|
||||||
contentSx={{ width: 450, maxHeight: 565 }}
|
contentSx={{ width: 450, maxHeight: 565 }}
|
||||||
okBtn={t("Save")}
|
okBtn={t("shared.actions.save")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
@@ -424,28 +421,37 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
disableOk={saving}
|
disableOk={saving}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
<BaseFieldset label={t("Current System Proxy")} padding="15px 10px">
|
<BaseFieldset
|
||||||
|
label={t("settings.modals.sysproxy.fieldsets.currentStatus")}
|
||||||
|
padding="15px 10px"
|
||||||
|
>
|
||||||
<FlexBox>
|
<FlexBox>
|
||||||
<Typography className="label">{t("Enable status")}</Typography>
|
<Typography className="label">
|
||||||
|
{t("settings.modals.sysproxy.fields.enableStatus")}
|
||||||
|
</Typography>
|
||||||
<Typography className="value">
|
<Typography className="value">
|
||||||
{value.pac
|
{value.pac
|
||||||
? autoproxy?.enable
|
? autoproxy?.enable
|
||||||
? t("Enabled")
|
? t("shared.statuses.enabled")
|
||||||
: t("Disabled")
|
: t("shared.statuses.disabled")
|
||||||
: sysproxy?.enable
|
: sysproxy?.enable
|
||||||
? t("Enabled")
|
? t("shared.statuses.enabled")
|
||||||
: t("Disabled")}
|
: t("shared.statuses.disabled")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</FlexBox>
|
</FlexBox>
|
||||||
{!value.pac && (
|
{!value.pac && (
|
||||||
<FlexBox>
|
<FlexBox>
|
||||||
<Typography className="label">{t("Server Addr")}</Typography>
|
<Typography className="label">
|
||||||
|
{t("settings.modals.sysproxy.fields.serverAddr")}
|
||||||
|
</Typography>
|
||||||
<Typography className="value">{getSystemProxyAddress}</Typography>
|
<Typography className="value">{getSystemProxyAddress}</Typography>
|
||||||
</FlexBox>
|
</FlexBox>
|
||||||
)}
|
)}
|
||||||
{value.pac && (
|
{value.pac && (
|
||||||
<FlexBox>
|
<FlexBox>
|
||||||
<Typography className="label">{t("PAC URL")}</Typography>
|
<Typography className="label">
|
||||||
|
{t("settings.modals.sysproxy.fields.pacUrl")}
|
||||||
|
</Typography>
|
||||||
<Typography className="value">
|
<Typography className="value">
|
||||||
{getCurrentPacUrl || "-"}
|
{getCurrentPacUrl || "-"}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -453,7 +459,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
)}
|
)}
|
||||||
</BaseFieldset>
|
</BaseFieldset>
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Proxy Host")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.sysproxy.fields.proxyHost")}
|
||||||
|
/>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: 150 }}
|
sx={{ width: 150 }}
|
||||||
@@ -478,7 +486,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Use PAC Mode")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.sysproxy.fields.usePacMode")}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
@@ -489,10 +499,13 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Proxy Guard")}
|
primary={t("settings.modals.sysproxy.fields.proxyGuard")}
|
||||||
sx={{ maxWidth: "fit-content" }}
|
sx={{ maxWidth: "fit-content" }}
|
||||||
/>
|
/>
|
||||||
<TooltipIcon title={t("Proxy Guard Info")} sx={{ opacity: "0.7" }} />
|
<TooltipIcon
|
||||||
|
title={t("settings.modals.sysproxy.tooltips.proxyGuard")}
|
||||||
|
sx={{ opacity: "0.7" }}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
@@ -503,7 +516,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Guard Duration")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.sysproxy.fields.guardDuration")}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -524,7 +539,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
{!value.pac && (
|
{!value.pac && (
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Always use Default Bypass")} />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"settings.modals.sysproxy.fields.alwaysUseDefaultBypass",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
@@ -543,7 +562,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
{!value.pac && !value.use_default && (
|
{!value.pac && !value.use_default && (
|
||||||
<>
|
<>
|
||||||
<ListItemText primary={t("Proxy Bypass")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.sysproxy.fields.proxyBypass")}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
error={value.bypass ? !validReg.test(value.bypass) : false}
|
error={value.bypass ? !validReg.test(value.bypass) : false}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
@@ -561,7 +582,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
{!value.pac && value.use_default && (
|
{!value.pac && value.use_default && (
|
||||||
<>
|
<>
|
||||||
<ListItemText primary={t("Bypass")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.sysproxy.fields.bypass")}
|
||||||
|
/>
|
||||||
<FlexBox>
|
<FlexBox>
|
||||||
<TextField
|
<TextField
|
||||||
disabled={true}
|
disabled={true}
|
||||||
@@ -578,7 +601,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
{value.pac && (
|
{value.pac && (
|
||||||
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
|
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("PAC Script Content")}
|
primary={t("settings.modals.sysproxy.fields.pacScriptContent")}
|
||||||
sx={{ padding: "3px 0" }}
|
sx={{ padding: "3px 0" }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -588,12 +611,12 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
setEditorOpen(true);
|
setEditorOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Edit")} PAC
|
{t("settings.modals.sysproxy.actions.editPac")}
|
||||||
</Button>
|
</Button>
|
||||||
{editorOpen && (
|
{editorOpen && (
|
||||||
<EditorViewer
|
<EditorViewer
|
||||||
open={true}
|
open={true}
|
||||||
title={`${t("Edit")} PAC`}
|
title={t("settings.modals.sysproxy.actions.editPac")}
|
||||||
initialData={Promise.resolve(value.pac_content ?? "")}
|
initialData={Promise.resolve(value.pac_content ?? "")}
|
||||||
language="javascript"
|
language="javascript"
|
||||||
onSave={(_prev, curr) => {
|
onSave={(_prev, curr) => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const ThemeModeSwitch = (props: Props) => {
|
|||||||
onClick={() => onChange?.(mode)}
|
onClick={() => onChange?.(mode)}
|
||||||
sx={{ textTransform: "capitalize" }}
|
sx={{ textTransform: "capitalize" }}
|
||||||
>
|
>
|
||||||
{t(`theme.${mode}`)}
|
{t(`settings.sections.appearance.${mode}`)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { useImperativeHandle, useState } from "react";
|
import { useImperativeHandle, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { BaseDialog, DialogRef } from "@/components/base";
|
import { BaseDialog, DialogRef } from "@/components/base";
|
||||||
@@ -50,8 +50,8 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
|
|||||||
try {
|
try {
|
||||||
await patchVerge({ theme_setting: theme });
|
await patchVerge({ theme_setting: theme });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
showNotice("error", err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,9 +62,48 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
|
|||||||
|
|
||||||
type ThemeKey = keyof typeof theme & keyof typeof defaultTheme;
|
type ThemeKey = keyof typeof theme & keyof typeof defaultTheme;
|
||||||
|
|
||||||
const renderItem = (label: string, key: ThemeKey) => {
|
const fieldDefinitions: Array<{ labelKey: string; key: ThemeKey }> = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
labelKey: "settings.components.verge.theme.fields.primaryColor",
|
||||||
|
key: "primary_color",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "settings.components.verge.theme.fields.secondaryColor",
|
||||||
|
key: "secondary_color",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "settings.components.verge.theme.fields.primaryText",
|
||||||
|
key: "primary_text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "settings.components.verge.theme.fields.secondaryText",
|
||||||
|
key: "secondary_text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "settings.components.verge.theme.fields.infoColor",
|
||||||
|
key: "info_color",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "settings.components.verge.theme.fields.warningColor",
|
||||||
|
key: "warning_color",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "settings.components.verge.theme.fields.errorColor",
|
||||||
|
key: "error_color",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "settings.components.verge.theme.fields.successColor",
|
||||||
|
key: "success_color",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = (labelKey: string, key: ThemeKey) => {
|
||||||
|
const label = t(labelKey);
|
||||||
return (
|
return (
|
||||||
<Item>
|
<Item key={key}>
|
||||||
<ListItemText primary={label} />
|
<ListItemText primary={label} />
|
||||||
<Round sx={{ background: theme[key] || dt[key] }} />
|
<Round sx={{ background: theme[key] || dt[key] }} />
|
||||||
<TextField
|
<TextField
|
||||||
@@ -81,33 +120,21 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
|
|||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("Theme Setting")}
|
title={t("settings.components.verge.theme.title")}
|
||||||
okBtn={t("Save")}
|
okBtn={t("shared.actions.save")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
contentSx={{ width: 400, maxHeight: 505, overflow: "auto", pb: 0 }}
|
contentSx={{ width: 400, maxHeight: 505, overflow: "auto", pb: 0 }}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
>
|
>
|
||||||
<List sx={{ pt: 0 }}>
|
<List sx={{ pt: 0 }}>
|
||||||
{renderItem(t("Primary Color"), "primary_color")}
|
{fieldDefinitions.map((field) => renderItem(field.labelKey, field.key))}
|
||||||
|
|
||||||
{renderItem(t("Secondary Color"), "secondary_color")}
|
|
||||||
|
|
||||||
{renderItem(t("Primary Text"), "primary_text")}
|
|
||||||
|
|
||||||
{renderItem(t("Secondary Text"), "secondary_text")}
|
|
||||||
|
|
||||||
{renderItem(t("Info Color"), "info_color")}
|
|
||||||
|
|
||||||
{renderItem(t("Warning Color"), "warning_color")}
|
|
||||||
|
|
||||||
{renderItem(t("Error Color"), "error_color")}
|
|
||||||
|
|
||||||
{renderItem(t("Success Color"), "success_color")}
|
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Font Family")} />
|
<ListItemText
|
||||||
|
primary={t("settings.components.verge.theme.fields.fontFamily")}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...textProps}
|
{...textProps}
|
||||||
value={theme.font_family ?? ""}
|
value={theme.font_family ?? ""}
|
||||||
@@ -116,7 +143,9 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
|
|||||||
/>
|
/>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("CSS Injection")} />
|
<ListItemText
|
||||||
|
primary={t("settings.components.verge.theme.fields.cssInjection")}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<EditRounded />}
|
startIcon={<EditRounded />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -124,12 +153,12 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
|
|||||||
setEditorOpen(true);
|
setEditorOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Edit")} CSS
|
{t("settings.components.verge.theme.actions.editCss")}
|
||||||
</Button>
|
</Button>
|
||||||
{editorOpen && (
|
{editorOpen && (
|
||||||
<EditorViewer
|
<EditorViewer
|
||||||
open={true}
|
open={true}
|
||||||
title={`${t("Edit")} CSS`}
|
title={t("settings.components.verge.theme.dialogs.editCssTitle")}
|
||||||
initialData={Promise.resolve(theme.css_injection ?? "")}
|
initialData={Promise.resolve(theme.css_injection ?? "")}
|
||||||
language="css"
|
language="css"
|
||||||
onSave={(_prev, curr) => {
|
onSave={(_prev, curr) => {
|
||||||
|
|||||||
@@ -80,13 +80,13 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await enhanceProfiles();
|
await enhanceProfiles();
|
||||||
showNotice("success", t("Settings Applied"));
|
showNotice.success("settings.modals.tun.messages.applied");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
open={open}
|
open={open}
|
||||||
title={
|
title={
|
||||||
<Box display="flex" justifyContent="space-between" gap={1}>
|
<Box display="flex" justifyContent="space-between" gap={1}>
|
||||||
<Typography variant="h6">{t("Tun Mode")}</Typography>
|
<Typography variant="h6">{t("settings.modals.tun.title")}</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -128,20 +128,20 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Reset to Default")}
|
{t("shared.actions.resetToDefault")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
contentSx={{ width: 450 }}
|
contentSx={{ width: 450 }}
|
||||||
okBtn={t("Save")}
|
okBtn={t("shared.actions.save")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Stack")} />
|
<ListItemText primary={t("settings.modals.tun.fields.stack")} />
|
||||||
<StackModeSwitch
|
<StackModeSwitch
|
||||||
value={values.stack}
|
value={values.stack}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -154,7 +154,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Device")} />
|
<ListItemText primary={t("settings.modals.tun.fields.device")} />
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -171,7 +171,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Auto Route")} />
|
<ListItemText primary={t("settings.modals.tun.fields.autoRoute")} />
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
checked={values.autoRoute}
|
checked={values.autoRoute}
|
||||||
@@ -180,7 +180,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Strict Route")} />
|
<ListItemText primary={t("settings.modals.tun.fields.strictRoute")} />
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
checked={values.strictRoute}
|
checked={values.strictRoute}
|
||||||
@@ -189,7 +189,9 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Auto Detect Interface")} />
|
<ListItemText
|
||||||
|
primary={t("settings.modals.tun.fields.autoDetectInterface")}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
checked={values.autoDetectInterface}
|
checked={values.autoDetectInterface}
|
||||||
@@ -200,7 +202,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("DNS Hijack")} />
|
<ListItemText primary={t("settings.modals.tun.fields.dnsHijack")} />
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -209,7 +211,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
sx={{ width: 250 }}
|
sx={{ width: 250 }}
|
||||||
value={values.dnsHijack.join(",")}
|
value={values.dnsHijack.join(",")}
|
||||||
placeholder="Please use , to separate multiple DNS servers"
|
placeholder={t("settings.modals.tun.tooltips.dnsHijack")}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setValues((v) => ({ ...v, dnsHijack: e.target.value.split(",") }))
|
setValues((v) => ({ ...v, dnsHijack: e.target.value.split(",") }))
|
||||||
}
|
}
|
||||||
@@ -217,7 +219,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("MTU")} />
|
<ListItemText primary={t("settings.modals.tun.fields.mtu")} />
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
const onUpdate = useLockFn(async () => {
|
const onUpdate = useLockFn(async () => {
|
||||||
if (portableFlag) {
|
if (portableFlag) {
|
||||||
showNotice("error", t("Portable Updater Error"));
|
showNotice.error("settings.modals.update.messages.portableError");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!updateInfo?.body) return;
|
if (!updateInfo?.body) return;
|
||||||
if (breakChangeFlag) {
|
if (breakChangeFlag) {
|
||||||
showNotice("error", t("Break Change Update Error"));
|
showNotice.error("settings.modals.update.messages.breakChangeError");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (updateState) return;
|
if (updateState) return;
|
||||||
@@ -89,7 +89,7 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
await updateInfo.downloadAndInstall();
|
await updateInfo.downloadAndInstall();
|
||||||
await relaunch();
|
await relaunch();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice("error", err?.message || err.toString());
|
showNotice.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
setUpdateState(false);
|
setUpdateState(false);
|
||||||
if (progressListener) {
|
if (progressListener) {
|
||||||
@@ -113,7 +113,9 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
open={open}
|
open={open}
|
||||||
title={
|
title={
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
{`New Version v${updateInfo?.version}`}
|
{t("settings.modals.update.title", {
|
||||||
|
version: updateInfo?.version ?? "",
|
||||||
|
})}
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -124,14 +126,14 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Go to Release Page")}
|
{t("settings.modals.update.actions.goToRelease")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
contentSx={{ minWidth: 360, maxWidth: 400, height: "50vh" }}
|
contentSx={{ minWidth: 360, maxWidth: 400, height: "50vh" }}
|
||||||
okBtn={t("Update")}
|
okBtn={t("settings.modals.update.actions.update")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onUpdate}
|
onOk={onUpdate}
|
||||||
|
|||||||
@@ -56,11 +56,13 @@ export const WebUIItem = (props: Props) => {
|
|||||||
size="small"
|
size="small"
|
||||||
value={editValue}
|
value={editValue}
|
||||||
onChange={(e) => setEditValue(e.target.value)}
|
onChange={(e) => setEditValue(e.target.value)}
|
||||||
placeholder={t("Support %host, %port, %secret")}
|
placeholder={t(
|
||||||
|
"settings.modals.webUI.messages.supportedPlaceholders",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title={t("Save")}
|
title={t("shared.actions.save")}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(editValue);
|
onChange(editValue);
|
||||||
@@ -71,7 +73,7 @@ export const WebUIItem = (props: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title={t("Cancel")}
|
title={t("shared.actions.cancel")}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onCancel?.();
|
onCancel?.();
|
||||||
@@ -127,7 +129,7 @@ export const WebUIItem = (props: Props) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title={t("Open URL")}
|
title={t("settings.modals.webUI.actions.openUrl")}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={() => onOpenUrl?.(value)}
|
onClick={() => onOpenUrl?.(value)}
|
||||||
>
|
>
|
||||||
@@ -135,7 +137,7 @@ export const WebUIItem = (props: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title={t("Edit")}
|
title={t("shared.actions.edit")}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
@@ -146,7 +148,7 @@ export const WebUIItem = (props: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title={t("Delete")}
|
title={t("shared.actions.delete")}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
await openWebUrl(url);
|
await openWebUrl(url);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
showNotice("error", e.message || e.toString());
|
showNotice.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,14 +101,14 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
open={open}
|
open={open}
|
||||||
title={
|
title={
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
{t("Web UI")}
|
{t("settings.modals.webUI.title")}
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
disabled={editing}
|
disabled={editing}
|
||||||
onClick={() => setEditing(true)}
|
onClick={() => setEditing(true)}
|
||||||
>
|
>
|
||||||
{t("New")}
|
{t("shared.actions.new")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
userSelect: "text",
|
userSelect: "text",
|
||||||
}}
|
}}
|
||||||
cancelBtn={t("Close")}
|
cancelBtn={t("shared.actions.close")}
|
||||||
disableOk
|
disableOk
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
@@ -128,7 +128,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
<BaseEmpty
|
<BaseEmpty
|
||||||
extra={
|
extra={
|
||||||
<Typography mt={2} sx={{ fontSize: "12px" }}>
|
<Typography mt={2} sx={{ fontSize: "12px" }}>
|
||||||
{t("Replace host, port, secret with %host, %port, %secret")}
|
{t("settings.modals.webUI.messages.placeholderInstruction")}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user