diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index c5b7c6ec4e..c47008146e 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -123,6 +123,12 @@ export enum IpcChannel { Windows_SetMinimumSize = 'window:set-minimum-size', Windows_Resize = 'window:resize', Windows_GetSize = 'window:get-size', + Windows_Minimize = 'window:minimize', + Windows_Maximize = 'window:maximize', + Windows_Unmaximize = 'window:unmaximize', + Windows_Close = 'window:close', + Windows_IsMaximized = 'window:is-maximized', + Windows_MaximizedChanged = 'window:maximized-changed', KnowledgeBase_Create = 'knowledge-base:create', KnowledgeBase_Reset = 'knowledge-base:reset', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index f989a3d808..bb094a8bac 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -587,6 +587,41 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { return [width, height] }) + // Window Controls + ipcMain.handle(IpcChannel.Windows_Minimize, () => { + checkMainWindow() + mainWindow.minimize() + }) + + ipcMain.handle(IpcChannel.Windows_Maximize, () => { + checkMainWindow() + mainWindow.maximize() + }) + + ipcMain.handle(IpcChannel.Windows_Unmaximize, () => { + checkMainWindow() + mainWindow.unmaximize() + }) + + ipcMain.handle(IpcChannel.Windows_Close, () => { + checkMainWindow() + mainWindow.close() + }) + + ipcMain.handle(IpcChannel.Windows_IsMaximized, () => { + checkMainWindow() + return mainWindow.isMaximized() + }) + + // Send maximized state changes to renderer + mainWindow.on('maximize', () => { + mainWindow.webContents.send(IpcChannel.Windows_MaximizedChanged, true) + }) + + mainWindow.on('unmaximize', () => { + mainWindow.webContents.send(IpcChannel.Windows_MaximizedChanged, false) + }) + // VertexAI ipcMain.handle(IpcChannel.VertexAI_GetAuthHeaders, async (_, params) => { return vertexAIService.getAuthHeaders(params) diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 8fcbebc642..9b8a176a34 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -66,11 +66,19 @@ export class WindowService { transparent: false, vibrancy: 'sidebar', visualEffectState: 'active', - titleBarStyle: 'hidden', - titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight, + // For Windows and Linux, we use frameless window with custom controls + // For Mac, we keep the native title bar style + ...(isMac + ? { + titleBarStyle: 'hidden', + titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight, + trafficLightPosition: { x: 8, y: 13 } + } + : { + frame: false // Frameless window for Windows and Linux + }), backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF', darkTheme: nativeTheme.shouldUseDarkColors, - trafficLightPosition: { x: 8, y: 13 }, ...(isLinux ? { icon } : {}), webPreferences: { preload: join(__dirname, '../preload/index.js'), diff --git a/src/preload/index.ts b/src/preload/index.ts index 0600b6b310..f049710937 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -438,6 +438,20 @@ const api = { cherryin: { generateSignature: (params: { method: string; path: string; query: string; body: Record }) => ipcRenderer.invoke(IpcChannel.Cherryin_GetSignature, params) + }, + windowControls: { + minimize: (): Promise => ipcRenderer.invoke(IpcChannel.Windows_Minimize), + maximize: (): Promise => ipcRenderer.invoke(IpcChannel.Windows_Maximize), + unmaximize: (): Promise => ipcRenderer.invoke(IpcChannel.Windows_Unmaximize), + close: (): Promise => ipcRenderer.invoke(IpcChannel.Windows_Close), + isMaximized: (): Promise => ipcRenderer.invoke(IpcChannel.Windows_IsMaximized), + onMaximizedChange: (callback: (isMaximized: boolean) => void): (() => void) => { + const channel = IpcChannel.Windows_MaximizedChanged + ipcRenderer.on(channel, (_, isMaximized: boolean) => callback(isMaximized)) + return () => { + ipcRenderer.removeAllListeners(channel) + } + } } } diff --git a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx index 2a99f2b560..e5846ffe05 100644 --- a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx +++ b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx @@ -11,6 +11,7 @@ import { ReloadOutlined } from '@ant-design/icons' import { loggerService } from '@logger' +import WindowControls from '@renderer/components/WindowControls' import { isLinux, isMac, isWin } from '@renderer/config/constant' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { useBridge } from '@renderer/hooks/useBridge' @@ -434,7 +435,10 @@ const MinappPopupContainer: React.FC = () => { )} - + handleGoBack(appInfo.id)}> @@ -500,6 +504,11 @@ const MinappPopupContainer: React.FC = () => { + {(isWin || isLinux) && ( +
+ +
+ )} ) } @@ -602,7 +611,6 @@ const ButtonsGroup = styled.div<{ isTopNavbar: boolean }>` gap: 5px; -webkit-app-region: no-drag; &.windows { - margin-right: ${isWin ? '130px' : isLinux ? '100px' : 0}; background-color: var(--color-background-mute); border-radius: 50px; padding: 0 3px; diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx index 6a1d85eb5f..b4dcafb757 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -1,7 +1,7 @@ import { PlusOutlined } from '@ant-design/icons' import { Sortable, useDndReorder } from '@renderer/components/dnd' import Scrollbar from '@renderer/components/Scrollbar' -import { isLinux, isMac, isWin } from '@renderer/config/constant' +import { isMac } from '@renderer/config/constant' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { useTheme } from '@renderer/context/ThemeProvider' import { useFullscreen } from '@renderer/hooks/useFullscreen' @@ -39,6 +39,7 @@ import { useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' import MinAppIcon from '../Icons/MinAppIcon' +import WindowControls from '../WindowControls' interface TabsContainerProps { children: React.ReactNode @@ -268,6 +269,7 @@ const TabsContainer: React.FC = ({ children }) => { + {children} @@ -288,7 +290,7 @@ const TabsBar = styled.div<{ $isFullscreen: boolean }>` align-items: center; gap: 5px; padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? '75px' : '15px')}; - padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')}; + padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : '0')}; height: var(--navbar-height); position: relative; -webkit-app-region: drag; @@ -427,6 +429,7 @@ const RightButtonsContainer = styled.div` align-items: center; gap: 6px; margin-left: auto; + padding-right: ${isMac ? '12px' : '0'}; flex-shrink: 0; ` diff --git a/src/renderer/src/components/WindowControls/WindowControls.styled.ts b/src/renderer/src/components/WindowControls/WindowControls.styled.ts new file mode 100644 index 0000000000..61d208d925 --- /dev/null +++ b/src/renderer/src/components/WindowControls/WindowControls.styled.ts @@ -0,0 +1,42 @@ +import styled from 'styled-components' + +export const WindowControlsContainer = styled.div` + display: flex; + align-items: center; + height: 100%; + -webkit-app-region: no-drag; + user-select: none; +` + +export const ControlButton = styled.button<{ $isClose?: boolean }>` + display: flex; + align-items: center; + justify-content: center; + width: 46px; + height: var(--navbar-height); + border: none; + background: transparent; + color: var(--color-text); + cursor: pointer; + outline: none; + transition: + background 0.15s, + color 0.15s; + padding: 0; + position: relative; + border-radius: 0; + + &:hover { + background: ${(props) => (props.$isClose ? '#e81123' : 'rgba(128, 128, 128, 0.3)')}; + color: ${(props) => (props.$isClose ? '#ffffff' : 'var(--color-text)')}; + } + + &:active { + background: ${(props) => (props.$isClose ? '#c50e1f' : 'rgba(128, 128, 128, 0.4)')}; + color: ${(props) => (props.$isClose ? '#ffffff' : 'var(--color-text)')}; + } + + svg { + pointer-events: none; + } +` diff --git a/src/renderer/src/components/WindowControls/index.tsx b/src/renderer/src/components/WindowControls/index.tsx new file mode 100644 index 0000000000..6c61a5cc28 --- /dev/null +++ b/src/renderer/src/components/WindowControls/index.tsx @@ -0,0 +1,80 @@ +import { isLinux, isWin } from '@renderer/config/constant' +import { Tooltip } from 'antd' +import { Minus, Square, X } from 'lucide-react' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { ControlButton, WindowControlsContainer } from './WindowControls.styled' + +// Custom restore icon - two overlapping squares like Windows +const RestoreIcon: React.FC<{ size?: number }> = ({ size = 14 }) => ( + + {/* Back square (top-right) */} + + {/* Front square (bottom-left) */} + + +) + +const WindowControls: React.FC = () => { + const [isMaximized, setIsMaximized] = useState(false) + const { t } = useTranslation() + + useEffect(() => { + // Check initial maximized state + window.api.windowControls.isMaximized().then(setIsMaximized) + + // Listen for maximized state changes + const unsubscribe = window.api.windowControls.onMaximizedChange(setIsMaximized) + + return () => { + unsubscribe() + } + }, []) + + // Only show on Windows and Linux + if (!isWin && !isLinux) { + return null + } + + const handleMinimize = () => { + window.api.windowControls.minimize() + } + + const handleMaximize = () => { + if (isMaximized) { + window.api.windowControls.unmaximize() + } else { + window.api.windowControls.maximize() + } + } + + const handleClose = () => { + window.api.windowControls.close() + } + + return ( + + + + + + + + + {isMaximized ? : } + + + + + + + + + ) +} + +export default WindowControls diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx index 0d0204eb59..4c80591be1 100644 --- a/src/renderer/src/components/app/Navbar.tsx +++ b/src/renderer/src/components/app/Navbar.tsx @@ -6,6 +6,8 @@ import type { FC, PropsWithChildren } from 'react' import type { HTMLAttributes } from 'react' import styled from 'styled-components' +import WindowControls from '../WindowControls' + type Props = PropsWithChildren & HTMLAttributes export const Navbar: FC = ({ children, ...props }) => { @@ -28,7 +30,17 @@ export const NavbarLeft: FC = ({ children, ...props }) => { } export const NavbarCenter: FC = ({ children, ...props }) => { - return {children} + return ( + + {children} + {/* Add WindowControls for Windows and Linux in NavbarCenter */} + {(isWin || isLinux) && ( +
+ +
+ )} +
+ ) } export const NavbarRight: FC = ({ children, ...props }) => { @@ -81,6 +93,7 @@ const NavbarCenterContainer = styled.div` padding: 0 ${isMac ? '20px' : 0}; font-weight: bold; color: var(--color-text-1); + position: relative; ` const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>` diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 627f57d9cc..e9a1a5c576 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1653,7 +1653,13 @@ "navbar": { "expand": "Expand Dialog", "hide_sidebar": "Hide Sidebar", - "show_sidebar": "Show Sidebar" + "show_sidebar": "Show Sidebar", + "window": { + "close": "Close", + "maximize": "Maximize", + "minimize": "Minimize", + "restore": "Restore" + } }, "navigate": { "provider_settings": "Go to provider settings" diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index b460ca1ae3..ea1ebf2452 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1653,7 +1653,13 @@ "navbar": { "expand": "伸缩对话框", "hide_sidebar": "隐藏侧边栏", - "show_sidebar": "显示侧边栏" + "show_sidebar": "显示侧边栏", + "window": { + "close": "关闭", + "maximize": "最大化", + "minimize": "最小化", + "restore": "还原" + } }, "navigate": { "provider_settings": "跳转到服务商设置界面" diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 3075f4e44b..31e9b1162c 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1653,7 +1653,13 @@ "navbar": { "expand": "伸縮對話框", "hide_sidebar": "隱藏側邊欄", - "show_sidebar": "顯示側邊欄" + "show_sidebar": "顯示側邊欄", + "window": { + "close": "關閉", + "maximize": "最大化", + "minimize": "最小化", + "restore": "還原" + } }, "navigate": { "provider_settings": "跳轉到服務商設置界面" diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 4513d00461..10fd4c2e54 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -1653,7 +1653,13 @@ "navbar": { "expand": "Επισκευή διαλόγου", "hide_sidebar": "Απόκρυψη πλάγιας μπάρας", - "show_sidebar": "Εμφάνιση πλάγιας μπάρας" + "show_sidebar": "Εμφάνιση πλάγιας μπάρας", + "window": { + "close": "Κλείσιμο", + "maximize": "Μεγιστοποίηση", + "minimize": "Ελαχιστοποίηση", + "restore": "Επαναφορά" + } }, "navigate": { "provider_settings": "Μετάβαση στις ρυθμίσεις παρόχου" diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index f4f37f4bf1..1f51c13632 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -1653,7 +1653,13 @@ "navbar": { "expand": "Expandir cuadro de diálogo", "hide_sidebar": "Ocultar barra lateral", - "show_sidebar": "Mostrar barra lateral" + "show_sidebar": "Mostrar barra lateral", + "window": { + "close": "Cerrar", + "maximize": "Maximizar", + "minimize": "Minimizar", + "restore": "Restaurar" + } }, "navigate": { "provider_settings": "Ir a la configuración del proveedor" diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 6e0e785db7..9b4a314c53 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -1653,7 +1653,13 @@ "navbar": { "expand": "Agrandir la boîte de dialogue", "hide_sidebar": "Cacher la barre latérale", - "show_sidebar": "Afficher la barre latérale" + "show_sidebar": "Afficher la barre latérale", + "window": { + "close": "Fermer", + "maximize": "Agrandir", + "minimize": "Réduire", + "restore": "Restaurer" + } }, "navigate": { "provider_settings": "Aller aux paramètres du fournisseur" diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 052077ab9b..46a66b2c5e 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -1653,7 +1653,13 @@ "navbar": { "expand": "ダイアログを展開", "hide_sidebar": "サイドバーを非表示", - "show_sidebar": "サイドバーを表示" + "show_sidebar": "サイドバーを表示", + "window": { + "close": "閉じる", + "maximize": "最大化", + "minimize": "最小化", + "restore": "元に戻す" + } }, "navigate": { "provider_settings": "プロバイダー設定に移動" diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 835a26cc51..37bbb3b6cc 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -1653,7 +1653,13 @@ "navbar": { "expand": "Expandir caixa de diálogo", "hide_sidebar": "Ocultar barra lateral", - "show_sidebar": "Mostrar barra lateral" + "show_sidebar": "Mostrar barra lateral", + "window": { + "close": "Fechar", + "maximize": "Maximizar", + "minimize": "Minimizar", + "restore": "Restaurar" + } }, "navigate": { "provider_settings": "Ir para as configurações do provedor" diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index de8305320b..6b5fb95cc5 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -1653,7 +1653,13 @@ "navbar": { "expand": "Развернуть диалоговое окно", "hide_sidebar": "Скрыть боковую панель", - "show_sidebar": "Показать боковую панель" + "show_sidebar": "Показать боковую панель", + "window": { + "close": "Закрыть", + "maximize": "Развернуть", + "minimize": "Свернуть", + "restore": "Восстановить" + } }, "navigate": { "provider_settings": "Перейти к настройкам поставщика" diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index 4fa1568954..21c0f8fd69 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -1,7 +1,8 @@ import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { HStack } from '@renderer/components/Layout' import SearchPopup from '@renderer/components/Popups/SearchPopup' -import { isMac } from '@renderer/config/constant' +import WindowControls from '@renderer/components/WindowControls' +import { isLinux, isMac, isWin } from '@renderer/config/constant' import { useAssistant } from '@renderer/hooks/useAssistant' import { useFullscreen } from '@renderer/hooks/useFullscreen' import { modelGenerating } from '@renderer/hooks/useRuntime' @@ -87,7 +88,9 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo )} - + {!showAssistants && ( @@ -114,18 +117,8 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo - + - - SearchPopup.show()}> - - - - - - - - {topicPosition === 'right' && !showTopics && ( @@ -140,7 +133,47 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo )} + {/* For Mac, show search and expand without WindowControls */} + {!isWin && !isLinux && ( + <> + + SearchPopup.show()}> + + + + + + + + + + )} + {/* Search, Expand and WindowControls positioned at the right edge */} + {(isWin || isLinux) && ( +
+ + SearchPopup.show()}> + + + + + + + + + +
+ )}
)