mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
feat: add settings window functionality and shortcuts
- Introduced a new IPC channel for showing the settings window. - Registered IPC handler for the settings window in the main process. - Updated shortcut service to include a shortcut for opening the settings window. - Modified the settings popup to use the new IPC method for displaying the settings. - Adjusted the main sidebar to navigate to the settings window using the new IPC call. - Enhanced the navbar to accommodate the new settings window functionality.
This commit is contained in:
parent
762732af9d
commit
e4434eb7c8
@ -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'
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
149
src/main/services/SettingsWindowService.ts
Normal file
149
src/main/services/SettingsWindowService.ts
Normal file
@ -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()
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
|
||||
23
src/renderer/settingsWindow.html
Normal file
23
src/renderer/settingsWindow.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<title>Cherry Studio</title>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/windows/settings/entryPoint.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -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<Props> = ({ defaultTab = 'provider', resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>(defaultTab)
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
const menuItems = [
|
||||
{ key: 'provider', icon: <Cloud size={18} />, label: t('settings.provider.title') },
|
||||
{ key: 'model', icon: <Package size={18} />, label: t('settings.model') },
|
||||
{ key: 'web-search', icon: <Globe size={18} />, label: t('settings.websearch.title') },
|
||||
{ key: 'general', icon: <Settings2 size={18} />, label: t('settings.general') },
|
||||
{ key: 'display', icon: <MonitorCog size={18} />, label: t('settings.display.title') },
|
||||
{ key: 'shortcut', icon: <Command size={18} />, label: t('settings.shortcuts.title') },
|
||||
{ key: 'quickAssistant', icon: <Rocket size={18} />, label: t('settings.quickAssistant.title') },
|
||||
{ key: 'selectionAssistant', icon: <TextCursorInput size={18} />, label: t('selection.name') },
|
||||
{ key: 'quickPhrase', icon: <Zap size={18} />, label: t('settings.quickPhrase.title') },
|
||||
{ key: 'data', icon: <HardDrive size={18} />, label: t('settings.data.title') },
|
||||
{ key: 'about', icon: <Info size={18} />, label: t('settings.about') }
|
||||
] as const
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'provider':
|
||||
return (
|
||||
<Suspense fallback={<Spin />}>
|
||||
<ProvidersList />
|
||||
</Suspense>
|
||||
)
|
||||
case 'model':
|
||||
return <ModelSettings />
|
||||
case 'web-search':
|
||||
return <WebSearchSettings />
|
||||
case 'general':
|
||||
return <GeneralSettings />
|
||||
case 'display':
|
||||
return <DisplaySettings />
|
||||
case 'shortcut':
|
||||
return <ShortcutSettings />
|
||||
case 'quickAssistant':
|
||||
return <QuickAssistantSettings />
|
||||
case 'selectionAssistant':
|
||||
return <SelectionAssistantSettings />
|
||||
case 'data':
|
||||
return <DataSettings />
|
||||
case 'about':
|
||||
return <AboutSettings />
|
||||
case 'quickPhrase':
|
||||
return <QuickPhraseSettings />
|
||||
default:
|
||||
return <ProvidersList />
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onAfterClose = () => {
|
||||
resolve && resolve(null)
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
|
||||
// 设置全局隐藏方法
|
||||
SettingsPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
title={t('settings.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onAfterClose}
|
||||
footer={null}
|
||||
width={1000}
|
||||
centered
|
||||
destroyOnClose>
|
||||
<ContentContainer>
|
||||
<SettingMenus>
|
||||
{menuItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.key}
|
||||
className={activeTab === item.key ? 'active' : ''}
|
||||
onClick={() => setActiveTab(item.key as SettingsTab)}>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SettingMenus>
|
||||
<SettingContent>{renderContent()}</SettingContent>
|
||||
</ContentContainer>
|
||||
</StyledModal>
|
||||
)
|
||||
}
|
||||
|
||||
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<any>((resolve) => {
|
||||
TopView.show(<SettingsPopupContainer {...props} resolve={resolve} />, 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;
|
||||
`
|
||||
|
||||
@ -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<Props> = ({ children, ...props }) => {
|
||||
|
||||
export const NavbarMain: FC<Props> = ({ children, ...props }) => {
|
||||
const isFullscreen = useFullscreen()
|
||||
const { showAssistants } = useShowAssistants()
|
||||
|
||||
return (
|
||||
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
|
||||
<NavbarMainContainer {...props} $isFullscreen={isFullscreen} $showAssistants={showAssistants}>
|
||||
{children}
|
||||
</NavbarMainContainer>
|
||||
)
|
||||
@ -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`
|
||||
|
||||
@ -266,7 +266,7 @@ const MainSidebar: FC = () => {
|
||||
key: 'settings',
|
||||
label: t('settings.title'),
|
||||
icon: <Settings size={16} className="icon" />,
|
||||
onClick: () => navigate('/settings/provider')
|
||||
onClick: () => window.api.showSettingsWindow({ defaultTab: 'provider' })
|
||||
}
|
||||
]
|
||||
}}>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = () => {
|
||||
<TabsContainer className="TabsContainer" style={{ marginBottom: 4 }}>
|
||||
<Divider />
|
||||
<TabsWrapper>
|
||||
<DragableList
|
||||
<DraggableList
|
||||
list={sortedApps}
|
||||
onUpdate={(newList) => {
|
||||
// 只更新固定应用的顺序
|
||||
@ -144,7 +144,7 @@ const OpenedMinapps: FC = () => {
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
</DragableList>
|
||||
</DraggableList>
|
||||
{isEmpty(sortedApps) && (
|
||||
<Center>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
|
||||
@ -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<TopicsTabProps> = ({ searchValue, style }) => {
|
||||
const Topics: FC<TopicsTabProps> = ({ 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<TopicsTabProps> = ({ searchValue, style }) => {
|
||||
// await modelGenerating()
|
||||
startTransition(() => {
|
||||
setActiveTopic(topic)
|
||||
// 如果当前不在聊天页面,导航到聊天页面
|
||||
if (location.pathname !== '/') {
|
||||
navigate('/')
|
||||
}
|
||||
})
|
||||
},
|
||||
[setActiveTopic]
|
||||
[setActiveTopic, location.pathname, navigate]
|
||||
)
|
||||
|
||||
const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions)
|
||||
|
||||
262
src/renderer/src/windows/settings/SettingsWindowApp.tsx
Normal file
262
src/renderer/src/windows/settings/SettingsWindowApp.tsx
Normal file
@ -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<SettingsTab>('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: <Cloud size={18} />, label: t('settings.provider.title') },
|
||||
{ key: 'model', icon: <Package size={18} />, label: t('settings.model') },
|
||||
{ key: 'tool', icon: <PencilRuler size={18} />, label: t('settings.tool.title') },
|
||||
{ key: 'general', icon: <Settings2 size={18} />, label: t('settings.general') },
|
||||
{ key: 'display', icon: <MonitorCog size={18} />, label: t('settings.display.title') },
|
||||
{ key: 'shortcut', icon: <Command size={18} />, label: t('settings.shortcuts.title') },
|
||||
{ key: 'quickAssistant', icon: <Rocket size={18} />, label: t('settings.quickAssistant.title') },
|
||||
{ key: 'selectionAssistant', icon: <TextCursorInput size={18} />, label: t('selection.name') },
|
||||
{ key: 'quickPhrase', icon: <Zap size={18} />, label: t('settings.quickPhrase.title') },
|
||||
{ key: 'data', icon: <HardDrive size={18} />, label: t('settings.data.title') },
|
||||
{ key: 'about', icon: <Info size={18} />, label: t('settings.about') }
|
||||
] as const
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'provider':
|
||||
return (
|
||||
<Suspense fallback={<Spin />}>
|
||||
<ProvidersList />
|
||||
</Suspense>
|
||||
)
|
||||
case 'model':
|
||||
return <ModelSettings />
|
||||
case 'tool':
|
||||
return <ToolSettings />
|
||||
case 'general':
|
||||
return <GeneralSettings />
|
||||
case 'display':
|
||||
return <DisplaySettings />
|
||||
case 'shortcut':
|
||||
return <ShortcutSettings />
|
||||
case 'quickAssistant':
|
||||
return <QuickAssistantSettings />
|
||||
case 'selectionAssistant':
|
||||
return <SelectionAssistantSettings />
|
||||
case 'data':
|
||||
return <DataSettings />
|
||||
case 'about':
|
||||
return <AboutSettings />
|
||||
case 'quickPhrase':
|
||||
return <QuickPhraseSettings />
|
||||
default:
|
||||
return <ProvidersList />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyleSheetManager>
|
||||
<Container>
|
||||
<TitleBar>
|
||||
<Title>{t('settings.title')}</Title>
|
||||
</TitleBar>
|
||||
<ContentContainer>
|
||||
<SettingMenus>
|
||||
{menuItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.key}
|
||||
className={activeTab === item.key ? 'active' : ''}
|
||||
onClick={() => setActiveTab(item.key as SettingsTab)}>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SettingMenus>
|
||||
<SettingContent>{renderContent()}</SettingContent>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
</StyleSheetManager>
|
||||
)
|
||||
}
|
||||
|
||||
const SettingsWindowApp: React.FC = () => {
|
||||
return <SettingsWindowContent />
|
||||
}
|
||||
|
||||
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
|
||||
79
src/renderer/src/windows/settings/entryPoint.tsx
Normal file
79
src/renderer/src/windows/settings/entryPoint.tsx
Normal file
@ -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 (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<CodeStyleProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<MemoryRouter initialEntries={['/settings/provider']}>
|
||||
<NavigationWrapper>
|
||||
{/* 消息提示容器,用于显示设置操作的反馈信息 */}
|
||||
{messageContextHolder}
|
||||
{modalContextHolder}
|
||||
<SettingsWindowApp />
|
||||
</NavigationWrapper>
|
||||
</MemoryRouter>
|
||||
</PersistGate>
|
||||
</CodeStyleProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement)
|
||||
root.render(<App />)
|
||||
Loading…
Reference in New Issue
Block a user