Skip to content

Commit

Permalink
feat: edit shortcuts (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
LemonNekoGH authored Feb 21, 2025
1 parent 3aefe2c commit fa918d6
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 30 deletions.
2 changes: 2 additions & 0 deletions apps/stage-tamagotchi/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions apps/stage-tamagotchi/src/renderer/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions apps/stage-tamagotchi/src/renderer/locales/zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ settings:
voices: 声线
quit: 退出
viewer: 查看器
shortcuts:
title: 快捷键
window:
move: 移动窗口
resize: 调整窗口大小
debug: 切换开发者模式
press_keys: 按下快捷键...
other: 其他
stage:
message: 消息
select-a-audio-input: 选择一个音频输入设备
Expand Down
Original file line number Diff line number Diff line change
@@ -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 })
}
106 changes: 105 additions & 1 deletion apps/stage-tamagotchi/src/renderer/src/pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
30 changes: 30 additions & 0 deletions apps/stage-tamagotchi/src/renderer/src/stores/shortcuts.ts
Original file line number Diff line number Diff line change
@@ -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,
}
})

0 comments on commit fa918d6

Please sign in to comment.