New Interface (initial commit)

This commit is contained in:
coolcoala
2025-07-04 02:28:27 +03:00
parent 4435a5aee4
commit 686490ded1
121 changed files with 12852 additions and 13274 deletions

View File

@@ -1,71 +1,43 @@
import {
alpha,
ListItem,
ListItemButton,
ListItemText,
ListItemIcon,
} from "@mui/material";
import { useMatch, useResolvedPath, useNavigate } from "react-router-dom";
import { Link, useMatch, useResolvedPath } from "react-router-dom";
import { useVerge } from "@/hooks/use-verge";
import { cn } from "@root/lib/utils";
interface Props {
to: string;
children: string;
icon: React.ReactNode[];
}
export const LayoutItem = (props: Props) => {
const { to, children, icon } = props;
const { verge } = useVerge();
const { menu_icon } = verge ?? {};
const resolved = useResolvedPath(to);
const match = useMatch({ path: resolved.pathname, end: true });
const navigate = useNavigate();
return (
<ListItem sx={{ py: 0.5, maxWidth: 250, mx: "auto", padding: "4px 0px" }}>
<ListItemButton
selected={!!match}
sx={[
{
borderRadius: 2,
marginLeft: 1.25,
paddingLeft: 1,
paddingRight: 1,
marginRight: 1.25,
"& .MuiListItemText-primary": {
color: "text.primary",
fontWeight: "700",
},
},
({ palette: { mode, primary } }) => {
const bgcolor =
mode === "light"
? alpha(primary.main, 0.15)
: alpha(primary.main, 0.35);
const color = mode === "light" ? "#1f1f1f" : "#ffffff";
return {
"&.Mui-selected": { bgcolor },
"&.Mui-selected:hover": { bgcolor },
"&.Mui-selected .MuiListItemText-primary": { color },
};
},
]}
onClick={() => navigate(to)}
>
{(menu_icon === "monochrome" || !menu_icon) && (
<ListItemIcon sx={{ color: "text.primary", marginLeft: "6px" }}>
{icon[0]}
</ListItemIcon>
<Link
to={to}
className={cn(
"flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
match
? "bg-primary text-primary-foreground shadow-md"
: "hover:bg-muted/50",
"mx-auto my-1 w-[calc(100%-10px)]",
)}
>
{(menu_icon === "monochrome" || !menu_icon) && (
<span className="mr-2 text-foreground">{icon[0]}</span>
)}
{menu_icon === "colorful" && <span className="mr-2">{icon[1]}</span>}
<span
className={cn(
"text-center",
menu_icon === "disable" ? "" : "ml-[-35px]",
)}
{menu_icon === "colorful" && <ListItemIcon>{icon[1]}</ListItemIcon>}
<ListItemText
sx={{
textAlign: "center",
marginLeft: menu_icon === "disable" ? "" : "-35px",
}}
primary={children}
/>
</ListItemButton>
</ListItem>
>
{children}
</span>
</Link>
);
};

View File

@@ -1,10 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { Box, Typography } from "@mui/material";
import {
ArrowDownwardRounded,
ArrowUpwardRounded,
MemoryRounded,
} from "@mui/icons-material";
import { ArrowDown, ArrowUp, Database } from "lucide-react";
import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
@@ -14,6 +9,7 @@ import useSWRSubscription from "swr/subscription";
import { createAuthSockette } from "@/utils/websocket";
import { useTranslation } from "react-i18next";
import { isDebugEnabled, gc } from "@/services/api";
import { cn } from "@root/lib/utils";
interface MemoryUsage {
inuse: number;
@@ -149,77 +145,77 @@ export const LayoutTraffic = () => {
const [down, downUnit] = parseTraffic(traffic.down);
const [inuse, inuseUnit] = parseTraffic(memory.inuse);
const boxStyle: any = {
display: "flex",
alignItems: "center",
whiteSpace: "nowrap",
};
const iconStyle: any = {
sx: { mr: "8px", fontSize: 16 },
};
const valStyle: any = {
component: "span",
textAlign: "center",
sx: { flex: "1 1 56px", userSelect: "none" },
};
const unitStyle: any = {
component: "span",
color: "grey.500",
fontSize: "12px",
textAlign: "right",
sx: { flex: "0 1 27px", userSelect: "none" },
};
return (
<Box position="relative">
<div className="relative">
{trafficGraph && pageVisible && (
<div
style={{ width: "100%", height: 60, marginBottom: 6 }}
className="mb-1.5 h-[60px] w-full"
onClick={trafficRef.current?.toggleStyle}
>
<TrafficGraph ref={trafficRef} />
</div>
)}
<Box display="flex" flexDirection="column" gap={0.75}>
<Box title={t("Upload Speed")} {...boxStyle}>
<ArrowUpwardRounded
{...iconStyle}
color={+up > 0 ? "secondary" : "disabled"}
<div className="flex flex-col gap-0.5">
<div
title={t("Upload Speed")}
className="flex items-center whitespace-nowrap"
>
<ArrowUp
className={cn(
"mr-2 h-4 w-4",
+up > 0 ? "text-secondary" : "text-muted-foreground",
)}
/>
<Typography {...valStyle} color="secondary">
<span className="w-[56px] flex-1 select-none text-center text-secondary">
{up}
</Typography>
<Typography {...unitStyle}>{upUnit}/s</Typography>
</Box>
</span>
<span className="w-[27px] flex-none select-none text-right text-xs text-muted-foreground">
{upUnit}/s
</span>
</div>
<Box title={t("Download Speed")} {...boxStyle}>
<ArrowDownwardRounded
{...iconStyle}
color={+down > 0 ? "primary" : "disabled"}
<div
title={t("Download Speed")}
className="flex items-center whitespace-nowrap"
>
<ArrowDown
className={cn(
"mr-2 h-4 w-4",
+down > 0 ? "text-primary" : "text-muted-foreground",
)}
/>
<Typography {...valStyle} color="primary">
<span className="w-[56px] flex-1 select-none text-center text-primary">
{down}
</Typography>
<Typography {...unitStyle}>{downUnit}/s</Typography>
</Box>
</span>
<span className="w-[27px] flex-none select-none text-right text-xs text-muted-foreground">
{downUnit}/s
</span>
</div>
{displayMemory && (
<Box
<div
title={t(isDebug ? "Memory Cleanup" : "Memory Usage")}
{...boxStyle}
sx={{ cursor: isDebug ? "pointer" : "auto" }}
color={isDebug ? "success.main" : "disabled"}
className={cn(
"flex items-center whitespace-nowrap",
isDebug
? "cursor-pointer text-green-500"
: "text-muted-foreground",
)}
onClick={async () => {
isDebug && (await gc());
}}
>
<MemoryRounded {...iconStyle} />
<Typography {...valStyle}>{inuse}</Typography>
<Typography {...unitStyle}>{inuseUnit}</Typography>
</Box>
<Database className="mr-2 h-4 w-4" />
<span className="w-[56px] flex-1 select-none text-center">
{inuse}
</span>
<span className="w-[27px] flex-none select-none text-right text-xs">
{inuseUnit}
</span>
</div>
)}
</Box>
</Box>
</div>
</div>
);
};

View File

@@ -1,37 +1,26 @@
import { IconButton, Fade, SxProps, Theme } from "@mui/material";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import { ArrowUp } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@root/lib/utils";
interface Props {
onClick: () => void;
show: boolean;
sx?: SxProps<Theme>;
className?: string;
}
export const ScrollTopButton = ({ onClick, show, sx }: Props) => {
export const ScrollTopButton = ({ onClick, show, className }: Props) => {
return (
<Fade in={show}>
<IconButton
onClick={onClick}
sx={{
position: "absolute",
bottom: "20px",
right: "20px",
backgroundColor: (theme) =>
theme.palette.mode === "dark"
? "rgba(255,255,255,0.1)"
: "rgba(0,0,0,0.1)",
"&:hover": {
backgroundColor: (theme) =>
theme.palette.mode === "dark"
? "rgba(255,255,255,0.2)"
: "rgba(0,0,0,0.2)",
},
visibility: show ? "visible" : "hidden",
...sx,
}}
>
<KeyboardArrowUpIcon />
</IconButton>
</Fade>
<Button
variant="outline"
size="icon"
onClick={onClick}
className={cn(
"absolute bottom-5 right-5 h-10 w-10 rounded-full bg-background/50 backdrop-blur-sm transition-opacity hover:bg-background/75",
show ? "opacity-100" : "opacity-0 pointer-events-none",
className,
)}
>
<ArrowUp className="h-5 w-5" />
</Button>
);
};

View File

@@ -1,5 +1,5 @@
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
import { useTheme } from "@mui/material";
import { useThemeMode } from "@/services/states";
const maxPoint = 30;
@@ -32,7 +32,7 @@ export const TrafficGraph = forwardRef<TrafficRef>((props, ref) => {
const cacheRef = useRef<TrafficData | null>(null);
const { palette } = useTheme();
const mode = useThemeMode();
useImperativeHandle(ref, () => ({
appendData: (data: TrafficData) => {
@@ -76,10 +76,14 @@ export const TrafficGraph = forwardRef<TrafficRef>((props, ref) => {
if (!context) return;
const { primary, secondary, divider } = palette;
const refLineColor = divider || "rgba(0, 0, 0, 0.12)";
const upLineColor = secondary.main || "#9c27b0";
const downLineColor = primary.main || "#5b5c9d";
const computedStyle = getComputedStyle(document.documentElement);
const refLineColor =
`hsl(${computedStyle.getPropertyValue("--border")})` ||
"rgba(0, 0, 0, 0.12)";
const upLineColor =
`hsl(${computedStyle.getPropertyValue("--secondary")})` || "#9c27b0";
const downLineColor =
`hsl(${computedStyle.getPropertyValue("--primary")})` || "#5b5c9d";
const width = canvas.width;
const height = canvas.height;
@@ -193,7 +197,7 @@ export const TrafficGraph = forwardRef<TrafficRef>((props, ref) => {
return () => {
cancelAnimationFrame(raf);
};
}, [palette]);
}, [mode]);
return <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />;
});

View File

@@ -1,10 +1,10 @@
import useSWR from "swr";
import { useRef } from "react";
import { Button } from "@mui/material";
import { check } from "@tauri-apps/plugin-updater";
import { UpdateViewer } from "../setting/mods/update-viewer";
import { DialogRef } from "../base";
import { useVerge } from "@/hooks/use-verge";
import { Button } from "@/components/ui/button";
interface Props {
className?: string;
@@ -34,9 +34,8 @@ export const UpdateButton = (props: Props) => {
<UpdateViewer ref={viewerRef} />
<Button
color="error"
variant="contained"
size="small"
variant="destructive"
size="sm"
className={className}
onClick={() => viewerRef.current?.open()}
>

View File

@@ -1,21 +1,21 @@
import { useVerge } from "@/hooks/use-verge";
import { defaultDarkTheme, defaultTheme } from "@/pages/_theme";
import { useSetThemeMode, useThemeMode } from "@/services/states";
import { alpha, createTheme, Theme as MuiTheme, Shadows } from "@mui/material";
import {
arSD as arXDataGrid,
enUS as enXDataGrid,
faIR as faXDataGrid,
ruRU as ruXDataGrid,
zhCN as zhXDataGrid,
} from "@mui/x-data-grid/locales";
import { useEffect, useMemo } from "react";
import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material";
import {
getCurrentWebviewWindow,
WebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { Theme as TauriOsTheme } from "@tauri-apps/api/window";
import { useEffect, useMemo } from "react";
import { useSetThemeMode, useThemeMode } from "@/services/states";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { useVerge } from "@/hooks/use-verge";
import {
zhCN as zhXDataGrid,
enUS as enXDataGrid,
ruRU as ruXDataGrid,
faIR as faXDataGrid,
arSD as arXDataGrid,
} from "@mui/x-data-grid/locales";
import { useTranslation } from "react-i18next";
import { Theme as TauriOsTheme } from "@tauri-apps/api/window";
const languagePackMap: Record<string, any> = {
zh: { ...zhXDataGrid },
@@ -39,10 +39,6 @@ export const useCustomTheme = () => {
const mode = useThemeMode();
const setMode = useSetThemeMode();
// 提取用户自定义的背景图URL
const userBackgroundImage = theme_setting?.background_image || "";
const hasUserBackground = !!userBackgroundImage;
useEffect(() => {
if (theme_mode === "light" || theme_mode === "dark") {
setMode(theme_mode);
@@ -56,16 +52,19 @@ export const useCustomTheme = () => {
let isMounted = true;
appWindow
.theme()
.then((systemTheme) => {
if (isMounted && systemTheme) {
setMode(systemTheme);
}
})
.catch((err) => {
console.error("Failed to get initial system theme:", err);
});
const timerId = setTimeout(() => {
if (!isMounted) return;
appWindow
.theme()
.then((systemTheme) => {
if (isMounted && systemTheme) {
setMode(systemTheme);
}
})
.catch((err) => {
console.error("Failed to get initial system theme:", err);
});
}, 0);
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
if (isMounted) {
@@ -75,6 +74,7 @@ export const useCustomTheme = () => {
return () => {
isMounted = false;
clearTimeout(timerId);
unlistenPromise
.then((unlistenFn) => {
if (typeof unlistenFn === "function") {
@@ -131,7 +131,6 @@ export const useCustomTheme = () => {
},
background: {
paper: dt.background_color,
default: dt.background_color,
},
},
shadows: Array(25).fill("none") as Shadows,
@@ -158,10 +157,6 @@ export const useCustomTheme = () => {
warning: { main: dt.warning_color },
success: { main: dt.success_color },
text: { primary: dt.primary_text, secondary: dt.secondary_text },
background: {
paper: dt.background_color,
default: dt.background_color,
},
},
typography: { fontFamily: dt.font_family },
});
@@ -169,10 +164,9 @@ export const useCustomTheme = () => {
const rootEle = document.documentElement;
if (rootEle) {
const backgroundColor =
mode === "light" ? "#ECECEC" : dt.background_color;
const selectColor = mode === "light" ? "#f5f5f5" : "#3E3E3E";
const scrollColor = mode === "light" ? "#90939980" : "#555555";
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
const dividerColor =
mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)";
@@ -188,96 +182,16 @@ export const useCustomTheme = () => {
"--background-color-alpha",
alpha(muiTheme.palette.primary.main, 0.1),
);
// 添加CSS变量
rootEle.style.setProperty(
"--window-border-color",
mode === "light" ? "#cccccc" : "#1E1E1E",
);
rootEle.style.setProperty(
"--scrollbar-bg",
mode === "light" ? "#f1f1f1" : "#2E303D",
);
rootEle.style.setProperty(
"--scrollbar-thumb",
mode === "light" ? "#c1c1c1" : "#555555",
);
// 设置背景图相关变量
rootEle.style.setProperty(
"--user-background-image",
hasUserBackground ? `url('${userBackgroundImage}')` : "none",
);
rootEle.style.setProperty(
"--background-blend-mode",
setting.background_blend_mode || "normal",
);
rootEle.style.setProperty(
"--background-opacity",
setting.background_opacity !== undefined
? String(setting.background_opacity)
: "1",
);
}
// inject css
let styleElement = document.querySelector("style#verge-theme");
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = "verge-theme";
document.head.appendChild(styleElement!);
}
if (styleElement) {
// 改进的全局样式,支持用户自定义背景图
const globalStyles = `
/* 修复滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: var(--scrollbar-bg);
}
::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: ${mode === "light" ? "#a1a1a1" : "#666666"};
}
/* 背景图处理 */
body {
background-color: var(--background-color);
${
hasUserBackground
? `
background-image: var(--user-background-image);
background-size: cover;
background-position: center;
background-attachment: fixed;
background-blend-mode: var(--background-blend-mode);
opacity: var(--background-opacity);
`
: ""
}
}
/* 修复可能的白色边框 */
.MuiPaper-root {
border-color: var(--window-border-color) !important;
}
/* 确保模态框和对话框也使用暗色主题 */
.MuiDialog-paper {
background-color: ${mode === "light" ? "#ffffff" : "#2E303D"} !important;
}
/* 移除可能的白色点或线条 */
* {
outline: none !important;
box-shadow: none !important;
}
`;
styleElement.innerHTML = (setting.css_injection || "") + globalStyles;
styleElement.innerHTML = setting.css_injection || "";
}
const { palette } = muiTheme;
@@ -293,13 +207,7 @@ export const useCustomTheme = () => {
}, 0);
return muiTheme;
}, [
mode,
theme_setting,
i18n.language,
userBackgroundImage,
hasUserBackground,
]);
}, [mode, theme_setting, i18n.language]);
return { theme };
};