feat(keybindings): 快捷键自定义系统 — store+冲突检测+交互式重新绑定UI | 13 files +1032/-137 | 72 tests passing tsc零错误
This commit is contained in:
1 parent
514d62644e
commit
3f636b1b0e
13 files changed
+1033
-138
No files matched your search
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* KeybindingStore — 快捷键自定义系统状态管理
|
||||
*
|
||||
* 管理用户自定义快捷键映射:
|
||||
* - customKeybindings: { actionId: customKeys } — 仅存储用户修改的
|
||||
* - getEffectiveKeys(): 合并默认值和自定义值返回实际生效的键
|
||||
* - 冲突检测: 检测新键绑定是否与现有绑定冲突
|
||||
* - 持久化: 通过 electronAPI.settingsSet 保存到 SQLite
|
||||
*
|
||||
* @module stores/keybindingStore
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import { createLogger } from '../utils/logger'
|
||||
import {
|
||||
KEYBINDING_DEFINITIONS,
|
||||
getKeybindingDefinition,
|
||||
type KeybindingActionId,
|
||||
type KeybindingDefinition,
|
||||
} from '../constants/keybindings'
|
||||
|
||||
const logger = createLogger('KeybindingStore')
|
||||
|
||||
// ─── 类型 ──────────────────────────────────────────────
|
||||
|
||||
/** 自定义键绑定映射 (仅存储用户修改的) */
|
||||
export type CustomKeybindings = Record<KeybindingActionId, string>
|
||||
|
||||
interface KeybindingState {
|
||||
/** 自定义键绑定 (仅存储与默认不同的) */
|
||||
customKeybindings: CustomKeybindings
|
||||
/** 是否已从持久化加载 */
|
||||
isLoaded: boolean
|
||||
|
||||
// Actions
|
||||
/** 从 electronAPI 加载自定义快捷键 */
|
||||
loadFromSettings: () => Promise<void>
|
||||
/** 获取操作的有效键绑定 (自定义优先,否则默认) */
|
||||
getEffectiveKeys: (actionId: KeybindingActionId) => string
|
||||
/** 获取所有操作的有效键绑定映射 */
|
||||
getAllEffective: () => Record<KeybindingActionId, string>
|
||||
/** 设置自定义键绑定 */
|
||||
setCustomKeybinding: (actionId: KeybindingActionId, keys: string) => Promise<void>
|
||||
/** 重置单个操作为默认 */
|
||||
resetKeybinding: (actionId: KeybindingActionId) => Promise<void>
|
||||
/** 重置所有为默认 */
|
||||
resetAll: () => Promise<void>
|
||||
/** 检测冲突 — 返回与新键绑定冲突的其他操作列表 */
|
||||
getConflicts: (actionId: KeybindingActionId, keys: string) => KeybindingDefinition[]
|
||||
/** 规范化键序列 (统一修饰键顺序和大小写) */
|
||||
normalizeKeySequence: (raw: string) => string
|
||||
}
|
||||
|
||||
// ─── Store ─────────────────────────────────────────────
|
||||
|
||||
export const useKeybindingStore = create<KeybindingState>((set, get) => ({
|
||||
customKeybindings: {} as CustomKeybindings,
|
||||
isLoaded: false,
|
||||
|
||||
loadFromSettings: async () => {
|
||||
try {
|
||||
const result = await window.electronAPI?.settingsGet('keybindings')
|
||||
if (result?.success && result.value && typeof result.value === 'object') {
|
||||
set({ customKeybindings: result.value as CustomKeybindings, isLoaded: true })
|
||||
} else {
|
||||
set({ isLoaded: true })
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to load keybindings:', error)
|
||||
set({ isLoaded: true })
|
||||
}
|
||||
},
|
||||
|
||||
getEffectiveKeys: (actionId: KeybindingActionId) => {
|
||||
const { customKeybindings } = get()
|
||||
const custom = customKeybindings[actionId]
|
||||
if (custom && custom.trim()) return custom
|
||||
const def = getKeybindingDefinition(actionId)
|
||||
return def?.defaultKeys ?? ''
|
||||
},
|
||||
|
||||
getAllEffective: () => {
|
||||
const { customKeybindings } = get()
|
||||
const result = {} as Record<KeybindingActionId, string>
|
||||
for (const def of KEYBINDING_DEFINITIONS) {
|
||||
const custom = customKeybindings[def.id]
|
||||
result[def.id] = custom && custom.trim() ? custom : def.defaultKeys
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
setCustomKeybinding: async (actionId: KeybindingActionId, keys: string) => {
|
||||
const newCustoms = { ...get().customKeybindings, [actionId]: keys }
|
||||
set({ customKeybindings: newCustoms })
|
||||
try {
|
||||
await window.electronAPI?.settingsSet('keybindings', newCustoms)
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist keybinding:', error)
|
||||
}
|
||||
},
|
||||
|
||||
resetKeybinding: async (actionId: KeybindingActionId) => {
|
||||
const { [actionId]: _, ...rest } = get().customKeybindings
|
||||
set({ customKeybindings: rest as CustomKeybindings })
|
||||
try {
|
||||
await window.electronAPI?.settingsSet('keybindings', rest)
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist keybinding reset:', error)
|
||||
}
|
||||
},
|
||||
|
||||
resetAll: async () => {
|
||||
set({ customKeybindings: {} as CustomKeybindings })
|
||||
try {
|
||||
await window.electronAPI?.settingsSet('keybindings', {})
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist keybinding resetAll:', error)
|
||||
}
|
||||
},
|
||||
|
||||
getConflicts: (actionId: KeybindingActionId, keys: string) => {
|
||||
if (!keys || !keys.trim()) return []
|
||||
// 特殊处理: Ctrl+1…9 范围键不参与冲突检测
|
||||
if (keys.includes('…') || keys.includes('...')) return []
|
||||
|
||||
const normalizedInput = get().normalizeKeySequence(keys)
|
||||
const conflicts: KeybindingDefinition[] = []
|
||||
|
||||
for (const def of KEYBINDING_DEFINITIONS) {
|
||||
// 跳过自身
|
||||
if (def.id === actionId) continue
|
||||
// 跳过范围键
|
||||
if (def.defaultKeys.includes('…') || def.defaultKeys.includes('...')) continue
|
||||
|
||||
const effectiveKeys = get().getEffectiveKeys(def.id)
|
||||
// 跳过范围键的有效值
|
||||
if (effectiveKeys.includes('…') || effectiveKeys.includes('...')) continue
|
||||
|
||||
const normalizedExisting = get().normalizeKeySequence(effectiveKeys)
|
||||
if (normalizedExisting === normalizedInput) {
|
||||
conflicts.push(def)
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts
|
||||
},
|
||||
|
||||
normalizeKeySequence: (raw: string) => {
|
||||
if (!raw) return ''
|
||||
// 分割、去空格、统一大小写
|
||||
const parts = raw
|
||||
.split('+')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
.map((p) => {
|
||||
const lower = p.toLowerCase()
|
||||
// Meta → Ctrl (跨平台统一)
|
||||
if (lower === 'meta' || lower === 'cmd') return 'Ctrl'
|
||||
// 修饰键首字母大写
|
||||
if (lower === 'ctrl' || lower === 'control') return 'Ctrl'
|
||||
if (lower === 'shift') return 'Shift'
|
||||
if (lower === 'alt' || lower === 'option') return 'Alt'
|
||||
// 普通键:首字母大写其余小写 (如 Tab, Enter, F11)
|
||||
return p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()
|
||||
})
|
||||
|
||||
// 排序: Ctrl > Shift > Alt > 其他键
|
||||
const modifierOrder: Record<string, number> = { Ctrl: 0, Shift: 1, Alt: 2 }
|
||||
parts.sort((a, b) => {
|
||||
const aOrder = modifierOrder[a] ?? 99
|
||||
const bOrder = modifierOrder[b] ?? 99
|
||||
return aOrder - bOrder
|
||||
})
|
||||
|
||||
return parts.join('+')
|
||||
},
|
||||
}))
|
||||
|
||||
export default useKeybindingStore
|
||||
Reference in new issue
Block a user