refactor: shortcuts feature

This commit is contained in:
kangfenmao 2024-12-02 22:55:56 +08:00
parent cd3c053f81
commit 7f2f3ad88a
24 changed files with 276 additions and 303 deletions

View File

@ -7,8 +7,9 @@ export default defineConfig({
plugins: [externalizeDepsPlugin()],
resolve: {
alias: {
'@main': resolve('src/main'),
'@types': resolve('src/renderer/src/types'),
'@main': resolve('src/main')
'@shared': resolve('packages/shared')
}
}
},
@ -16,14 +17,15 @@ export default defineConfig({
plugins: [externalizeDepsPlugin()]
},
renderer: {
plugins: [react()],
resolve: {
alias: {
'@renderer': resolve('src/renderer/src')
'@renderer': resolve('src/renderer/src'),
'@shared': resolve('packages/shared')
}
},
plugins: [react()],
optimizeDeps: {
exclude: ['chunk-KNVOMWSO.js', 'chunk-2NJP6ETL.js']
exclude: []
}
}
})

View File

@ -0,0 +1,112 @@
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件
'.java' // Java 代码文件
]
export const ZOOM_SHORTCUTS = [
{
key: 'zoom_in',
shortcut: ['CommandOrControl', '='],
editable: false,
enabled: true
},
{
key: 'zoom_out',
shortcut: ['CommandOrControl', '-'],
editable: false,
enabled: true
},
{
key: 'zoom_reset',
shortcut: ['CommandOrControl', '0'],
editable: false,
enabled: true
}
]

View File

@ -1,95 +1,3 @@
export const isMac = process.platform === 'darwin'
export const isWin = process.platform === 'win32'
export const isLinux = process.platform === 'linux'
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件
'.java' // Java 代码文件
]

View File

