|
1 | 1 | import { useCallback, useRef, useEffect, useState } from 'react'; |
2 | 2 | import { Dropdown, Tooltip, App, theme, Popover, Divider, Typography, Space, Spin } from 'antd'; |
3 | 3 | 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'; |
5 | 5 | import { useTranslation } from 'react-i18next'; |
6 | 6 | import { useUIStore, useSettingsStore } from '@/stores'; |
7 | 7 | import { useBackupStore } from '@/stores/backupStore'; |
8 | 8 | import { isTauri, invoke } from '@/lib/invoke'; |
9 | 9 | import { getShortcutBinding, formatShortcutForDisplay } from '@/lib/shortcuts'; |
10 | 10 | 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 | +); |
11 | 22 |
|
12 | 23 | const THEME_OPTIONS = [ |
13 | 24 | { key: 'system', icon: <Monitor size={14} />, labelKey: 'settings.themeSystem' }, |
@@ -117,6 +128,38 @@ export function TitleBar() { |
117 | 128 | }); |
118 | 129 | }, [modal, t]); |
119 | 130 |
|
| 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 | + |
120 | 163 | // Quick Backup state |
121 | 164 | const [backupPopoverOpen, setBackupPopoverOpen] = useState(false); |
122 | 165 | const [backingUp, setBackingUp] = useState<'local' | 'webdav' | null>(null); |
@@ -357,14 +400,23 @@ export function TitleBar() { |
357 | 400 | height: 36, |
358 | 401 | display: 'flex', |
359 | 402 | 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, |
363 | 406 | backgroundColor: 'transparent', |
364 | 407 | flexShrink: 0, |
365 | 408 | borderBottom: `1px solid ${token.colorBorderSecondary}`, |
366 | 409 | }} |
367 | 410 | > |
| 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 }}> |
368 | 420 | <div className="title-bar-nodrag" style={{ display: 'flex', alignItems: 'center', gap: 4 }}> |
369 | 421 | {/* Pin Toggle */} |
370 | 422 | <Tooltip title={t('desktop.alwaysOnTop')}> |
@@ -607,6 +659,74 @@ export function TitleBar() { |
607 | 659 | </button> |
608 | 660 | </Tooltip> |
609 | 661 | </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> |
610 | 730 | </div> |
611 | 731 | ); |
612 | 732 | } |
0 commit comments