mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-21 16:01:35 +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
|
||||||
Navigation_Url = 'navigation:url',
|
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 { FileServiceManager } from './services/remotefile/FileServiceManager'
|
||||||
import { searchService } from './services/SearchService'
|
import { searchService } from './services/SearchService'
|
||||||
import { SelectionService } from './services/SelectionService'
|
import { SelectionService } from './services/SelectionService'
|
||||||
|
import { SettingsWindowService } from './services/SettingsWindowService'
|
||||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||||
import storeSyncService from './services/StoreSyncService'
|
import storeSyncService from './services/StoreSyncService'
|
||||||
import { themeService } from './services/ThemeService'
|
import { themeService } from './services/ThemeService'
|
||||||
@ -583,4 +584,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.Navigation_Url, (_, url: string) => {
|
ipcMain.handle(IpcChannel.Navigation_Url, (_, url: string) => {
|
||||||
CacheService.set('navigation-url', url)
|
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 { configManager } from './ConfigManager'
|
||||||
import selectionService from './SelectionService'
|
import selectionService from './SelectionService'
|
||||||
|
import { settingsWindowService } from './SettingsWindowService'
|
||||||
import { windowService } from './WindowService'
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
let showAppAccelerator: string | null = null
|
let showAppAccelerator: string | null = null
|
||||||
let showMiniWindowAccelerator: string | null = null
|
let showMiniWindowAccelerator: string | null = null
|
||||||
|
let showSettingsAccelerator: string | null = null
|
||||||
let selectionAssistantToggleAccelerator: string | null = null
|
let selectionAssistantToggleAccelerator: string | null = null
|
||||||
let selectionAssistantSelectTextAccelerator: string | null = null
|
let selectionAssistantSelectTextAccelerator: string | null = null
|
||||||
|
|
||||||
@ -26,6 +28,10 @@ function getShortcutHandler(shortcut: Shortcut) {
|
|||||||
return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
|
return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
|
||||||
case 'zoom_reset':
|
case 'zoom_reset':
|
||||||
return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
|
return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
|
||||||
|
case 'show_settings':
|
||||||
|
return () => {
|
||||||
|
settingsWindowService.showSettingsWindow()
|
||||||
|
}
|
||||||
case 'show_app':
|
case 'show_app':
|
||||||
return () => {
|
return () => {
|
||||||
windowService.toggleMainWindow()
|
windowService.toggleMainWindow()
|
||||||
@ -146,9 +152,13 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
// only register universal shortcuts when needed
|
// only register universal shortcuts when needed
|
||||||
if (
|
if (
|
||||||
onlyUniversalShortcuts &&
|
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
|
return
|
||||||
}
|
}
|
||||||
@ -171,6 +181,10 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
|
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'show_settings':
|
||||||
|
showSettingsAccelerator = formatShortcutKey(shortcut.shortcut)
|
||||||
|
break
|
||||||
|
|
||||||
case 'selection_assistant_toggle':
|
case 'selection_assistant_toggle':
|
||||||
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
|
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
|
||||||
break
|
break
|
||||||
@ -222,6 +236,12 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
handler && globalShortcut.register(accelerator, () => handler(window))
|
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) {
|
if (selectionAssistantToggleAccelerator) {
|
||||||
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
|
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
|
||||||
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
|
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
|
||||||
@ -258,6 +278,7 @@ export function unregisterAllShortcuts() {
|
|||||||
try {
|
try {
|
||||||
showAppAccelerator = null
|
showAppAccelerator = null
|
||||||
showMiniWindowAccelerator = null
|
showMiniWindowAccelerator = null
|
||||||
|
showSettingsAccelerator = null
|
||||||
selectionAssistantToggleAccelerator = null
|
selectionAssistantToggleAccelerator = null
|
||||||
selectionAssistantSelectTextAccelerator = null
|
selectionAssistantSelectTextAccelerator = null
|
||||||
windowOnHandlers.forEach((handlers, window) => {
|
windowOnHandlers.forEach((handlers, window) => {
|
||||||
|
|||||||
@ -322,7 +322,21 @@ const api = {
|
|||||||
url: (url: string) => ipcRenderer.invoke(IpcChannel.Navigation_Url, url)
|
url: (url: string) => ipcRenderer.invoke(IpcChannel.Navigation_Url, url)
|
||||||
},
|
},
|
||||||
setDisableHardwareAcceleration: (isDisable: boolean) =>
|
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
|
// 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'
|
export interface SettingsPopupShowParams {
|
||||||
import DataSettings from '@renderer/pages/settings/DataSettings/DataSettings'
|
defaultTab?:
|
||||||
import DisplaySettings from '@renderer/pages/settings/DisplaySettings/DisplaySettings'
|
| 'provider'
|
||||||
import GeneralSettings from '@renderer/pages/settings/GeneralSettings'
|
| 'model'
|
||||||
import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings'
|
| 'tool'
|
||||||
import ProvidersList from '@renderer/pages/settings/ProviderSettings'
|
| 'general'
|
||||||
import QuickAssistantSettings from '@renderer/pages/settings/QuickAssistantSettings'
|
| 'display'
|
||||||
import QuickPhraseSettings from '@renderer/pages/settings/QuickPhraseSettings'
|
| 'shortcut'
|
||||||
import SelectionAssistantSettings from '@renderer/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings'
|
| 'quickAssistant'
|
||||||
import ShortcutSettings from '@renderer/pages/settings/ShortcutSettings'
|
| 'selectionAssistant'
|
||||||
import WebSearchSettings from '@renderer/pages/settings/WebSearchSettings'
|
| 'data'
|
||||||
import { Modal, Spin } from 'antd'
|
| 'about'
|
||||||
import {
|
| 'quickPhrase'
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
export default class SettingsPopup {
|
||||||
static hide() {
|
static hide() {
|
||||||
TopView.hide(TopViewKey)
|
// Settings window is now independent, user can close it manually
|
||||||
}
|
}
|
||||||
|
|
||||||
static show(props: SettingsPopupShowParams = {}) {
|
static show(props: SettingsPopupShowParams = {}) {
|
||||||
return new Promise<any>((resolve) => {
|
return window.api.showSettingsWindow({ defaultTab: props.defaultTab })
|
||||||
TopView.show(<SettingsPopupContainer {...props} resolve={resolve} />, TopViewKey)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||||
|
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||||
import type { FC, HTMLAttributes, PropsWithChildren } from 'react'
|
import type { FC, HTMLAttributes, PropsWithChildren } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -24,9 +25,10 @@ export const NavbarRight: FC<Props> = ({ children, ...props }) => {
|
|||||||
|
|
||||||
export const NavbarMain: FC<Props> = ({ children, ...props }) => {
|
export const NavbarMain: FC<Props> = ({ children, ...props }) => {
|
||||||
const isFullscreen = useFullscreen()
|
const isFullscreen = useFullscreen()
|
||||||
|
const { showAssistants } = useShowAssistants()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
|
<NavbarMainContainer {...props} $isFullscreen={isFullscreen} $showAssistants={showAssistants}>
|
||||||
{children}
|
{children}
|
||||||
</NavbarMainContainer>
|
</NavbarMainContainer>
|
||||||
)
|
)
|
||||||
@ -51,7 +53,7 @@ const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
`
|
`
|
||||||
|
|
||||||
const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
|
const NavbarMainContainer = styled.div<{ $isFullscreen: boolean; $showAssistants: boolean }>`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -65,6 +67,7 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
|
|||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
padding-left: ${({ $showAssistants }) => (isMac && !$showAssistants ? '70px' : '10px')};
|
||||||
`
|
`
|
||||||
|
|
||||||
const NavbarCenterContainer = styled.div`
|
const NavbarCenterContainer = styled.div`
|
||||||
|
|||||||
@ -266,7 +266,7 @@ const MainSidebar: FC = () => {
|
|||||||
key: 'settings',
|
key: 'settings',
|
||||||
label: t('settings.title'),
|
label: t('settings.title'),
|
||||||
icon: <Settings size={16} className="icon" />,
|
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);
|
width: var(--assistants-width);
|
||||||
max-width: var(--assistants-width);
|
max-width: var(--assistants-width);
|
||||||
border-right: 0.5px solid var(--color-border);
|
border-right: 0.5px solid var(--color-border);
|
||||||
height: var(--main-height);
|
height: calc(var(--main-height) - 50px);
|
||||||
min-height: var(--main-height);
|
min-height: calc(var(--main-height) - 50px);
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
margin-top: 50px;
|
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 MinAppIcon from '@renderer/components/Icons/MinAppIcon'
|
||||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||||
import { Center } from '@renderer/components/Layout'
|
import { Center } from '@renderer/components/Layout'
|
||||||
@ -84,7 +84,7 @@ const OpenedMinapps: FC = () => {
|
|||||||
<TabsContainer className="TabsContainer" style={{ marginBottom: 4 }}>
|
<TabsContainer className="TabsContainer" style={{ marginBottom: 4 }}>
|
||||||
<Divider />
|
<Divider />
|
||||||
<TabsWrapper>
|
<TabsWrapper>
|
||||||
<DragableList
|
<DraggableList
|
||||||
list={sortedApps}
|
list={sortedApps}
|
||||||
onUpdate={(newList) => {
|
onUpdate={(newList) => {
|
||||||
// 只更新固定应用的顺序
|
// 只更新固定应用的顺序
|
||||||
@ -144,7 +144,7 @@ const OpenedMinapps: FC = () => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</DragableList>
|
</DraggableList>
|
||||||
{isEmpty(sortedApps) && (
|
{isEmpty(sortedApps) && (
|
||||||
<Center>
|
<Center>
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
<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 { FC, startTransition, useCallback, useDeferredValue, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface TopicsTabProps {
|
interface TopicsTabProps {
|
||||||
@ -49,12 +50,14 @@ interface TopicsTabProps {
|
|||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
const Topics: FC<TopicsTabProps> = ({ searchValue, style }) => {
|
const Topics: FC<TopicsTabProps> = ({ searchValue }) => {
|
||||||
const { activeAssistant, activeTopic, setActiveTopic } = useChat()
|
const { activeAssistant, activeTopic, setActiveTopic } = useChat()
|
||||||
const { assistants } = useAssistants()
|
const { assistants } = useAssistants()
|
||||||
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(activeAssistant.id)
|
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(activeAssistant.id)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { showTopicTime, pinTopicsToTop, topicPosition } = useSettings()
|
const { showTopicTime, pinTopicsToTop } = useSettings()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
const topics = useTopicsForAssistant(activeAssistant.id)
|
const topics = useTopicsForAssistant(activeAssistant.id)
|
||||||
|
|
||||||
@ -173,9 +176,13 @@ const Topics: FC<TopicsTabProps> = ({ searchValue, style }) => {
|
|||||||
// await modelGenerating()
|
// await modelGenerating()
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setActiveTopic(topic)
|
setActiveTopic(topic)
|
||||||
|
// 如果当前不在聊天页面,导航到聊天页面
|
||||||
|
if (location.pathname !== '/') {
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[setActiveTopic]
|
[setActiveTopic, location.pathname, navigate]
|
||||||
)
|
)
|
||||||
|
|
||||||
const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions)
|
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