Add configurable keyboard shortcuts for score updates

This commit is contained in:
Pandipipas
2026-02-25 14:08:48 +01:00
parent a4351b369c
commit 8a3b3ebcaf
4 changed files with 231 additions and 3 deletions
+33
View File
@@ -12,6 +12,17 @@ type Translations = {
settingsDescription: string;
settingsLanguageLabel: string;
settingsLanguageHint: string;
settingsShortcutTitle: string;
settingsShortcutDescription: string;
settingsShortcutLeftIncrementLabel: string;
settingsShortcutLeftIncrementHint: string;
settingsShortcutLeftDecrementLabel: string;
settingsShortcutLeftDecrementHint: string;
settingsShortcutRightIncrementLabel: string;
settingsShortcutRightIncrementHint: string;
settingsShortcutRightDecrementLabel: string;
settingsShortcutRightDecrementHint: string;
settingsShortcutReset: string;
languageEnglish: string;
languageSpanish: string;
scoreboardUnassigned: string;
@@ -87,6 +98,17 @@ const messages: Record<Locale, Translations> = {
settingsDescription: 'Dashboard and bundle configuration.',
settingsLanguageLabel: 'Language',
settingsLanguageHint: 'Choose the dashboard language.',
settingsShortcutTitle: 'Keyboard shortcuts',
settingsShortcutDescription: 'Configure quick keys to update the score for each side.',
settingsShortcutLeftIncrementLabel: 'P1 score +1',
settingsShortcutLeftIncrementHint: 'Increases left player score by one.',
settingsShortcutLeftDecrementLabel: 'P1 score -1',
settingsShortcutLeftDecrementHint: 'Decreases left player score by one.',
settingsShortcutRightIncrementLabel: 'P2 score +1',
settingsShortcutRightIncrementHint: 'Increases right player score by one.',
settingsShortcutRightDecrementLabel: 'P2 score -1',
settingsShortcutRightDecrementHint: 'Decreases right player score by one.',
settingsShortcutReset: 'Reset shortcuts',
languageEnglish: 'English',
languageSpanish: 'Spanish',
scoreboardUnassigned: '(Unassigned)',
@@ -158,6 +180,17 @@ const messages: Record<Locale, Translations> = {
settingsDescription: 'Configuración del dashboard y del bundle.',
settingsLanguageLabel: 'Idioma',
settingsLanguageHint: 'Selecciona el idioma del dashboard.',
settingsShortcutTitle: 'Atajos de teclado',
settingsShortcutDescription: 'Configura teclas rápidas para actualizar el score de cada lado.',
settingsShortcutLeftIncrementLabel: 'Score P1 +1',
settingsShortcutLeftIncrementHint: 'Incrementa en uno el score del jugador izquierdo.',
settingsShortcutLeftDecrementLabel: 'Score P1 -1',
settingsShortcutLeftDecrementHint: 'Reduce en uno el score del jugador izquierdo.',
settingsShortcutRightIncrementLabel: 'Score P2 +1',
settingsShortcutRightIncrementHint: 'Incrementa en uno el score del jugador derecho.',
settingsShortcutRightDecrementLabel: 'Score P2 -1',
settingsShortcutRightDecrementHint: 'Reduce en uno el score del jugador derecho.',
settingsShortcutReset: 'Restablecer atajos',
languageEnglish: 'Inglés',
languageSpanish: 'Castellano',
scoreboardUnassigned: '(Sin asignar)',
+54 -1
View File
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, onMounted, onUnmounted } from 'vue';
import { t } from './i18n';
import { useScoreboardStore } from './stores/scoreboard';
import { useShortcutSettingsStore } from './stores/shortcut-settings';
const menuItems = computed(() => [
{ label: t('menuDashboard'), to: '/', icon: 'dashboard' },
@@ -11,6 +13,57 @@ const menuItems = computed(() => [
]);
const logoUrl = new URL('./image.png', import.meta.url).href;
const scoreboardStore = useScoreboardStore();
const shortcutSettingsStore = useShortcutSettingsStore();
const isEditableTarget = (target: EventTarget | null): boolean => {
if (!(target instanceof HTMLElement)) {
return false;
}
return target.isContentEditable
|| ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)
|| Boolean(target.closest('[contenteditable="true"]'));
};
const onShortcutPress = (event: KeyboardEvent) => {
if (event.ctrlKey || event.altKey || event.metaKey || isEditableTarget(event.target)) {
return;
}
const key = event.key.toLowerCase();
const { shortcuts } = shortcutSettingsStore;
if (key === shortcuts.leftIncrement) {
scoreboardStore.leftScore += 1;
event.preventDefault();
return;
}
if (key === shortcuts.leftDecrement) {
scoreboardStore.leftScore = Math.max(0, scoreboardStore.leftScore - 1);
event.preventDefault();
return;
}
if (key === shortcuts.rightIncrement) {
scoreboardStore.rightScore += 1;
event.preventDefault();
return;
}
if (key === shortcuts.rightDecrement) {
scoreboardStore.rightScore = Math.max(0, scoreboardStore.rightScore - 1);
event.preventDefault();
}
};
onMounted(() => {
window.addEventListener('keydown', onShortcutPress);
});
onUnmounted(() => {
window.removeEventListener('keydown', onShortcutPress);
});
</script>
<template>
@@ -0,0 +1,86 @@
import { defineStore } from 'pinia';
import { reactive } from 'vue';
export type ShortcutAction = 'leftIncrement' | 'leftDecrement' | 'rightIncrement' | 'rightDecrement';
export type ShortcutSettings = Record<ShortcutAction, string>;
const STORAGE_KEY = 'scoreko-dev.shortcut-settings';
const defaultShortcuts: ShortcutSettings = {
leftIncrement: 'q',
leftDecrement: 'a',
rightIncrement: 'p',
rightDecrement: 'l',
};
const normalizeShortcutKey = (value: unknown): string => {
if (typeof value !== 'string') {
return '';
}
const normalized = value.trim().toLowerCase();
return normalized.slice(0, 1);
};
const normalizeSettings = (input: unknown): ShortcutSettings => {
const candidate = typeof input === 'object' && input !== null ? (input as Record<string, unknown>) : {};
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,
};
};
const readStoredSettings = (): ShortcutSettings => {
if (typeof window === 'undefined') {
return { ...defaultShortcuts };
}
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return { ...defaultShortcuts };
}
return normalizeSettings(JSON.parse(raw));
} catch {
return { ...defaultShortcuts };
}
};
const persistSettings = (settings: ShortcutSettings) => {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
};
export const useShortcutSettingsStore = defineStore('shortcut-settings', () => {
const shortcuts = reactive<ShortcutSettings>(readStoredSettings());
const setShortcut = (action: ShortcutAction, value: string) => {
const normalized = normalizeShortcutKey(value);
if (!normalized) {
return;
}
shortcuts[action] = normalized;
persistSettings(shortcuts);
};
const resetShortcuts = () => {
shortcuts.leftIncrement = defaultShortcuts.leftIncrement;
shortcuts.leftDecrement = defaultShortcuts.leftDecrement;
shortcuts.rightIncrement = defaultShortcuts.rightIncrement;
shortcuts.rightDecrement = defaultShortcuts.rightDecrement;
persistSettings(shortcuts);
};
return {
shortcuts,
setShortcut,
resetShortcuts,
};
});
+58 -2
View File
@@ -3,6 +3,8 @@ import { computed } from 'vue';
import { useHead } from '@unhead/vue';
import type { Locale } from '../i18n';
import { locale, setLocale, t } from '../i18n';
import type { ShortcutAction } from '../stores/shortcut-settings';
import { useShortcutSettingsStore } from '../stores/shortcut-settings';
defineOptions({ name: 'SettingsView' });
@@ -18,6 +20,19 @@ const selectedLanguage = computed<Locale>({
},
});
const shortcutSettingsStore = useShortcutSettingsStore();
const shortcutFields = computed<{ action: ShortcutAction; label: string; hint: string }[]>(() => [
{ action: 'leftIncrement', label: t('settingsShortcutLeftIncrementLabel'), hint: t('settingsShortcutLeftIncrementHint') },
{ action: 'leftDecrement', label: t('settingsShortcutLeftDecrementLabel'), hint: t('settingsShortcutLeftDecrementHint') },
{ action: 'rightIncrement', label: t('settingsShortcutRightIncrementLabel'), hint: t('settingsShortcutRightIncrementHint') },
{ action: 'rightDecrement', label: t('settingsShortcutRightDecrementLabel'), hint: t('settingsShortcutRightDecrementHint') },
]);
const updateShortcut = (action: ShortcutAction, value: string | number | null) => {
shortcutSettingsStore.setShortcut(action, String(value ?? ''));
};
useHead(() => ({ title: t('settingsTitle') }));
</script>
@@ -33,8 +48,8 @@ useHead(() => ({ title: t('settingsTitle') }));
<QCard
flat
bordered
class="q-pa-md"
style="max-width: 360px;"
class="q-pa-md q-mb-md"
style="max-width: 420px;"
>
<QCardSection class="q-pa-none">
<QSelect
@@ -50,5 +65,46 @@ useHead(() => ({ title: t('settingsTitle') }));
</div>
</QCardSection>
</QCard>
<QCard
flat
bordered
class="q-pa-md"
style="max-width: 420px;"
>
<QCardSection class="q-pa-none">
<div class="text-subtitle1 q-mb-md">
{{ t('settingsShortcutTitle') }}
</div>
<div class="text-caption text-grey-5 q-mb-md">
{{ t('settingsShortcutDescription') }}
</div>
<div class="column q-gutter-md">
<QInput
v-for="field in shortcutFields"
:key="field.action"
:model-value="shortcutSettingsStore.shortcuts[field.action]"
outlined
maxlength="1"
:label="field.label"
@update:model-value="(value) => updateShortcut(field.action, value)"
>
<template #hint>
{{ field.hint }}
</template>
</QInput>
</div>
<QBtn
class="q-mt-md"
color="primary"
outline
:label="t('settingsShortcutReset')"
@click="shortcutSettingsStore.resetShortcuts"
/>
</QCardSection>
</QCard>
</QPage>
</template>