diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 9fdfa69c0e..7ed15a721a 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -246,5 +246,8 @@ export enum IpcChannel { // Navigation Navigation_Url = 'navigation:url', - Navigation_Close = 'navigation:close' + Navigation_Close = 'navigation:close', + + // Settings Window + SettingsWindow_Show = 'settings-window:show' } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 86e9cc0fe3..b0c2bfb572 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -31,6 +31,7 @@ import { pythonService } from './services/PythonService' import { FileServiceManager } from './services/remotefile/FileServiceManager' import { searchService } from './services/SearchService' import { SelectionService } from './services/SelectionService' +import { SettingsWindowService } from './services/SettingsWindowService' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import storeSyncService from './services/StoreSyncService' import { themeService } from './services/ThemeService' @@ -583,4 +584,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.Navigation_Url, (_, url: string) => { CacheService.set('navigation-url', url) }) + + // Settings Window + SettingsWindowService.registerIpcHandler() } diff --git a/src/main/services/SettingsWindowService.ts b/src/main/services/SettingsWindowService.ts new file mode 100644 index 0000000000..8b2c39b6f4 --- /dev/null +++ b/src/main/services/SettingsWindowService.ts @@ -0,0 +1,149 @@ +import { is } from '@electron-toolkit/utils' +import { isLinux, isMac } from '@main/constant' +import { IpcChannel } from '@shared/IpcChannel' +import { BrowserWindow, nativeTheme } from 'electron' +import { join } from 'path' + +import icon from '../../../build/icon.png?asset' +import { titleBarOverlayDark, titleBarOverlayLight } from '../config' + +export class SettingsWindowService { + private static instance: SettingsWindowService | null = null + private settingsWindow: BrowserWindow | null = null + + public static getInstance(): SettingsWindowService { + if (!SettingsWindowService.instance) { + SettingsWindowService.instance = new SettingsWindowService() + } + return SettingsWindowService.instance + } + + public createSettingsWindow(defaultTab?: string): BrowserWindow { + if (this.settingsWindow && !this.settingsWindow.isDestroyed()) { + this.settingsWindow.show() + this.settingsWindow.focus() + return this.settingsWindow + } + + this.settingsWindow = new BrowserWindow({ + width: 1000, + height: 700, + minWidth: 900, + minHeight: 600, + show: false, + autoHideMenuBar: true, + transparent: false, + vibrancy: 'sidebar', + visualEffectState: 'active', + titleBarStyle: 'hidden', + titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight, + backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF', + darkTheme: nativeTheme.shouldUseDarkColors, + trafficLightPosition: { x: 12, y: 12 }, + ...(isLinux ? { icon } : {}), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false, + webSecurity: false, + webviewTag: true, + allowRunningInsecureContent: true, + backgroundThrottling: false + } + }) + + this.setupSettingsWindow() + this.loadSettingsWindowContent(defaultTab) + + return this.settingsWindow + } + + private setupSettingsWindow() { + if (!this.settingsWindow) return + + this.settingsWindow.on('ready-to-show', () => { + this.settingsWindow?.show() + }) + + this.settingsWindow.on('closed', () => { + this.settingsWindow = null + }) + + this.settingsWindow.on('close', () => { + // Clean up when window is closed + }) + + // Handle theme changes + nativeTheme.on('updated', () => { + if (this.settingsWindow && !this.settingsWindow.isDestroyed()) { + this.settingsWindow.setTitleBarOverlay( + nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight + ) + } + }) + } + + private loadSettingsWindowContent(defaultTab?: string) { + if (!this.settingsWindow) return + + const queryParam = defaultTab ? `?tab=${defaultTab}` : '' + + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + this.settingsWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/settingsWindow.html' + queryParam) + } else { + this.settingsWindow.loadFile(join(__dirname, '../renderer/settingsWindow.html')) + if (defaultTab) { + this.settingsWindow.webContents.once('did-finish-load', () => { + this.settingsWindow?.webContents.send(IpcChannel.SettingsWindow_Show, { defaultTab }) + }) + } + } + } + + public showSettingsWindow(defaultTab?: string) { + if (this.settingsWindow && !this.settingsWindow.isDestroyed()) { + if (this.settingsWindow.isMinimized()) { + this.settingsWindow.restore() + } + + if (!isLinux) { + this.settingsWindow.setVisibleOnAllWorkspaces(true) + } + + this.settingsWindow.show() + this.settingsWindow.focus() + + if (!isLinux) { + this.settingsWindow.setVisibleOnAllWorkspaces(false) + } + } else { + this.createSettingsWindow(defaultTab) + } + } + + public hideSettingsWindow() { + if (this.settingsWindow && !this.settingsWindow.isDestroyed()) { + this.settingsWindow.hide() + } + } + + public closeSettingsWindow() { + if (this.settingsWindow && !this.settingsWindow.isDestroyed()) { + this.settingsWindow.close() + } + } + + public getSettingsWindow(): BrowserWindow | null { + return this.settingsWindow + } + + public static registerIpcHandler() { + const { ipcMain } = require('electron') + const service = SettingsWindowService.getInstance() + + ipcMain.handle(IpcChannel.SettingsWindow_Show, (_, options?: { defaultTab?: string }) => { + service.showSettingsWindow(options?.defaultTab) + }) + } +} + +export const settingsWindowService = SettingsWindowService.getInstance() diff --git a/src/main/services/ShortcutService.ts b/src/main/services/ShortcutService.ts index 24ea2324fd..d944119859 100644 --- a/src/main/services/ShortcutService.ts +++ b/src/main/services/ShortcutService.ts @@ -5,10 +5,12 @@ import Logger from 'electron-log' import { configManager } from './ConfigManager' import selectionService from './SelectionService' +import { settingsWindowService } from './SettingsWindowService' import { windowService } from './WindowService' let showAppAccelerator: string | null = null let showMiniWindowAccelerator: string | null = null +let showSettingsAccelerator: string | null = null let selectionAssistantToggleAccelerator: string | null = null let selectionAssistantSelectTextAccelerator: string | null = null @@ -26,6 +28,10 @@ function getShortcutHandler(shortcut: Shortcut) { return (window: BrowserWindow) => handleZoomFactor([window], -0.1) case 'zoom_reset': return (window: BrowserWindow) => handleZoomFactor([window], 0, true) + case 'show_settings': + return () => { + settingsWindowService.showSettingsWindow() + } case 'show_app': return () => { windowService.toggleMainWindow() @@ -146,9 +152,13 @@ export function registerShortcuts(window: BrowserWindow) { // only register universal shortcuts when needed if ( onlyUniversalShortcuts && - !['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes( - shortcut.key - ) + ![ + 'show_app', + 'mini_window', + 'show_settings', + 'selection_assistant_toggle', + 'selection_assistant_select_text' + ].includes(shortcut.key) ) { return } @@ -171,6 +181,10 @@ export function registerShortcuts(window: BrowserWindow) { showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut) break + case 'show_settings': + showSettingsAccelerator = formatShortcutKey(shortcut.shortcut) + break + case 'selection_assistant_toggle': selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut) break @@ -222,6 +236,12 @@ export function registerShortcuts(window: BrowserWindow) { handler && globalShortcut.register(accelerator, () => handler(window)) } + if (showSettingsAccelerator) { + const handler = getShortcutHandler({ key: 'show_settings' } as Shortcut) + const accelerator = convertShortcutFormat(showSettingsAccelerator) + handler && globalShortcut.register(accelerator, () => handler(window)) + } + if (selectionAssistantToggleAccelerator) { const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut) const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator) @@ -258,6 +278,7 @@ export function unregisterAllShortcuts() { try { showAppAccelerator = null showMiniWindowAccelerator = null + showSettingsAccelerator = null selectionAssistantToggleAccelerator = null selectionAssistantSelectTextAccelerator = null windowOnHandlers.forEach((handlers, window) => { diff --git a/src/preload/index.ts b/src/preload/index.ts index d420e693dd..62505b8d4b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -322,7 +322,21 @@ const api = { url: (url: string) => ipcRenderer.invoke(IpcChannel.Navigation_Url, url) }, setDisableHardwareAcceleration: (isDisable: boolean) => - ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable) + ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable), + + // Settings Window + showSettingsWindow: (options?: { defaultTab?: string }) => + ipcRenderer.invoke(IpcChannel.SettingsWindow_Show, options), + + on: (channel: string, func: any) => { + const listener = (_event: Electron.IpcRendererEvent, ...args: any[]) => { + func(...args) + } + ipcRenderer.on(channel, listener) + return () => { + ipcRenderer.off(channel, listener) + } + } } // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/settingsWindow.html b/src/renderer/settingsWindow.html new file mode 100644 index 0000000000..2974290358 --- /dev/null +++ b/src/renderer/settingsWindow.html @@ -0,0 +1,23 @@ + + + + + + + Cherry Studio + + + + + +
+ + + \ No newline at end of file diff --git a/src/renderer/src/components/Popups/SettingsPopup.tsx b/src/renderer/src/components/Popups/SettingsPopup.tsx index a4bd356999..6f16c0d51b 100644 --- a/src/renderer/src/components/Popups/SettingsPopup.tsx +++ b/src/renderer/src/components/Popups/SettingsPopup.tsx @@ -1,230 +1,24 @@ -import AboutSettings from '@renderer/pages/settings/AboutSettings' -import DataSettings from '@renderer/pages/settings/DataSettings/DataSettings' -import DisplaySettings from '@renderer/pages/settings/DisplaySettings/DisplaySettings' -import GeneralSettings from '@renderer/pages/settings/GeneralSettings' -import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings' -import ProvidersList from '@renderer/pages/settings/ProviderSettings' -import QuickAssistantSettings from '@renderer/pages/settings/QuickAssistantSettings' -import QuickPhraseSettings from '@renderer/pages/settings/QuickPhraseSettings' -import SelectionAssistantSettings from '@renderer/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings' -import ShortcutSettings from '@renderer/pages/settings/ShortcutSettings' -import WebSearchSettings from '@renderer/pages/settings/WebSearchSettings' -import { Modal, Spin } from 'antd' -import { - Cloud, - Command, - Globe, - HardDrive, - Info, - MonitorCog, - Package, - Rocket, - Settings2, - TextCursorInput, - Zap -} from 'lucide-react' -import React, { Suspense, useState } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' - -import { TopView } from '../TopView' - -type SettingsTab = - | 'provider' - | 'model' - | 'web-search' - | 'general' - | 'display' - | 'shortcut' - | 'quickAssistant' - | 'selectionAssistant' - | 'data' - | 'about' - | 'quickPhrase' - -interface SettingsPopupShowParams { - defaultTab?: SettingsTab +export interface SettingsPopupShowParams { + defaultTab?: + | 'provider' + | 'model' + | 'tool' + | 'general' + | 'display' + | 'shortcut' + | 'quickAssistant' + | 'selectionAssistant' + | 'data' + | 'about' + | 'quickPhrase' } -interface Props extends SettingsPopupShowParams { - resolve?: (value: any) => void -} - -const SettingsPopupContainer: React.FC = ({ defaultTab = 'provider', resolve }) => { - const { t } = useTranslation() - const [activeTab, setActiveTab] = useState(defaultTab) - const [open, setOpen] = useState(true) - - const menuItems = [ - { key: 'provider', icon: , label: t('settings.provider.title') }, - { key: 'model', icon: , label: t('settings.model') }, - { key: 'web-search', icon: , label: t('settings.websearch.title') }, - { key: 'general', icon: , label: t('settings.general') }, - { key: 'display', icon: , label: t('settings.display.title') }, - { key: 'shortcut', icon: , label: t('settings.shortcuts.title') }, - { key: 'quickAssistant', icon: , label: t('settings.quickAssistant.title') }, - { key: 'selectionAssistant', icon: , label: t('selection.name') }, - { key: 'quickPhrase', icon: , label: t('settings.quickPhrase.title') }, - { key: 'data', icon: , label: t('settings.data.title') }, - { key: 'about', icon: , label: t('settings.about') } - ] as const - - const renderContent = () => { - switch (activeTab) { - case 'provider': - return ( - }> - - - ) - case 'model': - return - case 'web-search': - return - case 'general': - return - case 'display': - return - case 'shortcut': - return - case 'quickAssistant': - return - case 'selectionAssistant': - return - case 'data': - return - case 'about': - return - case 'quickPhrase': - return - default: - return - } - } - - const onCancel = () => { - setOpen(false) - } - - const onAfterClose = () => { - resolve && resolve(null) - TopView.hide(TopViewKey) - } - - // 设置全局隐藏方法 - SettingsPopup.hide = onCancel - - return ( - - - - {menuItems.map((item) => ( - setActiveTab(item.key as SettingsTab)}> - {item.icon} - {item.label} - - ))} - - {renderContent()} - - - ) -} - -const TopViewKey = 'SettingsPopup' - export default class SettingsPopup { static hide() { - TopView.hide(TopViewKey) + // Settings window is now independent, user can close it manually } static show(props: SettingsPopupShowParams = {}) { - return new Promise((resolve) => { - TopView.show(, TopViewKey) - }) + return window.api.showSettingsWindow({ defaultTab: props.defaultTab }) } } - -const StyledModal = styled(Modal)` - .ant-modal-content { - height: 80vh; - display: flex; - flex-direction: column; - } - - .ant-modal-body { - flex: 1; - padding: 0; - overflow: hidden; - } -` - -const ContentContainer = styled.div` - display: flex; - flex: 1; - height: 100%; -` - -const SettingMenus = styled.div` - display: flex; - flex-direction: column; - min-width: var(--settings-width); - border-right: 0.5px solid var(--color-border); - padding: 10px; - user-select: none; - background: var(--color-background); -` - -const MenuItem = styled.div` - display: flex; - flex-direction: row; - align-items: center; - gap: 8px; - padding: 6px 10px; - width: 100%; - cursor: pointer; - border-radius: var(--list-item-border-radius); - font-weight: 500; - transition: all 0.2s ease-in-out; - border: 0.5px solid transparent; - margin-bottom: 5px; - - .anticon { - font-size: 16px; - opacity: 0.8; - } - - .iconfont { - font-size: 18px; - line-height: 18px; - opacity: 0.7; - margin-left: -1px; - } - - &:hover { - background: var(--color-background-soft); - } - - &.active { - background: var(--color-background-soft); - border: 0.5px solid var(--color-border); - } -` - -const SettingContent = styled.div` - display: flex; - height: 100%; - flex: 1; - overflow: auto; -` diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx index 73f6cbcfa1..d47431b073 100644 --- a/src/renderer/src/components/app/Navbar.tsx +++ b/src/renderer/src/components/app/Navbar.tsx @@ -1,5 +1,6 @@ -import { isLinux, isWin } from '@renderer/config/constant' +import { isLinux, isMac, isWin } from '@renderer/config/constant' import { useFullscreen } from '@renderer/hooks/useFullscreen' +import { useShowAssistants } from '@renderer/hooks/useStore' import type { FC, HTMLAttributes, PropsWithChildren } from 'react' import styled from 'styled-components' @@ -24,9 +25,10 @@ export const NavbarRight: FC = ({ children, ...props }) => { export const NavbarMain: FC = ({ children, ...props }) => { const isFullscreen = useFullscreen() + const { showAssistants } = useShowAssistants() return ( - + {children} ) @@ -51,7 +53,7 @@ const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>` justify-content: flex-end; ` -const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>` +const NavbarMainContainer = styled.div<{ $isFullscreen: boolean; $showAssistants: boolean }>` flex: 1; display: flex; flex-direction: row; @@ -65,6 +67,7 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>` color: var(--color-text-1); -webkit-app-region: drag; padding: 0 12px; + padding-left: ${({ $showAssistants }) => (isMac && !$showAssistants ? '70px' : '10px')}; ` const NavbarCenterContainer = styled.div` diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx index d52f586df5..cb0aa01ff6 100644 --- a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx +++ b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx @@ -266,7 +266,7 @@ const MainSidebar: FC = () => { key: 'settings', label: t('settings.title'), icon: , - onClick: () => navigate('/settings/provider') + onClick: () => window.api.showSettingsWindow({ defaultTab: 'provider' }) } ] }}> diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx index 6b42f4120f..d3a4eeebba 100644 --- a/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx +++ b/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx @@ -52,8 +52,8 @@ export const Container = styled.div<{ transparent?: boolean }>` width: var(--assistants-width); max-width: var(--assistants-width); border-right: 0.5px solid var(--color-border); - height: var(--main-height); - min-height: var(--main-height); + height: calc(var(--main-height) - 50px); + min-height: calc(var(--main-height) - 50px); background: var(--color-background); padding-top: 10px; margin-top: 50px; diff --git a/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx b/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx index 8aa94d5455..c26c610246 100644 --- a/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx +++ b/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx @@ -1,4 +1,4 @@ -import DragableList from '@renderer/components/DragableList' +import { DraggableList } from '@renderer/components/DraggableList' import MinAppIcon from '@renderer/components/Icons/MinAppIcon' import IndicatorLight from '@renderer/components/IndicatorLight' import { Center } from '@renderer/components/Layout' @@ -84,7 +84,7 @@ const OpenedMinapps: FC = () => { - { // 只更新固定应用的顺序 @@ -144,7 +144,7 @@ const OpenedMinapps: FC = () => { ) }} - + {isEmpty(sortedApps) && (
diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index d524a7bcba..c9609341e3 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -42,6 +42,7 @@ import { findIndex } from 'lodash' import { FC, startTransition, useCallback, useDeferredValue, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' +import { useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' interface TopicsTabProps { @@ -49,12 +50,14 @@ interface TopicsTabProps { style?: React.CSSProperties } -const Topics: FC = ({ searchValue, style }) => { +const Topics: FC = ({ searchValue }) => { const { activeAssistant, activeTopic, setActiveTopic } = useChat() const { assistants } = useAssistants() const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(activeAssistant.id) const { t } = useTranslation() - const { showTopicTime, pinTopicsToTop, topicPosition } = useSettings() + const { showTopicTime, pinTopicsToTop } = useSettings() + const navigate = useNavigate() + const location = useLocation() const topics = useTopicsForAssistant(activeAssistant.id) @@ -173,9 +176,13 @@ const Topics: FC = ({ searchValue, style }) => { // await modelGenerating() startTransition(() => { setActiveTopic(topic) + // 如果当前不在聊天页面,导航到聊天页面 + if (location.pathname !== '/') { + navigate('/') + } }) }, - [setActiveTopic] + [setActiveTopic, location.pathname, navigate] ) const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) diff --git a/src/renderer/src/windows/settings/SettingsWindowApp.tsx b/src/renderer/src/windows/settings/SettingsWindowApp.tsx new file mode 100644 index 0000000000..802efc36e6 --- /dev/null +++ b/src/renderer/src/windows/settings/SettingsWindowApp.tsx @@ -0,0 +1,262 @@ +import '@renderer/i18n' +import '@renderer/databases' + +import StyleSheetManager from '@renderer/context/StyleSheetManager' +import { useSettings } from '@renderer/hooks/useSettings' +import AboutSettings from '@renderer/pages/settings/AboutSettings' +import DataSettings from '@renderer/pages/settings/DataSettings/DataSettings' +import DisplaySettings from '@renderer/pages/settings/DisplaySettings/DisplaySettings' +import GeneralSettings from '@renderer/pages/settings/GeneralSettings' +import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings' +import ProvidersList from '@renderer/pages/settings/ProviderSettings' +import QuickAssistantSettings from '@renderer/pages/settings/QuickAssistantSettings' +import QuickPhraseSettings from '@renderer/pages/settings/QuickPhraseSettings' +import SelectionAssistantSettings from '@renderer/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings' +import ShortcutSettings from '@renderer/pages/settings/ShortcutSettings' +import ToolSettings from '@renderer/pages/settings/ToolSettings' +import { Spin } from 'antd' +import { + Cloud, + Command, + HardDrive, + Info, + MonitorCog, + Package, + PencilRuler, + Rocket, + Settings2, + TextCursorInput, + Zap +} from 'lucide-react' +import React, { Suspense, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +type SettingsTab = + | 'provider' + | 'model' + | 'tool' + | 'general' + | 'display' + | 'shortcut' + | 'quickAssistant' + | 'selectionAssistant' + | 'data' + | 'about' + | 'quickPhrase' + +// Inner component that uses hooks after Redux is initialized +function SettingsWindowContent(): React.ReactElement { + const { t } = useTranslation() + const { customCss } = useSettings() + const [activeTab, setActiveTab] = useState('provider') + + // Remove spinner after component mounts + useEffect(() => { + document.getElementById('spinner')?.remove() + console.timeEnd('settings-init') + }, []) + + // Handle custom CSS injection + useEffect(() => { + let customCssElement = document.getElementById('user-defined-custom-css') as HTMLStyleElement + if (customCssElement) { + customCssElement.remove() + } + + if (customCss) { + customCssElement = document.createElement('style') + customCssElement.id = 'user-defined-custom-css' + customCssElement.textContent = customCss + document.head.appendChild(customCssElement) + } + }, [customCss]) + + useEffect(() => { + // Parse URL parameters for initial tab + const urlParams = new URLSearchParams(window.location.search) + const tab = urlParams.get('tab') + if (tab && isValidTab(tab)) { + setActiveTab(tab as SettingsTab) + } + }, []) + + const isValidTab = (tab: string): boolean => { + const validTabs = [ + 'provider', + 'model', + 'tool', + 'general', + 'display', + 'shortcut', + 'quickAssistant', + 'selectionAssistant', + 'data', + 'about', + 'quickPhrase' + ] + return validTabs.includes(tab) + } + + const menuItems = [ + { key: 'provider', icon: , label: t('settings.provider.title') }, + { key: 'model', icon: , label: t('settings.model') }, + { key: 'tool', icon: , label: t('settings.tool.title') }, + { key: 'general', icon: , label: t('settings.general') }, + { key: 'display', icon: , label: t('settings.display.title') }, + { key: 'shortcut', icon: , label: t('settings.shortcuts.title') }, + { key: 'quickAssistant', icon: , label: t('settings.quickAssistant.title') }, + { key: 'selectionAssistant', icon: , label: t('selection.name') }, + { key: 'quickPhrase', icon: , label: t('settings.quickPhrase.title') }, + { key: 'data', icon: , label: t('settings.data.title') }, + { key: 'about', icon: , label: t('settings.about') } + ] as const + + const renderContent = () => { + switch (activeTab) { + case 'provider': + return ( + }> + + + ) + case 'model': + return + case 'tool': + return + case 'general': + return + case 'display': + return + case 'shortcut': + return + case 'quickAssistant': + return + case 'selectionAssistant': + return + case 'data': + return + case 'about': + return + case 'quickPhrase': + return + default: + return + } + } + + return ( + + + + {t('settings.title')} + + + + {menuItems.map((item) => ( + setActiveTab(item.key as SettingsTab)}> + {item.icon} + {item.label} + + ))} + + {renderContent()} + + + + ) +} + +const SettingsWindowApp: React.FC = () => { + return +} + +const Container = styled.div` + display: flex; + flex-direction: column; + height: 100vh; + width: 100vw; + background: var(--color-background); +` + +const TitleBar = styled.div` + display: flex; + align-items: center; + height: 40px; + padding: 0 20px; + background: var(--color-background); + border-bottom: 0.5px solid var(--color-border); + -webkit-app-region: drag; + user-select: none; +` + +const Title = styled.h1` + margin: 0; + font-size: 16px; + font-weight: 600; + color: var(--color-text); +` + +const ContentContainer = styled.div` + display: flex; + flex: 1; + height: calc(100vh - 40px); +` + +const SettingMenus = styled.div` + display: flex; + flex-direction: column; + min-width: var(--settings-width); + border-right: 0.5px solid var(--color-border); + padding: 10px; + user-select: none; + background: var(--color-background); +` + +const MenuItem = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + padding: 6px 10px; + width: 100%; + cursor: pointer; + border-radius: var(--list-item-border-radius); + font-weight: 500; + transition: all 0.2s ease-in-out; + border: 0.5px solid transparent; + margin-bottom: 5px; + + .anticon { + font-size: 16px; + opacity: 0.8; + } + + .iconfont { + font-size: 18px; + line-height: 18px; + opacity: 0.7; + margin-left: -1px; + } + + &:hover { + background: var(--color-background-soft); + } + + &.active { + background: var(--color-background-soft); + border: 0.5px solid var(--color-border); + } +` + +const SettingContent = styled.div` + display: flex; + height: 100%; + flex: 1; + overflow: auto; +` + +export default SettingsWindowApp diff --git a/src/renderer/src/windows/settings/entryPoint.tsx b/src/renderer/src/windows/settings/entryPoint.tsx new file mode 100644 index 0000000000..1fc7ea73c6 --- /dev/null +++ b/src/renderer/src/windows/settings/entryPoint.tsx @@ -0,0 +1,79 @@ +import '@renderer/assets/styles/index.scss' +import '@ant-design/v5-patch-for-react-19' + +import KeyvStorage from '@kangfenmao/keyv-storage' +import AntdProvider from '@renderer/context/AntdProvider' +import { CodeStyleProvider } from '@renderer/context/CodeStyleProvider' +import { ThemeProvider } from '@renderer/context/ThemeProvider' +import NavigationService from '@renderer/services/NavigationService' +import storeSyncService from '@renderer/services/StoreSyncService' +import store, { persistor } from '@renderer/store' +import { message, Modal } from 'antd' +import { FC } from 'react' +import { createRoot } from 'react-dom/client' +import { Provider } from 'react-redux' +import { MemoryRouter, useNavigate } from 'react-router-dom' +import { PersistGate } from 'redux-persist/integration/react' + +import SettingsWindowApp from './SettingsWindowApp' + +/** + * This function is required for model API + * eg. BaseProviders.ts + * Although the coupling is too strong, we have no choice but to load it + * In multi-window handling, decoupling is needed + */ +function initKeyv() { + window.keyv = new KeyvStorage() + window.keyv.init() +} + +initKeyv() + +//subscribe to store sync +storeSyncService.subscribe() + +// Navigation wrapper component to set up navigation service +const NavigationWrapper: FC<{ children: React.ReactNode }> = ({ children }) => { + const navigate = useNavigate() + + // Set up navigation service for the settings window + NavigationService.setNavigate(navigate) + + return <>{children} +} + +const App: FC = () => { + // 设置窗口需要显示各种操作的提示消息(如API连接测试、保存成功等) + // messageContextHolder 为 Ant Design 的全局消息提示提供上下文容器 + const [messageApi, messageContextHolder] = message.useMessage() + const [modal, modalContextHolder] = Modal.useModal() + + // Set up global APIs for the settings window + window.message = messageApi + window.modal = modal + + return ( + + + + + + + + {/* 消息提示容器,用于显示设置操作的反馈信息 */} + {messageContextHolder} + {modalContextHolder} + + + + + + + + + ) +} + +const root = createRoot(document.getElementById('root') as HTMLElement) +root.render()