feat: Implement custom window controls and titlebar management (#4919)

- Added WindowControls component for managing window actions (minimize, maximize, close) based on the operating system.
- Integrated window decoration toggle functionality to allow users to prefer system titlebar.
- Updated layout styles to accommodate new titlebar and window controls.
- Refactored layout components to utilize new window management hooks.
- Enhanced layout viewer to include a switch for enabling/disabling window decorations.
- Improved overall window management by introducing useWindow and useWindowDecorations hooks for better state handling.
This commit is contained in:
Tunglies
2025-10-08 20:23:26 +08:00
committed by GitHub
parent f195b3bccf
commit bfd1274a8c
10 changed files with 449 additions and 169 deletions

View File

@@ -6,6 +6,7 @@
- Linux 打包为 `.deb` `.rpm` 提供 pkexec 依赖项 - Linux 打包为 `.deb` `.rpm` 提供 pkexec 依赖项
- 支持前端修改日志(最大文件大小、最大保留数量) - 支持前端修改日志(最大文件大小、最大保留数量)
- 新增链式代理图形化设置功能 - 新增链式代理图形化设置功能
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
- 监听关机事件,自动关闭系统代理 - 监听关机事件,自动关闭系统代理
### 🚀 优化改进 ### 🚀 优化改进

View File

@@ -27,7 +27,8 @@ pub fn build_new_window() -> Result<WebviewWindow, String> {
) )
.title("Clash Verge") .title("Clash Verge")
.center() .center()
.decorations(true) // Using WindowManager::prefer_system_titlebar to control if show system built-in titlebar
// .decorations(true)
.fullscreen(false) .fullscreen(false)
.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT) .inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
.min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT) .min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT)

View File

