feat: edit shortcuts (#31)
This commit is contained in:
@@ -6,6 +6,8 @@ import { inertia } from 'popmotion'
|
||||
|
||||
import icon from '../../build/icon.png?asset'
|
||||
|
||||
// FIXME: electron i18n
|
||||
|
||||
let globalMouseTracker: ReturnType<typeof setInterval> | null = null
|
||||
let mainWindow: BrowserWindow
|
||||
let currentAnimationX: { stop: () => void } | null = null
|
||||
|
||||
@@ -65,6 +65,14 @@ settings:
|
||||
voices: Voice
|
||||
quit: Quit
|
||||
viewer: Viewer
|
||||
shortcuts:
|
||||
title: Shortcuts
|
||||
window:
|
||||
move: Move the window
|
||||
resize: Resize the window
|
||||
debug: Toggle developer tools
|
||||
press_keys: Press keys...
|
||||
other: Other
|
||||
stage:
|
||||
chat:
|
||||
message:
|
||||
|
||||
@@ -52,6 +52,14 @@ settings:
|
||||
voices: 声线
|
||||
quit: 退出
|
||||
viewer: 查看器
|
||||
shortcuts:
|
||||
title: 快捷键
|
||||
window:
|
||||
move: 移动窗口
|
||||
resize: 调整窗口大小
|
||||
debug: 切换开发者模式
|
||||
press_keys: 按下快捷键...
|
||||
other: 其他
|
||||
stage:
|
||||
message: 消息
|
||||
select-a-audio-input: 选择一个音频输入设备
|
||||
|
||||
@@ -1,39 +1,56 @@
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import type { EffectScope } from 'vue'
|
||||
|
||||
import { useShortcutsStore } from '@renderer/stores/shortcuts'
|
||||
import { useMagicKeys, whenever } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, effectScope, watch } from 'vue'
|
||||
|
||||
import { useWindowControlStore } from '../stores/window-controls'
|
||||
import { WindowControlMode } from '../types/window-controls'
|
||||
|
||||
export function useWindowShortcuts() {
|
||||
const windowStore = useWindowControlStore()
|
||||
const magicKeys = useMagicKeys()
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Ctrl/Cmd + Shift + D for debug mode
|
||||
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'd') {
|
||||
windowStore.setMode(WindowControlMode.DEBUG)
|
||||
windowStore.toggleControl()
|
||||
}
|
||||
// Ctrl/Cmd + M for move mode
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'm') {
|
||||
windowStore.setMode(WindowControlMode.MOVE)
|
||||
windowStore.toggleControl()
|
||||
}
|
||||
// Ctrl/Cmd + R for resize mode
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'r') {
|
||||
windowStore.setMode(WindowControlMode.RESIZE)
|
||||
windowStore.toggleControl()
|
||||
}
|
||||
// Escape to exit any mode
|
||||
if (event.key === 'Escape') {
|
||||
windowStore.setMode(WindowControlMode.DEFAULT)
|
||||
windowStore.toggleControl()
|
||||
}
|
||||
}
|
||||
const { shortcuts } = storeToRefs(useShortcutsStore())
|
||||
const handlers = computed(() => [
|
||||
{
|
||||
handle: () => {
|
||||
windowStore.setMode(WindowControlMode.MOVE)
|
||||
windowStore.toggleControl()
|
||||
},
|
||||
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'move')?.shortcut,
|
||||
},
|
||||
{
|
||||
handle: () => {
|
||||
windowStore.setMode(WindowControlMode.RESIZE)
|
||||
windowStore.toggleControl()
|
||||
},
|
||||
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'resize')?.shortcut,
|
||||
},
|
||||
{
|
||||
handle: () => {
|
||||
windowStore.setMode(WindowControlMode.DEBUG)
|
||||
windowStore.toggleControl()
|
||||
},
|
||||
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'debug')?.shortcut,
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
let currentScope: EffectScope | null = null
|
||||
watch(handlers, () => {
|
||||
if (currentScope) {
|
||||
currentScope.stop()
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
currentScope = effectScope()
|
||||
currentScope.run(() => {
|
||||
handlers.value.forEach((handler) => {
|
||||
if (!handler.shortcut) {
|
||||
return
|
||||
}
|
||||
whenever(magicKeys[handler.shortcut], handler.handle)
|
||||
})
|
||||
})
|
||||
}, { immediate: true })
|
||||
}
|
||||
|
||||
@@ -3,17 +3,29 @@ import type { Voice } from '@proj-airi/stage-ui/constants'
|
||||
|
||||
import { voiceList } from '@proj-airi/stage-ui/constants'
|
||||
import { useLLM, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useShortcutsStore } from '@renderer/stores/shortcuts'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const settings = useSettings()
|
||||
const { shortcuts } = storeToRefs(useShortcutsStore())
|
||||
const supportedModels = ref<{ id: string, name?: string }[]>([])
|
||||
const { models } = useLLM()
|
||||
const { openAiModel, openAiApiBaseURL, openAiApiKey, elevenlabsVoiceEnglish, elevenlabsVoiceJapanese, language } = storeToRefs(settings)
|
||||
|
||||
const recordingFor = ref<string | null>(null)
|
||||
const recordingKeys = ref<{
|
||||
modifier: string[]
|
||||
key: string
|
||||
}>({
|
||||
modifier: [],
|
||||
key: '',
|
||||
})
|
||||
|
||||
function handleModelChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const found = supportedModels.value.find(m => m.id === target.value)
|
||||
@@ -69,6 +81,68 @@ onMounted(async () => {
|
||||
function handleQuit() {
|
||||
window.electron.ipcRenderer.send('quit')
|
||||
}
|
||||
|
||||
// Add function to handle shortcut recording
|
||||
function startRecording(shortcut: typeof shortcuts.value[0]) {
|
||||
recordingFor.value = shortcut.type
|
||||
}
|
||||
|
||||
function isModifierKey(key: string) {
|
||||
return ['Shift', 'Control', 'Alt', 'Meta'].includes(key)
|
||||
}
|
||||
|
||||
// Handle key combinations
|
||||
useEventListener('keydown', (e) => {
|
||||
if (!recordingFor.value)
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (isModifierKey(e.key)) {
|
||||
if (recordingKeys.value.modifier.includes(e.key))
|
||||
return
|
||||
|
||||
recordingKeys.value.modifier.push(e.key)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (recordingKeys.value.modifier.length === 0)
|
||||
return
|
||||
|
||||
recordingKeys.value.key = e.key.toUpperCase()
|
||||
|
||||
const shortcut = shortcuts.value.find(s => s.type === recordingFor.value)
|
||||
if (shortcut)
|
||||
shortcut.shortcut = `${recordingKeys.value.modifier.join('+')}+${recordingKeys.value.key}`
|
||||
|
||||
recordingKeys.value = {
|
||||
modifier: [],
|
||||
key: '',
|
||||
}
|
||||
recordingFor.value = null
|
||||
}, { passive: false })
|
||||
|
||||
// Add click outside handler to cancel recording
|
||||
useEventListener('click', (e) => {
|
||||
if (recordingFor.value) {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest('.shortcut-item')) {
|
||||
recordingFor.value = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const pressKeysMessage = computed(() => {
|
||||
if (recordingKeys.value.modifier.length === 0)
|
||||
return t('settings.press_keys')
|
||||
|
||||
return `${t('settings.press_keys')}: ${recordingKeys.value.modifier.join('+')}+${recordingKeys.value.key}`
|
||||
})
|
||||
|
||||
function isConflict(shortcut: typeof shortcuts.value[0]) {
|
||||
return shortcuts.value.some(s => s.type !== shortcut.type && s.shortcut === shortcut.shortcut)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -201,6 +275,36 @@ function handleQuit() {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<h2 text="slate-800/80" font-bold>
|
||||
{{ t('settings.shortcuts.title') }}
|
||||
</h2>
|
||||
<div pb-2>
|
||||
<div
|
||||
grid="~ cols-[140px_1fr]" my-2 items-center gap-1.5 rounded-lg
|
||||
bg="[#fff6fc]" p-2 text="pink-400"
|
||||
>
|
||||
<template v-for="shortcut in shortcuts" :key="shortcut.type">
|
||||
<span text="xs pink-500">
|
||||
{{ t(shortcut.name) }}
|
||||
</span>
|
||||
<div
|
||||
class="shortcut-item flex items-center justify-end gap-x-2 px-2 py-0.5"
|
||||
:class="{ recording: recordingFor === shortcut.type }"
|
||||
text="xs pink-500"
|
||||
cursor-pointer
|
||||
@click="startRecording(shortcut)"
|
||||
>
|
||||
<div v-if="recordingFor === shortcut.type" class="pointer-events-none animate-flash animate-count-infinite">
|
||||
{{ pressKeysMessage }}
|
||||
</div>
|
||||
<div v-else class="pointer-events-none">
|
||||
{{ shortcut.shortcut }}
|
||||
</div>
|
||||
<div v-if="isConflict(shortcut)" text="xs pink-500" i-solar:danger-square-bold w-4 />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<h2 text="slate-800/80" font-bold>
|
||||
{{ t('settings.other') }}
|
||||
</h2>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useShortcutsStore = defineStore('shortcuts', () => {
|
||||
const shortcuts = ref([
|
||||
{
|
||||
name: 'settings.shortcuts.window.move',
|
||||
shortcut: useLocalStorage('shortcuts/window/move', 'Ctrl+M'),
|
||||
group: 'window',
|
||||
type: 'move',
|
||||
},
|
||||
{
|
||||
name: 'settings.shortcuts.window.resize',
|
||||
shortcut: useLocalStorage('shortcuts/window/resize', 'Ctrl+R'),
|
||||
group: 'window',
|
||||
type: 'resize',
|
||||
},
|
||||
{
|
||||
name: 'settings.shortcuts.window.debug',
|
||||
shortcut: useLocalStorage('shortcuts/window/debug', 'Ctrl+I'),
|
||||
group: 'window',
|
||||
type: 'debug',
|
||||
},
|
||||
])
|
||||
|
||||
return {
|
||||
shortcuts,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user