Skip to content

Commit 9889767

Browse files
LemonNekoGHnekomeowww
authored andcommittedFeb 22, 2025
feat: edit shortcuts (#31)
1 parent 84f6877 commit 9889767

File tree

6 files changed

+199
-30
lines changed

6 files changed

+199
-30
lines changed
 

‎apps/stage-tamagotchi/src/main/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { inertia } from 'popmotion'
66

77
import icon from '../../build/icon.png?asset'
88

9+
// FIXME: electron i18n
10+
911
let globalMouseTracker: ReturnType<typeof setInterval> | null = null
1012
let mainWindow: BrowserWindow
1113
let currentAnimationX: { stop: () => void } | null = null

‎apps/stage-tamagotchi/src/renderer/locales/en.yml

+8
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ settings:
6565
voices: Voice
6666
quit: Quit
6767
viewer: Viewer
68+
shortcuts:
69+
title: Shortcuts
70+
window:
71+
move: Move the window
72+
resize: Resize the window
73+
debug: Toggle developer tools
74+
press_keys: Press keys...
75+
other: Other
6876
stage:
6977
chat:
7078
message:

‎apps/stage-tamagotchi/src/renderer/locales/zh-CN.yml

+8
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ settings:
5252
voices: 声线
5353
quit: 退出
5454
viewer: 查看器
55+
shortcuts:
56+
title: 快捷键
57+
window:
58+
move: 移动窗口
59+
resize: 调整窗口大小
60+
debug: 切换开发者模式
61+
press_keys: 按下快捷键...
62+
other: 其他
5563
stage:
5664
message: 消息
5765
select-a-audio-input: 选择一个音频输入设备
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,56 @@
1-
import { onMounted, onUnmounted } from 'vue'
1+
import type { EffectScope } from 'vue'
2+
3+
import { useShortcutsStore } from '@renderer/stores/shortcuts'
4+
import { useMagicKeys, whenever } from '@vueuse/core'
5+
import { storeToRefs } from 'pinia'
6+
import { computed, effectScope, watch } from 'vue'
27

38
import { useWindowControlStore } from '../stores/window-controls'
49
import { WindowControlMode } from '../types/window-controls'
510

611
export function useWindowShortcuts() {
712
const windowStore = useWindowControlStore()
13+
const magicKeys = useMagicKeys()
814

9-
function handleKeydown(event: KeyboardEvent) {
10-
// Ctrl/Cmd + Shift + D for debug mode
11-
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'd') {
12-
windowStore.setMode(WindowControlMode.DEBUG)
13-
windowStore.toggleControl()
14-
}
15-
// Ctrl/Cmd + M for move mode
16-
if ((event.ctrlKey || event.metaKey) && event.key === 'm') {
17-
windowStore.setMode(WindowControlMode.MOVE)
18-
windowStore.toggleControl()
19-
}
20-
// Ctrl/Cmd + R for resize mode
21-
if ((event.ctrlKey || event.metaKey) && event.key === 'r') {
22-
windowStore.setMode(WindowControlMode.RESIZE)
23-
windowStore.toggleControl()
24-
}
25-
// Escape to exit any mode
26-
if (event.key === 'Escape') {
27-
windowStore.setMode(WindowControlMode.DEFAULT)
28-
windowStore.toggleControl()
29-
}
30-
}
15+
const { shortcuts } = storeToRefs(useShortcutsStore())
16+
const handlers = computed(() => [
17+
{
18+
handle: () => {
19+
windowStore.setMode(WindowControlMode.MOVE)
20+
windowStore.toggleControl()
21+
},
22+
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'move')?.shortcut,
23+
},
24+
{
25+
handle: () => {
26+
windowStore.setMode(WindowControlMode.RESIZE)
27+
windowStore.toggleControl()
28+
},
29+
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'resize')?.shortcut,
30+
},
31+
{
32+
handle: () => {
33+
windowStore.setMode(WindowControlMode.DEBUG)
34+
windowStore.toggleControl()
35+
},
36+
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'debug')?.shortcut,
37+
},
38+
])
3139

32-
onMounted(() => {
33-
window.addEventListener('keydown', handleKeydown)
34-
})
40+
let currentScope: EffectScope | null = null
41+
watch(handlers, () => {
42+
if (currentScope) {
43+
currentScope.stop()
44+
}
3545

36-
onUnmounted(() => {
37-
window.removeEventListener('keydown', handleKeydown)
38-
})
46+
currentScope = effectScope()
47+
currentScope.run(() => {
48+
handlers.value.forEach((handler) => {
49+
if (!handler.shortcut) {
50+
return
51+
}
52+
whenever(magicKeys[handler.shortcut], handler.handle)
53+
})
54+
})
55+
}, { immediate: true })
3956
}

‎apps/stage-tamagotchi/src/renderer/src/pages/settings.vue

