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:
committed by
GitHub
Unverified
parent
f195b3bccf
commit
bfd1274a8c
@@ -6,6 +6,7 @@
|
||||
- Linux 打包为 `.deb` `.rpm` 提供 pkexec 依赖项
|
||||
- 支持前端修改日志(最大文件大小、最大保留数量)
|
||||
- 新增链式代理图形化设置功能
|
||||
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
|
||||
- 监听关机事件,自动关闭系统代理
|
||||
|
||||
### 🚀 优化改进
|
||||
|
||||
@@ -27,7 +27,8 @@ pub fn build_new_window() -> Result<WebviewWindow, String> {
|
||||
)
|
||||
.title("Clash Verge")
|
||||
.center()
|
||||
.decorations(true)
|
||||
// Using WindowManager::prefer_system_titlebar to control if show system built-in titlebar
|
||||
// .decorations(true)
|
||||
.fullscreen(false)
|
||||
.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
|
||||
.min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT)
|
||||
|
||||
@@ -2,118 +2,103 @@
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
&__left {
|
||||
flex: 1 0 200px;
|
||||
.layout-content {
|
||||
/* New container for the flex layout */
|
||||
display: flex;
|
||||
height: 100%;
|
||||
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;
|
||||
flex: 1; /* Take remaining height */
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--divider-color);
|
||||
// background-color: var(--background-color-alpha);
|
||||
|
||||
// $maxLogo: 100px;
|
||||
|
||||
.the-logo {
|
||||
position: relative;
|
||||
flex: 1 0 58px;
|
||||
// width: 100%;
|
||||
&__left {
|
||||
flex: 1 0 200px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding: 0px 20px;
|
||||
width: 100%;
|
||||
padding: 0 0 8px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
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;
|
||||
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,
|
||||
svg {
|
||||
width: 100%;
|
||||
.the-logo {
|
||||
position: relative;
|
||||
flex: 1 0 58px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
// fill: var(--primary-main);
|
||||
padding: 0 20px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
box-sizing: border-box;
|
||||
|
||||
// #bg {
|
||||
// fill: var(--background-color);
|
||||
// }
|
||||
img,
|
||||
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;
|
||||
right: 10px;
|
||||
top: 15px;
|
||||
border-radius: 8px;
|
||||
padding: 2px 4px;
|
||||
transform: scale(0.8);
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 1px;
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.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,
|
||||
.unknown {
|
||||
&.layout {
|
||||
//.layout__left {
|
||||
// padding-top: 24px;
|
||||
//}
|
||||
.the_titlebar {
|
||||
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;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
@@ -135,7 +126,7 @@
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.layout__right .the-content {
|
||||
.layout-content__right .the-content {
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
@@ -143,15 +134,25 @@
|
||||
|
||||
.macos {
|
||||
&.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;
|
||||
}
|
||||
|
||||
.layout__right .the-content {
|
||||
.layout-content__right .the-content {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.layout__left .the-newbtn {
|
||||
.layout-content__left .the-newbtn {
|
||||
right: 9px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
114
src/components/controller/window-controller.tsx
Normal file
114
src/components/controller/window-controller.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
List,
|
||||
Box,
|
||||
Button,
|
||||
Select,
|
||||
MenuItem,
|
||||
styled,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Box,
|
||||
MenuItem,
|
||||
Select,
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
import { join } from "@tauri-apps/api/path";
|
||||
@@ -18,10 +18,10 @@ import { useTranslation } from "react-i18next";
|
||||
import { BaseDialog, DialogRef, Switch } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useWindowDecorations } from "@/hooks/use-window";
|
||||
import { copyIconFile, getAppDir } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
import { GuardState } from "./guard-state";
|
||||
|
||||
const OS = getSystem();
|
||||
@@ -47,6 +47,8 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const [sysproxyIcon, setSysproxyIcon] = useState("");
|
||||
const [tunIcon, setTunIcon] = useState("");
|
||||
|
||||
const { decorated, toggleDecorations } = useWindowDecorations();
|
||||
|
||||
useEffect(() => {
|
||||
initIconPath();
|
||||
}, []);
|
||||
@@ -108,6 +110,21 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<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>
|
||||
<ListItemText primary={t("Traffic Graph")} />
|
||||
<GuardState
|
||||
|
||||
@@ -8,11 +8,11 @@ import { DialogRef } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import {
|
||||
exitApp,
|
||||
exportDiagnosticInfo,
|
||||
openAppDir,
|
||||
openCoreDir,
|
||||
openLogsDir,
|
||||
openDevTools,
|
||||
exportDiagnosticInfo,
|
||||
openLogsDir,
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { version } from "@root/package.json";
|
||||
@@ -23,7 +23,7 @@ import { HotkeyViewer } from "./mods/hotkey-viewer";
|
||||
import { LayoutViewer } from "./mods/layout-viewer";
|
||||
import { LiteModeViewer } from "./mods/lite-mode-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 { UpdateViewer } from "./mods/update-viewer";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -19,7 +19,7 @@ import { GuardState } from "./mods/guard-state";
|
||||
import { HotkeyViewer } from "./mods/hotkey-viewer";
|
||||
import { LayoutViewer } from "./mods/layout-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 { ThemeViewer } from "./mods/theme-viewer";
|
||||
import { UpdateViewer } from "./mods/update-viewer";
|
||||
|
||||
114
src/hooks/use-window.tsx
Normal file
114
src/hooks/use-window.tsx
Normal 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 };
|
||||
};
|
||||
13
src/main.tsx
13
src/main.tsx
@@ -10,6 +10,7 @@ import { BrowserRouter } from "react-router-dom";
|
||||
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
||||
|
||||
import { BaseErrorBoundary } from "./components/base";
|
||||
import { WindowProvider } from "./hooks/use-window";
|
||||
import Layout from "./pages/_layout";
|
||||
import { AppDataProvider } from "./providers/app-data-provider";
|
||||
import { initializeLanguage } from "./services/i18n";
|
||||
@@ -61,11 +62,13 @@ const initializeApp = async () => {
|
||||
<React.StrictMode>
|
||||
<ComposeContextProvider contexts={contexts}>
|
||||
<BaseErrorBoundary>
|
||||
<AppDataProvider>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</AppDataProvider>
|
||||
<WindowProvider>
|
||||
<AppDataProvider>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</AppDataProvider>
|
||||
</WindowProvider>
|
||||
</BaseErrorBoundary>
|
||||
</ComposeContextProvider>
|
||||
</React.StrictMode>,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import dayjs from "dayjs";
|
||||
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 { useLocation, useNavigate, useRoutes } from "react-router-dom";
|
||||
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 { useTrafficData } from "@/hooks/use-traffic-data";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useWindowDecorations } from "@/hooks/use-window";
|
||||
import { getAxios } from "@/services/api";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { useClashLog, useThemeMode } from "@/services/states";
|
||||
@@ -35,6 +36,9 @@ import { routers } from "./_routers";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/zh-cn";
|
||||
|
||||
import { WindowControls } from "@/components/controller/window-controller";
|
||||
// 删除重复导入
|
||||
|
||||
const appWindow = getCurrentWebviewWindow();
|
||||
export const portableFlag = false;
|
||||
|
||||
@@ -174,6 +178,26 @@ const Layout = () => {
|
||||
const initRef = useRef(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(() => {
|
||||
setThemeReady(true);
|
||||
}, [theme]);
|
||||
@@ -389,7 +413,7 @@ const Layout = () => {
|
||||
console.log("[Layout] 开始监听启动完成事件");
|
||||
} catch (err) {
|
||||
console.error("[Layout] 监听启动完成事件失败:", err);
|
||||
return () => {};
|
||||
return () => { };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -420,7 +444,7 @@ const Layout = () => {
|
||||
if (!isInitialized) {
|
||||
console.error("[Layout] 紧急初始化触发:5秒内未完成初始化");
|
||||
removeLoadingOverlay();
|
||||
notifyBackend("UI就绪").catch(() => {});
|
||||
notifyBackend("UI就绪").catch(() => { });
|
||||
isInitialized = true;
|
||||
}
|
||||
}, 5000);
|
||||
@@ -495,6 +519,7 @@ const Layout = () => {
|
||||
}}
|
||||
>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* 左侧底部窗口控制按钮 */}
|
||||
<NoticeManager />
|
||||
<div
|
||||
style={{
|
||||
@@ -534,62 +559,66 @@ const Layout = () => {
|
||||
({ palette }) => ({ bgcolor: palette.background.paper }),
|
||||
OS === "linux"
|
||||
? {
|
||||
borderRadius: "8px",
|
||||
border: "1px solid var(--divider-color)",
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
}
|
||||
borderRadius: "8px",
|
||||
border: "1px solid var(--divider-color)",
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
}
|
||||
: {},
|
||||
]}
|
||||
>
|
||||
<div className="layout__left">
|
||||
<div className="the-logo" data-tauri-drag-region="true">
|
||||
<div
|
||||
data-tauri-drag-region="true"
|
||||
style={{
|
||||
height: "27px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<SvgIcon
|
||||
component={isDark ? iconDark : iconLight}
|
||||
{/* Custom titlebar - rendered only when decorated is false, memoized for performance */}
|
||||
{customTitlebar}
|
||||
|
||||
<div className="layout-content">
|
||||
<div className="layout-content__left">
|
||||
<div className="the-logo" data-tauri-drag-region="false">
|
||||
<div
|
||||
data-tauri-drag-region="true"
|
||||
style={{
|
||||
height: "36px",
|
||||
width: "36px",
|
||||
marginTop: "-3px",
|
||||
marginRight: "5px",
|
||||
marginLeft: "-3px",
|
||||
height: "27px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
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)}
|
||||
</LayoutItem>
|
||||
))}
|
||||
</List>
|
||||
<SvgIcon
|
||||
component={isDark ? iconDark : iconLight}
|
||||
style={{
|
||||
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">
|
||||
<LayoutTraffic />
|
||||
<List className="the-menu">
|
||||
{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 className="layout__right">
|
||||
<div className="the-bar"></div>
|
||||
|
||||
<div className="the-content">
|
||||
{React.cloneElement(routersEles, { key: location.pathname })}
|
||||
<div className="layout-content__right">
|
||||
<div className="the-bar"></div>
|
||||
<div className="the-content">
|
||||
{React.cloneElement(routersEles, { key: location.pathname })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
Reference in New Issue
Block a user