mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-15 22:40:42 +08:00
* chore: notice i18n * feat: add script to clean up unused i18n keys * chore: cleanup i18n keys * refactor(i18n/proxies): migrate proxies UI to structured locale keys * chore: i18n for rule module * chore: i18n for profile module * chore: i18n for connections module * chore: i18n for settings module * chore: i18n for verge settings * chore: i18n for theme settings * chore: i18n for theme * chore(i18n): components.home.* * chore(i18n): remove unused i18n keys * chore(i18n): components.profile.* * chore(i18n): components.connection * chore(i18n): pages.logs.* * chore(i18n): pages.*.provider * chore(i18n): components.settings.externalCors.* * chore(i18n): components.settings.clash.* * chore(i18n): components.settings.liteMode.* * chore(i18n): components.settings.backup.* * chore(i18n): components.settings.clash.port.* * chore(i18n): components.settings.misc.* * chore(i18n): components.settings.update.* * chore(i18n): components.settings.sysproxy.* * chore(i18n): components.settings.sysproxy.* * chore(i18n): pages.profiles.notices/components.providers.notices * refactor(notice): unify showNotice usage * refactor(notice): add typed showNotice shortcuts, centralize defaults, and simplify subscriptions * refactor: unify showNotice usage * refactor(notice): unify showNotice API * refactor(notice): unify showNotice usage * chore(i18n): components.test.* * chore(i18n): components.settings.dns.* * chore(i18n): components.home.clashInfo.* * chore(i18n): components.home.systemInfo.* * chore(i18n): components.home.ipInfo/traffic.* * chore(i18n): navigation.* * refactor(i18n): remove pages.* namespace and migrate route texts under module-level page keys * chore(i18n): common.* * chore(i18n): common.* * fix: change error handling in patch_profiles_config to return false when a switch is in progress * fix: improve error handling in patch_profiles_config to prevent requests during profile switching * fix: change error handling in patch_profiles_config to return false when a switch is in progress fix: ensure CURRENT_SWITCHING_PROFILE is reset after config updates in perform_config_update and patch_profiles_config * chore(i18n): restructure root-level locale keys into namespaces * chore(i18n): add missing i18n keys * docs: i18n guide * chore: adjust i18n * refactor(i18n): align UI actions and status labels with common keys * refactor(i18n): unify two-name locale namespaces * refactor(i18n/components): unify locale keys and update component references * chore(i18n): add shared and entities namespaces to all locale files * refactor(i18n): consolidate shared and entity namespaces across features * chore(deps): update npm dependencies to ^7.3.5 (#5310) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * refactor(i18n): migrate shared editor modes and consolidate entities namespaces * tmp * refactor(i18n): flatten locales and move theme/validation strings * docs: CONTRIBUTING_i18n.md * refactor(i18n): restructure feedback and profile namespaces for better organization * refactor(i18n): unify settings locale structure and update references * refactor(i18n): reorganize locale keys for home, proxies, rules, connections, logs, unlock, and tests * refactor(i18n/feedback/layout): unify shared toasts & normalize layout namespace * refactor(i18n): centralize common UI strings in shared * refactor(i18n): flatten headers and unify locale schema * refactor(i18n): consolidate duplicate per-feature translations into shared namespace * refactor(i18n): split locales into per-namespace files * style: lint * refactor(i18n): unify unlock UI translations under tests namespace * feat(i18n): add type-checked translation keys * style: eslint import order * feat(i18n): replace ad-hoc loader with rust-i18n backend bundles * chore(prebuild): remove locale-copy step * fix(i18n, notice): propagate runtime params and update cleanup script path * fix(i18n,notice): make locale formatting idempotent and guard early notice translations * fix(i18n): resolve locale aliases and match OS codes correctly * fix(unlock): use i18next-compatible double-brace interpolation in failure notice * fix(i18n): route unlock error notices through translation keys * fix(i18n): i18n types * feat(i18n): localize upgrade notice for Clash core viewer * fix(notice): ensure runtime overrides apply to prefix translations * chore(i18n): replace literal notices with translation keys * chore(i18n): types * chore(i18n): regen typings before formatting to keep keys in sync * chore(i18n): simply labels * chore(i18n): adjust translation * chore: remove eslint-plugin-i18next * chore(i18n): add/refine Korean translations across frontend scopes and Rust backend (#5341) * chore(i18n): translate settings.json (missed in previous pass) (#5343) * chore(i18n): add/refine Korean translations across frontend scopes and Rust backend * chore(i18n): add/refine Korean translations across frontend scopes and Rust backend * fix(i18n-tauri): quote placeholder-leading value in ko.yml to prevent rust_i18n parse panic * chore(i18n): translate settings.json (forgot to include previously) --------- Co-authored-by: rozan <34974262+thelojan@users.noreply.github.com>
350 lines
11 KiB
TypeScript
350 lines
11 KiB
TypeScript
import { Shuffle } from "@mui/icons-material";
|
||
import {
|
||
CircularProgress,
|
||
IconButton,
|
||
List,
|
||
ListItem,
|
||
ListItemText,
|
||
Stack,
|
||
TextField,
|
||
} from "@mui/material";
|
||
import { useLockFn, useRequest } from "ahooks";
|
||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||
import { useTranslation } from "react-i18next";
|
||
|
||
import { BaseDialog, Switch } from "@/components/base";
|
||
import { useClashInfo } from "@/hooks/use-clash";
|
||
import { useVerge } from "@/hooks/use-verge";
|
||
import { showNotice } from "@/services/noticeService";
|
||
import getSystem from "@/utils/get-system";
|
||
|
||
const OS = getSystem();
|
||
|
||
interface ClashPortViewerRef {
|
||
open: () => void;
|
||
close: () => void;
|
||
}
|
||
|
||
const generateRandomPort = () =>
|
||
Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
|
||
|
||
export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||
const { t } = useTranslation();
|
||
const { clashInfo, patchInfo } = useClashInfo();
|
||
const { verge, patchVerge } = useVerge();
|
||
const [open, setOpen] = useState(false);
|
||
|
||
// Mixed Port
|
||
const [mixedPort, setMixedPort] = useState(
|
||
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897,
|
||
);
|
||
|
||
// 其他端口状态
|
||
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
|
||
const [socksEnabled, setSocksEnabled] = useState(
|
||
verge?.verge_socks_enabled ?? false,
|
||
);
|
||
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
|
||
const [httpEnabled, setHttpEnabled] = useState(
|
||
verge?.verge_http_enabled ?? false,
|
||
);
|
||
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895);
|
||
const [redirEnabled, setRedirEnabled] = useState(
|
||
verge?.verge_redir_enabled ?? false,
|
||
);
|
||
const [tproxyPort, setTproxyPort] = useState(
|
||
verge?.verge_tproxy_port ?? 7896,
|
||
);
|
||
const [tproxyEnabled, setTproxyEnabled] = useState(
|
||
verge?.verge_tproxy_enabled ?? false,
|
||
);
|
||
|
||
// 添加保存请求,防止GUI卡死
|
||
const { loading, run: saveSettings } = useRequest(
|
||
async (params: { clashConfig: any; vergeConfig: any }) => {
|
||
const { clashConfig, vergeConfig } = params;
|
||
await Promise.all([patchInfo(clashConfig), patchVerge(vergeConfig)]);
|
||
},
|
||
{
|
||
manual: true,
|
||
onSuccess: () => {
|
||
setOpen(false);
|
||
showNotice.success("settings.modals.clashPort.messages.saved");
|
||
},
|
||
onError: (error) => {
|
||
showNotice.error(
|
||
"settings.modals.clashPort.messages.saveFailed",
|
||
error,
|
||
);
|
||
},
|
||
},
|
||
);
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
open: () => {
|
||
setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
|
||
setSocksPort(verge?.verge_socks_port ?? 7898);
|
||
setSocksEnabled(verge?.verge_socks_enabled ?? false);
|
||
setHttpPort(verge?.verge_port ?? 7899);
|
||
setHttpEnabled(verge?.verge_http_enabled ?? false);
|
||
setRedirPort(verge?.verge_redir_port ?? 7895);
|
||
setRedirEnabled(verge?.verge_redir_enabled ?? false);
|
||
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
|
||
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
|
||
setOpen(true);
|
||
},
|
||
close: () => setOpen(false),
|
||
}));
|
||
|
||
const onSave = useLockFn(async () => {
|
||
// 端口冲突检测
|
||
const portList = [
|
||
mixedPort,
|
||
socksEnabled ? socksPort : -1,
|
||
httpEnabled ? httpPort : -1,
|
||
redirEnabled ? redirPort : -1,
|
||
tproxyEnabled ? tproxyPort : -1,
|
||
].filter((p) => p !== -1);
|
||
|
||
if (new Set(portList).size !== portList.length) {
|
||
return;
|
||
}
|
||
|
||
// 验证端口范围
|
||
const isValidPort = (port: number) => port >= 1 && port <= 65535;
|
||
const allPortsValid = [
|
||
mixedPort,
|
||
socksEnabled ? socksPort : 0,
|
||
httpEnabled ? httpPort : 0,
|
||
redirEnabled ? redirPort : 0,
|
||
tproxyEnabled ? tproxyPort : 0,
|
||
].every((port) => port === 0 || isValidPort(port));
|
||
|
||
if (!allPortsValid) {
|
||
return;
|
||
}
|
||
|
||
// 准备配置数据
|
||
const clashConfig = {
|
||
"mixed-port": mixedPort,
|
||
"socks-port": socksPort,
|
||
port: httpPort,
|
||
"redir-port": redirPort,
|
||
"tproxy-port": tproxyPort,
|
||
};
|
||
|
||
const vergeConfig = {
|
||
verge_mixed_port: mixedPort,
|
||
verge_socks_port: socksPort,
|
||
verge_socks_enabled: socksEnabled,
|
||
verge_port: httpPort,
|
||
verge_http_enabled: httpEnabled,
|
||
verge_redir_port: redirPort,
|
||
verge_redir_enabled: redirEnabled,
|
||
verge_tproxy_port: tproxyPort,
|
||
verge_tproxy_enabled: tproxyEnabled,
|
||
};
|
||
|
||
// 提交保存请求
|
||
await saveSettings({ clashConfig, vergeConfig });
|
||
});
|
||
|
||
return (
|
||
<BaseDialog
|
||
open={open}
|
||
title={t("settings.modals.clashPort.title")}
|
||
contentSx={{
|
||
width: 400,
|
||
}}
|
||
okBtn={
|
||
loading ? (
|
||
<Stack direction="row" alignItems="center" spacing={1}>
|
||
<CircularProgress size={20} />
|
||
{t("shared.statuses.saving")}
|
||
</Stack>
|
||
) : (
|
||
t("shared.actions.save")
|
||
)
|
||
}
|
||
cancelBtn={t("shared.actions.cancel")}
|
||
onClose={() => setOpen(false)}
|
||
onCancel={() => setOpen(false)}
|
||
onOk={onSave}
|
||
>
|
||
<List sx={{ width: "100%" }}>
|
||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||
<ListItemText
|
||
primary={t("settings.modals.clashPort.fields.mixed")}
|
||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||
/>
|
||
<div style={{ display: "flex", alignItems: "center" }}>
|
||
<TextField
|
||
size="small"
|
||
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
||
value={mixedPort}
|
||
onChange={(e) =>
|
||
setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||
}
|
||
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => setMixedPort(generateRandomPort())}
|
||
title={t("settings.modals.clashPort.actions.random")}
|
||
sx={{ mr: 0.5 }}
|
||
>
|
||
<Shuffle fontSize="small" />
|
||
</IconButton>
|
||
<Switch
|
||
size="small"
|
||
checked={true}
|
||
disabled={true}
|
||
sx={{ ml: 0.5, opacity: 0.7 }}
|
||
/>
|
||
</div>
|
||
</ListItem>
|
||
|
||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||
<ListItemText
|
||
primary={t("settings.modals.clashPort.fields.socks")}
|
||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||
/>
|
||
<div style={{ display: "flex", alignItems: "center" }}>
|
||
<TextField
|
||
size="small"
|
||
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
||
value={socksPort}
|
||
onChange={(e) =>
|
||
setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||
}
|
||
disabled={!socksEnabled}
|
||
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => setSocksPort(generateRandomPort())}
|
||
title={t("settings.modals.clashPort.actions.random")}
|
||
disabled={!socksEnabled}
|
||
sx={{ mr: 0.5 }}
|
||
>
|
||
<Shuffle fontSize="small" />
|
||
</IconButton>
|
||
<Switch
|
||
size="small"
|
||
checked={socksEnabled}
|
||
onChange={(_, c) => setSocksEnabled(c)}
|
||
sx={{ ml: 0.5 }}
|
||
/>
|
||
</div>
|
||
</ListItem>
|
||
|
||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||
<ListItemText
|
||
primary={t("settings.modals.clashPort.fields.http")}
|
||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||
/>
|
||
<div style={{ display: "flex", alignItems: "center" }}>
|
||
<TextField
|
||
size="small"
|
||
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
||
value={httpPort}
|
||
onChange={(e) =>
|
||
setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||
}
|
||
disabled={!httpEnabled}
|
||
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => setHttpPort(generateRandomPort())}
|
||
title={t("settings.modals.clashPort.actions.random")}
|
||
disabled={!httpEnabled}
|
||
sx={{ mr: 0.5 }}
|
||
>
|
||
<Shuffle fontSize="small" />
|
||
</IconButton>
|
||
<Switch
|
||
size="small"
|
||
checked={httpEnabled}
|
||
onChange={(_, c) => setHttpEnabled(c)}
|
||
sx={{ ml: 0.5 }}
|
||
/>
|
||
</div>
|
||
</ListItem>
|
||
|
||
{OS !== "windows" && (
|
||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||
<ListItemText
|
||
primary={t("settings.modals.clashPort.fields.redir")}
|
||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||
/>
|
||
<div style={{ display: "flex", alignItems: "center" }}>
|
||
<TextField
|
||
size="small"
|
||
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
||
value={redirPort}
|
||
onChange={(e) =>
|
||
setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||
}
|
||
disabled={!redirEnabled}
|
||
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => setRedirPort(generateRandomPort())}
|
||
title={t("settings.modals.clashPort.actions.random")}
|
||
disabled={!redirEnabled}
|
||
sx={{ mr: 0.5 }}
|
||
>
|
||
<Shuffle fontSize="small" />
|
||
</IconButton>
|
||
<Switch
|
||
size="small"
|
||
checked={redirEnabled}
|
||
onChange={(_, c) => setRedirEnabled(c)}
|
||
sx={{ ml: 0.5 }}
|
||
/>
|
||
</div>
|
||
</ListItem>
|
||
)}
|
||
|
||
{OS === "linux" && (
|
||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||
<ListItemText
|
||
primary={t("settings.modals.clashPort.fields.tproxy")}
|
||
slotProps={{ primary: { sx: { fontSize: 12 } } }}
|
||
/>
|
||
<div style={{ display: "flex", alignItems: "center" }}>
|
||
<TextField
|
||
size="small"
|
||
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
||
value={tproxyPort}
|
||
onChange={(e) =>
|
||
setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||
}
|
||
disabled={!tproxyEnabled}
|
||
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => setTproxyPort(generateRandomPort())}
|
||
title={t("settings.modals.clashPort.actions.random")}
|
||
disabled={!tproxyEnabled}
|
||
sx={{ mr: 0.5 }}
|
||
>
|
||
<Shuffle fontSize="small" />
|
||
</IconButton>
|
||
<Switch
|
||
size="small"
|
||
checked={tproxyEnabled}
|
||
onChange={(_, c) => setTproxyEnabled(c)}
|
||
sx={{ ml: 0.5 }}
|
||
/>
|
||
</div>
|
||
</ListItem>
|
||
)}
|
||
</List>
|
||
</BaseDialog>
|
||
);
|
||
});
|