mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
Improve shortcut capture with key combinations
This commit is contained in:
@@ -23,6 +23,7 @@ type Translations = {
|
|||||||
settingsShortcutRightDecrementLabel: string;
|
settingsShortcutRightDecrementLabel: string;
|
||||||
settingsShortcutRightDecrementHint: string;
|
settingsShortcutRightDecrementHint: string;
|
||||||
settingsShortcutReset: string;
|
settingsShortcutReset: string;
|
||||||
|
settingsShortcutRecordingHint: string;
|
||||||
languageEnglish: string;
|
languageEnglish: string;
|
||||||
languageSpanish: string;
|
languageSpanish: string;
|
||||||
scoreboardUnassigned: string;
|
scoreboardUnassigned: string;
|
||||||
@@ -109,6 +110,7 @@ const messages: Record<Locale, Translations> = {
|
|||||||
settingsShortcutRightDecrementLabel: 'P2 score -1',
|
settingsShortcutRightDecrementLabel: 'P2 score -1',
|
||||||
settingsShortcutRightDecrementHint: 'Decreases right player score by one.',
|
settingsShortcutRightDecrementHint: 'Decreases right player score by one.',
|
||||||
settingsShortcutReset: 'Reset shortcuts',
|
settingsShortcutReset: 'Reset shortcuts',
|
||||||
|
settingsShortcutRecordingHint: 'Press the desired shortcut now (example: Alt+1).',
|
||||||
languageEnglish: 'English',
|
languageEnglish: 'English',
|
||||||
languageSpanish: 'Spanish',
|
languageSpanish: 'Spanish',
|
||||||
scoreboardUnassigned: '(Unassigned)',
|
scoreboardUnassigned: '(Unassigned)',
|
||||||
@@ -191,6 +193,7 @@ const messages: Record<Locale, Translations> = {
|
|||||||
settingsShortcutRightDecrementLabel: 'Score P2 -1',
|
settingsShortcutRightDecrementLabel: 'Score P2 -1',
|
||||||
settingsShortcutRightDecrementHint: 'Reduce en uno el score del jugador derecho.',
|
settingsShortcutRightDecrementHint: 'Reduce en uno el score del jugador derecho.',
|
||||||
settingsShortcutReset: 'Restablecer atajos',
|
settingsShortcutReset: 'Restablecer atajos',
|
||||||
|
settingsShortcutRecordingHint: 'Pulsa ahora el atajo deseado (ejemplo: Alt+1).',
|
||||||
languageEnglish: 'Inglés',
|
languageEnglish: 'Inglés',
|
||||||
languageSpanish: 'Castellano',
|
languageSpanish: 'Castellano',
|
||||||
scoreboardUnassigned: '(Sin asignar)',
|
scoreboardUnassigned: '(Sin asignar)',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { computed, onMounted, onUnmounted } from 'vue';
|
import { computed, onMounted, onUnmounted } from 'vue';
|
||||||
import { t } from './i18n';
|
import { t } from './i18n';
|
||||||
import { useScoreboardStore } from './stores/scoreboard';
|
import { useScoreboardStore } from './stores/scoreboard';
|
||||||
import { useShortcutSettingsStore } from './stores/shortcut-settings';
|
import { isShortcutMatch, useShortcutSettingsStore } from './stores/shortcut-settings';
|
||||||
|
|
||||||
const menuItems = computed(() => [
|
const menuItems = computed(() => [
|
||||||
{ label: t('menuDashboard'), to: '/', icon: 'dashboard' },
|
{ label: t('menuDashboard'), to: '/', icon: 'dashboard' },
|
||||||
@@ -27,31 +27,30 @@ const isEditableTarget = (target: EventTarget | null): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onShortcutPress = (event: KeyboardEvent) => {
|
const onShortcutPress = (event: KeyboardEvent) => {
|
||||||
if (event.ctrlKey || event.altKey || event.metaKey || isEditableTarget(event.target)) {
|
if (isEditableTarget(event.target) || document.body.dataset.shortcutRecording === 'true') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = event.key.toLowerCase();
|
|
||||||
const { shortcuts } = shortcutSettingsStore;
|
const { shortcuts } = shortcutSettingsStore;
|
||||||
if (key === shortcuts.leftIncrement) {
|
if (isShortcutMatch(event, shortcuts.leftIncrement)) {
|
||||||
scoreboardStore.leftScore += 1;
|
scoreboardStore.leftScore += 1;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === shortcuts.leftDecrement) {
|
if (isShortcutMatch(event, shortcuts.leftDecrement)) {
|
||||||
scoreboardStore.leftScore = Math.max(0, scoreboardStore.leftScore - 1);
|
scoreboardStore.leftScore = Math.max(0, scoreboardStore.leftScore - 1);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === shortcuts.rightIncrement) {
|
if (isShortcutMatch(event, shortcuts.rightIncrement)) {
|
||||||
scoreboardStore.rightScore += 1;
|
scoreboardStore.rightScore += 1;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === shortcuts.rightDecrement) {
|
if (isShortcutMatch(event, shortcuts.rightDecrement)) {
|
||||||
scoreboardStore.rightScore = Math.max(0, scoreboardStore.rightScore - 1);
|
scoreboardStore.rightScore = Math.max(0, scoreboardStore.rightScore - 1);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,88 @@ import { defineStore } from 'pinia';
|
|||||||
import { reactive } from 'vue';
|
import { reactive } from 'vue';
|
||||||
|
|
||||||
export type ShortcutAction = 'leftIncrement' | 'leftDecrement' | 'rightIncrement' | 'rightDecrement';
|
export type ShortcutAction = 'leftIncrement' | 'leftDecrement' | 'rightIncrement' | 'rightDecrement';
|
||||||
|
|
||||||
export type ShortcutSettings = Record<ShortcutAction, string>;
|
export type ShortcutSettings = Record<ShortcutAction, string>;
|
||||||
|
|
||||||
|
type ShortcutParts = {
|
||||||
|
key: string;
|
||||||
|
altKey: boolean;
|
||||||
|
ctrlKey: boolean;
|
||||||
|
shiftKey: boolean;
|
||||||
|
metaKey: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = 'scoreko-dev.shortcut-settings';
|
const STORAGE_KEY = 'scoreko-dev.shortcut-settings';
|
||||||
|
|
||||||
const defaultShortcuts: ShortcutSettings = {
|
const defaultShortcuts: ShortcutSettings = {
|
||||||
leftIncrement: 'q',
|
leftIncrement: 'Q',
|
||||||
leftDecrement: 'a',
|
leftDecrement: 'A',
|
||||||
rightIncrement: 'p',
|
rightIncrement: 'P',
|
||||||
rightDecrement: 'l',
|
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') {
|
if (typeof value !== 'string') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = value.trim().toLowerCase();
|
const parts = value.split('+').map((part) => part.trim()).filter(Boolean);
|
||||||
return normalized.slice(0, 1);
|
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 normalizeSettings = (input: unknown): ShortcutSettings => {
|
||||||
const candidate = typeof input === 'object' && input !== null ? (input as Record<string, unknown>) : {};
|
const candidate = typeof input === 'object' && input !== null ? (input as Record<string, unknown>) : {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
leftIncrement: normalizeShortcutKey(candidate.leftIncrement) || defaultShortcuts.leftIncrement,
|
leftIncrement: normalizeShortcut(candidate.leftIncrement) || defaultShortcuts.leftIncrement,
|
||||||
leftDecrement: normalizeShortcutKey(candidate.leftDecrement) || defaultShortcuts.leftDecrement,
|
leftDecrement: normalizeShortcut(candidate.leftDecrement) || defaultShortcuts.leftDecrement,
|
||||||
rightIncrement: normalizeShortcutKey(candidate.rightIncrement) || defaultShortcuts.rightIncrement,
|
rightIncrement: normalizeShortcut(candidate.rightIncrement) || defaultShortcuts.rightIncrement,
|
||||||
rightDecrement: normalizeShortcutKey(candidate.rightDecrement) || defaultShortcuts.rightDecrement,
|
rightDecrement: normalizeShortcut(candidate.rightDecrement) || defaultShortcuts.rightDecrement,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +97,7 @@ const readStoredSettings = (): ShortcutSettings => {
|
|||||||
if (!raw) {
|
if (!raw) {
|
||||||
return { ...defaultShortcuts };
|
return { ...defaultShortcuts };
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizeSettings(JSON.parse(raw));
|
return normalizeSettings(JSON.parse(raw));
|
||||||
} catch {
|
} catch {
|
||||||
return { ...defaultShortcuts };
|
return { ...defaultShortcuts };
|
||||||
@@ -58,14 +112,57 @@ const persistSettings = (settings: ShortcutSettings) => {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
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', () => {
|
export const useShortcutSettingsStore = defineStore('shortcut-settings', () => {
|
||||||
const shortcuts = reactive<ShortcutSettings>(readStoredSettings());
|
const shortcuts = reactive<ShortcutSettings>(readStoredSettings());
|
||||||
|
|
||||||
const setShortcut = (action: ShortcutAction, value: string) => {
|
const setShortcut = (action: ShortcutAction, value: string) => {
|
||||||
const normalized = normalizeShortcutKey(value);
|
const normalized = normalizeShortcut(value);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcuts[action] = normalized;
|
shortcuts[action] = normalized;
|
||||||
persistSettings(shortcuts);
|
persistSettings(shortcuts);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, onBeforeUnmount, ref } from 'vue';
|
||||||
import { useHead } from '@unhead/vue';
|
import { useHead } from '@unhead/vue';
|
||||||
import type { Locale } from '../i18n';
|
import type { Locale } from '../i18n';
|
||||||
import { locale, setLocale, t } from '../i18n';
|
import { locale, setLocale, t } from '../i18n';
|
||||||
import type { ShortcutAction } from '../stores/shortcut-settings';
|
import {
|
||||||
import { useShortcutSettingsStore } from '../stores/shortcut-settings';
|
eventToShortcut,
|
||||||
|
type ShortcutAction,
|
||||||
|
useShortcutSettingsStore,
|
||||||
|
} from '../stores/shortcut-settings';
|
||||||
|
|
||||||
defineOptions({ name: 'SettingsView' });
|
defineOptions({ name: 'SettingsView' });
|
||||||
|
|
||||||
@@ -21,6 +24,7 @@ const selectedLanguage = computed<Locale>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const shortcutSettingsStore = useShortcutSettingsStore();
|
const shortcutSettingsStore = useShortcutSettingsStore();
|
||||||
|
const recordingAction = ref<ShortcutAction | null>(null);
|
||||||
|
|
||||||
const shortcutFields = computed<{ action: ShortcutAction; label: string; hint: string }[]>(() => [
|
const shortcutFields = computed<{ action: ShortcutAction; label: string; hint: string }[]>(() => [
|
||||||
{ action: 'leftIncrement', label: t('settingsShortcutLeftIncrementLabel'), hint: t('settingsShortcutLeftIncrementHint') },
|
{ action: 'leftIncrement', label: t('settingsShortcutLeftIncrementLabel'), hint: t('settingsShortcutLeftIncrementHint') },
|
||||||
@@ -29,10 +33,51 @@ const shortcutFields = computed<{ action: ShortcutAction; label: string; hint: s
|
|||||||
{ action: 'rightDecrement', label: t('settingsShortcutRightDecrementLabel'), hint: t('settingsShortcutRightDecrementHint') },
|
{ action: 'rightDecrement', label: t('settingsShortcutRightDecrementLabel'), hint: t('settingsShortcutRightDecrementHint') },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updateShortcut = (action: ShortcutAction, value: string | number | null) => {
|
const stopRecording = () => {
|
||||||
shortcutSettingsStore.setShortcut(action, String(value ?? ''));
|
recordingAction.value = null;
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
delete document.body.dataset.shortcutRecording;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRecordKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (!recordingAction.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcut = eventToShortcut(event);
|
||||||
|
if (!shortcut) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
shortcutSettingsStore.setShortcut(recordingAction.value, shortcut);
|
||||||
|
stopRecording();
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRecording = (action: ShortcutAction) => {
|
||||||
|
if (recordingAction.value === action) {
|
||||||
|
stopRecording();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordingAction.value = action;
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.body.dataset.shortcutRecording = 'true';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('keydown', onRecordKeydown);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.removeEventListener('keydown', onRecordKeydown);
|
||||||
|
}
|
||||||
|
stopRecording();
|
||||||
|
});
|
||||||
|
|
||||||
useHead(() => ({ title: t('settingsTitle') }));
|
useHead(() => ({ title: t('settingsTitle') }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -70,7 +115,7 @@ useHead(() => ({ title: t('settingsTitle') }));
|
|||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
class="q-pa-md"
|
class="q-pa-md"
|
||||||
style="max-width: 420px;"
|
style="max-width: 500px;"
|
||||||
>
|
>
|
||||||
<QCardSection class="q-pa-none">
|
<QCardSection class="q-pa-none">
|
||||||
<div class="text-subtitle1 q-mb-md">
|
<div class="text-subtitle1 q-mb-md">
|
||||||
@@ -87,12 +132,21 @@ useHead(() => ({ title: t('settingsTitle') }));
|
|||||||
:key="field.action"
|
:key="field.action"
|
||||||
:model-value="shortcutSettingsStore.shortcuts[field.action]"
|
:model-value="shortcutSettingsStore.shortcuts[field.action]"
|
||||||
outlined
|
outlined
|
||||||
maxlength="1"
|
readonly
|
||||||
:label="field.label"
|
:label="field.label"
|
||||||
@update:model-value="(value) => updateShortcut(field.action, value)"
|
|
||||||
>
|
>
|
||||||
|
<template #append>
|
||||||
|
<QBtn
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
:icon="recordingAction === field.action ? 'stop_circle' : 'keyboard'"
|
||||||
|
:color="recordingAction === field.action ? 'negative' : 'primary'"
|
||||||
|
@click="startRecording(field.action)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #hint>
|
<template #hint>
|
||||||
{{ field.hint }}
|
{{ recordingAction === field.action ? t('settingsShortcutRecordingHint') : field.hint }}
|
||||||
</template>
|
</template>
|
||||||
</QInput>
|
</QInput>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user