Skip to content

Commit 6e61a29

Browse files
committed
feat(titlebar): 添加 Windows 窗口控制和自定义标题栏
1 parent b5ffaaf commit 6e61a29

2 files changed

Lines changed: 131 additions & 4 deletions

File tree

src-tauri/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,13 @@ pub fn run() {
397397
});
398398

399399
if let Some(main_window) = app.get_webview_window("main") {
400+
// On Windows, hide native decorations so the custom TitleBar is
401+
// the only title bar. macOS keeps its Overlay style (traffic lights).
402+
#[cfg(target_os = "windows")]
403+
{
404+
let _ = main_window.set_decorations(false);
405+
}
406+
400407
if let Some(saved_state) = window_state::load_window_state(&app_dir) {
401408
let restored_state = if let Ok(Some(monitor)) = main_window.current_monitor() {
402409
let monitor_size = monitor

src/components/layout/TitleBar.tsx

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
import { useCallback, useRef, useEffect, useState } from 'react';
22
import { Dropdown, Tooltip, App, theme, Popover, Divider, Typography, Space, Spin } from 'antd';
33
import type { MenuProps } from 'antd';
4-
import { Settings, XCircle, Sun, Moon, Monitor, Globe, Pin, PinOff, RotateCcw, CloudUpload, Github, Star, MessageSquarePlus, Bug, ArrowDownCircle } from 'lucide-react';
4+
import { Settings, XCircle, Sun, Moon, Monitor, Globe, Pin, PinOff, RotateCcw, CloudUpload, Github, Star, MessageSquarePlus, Bug, ArrowDownCircle, Minus, X, Square } from 'lucide-react';
55
import { useTranslation } from 'react-i18next';
66
import { useUIStore, useSettingsStore } from '@/stores';
77
import { useBackupStore } from '@/stores/backupStore';
88
import { isTauri, invoke } from '@/lib/invoke';
99
import { getShortcutBinding, formatShortcutForDisplay } from '@/lib/shortcuts';
1010
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
11+
import appLogo from '@/assets/image/logo.png';
12+
13+
const IS_WINDOWS = navigator.userAgent.includes('Windows');
14+
15+
/** Standard Windows "restore down" icon: two overlapping rectangles */
16+
const RestoreIcon = () => (
17+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.2">
18+
<rect x="3" y="5" width="8" height="7" rx="0.5" />
19+
<path d="M5 5V3.5a.5.5 0 0 1 .5-.5H12a.5.5 0 0 1 .5.5V10a.5.5 0 0 1-.5.5h-1.5" />
20+
</svg>
21+
);
1122

1223
const THEME_OPTIONS = [
1324
{ key: 'system', icon: <Monitor size={14} />, labelKey: 'settings.themeSystem' },
@@ -117,6 +128,38 @@ export function TitleBar() {
117128
});
118129
}, [modal, t]);
119130

131+
// Windows window controls
132+
const [isMaximized, setIsMaximized] = useState(false);
133+
134+
useEffect(() => {
135+
if (!IS_WINDOWS || !isTauri()) return;
136+
let unlisten: (() => void) | undefined;
137+
(async () => {
138+
const { getCurrentWindow } = await import('@tauri-apps/api/window');
139+
const win = getCurrentWindow();
140+
setIsMaximized(await win.isMaximized());
141+
unlisten = await win.onResized(async () => {
142+
setIsMaximized(await win.isMaximized());
143+
});
144+
})();
145+
return () => { unlisten?.(); };
146+
}, []);
147+
148+
const handleWindowMinimize = useCallback(async () => {
149+
const { getCurrentWindow } = await import('@tauri-apps/api/window');
150+
await getCurrentWindow().minimize();
151+
}, []);
152+
153+
const handleWindowMaximize = useCallback(async () => {
154+
const { getCurrentWindow } = await import('@tauri-apps/api/window');
155+
await getCurrentWindow().toggleMaximize();
156+
}, []);
157+
158+
const handleWindowClose = useCallback(async () => {
159+
const { getCurrentWindow } = await import('@tauri-apps/api/window');
160+
await getCurrentWindow().close();
161+
}, []);
162+
120163
// Quick Backup state
121164
const [backupPopoverOpen, setBackupPopoverOpen] = useState(false);
122165
const [backingUp, setBackingUp] = useState<'local' | 'webdav' | null>(null);
@@ -357,14 +400,23 @@ export function TitleBar() {
357400
height: 36,
358401
display: 'flex',
359402
alignItems: 'center',
360-
justifyContent: 'flex-end',
361-
paddingLeft: 72,
362-
paddingRight: 12,
403+
justifyContent: 'space-between',
404+
paddingLeft: IS_WINDOWS ? 12 : 72,
405+
paddingRight: IS_WINDOWS ? 0 : 12,
363406
backgroundColor: 'transparent',
364407
flexShrink: 0,
365408
borderBottom: `1px solid ${token.colorBorderSecondary}`,
366409
}}
367410
>
411+
{/* Left: App icon + name (Windows only) */}
412+
{IS_WINDOWS ? (
413+
<div className="title-bar-nodrag" style={{ display: 'flex', alignItems: 'center', gap: 6, marginRight: 8 }}>
414+
<img src={appLogo} alt="AQBot" style={{ width: 18, height: 18 }} draggable={false} />
415+
<span style={{ fontSize: 13, fontWeight: 600, color: token.colorTextBase, userSelect: 'none' }}>AQBot</span>
416+
</div>
417+
) : <div />}
418+
419+
<div style={{ display: 'flex', alignItems: 'center', gap: 0 }}>
368420
<div className="title-bar-nodrag" style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
369421
{/* Pin Toggle */}
370422
<Tooltip title={t('desktop.alwaysOnTop')}>
@@ -607,6 +659,74 @@ export function TitleBar() {
607659
</button>
608660
</Tooltip>
609661
</div>
662+
663+
{/* Windows window controls */}
664+
{IS_WINDOWS && isTauri() && (
665+
<div className="title-bar-nodrag" style={{ display: 'flex', alignItems: 'center', marginLeft: 4 }}>
666+
{/* Minimize */}
667+
<button
668+
onClick={handleWindowMinimize}
669+
style={{
670+
display: 'flex',
671+
alignItems: 'center',
672+
justifyContent: 'center',
673+
width: 46,
674+
height: 36,
675+
border: 'none',
676+
background: 'transparent',
677+
color: token.colorTextSecondary,
678+
cursor: 'pointer',
679+
outline: 'none',
680+
}}
681+
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = token.colorFillSecondary; e.currentTarget.style.color = token.colorTextBase; }}
682+
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; e.currentTarget.style.color = token.colorTextSecondary; }}
683+
>
684+
<Minus size={16} />
685+
</button>
686+
{/* Maximize / Restore */}
687+
<button
688+
onClick={handleWindowMaximize}
689+
style={{
690+
display: 'flex',
691+
alignItems: 'center',
692+
justifyContent: 'center',
693+
width: 46,
694+
height: 36,
695+
border: 'none',
696+
background: 'transparent',
697+
color: token.colorTextSecondary,
698+
cursor: 'pointer',
699+
outline: 'none',
700+
}}
701+
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = token.colorFillSecondary; e.currentTarget.style.color = token.colorTextBase; }}
702+
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; e.currentTarget.style.color = token.colorTextSecondary; }}
703+
>
704+
{isMaximized ? <RestoreIcon /> : <Square size={14} />}
705+
</button>
706+
{/* Close */}
707+
<button
708+
onClick={handleWindowClose}
709+
style={{
710+
display: 'flex',
711+
alignItems: 'center',
712+
justifyContent: 'center',
713+
width: 46,
714+
height: 36,
715+
border: 'none',
716+
background: 'transparent',
717+
color: token.colorTextSecondary,
718+
cursor: 'pointer',
719+
outline: 'none',
720+
borderRadius: 0,
721+
}}
722+
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#e81123'; e.currentTarget.style.color = '#ffffff'; }}
723+
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; e.currentTarget.style.color = token.colorTextSecondary; }}
724+
>
725+
<X size={16} />
726+
</button>
727+
</div>
728+
)}
729+
</div>
610730
</div>
611731
);
612732
}

0 commit comments

Comments
 (0)