Files
opsxshell/src/stores/keybindingStore.ts
T

180 lines
6.3 KiB
TypeScript

/**
* 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