mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-16 23:40:32 +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>
442 lines
12 KiB
TypeScript
442 lines
12 KiB
TypeScript
import {
|
||
DnsOutlined,
|
||
HelpOutlineRounded,
|
||
HistoryEduOutlined,
|
||
RouterOutlined,
|
||
SettingsOutlined,
|
||
SpeedOutlined,
|
||
} from "@mui/icons-material";
|
||
import {
|
||
Box,
|
||
Button,
|
||
Checkbox,
|
||
Dialog,
|
||
DialogActions,
|
||
DialogContent,
|
||
DialogTitle,
|
||
FormControlLabel,
|
||
FormGroup,
|
||
Grid,
|
||
IconButton,
|
||
Skeleton,
|
||
Tooltip,
|
||
} from "@mui/material";
|
||
import { useLockFn } from "ahooks";
|
||
import { Suspense, lazy, useCallback, useMemo, useState } from "react";
|
||
import { useTranslation } from "react-i18next";
|
||
|
||
import { BasePage } from "@/components/base";
|
||
import { ClashModeCard } from "@/components/home/clash-mode-card";
|
||
import { CurrentProxyCard } from "@/components/home/current-proxy-card";
|
||
import { EnhancedCard } from "@/components/home/enhanced-card";
|
||
import { EnhancedTrafficStats } from "@/components/home/enhanced-traffic-stats";
|
||
import { HomeProfileCard } from "@/components/home/home-profile-card";
|
||
import { ProxyTunCard } from "@/components/home/proxy-tun-card";
|
||
import { useProfiles } from "@/hooks/use-profiles";
|
||
import { useVerge } from "@/hooks/use-verge";
|
||
import { entry_lightweight_mode, openWebUrl } from "@/services/cmds";
|
||
|
||
const LazyTestCard = lazy(() =>
|
||
import("@/components/home/test-card").then((module) => ({
|
||
default: module.TestCard,
|
||
})),
|
||
);
|
||
const LazyIpInfoCard = lazy(() =>
|
||
import("@/components/home/ip-info-card").then((module) => ({
|
||
default: module.IpInfoCard,
|
||
})),
|
||
);
|
||
const LazyClashInfoCard = lazy(() =>
|
||
import("@/components/home/clash-info-card").then((module) => ({
|
||
default: module.ClashInfoCard,
|
||
})),
|
||
);
|
||
const LazySystemInfoCard = lazy(() =>
|
||
import("@/components/home/system-info-card").then((module) => ({
|
||
default: module.SystemInfoCard,
|
||
})),
|
||
);
|
||
|
||
// 定义首页卡片设置接口
|
||
interface HomeCardsSettings {
|
||
profile: boolean;
|
||
proxy: boolean;
|
||
network: boolean;
|
||
mode: boolean;
|
||
traffic: boolean;
|
||
info: boolean;
|
||
clashinfo: boolean;
|
||
systeminfo: boolean;
|
||
test: boolean;
|
||
ip: boolean;
|
||
[key: string]: boolean;
|
||
}
|
||
|
||
// 首页设置对话框组件接口
|
||
interface HomeSettingsDialogProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
homeCards: HomeCardsSettings;
|
||
onSave: (cards: HomeCardsSettings) => void;
|
||
}
|
||
|
||
const serializeCardFlags = (cards: HomeCardsSettings) =>
|
||
Object.keys(cards)
|
||
.sort()
|
||
.map((key) => `${key}:${cards[key] ? 1 : 0}`)
|
||
.join("|");
|
||
|
||
// 首页设置对话框组件
|
||
const HomeSettingsDialog = ({
|
||
open,
|
||
onClose,
|
||
homeCards,
|
||
onSave,
|
||
}: HomeSettingsDialogProps) => {
|
||
const { t } = useTranslation();
|
||
const [cards, setCards] = useState<HomeCardsSettings>(homeCards);
|
||
const { patchVerge } = useVerge();
|
||
|
||
const handleToggle = (key: string) => {
|
||
setCards((prev: HomeCardsSettings) => ({
|
||
...prev,
|
||
[key]: !prev[key],
|
||
}));
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
await patchVerge({ home_cards: cards });
|
||
onSave(cards);
|
||
onClose();
|
||
};
|
||
|
||
return (
|
||
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
||
<DialogTitle>{t("home.page.settings.title")}</DialogTitle>
|
||
<DialogContent>
|
||
<FormGroup>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={cards.profile || false}
|
||
onChange={() => handleToggle("profile")}
|
||
/>
|
||
}
|
||
label={t("home.page.settings.cards.profile")}
|
||
/>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={cards.proxy || false}
|
||
onChange={() => handleToggle("proxy")}
|
||
/>
|
||
}
|
||
label={t("home.page.settings.cards.currentProxy")}
|
||
/>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={cards.network || false}
|
||
onChange={() => handleToggle("network")}
|
||
/>
|
||
}
|
||
label={t("home.page.settings.cards.network")}
|
||
/>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={cards.mode || false}
|
||
onChange={() => handleToggle("mode")}
|
||
/>
|
||
}
|
||
label={t("home.page.settings.cards.proxyMode")}
|
||
/>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={cards.traffic || false}
|
||
onChange={() => handleToggle("traffic")}
|
||
/>
|
||
}
|
||
label={t("home.page.settings.cards.traffic")}
|
||
/>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={cards.test || false}
|
||
onChange={() => handleToggle("test")}
|
||
/>
|
||
}
|
||
label={t("home.page.settings.cards.tests")}
|
||
/>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={cards.ip || false}
|
||
onChange={() => handleToggle("ip")}
|
||
/>
|
||
}
|
||
label={t("home.page.settings.cards.ip")}
|
||
/>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={cards.clashinfo || false}
|
||
onChange={() => handleToggle("clashinfo")}
|
||
/>
|
||
}
|
||
label={t("home.page.settings.cards.clashInfo")}
|
||
/>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={cards.systeminfo || false}
|
||
onChange={() => handleToggle("systeminfo")}
|
||
/>
|
||
}
|
||
label={t("home.page.settings.cards.systemInfo")}
|
||
/>
|
||
</FormGroup>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={onClose}>{t("shared.actions.cancel")}</Button>
|
||
<Button onClick={handleSave} color="primary">
|
||
{t("shared.actions.save")}
|
||
</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
const HomePage = () => {
|
||
const { t } = useTranslation();
|
||
const { verge } = useVerge();
|
||
const { current, mutateProfiles } = useProfiles();
|
||
|
||
// 设置弹窗的状态
|
||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||
const [localHomeCards, setLocalHomeCards] = useState<{
|
||
value: HomeCardsSettings;
|
||
baseSignature: string;
|
||
} | null>(null);
|
||
|
||
// 卡片显示状态
|
||
const defaultCards = useMemo<HomeCardsSettings>(
|
||
() => ({
|
||
info: false,
|
||
profile: true,
|
||
proxy: true,
|
||
network: true,
|
||
mode: true,
|
||
traffic: true,
|
||
clashinfo: true,
|
||
systeminfo: true,
|
||
test: true,
|
||
ip: true,
|
||
}),
|
||
[],
|
||
);
|
||
|
||
const vergeHomeCards = useMemo<HomeCardsSettings | null>(
|
||
() => (verge?.home_cards as HomeCardsSettings | undefined) ?? null,
|
||
[verge],
|
||
);
|
||
|
||
const remoteHomeCards = useMemo<HomeCardsSettings>(
|
||
() => vergeHomeCards ?? defaultCards,
|
||
[defaultCards, vergeHomeCards],
|
||
);
|
||
|
||
const remoteSignature = useMemo(
|
||
() => serializeCardFlags(remoteHomeCards),
|
||
[remoteHomeCards],
|
||
);
|
||
|
||
const pendingLocalCards = useMemo<HomeCardsSettings | null>(() => {
|
||
if (!localHomeCards) return null;
|
||
return localHomeCards.baseSignature === remoteSignature
|
||
? localHomeCards.value
|
||
: null;
|
||
}, [localHomeCards, remoteSignature]);
|
||
|
||
const effectiveHomeCards = pendingLocalCards ?? remoteHomeCards;
|
||
|
||
// 文档链接函数
|
||
const toGithubDoc = useLockFn(() => {
|
||
return openWebUrl("https://clash-verge-rev.github.io/index.html");
|
||
});
|
||
|
||
// 新增:打开设置弹窗
|
||
const openSettings = useCallback(() => {
|
||
setSettingsOpen(true);
|
||
}, []);
|
||
|
||
const renderCard = useCallback(
|
||
(cardKey: string, component: React.ReactNode, size: number = 6) => {
|
||
if (!effectiveHomeCards[cardKey]) return null;
|
||
|
||
return (
|
||
<Grid size={size} key={cardKey}>
|
||
{component}
|
||
</Grid>
|
||
);
|
||
},
|
||
[effectiveHomeCards],
|
||
);
|
||
|
||
const criticalCards = useMemo(
|
||
() => [
|
||
renderCard(
|
||
"profile",
|
||
<HomeProfileCard current={current} onProfileUpdated={mutateProfiles} />,
|
||
),
|
||
renderCard("proxy", <CurrentProxyCard />),
|
||
renderCard("network", <NetworkSettingsCard />),
|
||
renderCard("mode", <ClashModeEnhancedCard />),
|
||
],
|
||
[current, mutateProfiles, renderCard],
|
||
);
|
||
|
||
// 新增:保存设置时用requestIdleCallback/setTimeout
|
||
const handleSaveSettings = (newCards: HomeCardsSettings) => {
|
||
if (window.requestIdleCallback) {
|
||
window.requestIdleCallback(() =>
|
||
setLocalHomeCards({
|
||
value: newCards,
|
||
baseSignature: remoteSignature,
|
||
}),
|
||
);
|
||
} else {
|
||
setTimeout(
|
||
() =>
|
||
setLocalHomeCards({
|
||
value: newCards,
|
||
baseSignature: remoteSignature,
|
||
}),
|
||
0,
|
||
);
|
||
}
|
||
};
|
||
|
||
const nonCriticalCards = useMemo(
|
||
() => [
|
||
renderCard(
|
||
"traffic",
|
||
<EnhancedCard
|
||
title={t("home.page.cards.trafficStats")}
|
||
icon={<SpeedOutlined />}
|
||
iconColor="secondary"
|
||
>
|
||
<EnhancedTrafficStats />
|
||
</EnhancedCard>,
|
||
12,
|
||
),
|
||
renderCard(
|
||
"test",
|
||
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
|
||
<LazyTestCard />
|
||
</Suspense>,
|
||
),
|
||
renderCard(
|
||
"ip",
|
||
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
|
||
<LazyIpInfoCard />
|
||
</Suspense>,
|
||
),
|
||
renderCard(
|
||
"clashinfo",
|
||
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
|
||
<LazyClashInfoCard />
|
||
</Suspense>,
|
||
),
|
||
renderCard(
|
||
"systeminfo",
|
||
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
|
||
<LazySystemInfoCard />
|
||
</Suspense>,
|
||
),
|
||
],
|
||
[t, renderCard],
|
||
);
|
||
const dialogKey = useMemo(
|
||
() => `${serializeCardFlags(effectiveHomeCards)}:${settingsOpen ? 1 : 0}`,
|
||
[effectiveHomeCards, settingsOpen],
|
||
);
|
||
return (
|
||
<BasePage
|
||
title={t("home.page.title")}
|
||
contentStyle={{ padding: 2 }}
|
||
header={
|
||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||
<Tooltip title={t("home.page.tooltips.lightweightMode")} arrow>
|
||
<IconButton
|
||
onClick={async () => await entry_lightweight_mode()}
|
||
size="small"
|
||
color="inherit"
|
||
>
|
||
<HistoryEduOutlined />
|
||
</IconButton>
|
||
</Tooltip>
|
||
<Tooltip title={t("home.page.tooltips.manual")} arrow>
|
||
<IconButton onClick={toGithubDoc} size="small" color="inherit">
|
||
<HelpOutlineRounded />
|
||
</IconButton>
|
||
</Tooltip>
|
||
<Tooltip title={t("home.page.tooltips.settings")} arrow>
|
||
<IconButton onClick={openSettings} size="small" color="inherit">
|
||
<SettingsOutlined />
|
||
</IconButton>
|
||
</Tooltip>
|
||
</Box>
|
||
}
|
||
>
|
||
<Grid container spacing={1.5} columns={{ xs: 6, sm: 6, md: 12 }}>
|
||
{criticalCards}
|
||
|
||
{nonCriticalCards}
|
||
</Grid>
|
||
|
||
{/* 首页设置弹窗 */}
|
||
<HomeSettingsDialog
|
||
key={dialogKey}
|
||
open={settingsOpen}
|
||
onClose={() => setSettingsOpen(false)}
|
||
homeCards={effectiveHomeCards}
|
||
onSave={handleSaveSettings}
|
||
/>
|
||
</BasePage>
|
||
);
|
||
};
|
||
|
||
// 增强版网络设置卡片组件
|
||
const NetworkSettingsCard = () => {
|
||
const { t } = useTranslation();
|
||
return (
|
||
<EnhancedCard
|
||
title={t("home.page.cards.networkSettings")}
|
||
icon={<DnsOutlined />}
|
||
iconColor="primary"
|
||
action={null}
|
||
>
|
||
<ProxyTunCard />
|
||
</EnhancedCard>
|
||
);
|
||
};
|
||
|
||
// 增强版 Clash 模式卡片组件
|
||
const ClashModeEnhancedCard = () => {
|
||
const { t } = useTranslation();
|
||
return (
|
||
<EnhancedCard
|
||
title={t("home.page.cards.proxyMode")}
|
||
icon={<RouterOutlined />}
|
||
iconColor="info"
|
||
action={null}
|
||
>
|
||
<ClashModeCard />
|
||
</EnhancedCard>
|
||
);
|
||
};
|
||
|
||
export default HomePage;
|