mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
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:
@@ -6,6 +6,7 @@
|
|||||||
- Linux 打包为 `.deb` `.rpm` 提供 pkexec 依赖项
|
- Linux 打包为 `.deb` `.rpm` 提供 pkexec 依赖项
|
||||||
- 支持前端修改日志(最大文件大小、最大保留数量)
|
- 支持前端修改日志(最大文件大小、最大保留数量)
|
||||||
- 新增链式代理图形化设置功能
|
- 新增链式代理图形化设置功能
|
||||||
|
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
|
||||||
- 监听关机事件,自动关闭系统代理
|
- 监听关机事件,自动关闭系统代理
|
||||||
|
|
||||||
### 🚀 优化改进
|
### 🚀 优化改进
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
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 {
|
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
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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
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 { 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>,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user