180 lines
6.3 KiB
TypeScript
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
|