+105-1
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,29 @@ import type { Voice } from '@proj-airi/stage-ui/constants'
33
44
import { voiceList } from '@proj-airi/stage-ui/constants'
55
import { useLLM, useSettings } from '@proj-airi/stage-ui/stores'
6+
import { useShortcutsStore } from '@renderer/stores/shortcuts'
7+
import { useEventListener } from '@vueuse/core'
68
import { storeToRefs } from 'pinia'
7-
import { onMounted, ref, watch } from 'vue'
9+
import { computed, onMounted, ref, watch } from 'vue'
810
import { useI18n } from 'vue-i18n'
911
1012
const { t, locale } = useI18n()
1113
1214
const settings = useSettings()
15+
const { shortcuts } = storeToRefs(useShortcutsStore())
1316
const supportedModels = ref<{ id: string, name?: string }[]>([])
1417
const { models } = useLLM()
1518
const { openAiModel, openAiApiBaseURL, openAiApiKey, elevenlabsVoiceEnglish, elevenlabsVoiceJapanese, language } = storeToRefs(settings)
1619
20+
const recordingFor = ref<string | null>(null)
21+
const recordingKeys = ref<{
22+
modifier: string[]
23+
key: string
24+
}>({
25+
modifier: [],
26+
key: '',
27+
})
28+
1729
function handleModelChange(event: Event) {
1830
const target = event.target as HTMLSelectElement
1931
const found = supportedModels.value.find(m => m.id === target.value)
@@ -69,6 +81,68 @@ onMounted(async () => {
6981
function handleQuit() {
7082
window.electron.ipcRenderer.send('quit')
7183
}
84+
85+
// Add function to handle shortcut recording
86+
function startRecording(shortcut: typeof shortcuts.value[0]) {
87+
recordingFor.value = shortcut.type
88+
}
89+
90+
function isModifierKey(key: string) {
91+
return ['Shift', 'Control', 'Alt', 'Meta'].includes(key)
92+
}
93+
94+
// Handle key combinations
95+
useEventListener('keydown', (e) => {
96+
if (!recordingFor.value)
97+
return
98+
99+
e.preventDefault()
100+
101+
if (isModifierKey(e.key)) {
102+
if (recordingKeys.value.modifier.includes(e.key))
103+
return
104+
105+
recordingKeys.value.modifier.push(e.key)
106+
107+
return
108+
}
109+
110+
if (recordingKeys.value.modifier.length === 0)
111+
return
112+
113+
recordingKeys.value.key = e.key.toUpperCase()
114+
115+
const shortcut = shortcuts.value.find(s => s.type === recordingFor.value)
116+
if (shortcut)
117+
shortcut.shortcut = `${recordingKeys.value.modifier.join('+')}+${recordingKeys.value.key}`
118+
119+
recordingKeys.value = {
120+
modifier: [],
121+
key: '',
122+
}
123+
recordingFor.value = null
124+
}, { passive: false })
125+
126+
// Add click outside handler to cancel recording
127+
useEventListener('click', (e) => {
128+
if (recordingFor.value) {
129+
const target = e.target as HTMLElement
130+
if (!target.closest('.shortcut-item')) {
131+
recordingFor.value = null
132+
}
133+
}
134+
})
135+
136+
const pressKeysMessage = computed(() => {
137+
if (recordingKeys.value.modifier.length === 0)
138+
return t('settings.press_keys')
139+
140+
return `${t('settings.press_keys')}: ${recordingKeys.value.modifier.join('+')}+${recordingKeys.value.key}`
141+
})
142+
143+
function isConflict(shortcut: typeof shortcuts.value[0]) {
144+
return shortcuts.value.some(s => s.type !== shortcut.type && s.shortcut === shortcut.shortcut)
145+
}
72146
</script>
73147

74148
<template>
@@ -201,6 +275,36 @@ function handleQuit() {
201275
</select>
202276
</div>
203277
</div>
278+
<h2 text="slate-800/80" font-bold>
279+
{{ t('settings.shortcuts.title') }}
280+
</h2>
281+
<div pb-2>
282+
<div
283+
grid="~ cols-[140px_1fr]" my-2 items-center gap-1.5 rounded-lg
284+
bg="[#fff6fc]" p-2 text="pink-400"
285+
>
286+
<template v-for="shortcut in shortcuts" :key="shortcut.type">
287+
<span text="xs pink-500">
288+
{{ t(shortcut.name) }}
289+
</span>
290+
<div
291+
class="shortcut-item flex items-center justify-end gap-x-2 px-2 py-0.5"
292+
:class="{ recording: recordingFor === shortcut.type }"
293+
text="xs pink-500"
294+
cursor-pointer
295+
@click="startRecording(shortcut)"
296+
>
297+
<div v-if="recordingFor === shortcut.type" class="pointer-events-none animate-flash animate-count-infinite">
298+
{{ pressKeysMessage }}
299+
</div>
300+
<div v-else class="pointer-events-none">
301+
{{ shortcut.shortcut }}
302+
</div>
303+
<div v-if="isConflict(shortcut)" text="xs pink-500" i-solar:danger-square-bold w-4 />
304+
</div>
305+
</template>
306+
</div>
307+
</div>
204308
<h2 text="slate-800/80" font-bold>
205309
{{ t('settings.other') }}
206310
</h2>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useLocalStorage } from '@vueuse/core'
2+
import { defineStore } from 'pinia'
3+
import { ref } from 'vue'
4+
5+
export const useShortcutsStore = defineStore('shortcuts', () => {
6+
const shortcuts = ref([
7+
{
8+
name: 'settings.shortcuts.window.move',
9+
shortcut: useLocalStorage('shortcuts/window/move', 'Ctrl+M'),
10+
group: 'window',
11+
type: 'move',
12+
},
13+
{
14+
name: 'settings.shortcuts.window.resize',
15+
shortcut: useLocalStorage('shortcuts/window/resize', 'Ctrl+R'),
16+
group: 'window',
17+
type: 'resize',
18+
},
19+
{
20+
name: 'settings.shortcuts.window.debug',
21+
shortcut: useLocalStorage('shortcuts/window/debug', 'Ctrl+I'),
22+
group: 'window',
23+
type: 'debug',
24+
},
25+
])
26+
27+
return {
28+
shortcuts,
29+
}
30+
})

0 commit comments

Comments
 (0)
Please sign in to comment.