Compare commits
106 Commits
renovate/n
...
chore/i18n
@@ -9,6 +9,19 @@ if ! command -v pnpm >/dev/null 2>&1; then
|
||||
exit 1
|
||||
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..."
|
||||
pnpm exec lint-staged
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
files:
|
||||
- source: /src/locales/en.json
|
||||
translation: /src/locales
|
||||
multilingual: 1
|
||||
@@ -1,81 +1,79 @@
|
||||
# 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:
|
||||
- Detect duplicated top-level keys
|
||||
- Find keys missing versus a base file (default: en.json)
|
||||
- Export missing entries for translators
|
||||
- Reorder keys to match the base file for predictable diffs
|
||||
- Operate on a directory or a single file
|
||||
- Update the language folder under `src/locales/<lang>/`; use `src/locales/en/` as the canonical reference for keys and intent.
|
||||
- Run `pnpm format:i18n` to align structure and `pnpm i18n:types` to refresh generated typings.
|
||||
- If you touch backend copy, edit the matching YAML file in `src-tauri/locales/<lang>.yml`.
|
||||
- Preview UI changes with `pnpm dev` (desktop shell) or `pnpm web:dev` (web only).
|
||||
- Keep PRs focused and add screenshots whenever layout could be affected by text length.
|
||||
|
||||
Get the CLI (No binary provided yet)
|
||||
## Frontend locale structure
|
||||
|
||||
```bash
|
||||
git clone https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
||||
cd clash-verge-rev-i18n-cli
|
||||
cargo install --path .
|
||||
# or
|
||||
cargo install --git https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
||||
Each locale folder mirrors the namespaces under `src/locales/en/`:
|
||||
|
||||
```
|
||||
src/locales/
|
||||
en/
|
||||
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`
|
||||
- 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`
|
||||
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.
|
||||
|
||||
Options (short)
|
||||
## Tooling for frontend contributors
|
||||
|
||||
- `-d, --directory <DIR>`
|
||||
- `-f, --file <FILE>`
|
||||
- `-k, --duplicated-key`
|
||||
- `-m, --missing-key`
|
||||
- `-e, --export <DIR>`
|
||||
- `-s, --sort`
|
||||
- `-b, --base <FILE>`
|
||||
- `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.
|
||||
- `pnpm node scripts/cleanup-unused-i18n.mjs` (without flags) performs a dry-run audit. Use it to inspect missing or extra keys before committing.
|
||||
- `pnpm i18n:types` regenerates `src/types/generated/i18n-keys.ts` and `src/types/generated/i18n-resources.ts`, ensuring TypeScript catches invalid key usage.
|
||||
- For dynamic keys that the analyzer cannot statically detect, add explicit references in code or update the script whitelist to avoid false positives.
|
||||
|
||||
Exit codes
|
||||
## Backend (Tauri) locale bundles
|
||||
|
||||
- `0` — success (no issues)
|
||||
- `1` — issues found (duplicates/missing)
|
||||
- `2` — error (IO/parse/runtime)
|
||||
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.
|
||||
|
||||
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.
|
||||
- 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.
|
||||
## Adding a new language
|
||||
|
||||
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.
|
||||
- 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.
|
||||
## Authoring guidelines
|
||||
|
||||
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.
|
||||
- Exported JSONs are starter files for translators (fill in values, keep keys).
|
||||
- Sorting keeps diffs consistent and reviewable.
|
||||
## Testing & QA
|
||||
|
||||
Repository
|
||||
https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
||||
- Launch the desktop shell with `pnpm dev` (or `pnpm web:dev`) and navigate through the affected views to confirm translations load and layouts behave.
|
||||
- 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.
|
||||
- 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.
|
||||
- If you need help or a review, leave a comment on your submission requesting assistance.
|
||||
- File an issue for missing context, tooling bugs, or localization gaps so we can track them.
|
||||
- PRs that touch UI should include screenshots or GIFs whenever text length may affect layout.
|
||||
- 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: {
|
||||
js: eslintJS,
|
||||
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
|
||||
"react-hooks": pluginReactHooks,
|
||||
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
|
||||
"import-x": pluginImportX,
|
||||
@@ -132,4 +133,14 @@ export default defineConfig([
|
||||
"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",
|
||||
"format": "prettier --write .",
|
||||
"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",
|
||||
"test": "vitest run"
|
||||
},
|
||||
@@ -57,7 +59,7 @@
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "1.11.19",
|
||||
"foxact": "^0.2.49",
|
||||
"i18next": "^25.6.1",
|
||||
"i18next": "^25.6.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -96,6 +98,7 @@
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-i18next": "^6.1.3",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
|
||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -78,8 +78,8 @@ importers:
|
||||
specifier: ^0.2.49
|
||||
version: 0.2.49(react@19.2.0)
|
||||
i18next:
|
||||
specifier: ^25.6.1
|
||||
version: 25.6.1(typescript@5.9.3)
|
||||
specifier: ^25.6.0
|
||||
version: 25.6.0(typescript@5.9.3)
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
@@ -112,7 +112,7 @@ importers:
|
||||
version: 7.66.0(react@19.2.0)
|
||||
react-i18next:
|
||||
specifier: 16.2.4
|
||||
version: 16.2.4(i18next@25.6.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
version: 16.2.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react-markdown:
|
||||
specifier: 10.1.0
|
||||
version: 10.1.0(@types/react@19.2.2)(react@19.2.0)
|
||||
@@ -189,6 +189,9 @@ importers:
|
||||
eslint-import-resolver-typescript:
|
||||
specifier: ^4.4.4
|
||||
version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-i18next:
|
||||
specifier: ^6.1.3
|
||||
version: 6.1.3
|
||||
eslint-plugin-import-x:
|
||||
specifier: ^4.16.1
|
||||
version: 4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))
|
||||
@@ -2566,6 +2569,10 @@ packages:
|
||||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
|
||||
eslint-plugin-i18next@6.1.3:
|
||||
resolution: {integrity: sha512-z/h4oBRd9wI1ET60HqcLSU6XPeAh/EPOrBBTyCdkWeMoYrWAaUVA+DOQkWTiNIyCltG4NTmy62SQisVXxoXurw==}
|
||||
engines: {node: '>=18.10.0'}
|
||||
|
||||
eslint-plugin-import-x@4.16.1:
|
||||
resolution: {integrity: sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -2943,8 +2950,8 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
i18next@25.6.1:
|
||||
resolution: {integrity: sha512-yUWvdXtalZztmKrKw3yz/AvSP3yKyqIkVPx/wyvoYy9lkLmwzItLxp0iHZLG5hfVQ539Jor4XLO+U+NHIXg7pw==}
|
||||
i18next@25.6.0:
|
||||
resolution: {integrity: sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==}
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
@@ -3724,6 +3731,10 @@ packages:
|
||||
remark-rehype@11.1.2:
|
||||
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
|
||||
|
||||
requireindex@1.1.0:
|
||||
resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==}
|
||||
engines: {node: '>=0.10.5'}
|
||||
|
||||
reselect@5.1.1:
|
||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||
|
||||
@@ -6853,6 +6864,11 @@ snapshots:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
eslint-plugin-i18next@6.1.3:
|
||||
dependencies:
|
||||
lodash: 4.17.21
|
||||
requireindex: 1.1.0
|
||||
|
||||
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.46.3
|
||||
@@ -7359,7 +7375,7 @@ snapshots:
|
||||
|
||||
husky@9.1.7: {}
|
||||
|
||||
i18next@25.6.1(typescript@5.9.3):
|
||||
i18next@25.6.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
optionalDependencies:
|
||||
@@ -8183,11 +8199,11 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
react-i18next@16.2.4(i18next@25.6.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
|
||||
react-i18next@16.2.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 25.6.1(typescript@5.9.3)
|
||||
i18next: 25.6.0(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
use-sync-external-store: 1.6.0(react@19.2.0)
|
||||
optionalDependencies:
|
||||
@@ -8310,6 +8326,8 @@ snapshots:
|
||||
unified: 11.0.5
|
||||
vfile: 6.0.3
|
||||
|
||||
requireindex@1.1.0: {}
|
||||
|
||||
reselect@5.1.1: {}
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
@@ -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();
|
||||
1321
scripts/cleanup-unused-i18n.mjs
Normal file
1321
scripts/cleanup-unused-i18n.mjs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
/**
|
||||
* 为Alpha版本重命名版本号
|
||||
|
||||
98
scripts/generate-i18n-keys.mjs
Normal file
98
scripts/generate-i18n-keys.mjs
Normal file
@@ -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 fsp from "fs/promises";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { createRequire } from "module";
|
||||
import path from "path";
|
||||
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import AdmZip from "adm-zip";
|
||||
|
||||
const target = process.argv.slice(2)[0];
|
||||
const alpha = process.argv.slice(2)[1];
|
||||
@@ -79,11 +80,11 @@ async function resolvePortable() {
|
||||
tag,
|
||||
});
|
||||
|
||||
let assets = release.assets.filter((x) => {
|
||||
const assets = release.assets.filter((x) => {
|
||||
return x.name === zipFile;
|
||||
});
|
||||
if (assets.length > 0) {
|
||||
let id = assets[0].id;
|
||||
const id = assets[0].id;
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
asset_id: id,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { createRequire } from "module";
|
||||
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 ARCH_MAP = {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import AdmZip from "adm-zip";
|
||||
import { execSync } from "child_process";
|
||||
import { createHash } from "crypto";
|
||||
import fs from "fs";
|
||||
import fsp from "fs/promises";
|
||||
import path from "path";
|
||||
import zlib from "zlib";
|
||||
|
||||
import AdmZip from "adm-zip";
|
||||
import { glob } from "glob";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import fetch from "node-fetch";
|
||||
import path from "path";
|
||||
import { extract } from "tar";
|
||||
import zlib from "zlib";
|
||||
|
||||
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 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
|
||||
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
|
||||
: process;
|
||||
@@ -113,7 +115,7 @@ async function calculateFileHash(filePath) {
|
||||
const hashSum = createHash("sha256");
|
||||
hashSum.update(fileBuffer);
|
||||
return hashSum.digest("hex");
|
||||
} catch (err) {
|
||||
} catch (ignoreErr) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -547,9 +549,9 @@ const resolveServicePermission = async () => {
|
||||
const hashCache = await loadHashCache();
|
||||
let hasChanges = false;
|
||||
|
||||
for (let f of serviceExecutables) {
|
||||
for (const f of serviceExecutables) {
|
||||
const files = glob.sync(path.join(resDir, f));
|
||||
for (let filePath of files) {
|
||||
for (const filePath of files) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const currentHash = await calculateFileHash(filePath);
|
||||
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)
|
||||
// =======================
|
||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
|
||||
const resolveService = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
const ext = platform === "win32" ? ".exe" : "";
|
||||
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
return resolveResource({
|
||||
file: "clash-verge-service" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
||||
});
|
||||
};
|
||||
const resolveInstall = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
const ext = platform === "win32" ? ".exe" : "";
|
||||
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
return resolveResource({
|
||||
file: "clash-verge-service-install" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
|
||||
});
|
||||
};
|
||||
const resolveUninstall = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
const ext = platform === "win32" ? ".exe" : "";
|
||||
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
return resolveResource({
|
||||
file: "clash-verge-service-uninstall" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
|
||||
@@ -715,7 +694,6 @@ const tasks = [
|
||||
retry: 5,
|
||||
macosOnly: true,
|
||||
},
|
||||
{ name: "locales", func: resolveLocales, retry: 2 },
|
||||
];
|
||||
|
||||
async function runTask() {
|
||||
|
||||
@@ -30,10 +30,11 @@
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { program } from "commander";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
import { program } from "commander";
|
||||
|
||||
/**
|
||||
* 获取当前 git 短 commit hash
|
||||
* @returns {string}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import axios from "axios";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import { log_error, log_info, log_success } from "./utils.mjs";
|
||||
|
||||
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
|
||||
|
||||
@@ -58,7 +58,7 @@ export async function resolveUpdateLogDefault() {
|
||||
const reEnd = /^---/;
|
||||
|
||||
let isCapturing = false;
|
||||
let content = [];
|
||||
const content = [];
|
||||
let firstTag = "";
|
||||
|
||||
for (const line of data.split("\n")) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fetch from "node-fetch";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
||||
|
||||
const UPDATE_TAG_NAME = "updater";
|
||||
@@ -113,7 +114,7 @@ async function resolveUpdater() {
|
||||
});
|
||||
|
||||
// delete the old assets
|
||||
for (let asset of updateRelease.assets) {
|
||||
for (const asset of updateRelease.assets) {
|
||||
if (asset.name === UPDATE_JSON_FILE) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fetch from "node-fetch";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||
|
||||
// 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;
|
||||
|
||||
// Delete existing assets with these names
|
||||
for (let asset of updateRelease.assets) {
|
||||
for (const asset of updateRelease.assets) {
|
||||
if (asset.name === jsonFile) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
|
||||
198
src-tauri/Cargo.lock
generated
198
src-tauri/Cargo.lock
generated
@@ -572,6 +572,12 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base62"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
@@ -822,6 +828,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
@@ -1131,6 +1147,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"reqwest_dav",
|
||||
"runas",
|
||||
"rust-i18n",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2848,7 +2865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro-crate 2.0.2",
|
||||
"proc-macro-crate 2.0.0",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2889,6 +2906,30 @@ dependencies = [
|
||||
"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]]
|
||||
name = "gobject-sys"
|
||||
version = "0.18.0"
|
||||
@@ -3513,6 +3554,22 @@ dependencies = [
|
||||
"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]]
|
||||
name = "image"
|
||||
version = "0.25.8"
|
||||
@@ -3702,6 +3759,15 @@ dependencies = [
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
@@ -4433,6 +4499,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "notify-rust"
|
||||
version = "4.11.7"
|
||||
@@ -5555,11 +5630,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "2.0.2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
|
||||
checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
|
||||
dependencies = [
|
||||
"toml_datetime 0.6.3",
|
||||
"toml_edit 0.20.2",
|
||||
]
|
||||
|
||||
@@ -6231,6 +6305,60 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.3"
|
||||
@@ -6673,6 +6801,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "serde_yaml_ng"
|
||||
version = "0.10.0"
|
||||
@@ -7186,7 +7327,7 @@ dependencies = [
|
||||
"cfg-expr",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.8.2",
|
||||
"toml 0.8.23",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
@@ -8127,14 +8268,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.2"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.3",
|
||||
"toml_edit 0.20.2",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_edit 0.22.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8154,9 +8295,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.3"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -8177,7 +8318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.12.0",
|
||||
"toml_datetime 0.6.3",
|
||||
"toml_datetime 0.6.11",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
@@ -8186,12 +8327,24 @@ name = "toml_edit"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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 = [
|
||||
"indexmap 2.12.0",
|
||||
"serde",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.3",
|
||||
"winnow 0.5.40",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"winnow 0.7.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8215,6 +8368,12 @@ dependencies = [
|
||||
"winnow 0.7.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.4"
|
||||
@@ -8514,6 +8673,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
|
||||
@@ -87,6 +87,7 @@ clash_verge_service_ipc = { version = "2.0.21", features = [
|
||||
"client",
|
||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||
arc-swap = "1.7.1"
|
||||
rust-i18n = "3.1.5"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
|
||||
52
src-tauri/locales/ar.yml
Normal file
52
src-tauri/locales/ar.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/de.yml
Normal file
52
src-tauri/locales/de.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/en.yml
Normal file
52
src-tauri/locales/en.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/es.yml
Normal file
52
src-tauri/locales/es.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/fa.yml
Normal file
52
src-tauri/locales/fa.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/id.yml
Normal file
52
src-tauri/locales/id.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/jp.yml
Normal file
52
src-tauri/locales/jp.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/ko.yml
Normal file
52
src-tauri/locales/ko.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/ru.yml
Normal file
52
src-tauri/locales/ru.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/tr.yml
Normal file
52
src-tauri/locales/tr.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/tt.yml
Normal file
52
src-tauri/locales/tt.yml
Normal file
@@ -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
|
||||
52
src-tauri/locales/zh.yml
Normal file
52
src-tauri/locales/zh.yml
Normal file
@@ -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: 订阅
|
||||
52
src-tauri/locales/zhtw.yml
Normal file
52
src-tauri/locales/zhtw.yml
Normal file
@@ -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 crate::{
|
||||
core::service::{self, SERVICE_MANAGER, ServiceStatus},
|
||||
utils::i18n::t,
|
||||
};
|
||||
use crate::core::service::{self, SERVICE_MANAGER, ServiceStatus};
|
||||
use smartstring::SmartString;
|
||||
|
||||
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
|
||||
{
|
||||
let emsg = format!("{} Service failed: {}", op_type, e);
|
||||
return Err(SmartString::from(&*t(emsg.as_str()).await));
|
||||
return Err(SmartString::from(emsg));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
time::Instant,
|
||||
};
|
||||
use tauri::{Emitter, WebviewWindow};
|
||||
|
||||
@@ -92,15 +92,12 @@ impl NotificationSystem {
|
||||
}
|
||||
|
||||
fn worker_loop(rx: mpsc::Receiver<FrontendEvent>) {
|
||||
loop {
|
||||
let handle = Handle::global();
|
||||
if handle.is_exiting() {
|
||||
break;
|
||||
}
|
||||
match rx.recv_timeout(Duration::from_millis(1_000)) {
|
||||
let handle = Handle::global();
|
||||
while !handle.is_exiting() {
|
||||
match rx.try_recv() {
|
||||
Ok(event) => Self::process_event(handle, event),
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => (),
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
Err(mpsc::TryRecvError::Disconnected) => break,
|
||||
Err(mpsc::TryRecvError::Empty) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,8 +220,6 @@ async fn reinstall_service() -> Result<()> {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn uninstall_service() -> Result<()> {
|
||||
use crate::utils::i18n::t;
|
||||
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let binary_path = dirs::service_path()?;
|
||||
@@ -233,7 +231,9 @@ async fn uninstall_service() -> Result<()> {
|
||||
|
||||
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!(
|
||||
r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||
);
|
||||
@@ -256,8 +256,6 @@ async fn uninstall_service() -> Result<()> {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn install_service() -> Result<()> {
|
||||
use crate::utils::i18n::t;
|
||||
|
||||
logging!(info, Type::Service, "install service");
|
||||
|
||||
let binary_path = dirs::service_path()?;
|
||||
@@ -269,7 +267,9 @@ async fn install_service() -> Result<()> {
|
||||
|
||||
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!(
|
||||
r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||
);
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
use crate::utils::i18n::t;
|
||||
use std::sync::Arc;
|
||||
use rust_i18n::t;
|
||||
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 {
|
||||
($($field:ident => $const_name:ident, $id:expr, $text:expr),+ $(,)?) => {
|
||||
@@ -11,9 +18,10 @@ macro_rules! define_menu {
|
||||
pub struct MenuIds;
|
||||
|
||||
impl MenuTexts {
|
||||
pub async fn new() -> Self {
|
||||
let ($($field,)+) = futures::join!($(t($text),)+);
|
||||
Self { $($field,)+ }
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
$($field: to_arc_str(t!($text)),)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,26 +32,26 @@ macro_rules! define_menu {
|
||||
}
|
||||
|
||||
define_menu! {
|
||||
dashboard => DASHBOARD, "tray_dashboard", "Dashboard",
|
||||
rule_mode => RULE_MODE, "tray_rule_mode", "Rule Mode",
|
||||
global_mode => GLOBAL_MODE, "tray_global_mode", "Global Mode",
|
||||
direct_mode => DIRECT_MODE, "tray_direct_mode", "Direct Mode",
|
||||
profiles => PROFILES, "tray_profiles", "Profiles",
|
||||
proxies => PROXIES, "tray_proxies", "Proxies",
|
||||
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "System Proxy",
|
||||
tun_mode => TUN_MODE, "tray_tun_mode", "TUN Mode",
|
||||
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "Close All Connections",
|
||||
lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "LightWeight Mode",
|
||||
copy_env => COPY_ENV, "tray_copy_env", "Copy Env",
|
||||
conf_dir => CONF_DIR, "tray_conf_dir", "Conf Dir",
|
||||
core_dir => CORE_DIR, "tray_core_dir", "Core Dir",
|
||||
logs_dir => LOGS_DIR, "tray_logs_dir", "Logs Dir",
|
||||
open_dir => OPEN_DIR, "tray_open_dir", "Open Dir",
|
||||
app_log => APP_LOG, "tray_app_log", "Open App Log",
|
||||
core_log => CORE_LOG, "tray_core_log", "Open Core Log",
|
||||
restart_clash => RESTART_CLASH, "tray_restart_clash", "Restart Clash Core",
|
||||
restart_app => RESTART_APP, "tray_restart_app", "Restart App",
|
||||
verge_version => VERGE_VERSION, "tray_verge_version", "Verge Version",
|
||||
more => MORE, "tray_more", "More",
|
||||
exit => EXIT, "tray_exit", "Exit",
|
||||
dashboard => DASHBOARD, "tray_dashboard", "tray.dashboard",
|
||||
rule_mode => RULE_MODE, "tray_rule_mode", "tray.ruleMode",
|
||||
global_mode => GLOBAL_MODE, "tray_global_mode", "tray.globalMode",
|
||||
direct_mode => DIRECT_MODE, "tray_direct_mode", "tray.directMode",
|
||||
profiles => PROFILES, "tray_profiles", "tray.profiles",
|
||||
proxies => PROXIES, "tray_proxies", "tray.proxies",
|
||||
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy",
|
||||
tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode",
|
||||
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections",
|
||||
lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "tray.lightweightMode",
|
||||
copy_env => COPY_ENV, "tray_copy_env", "tray.copyEnv",
|
||||
conf_dir => CONF_DIR, "tray_conf_dir", "tray.confDir",
|
||||
core_dir => CORE_DIR, "tray_core_dir", "tray.coreDir",
|
||||
logs_dir => LOGS_DIR, "tray_logs_dir", "tray.logsDir",
|
||||
open_dir => OPEN_DIR, "tray_open_dir", "tray.openDir",
|
||||
app_log => APP_LOG, "tray_app_log", "tray.appLog",
|
||||
core_log => CORE_LOG, "tray_core_log", "tray.coreLog",
|
||||
restart_clash => RESTART_CLASH, "tray_restart_clash", "tray.restartClash",
|
||||
restart_app => RESTART_APP, "tray_restart_app", "tray.restartApp",
|
||||
verge_version => VERGE_VERSION, "tray_verge_version", "tray.vergeVersion",
|
||||
more => MORE, "tray_more", "tray.more",
|
||||
exit => EXIT, "tray_exit", "tray.exit",
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::{
|
||||
feat, logging,
|
||||
module::lightweight::is_in_lightweight_mode,
|
||||
singleton_lazy,
|
||||
utils::{dirs::find_target_icons, i18n::t},
|
||||
utils::{dirs::find_target_icons, i18n},
|
||||
};
|
||||
|
||||
use super::handle;
|
||||
@@ -440,6 +440,8 @@ impl Tray {
|
||||
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
|
||||
i18n::sync_locale().await;
|
||||
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
@@ -466,9 +468,9 @@ impl Tray {
|
||||
}
|
||||
|
||||
// Get localized strings before using them
|
||||
let sys_proxy_text = t("SysProxy").await;
|
||||
let tun_text = t("TUN").await;
|
||||
let profile_text = t("Profile").await;
|
||||
let sys_proxy_text = rust_i18n::t!("tray.tooltip.systemProxy");
|
||||
let tun_text = rust_i18n::t!("tray.tooltip.tun");
|
||||
let profile_text = rust_i18n::t!("tray.tooltip.profile");
|
||||
|
||||
let v = env!("CARGO_PKG_VERSION");
|
||||
let reassembled_version = v.split_once('+').map_or_else(
|
||||
@@ -639,7 +641,7 @@ async fn create_profile_menu_item(
|
||||
CheckMenuItem::with_id(
|
||||
&app_handle,
|
||||
format!("profiles_{profile_uid}"),
|
||||
t(profile_name).await,
|
||||
profile_name.as_str(),
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
@@ -834,6 +836,8 @@ async fn create_tray_menu(
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let current_proxy_mode = mode.unwrap_or("");
|
||||
|
||||
i18n::sync_locale().await;
|
||||
|
||||
// 获取当前配置文件的选中代理组信息
|
||||
let current_profile_selected = {
|
||||
let profiles_config = Config::profiles().await;
|
||||
@@ -894,7 +898,7 @@ async fn create_tray_menu(
|
||||
create_profile_menu_item(app_handle, profile_uid_and_name).await?;
|
||||
|
||||
// Pre-fetch all localized strings
|
||||
let texts = &MenuTexts::new().await;
|
||||
let texts = MenuTexts::new();
|
||||
// Convert to references only when needed
|
||||
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||
.iter()
|
||||
|
||||
@@ -25,12 +25,15 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use config::Config;
|
||||
use once_cell::sync::OnceCell;
|
||||
use rust_i18n::i18n;
|
||||
use tauri::{AppHandle, Manager};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use utils::logging::Type;
|
||||
|
||||
i18n!("locales", fallback = "zh");
|
||||
|
||||
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
|
||||
/// Application initialization helper functions
|
||||
mod app_init {
|
||||
|
||||
@@ -1,43 +1,80 @@
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use once_cell::sync::Lazy;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use crate::config::Config;
|
||||
use sys_locale;
|
||||
|
||||
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> {
|
||||
dirs::app_resources_dir()
|
||||
.map(|resource_path| resource_path.join("locales"))
|
||||
.ok()
|
||||
const fn fallback_language() -> &'static str {
|
||||
DEFAULT_LANGUAGE
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut languages = Vec::new();
|
||||
supported_languages_internal()
|
||||
.into_iter()
|
||||
.map(|lang| lang.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
if let Some(locales_dir) = get_locales_dir()
|
||||
&& let Ok(entries) = fs::read_dir(locales_dir)
|
||||
{
|
||||
for entry in entries.flatten() {
|
||||
if let Some(file_name) = entry.file_name().to_str()
|
||||
&& let Some(lang) = file_name.strip_suffix(".json")
|
||||
{
|
||||
languages.push(lang.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if languages.is_empty() {
|
||||
languages.push(DEFAULT_LANGUAGE.into());
|
||||
}
|
||||
languages
|
||||
pub fn set_locale(language: &str) {
|
||||
let lang =
|
||||
resolve_supported_language(language).unwrap_or_else(|| fallback_language().to_string());
|
||||
rust_i18n::set_locale(&lang);
|
||||
}
|
||||
|
||||
pub async fn current_language() -> String {
|
||||
@@ -45,70 +82,18 @@ pub async fn current_language() -> String {
|
||||
.await
|
||||
.latest_arc()
|
||||
.language
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
.unwrap_or_else(get_system_language)
|
||||
.clone()
|
||||
.filter(|lang| !lang.is_empty())
|
||||
.and_then(|lang| resolve_supported_language(&lang))
|
||||
.unwrap_or_else(system_language)
|
||||
}
|
||||
|
||||
static TRANSLATIONS: Lazy<RwLock<TranslationMap>> = Lazy::new(|| {
|
||||
let lang = get_system_language();
|
||||
let map = load_lang_file(&lang).unwrap_or_default();
|
||||
RwLock::new((lang, map))
|
||||
});
|
||||
|
||||
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()
|
||||
})
|
||||
pub async fn sync_locale() -> String {
|
||||
let language = current_language().await;
|
||||
set_locale(&language);
|
||||
language
|
||||
}
|
||||
|
||||
fn get_system_language() -> String {
|
||||
sys_locale::get_locale()
|
||||
.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)
|
||||
pub const fn default_language() -> &'static str {
|
||||
fallback_language()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::{core::handle, utils::i18n::t};
|
||||
|
||||
use crate::{core::handle, utils::i18n};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
|
||||
pub enum NotificationEvent<'a> {
|
||||
@@ -27,48 +26,44 @@ fn notify(title: &str, body: &str) {
|
||||
}
|
||||
|
||||
pub async fn notify_event<'a>(event: NotificationEvent<'a>) {
|
||||
i18n::sync_locale().await;
|
||||
|
||||
match event {
|
||||
NotificationEvent::DashboardToggled => {
|
||||
notify(
|
||||
&t("DashboardToggledTitle").await,
|
||||
&t("DashboardToggledBody").await,
|
||||
);
|
||||
let title = rust_i18n::t!("notifications.dashboardToggled.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.dashboardToggled.body").to_string();
|
||||
notify(&title, &body);
|
||||
}
|
||||
NotificationEvent::ClashModeChanged { mode } => {
|
||||
notify(
|
||||
&t("ClashModeChangedTitle").await,
|
||||
&t_with_args("ClashModeChangedBody", mode).await,
|
||||
);
|
||||
let title = rust_i18n::t!("notifications.clashModeChanged.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.clashModeChanged.body").replace("{mode}", mode);
|
||||
notify(&title, &body);
|
||||
}
|
||||
NotificationEvent::SystemProxyToggled => {
|
||||
notify(
|
||||
&t("SystemProxyToggledTitle").await,
|
||||
&t("SystemProxyToggledBody").await,
|
||||
);
|
||||
let title = rust_i18n::t!("notifications.systemProxyToggled.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.systemProxyToggled.body").to_string();
|
||||
notify(&title, &body);
|
||||
}
|
||||
NotificationEvent::TunModeToggled => {
|
||||
notify(
|
||||
&t("TunModeToggledTitle").await,
|
||||
&t("TunModeToggledBody").await,
|
||||
);
|
||||
let title = rust_i18n::t!("notifications.tunModeToggled.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.tunModeToggled.body").to_string();
|
||||
notify(&title, &body);
|
||||
}
|
||||
NotificationEvent::LightweightModeEntered => {
|
||||
notify(
|
||||
&t("LightweightModeEnteredTitle").await,
|
||||
&t("LightweightModeEnteredBody").await,
|
||||
);
|
||||
let title = rust_i18n::t!("notifications.lightweightModeEntered.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.lightweightModeEntered.body").to_string();
|
||||
notify(&title, &body);
|
||||
}
|
||||
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")]
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use percent_encoding::percent_decode_str;
|
||||
use smartstring::alias::String;
|
||||
@@ -75,23 +73,24 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
|
||||
"failed to parse profile from url: {:?}",
|
||||
e
|
||||
);
|
||||
// TODO 通知系统疑似损坏,前端无法显示通知事件
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let uid = item.uid.clone().unwrap_or_default();
|
||||
// TODO 通过 deep link 导入后需要正确调用前端刷新订阅页面,以及通知结果
|
||||
match profiles::profiles_append_item_safe(&mut item).await {
|
||||
Ok(_) => {
|
||||
Config::profiles().await.apply();
|
||||
let _ = Config::profiles().await.data_arc().save_file().await;
|
||||
// TODO 通知系统疑似损坏,前端无法显示通知事件
|
||||
handle::Handle::notice_message(
|
||||
"import_sub_url::ok",
|
||||
"", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
item.uid.clone().unwrap_or_default(),
|
||||
);
|
||||
handle::Handle::refresh_verge();
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
// TODO fuck me this shit is fucking broken as fucked
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -102,10 +101,14 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
|
||||
e
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
// TODO 通知系统疑似损坏,前端无法显示通知事件
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
handle::Handle::refresh_verge();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": ["resources", "resources/locales/*"],
|
||||
"resources": ["resources"],
|
||||
"publisher": "Clash Verge Rev",
|
||||
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
|
||||
"copyright": "GNU General Public License v3.0",
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { CloseRounded } from "@mui/icons-material";
|
||||
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
|
||||
import React, { useSyncExternalStore } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
subscribeNotices,
|
||||
hideNotice,
|
||||
getSnapshotNotices,
|
||||
} from "@/services/noticeService";
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
|
||||
export const NoticeManager: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const currentNotices = useSyncExternalStore(
|
||||
subscribeNotices,
|
||||
getSnapshotNotices,
|
||||
@@ -60,7 +63,61 @@ export const NoticeManager: React.FC = () => {
|
||||
</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>
|
||||
</Snackbar>
|
||||
))}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { InboxRounded } from "@mui/icons-material";
|
||||
import { alpha, Box, Typography } from "@mui/material";
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
|
||||
interface Props {
|
||||
text?: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
text?: ReactNode;
|
||||
textKey?: TranslationKey;
|
||||
extra?: ReactNode;
|
||||
}
|
||||
|
||||
export const BaseEmpty = (props: Props) => {
|
||||
const { text = "Empty", extra } = props;
|
||||
export const BaseEmpty = ({
|
||||
text,
|
||||
textKey = "shared.statuses.empty",
|
||||
extra,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resolvedText: ReactNode = text !== undefined ? text : t(textKey);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={({ palette }) => ({
|
||||
@@ -24,7 +33,7 @@ export const BaseEmpty = (props: Props) => {
|
||||
})}
|
||||
>
|
||||
<InboxRounded sx={{ fontSize: "4em" }} />
|
||||
<Typography sx={{ fontSize: "1.25em" }}>{t(`${text}`)}</Typography>
|
||||
<Typography sx={{ fontSize: "1.25em" }}>{resolvedText}</Typography>
|
||||
{extra}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -135,7 +135,7 @@ export const BaseSearchBox = ({
|
||||
if (useRegularExpression && value) {
|
||||
const isValid = validateRegex(value);
|
||||
if (!isValid) {
|
||||
setErrorMessage(t("Invalid regular expression"));
|
||||
setErrorMessage(t("shared.validation.invalidRegex"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ export const BaseSearchBox = ({
|
||||
} else {
|
||||
const value = inputRef.current?.value ?? "";
|
||||
if (value && !validateRegex(value)) {
|
||||
setErrorMessage(t("Invalid regular expression"));
|
||||
setErrorMessage(t("shared.validation.invalidRegex"));
|
||||
}
|
||||
}
|
||||
return next;
|
||||
@@ -173,7 +173,7 @@ export const BaseSearchBox = ({
|
||||
size="small"
|
||||
variant="outlined"
|
||||
spellCheck="false"
|
||||
placeholder={placeholder ?? t("Filter conditions")}
|
||||
placeholder={placeholder ?? t("shared.placeholders.filter")}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
onChange={onChange}
|
||||
error={!!errorMessage}
|
||||
@@ -182,7 +182,7 @@ export const BaseSearchBox = ({
|
||||
sx: { pr: 1 },
|
||||
endAdornment: (
|
||||
<Box display="flex">
|
||||
<Tooltip title={t("Match Case")}>
|
||||
<Tooltip title={t("shared.placeholders.matchCase")}>
|
||||
<div>
|
||||
<SvgIcon
|
||||
component={matchCaseIcon}
|
||||
@@ -192,7 +192,7 @@ export const BaseSearchBox = ({
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("Match Whole Word")}>
|
||||
<Tooltip title={t("shared.placeholders.matchWholeWord")}>
|
||||
<div>
|
||||
<SvgIcon
|
||||
component={matchWholeWordIcon}
|
||||
@@ -202,7 +202,7 @@ export const BaseSearchBox = ({
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("Use Regular Expression")}>
|
||||
<Tooltip title={t("shared.placeholders.useRegex")}>
|
||||
<div>
|
||||
<SvgIcon
|
||||
component={useRegularExpressionIcon}
|
||||
|
||||
@@ -12,7 +12,7 @@ export const BaseStyledTextField = styled((props: TextFieldProps) => {
|
||||
size="small"
|
||||
variant="outlined"
|
||||
spellCheck="false"
|
||||
placeholder={t("Filter conditions")}
|
||||
placeholder={t("shared.placeholders.filter")}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -177,7 +177,7 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
||||
<ErrorOutlineRounded sx={{ fontSize: 48, mb: 2, color: "error.main" }} />
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("Traffic Statistics Error")}
|
||||
{t("shared.feedback.errors.trafficStats")}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
@@ -186,18 +186,17 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
||||
textAlign="center"
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{t(
|
||||
"The traffic statistics component encountered an error and has been disabled to prevent crashes.",
|
||||
)}
|
||||
{t("shared.feedback.errors.trafficStatsDescription")}
|
||||
</Typography>
|
||||
|
||||
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>Error:</strong> {error?.message || "Unknown error"}
|
||||
<strong>Error:</strong>{" "}
|
||||
{error instanceof Error ? error.message : "Unknown error"}
|
||||
</Typography>
|
||||
{retryCount > 0 && (
|
||||
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
||||
{t("Retry attempts")}: {retryCount}/{maxRetries}
|
||||
{t("shared.labels.retryAttempts")}: {retryCount}/{maxRetries}
|
||||
</Typography>
|
||||
)}
|
||||
</Alert>
|
||||
@@ -211,12 +210,12 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
||||
onClick={onRetry}
|
||||
size="small"
|
||||
>
|
||||
{t("Retry")}
|
||||
{t("shared.actions.retry")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button variant="outlined" onClick={onRefresh} size="small">
|
||||
{t("Refresh Page")}
|
||||
{t("shared.actions.refreshPage")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -225,7 +224,9 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
||||
onClick={onToggleDetails}
|
||||
size="small"
|
||||
>
|
||||
{showDetails ? t("Hide Details") : t("Show Details")}
|
||||
{showDetails
|
||||
? t("shared.actions.hideDetails")
|
||||
: t("shared.actions.showDetails")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box, Button, Snackbar, useTheme } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import { t } from "i18next";
|
||||
import { useImperativeHandle, useState, type Ref } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { closeConnections } from "tauri-plugin-mihomo-api";
|
||||
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
@@ -55,6 +55,7 @@ interface InnerProps {
|
||||
}
|
||||
|
||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { metadata, rulePayload } = data;
|
||||
const theme = useTheme();
|
||||
const chains = [...data.chains].reverse().join(" / ");
|
||||
@@ -67,34 +68,52 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
: metadata.remoteDestination;
|
||||
|
||||
const information = [
|
||||
{ label: t("Host"), value: host },
|
||||
{ label: t("Downloaded"), value: parseTraffic(data.download).join(" ") },
|
||||
{ label: t("Uploaded"), value: parseTraffic(data.upload).join(" ") },
|
||||
{ label: t("connections.components.fields.host"), value: host },
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
label: t("UL Speed"),
|
||||
label: t("connections.components.fields.ulSpeed"),
|
||||
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
label: t("Chains"),
|
||||
label: t("connections.components.fields.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})` : ""}`,
|
||||
},
|
||||
{ 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}`,
|
||||
},
|
||||
{ label: t("Destination"), value: Destination },
|
||||
{ label: t("DestinationPort"), value: `${metadata.destinationPort}` },
|
||||
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
|
||||
{
|
||||
label: t("connections.components.fields.destination"),
|
||||
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));
|
||||
@@ -118,13 +137,13 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
<Box sx={{ textAlign: "right" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
title={t("Close Connection")}
|
||||
title={t("connections.components.actions.closeConnection")}
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
{t("Close Connection")}
|
||||
{t("connections.components.actions.closeConnection")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { closeConnections } from "tauri-plugin-mihomo-api";
|
||||
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
@@ -33,6 +34,7 @@ export const ConnectionItem = (props: Props) => {
|
||||
const { value, onShowDetail } = props;
|
||||
|
||||
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onDelete = useLockFn(async () => closeConnections(id));
|
||||
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
|
||||
@@ -42,7 +44,13 @@ export const ConnectionItem = (props: Props) => {
|
||||
dense
|
||||
sx={{ borderBottom: "1px solid var(--divider-color)" }}
|
||||
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 />
|
||||
</IconButton>
|
||||
}
|
||||
|
||||
@@ -162,13 +162,13 @@ export const ConnectionTable = (props: Props) => {
|
||||
return [
|
||||
{
|
||||
field: "host",
|
||||
headerName: t("Host"),
|
||||
headerName: t("connections.components.fields.host"),
|
||||
width: columnWidths["host"] || 220,
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: "download",
|
||||
headerName: t("Downloaded"),
|
||||
headerName: t("shared.labels.downloaded"),
|
||||
width: columnWidths["download"] || 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
@@ -176,7 +176,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
},
|
||||
{
|
||||
field: "upload",
|
||||
headerName: t("Uploaded"),
|
||||
headerName: t("shared.labels.uploaded"),
|
||||
width: columnWidths["upload"] || 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
@@ -184,7 +184,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
},
|
||||
{
|
||||
field: "dlSpeed",
|
||||
headerName: t("DL Speed"),
|
||||
headerName: t("connections.components.fields.dlSpeed"),
|
||||
width: columnWidths["dlSpeed"] || 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
@@ -192,7 +192,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
},
|
||||
{
|
||||
field: "ulSpeed",
|
||||
headerName: t("UL Speed"),
|
||||
headerName: t("connections.components.fields.ulSpeed"),
|
||||
width: columnWidths["ulSpeed"] || 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
@@ -200,25 +200,25 @@ export const ConnectionTable = (props: Props) => {
|
||||
},
|
||||
{
|
||||
field: "chains",
|
||||
headerName: t("Chains"),
|
||||
headerName: t("connections.components.fields.chains"),
|
||||
width: columnWidths["chains"] || 340,
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: "rule",
|
||||
headerName: t("Rule"),
|
||||
headerName: t("connections.components.fields.rule"),
|
||||
width: columnWidths["rule"] || 280,
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: "process",
|
||||
headerName: t("Process"),
|
||||
headerName: t("connections.components.fields.process"),
|
||||
width: columnWidths["process"] || 220,
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: "time",
|
||||
headerName: t("Time"),
|
||||
headerName: t("connections.components.fields.time"),
|
||||
width: columnWidths["time"] || 120,
|
||||
minWidth: 100,
|
||||
align: "right",
|
||||
@@ -229,19 +229,19 @@ export const ConnectionTable = (props: Props) => {
|
||||
},
|
||||
{
|
||||
field: "source",
|
||||
headerName: t("Source"),
|
||||
headerName: t("connections.components.fields.source"),
|
||||
width: columnWidths["source"] || 200,
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: "remoteDestination",
|
||||
headerName: t("Destination"),
|
||||
headerName: t("connections.components.fields.destination"),
|
||||
width: columnWidths["remoteDestination"] || 200,
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: "type",
|
||||
headerName: t("Type"),
|
||||
headerName: t("connections.components.fields.type"),
|
||||
width: columnWidths["type"] || 160,
|
||||
minWidth: 100,
|
||||
},
|
||||
@@ -250,7 +250,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
|
||||
const handleColumnResize = (params: GridColumnResizeParams) => {
|
||||
const { colDef, width } = params;
|
||||
console.log("Column resize:", colDef.field, width);
|
||||
setColumnWidths((prev) => ({
|
||||
...prev,
|
||||
[colDef.field]: width,
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ClashInfoCard = () => {
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Core Version")}
|
||||
{t("home.components.clashInfo.fields.coreVersion")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashVersion || "-"}
|
||||
@@ -41,7 +41,7 @@ export const ClashInfoCard = () => {
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("System Proxy Address")}
|
||||
{t("home.components.clashInfo.fields.systemProxyAddress")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{systemProxyAddress}
|
||||
@@ -50,7 +50,7 @@ export const ClashInfoCard = () => {
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Mixed Port")}
|
||||
{t("home.components.clashInfo.fields.mixedPort")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashConfig.mixedPort || "-"}
|
||||
@@ -59,7 +59,7 @@ export const ClashInfoCard = () => {
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Uptime")}
|
||||
{t("home.components.clashInfo.fields.uptime")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{formattedUptime}
|
||||
@@ -68,7 +68,7 @@ export const ClashInfoCard = () => {
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Rules Count")}
|
||||
{t("home.components.clashInfo.fields.rulesCount")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{rules.length}
|
||||
@@ -87,7 +87,7 @@ export const ClashInfoCard = () => {
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("Clash Info")}
|
||||
title={t("home.components.clashInfo.title")}
|
||||
icon={<DeveloperBoardOutlined />}
|
||||
iconColor="warning"
|
||||
action={null}
|
||||
|
||||
@@ -12,6 +12,31 @@ import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
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 = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -19,19 +44,21 @@ export const ClashModeCard = () => {
|
||||
const { clashConfig, refreshClashConfig } = useAppData();
|
||||
|
||||
// 支持的模式列表
|
||||
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
||||
const modeList = CLASH_MODES;
|
||||
|
||||
// 直接使用API返回的模式,不维护本地状态
|
||||
const currentMode = clashConfig?.mode?.toLowerCase();
|
||||
const currentModeKey =
|
||||
typeof currentMode === "string" && isClashMode(currentMode)
|
||||
? currentMode
|
||||
: undefined;
|
||||
|
||||
const modeDescription = useMemo(() => {
|
||||
if (typeof currentMode === "string" && currentMode.length > 0) {
|
||||
return t(
|
||||
`${currentMode[0].toLocaleUpperCase()}${currentMode.slice(1)} Mode Description`,
|
||||
);
|
||||
if (currentModeKey) {
|
||||
return t(MODE_META[currentModeKey].description);
|
||||
}
|
||||
return t("Core communication error");
|
||||
}, [currentMode, t]);
|
||||
return t("home.components.clashMode.errors.communication");
|
||||
}, [currentModeKey, t]);
|
||||
|
||||
// 模式图标映射
|
||||
const modeIcons = useMemo(
|
||||
@@ -44,8 +71,8 @@ export const ClashModeCard = () => {
|
||||
);
|
||||
|
||||
// 切换模式的处理函数
|
||||
const onChangeMode = useLockFn(async (mode: string) => {
|
||||
if (mode === currentMode) return;
|
||||
const onChangeMode = useLockFn(async (mode: ClashMode) => {
|
||||
if (mode === currentModeKey) return;
|
||||
if (verge?.auto_close_connection) {
|
||||
closeAllConnections();
|
||||
}
|
||||
@@ -60,7 +87,7 @@ export const ClashModeCard = () => {
|
||||
});
|
||||
|
||||
// 按钮样式
|
||||
const buttonStyles = (mode: string) => ({
|
||||
const buttonStyles = (mode: ClashMode) => ({
|
||||
cursor: "pointer",
|
||||
px: 2,
|
||||
py: 1.2,
|
||||
@@ -68,8 +95,8 @@ export const ClashModeCard = () => {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 1,
|
||||
bgcolor: mode === currentMode ? "primary.main" : "background.paper",
|
||||
color: mode === currentMode ? "primary.contrastText" : "text.primary",
|
||||
bgcolor: mode === currentModeKey ? "primary.main" : "background.paper",
|
||||
color: mode === currentModeKey ? "primary.contrastText" : "text.primary",
|
||||
borderRadius: 1.5,
|
||||
transition: "all 0.2s ease-in-out",
|
||||
position: "relative",
|
||||
@@ -82,7 +109,7 @@ export const ClashModeCard = () => {
|
||||
transform: "translateY(1px)",
|
||||
},
|
||||
"&::after":
|
||||
mode === currentMode
|
||||
mode === currentModeKey
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
@@ -128,7 +155,7 @@ export const ClashModeCard = () => {
|
||||
{modeList.map((mode) => (
|
||||
<Paper
|
||||
key={mode}
|
||||
elevation={mode === currentMode ? 2 : 0}
|
||||
elevation={mode === currentModeKey ? 2 : 0}
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={buttonStyles(mode)}
|
||||
>
|
||||
@@ -137,10 +164,10 @@ export const ClashModeCard = () => {
|
||||
variant="body2"
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
fontWeight: mode === currentMode ? 600 : 400,
|
||||
fontWeight: mode === currentModeKey ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{t(mode)}
|
||||
{t(MODE_META[mode].label)}
|
||||
</Typography>
|
||||
</Paper>
|
||||
))}
|
||||
|
||||
@@ -811,11 +811,11 @@ export const CurrentProxyCard = () => {
|
||||
const getSortTooltip = (): string => {
|
||||
switch (sortType) {
|
||||
case 0:
|
||||
return t("Sort by default");
|
||||
return t("proxies.page.tooltips.sortDefault");
|
||||
case 1:
|
||||
return t("Sort by delay");
|
||||
return t("proxies.page.tooltips.sortDelay");
|
||||
case 2:
|
||||
return t("Sort by name");
|
||||
return t("proxies.page.tooltips.sortName");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -823,7 +823,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("Current Node")}
|
||||
title={t("home.components.currentProxy.title")}
|
||||
icon={
|
||||
<Tooltip
|
||||
title={
|
||||
@@ -840,7 +840,9 @@ export const CurrentProxyCard = () => {
|
||||
iconColor={currentProxy ? "primary" : undefined}
|
||||
action={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Tooltip title={t("Delay check")}>
|
||||
<Tooltip
|
||||
title={t("home.components.currentProxy.actions.refreshDelay")}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -868,7 +870,7 @@ export const CurrentProxyCard = () => {
|
||||
sx={{ borderRadius: 1.5 }}
|
||||
endIcon={<ChevronRight fontSize="small" />}
|
||||
>
|
||||
{t("Label-Proxies")}
|
||||
{t("layout.components.navigation.tabs.proxies")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
@@ -906,7 +908,7 @@ export const CurrentProxyCard = () => {
|
||||
{isGlobalMode && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={t("Global Mode")}
|
||||
label={t("home.components.currentProxy.labels.globalMode")}
|
||||
color="primary"
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
@@ -914,7 +916,7 @@ export const CurrentProxyCard = () => {
|
||||
{isDirectMode && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={t("Direct Mode")}
|
||||
label={t("home.components.currentProxy.labels.directMode")}
|
||||
color="success"
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
@@ -954,12 +956,14 @@ export const CurrentProxyCard = () => {
|
||||
size="small"
|
||||
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
|
||||
labelId="proxy-group-select-label"
|
||||
value={state.selection.group}
|
||||
onChange={handleGroupChange}
|
||||
label={t("Group")}
|
||||
label={t("home.components.currentProxy.labels.group")}
|
||||
disabled={isGlobalMode || isDirectMode}
|
||||
>
|
||||
{state.proxyData.groups.map((group) => (
|
||||
@@ -972,12 +976,14 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
{/* 代理节点选择器 */}
|
||||
<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
|
||||
labelId="proxy-select-label"
|
||||
value={state.selection.proxy}
|
||||
onChange={handleProxyChange}
|
||||
label={t("Proxy")}
|
||||
label={t("home.components.currentProxy.labels.proxy")}
|
||||
disabled={isDirectMode}
|
||||
renderValue={renderProxyValue}
|
||||
MenuProps={{
|
||||
@@ -1033,7 +1039,7 @@ export const CurrentProxyCard = () => {
|
||||
) : (
|
||||
<Box sx={{ textAlign: "center", py: 4 }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{t("No active proxy node")}
|
||||
{t("home.components.currentProxy.labels.noActiveNode")}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -851,7 +851,9 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
|
||||
// 获取时间范围文本
|
||||
const getTimeRangeText = useCallback(() => {
|
||||
return t("{{time}} Minutes", { time: timeRange });
|
||||
return t("home.components.traffic.patterns.minutes", {
|
||||
time: timeRange,
|
||||
});
|
||||
}, [timeRange, t]);
|
||||
|
||||
return (
|
||||
@@ -934,7 +936,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{t("Upload")}
|
||||
{t("home.components.traffic.legends.upload")}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -944,7 +946,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{t("Download")}
|
||||
{t("home.components.traffic.legends.download")}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -219,42 +219,42 @@ export const EnhancedTrafficStats = () => {
|
||||
() => [
|
||||
{
|
||||
icon: <ArrowUpwardRounded fontSize="small" />,
|
||||
title: t("Upload Speed"),
|
||||
title: t("home.components.traffic.metrics.uploadSpeed"),
|
||||
value: parsedData.up,
|
||||
unit: `${parsedData.upUnit}/s`,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <ArrowDownwardRounded fontSize="small" />,
|
||||
title: t("Download Speed"),
|
||||
title: t("home.components.traffic.metrics.downloadSpeed"),
|
||||
value: parsedData.down,
|
||||
unit: `${parsedData.downUnit}/s`,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <LinkRounded fontSize="small" />,
|
||||
title: t("Active Connections"),
|
||||
title: t("home.components.traffic.metrics.activeConnections"),
|
||||
value: parsedData.connectionsCount,
|
||||
unit: "",
|
||||
color: "success" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudUploadRounded fontSize="small" />,
|
||||
title: t("Uploaded"),
|
||||
title: t("shared.labels.uploaded"),
|
||||
value: parsedData.uploadTotal,
|
||||
unit: parsedData.uploadTotalUnit,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudDownloadRounded fontSize="small" />,
|
||||
title: t("Downloaded"),
|
||||
title: t("shared.labels.downloaded"),
|
||||
value: parsedData.downloadTotal,
|
||||
unit: parsedData.downloadTotalUnit,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <MemoryRounded fontSize="small" />,
|
||||
title: t("Memory Usage"),
|
||||
title: t("home.components.traffic.metrics.memoryUsage"),
|
||||
value: parsedData.inuse,
|
||||
unit: parsedData.inuseUnit,
|
||||
color: "error" as const,
|
||||
|
||||
@@ -111,7 +111,7 @@ const ProfileDetails = ({
|
||||
noWrap
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span style={{ flexShrink: 0 }}>{t("From")}: </span>
|
||||
<span style={{ flexShrink: 0 }}>{t("shared.labels.from")}: </span>
|
||||
{current.home ? (
|
||||
<Link
|
||||
component="button"
|
||||
@@ -186,7 +186,7 @@ const ProfileDetails = ({
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={onUpdateProfile}
|
||||
>
|
||||
{t("Update Time")}:{" "}
|
||||
{t("shared.labels.updateTime")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")}
|
||||
</Box>
|
||||
@@ -199,7 +199,7 @@ const ProfileDetails = ({
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<SpeedOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Used / Total")}:{" "}
|
||||
{t("shared.labels.usedTotal")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{parseTraffic(usedTraffic)} /{" "}
|
||||
{parseTraffic(current.extra.total)}
|
||||
@@ -211,7 +211,7 @@ const ProfileDetails = ({
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<EventOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Expire Time")}:{" "}
|
||||
{t("shared.labels.expireTime")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{parseExpire(current.extra.expire)}
|
||||
</Box>
|
||||
@@ -266,10 +266,10 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
||||
sx={{ fontSize: 60, color: "primary.main", mb: 2 }}
|
||||
/>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("Import")} {t("Profiles")}
|
||||
{t("profiles.page.actions.import")} {t("profiles.page.title")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Click to import subscription")}
|
||||
{t("profiles.components.card.labels.clickToImport")}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
@@ -296,8 +296,8 @@ export const HomeProfileCard = ({
|
||||
|
||||
// 刷新首页数据
|
||||
refreshAll();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString(), 3000);
|
||||
} catch (err) {
|
||||
showNotice.error(err, 3000);
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
@@ -310,7 +310,7 @@ export const HomeProfileCard = ({
|
||||
|
||||
// 卡片标题
|
||||
const cardTitle = useMemo(() => {
|
||||
if (!current) return t("Profiles");
|
||||
if (!current) return t("profiles.page.title");
|
||||
|
||||
if (!current.home) return current.name;
|
||||
|
||||
@@ -363,7 +363,7 @@ export const HomeProfileCard = ({
|
||||
endIcon={<StorageOutlined fontSize="small" />}
|
||||
sx={{ borderRadius: 1.5 }}
|
||||
>
|
||||
{t("Label-Profiles")}
|
||||
{t("layout.components.navigation.tabs.proxies")}
|
||||
</Button>
|
||||
);
|
||||
}, [current, goToProfiles, t]);
|
||||
|
||||
@@ -68,8 +68,12 @@ export const IpInfoCard = () => {
|
||||
const data = await getIpInfo();
|
||||
setIpInfo(data);
|
||||
setCountdown(IP_REFRESH_SECONDS);
|
||||
} catch (err: any) {
|
||||
setError(err.message || t("Failed to get IP info"));
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t("home.components.ipInfo.errors.load"),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -114,7 +118,7 @@ export const IpInfoCard = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("IP Information")}
|
||||
title={t("home.components.ipInfo.title")}
|
||||
icon={<LocationOnOutlined />}
|
||||
iconColor="info"
|
||||
action={
|
||||
@@ -137,7 +141,7 @@ export const IpInfoCard = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("IP Information")}
|
||||
title={t("home.components.ipInfo.title")}
|
||||
icon={<LocationOnOutlined />}
|
||||
iconColor="info"
|
||||
action={
|
||||
@@ -160,7 +164,7 @@ export const IpInfoCard = () => {
|
||||
{error}
|
||||
</Typography>
|
||||
<Button onClick={fetchIpInfo} sx={{ mt: 2 }}>
|
||||
{t("Retry")}
|
||||
{t("shared.actions.retry")}
|
||||
</Button>
|
||||
</Box>
|
||||
</EnhancedCard>
|
||||
@@ -170,7 +174,7 @@ export const IpInfoCard = () => {
|
||||
// 渲染正常数据
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("IP Information")}
|
||||
title={t("home.components.ipInfo.title")}
|
||||
icon={<LocationOnOutlined />}
|
||||
iconColor="info"
|
||||
action={
|
||||
@@ -222,7 +226,7 @@ export const IpInfoCard = () => {
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{ipInfo?.country || t("Unknown")}
|
||||
{ipInfo?.country || t("home.components.ipInfo.labels.unknown")}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -232,7 +236,7 @@ export const IpInfoCard = () => {
|
||||
color="text.secondary"
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
{t("IP")}:
|
||||
{t("home.components.ipInfo.labels.ip")}:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -266,20 +270,29 @@ export const IpInfoCard = () => {
|
||||
</Box>
|
||||
|
||||
<InfoItem
|
||||
label={t("ASN")}
|
||||
label={t("home.components.ipInfo.labels.asn")}
|
||||
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 右侧:组织、ISP和位置信息 */}
|
||||
<Box sx={{ width: "60%", overflow: "auto" }}>
|
||||
<InfoItem label={t("ISP")} value={ipInfo?.isp} />
|
||||
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
|
||||
<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(", ")}
|
||||
/>
|
||||
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
|
||||
<InfoItem
|
||||
label={t("home.components.ipInfo.labels.timezone")}
|
||||
value={ipInfo?.timezone}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -297,7 +310,7 @@ export const IpInfoCard = () => {
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">
|
||||
{t("Auto refresh")}: {countdown}s
|
||||
{t("home.components.ipInfo.labels.autoRefresh")}: {countdown}s
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
|
||||
@@ -147,8 +147,8 @@ export const ProxyTunCard: FC = () => {
|
||||
|
||||
const { enable_tun_mode } = verge ?? {};
|
||||
|
||||
const handleError = (err: Error) => {
|
||||
showNotice("error", err.message || err.toString());
|
||||
const handleError = (err: unknown) => {
|
||||
showNotice.error(err);
|
||||
};
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
@@ -160,18 +160,18 @@ export const ProxyTunCard: FC = () => {
|
||||
if (activeTab === "system") {
|
||||
return {
|
||||
text: systemProxyActualState
|
||||
? t("System Proxy Enabled")
|
||||
: t("System Proxy Disabled"),
|
||||
tooltip: t("System Proxy Info"),
|
||||
? t("home.components.proxyTun.status.systemProxyEnabled")
|
||||
: t("home.components.proxyTun.status.systemProxyDisabled"),
|
||||
tooltip: t("home.components.proxyTun.tooltips.systemProxy"),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: !isTunModeAvailable
|
||||
? t("TUN Mode Service Required")
|
||||
? t("home.components.proxyTun.status.tunModeServiceRequired")
|
||||
: enable_tun_mode
|
||||
? t("TUN Mode Enabled")
|
||||
: t("TUN Mode Disabled"),
|
||||
tooltip: t("TUN Mode Intercept Info"),
|
||||
? t("home.components.proxyTun.status.tunModeEnabled")
|
||||
: t("home.components.proxyTun.status.tunModeDisabled"),
|
||||
tooltip: t("home.components.proxyTun.tooltips.tunMode"),
|
||||
};
|
||||
}
|
||||
}, [
|
||||
@@ -198,14 +198,14 @@ export const ProxyTunCard: FC = () => {
|
||||
isActive={activeTab === "system"}
|
||||
onClick={() => handleTabChange("system")}
|
||||
icon={ComputerRounded}
|
||||
label={t("System Proxy")}
|
||||
label={t("settings.sections.system.toggles.systemProxy")}
|
||||
hasIndicator={systemProxyActualState}
|
||||
/>
|
||||
<TabButton
|
||||
isActive={activeTab === "tun"}
|
||||
onClick={() => handleTabChange("tun")}
|
||||
icon={TroubleshootRounded}
|
||||
label={t("Tun Mode")}
|
||||
label={t("settings.sections.system.toggles.tunMode")}
|
||||
hasIndicator={enable_tun_mode && isTunModeAvailable}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -236,7 +236,11 @@ export const ProxyTunCard: FC = () => {
|
||||
>
|
||||
<ProxyControlSwitches
|
||||
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}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -174,13 +174,15 @@ export const SystemInfoCard = () => {
|
||||
try {
|
||||
const info = await checkUpdate();
|
||||
if (!info?.available) {
|
||||
showNotice("success", t("Currently on the Latest Version"));
|
||||
showNotice.success(
|
||||
"settings.components.verge.advanced.notifications.latestVersion",
|
||||
);
|
||||
} else {
|
||||
showNotice("info", t("Update Available"), 2000);
|
||||
showNotice.info("shared.feedback.notifications.updateAvailable", 2000);
|
||||
goToSettings();
|
||||
}
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -217,11 +219,11 @@ export const SystemInfoCard = () => {
|
||||
<>
|
||||
<AdminPanelSettingsOutlined
|
||||
sx={{ color: "primary.main", fontSize: 16 }}
|
||||
titleAccess={t("Administrator Mode")}
|
||||
titleAccess={t("home.components.systemInfo.badges.adminMode")}
|
||||
/>
|
||||
<DnsOutlined
|
||||
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 (
|
||||
<AdminPanelSettingsOutlined
|
||||
sx={{ color: "primary.main", fontSize: 16 }}
|
||||
titleAccess={t("Administrator Mode")}
|
||||
titleAccess={t("home.components.systemInfo.badges.adminMode")}
|
||||
/>
|
||||
);
|
||||
} else if (isSidecarMode) {
|
||||
return (
|
||||
<ExtensionOutlined
|
||||
sx={{ color: "info.main", fontSize: 16 }}
|
||||
titleAccess={t("Sidecar Mode")}
|
||||
titleAccess={t("home.components.systemInfo.badges.sidecarMode")}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DnsOutlined
|
||||
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 (!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) {
|
||||
return t("Sidecar Mode");
|
||||
return t("home.components.systemInfo.badges.sidecarMode");
|
||||
} else {
|
||||
return t("Service Mode");
|
||||
return t("home.components.systemInfo.badges.serviceMode");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -269,11 +271,15 @@ export const SystemInfoCard = () => {
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("System Info")}
|
||||
title={t("home.components.systemInfo.title")}
|
||||
icon={<InfoOutlined />}
|
||||
iconColor="error"
|
||||
action={
|
||||
<IconButton size="small" onClick={goToSettings} title={t("Settings")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={goToSettings}
|
||||
title={t("home.components.systemInfo.actions.settings")}
|
||||
>
|
||||
<SettingsOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
}
|
||||
@@ -281,7 +287,7 @@ export const SystemInfoCard = () => {
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("OS Info")}
|
||||
{t("home.components.systemInfo.fields.osInfo")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{systemState.osInfo}
|
||||
@@ -294,19 +300,23 @@ export const SystemInfoCard = () => {
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Auto Launch")}
|
||||
{t("home.components.systemInfo.fields.autoLaunch")}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
{isAdminMode && (
|
||||
<Tooltip
|
||||
title={t("Administrator mode may not support auto launch")}
|
||||
title={t("home.components.systemInfo.tooltips.autoLaunchAdmin")}
|
||||
>
|
||||
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Chip
|
||||
size="small"
|
||||
label={autoLaunchEnabled ? t("Enabled") : t("Disabled")}
|
||||
label={
|
||||
autoLaunchEnabled
|
||||
? t("shared.statuses.enabled")
|
||||
: t("shared.statuses.disabled")
|
||||
}
|
||||
color={autoLaunchEnabled ? "success" : "default"}
|
||||
variant={autoLaunchEnabled ? "filled" : "outlined"}
|
||||
onClick={toggleAutoLaunch}
|
||||
@@ -321,7 +331,7 @@ export const SystemInfoCard = () => {
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Running Mode")}
|
||||
{t("home.components.systemInfo.fields.runningMode")}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
@@ -336,7 +346,7 @@ export const SystemInfoCard = () => {
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Last Check Update")}
|
||||
{t("home.components.systemInfo.fields.lastCheckUpdate")}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
@@ -354,7 +364,7 @@ export const SystemInfoCard = () => {
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Verge Version")}
|
||||
{t("home.components.systemInfo.fields.vergeVersion")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
v{appVersion}
|
||||
|
||||
@@ -173,16 +173,16 @@ export const TestCard = () => {
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("Website Tests")}
|
||||
title={t("home.components.tests.title")}
|
||||
icon={<NetworkCheck />}
|
||||
action={
|
||||
<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}>
|
||||
<NetworkCheck fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("Create Test")} arrow>
|
||||
<Tooltip title={t("tests.modals.test.title.create")} arrow>
|
||||
<IconButton size="small" onClick={handleCreateTest}>
|
||||
<Add fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
@@ -87,7 +87,7 @@ export const LayoutTraffic = () => {
|
||||
|
||||
<Box display="flex" flexDirection="column" gap={0.75}>
|
||||
<Box
|
||||
title={`${t("Upload Speed")}`}
|
||||
title={`${t("home.components.traffic.metrics.uploadSpeed")}`}
|
||||
{...boxStyle}
|
||||
sx={{
|
||||
...boxStyle.sx,
|
||||
@@ -105,7 +105,7 @@ export const LayoutTraffic = () => {
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
title={`${t("Download Speed")}`}
|
||||
title={`${t("home.components.traffic.metrics.downloadSpeed")}`}
|
||||
{...boxStyle}
|
||||
sx={{
|
||||
...boxStyle.sx,
|
||||
@@ -124,7 +124,7 @@ export const LayoutTraffic = () => {
|
||||
|
||||
{displayMemory && (
|
||||
<Box
|
||||
title={`${t("Memory Usage")} `}
|
||||
title={`${t("home.components.traffic.metrics.memoryUsage")} `}
|
||||
{...boxStyle}
|
||||
sx={{
|
||||
cursor: "auto",
|
||||
|
||||
@@ -35,10 +35,10 @@ export const ConfirmViewer = (props: Props) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Cancel")}
|
||||
{t("shared.actions.cancel")}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} variant="contained">
|
||||
{t("Confirm")}
|
||||
{t("shared.actions.confirm")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -97,7 +97,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
onClose,
|
||||
} = props;
|
||||
|
||||
const resolvedTitle = title ?? t("Edit File");
|
||||
const resolvedTitle = title ?? t("profiles.components.menu.editFile");
|
||||
const resolvedInitialData = useMemo(
|
||||
() => initialData ?? Promise.resolve(""),
|
||||
[initialData],
|
||||
@@ -132,8 +132,8 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
try {
|
||||
currData.current = value;
|
||||
onChange?.(prevData.current, currData.current);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -143,16 +143,16 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
onSave?.(prevData.current, currData.current);
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
const handleClose = useLockFn(async () => {
|
||||
try {
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -202,7 +202,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
},
|
||||
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
|
||||
readOnly: readOnly, // 只读模式
|
||||
readOnlyMessage: { value: t("ReadOnlyMessage") }, // 只读模式尝试编辑时的提示信息
|
||||
readOnlyMessage: {
|
||||
value: t("profiles.modals.editor.messages.readOnly"),
|
||||
}, // 只读模式尝试编辑时的提示信息
|
||||
renderValidationDecorations: "on", // 只读模式下显示校验信息
|
||||
quickSuggestions: {
|
||||
strings: true, // 字符串类型的建议
|
||||
@@ -231,7 +233,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
size="medium"
|
||||
color="inherit"
|
||||
sx={{ display: readOnly ? "none" : "" }}
|
||||
title={t("Format document")}
|
||||
title={t("profiles.modals.editor.actions.format")}
|
||||
onClick={() =>
|
||||
editorRef.current
|
||||
?.getAction("editor.action.formatDocument")
|
||||
@@ -243,7 +245,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
<IconButton
|
||||
size="medium"
|
||||
color="inherit"
|
||||
title={t(isMaximized ? "Minimize" : "Maximize")}
|
||||
title={t(
|
||||
isMaximized ? "shared.window.minimize" : "shared.window.maximize",
|
||||
)}
|
||||
onClick={() => appWindow.toggleMaximize().then(editorResize)}
|
||||
>
|
||||
{isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
|
||||
@@ -253,11 +257,11 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="outlined">
|
||||
{t(readOnly ? "Close" : "Cancel")}
|
||||
{t(readOnly ? "shared.actions.close" : "shared.actions.cancel")}
|
||||
</Button>
|
||||
{!readOnly && (
|
||||
<Button onClick={handleSave} variant="contained">
|
||||
{t("Save")}
|
||||
{t("shared.actions.save")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const FileInput = (props: Props) => {
|
||||
sx={{ flex: "none" }}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
{t("Choose File")}
|
||||
{t("profiles.components.fileInput.chooseFile")}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { useThemeMode } from "@/services/states";
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
import { BaseSearchBox } from "../base/base-search-box";
|
||||
@@ -73,6 +74,24 @@ interface Props {
|
||||
|
||||
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[] => {
|
||||
if (!Array.isArray(input)) {
|
||||
return [];
|
||||
@@ -121,6 +140,20 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
|
||||
props;
|
||||
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 [prevData, setPrevData] = useState("");
|
||||
const [currData, setCurrData] = useState("");
|
||||
@@ -369,7 +402,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const validateGroup = () => {
|
||||
const group = formIns.getValues();
|
||||
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);
|
||||
showNotice("success", t("Saved Successfully"));
|
||||
showNotice.success("shared.feedback.notifications.saved");
|
||||
setPrevData(nextData);
|
||||
onSave?.(prevData, nextData);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -398,7 +431,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
<DialogTitle>
|
||||
{
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Edit Groups")}
|
||||
{t("profiles.modals.groupsEditor.title")}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -407,7 +440,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
setVisualization((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{visualization ? t("Advanced") : t("Visualization")}
|
||||
{visualization
|
||||
? t("shared.editorModes.advanced")
|
||||
: t("shared.editorModes.visualization")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -436,7 +471,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Group Type")} />
|
||||
<ListItemText
|
||||
primary={t("profiles.modals.groupsEditor.fields.type")}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ width: "calc(100% - 150px)" }}
|
||||
@@ -448,9 +485,10 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
"relay",
|
||||
]}
|
||||
value={field.value}
|
||||
getOptionLabel={translateStrategy}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option)}>
|
||||
{option}
|
||||
<li {...props} title={translateStrategy(option)}>
|
||||
{translateStrategy(option)}
|
||||
</li>
|
||||
)}
|
||||
onChange={(_, value) => value && field.onChange(value)}
|
||||
@@ -464,7 +502,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Group Name")} />
|
||||
<ListItemText
|
||||
primary={t("profiles.modals.groupsEditor.fields.name")}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -481,7 +521,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Proxy Group Icon")} />
|
||||
<ListItemText
|
||||
primary={t("profiles.modals.groupsEditor.fields.icon")}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -496,7 +538,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Use Proxies")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.proxies",
|
||||
)}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{
|
||||
@@ -508,10 +554,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
onChange={(_, value) => value && field.onChange(value)}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option)}>
|
||||
{option}
|
||||
<li {...props} title={translatePolicy(option)}>
|
||||
{translatePolicy(option)}
|
||||
</li>
|
||||
)}
|
||||
getOptionLabel={translatePolicy}
|
||||
/>
|
||||
</Item>
|
||||
)}
|
||||
@@ -521,7 +568,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Use Provider")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.provider",
|
||||
)}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ width: "calc(100% - 150px)" }}
|
||||
@@ -539,7 +590,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Health Check Url")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.healthCheckUrl",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder="https://cp.cloudflare.com/generate_204"
|
||||
@@ -555,7 +610,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Expected Status")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.expectedStatus",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder="*"
|
||||
@@ -573,7 +632,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Interval")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.interval",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder="300"
|
||||
@@ -587,7 +650,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("seconds")}
|
||||
{t("shared.units.seconds")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
@@ -601,7 +664,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Timeout")} />
|
||||
<ListItemText primary={t("shared.labels.timeout")} />
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder="5000"
|
||||
@@ -615,7 +678,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("millis")}
|
||||
{t("shared.units.milliseconds")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
@@ -629,7 +692,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Max Failed Times")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.maxFailedTimes",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder="5"
|
||||
@@ -648,7 +715,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Interface Name")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.interfaceName",
|
||||
)}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ width: "calc(100% - 150px)" }}
|
||||
@@ -665,7 +736,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Routing Mark")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.routingMark",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
type="number"
|
||||
@@ -683,7 +758,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Filter")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.filter",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -698,7 +777,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Exclude Filter")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.excludeFilter",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -713,7 +796,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Exclude Type")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.excludeType",
|
||||
)}
|
||||
/>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={[
|
||||
@@ -759,7 +846,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Include All")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.includeAll",
|
||||
)}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -769,7 +860,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Include All Proxies")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.includeAllProxies",
|
||||
)}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -779,7 +874,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Include All Providers")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.includeAllProviders",
|
||||
)}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -789,7 +888,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Lazy")} />
|
||||
<ListItemText
|
||||
primary={t("profiles.modals.groupsEditor.toggles.lazy")}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -799,7 +900,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Disable UDP")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.toggles.disableUdp",
|
||||
)}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -809,7 +914,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Hidden")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.toggles.hidden",
|
||||
)}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -825,16 +934,18 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
validateGroup();
|
||||
for (const item of [...prependSeq, ...groupList]) {
|
||||
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]);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Prepend Group")}
|
||||
{t("profiles.modals.groupsEditor.actions.prepend")}
|
||||
</Button>
|
||||
</Item>
|
||||
<Item>
|
||||
@@ -847,16 +958,18 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
validateGroup();
|
||||
for (const item of [...appendSeq, ...groupList]) {
|
||||
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()]);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Append Group")}
|
||||
{t("profiles.modals.groupsEditor.actions.append")}
|
||||
</Button>
|
||||
</Item>
|
||||
</List>
|
||||
@@ -1007,11 +1120,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Cancel")}
|
||||
{t("shared.actions.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleSave} variant="contained">
|
||||
{t("Save")}
|
||||
{t("shared.actions.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const LogViewer = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>{t("Script Console")}</DialogTitle>
|
||||
<DialogTitle>{t("profiles.modals.logViewer.title")}</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
sx={{
|
||||
@@ -62,7 +62,7 @@ export const LogViewer = (props: Props) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Close")}
|
||||
{t("shared.actions.close")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { useLoadingCache, useSetLoadingCache } from "@/services/states";
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
import { ProfileBox } from "./profile-box";
|
||||
@@ -120,34 +121,46 @@ export const ProfileItem = (props: Props) => {
|
||||
|
||||
// 如果已经过期,显示"更新失败"
|
||||
if (nextUpdateDate.isBefore(now)) {
|
||||
setNextUpdateTime(t("Last Update failed"));
|
||||
setNextUpdateTime(
|
||||
t("profiles.components.profileItem.status.lastUpdateFailed"),
|
||||
);
|
||||
} else {
|
||||
// 否则显示剩余时间
|
||||
const diffMinutes = nextUpdateDate.diff(now, "minute");
|
||||
|
||||
if (diffMinutes < 60) {
|
||||
if (diffMinutes <= 0) {
|
||||
setNextUpdateTime(`${t("Next Up")} <1m`);
|
||||
setNextUpdateTime(
|
||||
`${t("profiles.components.profileItem.status.nextUp")} <1m`,
|
||||
);
|
||||
} else {
|
||||
setNextUpdateTime(`${t("Next Up")} ${diffMinutes}m`);
|
||||
setNextUpdateTime(
|
||||
`${t("profiles.components.profileItem.status.nextUp")} ${diffMinutes}m`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const hours = Math.floor(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 {
|
||||
console.log(`返回的下次更新时间为空`);
|
||||
setNextUpdateTime(t("No schedule"));
|
||||
setNextUpdateTime(
|
||||
t("profiles.components.profileItem.status.noSchedule"),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`获取下次更新时间出错:`, err);
|
||||
setNextUpdateTime(t("Unknown"));
|
||||
setNextUpdateTime(t("profiles.components.profileItem.status.unknown"));
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
try {
|
||||
await viewProfile(itemData.uid);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err?.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -354,42 +367,95 @@ export const ProfileItem = (props: Props) => {
|
||||
}
|
||||
});
|
||||
|
||||
const urlModeMenu = (
|
||||
hasHome ? [{ label: "Home", handler: onOpenHome, disabled: false }] : []
|
||||
).concat([
|
||||
{ label: "Select", handler: onForceSelect, disabled: false },
|
||||
{ label: "Edit Info", handler: onEditInfo, disabled: false },
|
||||
{ label: "Edit File", handler: onEditFile, disabled: false },
|
||||
type ContextMenuItem = {
|
||||
label: string;
|
||||
handler: () => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
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,
|
||||
disabled: !option?.rules,
|
||||
},
|
||||
{
|
||||
label: "Edit Proxies",
|
||||
label: menuLabels.editProxies,
|
||||
handler: onEditProxies,
|
||||
disabled: !option?.proxies,
|
||||
},
|
||||
{
|
||||
label: "Edit Groups",
|
||||
label: menuLabels.editGroups,
|
||||
handler: onEditGroups,
|
||||
disabled: !option?.groups,
|
||||
},
|
||||
{
|
||||
label: "Extend Config",
|
||||
label: menuLabels.extendConfig,
|
||||
handler: onEditMerge,
|
||||
disabled: !option?.merge,
|
||||
},
|
||||
{
|
||||
label: "Extend Script",
|
||||
label: menuLabels.extendScript,
|
||||
handler: onEditScript,
|
||||
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: () => {
|
||||
setAnchorEl(null);
|
||||
if (batchMode) {
|
||||
@@ -403,39 +469,55 @@ export const ProfileItem = (props: Props) => {
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
const fileModeMenu = [
|
||||
{ label: "Select", handler: onForceSelect, disabled: false },
|
||||
{ label: "Edit Info", handler: onEditInfo, disabled: false },
|
||||
{ label: "Edit File", handler: onEditFile, disabled: false },
|
||||
];
|
||||
const fileModeMenu: 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,
|
||||
disabled: !option?.rules,
|
||||
},
|
||||
{
|
||||
label: "Edit Proxies",
|
||||
label: menuLabels.editProxies,
|
||||
handler: onEditProxies,
|
||||
disabled: !option?.proxies,
|
||||
},
|
||||
{
|
||||
label: "Edit Groups",
|
||||
label: menuLabels.editGroups,
|
||||
handler: onEditGroups,
|
||||
disabled: !option?.groups,
|
||||
},
|
||||
{
|
||||
label: "Extend Config",
|
||||
label: menuLabels.extendConfig,
|
||||
handler: onEditMerge,
|
||||
disabled: !option?.merge,
|
||||
},
|
||||
{
|
||||
label: "Extend Script",
|
||||
label: menuLabels.extendScript,
|
||||
handler: onEditScript,
|
||||
disabled: !option?.script,
|
||||
},
|
||||
{ label: "Open File", handler: onOpenFile, disabled: false },
|
||||
{
|
||||
label: "Delete",
|
||||
label: menuLabels.openFile,
|
||||
handler: onOpenFile,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.delete,
|
||||
handler: () => {
|
||||
setAnchorEl(null);
|
||||
if (batchMode) {
|
||||
@@ -599,7 +681,7 @@ export const ProfileItem = (props: Props) => {
|
||||
{/* only if has url can it be updated */}
|
||||
{hasUrl && (
|
||||
<IconButton
|
||||
title={t("Refresh")}
|
||||
title={t("shared.actions.refresh")}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
p: "3px",
|
||||
@@ -637,7 +719,10 @@ export const ProfileItem = (props: Props) => {
|
||||
</Typography>
|
||||
) : (
|
||||
hasUrl && (
|
||||
<Typography noWrap title={`${t("From")} ${from}`}>
|
||||
<Typography
|
||||
noWrap
|
||||
title={`${t("shared.labels.from")} ${from}`}
|
||||
>
|
||||
{from}
|
||||
</Typography>
|
||||
)
|
||||
@@ -657,8 +742,8 @@ export const ProfileItem = (props: Props) => {
|
||||
textAlign="right"
|
||||
title={
|
||||
showNextUpdate
|
||||
? t("Click to show last update time")
|
||||
: `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`
|
||||
? t("profiles.components.profileItem.tooltips.showLast")
|
||||
: `${t("shared.labels.updateTime")}: ${parseExpire(updated)}\n${t("profiles.components.profileItem.tooltips.showNext")}`
|
||||
}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
@@ -686,14 +771,16 @@ export const ProfileItem = (props: Props) => {
|
||||
{/* the third line show extra info or last updated time */}
|
||||
{hasExtra ? (
|
||||
<Box sx={{ ...boxStyle, fontSize: 14 }}>
|
||||
<span title={t("Used / Total")}>
|
||||
<span title={t("shared.labels.usedTotal")}>
|
||||
{parseTraffic(upload + download)} / {parseTraffic(total)}
|
||||
</span>
|
||||
<span title={t("Expire Time")}>{expire}</span>
|
||||
<span title={t("shared.labels.expireTime")}>{expire}</span>
|
||||
</Box>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<LinearProgress
|
||||
@@ -728,7 +815,7 @@ export const ProfileItem = (props: Props) => {
|
||||
(theme) => {
|
||||
return {
|
||||
color:
|
||||
item.label === "Delete"
|
||||
item.label === menuLabels.delete
|
||||
? theme.palette.error.main
|
||||
: undefined,
|
||||
};
|
||||
@@ -813,8 +900,8 @@ export const ProfileItem = (props: Props) => {
|
||||
)}
|
||||
|
||||
<ConfirmViewer
|
||||
title={t("Confirm deletion")}
|
||||
message={t("This operation is not reversible")}
|
||||
title={t("profiles.modals.confirmDelete.title")}
|
||||
message={t("profiles.modals.confirmDelete.message")}
|
||||
open={confirmOpen}
|
||||
onClose={() => setConfirmOpen(false)}
|
||||
onConfirm={() => {
|
||||
|
||||
@@ -47,16 +47,26 @@ export const ProfileMore = (props: Props) => {
|
||||
setAnchorEl(null);
|
||||
try {
|
||||
await viewProfile(id);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err?.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
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 = [
|
||||
{ label: "Edit File", handler: onEditFile },
|
||||
{ label: "Open File", handler: onOpenFile },
|
||||
{ label: "profiles.components.menu.editFile", handler: onEditFile },
|
||||
{ label: "profiles.components.menu.openFile", handler: onOpenFile },
|
||||
];
|
||||
|
||||
const boxStyle = {
|
||||
@@ -89,13 +99,13 @@ export const ProfileMore = (props: Props) => {
|
||||
variant="h6"
|
||||
component="h2"
|
||||
noWrap
|
||||
title={t(`Global ${id}`)}
|
||||
title={t(globalTitles[id])}
|
||||
>
|
||||
{t(`Global ${id}`)}
|
||||
{t(globalTitles[id])}
|
||||
</Typography>
|
||||
|
||||
<Chip
|
||||
label={id}
|
||||
label={t(chipLabels[id])}
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@@ -111,7 +121,7 @@ export const ProfileMore = (props: Props) => {
|
||||
size="small"
|
||||
edge="start"
|
||||
color="error"
|
||||
title={t("Script Console")}
|
||||
title={t("profiles.modals.logViewer.title")}
|
||||
onClick={() => setLogOpen(true)}
|
||||
>
|
||||
<FeaturedPlayListRounded fontSize="inherit" />
|
||||
@@ -122,7 +132,7 @@ export const ProfileMore = (props: Props) => {
|
||||
size="small"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
title={t("Script Console")}
|
||||
title={t("profiles.modals.logViewer.title")}
|
||||
onClick={() => setLogOpen(true)}
|
||||
>
|
||||
<FeaturedPlayListRounded fontSize="inherit" />
|
||||
@@ -170,7 +180,7 @@ export const ProfileMore = (props: Props) => {
|
||||
{fileOpen && (
|
||||
<EditorViewer
|
||||
open={true}
|
||||
title={`${t("Global " + id)}`}
|
||||
title={t(globalTitles[id])}
|
||||
initialData={readProfileFile(id)}
|
||||
language={id === "Merge" ? "yaml" : "javascript"}
|
||||
schema={id === "Merge" ? "clash" : undefined}
|
||||
|
||||
@@ -144,9 +144,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
}
|
||||
} catch {
|
||||
// 首次创建/更新失败,尝试使用自身代理
|
||||
showNotice(
|
||||
"info",
|
||||
t("Profile creation failed, retrying with Clash proxy..."),
|
||||
showNotice.info(
|
||||
"profiles.modals.profileForm.feedback.notifications.creationRetry",
|
||||
);
|
||||
|
||||
// 使用自身代理的配置
|
||||
@@ -170,9 +169,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
await patchProfile(form.uid, { option: originalOptions });
|
||||
}
|
||||
|
||||
showNotice(
|
||||
"success",
|
||||
t("Profile creation succeeded with Clash proxy"),
|
||||
showNotice.success(
|
||||
"profiles.modals.profileForm.feedback.notifications.creationSuccess",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -186,8 +184,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
setTimeout(() => {
|
||||
onChange(isActivating);
|
||||
}, 0);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -220,10 +218,14 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
return (
|
||||
<BaseDialog
|
||||
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%" }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
onOk={handleOk}
|
||||
@@ -234,8 +236,14 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
|
||||
<InputLabel>{t("Type")}</InputLabel>
|
||||
<Select {...field} autoFocus label={t("Type")}>
|
||||
<InputLabel>
|
||||
{t("profiles.modals.profileForm.fields.type")}
|
||||
</InputLabel>
|
||||
<Select
|
||||
{...field}
|
||||
autoFocus
|
||||
label={t("profiles.modals.profileForm.fields.type")}
|
||||
>
|
||||
<MenuItem value="remote">Remote</MenuItem>
|
||||
<MenuItem value="local">Local</MenuItem>
|
||||
</Select>
|
||||
@@ -247,7 +255,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
name="name"
|
||||
control={control}
|
||||
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"
|
||||
control={control}
|
||||
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}
|
||||
{...field}
|
||||
multiline
|
||||
label={t("Subscription URL")}
|
||||
label={t("profiles.modals.profileForm.fields.subscriptionUrl")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -296,12 +308,12 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder="60"
|
||||
label={t("HTTP Request Timeout")}
|
||||
label={t("profiles.modals.profileForm.fields.httpTimeout")}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("seconds")}
|
||||
{t("shared.units.seconds")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
@@ -321,11 +333,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
{...text}
|
||||
{...field}
|
||||
type="number"
|
||||
label={t("Update Interval")}
|
||||
label={t("profiles.modals.profileForm.fields.updateInterval")}
|
||||
slotProps={{
|
||||
input: {
|
||||
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}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Use System Proxy")}</InputLabel>
|
||||
<InputLabel>
|
||||
{t("profiles.modals.profileForm.fields.useSystemProxy")}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
@@ -361,7 +377,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Use Clash Proxy")}</InputLabel>
|
||||
<InputLabel>
|
||||
{t("profiles.modals.profileForm.fields.useClashProxy")}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
@@ -372,7 +390,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Accept Invalid Certs (Danger)")}</InputLabel>
|
||||
<InputLabel>
|
||||
{t("profiles.modals.profileForm.fields.acceptInvalidCerts")}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
@@ -383,7 +403,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Allow Auto Update")}</InputLabel>
|
||||
<InputLabel>
|
||||
{t("profiles.modals.profileForm.fields.allowAutoUpdate")}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
|
||||
@@ -163,11 +163,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
proxies.push(proxy);
|
||||
names.push(proxy.name);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[ProxiesEditorViewer] parseUri failed for line:",
|
||||
uri,
|
||||
err?.message || err,
|
||||
err,
|
||||
);
|
||||
// 不阻塞主流程
|
||||
}
|
||||
@@ -263,11 +263,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
await saveProfileFile(property, currData);
|
||||
showNotice("success", t("Saved Successfully"));
|
||||
showNotice.success("shared.feedback.notifications.saved");
|
||||
onSave?.(prevData, currData);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -276,7 +276,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
<DialogTitle>
|
||||
{
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Edit Proxies")}
|
||||
{t("profiles.modals.proxiesEditor.title")}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -285,7 +285,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
setVisualization((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{visualization ? t("Advanced") : t("Visualization")}
|
||||
{visualization
|
||||
? t("shared.editorModes.advanced")
|
||||
: t("shared.editorModes.visualization")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -312,7 +314,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
<Item>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder={t("Use newlines for multiple uri")}
|
||||
placeholder={t(
|
||||
"profiles.modals.proxiesEditor.placeholders.multiUri",
|
||||
)}
|
||||
fullWidth
|
||||
rows={9}
|
||||
multiline
|
||||
@@ -332,7 +336,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Prepend Proxy")}
|
||||
{t("profiles.modals.proxiesEditor.actions.prepend")}
|
||||
</Button>
|
||||
</Item>
|
||||
<Item>
|
||||
@@ -346,7 +350,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Append Proxy")}
|
||||
{t("profiles.modals.proxiesEditor.actions.append")}
|
||||
</Button>
|
||||
</Item>
|
||||
</List>
|
||||
@@ -497,11 +501,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Cancel")}
|
||||
{t("shared.actions.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleSave} variant="contained">
|
||||
{t("Save")}
|
||||
{t("shared.actions.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</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 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) => {
|
||||
const { groupsUid, mergeUid, profileUid, property, open, onClose, onSave } =
|
||||
props;
|
||||
@@ -350,8 +366,8 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
{ forceQuotes: true },
|
||||
),
|
||||
);
|
||||
} catch (e: any) {
|
||||
showNotice("error", e?.message || e?.toString() || "YAML dump error");
|
||||
} catch (error) {
|
||||
showNotice.error(error ?? "YAML dump error");
|
||||
}
|
||||
};
|
||||
let idleId: number | undefined;
|
||||
@@ -462,10 +478,12 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
const validateRule = () => {
|
||||
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)) {
|
||||
throw new Error(t("Invalid Rule"));
|
||||
throw new Error(t("rules.modals.editor.form.validation.invalidRule"));
|
||||
}
|
||||
|
||||
const condition = (ruleType.required ?? true) ? ruleContent : "";
|
||||
@@ -477,11 +495,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
await saveProfileFile(property, currData);
|
||||
showNotice("success", t("Saved Successfully"));
|
||||
showNotice.success("shared.feedback.notifications.saved");
|
||||
onSave?.(prevData, currData);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.toString());
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -490,7 +508,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
<DialogTitle>
|
||||
{
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Edit Rules")}
|
||||
{t("rules.modals.editor.title")}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -499,7 +517,9 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
setVisualization((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{visualization ? t("Advanced") : t("Visualization")}
|
||||
{visualization
|
||||
? t("shared.editorModes.advanced")
|
||||
: t("shared.editorModes.visualization")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -518,26 +538,37 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<Item>
|
||||
<ListItemText primary={t("Rule Type")} />
|
||||
<ListItemText
|
||||
primary={t("rules.modals.editor.form.labels.type")}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={rules}
|
||||
value={ruleType}
|
||||
getOptionLabel={(option) => option.name}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option.name)}>
|
||||
{option.name}
|
||||
</li>
|
||||
)}
|
||||
getOptionLabel={(option) =>
|
||||
t(RULE_TYPE_LABEL_KEYS[option.name] ?? option.name)
|
||||
}
|
||||
renderOption={(props, option) => {
|
||||
const label = t(
|
||||
RULE_TYPE_LABEL_KEYS[option.name] ?? option.name,
|
||||
);
|
||||
return (
|
||||
<li {...props} title={label}>
|
||||
{label}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
onChange={(_, value) => value && setRuleType(value)}
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}
|
||||
>
|
||||
<ListItemText primary={t("Rule Content")} />
|
||||
<ListItemText
|
||||
primary={t("rules.modals.editor.form.labels.content")}
|
||||
/>
|
||||
|
||||
{ruleType.name === "RULE-SET" && (
|
||||
<Autocomplete
|
||||
@@ -574,24 +605,34 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
)}
|
||||
</Item>
|
||||
<Item>
|
||||
<ListItemText primary={t("Proxy Policy")} />
|
||||
<ListItemText
|
||||
primary={t("rules.modals.editor.form.labels.proxyPolicy")}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={proxyPolicyList}
|
||||
value={proxyPolicy}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option)}>
|
||||
{option}
|
||||
</li>
|
||||
)}
|
||||
getOptionLabel={(option) =>
|
||||
t(PROXY_POLICY_LABEL_KEYS[option] ?? option)
|
||||
}
|
||||
renderOption={(props, option) => {
|
||||
const label = t(PROXY_POLICY_LABEL_KEYS[option] ?? option);
|
||||
return (
|
||||
<li {...props} title={label}>
|
||||
{label}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
onChange={(_, value) => value && setProxyPolicy(value)}
|
||||
/>
|
||||
</Item>
|
||||
{ruleType.noResolve && (
|
||||
<Item>
|
||||
<ListItemText primary={t("No Resolve")} />
|
||||
<ListItemText
|
||||
primary={t("rules.modals.editor.form.toggles.noResolve")}
|
||||
/>
|
||||
<Switch
|
||||
checked={noResolve}
|
||||
onChange={() => setNoResolve(!noResolve)}
|
||||
@@ -609,11 +650,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
if (prependSeq.includes(raw)) return;
|
||||
setPrependSeq([raw, ...prependSeq]);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
showNotice.error(err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Prepend Rule")}
|
||||
{t("rules.modals.editor.form.actions.prependRule")}
|
||||
</Button>
|
||||
</Item>
|
||||
<Item>
|
||||
@@ -627,11 +668,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
if (appendSeq.includes(raw)) return;
|
||||
setAppendSeq([...appendSeq, raw]);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
showNotice.error(err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Append Rule")}
|
||||
{t("rules.modals.editor.form.actions.appendRule")}
|
||||
</Button>
|
||||
</Item>
|
||||
</List>
|
||||
@@ -776,11 +817,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Cancel")}
|
||||
{t("shared.actions.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleSave} variant="contained">
|
||||
{t("Save")}
|
||||
{t("shared.actions.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -66,12 +66,17 @@ export const ProviderButton = () => {
|
||||
await refreshProxy();
|
||||
await refreshProxyProviders();
|
||||
|
||||
showNotice("success", `${name} 更新成功`);
|
||||
} catch (err: any) {
|
||||
showNotice(
|
||||
"error",
|
||||
`${name} 更新失败: ${err?.message || err.toString()}`,
|
||||
showNotice.success(
|
||||
"proxies.feedback.notifications.provider.updateSuccess",
|
||||
{
|
||||
name,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
showNotice.error("proxies.feedback.notifications.provider.updateFailed", {
|
||||
name,
|
||||
message: String(err),
|
||||
});
|
||||
} finally {
|
||||
// 清除更新状态
|
||||
setUpdating((prev) => ({ ...prev, [name]: false }));
|
||||
@@ -84,7 +89,7 @@ export const ProviderButton = () => {
|
||||
// 获取所有provider的名称
|
||||
const allProviders = Object.keys(proxyProviders || {});
|
||||
if (allProviders.length === 0) {
|
||||
showNotice("info", "没有可更新的代理提供者");
|
||||
showNotice.info("proxies.feedback.notifications.provider.none");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,9 +119,11 @@ export const ProviderButton = () => {
|
||||
await refreshProxy();
|
||||
await refreshProxyProviders();
|
||||
|
||||
showNotice("success", "全部代理提供者更新成功");
|
||||
} catch (err: any) {
|
||||
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
|
||||
showNotice.success("proxies.feedback.notifications.provider.allUpdated");
|
||||
} catch (err) {
|
||||
showNotice.error("proxies.feedback.notifications.provider.genericError", {
|
||||
message: String(err),
|
||||
});
|
||||
} finally {
|
||||
// 清除所有更新状态
|
||||
setUpdating({});
|
||||
@@ -138,7 +145,7 @@ export const ProviderButton = () => {
|
||||
onClick={() => setOpen(true)}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
{t("Proxy Provider")}
|
||||
{t("proxies.page.provider.title")}
|
||||
</Button>
|
||||
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
@@ -148,14 +155,17 @@ export const ProviderButton = () => {
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="h6">{t("Proxy Provider")}</Typography>
|
||||
<Typography variant="h6">
|
||||
{t("proxies.page.provider.title")}
|
||||
</Typography>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={updateAllProviders}
|
||||
aria-label={t("proxies.page.provider.actions.updateAll")}
|
||||
>
|
||||
{t("Update All")}
|
||||
{t("proxies.page.provider.actions.updateAll")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -246,7 +256,7 @@ export const ProviderButton = () => {
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
>
|
||||
<small>{t("Update At")}: </small>
|
||||
<small>{t("shared.labels.updateAt")}: </small>
|
||||
{time.fromNow()}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -264,11 +274,17 @@ export const ProviderButton = () => {
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span title={t("Used / Total") as string}>
|
||||
<span
|
||||
title={t("shared.labels.usedTotal") as string}
|
||||
>
|
||||
{parseTraffic(upload + download)} /{" "}
|
||||
{parseTraffic(total)}
|
||||
</span>
|
||||
<span title={t("Expire Time") as string}>
|
||||
<span
|
||||
title={
|
||||
t("shared.labels.expireTime") as string
|
||||
}
|
||||
>
|
||||
{parseExpire(expire)}
|
||||
</span>
|
||||
</Box>
|
||||
@@ -313,7 +329,8 @@ export const ProviderButton = () => {
|
||||
"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 />
|
||||
</IconButton>
|
||||
@@ -326,7 +343,7 @@ export const ProviderButton = () => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="outlined">
|
||||
{t("Close")}
|
||||
{t("shared.actions.close")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -156,7 +156,11 @@ const SortableItem = ({ proxy, index, onRemove }: SortableItemProps) => {
|
||||
|
||||
{proxy.delay !== undefined && (
|
||||
<Chip
|
||||
label={proxy.delay > 0 ? `${proxy.delay}ms` : t("timeout") || "超时"}
|
||||
label={
|
||||
proxy.delay > 0
|
||||
? `${proxy.delay}ms`
|
||||
: t("shared.labels.timeout") || "超时"
|
||||
}
|
||||
size="small"
|
||||
color={
|
||||
proxy.delay > 0 && proxy.delay < 200
|
||||
@@ -299,7 +303,7 @@ export const ProxyChain = ({
|
||||
// onUpdateChain([]);
|
||||
} catch (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 {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
@@ -307,9 +311,7 @@ export const ProxyChain = ({
|
||||
}
|
||||
|
||||
if (proxyChain.length < 2) {
|
||||
alert(
|
||||
t("Chain proxy requires at least 2 nodes") || "链式代理至少需要2个节点",
|
||||
);
|
||||
alert(t("proxies.page.chain.minimumNodes") || "链式代理至少需要2个节点");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -341,7 +343,7 @@ export const ProxyChain = ({
|
||||
console.log("Successfully connected to proxy chain");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to proxy chain:", error);
|
||||
alert(t("Failed to connect to proxy chain") || "连接链式代理失败");
|
||||
alert(t("proxies.page.chain.connectFailed") || "连接链式代理失败");
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
@@ -471,7 +473,7 @@ export const ProxyChain = ({
|
||||
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 }}>
|
||||
{proxyChain.length > 0 && (
|
||||
<IconButton
|
||||
@@ -486,7 +488,9 @@ export const ProxyChain = ({
|
||||
backgroundColor: theme.palette.error.light + "20",
|
||||
},
|
||||
}}
|
||||
title={t("Delete Chain Config") || "删除链式配置"}
|
||||
title={
|
||||
t("proxies.page.actions.clearChainConfig") || "删除链式配置"
|
||||
}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
@@ -507,16 +511,16 @@ export const ProxyChain = ({
|
||||
}}
|
||||
title={
|
||||
proxyChain.length < 2
|
||||
? t("Chain proxy requires at least 2 nodes") ||
|
||||
? t("proxies.page.chain.minimumNodes") ||
|
||||
"链式代理至少需要2个节点"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{isConnecting
|
||||
? t("Connecting...") || "连接中..."
|
||||
? t("proxies.page.actions.connecting") || "连接中..."
|
||||
: isConnected
|
||||
? t("Disconnect") || "断开"
|
||||
: t("Connect") || "连接"}
|
||||
? t("proxies.page.actions.disconnect") || "断开"
|
||||
: t("proxies.page.actions.connect") || "连接"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -526,10 +530,9 @@ export const ProxyChain = ({
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{proxyChain.length === 1
|
||||
? t(
|
||||
"Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
) || "链式代理至少需要2个节点,请再添加一个节点。"
|
||||
: t("Click nodes in order to add to proxy chain") ||
|
||||
? t("proxies.page.chain.minimumNodesHint") ||
|
||||
"链式代理至少需要2个节点,请再添加一个节点。"
|
||||
: t("proxies.page.chain.instruction") ||
|
||||
"按顺序点击节点添加到代理链中"}
|
||||
</Alert>
|
||||
|
||||
@@ -544,7 +547,7 @@ export const ProxyChain = ({
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<Typography>{t("No proxy chain configured")}</Typography>
|
||||
<Typography>{t("proxies.page.chain.empty")}</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<DndContext
|
||||
|
||||
@@ -239,7 +239,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
setProxyChain((prev) => {
|
||||
// 检查是否已经存在相同名称的代理,防止重复添加
|
||||
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({
|
||||
open: true,
|
||||
message: warningMessage,
|
||||
@@ -372,7 +372,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
}, [renderList]);
|
||||
|
||||
if (mode === "direct") {
|
||||
return <BaseEmpty text={t("clash_mode_direct")} />;
|
||||
return <BaseEmpty textKey="proxies.page.messages.directMode" />;
|
||||
}
|
||||
|
||||
if (isChainMode) {
|
||||
@@ -403,7 +403,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
variant="h6"
|
||||
sx={{ fontWeight: 600, fontSize: "16px" }}
|
||||
>
|
||||
{t("Proxy Rules")}
|
||||
{t("proxies.page.rules.title")}
|
||||
</Typography>
|
||||
{currentGroup && (
|
||||
<Box
|
||||
@@ -442,7 +442,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
variant="body2"
|
||||
sx={{ mr: 0.5, fontSize: "12px" }}
|
||||
>
|
||||
{t("Select Rules")}
|
||||
{t("proxies.page.rules.select")}
|
||||
</Typography>
|
||||
<ExpandMoreRounded fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
@@ -67,7 +67,7 @@ export const ProxyHead = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("locate")}
|
||||
title={t("proxies.page.tooltips.locate")}
|
||||
onClick={onLocation}
|
||||
>
|
||||
<MyLocationRounded />
|
||||
@@ -76,7 +76,7 @@ export const ProxyHead = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("Delay check")}
|
||||
title={t("proxies.page.tooltips.delayCheck")}
|
||||
onClick={() => {
|
||||
console.log(`[ProxyHead] 点击延迟测试按钮,组: ${groupName}`);
|
||||
// Remind the user that it is custom test url
|
||||
@@ -94,9 +94,11 @@ export const ProxyHead = ({
|
||||
size="small"
|
||||
color="inherit"
|
||||
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={() =>
|
||||
onHeadState({ sortType: ((sortType + 1) % 3) as ProxySortType })
|
||||
@@ -110,7 +112,7 @@ export const ProxyHead = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("Delay check URL")}
|
||||
title={t("proxies.page.tooltips.delayCheckUrl")}
|
||||
onClick={() =>
|
||||
onHeadState({ textState: textState === "url" ? null : "url" })
|
||||
}
|
||||
@@ -125,7 +127,11 @@ export const ProxyHead = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
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 })}
|
||||
>
|
||||
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
|
||||
@@ -134,7 +140,7 @@ export const ProxyHead = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("Filter")}
|
||||
title={t("proxies.page.tooltips.filter")}
|
||||
onClick={() =>
|
||||
onHeadState({ textState: textState === "filter" ? null : "filter" })
|
||||
}
|
||||
@@ -154,7 +160,7 @@ export const ProxyHead = ({
|
||||
value={filterText}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder={t("Filter conditions")}
|
||||
placeholder={t("shared.placeholders.filter")}
|
||||
onChange={(e) => onHeadState({ filterText: e.target.value })}
|
||||
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
|
||||
/>
|
||||
@@ -169,7 +175,7 @@ export const ProxyHead = ({
|
||||
value={testUrl}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder={t("Delay check URL")}
|
||||
placeholder={t("proxies.page.placeholders.delayCheckUrl")}
|
||||
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
||||
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
|
||||
/>
|
||||
|
||||
@@ -271,7 +271,9 @@ export const ProxyItemMini = (props: Props) => {
|
||||
<span
|
||||
className={proxy.name === group.now ? "the-pin" : "the-unpin"}
|
||||
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" }}>
|
||||
<Tooltip title={t("Proxy Count")} arrow>
|
||||
<Tooltip title={t("proxies.page.labels.proxyCount")} arrow>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`${group.all.length}`}
|
||||
|
||||
@@ -58,12 +58,17 @@ export const ProviderButton = () => {
|
||||
await refreshRules();
|
||||
await refreshRuleProviders();
|
||||
|
||||
showNotice("success", `${name} 更新成功`);
|
||||
} catch (err: any) {
|
||||
showNotice(
|
||||
"error",
|
||||
`${name} 更新失败: ${err?.message || err.toString()}`,
|
||||
showNotice.success(
|
||||
"rules.feedback.notifications.provider.updateSuccess",
|
||||
{
|
||||
name,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
showNotice.error("rules.feedback.notifications.provider.updateFailed", {
|
||||
name,
|
||||
message: String(err),
|
||||
});
|
||||
} finally {
|
||||
// 清除更新状态
|
||||
setUpdating((prev) => ({ ...prev, [name]: false }));
|
||||
@@ -76,7 +81,7 @@ export const ProviderButton = () => {
|
||||
// 获取所有provider的名称
|
||||
const allProviders = Object.keys(ruleProviders || {});
|
||||
if (allProviders.length === 0) {
|
||||
showNotice("info", "没有可更新的规则提供者");
|
||||
showNotice.info("rules.feedback.notifications.provider.none");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,9 +111,11 @@ export const ProviderButton = () => {
|
||||
await refreshRules();
|
||||
await refreshRuleProviders();
|
||||
|
||||
showNotice("success", "全部规则提供者更新成功");
|
||||
} catch (err: any) {
|
||||
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
|
||||
showNotice.success("rules.feedback.notifications.provider.allUpdated");
|
||||
} catch (err) {
|
||||
showNotice.error("rules.feedback.notifications.provider.genericError", {
|
||||
message: String(err),
|
||||
});
|
||||
} finally {
|
||||
// 清除所有更新状态
|
||||
setUpdating({});
|
||||
@@ -129,7 +136,7 @@ export const ProviderButton = () => {
|
||||
startIcon={<StorageOutlined />}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t("Rule Provider")}
|
||||
{t("rules.page.provider.trigger")}
|
||||
</Button>
|
||||
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
@@ -139,13 +146,15 @@ export const ProviderButton = () => {
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="h6">{t("Rule Providers")}</Typography>
|
||||
<Typography variant="h6">
|
||||
{t("rules.page.provider.dialogTitle")}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={updateAllProviders}
|
||||
>
|
||||
{t("Update All")}
|
||||
{t("rules.page.provider.actions.updateAll")}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
@@ -216,7 +225,7 @@ export const ProviderButton = () => {
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
>
|
||||
<small>{t("Update At")}: </small>
|
||||
<small>{t("shared.labels.updateAt")}: </small>
|
||||
{time.fromNow()}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -246,6 +255,7 @@ export const ProviderButton = () => {
|
||||
color="primary"
|
||||
onClick={() => updateProvider(key)}
|
||||
disabled={isUpdating}
|
||||
aria-label={t("rules.page.provider.actions.update")}
|
||||
sx={{
|
||||
animation: isUpdating
|
||||
? "spin 1s linear infinite"
|
||||
@@ -255,7 +265,7 @@ export const ProviderButton = () => {
|
||||
"100%": { transform: "rotate(360deg)" },
|
||||
},
|
||||
}}
|
||||
title={t("Update Provider") as string}
|
||||
title={t("rules.page.provider.actions.update")}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
@@ -268,7 +278,7 @@ export const ProviderButton = () => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="outlined">
|
||||
{t("Close")}
|
||||
{t("shared.actions.close")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -84,22 +84,22 @@ export const BackupConfigViewer = memo(
|
||||
|
||||
if (!url) {
|
||||
urlRef.current?.focus();
|
||||
showNotice("error", t("WebDAV URL Required"));
|
||||
throw new Error(t("WebDAV URL Required"));
|
||||
showNotice.error("settings.modals.backup.messages.webdavUrlRequired");
|
||||
throw new Error(t("settings.modals.backup.messages.webdavUrlRequired"));
|
||||
} else if (!isValidUrl(url)) {
|
||||
urlRef.current?.focus();
|
||||
showNotice("error", t("Invalid WebDAV URL"));
|
||||
throw new Error(t("Invalid WebDAV URL"));
|
||||
showNotice.error("settings.modals.backup.messages.invalidWebdavUrl");
|
||||
throw new Error(t("settings.modals.backup.messages.invalidWebdavUrl"));
|
||||
}
|
||||
if (!username) {
|
||||
usernameRef.current?.focus();
|
||||
showNotice("error", t("WebDAV URL Required"));
|
||||
throw new Error(t("Username Required"));
|
||||
showNotice.error("settings.modals.backup.messages.usernameRequired");
|
||||
throw new Error(t("settings.modals.backup.messages.usernameRequired"));
|
||||
}
|
||||
if (!password) {
|
||||
passwordRef.current?.focus();
|
||||
showNotice("error", t("WebDAV URL Required"));
|
||||
throw new Error(t("Password Required"));
|
||||
showNotice.error("settings.modals.backup.messages.passwordRequired");
|
||||
throw new Error(t("settings.modals.backup.messages.passwordRequired"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,11 +112,17 @@ export const BackupConfigViewer = memo(
|
||||
data.username.trim(),
|
||||
data.password,
|
||||
).then(() => {
|
||||
showNotice("success", t("WebDAV Config Saved"));
|
||||
showNotice.success(
|
||||
"settings.modals.backup.messages.webdavConfigSaved",
|
||||
);
|
||||
onSaveSuccess();
|
||||
});
|
||||
} catch (error) {
|
||||
showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
|
||||
showNotice.error(
|
||||
"settings.modals.backup.messages.webdavConfigSaveFailed",
|
||||
{ error },
|
||||
3000,
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -127,11 +133,13 @@ export const BackupConfigViewer = memo(
|
||||
try {
|
||||
setLoading(true);
|
||||
await createWebdavBackup().then(async () => {
|
||||
showNotice("success", t("Backup Created"));
|
||||
showNotice.success("settings.modals.backup.messages.backupCreated");
|
||||
await onBackupSuccess();
|
||||
});
|
||||
} catch (error) {
|
||||
showNotice("error", t("Backup Failed", { error }));
|
||||
showNotice.error("settings.modals.backup.messages.backupFailed", {
|
||||
error,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -145,7 +153,7 @@ export const BackupConfigViewer = memo(
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t("WebDAV Server URL")}
|
||||
label={t("settings.modals.backup.fields.webdavUrl")}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
{...register("url")}
|
||||
@@ -157,7 +165,7 @@ export const BackupConfigViewer = memo(
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TextField
|
||||
label={t("Username")}
|
||||
label={t("settings.modals.backup.fields.username")}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
{...register("username")}
|
||||
@@ -169,7 +177,7 @@ export const BackupConfigViewer = memo(
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TextField
|
||||
label={t("Password")}
|
||||
label={t("shared.labels.password")}
|
||||
type={showPassword ? "text" : "password"}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@@ -214,7 +222,7 @@ export const BackupConfigViewer = memo(
|
||||
type="button"
|
||||
onClick={handleSubmit(save)}
|
||||
>
|
||||
{t("Save")}
|
||||
{t("shared.actions.save")}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
@@ -225,7 +233,7 @@ export const BackupConfigViewer = memo(
|
||||
type="button"
|
||||
size="large"
|
||||
>
|
||||
{t("Backup")}
|
||||
{t("settings.modals.backup.actions.backup")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -233,7 +241,7 @@ export const BackupConfigViewer = memo(
|
||||
type="button"
|
||||
size="large"
|
||||
>
|
||||
{t("Refresh")}
|
||||
{t("shared.actions.refresh")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -75,7 +75,7 @@ export const BackupTableViewer = memo(
|
||||
|
||||
const handleRestore = useLockFn(async (filename: string) => {
|
||||
await onRestore(filename).then(() => {
|
||||
showNotice("success", t("Restore Success, App will restart in 1s"));
|
||||
showNotice.success("settings.modals.backup.messages.restoreSuccess");
|
||||
});
|
||||
await restartApp();
|
||||
});
|
||||
@@ -92,10 +92,14 @@ export const BackupTableViewer = memo(
|
||||
return;
|
||||
}
|
||||
await onExport(filename, savePath);
|
||||
showNotice("success", t("Local Backup Exported"));
|
||||
showNotice.success(
|
||||
"settings.modals.backup.messages.localBackupExported",
|
||||
);
|
||||
} catch (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>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{t("Filename")}</TableCell>
|
||||
<TableCell>{t("Backup Time")}</TableCell>
|
||||
<TableCell align="right">{t("Actions")}</TableCell>
|
||||
<TableCell>
|
||||
{t("settings.modals.backup.table.filename")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{t("settings.modals.backup.table.backupTime")}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{t("settings.modals.backup.table.actions")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -140,9 +150,13 @@ export const BackupTableViewer = memo(
|
||||
<>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label={t("Export")}
|
||||
aria-label={t(
|
||||
"settings.modals.backup.actions.export",
|
||||
)}
|
||||
size="small"
|
||||
title={t("Export Backup")}
|
||||
title={t(
|
||||
"settings.modals.backup.actions.exportBackup",
|
||||
)}
|
||||
onClick={async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
await handleExport(file.filename);
|
||||
@@ -159,13 +173,17 @@ export const BackupTableViewer = memo(
|
||||
)}
|
||||
<IconButton
|
||||
color="secondary"
|
||||
aria-label={t("Delete")}
|
||||
aria-label={t("shared.actions.delete")}
|
||||
size="small"
|
||||
title={t("Delete Backup")}
|
||||
title={t(
|
||||
"settings.modals.backup.actions.deleteBackup",
|
||||
)}
|
||||
onClick={async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const confirmed = await confirmAsync(
|
||||
t("Confirm to delete this backup file?"),
|
||||
t(
|
||||
"settings.modals.backup.messages.confirmDelete",
|
||||
),
|
||||
);
|
||||
if (confirmed) {
|
||||
await handleDelete(file.filename);
|
||||
@@ -181,14 +199,20 @@ export const BackupTableViewer = memo(
|
||||
/>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label={t("Restore")}
|
||||
aria-label={t(
|
||||
"settings.modals.backup.actions.restore",
|
||||
)}
|
||||
size="small"
|
||||
title={t("Restore Backup")}
|
||||
title={t(
|
||||
"settings.modals.backup.actions.restoreBackup",
|
||||
)}
|
||||
disabled={!file.allow_apply}
|
||||
onClick={async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const confirmed = await confirmAsync(
|
||||
t("Confirm to restore this backup file?"),
|
||||
t(
|
||||
"settings.modals.backup.messages.confirmRestore",
|
||||
),
|
||||
);
|
||||
if (confirmed) {
|
||||
await handleRestore(file.filename);
|
||||
@@ -219,7 +243,7 @@ export const BackupTableViewer = memo(
|
||||
color="textSecondary"
|
||||
align="center"
|
||||
>
|
||||
{t("No Backups")}
|
||||
{t("settings.modals.backup.table.noBackups")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
@@ -234,7 +258,7 @@ export const BackupTableViewer = memo(
|
||||
rowsPerPage={DEFAULT_ROWS_PER_PAGE}
|
||||
page={page}
|
||||
onPageChange={onPageChange}
|
||||
labelRowsPerPage={t("Rows per page")}
|
||||
labelRowsPerPage={t("settings.modals.backup.table.rowsPerPage")}
|
||||
/>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
@@ -127,7 +127,6 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
setBackupFiles([]);
|
||||
setTotal(0);
|
||||
console.error(error);
|
||||
// Notice.error(t("Failed to fetch backup files"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -247,7 +246,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Backup Setting")}
|
||||
title={t("settings.modals.backup.title")}
|
||||
contentSx={{
|
||||
minWidth: { xs: 320, sm: 620 },
|
||||
maxWidth: "unset",
|
||||
@@ -278,11 +277,14 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
<Tabs
|
||||
value={source}
|
||||
onChange={handleChangeSource}
|
||||
aria-label={t("Select Backup Target")}
|
||||
aria-label={t("settings.modals.backup.actions.selectTarget")}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
<Tab value="local" label={t("Local Backup")} />
|
||||
<Tab value="webdav" label={t("WebDAV Backup")} />
|
||||
<Tab value="local" label={t("settings.modals.backup.tabs.local")} />
|
||||
<Tab
|
||||
value="webdav"
|
||||
label={t("settings.modals.backup.tabs.webdav")}
|
||||
/>
|
||||
</Tabs>
|
||||
{source === "local" ? (
|
||||
<LocalBackupActions
|
||||
@@ -342,7 +344,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
backgroundColor: (theme) => theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
{t("Close")}
|
||||
{t("shared.actions.close")}
|
||||
</Button>
|
||||
</Box>,
|
||||
dialogPaper,
|
||||
|
||||
@@ -24,8 +24,16 @@ import { changeClashCore, restartCore } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
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> }) {
|
||||
@@ -54,7 +62,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const errorMsg = await changeClashCore(core);
|
||||
|
||||
if (errorMsg) {
|
||||
showNotice("error", errorMsg);
|
||||
showNotice.error(errorMsg);
|
||||
setChangingCore(null);
|
||||
return;
|
||||
}
|
||||
@@ -65,9 +73,9 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
mutate("getVersion");
|
||||
setChangingCore(null);
|
||||
}, 500);
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
setChangingCore(null);
|
||||
showNotice("error", err.message || err.toString());
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,11 +83,13 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
try {
|
||||
setRestarting(true);
|
||||
await restartCore();
|
||||
showNotice("success", t(`Clash Core Restarted`));
|
||||
showNotice.success(
|
||||
t("settings.feedback.notifications.clash.restartSuccess"),
|
||||
);
|
||||
setRestarting(false);
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
setRestarting(false);
|
||||
showNotice("error", err.message || err.toString());
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,14 +98,16 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
setUpgrading(true);
|
||||
await upgradeCore();
|
||||
setUpgrading(false);
|
||||
showNotice("success", t(`Core Version Updated`));
|
||||
showNotice.success(
|
||||
t("settings.feedback.notifications.clash.versionUpdated"),
|
||||
);
|
||||
} catch (err: any) {
|
||||
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")
|
||||
? "Already Using Latest Core Version"
|
||||
? t("settings.feedback.notifications.clash.alreadyLatestVersion")
|
||||
: errMsg;
|
||||
showNotice("error", t(showMsg));
|
||||
showNotice.info(showMsg);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -104,7 +116,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
open={open}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Clash Core")}
|
||||
{t("settings.sections.clash.form.fields.clashCore")}
|
||||
<Box>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
@@ -116,7 +128,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
sx={{ marginRight: "8px" }}
|
||||
onClick={onUpgrade}
|
||||
>
|
||||
{t("Upgrade")}
|
||||
{t("shared.actions.upgrade")}
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
@@ -127,7 +139,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
disabled={upgrading}
|
||||
onClick={onRestart}
|
||||
>
|
||||
{t("Restart")}
|
||||
{t("shared.actions.restart")}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -141,7 +153,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
marginTop: "-8px",
|
||||
}}
|
||||
disableOk
|
||||
cancelBtn={t("Close")}
|
||||
cancelBtn={t("shared.actions.close")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
@@ -157,7 +169,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
{changingCore === each.core ? (
|
||||
<CircularProgress size={20} sx={{ mr: 1 }} />
|
||||
) : (
|
||||
<Chip label={t(`${each.chip}`)} size="small" />
|
||||
<Chip label={t(each.chipKey)} size="small" />
|
||||
)}
|
||||
</ListItemButton>
|
||||
))}
|
||||
|
||||
@@ -69,11 +69,13 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
showNotice("success", t("Port settings saved"));
|
||||
showNotice.success("settings.modals.clashPort.messages.saved");
|
||||
},
|
||||
onError: (e) => {
|
||||
showNotice("error", e.message || t("Failed to save port settings"));
|
||||
// showNotice("error", t("Failed to save port settings"));
|
||||
onError: (error) => {
|
||||
showNotice.error(
|
||||
"settings.modals.clashPort.messages.saveFailed",
|
||||
error,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -150,7 +152,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Port Config")}
|
||||
title={t("settings.modals.clashPort.title")}
|
||||
contentSx={{
|
||||
width: 400,
|
||||
}}
|
||||
@@ -158,13 +160,13 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
loading ? (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<CircularProgress size={20} />
|
||||
{t("Saving...")}
|
||||
{t("shared.statuses.saving")}
|
||||
</Stack>
|
||||
) : (
|
||||
t("Save")
|
||||
t("shared.actions.save")
|
||||
)
|
||||
}
|
||||
cancelBtn={t("Cancel")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
@@ -172,7 +174,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
<List sx={{ width: "100%" }}>
|
||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||
<ListItemText
|
||||
primary={t("Mixed Port")}
|
||||
primary={t("settings.modals.clashPort.fields.mixed")}
|
||||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
@@ -188,7 +190,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setMixedPort(generateRandomPort())}
|
||||
title={t("Random Port")}
|
||||
title={t("settings.modals.clashPort.actions.random")}
|
||||
sx={{ mr: 0.5 }}
|
||||
>
|
||||
<Shuffle fontSize="small" />
|
||||
@@ -204,7 +206,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
|
||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||
<ListItemText
|
||||
primary={t("Socks Port")}
|
||||
primary={t("settings.modals.clashPort.fields.socks")}
|
||||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
@@ -221,7 +223,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setSocksPort(generateRandomPort())}
|
||||
title={t("Random Port")}
|
||||
title={t("settings.modals.clashPort.actions.random")}
|
||||
disabled={!socksEnabled}
|
||||
sx={{ mr: 0.5 }}
|
||||
>
|
||||
@@ -238,7 +240,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
|
||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||
<ListItemText
|
||||
primary={t("Http Port")}
|
||||
primary={t("settings.modals.clashPort.fields.http")}
|
||||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
@@ -255,7 +257,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setHttpPort(generateRandomPort())}
|
||||
title={t("Random Port")}
|
||||
title={t("settings.modals.clashPort.actions.random")}
|
||||
disabled={!httpEnabled}
|
||||
sx={{ mr: 0.5 }}
|
||||
>
|
||||
@@ -273,7 +275,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
{OS !== "windows" && (
|
||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||
<ListItemText
|
||||
primary={t("Redir Port")}
|
||||
primary={t("settings.modals.clashPort.fields.redir")}
|
||||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
@@ -290,7 +292,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setRedirPort(generateRandomPort())}
|
||||
title={t("Random Port")}
|
||||
title={t("settings.modals.clashPort.actions.random")}
|
||||
disabled={!redirEnabled}
|
||||
sx={{ mr: 0.5 }}
|
||||
>
|
||||
@@ -309,7 +311,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
{OS === "linux" && (
|
||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||
<ListItemText
|
||||
primary={t("Tproxy Port")}
|
||||
primary={t("settings.modals.clashPort.fields.tproxy")}
|
||||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
@@ -326,7 +328,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setTproxyPort(generateRandomPort())}
|
||||
title={t("Random Port")}
|
||||
title={t("settings.modals.clashPort.actions.random")}
|
||||
disabled={!tproxyEnabled}
|
||||
sx={{ mr: 0.5 }}
|
||||
>
|
||||
|
||||
@@ -27,8 +27,8 @@ export const ConfigViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
open={true}
|
||||
title={
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
{t("Runtime Config")}
|
||||
<Chip label={t("ReadOnly")} size="small" />
|
||||
{t("settings.components.verge.advanced.fields.runtimeConfig")}
|
||||
<Chip label={t("shared.labels.readOnly")} size="small" />
|
||||
</Box>
|
||||
}
|
||||
initialData={Promise.resolve(runtimeConfig)}
|
||||
|
||||
@@ -56,12 +56,16 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
// 如果启用了外部控制器,则保存控制器地址和密钥
|
||||
if (enableController) {
|
||||
if (!controller.trim()) {
|
||||
showNotice("error", t("Controller address cannot be empty"));
|
||||
showNotice.error(
|
||||
"settings.sections.externalController.messages.addressRequired",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!secret.trim()) {
|
||||
showNotice("error", t("Secret cannot be empty"));
|
||||
showNotice.error(
|
||||
"settings.sections.externalController.messages.secretRequired",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,12 +75,12 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
await patchInfo({ "external-controller": "" });
|
||||
}
|
||||
|
||||
showNotice("success", t("Configuration saved successfully"));
|
||||
showNotice.success("shared.feedback.notifications.common.saveSuccess");
|
||||
setOpen(false);
|
||||
} catch (err: any) {
|
||||
showNotice(
|
||||
"error",
|
||||
err.message || t("Failed to save configuration"),
|
||||
} catch (err) {
|
||||
showNotice.error(
|
||||
"shared.feedback.notifications.common.saveFailed",
|
||||
err,
|
||||
4000,
|
||||
);
|
||||
} finally {
|
||||
@@ -93,7 +97,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
setTimeout(() => setCopySuccess(null));
|
||||
} catch (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 (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("External Controller")}
|
||||
title={t("settings.sections.externalController.title")}
|
||||
contentSx={{ width: 400 }}
|
||||
okBtn={
|
||||
isSaving ? (
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<CircularProgress size={16} color="inherit" />
|
||||
{t("Saving...")}
|
||||
{t("shared.statuses.saving")}
|
||||
</Box>
|
||||
) : (
|
||||
t("Save")
|
||||
t("shared.actions.save")
|
||||
)
|
||||
}
|
||||
cancelBtn={t("Cancel")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
@@ -126,7 +132,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={t("Enable External Controller")} />
|
||||
<ListItemText
|
||||
primary={t("settings.sections.externalController.fields.enable")}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={enableController}
|
||||
@@ -142,7 +150,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={t("External Controller")} />
|
||||
<ListItemText
|
||||
primary={t("settings.sections.externalController.fields.address")}
|
||||
/>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -152,11 +162,15 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
pointerEvents: enableController ? "auto" : "none",
|
||||
}}
|
||||
value={controller}
|
||||
placeholder="Required"
|
||||
placeholder={t(
|
||||
"settings.sections.externalController.placeholders.address",
|
||||
)}
|
||||
onChange={(e) => setController(e.target.value)}
|
||||
disabled={isSaving || !enableController}
|
||||
/>
|
||||
<Tooltip title={t("Copy to clipboard")}>
|
||||
<Tooltip
|
||||
title={t("settings.sections.externalController.tooltips.copy")}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleCopyToClipboard(controller, "controller")}
|
||||
@@ -176,7 +190,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={t("Core Secret")} />
|
||||
<ListItemText
|
||||
primary={t("settings.sections.externalController.fields.secret")}
|
||||
/>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -186,11 +202,15 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
pointerEvents: enableController ? "auto" : "none",
|
||||
}}
|
||||
value={secret}
|
||||
placeholder={t("Recommended")}
|
||||
placeholder={t(
|
||||
"settings.sections.externalController.placeholders.secret",
|
||||
)}
|
||||
onChange={(e) => setSecret(e.target.value)}
|
||||
disabled={isSaving || !enableController}
|
||||
/>
|
||||
<Tooltip title={t("Copy to clipboard")}>
|
||||
<Tooltip
|
||||
title={t("settings.sections.externalController.tooltips.copy")}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleCopyToClipboard(secret, "secret")}
|
||||
@@ -211,8 +231,10 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
>
|
||||
<Alert severity="success">
|
||||
{copySuccess === "controller"
|
||||
? t("Controller address copied to clipboard")
|
||||
: t("Secret copied to clipboard")}
|
||||
? t(
|
||||
"settings.sections.externalController.messages.controllerCopied",
|
||||
)
|
||||
: t("settings.sections.externalController.messages.secretCopied")}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -424,9 +424,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
skipYamlSyncRef.current = true;
|
||||
updateValuesFromConfig(parsedYaml);
|
||||
} catch {
|
||||
showNotice("error", t("Invalid YAML format"));
|
||||
showNotice.error("settings.modals.dns.errors.invalidYaml");
|
||||
}
|
||||
}, [yamlContent, t, updateValuesFromConfig]);
|
||||
}, [yamlContent, updateValuesFromConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (skipYamlSyncRef.current) {
|
||||
@@ -509,7 +509,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
// 使用YAML编辑器的值
|
||||
const parsedConfig = yaml.load(yamlContent);
|
||||
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>;
|
||||
}
|
||||
@@ -547,9 +547,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
}
|
||||
}
|
||||
|
||||
showNotice(
|
||||
"error",
|
||||
t("DNS configuration error") + ": " + cleanErrorMsg,
|
||||
showNotice.error(
|
||||
"settings.modals.dns.messages.configError",
|
||||
cleanErrorMsg,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -561,9 +561,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
showNotice("success", t("DNS settings saved"));
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
showNotice.success("settings.modals.dns.messages.saved");
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -613,7 +613,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
open={open}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
{t("DNS Overwrite")}
|
||||
{t("settings.modals.dns.dialog.title")}
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -622,7 +622,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
startIcon={<RestartAltRounded />}
|
||||
onClick={resetToDefaults}
|
||||
>
|
||||
{t("Reset to Default")}
|
||||
{t("shared.actions.resetToDefault")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -631,7 +631,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
setVisualization((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{visualization ? t("Advanced") : t("Visualization")}
|
||||
{visualization
|
||||
? t("shared.editorModes.advanced")
|
||||
: t("shared.editorModes.visualization")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -643,8 +645,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
? {}
|
||||
: { padding: "0 24px", display: "flex", flexDirection: "column" }),
|
||||
}}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
@@ -655,7 +657,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
color="warning.main"
|
||||
sx={{ mb: 2, mt: 0, fontStyle: "italic" }}
|
||||
>
|
||||
{t("DNS Settings Warning")}
|
||||
{t("settings.modals.dns.dialog.warning")}
|
||||
</Typography>
|
||||
|
||||
{visualization ? (
|
||||
@@ -664,11 +666,11 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
variant="subtitle1"
|
||||
sx={{ mt: 1, mb: 1, fontWeight: "bold" }}
|
||||
>
|
||||
{t("DNS Settings")}
|
||||
{t("settings.modals.dns.sections.general")}
|
||||
</Typography>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Enable DNS")} />
|
||||
<ListItemText primary={t("settings.modals.dns.fields.enable")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.enable}
|
||||
@@ -677,7 +679,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("DNS Listen")} />
|
||||
<ListItemText primary={t("settings.modals.dns.fields.listen")} />
|
||||
<TextField
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
@@ -689,7 +691,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Enhanced Mode")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.dns.fields.enhancedMode")}
|
||||
/>
|
||||
<FormControl size="small" sx={{ width: 150 }}>
|
||||
<Select
|
||||
value={values.enhancedMode}
|
||||
@@ -702,7 +706,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Fake IP Range")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.dns.fields.fakeIpRange")}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
@@ -714,7 +720,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Fake IP Filter Mode")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.dns.fields.fakeIpFilterMode")}
|
||||
/>
|
||||
<FormControl size="small" sx={{ width: 150 }}>
|
||||
<Select
|
||||
value={values.fakeIpFilterMode}
|
||||
@@ -728,8 +736,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item>
|
||||
<ListItemText
|
||||
primary={t("IPv6")}
|
||||
secondary={t("Enable IPv6 DNS resolution")}
|
||||
primary={t("settings.modals.dns.fields.ipv6.label")}
|
||||
secondary={t("settings.modals.dns.fields.ipv6.description")}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -740,8 +748,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item>
|
||||
<ListItemText
|
||||
primary={t("Prefer H3")}
|
||||
secondary={t("DNS DOH使用HTTP/3")}
|
||||
primary={t("settings.modals.dns.fields.preferH3.label")}
|
||||
secondary={t("settings.modals.dns.fields.preferH3.description")}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -752,8 +760,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item>
|
||||
<ListItemText
|
||||
primary={t("Respect Rules")}
|
||||
secondary={t("DNS connections follow routing rules")}
|
||||
primary={t("settings.modals.dns.fields.respectRules.label")}
|
||||
secondary={t(
|
||||
"settings.modals.dns.fields.respectRules.description",
|
||||
)}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -764,8 +774,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item>
|
||||
<ListItemText
|
||||
primary={t("Use Hosts")}
|
||||
secondary={t("Enable to resolve hosts through hosts file")}
|
||||
primary={t("settings.modals.dns.fields.useHosts.label")}
|
||||
secondary={t("settings.modals.dns.fields.useHosts.description")}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -776,8 +786,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item>
|
||||
<ListItemText
|
||||
primary={t("Use System Hosts")}
|
||||
secondary={t("Enable to resolve hosts through system hosts file")}
|
||||
primary={t("settings.modals.dns.fields.useSystemHosts.label")}
|
||||
secondary={t(
|
||||
"settings.modals.dns.fields.useSystemHosts.description",
|
||||
)}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -788,8 +800,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item>
|
||||
<ListItemText
|
||||
primary={t("Direct Nameserver Follow Policy")}
|
||||
secondary={t("Whether to follow nameserver policy")}
|
||||
primary={t("settings.modals.dns.fields.directPolicy.label")}
|
||||
secondary={t(
|
||||
"settings.modals.dns.fields.directPolicy.description",
|
||||
)}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -800,8 +814,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary={t("Default Nameserver")}
|
||||
secondary={t("Default DNS servers used to resolve DNS servers")}
|
||||
primary={t("settings.modals.dns.fields.defaultNameserver.label")}
|
||||
secondary={t(
|
||||
"settings.modals.dns.fields.defaultNameserver.description",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
@@ -817,8 +833,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary={t("Nameserver")}
|
||||
secondary={t("List of DNS servers")}
|
||||
primary={t("settings.modals.dns.fields.nameserver.label")}
|
||||
secondary={t("settings.modals.dns.fields.nameserver.description")}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
@@ -834,8 +850,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary={t("Fallback")}
|
||||
secondary={t("List of fallback DNS servers")}
|
||||
primary={t("settings.modals.dns.fields.fallback.label")}
|
||||
secondary={t("settings.modals.dns.fields.fallback.description")}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
@@ -851,8 +867,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary={t("Proxy Server Nameserver")}
|
||||
secondary={t("Proxy Node Nameserver")}
|
||||
primary={t("settings.modals.dns.fields.proxy.label")}
|
||||
secondary={t("settings.modals.dns.fields.proxy.description")}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
@@ -868,8 +884,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary={t("Direct Nameserver")}
|
||||
secondary={t("Direct outbound Nameserver")}
|
||||
primary={t("settings.modals.dns.fields.directNameserver.label")}
|
||||
secondary={t(
|
||||
"settings.modals.dns.fields.directNameserver.description",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
@@ -885,8 +903,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary={t("Fake IP Filter")}
|
||||
secondary={t("Domains that skip fake IP resolution")}
|
||||
primary={t("settings.modals.dns.fields.fakeIpFilter.label")}
|
||||
secondary={t(
|
||||
"settings.modals.dns.fields.fakeIpFilter.description",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
@@ -902,8 +922,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary={t("Nameserver Policy")}
|
||||
secondary={t("Domain-specific DNS server")}
|
||||
primary={t("settings.modals.dns.fields.nameserverPolicy.label")}
|
||||
secondary={t(
|
||||
"settings.modals.dns.fields.nameserverPolicy.description",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
@@ -921,13 +943,15 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
variant="subtitle2"
|
||||
sx={{ mt: 2, mb: 1, fontWeight: "bold" }}
|
||||
>
|
||||
{t("Fallback Filter Settings")}
|
||||
{t("settings.modals.dns.sections.fallbackFilter")}
|
||||
</Typography>
|
||||
|
||||
<Item>
|
||||
<ListItemText
|
||||
primary={t("GeoIP Filtering")}
|
||||
secondary={t("Enable GeoIP filtering for fallback")}
|
||||
primary={t("settings.modals.dns.fields.geoipFiltering.label")}
|
||||
secondary={t(
|
||||
"settings.modals.dns.fields.geoipFiltering.description",
|
||||
)}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -937,7 +961,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("GeoIP Code")} />
|
||||
<ListItemText primary={t("settings.modals.dns.fields.geoipCode")} />
|
||||
<TextField
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
@@ -950,8 +974,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary={t("Fallback IP CIDR")}
|
||||
secondary={t("IP CIDRs not using fallback servers")}
|
||||
primary={t("settings.modals.dns.fields.fallbackIpCidr.label")}
|
||||
secondary={t(
|
||||
"settings.modals.dns.fields.fallbackIpCidr.description",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
@@ -967,8 +993,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary={t("Fallback Domain")}
|
||||
secondary={t("Domains using fallback servers")}
|
||||
primary={t("settings.modals.dns.fields.fallbackDomain.label")}
|
||||
secondary={t(
|
||||
"settings.modals.dns.fields.fallbackDomain.description",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
@@ -987,13 +1015,13 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
variant="subtitle1"
|
||||
sx={{ mt: 3, mb: 0, fontWeight: "bold" }}
|
||||
>
|
||||
{t("Hosts Settings")}
|
||||
{t("settings.modals.dns.sections.hosts")}
|
||||
</Typography>
|
||||
|
||||
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary={t("Hosts")}
|
||||
secondary={t("Custom domain to IP or domain mapping")}
|
||||
primary={t("settings.modals.dns.fields.hosts.label")}
|
||||
secondary={t("settings.modals.dns.fields.hosts.description")}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
|
||||
@@ -140,10 +140,12 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
showNotice("success", t("Configuration saved successfully"));
|
||||
showNotice.success(
|
||||
"shared.feedback.notifications.common.saveSuccess",
|
||||
);
|
||||
},
|
||||
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 (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("External Cors Configuration")}
|
||||
title={t("settings.sections.externalCors.title")}
|
||||
contentSx={{ width: 500 }}
|
||||
okBtn={loading ? t("Saving...") : t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
okBtn={loading ? t("shared.statuses.saving") : t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={handleSave}
|
||||
@@ -198,7 +200,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
width="100%"
|
||||
>
|
||||
<span style={{ fontWeight: "normal" }}>
|
||||
{t("Allow private network access")}
|
||||
{t("settings.sections.externalCors.fields.allowPrivateNetwork")}
|
||||
</span>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -218,7 +220,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
<ListItem sx={{ padding: "8px 0" }}>
|
||||
<div style={{ width: "100%" }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
|
||||
{t("Allowed Origins")}
|
||||
{t("settings.sections.externalCors.fields.allowedOrigins")}
|
||||
</div>
|
||||
{originEntries.map(({ origin, index, key }) => (
|
||||
<div
|
||||
@@ -235,7 +237,9 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
sx={{ fontSize: 14, marginRight: 2 }}
|
||||
value={origin}
|
||||
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 } }}
|
||||
/>
|
||||
<Button
|
||||
@@ -256,7 +260,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
onClick={handleAddOrigin}
|
||||
sx={addButtonStyle}
|
||||
>
|
||||
{t("Add")}
|
||||
{t("settings.sections.externalCors.actions.add")}
|
||||
</Button>
|
||||
|
||||
<div
|
||||
@@ -270,7 +274,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
<div
|
||||
style={{ color: "#666", fontSize: 12, fontStyle: "italic" }}
|
||||
>
|
||||
{t("Always included origins: {{urls}}", {
|
||||
{t("settings.sections.externalCors.messages.alwaysIncluded", {
|
||||
urls: DEV_URLS.join(", "),
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,7 @@ export const HotkeyInput = (props: Props) => {
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
title={t("Delete")}
|
||||
title={t("shared.actions.delete")}
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
onChange([]);
|
||||
|
||||
@@ -24,7 +24,19 @@ const HOTKEY_FUNC = [
|
||||
"toggle_system_proxy",
|
||||
"toggle_tun_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) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -81,24 +93,26 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
enable_global_hotkey: enableGlobalHotkey,
|
||||
});
|
||||
setOpen(false);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Hotkey Setting")}
|
||||
title={t("settings.modals.hotkey.title")}
|
||||
contentSx={{ width: 450, maxHeight: 380 }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<ItemWrapper style={{ marginBottom: 16 }}>
|
||||
<Typography>{t("Enable Global Hotkey")}</Typography>
|
||||
<Typography>
|
||||
{t("settings.modals.hotkey.toggles.enableGlobal")}
|
||||
</Typography>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={enableGlobalHotkey}
|
||||
@@ -108,7 +122,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
{HOTKEY_FUNC.map((func) => (
|
||||
<ItemWrapper key={func}>
|
||||
<Typography>{t(func)}</Typography>
|
||||
<Typography>{t(HOTKEY_FUNC_LABELS[func])}</Typography>
|
||||
<HotkeyInput
|
||||
value={hotkeyMap[func] ?? []}
|
||||
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 onError = (err: any) => {
|
||||
showNotice("error", err.message || err.toString());
|
||||
showNotice.error(err);
|
||||
};
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
@@ -113,16 +113,20 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Layout Setting")}
|
||||
title={t("settings.components.verge.layout.title")}
|
||||
contentSx={{ width: 450 }}
|
||||
disableOk
|
||||
cancelBtn={t("Close")}
|
||||
cancelBtn={t("shared.actions.close")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List>
|
||||
<Item>
|
||||
<ListItemText primary={t("Prefer System Titlebar")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"settings.components.verge.layout.fields.preferSystemTitlebar",
|
||||
)}
|
||||
/>
|
||||
<GuardState
|
||||
value={decorated}
|
||||
valueProps="checked"
|
||||
@@ -137,7 +141,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Traffic Graph")} />
|
||||
<ListItemText
|
||||
primary={t("settings.components.verge.layout.fields.trafficGraph")}
|
||||
/>
|
||||
<GuardState
|
||||
value={verge?.traffic_graph ?? true}
|
||||
valueProps="checked"
|
||||
@@ -151,7 +157,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Memory Usage")} />
|
||||
<ListItemText
|
||||
primary={t("settings.components.verge.layout.fields.memoryUsage")}
|
||||
/>
|
||||
<GuardState
|
||||
value={verge?.enable_memory_usage ?? true}
|
||||
valueProps="checked"
|
||||
@@ -165,7 +173,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Proxy Group Icon")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"settings.components.verge.layout.fields.proxyGroupIcon",
|
||||
)}
|
||||
/>
|
||||
<GuardState
|
||||
value={verge?.enable_group_icon ?? true}
|
||||
valueProps="checked"
|
||||
@@ -182,9 +194,13 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
<ListItemText
|
||||
primary={
|
||||
<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
|
||||
title={t("Hover Jump Navigator Info")}
|
||||
title={t(
|
||||
"settings.components.verge.layout.tooltips.hoverNavigator",
|
||||
)}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
</Box>
|
||||
@@ -206,9 +222,15 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
<ListItemText
|
||||
primary={
|
||||
<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
|
||||
title={t("Hover Jump Navigator Delay Info")}
|
||||
title={t(
|
||||
"settings.components.verge.layout.tooltips.hoverNavigatorDelay",
|
||||
)}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
</Box>
|
||||
@@ -241,7 +263,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("millis")}
|
||||
{t("shared.units.milliseconds")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
@@ -256,7 +278,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Nav Icon")} />
|
||||
<ListItemText
|
||||
primary={t("settings.components.verge.layout.fields.navIcon")}
|
||||
/>
|
||||
<GuardState
|
||||
value={verge?.menu_icon ?? "monochrome"}
|
||||
onCatch={onError}
|
||||
@@ -265,16 +289,24 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
onGuard={(value) => patchVerge({ menu_icon: value })}
|
||||
>
|
||||
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
||||
<MenuItem value="monochrome">{t("Monochrome")}</MenuItem>
|
||||
<MenuItem value="colorful">{t("Colorful")}</MenuItem>
|
||||
<MenuItem value="disable">{t("Disable")}</MenuItem>
|
||||
<MenuItem value="monochrome">
|
||||
{t("settings.components.verge.layout.options.icon.monochrome")}
|
||||
</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>
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
{OS === "macos" && (
|
||||
<Item>
|
||||
<ListItemText primary={t("Tray Icon")} />
|
||||
<ListItemText
|
||||
primary={t("settings.components.verge.layout.fields.trayIcon")}
|
||||
/>
|
||||
<GuardState
|
||||
value={verge?.tray_icon ?? "monochrome"}
|
||||
onCatch={onError}
|
||||
@@ -286,15 +318,21 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
size="small"
|
||||
sx={{ width: 140, "> div": { py: "7.5px" } }}
|
||||
>
|
||||
<MenuItem value="monochrome">{t("Monochrome")}</MenuItem>
|
||||
<MenuItem value="colorful">{t("Colorful")}</MenuItem>
|
||||
<MenuItem value="monochrome">
|
||||
{t(
|
||||
"settings.components.verge.layout.options.icon.monochrome",
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value="colorful">
|
||||
{t("settings.components.verge.layout.options.icon.colorful")}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</GuardState>
|
||||
</Item>
|
||||
)}
|
||||
{/* {OS === "macos" && (
|
||||
<Item>
|
||||
<ListItemText primary={t("Enable Tray Speed")} />
|
||||
<ListItemText primary={t("settings.components.verge.layout.fields.enableTraySpeed")} />
|
||||
<GuardState
|
||||
value={verge?.enable_tray_speed ?? false}
|
||||
valueProps="checked"
|
||||
@@ -309,7 +347,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
)} */}
|
||||
{/* {OS === "macos" && (
|
||||
<Item>
|
||||
<ListItemText primary={t("Enable Tray Icon")} />
|
||||
<ListItemText primary={t("settings.components.verge.layout.fields.enableTrayIcon")} />
|
||||
<GuardState
|
||||
value={
|
||||
verge?.enable_tray_icon === false &&
|
||||
@@ -328,7 +366,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</Item>
|
||||
)} */}
|
||||
<Item>
|
||||
<ListItemText primary={t("Show Proxy Groups Inline")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"settings.components.verge.layout.fields.showProxyGroupsInline",
|
||||
)}
|
||||
/>
|
||||
<GuardState
|
||||
value={verge?.tray_inline_proxy_groups ?? false}
|
||||
valueProps="checked"
|
||||
@@ -342,7 +384,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Common Tray Icon")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"settings.components.verge.layout.fields.commonTrayIcon",
|
||||
)}
|
||||
/>
|
||||
<GuardState
|
||||
value={verge?.common_tray_icon}
|
||||
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>
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("System Proxy Tray Icon")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"settings.components.verge.layout.fields.systemProxyTrayIcon",
|
||||
)}
|
||||
/>
|
||||
<GuardState
|
||||
value={verge?.sysproxy_tray_icon}
|
||||
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>
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Tun Tray Icon")} />
|
||||
<ListItemText
|
||||
primary={t("settings.components.verge.layout.fields.tunTrayIcon")}
|
||||
/>
|
||||
<GuardState
|
||||
value={verge?.tun_tray_icon}
|
||||
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>
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
@@ -45,25 +45,27 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
auto_light_weight_minutes: values.autoEnterLiteModeDelay,
|
||||
});
|
||||
setOpen(false);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("LightWeight Mode Settings")}
|
||||
title={t("settings.modals.liteMode.title")}
|
||||
contentSx={{ width: 450 }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<List>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Enter LightWeight Mode Now")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.liteMode.actions.enterNow")}
|
||||
/>
|
||||
<Typography
|
||||
variant="button"
|
||||
sx={{
|
||||
@@ -73,17 +75,17 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
}}
|
||||
onClick={async () => await entry_lightweight_mode()}
|
||||
>
|
||||
{t("Enable")}
|
||||
{t("shared.actions.enable")}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText
|
||||
primary={t("Auto Enter LightWeight Mode")}
|
||||
primary={t("settings.modals.liteMode.toggles.autoEnter")}
|
||||
sx={{ maxWidth: "fit-content" }}
|
||||
/>
|
||||
<TooltipIcon
|
||||
title={t("Auto Enter LightWeight Mode Info")}
|
||||
title={t("settings.modals.liteMode.tooltips.autoEnter")}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
<Switch
|
||||
@@ -99,7 +101,9 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
{values.autoEnterLiteMode && (
|
||||
<>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Auto Enter LightWeight Mode Delay")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.liteMode.fields.delay")}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
size="small"
|
||||
@@ -119,7 +123,7 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("mins")}
|
||||
{t("shared.units.minutes")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
@@ -133,10 +137,9 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
color="text.secondary"
|
||||
sx={{ fontStyle: "italic" }}
|
||||
>
|
||||
{t(
|
||||
"When closing the window, LightWeight Mode will be automatically activated after _n minutes",
|
||||
{ n: values.autoEnterLiteModeDelay },
|
||||
)}
|
||||
{t("settings.modals.liteMode.messages.autoEnterHint", {
|
||||
n: values.autoEnterLiteModeDelay,
|
||||
})}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</>
|
||||
|
||||
@@ -20,11 +20,13 @@ export const LocalBackupActions = memo(
|
||||
try {
|
||||
setLoading(true);
|
||||
await createLocalBackup();
|
||||
showNotice("success", t("Local Backup Created"));
|
||||
showNotice.success(
|
||||
"settings.modals.backup.messages.localBackupCreated",
|
||||
);
|
||||
await onBackupSuccess();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showNotice("error", t("Local Backup Failed"));
|
||||
showNotice.error("settings.modals.backup.messages.localBackupFailed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -43,7 +45,7 @@ export const LocalBackupActions = memo(
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 9 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Local Backup Info")}
|
||||
{t("settings.modals.backup.fields.info")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
@@ -60,7 +62,7 @@ export const LocalBackupActions = memo(
|
||||
type="button"
|
||||
size="large"
|
||||
>
|
||||
{t("Backup")}
|
||||
{t("settings.modals.backup.actions.backup")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -68,7 +70,7 @@ export const LocalBackupActions = memo(
|
||||
type="button"
|
||||
size="large"
|
||||
>
|
||||
{t("Refresh")}
|
||||
{t("shared.actions.refresh")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
@@ -69,25 +69,27 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
auto_log_clean: values.autoLogClean as any,
|
||||
});
|
||||
setOpen(false);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Miscellaneous")}
|
||||
title={t("settings.modals.misc.title")}
|
||||
contentSx={{ width: 450 }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<List>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("App Log Level")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.misc.fields.appLogLevel")}
|
||||
/>
|
||||
<Select
|
||||
size="small"
|
||||
sx={{ width: 100, "> div": { py: "7.5px" } }}
|
||||
@@ -109,7 +111,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText
|
||||
primary={t("App Log Max Size")}
|
||||
primary={t("settings.modals.misc.fields.appLogMaxSize")}
|
||||
sx={{ maxWidth: "fit-content" }}
|
||||
/>
|
||||
<TextField
|
||||
@@ -130,7 +132,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
slotProps={{
|
||||
input: {
|
||||
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" }}>
|
||||
<ListItemText
|
||||
primary={t("App Log Max Count")}
|
||||
primary={t("settings.modals.misc.fields.appLogMaxCount")}
|
||||
sx={{ maxWidth: "fit-content" }}
|
||||
/>
|
||||
<TextField
|
||||
@@ -160,7 +164,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
slotProps={{
|
||||
input: {
|
||||
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" }}>
|
||||
<ListItemText
|
||||
primary={t("Auto Close Connections")}
|
||||
primary={t("settings.modals.misc.fields.autoCloseConnections")}
|
||||
sx={{ maxWidth: "fit-content" }}
|
||||
/>
|
||||
<TooltipIcon
|
||||
title={t("Auto Close Connections Info")}
|
||||
title={t("settings.modals.misc.tooltips.autoCloseConnections")}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
<Switch
|
||||
@@ -187,7 +193,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Auto Check Update")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.misc.fields.autoCheckUpdate")}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.autoCheckUpdate}
|
||||
@@ -199,11 +207,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText
|
||||
primary={t("Enable Builtin Enhanced")}
|
||||
primary={t("settings.modals.misc.fields.enableBuiltinEnhanced")}
|
||||
sx={{ maxWidth: "fit-content" }}
|
||||
/>
|
||||
<TooltipIcon
|
||||
title={t("Enable Builtin Enhanced Info")}
|
||||
title={t("settings.modals.misc.tooltips.enableBuiltinEnhanced")}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
<Switch
|
||||
@@ -217,7 +225,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Proxy Layout Columns")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.misc.fields.proxyLayoutColumns")}
|
||||
/>
|
||||
<Select
|
||||
size="small"
|
||||
sx={{ width: 160, "> div": { py: "7.5px" } }}
|
||||
@@ -230,7 +240,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
}
|
||||
>
|
||||
<MenuItem value={6} key={6}>
|
||||
{t("Auto Columns")}
|
||||
{t("settings.modals.misc.options.proxyLayoutColumns.auto")}
|
||||
</MenuItem>
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<MenuItem value={i} key={i}>
|
||||
@@ -241,7 +251,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Auto Log Clean")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.misc.fields.autoLogClean")}
|
||||
/>
|
||||
<Select
|
||||
size="small"
|
||||
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天*/}
|
||||
{[
|
||||
{ key: t("Never Clean"), value: 0 },
|
||||
{ key: t("Retain _n Days", { n: 1 }), value: 1 },
|
||||
{ key: t("Retain _n Days", { n: 7 }), value: 2 },
|
||||
{ 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.never"),
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<MenuItem key={i.value} value={i.value}>
|
||||
{i.key}
|
||||
@@ -270,11 +305,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText
|
||||
primary={t("Auto Delay Detection")}
|
||||
primary={t("settings.modals.misc.fields.autoDelayDetection")}
|
||||
sx={{ maxWidth: "fit-content" }}
|
||||
/>
|
||||
<TooltipIcon
|
||||
title={t("Auto Delay Detection Info")}
|
||||
title={t("settings.modals.misc.tooltips.autoDelayDetection")}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
<Switch
|
||||
@@ -289,11 +324,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText
|
||||
primary={t("Default Latency Test")}
|
||||
primary={t("settings.modals.misc.fields.defaultLatencyTest")}
|
||||
sx={{ maxWidth: "fit-content" }}
|
||||
/>
|
||||
<TooltipIcon
|
||||
title={t("Default Latency Test Info")}
|
||||
title={t("settings.modals.misc.tooltips.defaultLatencyTest")}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
<TextField
|
||||
@@ -312,7 +347,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Default Latency Timeout")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.misc.fields.defaultLatencyTimeout")}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -332,7 +369,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
slotProps={{
|
||||
input: {
|
||||
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}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Network Interface")}
|
||||
{t("settings.modals.networkInterface.title")}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -51,7 +51,7 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
}
|
||||
contentSx={{ width: 450 }}
|
||||
disableOk
|
||||
cancelBtn={t("Close")}
|
||||
cancelBtn={t("shared.actions.close")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
@@ -66,13 +66,17 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
address.V4 && (
|
||||
<AddressDisplay
|
||||
key={address.V4.ip}
|
||||
label={t("Ip Address")}
|
||||
label={t(
|
||||
"settings.modals.networkInterface.fields.ipAddress",
|
||||
)}
|
||||
content={address.V4.ip}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<AddressDisplay
|
||||
label={t("Mac Address")}
|
||||
label={t(
|
||||
"settings.modals.networkInterface.fields.macAddress",
|
||||
)}
|
||||
content={item.mac_addr ?? ""}
|
||||
/>
|
||||
</>
|
||||
@@ -84,13 +88,17 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
address.V6 && (
|
||||
<AddressDisplay
|
||||
key={address.V6.ip}
|
||||
label={t("Ip Address")}
|
||||
label={t(
|
||||
"settings.modals.networkInterface.fields.ipAddress",
|
||||
)}
|
||||
content={address.V6.ip}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<AddressDisplay
|
||||
label={t("Mac Address")}
|
||||
label={t(
|
||||
"settings.modals.networkInterface.fields.macAddress",
|
||||
)}
|
||||
content={item.mac_addr ?? ""}
|
||||
/>
|
||||
</>
|
||||
@@ -109,8 +117,6 @@ const AddressDisplay = ({
|
||||
label: string;
|
||||
content: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -135,7 +141,9 @@ const AddressDisplay = ({
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
await writeText(content);
|
||||
showNotice("success", t("Copy Success"));
|
||||
showNotice.success(
|
||||
"shared.feedback.notifications.common.copySuccess",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ContentCopyRounded sx={{ fontSize: "18px" }} />
|
||||
|
||||
@@ -21,13 +21,15 @@ export const PasswordInput = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={true} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>{t("Please enter your root password")}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{t("settings.modals.password.prompts.enterRoot")}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<TextField
|
||||
sx={{ mt: 1 }}
|
||||
autoFocus
|
||||
label={t("Password")}
|
||||
label={t("shared.labels.password")}
|
||||
fullWidth
|
||||
size="small"
|
||||
type="password"
|
||||
@@ -42,7 +44,7 @@ export const PasswordInput = (props: Props) => {
|
||||
onClick={async () => await onConfirm(passwd)}
|
||||
variant="contained"
|
||||
>
|
||||
{t("Confirm")}
|
||||
{t("shared.actions.confirm")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -160,8 +160,8 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
mutate("getAutotemProxy"),
|
||||
]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -277,14 +277,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
const onSave = useLockFn(async () => {
|
||||
if (value.duration < 1) {
|
||||
showNotice(
|
||||
"error",
|
||||
t("Proxy Daemon Duration Cannot be Less than 1 Second"),
|
||||
);
|
||||
showNotice.error("settings.modals.sysproxy.messages.durationTooShort");
|
||||
return;
|
||||
}
|
||||
if (value.bypass && !validReg.test(value.bypass)) {
|
||||
showNotice("error", t("Invalid Bypass Format"));
|
||||
showNotice.error("settings.modals.sysproxy.messages.invalidBypass");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -301,7 +298,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
!ipv6Regex.test(value.proxy_host) &&
|
||||
!hostnameRegex.test(value.proxy_host)
|
||||
) {
|
||||
showNotice("error", t("Invalid Proxy Host Format"));
|
||||
showNotice.error("settings.modals.sysproxy.messages.invalidProxyHost");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -401,10 +398,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
console.warn("代理状态更新失败:", err);
|
||||
}
|
||||
}, 50);
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
console.error("配置保存失败:", err);
|
||||
mutateVerge();
|
||||
showNotice("error", err.toString());
|
||||
showNotice.error(err);
|
||||
// setOpen(true);
|
||||
}
|
||||
});
|
||||
@@ -413,10 +410,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("System Proxy Setting")}
|
||||
title={t("settings.modals.sysproxy.title")}
|
||||
contentSx={{ width: 450, maxHeight: 565 }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
@@ -424,28 +421,37 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
disableOk={saving}
|
||||
>
|
||||
<List>
|
||||
<BaseFieldset label={t("Current System Proxy")} padding="15px 10px">
|
||||
<BaseFieldset
|
||||
label={t("settings.modals.sysproxy.fieldsets.currentStatus")}
|
||||
padding="15px 10px"
|
||||
>
|
||||
<FlexBox>
|
||||
<Typography className="label">{t("Enable status")}</Typography>
|
||||
<Typography className="label">
|
||||
{t("settings.modals.sysproxy.fields.enableStatus")}
|
||||
</Typography>
|
||||
<Typography className="value">
|
||||
{value.pac
|
||||
? autoproxy?.enable
|
||||
? t("Enabled")
|
||||
: t("Disabled")
|
||||
? t("shared.statuses.enabled")
|
||||
: t("shared.statuses.disabled")
|
||||
: sysproxy?.enable
|
||||
? t("Enabled")
|
||||
: t("Disabled")}
|
||||
? t("shared.statuses.enabled")
|
||||
: t("shared.statuses.disabled")}
|
||||
</Typography>
|
||||
</FlexBox>
|
||||
{!value.pac && (
|
||||
<FlexBox>
|
||||
<Typography className="label">{t("Server Addr")}</Typography>
|
||||
<Typography className="label">
|
||||
{t("settings.modals.sysproxy.fields.serverAddr")}
|
||||
</Typography>
|
||||
<Typography className="value">{getSystemProxyAddress}</Typography>
|
||||
</FlexBox>
|
||||
)}
|
||||
{value.pac && (
|
||||
<FlexBox>
|
||||
<Typography className="label">{t("PAC URL")}</Typography>
|
||||
<Typography className="label">
|
||||
{t("settings.modals.sysproxy.fields.pacUrl")}
|
||||
</Typography>
|
||||
<Typography className="value">
|
||||
{getCurrentPacUrl || "-"}
|
||||
</Typography>
|
||||
@@ -453,7 +459,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
)}
|
||||
</BaseFieldset>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Proxy Host")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.sysproxy.fields.proxyHost")}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ width: 150 }}
|
||||
@@ -478,7 +486,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Use PAC Mode")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.sysproxy.fields.usePacMode")}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
disabled={!enabled}
|
||||
@@ -489,10 +499,13 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText
|
||||
primary={t("Proxy Guard")}
|
||||
primary={t("settings.modals.sysproxy.fields.proxyGuard")}
|
||||
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
|
||||
edge="end"
|
||||
disabled={!enabled}
|
||||
@@ -503,7 +516,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Guard Duration")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.sysproxy.fields.guardDuration")}
|
||||
/>
|
||||
<TextField
|
||||
disabled={!enabled}
|
||||
size="small"
|
||||
@@ -524,7 +539,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</ListItem>
|
||||
{!value.pac && (
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Always use Default Bypass")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"settings.modals.sysproxy.fields.alwaysUseDefaultBypass",
|
||||
)}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
disabled={!enabled}
|
||||
@@ -543,7 +562,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
{!value.pac && !value.use_default && (
|
||||
<>
|
||||
<ListItemText primary={t("Proxy Bypass")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.sysproxy.fields.proxyBypass")}
|
||||
/>
|
||||
<TextField
|
||||
error={value.bypass ? !validReg.test(value.bypass) : false}
|
||||
disabled={!enabled}
|
||||
@@ -561,7 +582,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
{!value.pac && value.use_default && (
|
||||
<>
|
||||
<ListItemText primary={t("Bypass")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.sysproxy.fields.bypass")}
|
||||
/>
|
||||
<FlexBox>
|
||||
<TextField
|
||||
disabled={true}
|
||||
@@ -578,7 +601,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
{value.pac && (
|
||||
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
|
||||
<ListItemText
|
||||
primary={t("PAC Script Content")}
|
||||
primary={t("settings.modals.sysproxy.fields.pacScriptContent")}
|
||||
sx={{ padding: "3px 0" }}
|
||||
/>
|
||||
<Button
|
||||
@@ -588,12 +611,12 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
setEditorOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("Edit")} PAC
|
||||
{t("settings.modals.sysproxy.actions.editPac")}
|
||||
</Button>
|
||||
{editorOpen && (
|
||||
<EditorViewer
|
||||
open={true}
|
||||
title={`${t("Edit")} PAC`}
|
||||
title={t("settings.modals.sysproxy.actions.editPac")}
|
||||
initialData={Promise.resolve(value.pac_content ?? "")}
|
||||
language="javascript"
|
||||
onSave={(_prev, curr) => {
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ThemeModeSwitch = (props: Props) => {
|
||||
onClick={() => onChange?.(mode)}
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
>
|
||||
{t(`theme.${mode}`)}
|
||||
{t(`settings.sections.appearance.${mode}`)}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useImperativeHandle, useState } from "react";
|
||||
import { useImperativeHandle, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
@@ -50,8 +50,8 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
|
||||
try {
|
||||
await patchVerge({ theme_setting: theme });
|
||||
setOpen(false);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,9 +62,48 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
|
||||
|
||||
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 (
|
||||
<Item>
|
||||
<Item key={key}>
|
||||
<ListItemText primary={label} />
|
||||
<Round sx={{ background: theme[key] || dt[key] }} />
|
||||
<TextField
|
||||
@@ -81,33 +120,21 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Theme Setting")}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
title={t("settings.components.verge.theme.title")}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
contentSx={{ width: 400, maxHeight: 505, overflow: "auto", pb: 0 }}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<List sx={{ pt: 0 }}>
|
||||
{renderItem(t("Primary Color"), "primary_color")}
|
||||
|
||||
{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")}
|
||||
{fieldDefinitions.map((field) => renderItem(field.labelKey, field.key))}
|
||||
|
||||
<Item>
|
||||
<ListItemText primary={t("Font Family")} />
|
||||
<ListItemText
|
||||
primary={t("settings.components.verge.theme.fields.fontFamily")}
|
||||
/>
|
||||
<TextField
|
||||
{...textProps}
|
||||
value={theme.font_family ?? ""}
|
||||
@@ -116,7 +143,9 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
<Item>
|
||||
<ListItemText primary={t("CSS Injection")} />
|
||||
<ListItemText
|
||||
primary={t("settings.components.verge.theme.fields.cssInjection")}
|
||||
/>
|
||||
<Button
|
||||
startIcon={<EditRounded />}
|
||||
variant="outlined"
|
||||
@@ -124,12 +153,12 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
|
||||
setEditorOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("Edit")} CSS
|
||||
{t("settings.components.verge.theme.actions.editCss")}
|
||||
</Button>
|
||||
{editorOpen && (
|
||||
<EditorViewer
|
||||
open={true}
|
||||
title={`${t("Edit")} CSS`}
|
||||
title={t("settings.components.verge.theme.dialogs.editCssTitle")}
|
||||
initialData={Promise.resolve(theme.css_injection ?? "")}
|
||||
language="css"
|
||||
onSave={(_prev, curr) => {
|
||||
|
||||
@@ -80,13 +80,13 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
);
|
||||
try {
|
||||
await enhanceProfiles();
|
||||
showNotice("success", t("Settings Applied"));
|
||||
showNotice.success("settings.modals.tun.messages.applied");
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
showNotice.error(err);
|
||||
}
|
||||
setOpen(false);
|
||||
} 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}
|
||||
title={
|
||||
<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
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@@ -128,20 +128,20 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t("Reset to Default")}
|
||||
{t("shared.actions.resetToDefault")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
contentSx={{ width: 450 }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<List>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Stack")} />
|
||||
<ListItemText primary={t("settings.modals.tun.fields.stack")} />
|
||||
<StackModeSwitch
|
||||
value={values.stack}
|
||||
onChange={(value) => {
|
||||
@@ -154,7 +154,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Device")} />
|
||||
<ListItemText primary={t("settings.modals.tun.fields.device")} />
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -171,7 +171,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Auto Route")} />
|
||||
<ListItemText primary={t("settings.modals.tun.fields.autoRoute")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.autoRoute}
|
||||
@@ -180,7 +180,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Strict Route")} />
|
||||
<ListItemText primary={t("settings.modals.tun.fields.strictRoute")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.strictRoute}
|
||||
@@ -189,7 +189,9 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Auto Detect Interface")} />
|
||||
<ListItemText
|
||||
primary={t("settings.modals.tun.fields.autoDetectInterface")}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.autoDetectInterface}
|
||||
@@ -200,7 +202,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("DNS Hijack")} />
|
||||
<ListItemText primary={t("settings.modals.tun.fields.dnsHijack")} />
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -209,7 +211,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
spellCheck="false"
|
||||
sx={{ width: 250 }}
|
||||
value={values.dnsHijack.join(",")}
|
||||
placeholder="Please use , to separate multiple DNS servers"
|
||||
placeholder={t("settings.modals.tun.tooltips.dnsHijack")}
|
||||
onChange={(e) =>
|
||||
setValues((v) => ({ ...v, dnsHijack: e.target.value.split(",") }))
|
||||
}
|
||||
@@ -217,7 +219,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("MTU")} />
|
||||
<ListItemText primary={t("settings.modals.tun.fields.mtu")} />
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user