@ -138,6 +138,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// shortcuts
ipcMain.handle('shortcuts:update', (_, shortcuts: Shortcut[]) => {
configManager.setShortcuts(shortcuts)
log.info('[ipc] shortcuts updated', shortcuts)
// Refresh shortcuts registration
if (mainWindow) {
unregisterAllShortcuts()

View File

@ -1,4 +1,5 @@
import { LanguageVarious, ThemeMode } from '@types'
import { ZOOM_SHORTCUTS } from '@shared/config/constant'
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
import { app } from 'electron'
import Store from 'electron-store'
@ -72,7 +73,7 @@ export class ConfigManager {
}
getShortcuts() {
return this.store.get('shortcuts') as Shortcut[] | undefined
return this.store.get('shortcuts', ZOOM_SHORTCUTS) as Shortcut[] | []
}
setShortcuts(shortcuts: Shortcut[]) {

View File

@ -1,5 +1,5 @@
import { documentExts, imageExts } from '@main/constant'
import { getFileType } from '@main/utils/file'
import { documentExts, imageExts } from '@shared/config/constant'
import { FileType } from '@types'
import * as crypto from 'crypto'
import {

View File

@ -1,6 +1,5 @@
import { Shortcut } from '@types'
import { BrowserWindow, globalShortcut } from 'electron'
import Logger from 'electron-log'
import { configManager } from './ConfigManager'
@ -9,9 +8,9 @@ let showAppAccelerator: string | null = null
function getShortcutHandler(shortcut: Shortcut) {
switch (shortcut.key) {
case 'zoom_in':
return () => handleZoom(0.1)
return (window: BrowserWindow) => handleZoom(0.1)(window)
case 'zoom_out':
return () => handleZoom(-0.1)
return (window: BrowserWindow) => handleZoom(-0.1)(window)
case 'zoom_reset':
return (window: BrowserWindow) => {
window.webContents.setZoomFactor(1)
@ -46,7 +45,7 @@ function handleZoom(delta: number) {
}
}
function registerWindowShortcuts(window: BrowserWindow) {
export function registerShortcuts(window: BrowserWindow) {
window.webContents.setZoomFactor(configManager.getZoomFactor())
const register = () => {
@ -56,10 +55,15 @@ function registerWindowShortcuts(window: BrowserWindow) {
if (!shortcuts) return
shortcuts.forEach((shortcut) => {
if (!shortcut.enabled || shortcut.shortcut.length === 0) return
if (!shortcut.enabled || shortcut.shortcut.length === 0) {
return
}
const handler = getShortcutHandler(shortcut)
if (!handler) return
if (!handler) {
return
}
const accelerator = formatShortcutKey(shortcut.shortcut)
@ -67,7 +71,22 @@ function registerWindowShortcuts(window: BrowserWindow) {
showAppAccelerator = accelerator
}
Logger.info(`Register shortcut: ${accelerator}`)
if (shortcut.key.includes('zoom')) {
switch (shortcut.key) {
case 'zoom_in':
globalShortcut.register('CommandOrControl+=', () => handler(window))
globalShortcut.register('CommandOrControl+numadd', () => handler(window))
return
case 'zoom_out':
globalShortcut.register('CommandOrControl+-', () => handler(window))
globalShortcut.register('CommandOrControl+numsub', () => handler(window))
return
case 'zoom_reset':
globalShortcut.register('CommandOrControl+0', () => handler(window))
return
}
}
globalShortcut.register(accelerator, () => handler(window))
})
}
@ -79,9 +98,7 @@ function registerWindowShortcuts(window: BrowserWindow) {
if (showAppAccelerator) {
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
if (handler) {
globalShortcut.register(showAppAccelerator, () => handler(window))
}
handler && globalShortcut.register(showAppAccelerator, () => handler(window))
}
}
@ -93,10 +110,6 @@ function registerWindowShortcuts(window: BrowserWindow) {
}
}
export function registerShortcuts(mainWindow: BrowserWindow) {
registerWindowShortcuts(mainWindow)
}
export function unregisterAllShortcuts() {
showAppAccelerator = null
globalShortcut.unregisterAll()

View File

@ -1,6 +1,5 @@
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@main/constant'
import { FileTypes } from '../../renderer/src/types'
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
import { FileTypes } from '@types'
export function getFileType(ext: string): FileTypes {
ext = ext.toLowerCase()

View File

@ -3,97 +3,8 @@ export const DEFAULT_CONTEXTCOUNT = 5
export const DEFAULT_MAX_TOKENS = 4096
export const FONT_FAMILY =
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
export const platform = window.electron?.process?.platform
export const isMac = platform === 'darwin'
export const isWindows = platform === 'win32' || platform === 'win64'
export const isLinux = platform === 'linux'
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件
'.java' // Java 代码文件
]

View File

@ -1,9 +1,10 @@
import { isMac } from '@renderer/config/constant'
import { isMac, isWindows } from '@renderer/config/constant'
import { isLocalAi } from '@renderer/config/env'
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { useAppDispatch } from '@renderer/store'
import { setAvatar, setFilesPath } from '@renderer/store/runtime'
import { updateShortcut } from '@renderer/store/shortcuts'
import { runAsyncFunction } from '@renderer/utils'
import { useLiveQuery } from 'dexie-react-hooks'
import { useEffect } from 'react'
@ -11,6 +12,7 @@ import { useEffect } from 'react'
import { useDefaultModel } from './useAssistant'
import { useRuntime } from './useRuntime'
import { useSettings } from './useSettings'
import { useShortcuts } from './useShortcuts'
export function useAppInit() {
const dispatch = useAppDispatch()
@ -18,6 +20,7 @@ export function useAppInit() {
const { minappShow } = useRuntime()
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
const { shortcuts } = useShortcuts()
useEffect(() => {
avatar?.value && dispatch(setAvatar(avatar.value))
@ -69,4 +72,15 @@ export function useAppInit() {
dispatch(setFilesPath(info.filesPath))
})
}, [dispatch])
useEffect(() => {
if (isWindows) {
shortcuts.forEach((shortcut) => {
if (shortcut.shortcut[0] === 'Command') {
shortcut.shortcut[0] = 'Ctrl'
dispatch(updateShortcut(shortcut))
}
})
}
}, [dispatch, shortcuts])
}

View File

@ -49,7 +49,12 @@ export const useShortcut = (
},
{
enableOnFormTags: options.enableOnFormTags,
description: options.description || shortcutConfig?.name
description: options.description || shortcutConfig?.key
}
)
}
export function useShortcuts() {
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
return { shortcuts }
}

View File

@ -458,7 +458,9 @@
"reset_defaults": "Reset Defaults",
"reset_defaults_confirm": "Are you sure you want to reset all shortcuts?",
"press_shortcut": "Press Shortcut",
"alt_warning": "Mac does not support the Alt key"
"alt_warning": "Mac does not support the Alt key",
"reset_to_default": "Reset to Default",
"clear_shortcut": "Clear Shortcut"
},
"theme.auto": "Auto",
"theme.dark": "Dark",

View File

@ -458,7 +458,9 @@
"reset_defaults": "Сбросить настройки по умолчанию",
"reset_defaults_confirm": "Вы уверены, что хотите сбросить все горячие клавиши?",
"press_shortcut": "Нажмите сочетание клавиш",
"alt_warning": "Mac не поддерживает Alt"
"alt_warning": "Mac не поддерживает Alt",
"reset_to_default": "Сбросить настройки по умолчанию",
"clear_shortcut": "Очистить сочетание клавиш"
},
"theme.auto": "Автоматически",
"theme.dark": "Темная",

View File

@ -446,7 +446,9 @@
"reset_defaults": "重置默认快捷键",
"reset_defaults_confirm": "确定要重置所有快捷键吗?",
"press_shortcut": "按下快捷键",
"alt_warning": "Mac 系统不支持 Alt 键"
"alt_warning": "Mac 系统不支持 Alt 键",
"reset_to_default": "重置为默认",
"clear_shortcut": "清除快捷键"
},
"theme.auto": "跟随系统",
"theme.dark": "深色主题",

View File

@ -446,7 +446,9 @@
"reset_defaults": "重置預設快捷鍵",
"reset_defaults_confirm": "確定要重置所有快捷鍵嗎?",
"press_shortcut": "按下快捷鍵",
"alt_warning": "Mac 系統不支持 Alt 鍵"
"alt_warning": "Mac 系統不支持 Alt 鍵",
"reset_to_default": "重置為預設",
"clear_shortcut": "清除快捷鍵"
},
"theme.auto": "自動",
"theme.dark": "深色主題",

View File

@ -1,7 +1,7 @@
import { PaperClipOutlined } from '@ant-design/icons'
import { documentExts, imageExts, textExts } from '@renderer/config/constant'
import { isVisionModel } from '@renderer/config/models'
import { FileType, Model } from '@renderer/types'
import { documentExts, imageExts, textExts } from '@shared/config/constant'
import { Tooltip } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'

View File

@ -9,7 +9,7 @@ import {
} from '@ant-design/icons'
import { PicCenterOutlined } from '@ant-design/icons'
import TranslateButton from '@renderer/components/TranslateButton'
import { documentExts, imageExts, isMac, textExts } from '@renderer/config/constant'
import { isMac } from '@renderer/config/constant'
import { isVisionModel } from '@renderer/config/models'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
@ -26,6 +26,7 @@ import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { setGenerating, setSearching } from '@renderer/store/runtime'
import { Assistant, FileType, Message, Topic } from '@renderer/types'
import { delay, getFileExtension, uuid } from '@renderer/utils'
import { documentExts, imageExts, textExts } from '@shared/config/constant'
import { Button, Popconfirm, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs'

View File

@ -11,7 +11,6 @@ import {
} from '@ant-design/icons'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { translateText } from '@renderer/services/TranslateService'
import { Message, Model } from '@renderer/types'
@ -37,7 +36,6 @@ const MessageMenubar: FC<Props> = (props) => {
const { message, index, model, isLastMessage, isAssistantMessage, setModel, onEditMessage, onDeleteMessage } = props
const { t } = useTranslation()
const [copied, setCopied] = useState(false)
const { translateModel } = useDefaultModel()
const [isTranslating, setIsTranslating] = useState(false)
const isUserMessage = message.role === 'user'

View File

@ -490,8 +490,4 @@ const InfoIcon = styled(QuestionCircleOutlined)`
}
`
const RefreshIcon = styled(RedoOutlined)`
cursor: pointer;
`
export default PaintingsPage

View File

@ -1,10 +1,11 @@
import { UndoOutlined } from '@ant-design/icons'
import { ClearOutlined, UndoOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { isMac } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { initialState, resetShortcuts, updateShortcut } from '@renderer/store/shortcuts'
import { Button, Input, InputRef, Table as AntTable } from 'antd'
import { initialState, resetShortcuts, toggleShortcut, updateShortcut } from '@renderer/store/shortcuts'
import { Shortcut } from '@renderer/types'
import { Button, Input, InputRef, Switch, Table as AntTable, Tooltip } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { FC, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -12,13 +13,6 @@ import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '.'
interface ShortcutItem {
key: string
name: string
shortcut: string[]
enabled: boolean
}
const ShortcutSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
@ -27,7 +21,7 @@ const ShortcutSettings: FC = () => {
const inputRefs = useRef<Record<string, InputRef>>({})
const [editingKey, setEditingKey] = useState<string | null>(null)
const handleClear = (record: ShortcutItem) => {
const handleClear = (record: Shortcut) => {
dispatch(
updateShortcut({
...record,
@ -36,19 +30,19 @@ const ShortcutSettings: FC = () => {
)
}
const handleAddShortcut = (record: ShortcutItem) => {
const handleAddShortcut = (record: Shortcut) => {
setEditingKey(record.key)
setTimeout(() => {
inputRefs.current[record.key]?.focus()
}, 0)
}
const isShortcutModified = (record: ShortcutItem) => {
const isShortcutModified = (record: Shortcut) => {
const defaultShortcut = initialState.shortcuts.find((s) => s.key === record.key)
return defaultShortcut?.shortcut.join('+') !== record.shortcut.join('+')
}
const handleResetShortcut = (record: ShortcutItem) => {
const handleResetShortcut = (record: Shortcut) => {
const defaultShortcut = initialState.shortcuts.find((s) => s.key === record.key)
if (defaultShortcut) {
dispatch(
@ -95,6 +89,8 @@ const ShortcutSettings: FC = () => {
return isMac ? '⌥' : 'Alt'
case 'Shift':
return isMac ? '⇧' : 'Shift'
case 'CommandOrControl':
return isMac ? '⌘' : 'Ctrl'
case ' ':
return 'Space'
default:
@ -104,7 +100,8 @@ const ShortcutSettings: FC = () => {
.join(' + ')
}
const handleKeyDown = (e: React.KeyboardEvent, record: ShortcutItem) => {
const handleKeyDown = (e: React.KeyboardEvent, record: Shortcut) => {
console.debug('handleKeyDown', e, record)
e.preventDefault()
const keys: string[] = []
@ -115,6 +112,8 @@ const ShortcutSettings: FC = () => {
const key = e.key
console.debug('key', key)
if (!['Control', 'Alt', 'Shift', 'Meta'].includes(key)) {
keys.push(key.toUpperCase())
}
@ -144,7 +143,7 @@ const ShortcutSettings: FC = () => {
})
}
const columns: ColumnsType<ShortcutItem> = [
const columns: ColumnsType<Shortcut> = [
{
title: t('settings.shortcuts.action'),
dataIndex: 'name',
@ -155,14 +154,16 @@ const ShortcutSettings: FC = () => {
dataIndex: 'shortcut',
key: 'shortcut',
align: 'right',
render: (shortcut: string[], record: ShortcutItem) => {
render: (shortcut: string[], record: Shortcut) => {
const isEditing = editingKey === record.key
const shortcutConfig = shortcuts.find((s) => s.key === record.key)
const isEditable = shortcutConfig?.editable !== false
return (
<div style={{ display: 'flex', gap: '8px' }}>
<div style={{ position: 'relative', flex: 1 }}>
<HStack style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end', alignItems: 'center' }}>
<HStack alignItems="center" style={{ position: 'relative' }}>
{isEditing ? (
<Input
<ShortcutInput
ref={(el) => el && (inputRefs.current[record.key] = el)}
value={formatShortcut(shortcut)}
placeholder={t('settings.shortcuts.press_shortcut')}
@ -173,39 +174,51 @@ const ShortcutSettings: FC = () => {
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)
}}
/>
)
}
/>
) : (
<div style={{ cursor: 'pointer', padding: '4px 11px' }} onClick={() => handleAddShortcut(record)}>
<ShortcutText isEditable={isEditable} onClick={() => isEditable && handleAddShortcut(record)}>
{shortcut.length > 0 ? formatShortcut(shortcut) : t('settings.shortcuts.press_shortcut')}
</div>
</ShortcutText>
)}
</div>
<Button onClick={() => (shortcut ? handleClear(record) : handleAddShortcut(record))}>
{shortcut ? t('common.clear') : t('common.add')}
</Button>
</div>
</HStack>
</HStack>
)
}
},
{
title: t('settings.shortcuts.actions'),
key: 'actions',
align: 'right',
width: '70px',
render: (record: Shortcut) => (
<HStack style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end', alignItems: 'center' }}>
<Tooltip title={t('settings.shortcuts.reset_to_default')}>
<Button
icon={<UndoOutlined />}
size="small"
onClick={() => handleResetShortcut(record)}
disabled={!isShortcutModified(record)}
/>
</Tooltip>
<Tooltip title={t('settings.shortcuts.clear_shortcut')}>
<Button
icon={<ClearOutlined />}
size="small"
onClick={() => handleClear(record)}
disabled={record.shortcut.length === 0 || !record.editable}
/>
</Tooltip>
</HStack>
)
},
{
title: t('settings.shortcuts.enabled'),
key: 'enabled',
align: 'right',
width: '50px',
render: (record: Shortcut) => (
<Switch size="small" checked={record.enabled} onChange={() => dispatch(toggleShortcut(record.key))} />
)
}
]
@ -216,7 +229,7 @@ const ShortcutSettings: FC = () => {
<SettingDivider style={{ marginBottom: 0 }} />
<Table
columns={columns as ColumnsType<unknown>}
dataSource={shortcuts.map((s) => ({ ...s, name: t(s.name) }))}
dataSource={shortcuts.map((s) => ({ ...s, name: t(`settings.shortcuts.${s.key}`) }))}
pagination={false}
size="middle"
showHeader={false}
@ -245,4 +258,15 @@ const Table = styled(AntTable)`
}
`
const ShortcutInput = styled(Input)`
width: 120px;
text-align: center;
`
const ShortcutText = styled.span<{ isEditable: boolean }>`
cursor: ${({ isEditable }) => (isEditable ? 'pointer' : 'not-allowed')};
padding: 4px 11px;
opacity: ${({ isEditable }) => (isEditable ? 1 : 0.5)};
`
export default ShortcutSettings

View File

@ -1,6 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { isMac } from '@renderer/config/constant'
import { Shortcut } from '@renderer/types'
import { ZOOM_SHORTCUTS } from '@shared/config/constant'
export interface ShortcutsState {
shortcuts: Shortcut[]
@ -8,34 +9,17 @@ export interface ShortcutsState {
const initialState: ShortcutsState = {
shortcuts: [
...ZOOM_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'],
editable: true,
enabled: true
},
{
key: 'show_app',
name: 'settings.shortcuts.show_app',
shortcut: [isMac ? 'Command' : 'Ctrl', 'Shift', 'A'],
shortcut: [],
editable: true,
enabled: true
}
]
@ -44,7 +28,6 @@ const initialState: ShortcutsState = {
const getSerializableShortcuts = (shortcuts: Shortcut[]) => {
return shortcuts.map((shortcut) => ({
key: shortcut.key,
name: shortcut.name,
shortcut: [...shortcut.shortcut],
enabled: shortcut.enabled
}))

View File

@ -165,7 +165,7 @@ export type AppInfo = {
export interface Shortcut {
key: string
name: string
shortcut: string[]
editable: boolean
enabled: boolean
}

View File

@ -5,20 +5,19 @@
"src/main/**/*",
"src/preload/**/*",
"src/main/env.d.ts",
"src/renderer/src/types/index.ts"
"src/renderer/src/types/index.ts",
"packages/shared/**/*"
],
"compilerOptions": {
"composite": true,
"types": [
"electron-vite/node"
],
"baseUrl": ".",
"paths": {
"@types": [
"./src/renderer/src/types/index.ts"
],
"@main/*": [
"./src/main/*"
]
"@main/*": ["src/main/*"],
"@types": ["src/renderer/src/types/index.ts"],
"@shared/*": ["packages/shared/*"]
}
}
}

View File

@ -1,20 +1,18 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
"include": [
"src/renderer/src/env.d.ts",
"src/renderer/src/**/*",
"src/renderer/src/**/*.tsx",
"src/preload/*.d.ts",
"local/src/renderer/**/*",
"packages/shared/**/*"
],
"compilerOptions": {
"composite": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@renderer/*": [
"src/renderer/src/*"
]
"@renderer/*": ["src/renderer/src/*"],
"@shared/*": ["packages/shared/*"]
}
}
}