mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-14 05:51:02 +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>
194 lines
5.1 KiB
TypeScript
194 lines
5.1 KiB
TypeScript
import {
|
||
DirectionsRounded,
|
||
LanguageRounded,
|
||
MultipleStopRounded,
|
||
} from "@mui/icons-material";
|
||
import { Box, Paper, Stack, Typography } from "@mui/material";
|
||
import { useLockFn } from "ahooks";
|
||
import { useMemo } from "react";
|
||
import { useTranslation } from "react-i18next";
|
||
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();
|
||
const { verge } = useVerge();
|
||
const { clashConfig, refreshClashConfig } = useAppData();
|
||
|
||
// 支持的模式列表
|
||
const modeList = CLASH_MODES;
|
||
|
||
// 直接使用API返回的模式,不维护本地状态
|
||
const currentMode = clashConfig?.mode?.toLowerCase();
|
||
const currentModeKey =
|
||
typeof currentMode === "string" && isClashMode(currentMode)
|
||
? currentMode
|
||
: undefined;
|
||
|
||
const modeDescription = useMemo(() => {
|
||
if (currentModeKey) {
|
||
return t(MODE_META[currentModeKey].description);
|
||
}
|
||
return t("home.components.clashMode.errors.communication");
|
||
}, [currentModeKey, t]);
|
||
|
||
// 模式图标映射
|
||
const modeIcons = useMemo(
|
||
() => ({
|
||
rule: <MultipleStopRounded fontSize="small" />,
|
||
global: <LanguageRounded fontSize="small" />,
|
||
direct: <DirectionsRounded fontSize="small" />,
|
||
}),
|
||
[],
|
||
);
|
||
|
||
// 切换模式的处理函数
|
||
const onChangeMode = useLockFn(async (mode: ClashMode) => {
|
||
if (mode === currentModeKey) return;
|
||
if (verge?.auto_close_connection) {
|
||
closeAllConnections();
|
||
}
|
||
|
||
try {
|
||
await patchClashMode(mode);
|
||
// 使用共享的刷新方法
|
||
refreshClashConfig();
|
||
} catch (error) {
|
||
console.error("Failed to change mode:", error);
|
||
}
|
||
});
|
||
|
||
// 按钮样式
|
||
const buttonStyles = (mode: ClashMode) => ({
|
||
cursor: "pointer",
|
||
px: 2,
|
||
py: 1.2,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
gap: 1,
|
||
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",
|
||
overflow: "visible",
|
||
"&:hover": {
|
||
transform: "translateY(-1px)",
|
||
boxShadow: 1,
|
||
},
|
||
"&:active": {
|
||
transform: "translateY(1px)",
|
||
},
|
||
"&::after":
|
||
mode === currentModeKey
|
||
? {
|
||
content: '""',
|
||
position: "absolute",
|
||
bottom: -16,
|
||
left: "50%",
|
||
width: 2,
|
||
height: 16,
|
||
bgcolor: "primary.main",
|
||
transform: "translateX(-50%)",
|
||
}
|
||
: {},
|
||
});
|
||
|
||
// 描述样式
|
||
const descriptionStyles = {
|
||
width: "95%",
|
||
textAlign: "center",
|
||
color: "text.secondary",
|
||
p: 0.8,
|
||
borderRadius: 1,
|
||
borderColor: "primary.main",
|
||
borderWidth: 1,
|
||
borderStyle: "solid",
|
||
backgroundColor: "background.paper",
|
||
wordBreak: "break-word",
|
||
hyphens: "auto",
|
||
};
|
||
|
||
return (
|
||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||
{/* 模式选择按钮组 */}
|
||
<Stack
|
||
direction="row"
|
||
spacing={1}
|
||
sx={{
|
||
display: "flex",
|
||
justifyContent: "center",
|
||
py: 1,
|
||
position: "relative",
|
||
zIndex: 2,
|
||
}}
|
||
>
|
||
{modeList.map((mode) => (
|
||
<Paper
|
||
key={mode}
|
||
elevation={mode === currentModeKey ? 2 : 0}
|
||
onClick={() => onChangeMode(mode)}
|
||
sx={buttonStyles(mode)}
|
||
>
|
||
{modeIcons[mode]}
|
||
<Typography
|
||
variant="body2"
|
||
sx={{
|
||
textTransform: "capitalize",
|
||
fontWeight: mode === currentModeKey ? 600 : 400,
|
||
}}
|
||
>
|
||
{t(MODE_META[mode].label)}
|
||
</Typography>
|
||
</Paper>
|
||
))}
|
||
</Stack>
|
||
|
||
{/* 说明文本区域 */}
|
||
<Box
|
||
sx={{
|
||
width: "100%",
|
||
my: 1,
|
||
position: "relative",
|
||
display: "flex",
|
||
justifyContent: "center",
|
||
overflow: "visible",
|
||
}}
|
||
>
|
||
<Typography variant="caption" component="div" sx={descriptionStyles}>
|
||
{modeDescription}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
};
|