mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
Add configurable keyboard shortcuts for score updates
This commit is contained in:
@@ -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)',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user