@@ -2,118 +2,103 @@
width: 100%; width: 100%;
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column;
overflow: hidden; overflow: hidden;
&__left { .layout-content {
flex: 1 0 200px; /* New container for the flex layout */
display: flex; display: flex;
height: 100%; flex: 1; /* Take remaining height */
width: 100%;
// max-width: 225px;
// min-width: 225px;
// padding: 16px 0 8px;
padding: 0px 0px 8px;
// position: relative;
flex-direction: column;
align-self: stretch;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
overflow: hidden; overflow: hidden;
border-right: 1px solid var(--divider-color);
// background-color: var(--background-color-alpha);
// $maxLogo: 100px; &__left {
flex: 1 0 200px;
.the-logo {
position: relative;
flex: 1 0 58px;
// width: 100%;
display: flex; display: flex;
height: 100%; height: 100%;
padding: 0px 20px; width: 100%;
padding: 0 0 8px;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: flex-start;
align-self: stretch; align-self: stretch;
// border-bottom: 1px solid var(--divider-color);
// max-width: $maxLogo + 32px;
// max-height: $maxLogo;
// margin: 0 auto;
// padding: 0 auto;
// text-align: center;
box-sizing: border-box; box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
overflow: hidden;
border-right: 1px solid var(--divider-color);
img, .the-logo {
svg { position: relative;
width: 100%; flex: 1 0 58px;
display: flex;
height: 100%; height: 100%;
pointer-events: none; padding: 0 20px;
// fill: var(--primary-main); flex-direction: column;
justify-content: center;
align-items: flex-start;
align-self: stretch;
box-sizing: border-box;
// #bg { img,
// fill: var(--background-color); svg {
// } width: 100%;
height: 100%;
pointer-events: none;
}
.the-newbtn {
position: absolute;
right: 10px;
top: 15px;
border-radius: 8px;
padding: 2px 4px;
transform: scale(0.8);
}
} }
.the-newbtn { .the-menu {
flex: 1 1 80%;
overflow-y: auto;
margin-bottom: 0px;
padding-top: 4px;
}
.the-traffic {
flex: 0 0 60px;
> div {
margin: 0 auto;
padding: 0 20px;
}
}
}
&__right {
position: relative;
flex: 1 1 100%;
height: 100%;
.the-bar {
height: 36px;
display: flex;
justify-content: end;
box-sizing: border-box;
z-index: 2;
.the-dragbar {
margin-top: 5px;
app-region: drag;
}
}
.the-content {
position: absolute; position: absolute;
right: 10px; top: 0;
top: 15px; left: 0;
border-radius: 8px; right: 1px;
padding: 2px 4px; bottom: 0px;
transform: scale(0.8);
} }
} }
.the-menu {
flex: 1 1 80%;
overflow-y: auto;
margin-bottom: 0px;
padding-top: 4px;
}
.the-traffic {
flex: 0 0 60px;
> div {
margin: 0 auto;
padding: 0px 20px;
}
}
}
&__right {
position: relative;
flex: 1 1 100%;
height: 100%;
// background-color: var(--background-color-alpha);
.the-bar {
// position: absolute;
// top: 0px;
// right: 0px;
height: 36px;
display: flex;
// align-items: center;
justify-content: end;
box-sizing: border-box;
z-index: 2;
.the-dragbar {
margin-top: 5px;
app-region: drag;
}
}
.the-content {
position: absolute;
top: 0;
left: 0;
right: 1px;
bottom: 0px;
}
} }
} }
@@ -121,11 +106,17 @@
.windows, .windows,
.unknown { .unknown {
&.layout { &.layout {
//.layout__left { .the_titlebar {
// padding-top: 24px; width: 100%;
//} display: flex;
justify-content: flex-end;
padding: 10px;
box-sizing: border-box;
height: 36px;
border-bottom: 1px solid var(--divider-color);
}
.layout__left .the-logo { .layout-content__left .the-logo {
flex: 1 0 58px; flex: 1 0 58px;
margin-top: 10px; margin-top: 10px;
margin-left: 10px; margin-left: 10px;
@@ -135,7 +126,7 @@
padding-bottom: 16px; padding-bottom: 16px;
} }
.layout__right .the-content { .layout-content__right .the-content {
top: 5px; top: 5px;
} }
} }
@@ -143,15 +134,25 @@
.macos { .macos {
&.layout { &.layout {
.layout__left { .the_titlebar {
width: 100%;
display: flex;
justify-content: flex-start;
padding: 10px;
box-sizing: border-box;
height: 36px;
border-bottom: 1px solid var(--divider-color);
}
.layout-content__left {
padding-top: 5px; padding-top: 5px;
} }
.layout__right .the-content { .layout-content__right .the-content {
top: 5px; top: 5px;
} }
.layout__left .the-newbtn { .layout-content__left .the-newbtn {
right: 9px; right: 9px;
top: 2px; top: 2px;
} }

View File

@@ -0,0 +1,114 @@
import { Close, CropSquare, FilterNone, Minimize } from "@mui/icons-material";
import { IconButton } from "@mui/material";
import { forwardRef, useImperativeHandle } from "react";
import { useWindowControls } from "@/hooks/use-window";
import getSystem from "@/utils/get-system";
export const WindowControls = forwardRef(function WindowControls(props, ref) {
const OS = getSystem();
const {
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
} = useWindowControls();
useImperativeHandle(
ref,
() => ({
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
}),
[
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
],
);
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准
return (
<div style={{ display: "flex", gap: 4 }}>
{OS === "macos" && (
<>
{/* macOS 风格:关闭 → 最小化 → 全屏 */}
<IconButton size="small" sx={{ fontSize: 14 }} onClick={close}>
<Close fontSize="inherit" color="inherit" />
</IconButton>
<IconButton size="small" sx={{ fontSize: 14 }} onClick={minimize}>
<Minimize fontSize="inherit" color="inherit" />
</IconButton>
<IconButton
size="small"
sx={{ fontSize: 14 }}
onClick={toggleMaximize}
>
{maximized ? (
<FilterNone fontSize="inherit" color="inherit" />
) : (
<CropSquare fontSize="inherit" color="inherit" />
)}
</IconButton>
</>
)}
{OS === "windows" && (
<>
{/* Windows 风格:最小化 → 最大化 → 关闭 */}
<IconButton size="small" sx={{ fontSize: 14 }} onClick={minimize}>
<Minimize fontSize="small" color="inherit" />
</IconButton>
<IconButton
size="small"
sx={{ fontSize: 14 }}
onClick={toggleMaximize}
>
{maximized ? (
<FilterNone fontSize="small" color="inherit" />
) : (
<CropSquare fontSize="small" color="inherit" />
)}
</IconButton>
<IconButton size="small" sx={{ fontSize: 14 }} onClick={close}>
<Close fontSize="small" color="inherit" />
</IconButton>
</>
)}
{OS === "linux" && (
<>
{/* Linux 桌面常见布局GNOME/KDE 多为:最小化 → 最大化 → 关闭) */}
<IconButton size="small" sx={{ fontSize: 14 }} onClick={minimize}>
<Minimize fontSize="small" color="inherit" />
</IconButton>
<IconButton
size="small"
sx={{ fontSize: 14 }}
onClick={toggleMaximize}
>
{maximized ? (
<FilterNone fontSize="small" color="inherit" />
) : (
<CropSquare fontSize="small" color="inherit" />
)}
</IconButton>
<IconButton size="small" sx={{ fontSize: 14 }} onClick={close}>
<Close fontSize="small" color="inherit" />
</IconButton>
</>
)}
</div>
);
});

View File

@@ -1,12 +1,12 @@
import { import {
List, Box,
Button, Button,
Select, List,
MenuItem,
styled,
ListItem, ListItem,
ListItemText, ListItemText,
Box, MenuItem,
Select,
styled,
} from "@mui/material"; } from "@mui/material";
import { convertFileSrc } from "@tauri-apps/api/core"; import { convertFileSrc } from "@tauri-apps/api/core";
import { join } from "@tauri-apps/api/path"; import { join } from "@tauri-apps/api/path";
@@ -18,10 +18,10 @@ import { useTranslation } from "react-i18next";
import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { BaseDialog, DialogRef, Switch } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useWindowDecorations } from "@/hooks/use-window";
import { copyIconFile, getAppDir } from "@/services/cmds"; import { copyIconFile, getAppDir } from "@/services/cmds";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { GuardState } from "./guard-state"; import { GuardState } from "./guard-state";
const OS = getSystem(); const OS = getSystem();
@@ -47,6 +47,8 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const [sysproxyIcon, setSysproxyIcon] = useState(""); const [sysproxyIcon, setSysproxyIcon] = useState("");
const [tunIcon, setTunIcon] = useState(""); const [tunIcon, setTunIcon] = useState("");
const { decorated, toggleDecorations } = useWindowDecorations();
useEffect(() => { useEffect(() => {
initIconPath(); initIconPath();
}, []); }, []);
@@ -108,6 +110,21 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
> >
<List> <List>
<Item>
<ListItemText primary={t("Prefer System Titlebar")} />
<GuardState
value={decorated}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={async (e) => {
await toggleDecorations();
}}
>
<Switch edge="end" />
</GuardState>
</Item>
<Item> <Item>
<ListItemText primary={t("Traffic Graph")} /> <ListItemText primary={t("Traffic Graph")} />
<GuardState <GuardState

View File

@@ -8,11 +8,11 @@ import { DialogRef } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { import {
exitApp, exitApp,
exportDiagnosticInfo,
openAppDir, openAppDir,
openCoreDir, openCoreDir,
openLogsDir,
openDevTools, openDevTools,
exportDiagnosticInfo, openLogsDir,
} from "@/services/cmds"; } from "@/services/cmds";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
import { version } from "@root/package.json"; import { version } from "@root/package.json";
@@ -23,7 +23,7 @@ import { HotkeyViewer } from "./mods/hotkey-viewer";
import { LayoutViewer } from "./mods/layout-viewer"; import { LayoutViewer } from "./mods/layout-viewer";
import { LiteModeViewer } from "./mods/lite-mode-viewer"; import { LiteModeViewer } from "./mods/lite-mode-viewer";
import { MiscViewer } from "./mods/misc-viewer"; import { MiscViewer } from "./mods/misc-viewer";
import { SettingList, SettingItem } from "./mods/setting-comp"; import { SettingItem, SettingList } from "./mods/setting-comp";
import { ThemeViewer } from "./mods/theme-viewer"; import { ThemeViewer } from "./mods/theme-viewer";
import { UpdateViewer } from "./mods/update-viewer"; import { UpdateViewer } from "./mods/update-viewer";

View File

@@ -1,5 +1,5 @@
import { ContentCopyRounded } from "@mui/icons-material"; import { ContentCopyRounded } from "@mui/icons-material";
import { Button, MenuItem, Select, Input } from "@mui/material"; import { Button, Input, MenuItem, Select } from "@mui/material";
import { open } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { useCallback, useRef } from "react"; import { useCallback, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -19,7 +19,7 @@ import { GuardState } from "./mods/guard-state";
import { HotkeyViewer } from "./mods/hotkey-viewer"; import { HotkeyViewer } from "./mods/hotkey-viewer";
import { LayoutViewer } from "./mods/layout-viewer"; import { LayoutViewer } from "./mods/layout-viewer";
import { MiscViewer } from "./mods/misc-viewer"; import { MiscViewer } from "./mods/misc-viewer";
import { SettingList, SettingItem } from "./mods/setting-comp"; import { SettingItem, SettingList } from "./mods/setting-comp";
import { ThemeModeSwitch } from "./mods/theme-mode-switch"; import { ThemeModeSwitch } from "./mods/theme-mode-switch";
import { ThemeViewer } from "./mods/theme-viewer"; import { ThemeViewer } from "./mods/theme-viewer";
import { UpdateViewer } from "./mods/update-viewer"; import { UpdateViewer } from "./mods/update-viewer";

114
src/hooks/use-window.tsx Normal file
View File

@@ -0,0 +1,114 @@
import { getCurrentWindow } from "@tauri-apps/api/window";
import React, {
createContext,
useCallback,
use,
useEffect,
useState,
} from "react";
interface WindowContextType {
decorated: boolean | null;
maximized: boolean | null;
toggleDecorations: () => Promise<void>;
refreshDecorated: () => Promise<boolean>;
minimize: () => void;
close: () => void;
toggleMaximize: () => Promise<void>;
toggleFullscreen: () => Promise<void>;
currentWindow: ReturnType<typeof getCurrentWindow>;
}
const WindowContext = createContext<WindowContextType | undefined>(undefined);
export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const currentWindow = getCurrentWindow();
const [decorated, setDecorated] = useState<boolean | null>(null);
const [maximized, setMaximized] = useState<boolean | null>(null);
const close = useCallback(() => currentWindow.close(), [currentWindow]);
const minimize = useCallback(() => currentWindow.minimize(), [currentWindow]);
const toggleMaximize = useCallback(async () => {
if (await currentWindow.isMaximized()) {
await currentWindow.unmaximize();
setMaximized(false);
} else {
await currentWindow.maximize();
setMaximized(true);
}
}, [currentWindow]);
const toggleFullscreen = useCallback(async () => {
await currentWindow.setFullscreen(!(await currentWindow.isFullscreen()));
}, [currentWindow]);
const refreshDecorated = useCallback(async () => {
const val = await currentWindow.isDecorated();
setDecorated(val);
return val;
}, [currentWindow]);
const toggleDecorations = useCallback(async () => {
const currentVal = await currentWindow.isDecorated();
await currentWindow.setDecorations(!currentVal);
setDecorated(!currentVal);
}, [currentWindow]);
useEffect(() => {
refreshDecorated();
currentWindow.setMinimizable?.(true);
}, [currentWindow, refreshDecorated]);
return (
<WindowContext
value={{
decorated,
maximized,
toggleDecorations,
refreshDecorated,
minimize,
close,
toggleMaximize,
toggleFullscreen,
currentWindow,
}}
>
{children}
</WindowContext>
);
};
export const useWindow = () => {
const context = use(WindowContext);
if (context === undefined) {
throw new Error("useWindow must be used within WindowProvider");
}
return context;
};
export const useWindowControls = () => {
const {
maximized,
minimize,
toggleMaximize,
close,
toggleFullscreen,
currentWindow,
} = useWindow();
return {
maximized,
minimize,
toggleMaximize,
close,
toggleFullscreen,
currentWindow,
};
};
export const useWindowDecorations = () => {
const { decorated, toggleDecorations, refreshDecorated } = useWindow();
return { decorated, toggleDecorations, refreshDecorated };
};

View File

@@ -10,6 +10,7 @@ import { BrowserRouter } from "react-router-dom";
import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
import { BaseErrorBoundary } from "./components/base"; import { BaseErrorBoundary } from "./components/base";
import { WindowProvider } from "./hooks/use-window";
import Layout from "./pages/_layout"; import Layout from "./pages/_layout";
import { AppDataProvider } from "./providers/app-data-provider"; import { AppDataProvider } from "./providers/app-data-provider";
import { initializeLanguage } from "./services/i18n"; import { initializeLanguage } from "./services/i18n";
@@ -61,11 +62,13 @@ const initializeApp = async () => {
<React.StrictMode> <React.StrictMode>
<ComposeContextProvider contexts={contexts}> <ComposeContextProvider contexts={contexts}>
<BaseErrorBoundary> <BaseErrorBoundary>
<AppDataProvider> <WindowProvider>
<BrowserRouter> <AppDataProvider>
<Layout /> <BrowserRouter>
</BrowserRouter> <Layout />
</AppDataProvider> </BrowserRouter>
</AppDataProvider>
</WindowProvider>
</BaseErrorBoundary> </BaseErrorBoundary>
</ComposeContextProvider> </ComposeContextProvider>
</React.StrictMode>, </React.StrictMode>,

View File

@@ -4,7 +4,7 @@ import { listen } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate, useRoutes } from "react-router-dom"; import { useLocation, useNavigate, useRoutes } from "react-router-dom";
import { SWRConfig, mutate } from "swr"; import { SWRConfig, mutate } from "swr";
@@ -25,6 +25,7 @@ import { useLogData } from "@/hooks/use-log-data-new";
import { useMemoryData } from "@/hooks/use-memory-data"; import { useMemoryData } from "@/hooks/use-memory-data";
import { useTrafficData } from "@/hooks/use-traffic-data"; import { useTrafficData } from "@/hooks/use-traffic-data";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useWindowDecorations } from "@/hooks/use-window";
import { getAxios } from "@/services/api"; import { getAxios } from "@/services/api";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
import { useClashLog, useThemeMode } from "@/services/states"; import { useClashLog, useThemeMode } from "@/services/states";
@@ -35,6 +36,9 @@ import { routers } from "./_routers";
import "dayjs/locale/ru"; import "dayjs/locale/ru";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import { WindowControls } from "@/components/controller/window-controller";
// 删除重复导入
const appWindow = getCurrentWebviewWindow(); const appWindow = getCurrentWebviewWindow();
export const portableFlag = false; export const portableFlag = false;
@@ -174,6 +178,26 @@ const Layout = () => {
const initRef = useRef(false); const initRef = useRef(false);
const [themeReady, setThemeReady] = useState(false); const [themeReady, setThemeReady] = useState(false);
const windowControls = useRef<any>(null);
const { decorated } = useWindowDecorations();
const customTitlebar = useMemo(() => {
console.debug(
"[Layout] Titlebar rendering - decorated:",
decorated,
"| showing:",
!decorated,
);
if (!decorated) {
return (
<div className="the_titlebar" data-tauri-drag-region="true">
<WindowControls ref={windowControls} />
</div>
);
}
return null;
}, [decorated]);
useEffect(() => { useEffect(() => {
setThemeReady(true); setThemeReady(true);
}, [theme]); }, [theme]);
@@ -389,7 +413,7 @@ const Layout = () => {
console.log("[Layout] 开始监听启动完成事件"); console.log("[Layout] 开始监听启动完成事件");
} catch (err) { } catch (err) {
console.error("[Layout] 监听启动完成事件失败:", err); console.error("[Layout] 监听启动完成事件失败:", err);
return () => {}; return () => { };
} }
}; };
@@ -420,7 +444,7 @@ const Layout = () => {
if (!isInitialized) { if (!isInitialized) {
console.error("[Layout] 紧急初始化触发5秒内未完成初始化"); console.error("[Layout] 紧急初始化触发5秒内未完成初始化");
removeLoadingOverlay(); removeLoadingOverlay();
notifyBackend("UI就绪").catch(() => {}); notifyBackend("UI就绪").catch(() => { });
isInitialized = true; isInitialized = true;
} }
}, 5000); }, 5000);
@@ -495,6 +519,7 @@ const Layout = () => {
}} }}
> >
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
{/* 左侧底部窗口控制按钮 */}
<NoticeManager /> <NoticeManager />
<div <div
style={{ style={{
@@ -534,62 +559,66 @@ const Layout = () => {
({ palette }) => ({ bgcolor: palette.background.paper }), ({ palette }) => ({ bgcolor: palette.background.paper }),
OS === "linux" OS === "linux"
? { ? {
borderRadius: "8px", borderRadius: "8px",
border: "1px solid var(--divider-color)", border: "1px solid var(--divider-color)",
width: "100vw", width: "100vw",
height: "100vh", height: "100vh",
} }
: {}, : {},
]} ]}
> >
<div className="layout__left"> {/* Custom titlebar - rendered only when decorated is false, memoized for performance */}
<div className="the-logo" data-tauri-drag-region="true"> {customTitlebar}
<div
data-tauri-drag-region="true" <div className="layout-content">
style={{ <div className="layout-content__left">
height: "27px", <div className="the-logo" data-tauri-drag-region="false">
display: "flex", <div
justifyContent: "space-between", data-tauri-drag-region="true"
}}
>
<SvgIcon
component={isDark ? iconDark : iconLight}
style={{ style={{
height: "36px", height: "27px",
width: "36px", display: "flex",
marginTop: "-3px", justifyContent: "space-between",
marginRight: "5px",
marginLeft: "-3px",
}} }}
inheritViewBox
/>
<LogoSvg fill={isDark ? "white" : "black"} />
</div>
<UpdateButton className="the-newbtn" />
</div>
<List className="the-menu">
{routers.map((router) => (
<LayoutItem
key={router.label}
to={router.path}
icon={router.icon}
> >
{t(router.label)} <SvgIcon
</LayoutItem> component={isDark ? iconDark : iconLight}
))} style={{
</List> height: "36px",
width: "36px",
marginTop: "-3px",
marginRight: "5px",
marginLeft: "-3px",
}}
inheritViewBox
/>
<LogoSvg fill={isDark ? "white" : "black"} />
</div>
<UpdateButton className="the-newbtn" />
</div>
<div className="the-traffic"> <List className="the-menu">
<LayoutTraffic /> {routers.map((router) => (
<LayoutItem
key={router.label}
to={router.path}
icon={router.icon}
>
{t(router.label)}
</LayoutItem>
))}
</List>
<div className="the-traffic">
<LayoutTraffic />
</div>
</div> </div>
</div>
<div className="layout__right"> <div className="layout-content__right">
<div className="the-bar"></div> <div className="the-bar"></div>
<div className="the-content">
<div className="the-content"> {React.cloneElement(routersEles, { key: location.pathname })}
{React.cloneElement(routersEles, { key: location.pathname })} </div>
</div> </div>
</div> </div>
</Paper> </Paper>