feat: add shortcut feature
This commit is contained in:
parent
744a6ac7cb
commit
7dacd58821
@ -23,7 +23,7 @@ export default defineConfig({
|
||||
},
|
||||
plugins: [react()],
|
||||
optimizeDeps: {
|
||||
exclude: ['chunk-KNVOMWSO.js']
|
||||
exclude: ['chunk-KNVOMWSO.js', 'chunk-2NJP6ETL.js']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -96,6 +96,7 @@
|
||||
"prettier": "^3.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-redux": "^9.1.2",
|
||||
|
||||
@ -3,7 +3,7 @@ import { app, BrowserWindow } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
|
||||
import { registerIpc } from './ipc'
|
||||
import { registerZoomShortcut } from './services/ShortcutService'
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { updateUserDataPath } from './utils/upgrade'
|
||||
@ -35,7 +35,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
}
|
||||
})
|
||||
|
||||
registerZoomShortcut(mainWindow)
|
||||
registerShortcuts(mainWindow)
|
||||
|
||||
registerIpc(mainWindow, app)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { ThemeMode } from '@types'
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
@ -11,6 +11,7 @@ import BackupManager from './services/BackupManager'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { ExportService } from './services/ExportService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
|
||||
@ -133,4 +134,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('open:path', async (_, path: string) => {
|
||||
await shell.openPath(path)
|
||||
})
|
||||
|
||||
// shortcuts
|
||||
ipcMain.handle('shortcuts:update', (_, shortcuts: Shortcut[]) => {
|
||||
configManager.setShortcuts(shortcuts)
|
||||
// Refresh shortcuts registration
|
||||
if (mainWindow) {
|
||||
unregisterAllShortcuts()
|
||||
registerShortcuts(mainWindow)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -70,6 +70,15 @@ export class ConfigManager {
|
||||
subscribers.forEach((subscriber) => subscriber(newValue))
|
||||
}
|
||||
}
|
||||
|
||||
getShortcuts() {
|
||||
return this.store.get('shortcuts') as Shortcut[] | undefined
|
||||
}
|
||||
|
||||
setShortcuts(shortcuts: Shortcut[]) {
|
||||
this.store.set('shortcuts', shortcuts)
|
||||
this.notifySubscribers('shortcuts', shortcuts)
|
||||
}
|
||||
}
|
||||
|
||||
export const configManager = new ConfigManager()
|
||||
|
||||
@ -1,70 +1,103 @@
|
||||
import { Shortcut } from '@types'
|
||||
import { BrowserWindow, globalShortcut } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { configManager } from './ConfigManager'
|
||||
|
||||
export function registerZoomShortcut(mainWindow: BrowserWindow) {
|
||||
// 初始化缩放值
|
||||
const initialZoom = configManager.getZoomFactor()
|
||||
mainWindow.webContents.setZoomFactor(initialZoom)
|
||||
let showAppAccelerator: string | null = null
|
||||
|
||||
const handleZoom = (delta: number) => {
|
||||
if (mainWindow) {
|
||||
const currentZoom = mainWindow.webContents.getZoomFactor()
|
||||
function getShortcutHandler(shortcut: Shortcut) {
|
||||
switch (shortcut.key) {
|
||||
case 'zoom_in':
|
||||
return () => handleZoom(0.1)
|
||||
case 'zoom_out':
|
||||
return () => handleZoom(-0.1)
|
||||
case 'zoom_reset':
|
||||
return (window: BrowserWindow) => {
|
||||
window.webContents.setZoomFactor(1)
|
||||
configManager.setZoomFactor(1)
|
||||
}
|
||||
case 'show_app':
|
||||
return (window: BrowserWindow) => {
|
||||
if (window.isVisible()) {
|
||||
window.hide()
|
||||
} else {
|
||||
window.show()
|
||||
window.focus()
|
||||
}
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function formatShortcutKey(shortcut: string[]): string {
|
||||
return shortcut.join('+')
|
||||
}
|
||||
|
||||
function handleZoom(delta: number) {
|
||||
return (window: BrowserWindow) => {
|
||||
const currentZoom = window.webContents.getZoomFactor()
|
||||
const newZoom = currentZoom + delta
|
||||
if (newZoom >= 0.1 && newZoom <= 5.0) {
|
||||
mainWindow.webContents.setZoomFactor(newZoom)
|
||||
window.webContents.setZoomFactor(newZoom)
|
||||
configManager.setZoomFactor(newZoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerWindowShortcuts(window: BrowserWindow) {
|
||||
window.webContents.setZoomFactor(configManager.getZoomFactor())
|
||||
|
||||
const register = () => {
|
||||
if (window.isDestroyed()) return
|
||||
|
||||
const shortcuts = configManager.getShortcuts()
|
||||
if (!shortcuts) return
|
||||
|
||||
shortcuts.forEach((shortcut) => {
|
||||
if (!shortcut.enabled || shortcut.shortcut.length === 0) return
|
||||
|
||||
const handler = getShortcutHandler(shortcut)
|
||||
if (!handler) return
|
||||
|
||||
const accelerator = formatShortcutKey(shortcut.shortcut)
|
||||
|
||||
if (shortcut.key === 'show_app') {
|
||||
showAppAccelerator = accelerator
|
||||
}
|
||||
|
||||
const registerShortcuts = () => {
|
||||
// 放大快捷键
|
||||
globalShortcut.register('CommandOrControl+=', () => handleZoom(0.1))
|
||||
globalShortcut.register('CommandOrControl+numadd', () => handleZoom(0.1))
|
||||
|
||||
// 缩小快捷键
|
||||
globalShortcut.register('CommandOrControl+-', () => handleZoom(-0.1))
|
||||
globalShortcut.register('CommandOrControl+numsub', () => handleZoom(-0.1))
|
||||
|
||||
// 重置快捷键
|
||||
globalShortcut.register('CommandOrControl+0', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.setZoomFactor(1)
|
||||
configManager.setZoomFactor(1)
|
||||
}
|
||||
Logger.info(`Register shortcut: ${accelerator}`)
|
||||
globalShortcut.register(accelerator, () => handler(window))
|
||||
})
|
||||
}
|
||||
|
||||
const unregisterShortcuts = () => {
|
||||
globalShortcut.unregister('CommandOrControl+=')
|
||||
globalShortcut.unregister('CommandOrControl+numadd')
|
||||
globalShortcut.unregister('CommandOrControl+-')
|
||||
globalShortcut.unregister('CommandOrControl+numsub')
|
||||
globalShortcut.unregister('CommandOrControl+0')
|
||||
const unregister = () => {
|
||||
if (window.isDestroyed()) return
|
||||
|
||||
globalShortcut.unregisterAll()
|
||||
|
||||
if (showAppAccelerator) {
|
||||
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
||||
if (handler) {
|
||||
globalShortcut.register(showAppAccelerator, () => handler(window))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add check for window destruction
|
||||
if (mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
window.on('focus', () => register())
|
||||
window.on('blur', () => unregister())
|
||||
|
||||
// When window gains focus, register shortcuts
|
||||
mainWindow.on('focus', () => {
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
registerShortcuts()
|
||||
}
|
||||
})
|
||||
|
||||
// When window loses focus, unregister shortcuts
|
||||
mainWindow.on('blur', () => {
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
unregisterShortcuts()
|
||||
}
|
||||
})
|
||||
|
||||
// Initial registration (if window is already focused)
|
||||
if (!mainWindow.isDestroyed() && mainWindow.isFocused()) {
|
||||
registerShortcuts()
|
||||
if (!window.isDestroyed() && window.isFocused()) {
|
||||
register()
|
||||
}
|
||||
}
|
||||
|
||||
export function registerShortcuts(mainWindow: BrowserWindow) {
|
||||
registerWindowShortcuts(mainWindow)
|
||||
}
|
||||
|
||||
export function unregisterAllShortcuts() {
|
||||
showAppAccelerator = null
|
||||
globalShortcut.unregisterAll()
|
||||
}
|
||||
|
||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@ -54,6 +54,9 @@ declare global {
|
||||
toWord: (markdown: string, fileName: string) => Promise<void>
|
||||
}
|
||||
openPath: (path: string) => Promise<void>
|
||||
shortcuts: {
|
||||
update: (shortcuts: Shortcut[]) => Promise<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { WebDavConfig } from '@types'
|
||||
import { Shortcut, WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
@ -47,7 +47,10 @@ const api = {
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName)
|
||||
},
|
||||
openPath: (path: string) => ipcRenderer.invoke('open:path', path)
|
||||
openPath: (path: string) => ipcRenderer.invoke('open:path', path),
|
||||
shortcuts: {
|
||||
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke('shortcuts:update', shortcuts)
|
||||
}
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
55
src/renderer/src/hooks/useShortcuts.ts
Normal file
55
src/renderer/src/hooks/useShortcuts.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { useCallback } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
interface UseShortcutOptions {
|
||||
preventDefault?: boolean
|
||||
enableOnFormTags?: boolean
|
||||
enabled?: boolean
|
||||
description?: string
|
||||
}
|
||||
|
||||
const defaultOptions: UseShortcutOptions = {
|
||||
preventDefault: true,
|
||||
enableOnFormTags: true,
|
||||
enabled: true
|
||||
}
|
||||
|
||||
export const useShortcut = (
|
||||
shortcutKey: string,
|
||||
callback: (e: KeyboardEvent) => void,
|
||||
options: UseShortcutOptions = defaultOptions
|
||||
) => {
|
||||
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
|
||||
|
||||
const formatShortcut = useCallback((shortcut: string[]) => {
|
||||
return shortcut
|
||||
.map((key) => {
|
||||
switch (key.toLowerCase()) {
|
||||
case 'command':
|
||||
return 'meta'
|
||||
default:
|
||||
return key.toLowerCase()
|
||||
}
|
||||
})
|
||||
.join('+')
|
||||
}, [])
|
||||
|
||||
const shortcutConfig = shortcuts.find((s) => s.key === shortcutKey)
|
||||
|
||||
useHotkeys(
|
||||
shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : '',
|
||||
(e) => {
|
||||
if (options.preventDefault) {
|
||||
e.preventDefault()
|
||||
}
|
||||
if (options.enabled !== false) {
|
||||
callback(e)
|
||||
}
|
||||
},
|
||||
{
|
||||
enableOnFormTags: options.enableOnFormTags,
|
||||
description: options.description || shortcutConfig?.name
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -142,7 +142,9 @@
|
||||
"select": "Select",
|
||||
"topics": "Topics",
|
||||
"warning": "Warning",
|
||||
"you": "You"
|
||||
"you": "You",
|
||||
"clear": "Clear",
|
||||
"add": "Add"
|
||||
},
|
||||
"error": {
|
||||
"backup.file_format": "Backup file format error",
|
||||
@ -451,7 +453,10 @@
|
||||
"title": "Keyboard Shortcuts",
|
||||
"zoom_in": "Zoom In",
|
||||
"zoom_out": "Zoom Out",
|
||||
"zoom_reset": "Reset Zoom"
|
||||
"zoom_reset": "Reset Zoom",
|
||||
"show_app": "Show App",
|
||||
"reset_defaults": "Reset Defaults",
|
||||
"press_shortcut": "Press Shortcut"
|
||||
},
|
||||
"theme.auto": "Auto",
|
||||
"theme.dark": "Dark",
|
||||
|
||||
@ -142,7 +142,9 @@
|
||||
"select": "Выбрать",
|
||||
"topics": "Топики",
|
||||
"warning": "Предупреждение",
|
||||
"you": "Вы"
|
||||
"you": "Вы",
|
||||
"clear": "Очистить",
|
||||
"add": "Добавить"
|
||||
},
|
||||
"error": {
|
||||
"backup.file_format": "Ошибка формата файла резервной копии",
|
||||
@ -451,7 +453,10 @@
|
||||
"title": "Горячие клавиши",
|
||||
"zoom_in": "Увеличить",
|
||||
"zoom_out": "Уменьшить",
|
||||
"zoom_reset": "Сбросить масштаб"
|
||||
"zoom_reset": "Сбросить масштаб",
|
||||
"show_app": "Показать приложение",
|
||||
"reset_defaults": "Сбросить настройки по умолчанию",
|
||||
"press_shortcut": "Нажмите сочетание клавиш"
|
||||
},
|
||||
"theme.auto": "Автоматически",
|
||||
"theme.dark": "Темная",
|
||||
|
||||
@ -142,7 +142,9 @@
|
||||
"select": "选择",
|
||||
"topics": "话题",
|
||||
"warning": "警告",
|
||||
"you": "用户"
|
||||
"you": "用户",
|
||||
"clear": "清除",
|
||||
"add": "添加"
|
||||
},
|
||||
"error": {
|
||||
"backup.file_format": "备份文件格式错误",
|
||||
@ -436,10 +438,13 @@
|
||||
"action": "操作",
|
||||
"key": "按键",
|
||||
"new_topic": "新建话题",
|
||||
"title": "快捷方式",
|
||||
"title": "快捷键",
|
||||
"zoom_in": "放大界面",
|
||||
"zoom_out": "缩小界面",
|
||||
"zoom_reset": "重置缩放"
|
||||
"zoom_reset": "重置缩放",
|
||||
"show_app": "显示应用",
|
||||
"reset_defaults": "重置默认快捷键",
|
||||
"press_shortcut": "按下快捷键"
|
||||
},
|
||||
"theme.auto": "跟随系统",
|
||||
"theme.dark": "深色主题",
|
||||
|
||||
@ -142,7 +142,9 @@
|
||||
"select": "選擇",
|
||||
"topics": "話題",
|
||||
"warning": "警告",
|
||||
"you": "您"
|
||||
"you": "您",
|
||||
"clear": "清除",
|
||||
"add": "添加"
|
||||
},
|
||||
"error": {
|
||||
"backup.file_format": "備份文件格式錯誤",
|
||||
@ -439,7 +441,10 @@
|
||||
"title": "快速方式",
|
||||
"zoom_in": "放大界面",
|
||||
"zoom_out": "縮小界面",
|
||||
"zoom_reset": "重置縮放"
|
||||
"zoom_reset": "重置縮放",
|
||||
"show_app": "顯示應用",
|
||||
"reset_defaults": "重置預設快捷鍵",
|
||||
"press_shortcut": "按下快捷鍵"
|
||||
},
|
||||
"theme.auto": "自動",
|
||||
"theme.dark": "深色主題",
|
||||
|
||||
@ -15,6 +15,7 @@ import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
@ -316,20 +317,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
}
|
||||
|
||||
// Command or Ctrl + N create new topic
|
||||
useEffect(() => {
|
||||
const onKeydown = (e) => {
|
||||
useShortcut('new_topic', () => {
|
||||
if (!generating) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||||
addNewTopic()
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
textareaRef.current?.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', onKeydown)
|
||||
return () => document.removeEventListener('keydown', onKeydown)
|
||||
}, [addNewTopic, generating])
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { UndoOutlined } from '@ant-design/icons'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { Switch, Table as AntTable, Tag } from 'antd'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { initialState, resetShortcuts, updateShortcut } from '@renderer/store/shortcuts'
|
||||
import { Button, Input, InputRef, Table as AntTable } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { FC } from 'react'
|
||||
import { FC, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -11,74 +14,179 @@ import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '.'
|
||||
interface ShortcutItem {
|
||||
key: string
|
||||
name: string
|
||||
shortcut: string
|
||||
shortcut: string[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const ShortcutSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
|
||||
const inputRefs = useRef<Record<string, InputRef>>({})
|
||||
const [editingKey, setEditingKey] = useState<string | null>(null)
|
||||
|
||||
const commandKey = isMac ? '⌘' : 'Ctrl'
|
||||
const handleClear = (record: ShortcutItem) => {
|
||||
dispatch(
|
||||
updateShortcut({
|
||||
...record,
|
||||
shortcut: []
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const handleAddShortcut = (record: ShortcutItem) => {
|
||||
setEditingKey(record.key)
|
||||
setTimeout(() => {
|
||||
inputRefs.current[record.key]?.focus()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const isShortcutModified = (record: ShortcutItem) => {
|
||||
const defaultShortcut = initialState.shortcuts.find((s) => s.key === record.key)
|
||||
return defaultShortcut?.shortcut.join('+') !== record.shortcut.join('+')
|
||||
}
|
||||
|
||||
const handleResetShortcut = (record: ShortcutItem) => {
|
||||
const defaultShortcut = initialState.shortcuts.find((s) => s.key === record.key)
|
||||
if (defaultShortcut) {
|
||||
dispatch(
|
||||
updateShortcut({
|
||||
...record,
|
||||
shortcut: defaultShortcut.shortcut
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const isValidShortcut = (keys: string[]): boolean => {
|
||||
const hasModifier = keys.some((key) => ['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
|
||||
const hasNonModifier = keys.some((key) => !['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
|
||||
return hasModifier && hasNonModifier && keys.length >= 2
|
||||
}
|
||||
|
||||
const isDuplicateShortcut = (newShortcut: string[], currentKey: string): boolean => {
|
||||
return shortcuts.some(
|
||||
(s) => s.key !== currentKey && s.shortcut.length > 0 && s.shortcut.join('+') === newShortcut.join('+')
|
||||
)
|
||||
}
|
||||
|
||||
const formatShortcut = (shortcut: string[]): string => {
|
||||
return shortcut
|
||||
.map((key) => {
|
||||
switch (key) {
|
||||
case 'Control':
|
||||
return isMac ? '⌃' : 'Ctrl'
|
||||
case 'Ctrl':
|
||||
return isMac ? '⌃' : 'Ctrl'
|
||||
case 'Command':
|
||||
return '⌘'
|
||||
case 'Alt':
|
||||
return isMac ? '⌥' : 'Alt'
|
||||
case 'Shift':
|
||||
return isMac ? '⇧' : 'Shift'
|
||||
case ' ':
|
||||
return 'Space'
|
||||
default:
|
||||
return key.charAt(0).toUpperCase() + key.slice(1)
|
||||
}
|
||||
})
|
||||
.join(' + ')
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent, record: ShortcutItem) => {
|
||||
e.preventDefault()
|
||||
|
||||
const keys: string[] = []
|
||||
if (e.ctrlKey) keys.push(isMac ? 'Control' : 'Ctrl')
|
||||
if (e.metaKey) keys.push('Command')
|
||||
if (e.altKey) keys.push('Alt')
|
||||
if (e.shiftKey) keys.push('Shift')
|
||||
|
||||
const key = e.key
|
||||
if (!['Control', 'Alt', 'Shift', 'Meta'].includes(key)) {
|
||||
keys.push(key.toUpperCase())
|
||||
}
|
||||
|
||||
if (!isValidShortcut(keys)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isDuplicateShortcut(keys, record.key)) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(
|
||||
updateShortcut({
|
||||
...record,
|
||||
shortcut: keys
|
||||
})
|
||||
)
|
||||
setEditingKey(null)
|
||||
}
|
||||
|
||||
const columns: ColumnsType<ShortcutItem> = [
|
||||
{
|
||||
title: t('settings.shortcuts.action'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '50%'
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: t('settings.shortcuts.key'),
|
||||
dataIndex: 'shortcut',
|
||||
key: 'shortcut',
|
||||
width: '30%',
|
||||
render: (shortcut: string) => {
|
||||
const keys = shortcut.split(' ').map((key) => key.trim())
|
||||
align: 'right',
|
||||
render: (shortcut: string[], record: ShortcutItem) => {
|
||||
const isEditing = editingKey === record.key
|
||||
|
||||
return (
|
||||
<span>
|
||||
{keys.map((key) => (
|
||||
<Tag key={key} style={{ padding: '2px 8px', fontSize: '13px' }}>
|
||||
<span style={{ fontFamily: 'monospace' }}>{key}</span>
|
||||
</Tag>
|
||||
))}
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<div style={{ position: 'relative', flex: 1 }}>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
ref={(el) => el && (inputRefs.current[record.key] = el)}
|
||||
value={formatShortcut(shortcut)}
|
||||
placeholder={t('settings.shortcuts.press_shortcut')}
|
||||
onKeyDown={(e) => handleKeyDown(e, record)}
|
||||
onBlur={(e) => {
|
||||
const isUndoClick = e.relatedTarget?.closest('.shortcut-undo-icon')
|
||||
if (!isUndoClick) {
|
||||
setEditingKey(null)
|
||||
}
|
||||
}}
|
||||
style={{ width: '120px' }}
|
||||
suffix={
|
||||
isShortcutModified(record) && (
|
||||
<UndoOutlined
|
||||
className="shortcut-undo-icon"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '8px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
cursor: 'pointer',
|
||||
color: '#999'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleResetShortcut(record)
|
||||
setEditingKey(null)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'enabled',
|
||||
width: '20%',
|
||||
align: 'right',
|
||||
render: () => <Switch defaultChecked disabled />
|
||||
/>
|
||||
) : (
|
||||
<div style={{ cursor: 'pointer', padding: '4px 11px' }} onClick={() => handleAddShortcut(record)}>
|
||||
{shortcut.length > 0 ? formatShortcut(shortcut) : t('settings.shortcuts.press_shortcut')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button onClick={() => (shortcut ? handleClear(record) : handleAddShortcut(record))}>
|
||||
{shortcut ? t('common.clear') : t('common.add')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const shortcuts: ShortcutItem[] = [
|
||||
{
|
||||
key: 'new_topic',
|
||||
name: t('settings.shortcuts.new_topic'),
|
||||
shortcut: `${commandKey} N`,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
key: 'zoom_in',
|
||||
name: t('settings.shortcuts.zoom_in'),
|
||||
shortcut: `${commandKey} +`,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
key: 'zoom_out',
|
||||
name: t('settings.shortcuts.zoom_out'),
|
||||
shortcut: `${commandKey} -`,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
key: 'zoom_reset',
|
||||
name: t('settings.shortcuts.zoom_reset'),
|
||||
shortcut: `${commandKey} 0`,
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
|
||||
@ -89,11 +197,15 @@ const ShortcutSettings: FC = () => {
|
||||
<SettingDivider style={{ marginBottom: 0 }} />
|
||||
<Table
|
||||
columns={columns as ColumnsType<unknown>}
|
||||
dataSource={shortcuts}
|
||||
dataSource={shortcuts.map((s) => ({ ...s, name: t(s.name) }))}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
showHeader={false}
|
||||
/>
|
||||
<SettingDivider style={{ marginBottom: 0 }} />
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '16px 0' }}>
|
||||
<Button onClick={() => dispatch(resetShortcuts())}>{t('settings.shortcuts.reset_defaults')}</Button>
|
||||
</div>
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
)
|
||||
|
||||
@ -10,6 +10,7 @@ import migrate from './migrate'
|
||||
import paintings from './paintings'
|
||||
import runtime from './runtime'
|
||||
import settings from './settings'
|
||||
import shortcuts from './shortcuts'
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
assistants,
|
||||
@ -17,7 +18,8 @@ const rootReducer = combineReducers({
|
||||
paintings,
|
||||
llm,
|
||||
settings,
|
||||
runtime
|
||||
runtime,
|
||||
shortcuts
|
||||
})
|
||||
|
||||
const persistedReducer = persistReducer(
|
||||
|
||||
74
src/renderer/src/store/shortcuts.ts
Normal file
74
src/renderer/src/store/shortcuts.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { Shortcut } from '@renderer/types'
|
||||
|
||||
export interface ShortcutsState {
|
||||
shortcuts: Shortcut[]
|
||||
}
|
||||
|
||||
const initialState: ShortcutsState = {
|
||||
shortcuts: [
|
||||
{
|
||||
key: 'new_topic',
|
||||
name: 'settings.shortcuts.new_topic',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'N'],
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
key: 'zoom_in',
|
||||
name: 'settings.shortcuts.zoom_in',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', '='],
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
key: 'zoom_out',
|
||||
name: 'settings.shortcuts.zoom_out',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', '-'],
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
key: 'zoom_reset',
|
||||
name: 'settings.shortcuts.zoom_reset',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', '0'],
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
key: 'show_app',
|
||||
name: 'settings.shortcuts.show_app',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'Shift', 'A'],
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getSerializableShortcuts = (shortcuts: Shortcut[]) => {
|
||||
return shortcuts.map((shortcut) => ({
|
||||
key: shortcut.key,
|
||||
name: shortcut.name,
|
||||
shortcut: [...shortcut.shortcut],
|
||||
enabled: shortcut.enabled
|
||||
}))
|
||||
}
|
||||
|
||||
const shortcutsSlice = createSlice({
|
||||
name: 'shortcuts',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateShortcut: (state, action: PayloadAction<Shortcut>) => {
|
||||
state.shortcuts = state.shortcuts.map((s) => (s.key === action.payload.key ? action.payload : s))
|
||||
window.api.shortcuts.update(getSerializableShortcuts(state.shortcuts))
|
||||
},
|
||||
toggleShortcut: (state, action: PayloadAction<string>) => {
|
||||
state.shortcuts = state.shortcuts.map((s) => (s.key === action.payload ? { ...s, enabled: !s.enabled } : s))
|
||||
window.api.shortcuts.update(getSerializableShortcuts(state.shortcuts))
|
||||
},
|
||||
resetShortcuts: (state) => {
|
||||
state.shortcuts = initialState.shortcuts
|
||||
window.api.shortcuts.update(getSerializableShortcuts(state.shortcuts))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { updateShortcut, toggleShortcut, resetShortcuts } = shortcutsSlice.actions
|
||||
export default shortcutsSlice.reducer
|
||||
export { initialState }
|
||||
@ -162,3 +162,10 @@ export type AppInfo = {
|
||||
filesPath: string
|
||||
logsPath: string
|
||||
}
|
||||
|
||||
export interface Shortcut {
|
||||
key: string
|
||||
name: string
|
||||
shortcut: string[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
11
yarn.lock
11
yarn.lock
@ -2383,6 +2383,7 @@ __metadata:
|
||||
prettier: "npm:^3.2.4"
|
||||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
react-hotkeys-hook: "npm:^4.6.1"
|
||||
react-i18next: "npm:^14.1.2"
|
||||
react-markdown: "npm:^9.0.1"
|
||||
react-redux: "npm:^9.1.2"
|
||||
@ -9955,6 +9956,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-hotkeys-hook@npm:^4.6.1":
|
||||
version: 4.6.1
|
||||
resolution: "react-hotkeys-hook@npm:4.6.1"
|
||||
peerDependencies:
|
||||
react: ">=16.8.1"
|
||||
react-dom: ">=16.8.1"
|
||||
checksum: 10c0/e23916567b0b863831cf7e2ddba591e82988a3212a9e4b818ac7da6d751b7e8b102f2245d8bf7e9526a97766ffd092efdbfbcc457b74564415f5d5f8ab51919e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-i18next@npm:^14.1.2":
|
||||
version: 14.1.3
|
||||
resolution: "react-i18next@npm:14.1.3"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user