diff --git a/src/dashboard/scoreko-dev/i18n.ts b/src/dashboard/scoreko-dev/i18n.ts index 87fd8fc..29b5c37 100644 --- a/src/dashboard/scoreko-dev/i18n.ts +++ b/src/dashboard/scoreko-dev/i18n.ts @@ -23,6 +23,7 @@ type Translations = { settingsShortcutRightDecrementLabel: string; settingsShortcutRightDecrementHint: string; settingsShortcutReset: string; + settingsShortcutRecordingHint: string; languageEnglish: string; languageSpanish: string; scoreboardUnassigned: string; @@ -109,6 +110,7 @@ const messages: Record = { settingsShortcutRightDecrementLabel: 'P2 score -1', settingsShortcutRightDecrementHint: 'Decreases right player score by one.', settingsShortcutReset: 'Reset shortcuts', + settingsShortcutRecordingHint: 'Press the desired shortcut now (example: Alt+1).', languageEnglish: 'English', languageSpanish: 'Spanish', scoreboardUnassigned: '(Unassigned)', @@ -191,6 +193,7 @@ const messages: Record = { settingsShortcutRightDecrementLabel: 'Score P2 -1', settingsShortcutRightDecrementHint: 'Reduce en uno el score del jugador derecho.', settingsShortcutReset: 'Restablecer atajos', + settingsShortcutRecordingHint: 'Pulsa ahora el atajo deseado (ejemplo: Alt+1).', languageEnglish: 'Inglés', languageSpanish: 'Castellano', scoreboardUnassigned: '(Sin asignar)', diff --git a/src/dashboard/scoreko-dev/main.vue b/src/dashboard/scoreko-dev/main.vue index d18ffaa..87f2c34 100644 --- a/src/dashboard/scoreko-dev/main.vue +++ b/src/dashboard/scoreko-dev/main.vue @@ -2,7 +2,7 @@ import { computed, onMounted, onUnmounted } from 'vue'; import { t } from './i18n'; import { useScoreboardStore } from './stores/scoreboard'; -import { useShortcutSettingsStore } from './stores/shortcut-settings'; +import { isShortcutMatch, useShortcutSettingsStore } from './stores/shortcut-settings'; const menuItems = computed(() => [ { label: t('menuDashboard'), to: '/', icon: 'dashboard' }, @@ -27,31 +27,30 @@ const isEditableTarget = (target: EventTarget | null): boolean => { }; const onShortcutPress = (event: KeyboardEvent) => { - if (event.ctrlKey || event.altKey || event.metaKey || isEditableTarget(event.target)) { + if (isEditableTarget(event.target) || document.body.dataset.shortcutRecording === 'true') { return; } - const key = event.key.toLowerCase(); const { shortcuts } = shortcutSettingsStore; - if (key === shortcuts.leftIncrement) { + if (isShortcutMatch(event, shortcuts.leftIncrement)) { scoreboardStore.leftScore += 1; event.preventDefault(); return; } - if (key === shortcuts.leftDecrement) { + if (isShortcutMatch(event, shortcuts.leftDecrement)) { scoreboardStore.leftScore = Math.max(0, scoreboardStore.leftScore - 1); event.preventDefault(); return; } - if (key === shortcuts.rightIncrement) { + if (isShortcutMatch(event, shortcuts.rightIncrement)) { scoreboardStore.rightScore += 1; event.preventDefault(); return; } - if (key === shortcuts.rightDecrement) { + if (isShortcutMatch(event, shortcuts.rightDecrement)) { scoreboardStore.rightScore = Math.max(0, scoreboardStore.rightScore - 1); event.preventDefault(); } diff --git a/src/dashboard/scoreko-dev/stores/shortcut-settings.ts b/src/dashboard/scoreko-dev/stores/shortcut-settings.ts index de740cc..fc67b23 100644 --- a/src/dashboard/scoreko-dev/stores/shortcut-settings.ts +++ b/src/dashboard/scoreko-dev/stores/shortcut-settings.ts @@ -2,35 +2,88 @@ import { defineStore } from 'pinia'; import { reactive } from 'vue'; export type ShortcutAction = 'leftIncrement' | 'leftDecrement' | 'rightIncrement' | 'rightDecrement'; - export type ShortcutSettings = Record; +type ShortcutParts = { + key: string; + altKey: boolean; + ctrlKey: boolean; + shiftKey: boolean; + metaKey: boolean; +}; + const STORAGE_KEY = 'scoreko-dev.shortcut-settings'; const defaultShortcuts: ShortcutSettings = { - leftIncrement: 'q', - leftDecrement: 'a', - rightIncrement: 'p', - rightDecrement: 'l', + leftIncrement: 'Q', + leftDecrement: 'A', + rightIncrement: 'P', + rightDecrement: 'L', }; -const normalizeShortcutKey = (value: unknown): string => { +const normalizeKey = (value: unknown): string => { + if (typeof value !== 'string') { + return ''; + } + const normalized = value.trim(); + return normalized.length === 1 ? normalized.toUpperCase() : ''; +}; + +const normalizeShortcut = (value: unknown): string => { if (typeof value !== 'string') { return ''; } - const normalized = value.trim().toLowerCase(); - return normalized.slice(0, 1); + const parts = value.split('+').map((part) => part.trim()).filter(Boolean); + if (parts.length === 0) { + return ''; + } + + const modifiers = { + altKey: false, + ctrlKey: false, + shiftKey: false, + metaKey: false, + }; + + let key = ''; + for (const part of parts) { + const lowered = part.toLowerCase(); + if (lowered === 'alt') { + modifiers.altKey = true; + continue; + } + if (lowered === 'ctrl' || lowered === 'control') { + modifiers.ctrlKey = true; + continue; + } + if (lowered === 'shift') { + modifiers.shiftKey = true; + continue; + } + if (lowered === 'meta' || lowered === 'cmd' || lowered === 'command') { + modifiers.metaKey = true; + continue; + } + + key = normalizeKey(part); + } + + if (!key) { + return ''; + } + + return formatShortcut({ key, ...modifiers }); }; const normalizeSettings = (input: unknown): ShortcutSettings => { const candidate = typeof input === 'object' && input !== null ? (input as Record) : {}; return { - leftIncrement: normalizeShortcutKey(candidate.leftIncrement) || defaultShortcuts.leftIncrement, - leftDecrement: normalizeShortcutKey(candidate.leftDecrement) || defaultShortcuts.leftDecrement, - rightIncrement: normalizeShortcutKey(candidate.rightIncrement) || defaultShortcuts.rightIncrement, - rightDecrement: normalizeShortcutKey(candidate.rightDecrement) || defaultShortcuts.rightDecrement, + leftIncrement: normalizeShortcut(candidate.leftIncrement) || defaultShortcuts.leftIncrement, + leftDecrement: normalizeShortcut(candidate.leftDecrement) || defaultShortcuts.leftDecrement, + rightIncrement: normalizeShortcut(candidate.rightIncrement) || defaultShortcuts.rightIncrement, + rightDecrement: normalizeShortcut(candidate.rightDecrement) || defaultShortcuts.rightDecrement, }; }; @@ -44,6 +97,7 @@ const readStoredSettings = (): ShortcutSettings => { if (!raw) { return { ...defaultShortcuts }; } + return normalizeSettings(JSON.parse(raw)); } catch { return { ...defaultShortcuts }; @@ -58,14 +112,57 @@ const persistSettings = (settings: ShortcutSettings) => { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); }; +export const formatShortcut = ({ key, altKey, ctrlKey, shiftKey, metaKey }: ShortcutParts): string => { + const parts: string[] = []; + if (ctrlKey) { + parts.push('Ctrl'); + } + if (altKey) { + parts.push('Alt'); + } + if (shiftKey) { + parts.push('Shift'); + } + if (metaKey) { + parts.push('Meta'); + } + parts.push(key); + return parts.join('+'); +}; + +export const eventToShortcut = (event: KeyboardEvent): string => { + const key = normalizeKey(event.key); + if (!key) { + return ''; + } + + return formatShortcut({ + key, + altKey: event.altKey, + ctrlKey: event.ctrlKey, + shiftKey: event.shiftKey, + metaKey: event.metaKey, + }); +}; + +export const isShortcutMatch = (event: KeyboardEvent, shortcut: string): boolean => { + const normalizedShortcut = normalizeShortcut(shortcut); + if (!normalizedShortcut) { + return false; + } + + return eventToShortcut(event) === normalizedShortcut; +}; + export const useShortcutSettingsStore = defineStore('shortcut-settings', () => { const shortcuts = reactive(readStoredSettings()); const setShortcut = (action: ShortcutAction, value: string) => { - const normalized = normalizeShortcutKey(value); + const normalized = normalizeShortcut(value); if (!normalized) { return; } + shortcuts[action] = normalized; persistSettings(shortcuts); }; diff --git a/src/dashboard/scoreko-dev/views/Settings.vue b/src/dashboard/scoreko-dev/views/Settings.vue index 8e19664..e4a622e 100644 --- a/src/dashboard/scoreko-dev/views/Settings.vue +++ b/src/dashboard/scoreko-dev/views/Settings.vue @@ -1,10 +1,13 @@ @@ -70,7 +115,7 @@ useHead(() => ({ title: t('settingsTitle') })); flat bordered class="q-pa-md" - style="max-width: 420px;" + style="max-width: 500px;" >
@@ -87,12 +132,21 @@ useHead(() => ({ title: t('settingsTitle') })); :key="field.action" :model-value="shortcutSettingsStore.shortcuts[field.action]" outlined - maxlength="1" + readonly :label="field.label" - @update:model-value="(value) => updateShortcut(field.action, value)" > +