overlay for the hotkey
This commit is contained in:
@@ -187,8 +187,7 @@ npm run dev
|
|||||||
Open it in your browser:
|
Open it in your browser:
|
||||||
[http://localhost:8080/](http://localhost:8080/)
|
[http://localhost:8080/](http://localhost:8080/)
|
||||||
|
|
||||||
|
When done, do not forget to update the `CACHE_VERSION` in the `service-worker.js`. It is the indicator for the PWA that the new version is available.
|
||||||
When done, do not forget to update the cache version in the `service-worker.js`
|
|
||||||
```bash
|
```bash
|
||||||
ver=$(grep -oP "CACHE_VERSION = 'nexus-timer-cache-v\K[0-9]+" public/service-worker.js)
|
ver=$(grep -oP "CACHE_VERSION = 'nexus-timer-cache-v\K[0-9]+" public/service-worker.js)
|
||||||
sed -i "s/nexus-timer-cache-v$ver/nexus-timer-cache-v$((ver+1))/" public/service-worker.js
|
sed -i "s/nexus-timer-cache-v$ver/nexus-timer-cache-v$((ver+1))/" public/service-worker.js
|
||||||
@@ -212,4 +211,4 @@ View real-time logs
|
|||||||
```bash
|
```bash
|
||||||
journalctl -fu virt-nexus-timer.service
|
journalctl -fu virt-nexus-timer.service
|
||||||
```
|
```
|
||||||
The previously installed PWA should now offer an upgrade
|
The previously installed PWA should update automatically or offer an upgrade
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_VERSION = 'nexus-timer-cache-v4';
|
const CACHE_VERSION = 'nexus-timer-cache-v5';
|
||||||
const APP_SHELL_URLS = [
|
const APP_SHELL_URLS = [
|
||||||
// '/', // Let NetworkFirst handle '/'
|
// '/', // Let NetworkFirst handle '/'
|
||||||
'/manifest.json',
|
'/manifest.json',
|
||||||
|
|||||||
79
src/components/HotkeyCaptureOverlay.vue
Normal file
79
src/components/HotkeyCaptureOverlay.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center z-[100]"
|
||||||
|
@click.self="cancel"
|
||||||
|
@keydown.esc="cancel"
|
||||||
|
tabindex="0"
|
||||||
|
ref="overlay"
|
||||||
|
>
|
||||||
|
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-xl text-center">
|
||||||
|
<h3 class="text-2xl font-semibold text-gray-800 dark:text-gray-100 mb-4">
|
||||||
|
Press a Key
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
|
Waiting for a single key press to assign as the hotkey.
|
||||||
|
<br>
|
||||||
|
(Esc to cancel)
|
||||||
|
</p>
|
||||||
|
<div class="animate-pulse text-blue-500 dark:text-blue-400 text-xl">
|
||||||
|
Listening...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, nextTick, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: Boolean,
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['captured', 'cancel']);
|
||||||
|
|
||||||
|
const overlay = ref(null);
|
||||||
|
|
||||||
|
const handleKeyPress = (event) => {
|
||||||
|
if (!props.visible) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation(); // Prevent further propagation if overlay is active
|
||||||
|
|
||||||
|
let key = event.key;
|
||||||
|
if (event.code === 'Space') {
|
||||||
|
key = ' ';
|
||||||
|
} else if (key.length > 1 && key !== ' ') { // Ignore modifiers, Enter, Tab etc.
|
||||||
|
if (key === 'Escape') { // Handle Escape specifically for cancellation
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
key = key.toLowerCase();
|
||||||
|
emit('captured', key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.visible, async (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
await nextTick(); // Ensure overlay is in DOM
|
||||||
|
overlay.value?.focus(); // Focus the overlay to capture keydown events directly
|
||||||
|
document.addEventListener('keydown', handleKeyPress, { capture: true }); // Use capture phase
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('keydown', handleKeyPress, { capture: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', handleKeyPress, { capture: true });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Ensure overlay is above other modals if any (PlayerForm uses z-50) */
|
||||||
|
.z-\[100\] {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,20 +3,18 @@
|
|||||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl w-full max-w-md">
|
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl w-full max-w-md">
|
||||||
<h2 class="text-2xl font-semibold mb-4">{{ isEditing ? 'Edit Player' : 'Add New Player' }}</h2>
|
<h2 class="text-2xl font-semibold mb-4">{{ isEditing ? 'Edit Player' : 'Add New Player' }}</h2>
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<!-- Name -->
|
<!-- ... (Name, Remaining Time, Avatar sections remain the same) ... -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="playerName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
|
<label for="playerName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
|
||||||
<input type="text" id="playerName" v-model="editablePlayer.name" required class="input-base mt-1">
|
<input type="text" id="playerName" v-model="editablePlayer.name" required class="input-base mt-1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Remaining Time -->
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="remainingTime" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Remaining Time (MM:SS or -MM:SS)</label>
|
<label for="remainingTime" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Remaining Time (MM:SS or -MM:SS)</label>
|
||||||
<input type="text" id="remainingTime" v-model="currentTimeFormatted" @blur="validateCurrentTimeFormat" placeholder="e.g., 55:30 or -02:15" required class="input-base mt-1">
|
<input type="text" id="remainingTime" v-model="currentTimeFormatted" @blur="validateCurrentTimeFormat" placeholder="e.g., 55:30 or -02:15" required class="input-base mt-1">
|
||||||
<p v-if="currentTimeFormatError" class="text-red-500 text-xs mt-1">{{ currentTimeFormatError }}</p>
|
<p v-if="currentTimeFormatError" class="text-red-500 text-xs mt-1">{{ currentTimeFormatError }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Avatar -->
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Avatar</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Avatar</label>
|
||||||
<div class="mt-1 flex items-center">
|
<div class="mt-1 flex items-center">
|
||||||
@@ -36,39 +34,45 @@
|
|||||||
<p v-if="cameraError" class="text-red-500 text-xs mt-1">{{ cameraError }}</p>
|
<p v-if="cameraError" class="text-red-500 text-xs mt-1">{{ cameraError }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- "Pass Turn / My Pause" Hotkey -->
|
<!-- "Pass Turn / My Pause" Hotkey -->
|
||||||
<div class="flex items-center justify-between mb-6"> <!-- Added flex container and margin -->
|
<div class="flex items-center justify-between mb-6">
|
||||||
<label for="playerHotkey" class="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap mr-3">
|
<label for="playerHotkeyDisplay" class="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap mr-3">
|
||||||
"Pass Turn / My Pause" Hotkey:
|
"Pass Turn / My Pause" Hotkey:
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center"> <!-- Group input and clear button -->
|
<div class="flex items-center">
|
||||||
<input
|
<!-- Display field - not an input anymore, triggers overlay -->
|
||||||
type="text"
|
<button
|
||||||
id="playerHotkey"
|
type="button"
|
||||||
:value="editablePlayer.hotkey ? editablePlayer.hotkey.toUpperCase() : ''"
|
id="playerHotkeyDisplay"
|
||||||
@keydown.prevent="captureHotkey($event, 'player')"
|
@click="startCapturePlayerHotkey"
|
||||||
placeholder="-"
|
class="input-base w-12 h-8 text-center font-mono text-lg p-0 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
class="input-base w-12 h-8 text-center font-mono text-lg p-0"
|
|
||||||
readonly
|
|
||||||
maxlength="1"
|
|
||||||
>
|
>
|
||||||
|
{{ editablePlayer.hotkey ? editablePlayer.hotkey.toUpperCase() : '-' }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="clearHotkey('player')"
|
@click="clearHotkey('player')"
|
||||||
class="text-xs text-blue-500 hover:underline ml-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-600"
|
class="text-xs text-blue-500 hover:underline ml-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-600"
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="flex justify-end space-x-3 mt-6">
|
<div class="flex justify-end space-x-3 mt-6">
|
||||||
<button type="button" @click="closeModal" class="btn btn-secondary">Cancel</button>
|
<button type="button" @click="closeModal" class="btn btn-secondary">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary" :disabled="!!currentTimeFormatError">{{ isEditing ? 'Save Changes' : 'Add Player' }}</button>
|
<button type="submit" class="btn btn-primary" :disabled="!!currentTimeFormatError">{{ isEditing ? 'Save Changes' : 'Add Player' }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hotkey Capture Overlay for Player Hotkey -->
|
||||||
|
<HotkeyCaptureOverlay
|
||||||
|
:visible="isCapturingPlayerHotkey"
|
||||||
|
@captured="handlePlayerHotkeyCaptured"
|
||||||
|
@cancel="cancelCapturePlayerHotkey"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -78,32 +82,65 @@ import { useStore } from 'vuex';
|
|||||||
import { CameraService } from '../services/CameraService';
|
import { CameraService } from '../services/CameraService';
|
||||||
import { formatTime, parseTime } from '../utils/timeFormatter';
|
import { formatTime, parseTime } from '../utils/timeFormatter';
|
||||||
import DefaultAvatarIcon from './DefaultAvatarIcon.vue';
|
import DefaultAvatarIcon from './DefaultAvatarIcon.vue';
|
||||||
|
import HotkeyCaptureOverlay from './HotkeyCaptureOverlay.vue'; // Import overlay
|
||||||
import { AudioService } from '../services/AudioService';
|
import { AudioService } from '../services/AudioService';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({ player: Object });
|
||||||
player: Object,
|
|
||||||
});
|
|
||||||
const emit = defineEmits(['close', 'save']);
|
const emit = defineEmits(['close', 'save']);
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const DEFAULT_AVATAR_MARKER = null;
|
const DEFAULT_AVATAR_MARKER = null;
|
||||||
|
|
||||||
const isEditing = computed(() => !!props.player);
|
const isEditing = computed(() => !!props.player);
|
||||||
const editablePlayer = reactive({
|
const editablePlayer = reactive({ /* ... same as before ... */
|
||||||
id: null,
|
id: null, name: '', avatar: DEFAULT_AVATAR_MARKER, initialTimerSec: 3600, currentTimerSec: 3600, hotkey: '',
|
||||||
name: '',
|
|
||||||
avatar: DEFAULT_AVATAR_MARKER,
|
|
||||||
initialTimerSec: 3600,
|
|
||||||
currentTimerSec: 3600,
|
|
||||||
hotkey: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentTimeFormatted = ref('60:00');
|
const currentTimeFormatted = ref('60:00');
|
||||||
const currentTimeFormatError = ref('');
|
const currentTimeFormatError = ref('');
|
||||||
const cameraError = ref('');
|
const cameraError = ref('');
|
||||||
|
|
||||||
const maxNegativeSeconds = computed(() => store.getters.maxNegativeTimeReached);
|
const maxNegativeSeconds = computed(() => store.getters.maxNegativeTimeReached);
|
||||||
|
|
||||||
|
// --- State for Player Hotkey Capture ---
|
||||||
|
const isCapturingPlayerHotkey = ref(false);
|
||||||
|
|
||||||
|
const startCapturePlayerHotkey = () => {
|
||||||
|
isCapturingPlayerHotkey.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlayerHotkeyCaptured = (key) => {
|
||||||
|
isCapturingPlayerHotkey.value = false;
|
||||||
|
// Validate the captured key (conflicts etc.)
|
||||||
|
const existingPlayerHotkey = store.state.players.some(p => p.hotkey === key && p.id !== editablePlayer.id);
|
||||||
|
const globalHotkeyInUseStopPause = store.state.globalHotkeyStopPause === key;
|
||||||
|
const globalHotkeyInUseRunAll = store.state.globalHotkeyRunAll === key;
|
||||||
|
|
||||||
|
if (existingPlayerHotkey) {
|
||||||
|
alert(`Hotkey "${key.toUpperCase()}" is already assigned to another player.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (globalHotkeyInUseStopPause) {
|
||||||
|
alert(`Hotkey "${key.toUpperCase()}" is already assigned as the Global Stop/Pause hotkey.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (globalHotkeyInUseRunAll) {
|
||||||
|
alert(`Hotkey "${key.toUpperCase()}" is already assigned as the Global Run All Timers hotkey.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editablePlayer.hotkey = key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelCapturePlayerHotkey = () => {
|
||||||
|
isCapturingPlayerHotkey.value = false;
|
||||||
|
};
|
||||||
|
// --- End Player Hotkey Capture ---
|
||||||
|
|
||||||
|
// ... (onMounted, watch for currentTimeFormatted, validateCurrentTimeFormat, capturePhoto, useDefaultAvatar, submitForm, closeModal remain the same)
|
||||||
|
// No change to captureHotkey itself, as the overlay handles the key capture event
|
||||||
|
// The clearHotkey method is still relevant
|
||||||
|
function clearHotkey(type) { // 'type' is 'player' here
|
||||||
|
if (type === 'player') {
|
||||||
|
editablePlayer.hotkey = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (isEditing.value && props.player) {
|
if (isEditing.value && props.player) {
|
||||||
@@ -167,60 +204,18 @@ function useDefaultAvatar() {
|
|||||||
editablePlayer.avatar = DEFAULT_AVATAR_MARKER;
|
editablePlayer.avatar = DEFAULT_AVATAR_MARKER;
|
||||||
}
|
}
|
||||||
|
|
||||||
function captureHotkey(event, type) {
|
|
||||||
event.preventDefault();
|
|
||||||
let key = event.key;
|
|
||||||
if (event.code === 'Space') {
|
|
||||||
key = ' ';
|
|
||||||
} else if (key.length > 1 && key !== ' ') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
key = key.toLowerCase();
|
|
||||||
|
|
||||||
if (type === 'player') {
|
|
||||||
const existingPlayerHotkey = store.state.players.some(p => p.hotkey === key && p.id !== editablePlayer.id);
|
|
||||||
const globalHotkeyInUseStopPause = store.state.globalHotkeyStopPause === key;
|
|
||||||
const globalHotkeyInUseRunAll = store.state.globalHotkeyRunAll === key;
|
|
||||||
|
|
||||||
if (existingPlayerHotkey) {
|
|
||||||
alert(`Hotkey "${key.toUpperCase()}" is already assigned to another player.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (globalHotkeyInUseStopPause) {
|
|
||||||
alert(`Hotkey "${key.toUpperCase()}" is already assigned as the Global Stop/Pause hotkey.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (globalHotkeyInUseRunAll) {
|
|
||||||
alert(`Hotkey "${key.toUpperCase()}" is already assigned as the Global Run All Timers hotkey.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editablePlayer.hotkey = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function clearHotkey(type) {
|
|
||||||
if (type === 'player') {
|
|
||||||
editablePlayer.hotkey = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitForm() {
|
function submitForm() {
|
||||||
validateCurrentTimeFormat();
|
validateCurrentTimeFormat();
|
||||||
if (currentTimeFormatError.value) return;
|
if (currentTimeFormatError.value) return;
|
||||||
|
|
||||||
const playerPayload = { ...editablePlayer };
|
const playerPayload = { ...editablePlayer };
|
||||||
|
|
||||||
if (playerPayload.currentTimerSec <= maxNegativeSeconds.value) {
|
if (playerPayload.currentTimerSec <= maxNegativeSeconds.value) {
|
||||||
playerPayload.isSkipped = true;
|
playerPayload.isSkipped = true;
|
||||||
} else if (playerPayload.isSkipped && playerPayload.currentTimerSec > maxNegativeSeconds.value) {
|
} else if (playerPayload.isSkipped && playerPayload.currentTimerSec > maxNegativeSeconds.value) {
|
||||||
playerPayload.isSkipped = false;
|
playerPayload.isSkipped = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEditing.value) {
|
if (!isEditing.value) {
|
||||||
playerPayload.initialTimerSec = playerPayload.currentTimerSec;
|
playerPayload.initialTimerSec = playerPayload.currentTimerSec;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('save', playerPayload);
|
emit('save', playerPayload);
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
@@ -228,4 +223,5 @@ function submitForm() {
|
|||||||
function closeModal() {
|
function closeModal() {
|
||||||
emit('close');
|
emit('close');
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-grow flex flex-col p-4 md:p-6 lg:p-8 items-center dark:bg-gray-800">
|
<div class="flex-grow flex flex-col p-4 md:p-6 lg:p-8 items-center dark:bg-gray-800">
|
||||||
|
<!-- ... (header, Player Management section remain the same) ... -->
|
||||||
<header class="w-full max-w-3xl mb-6 text-center">
|
<header class="w-full max-w-3xl mb-6 text-center">
|
||||||
<h1 class="text-4xl font-bold text-blue-600 dark:text-blue-400">Nexus Timer Setup</h1>
|
<h1 class="text-4xl font-bold text-blue-600 dark:text-blue-400">Nexus Timer Setup</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Player Management -->
|
|
||||||
<section class="w-full max-w-3xl bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md mb-6">
|
<section class="w-full max-w-3xl bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md mb-6">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h2 class="text-2xl font-semibold">Players ({{ players.length }})</h2>
|
<h2 class="text-2xl font-semibold">Players ({{ players.length }})</h2>
|
||||||
<button @click="openAddPlayerModal" class="btn btn-primary" :disabled="players.length >= 99">
|
<button @click="openAddPlayerModal" class="btn btn-primary" :disabled="players.length >= 99">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" /></svg>
|
||||||
Add Player
|
Add Player
|
||||||
@@ -15,28 +15,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="players.length < 2" class="text-sm text-yellow-600 dark:text-yellow-400 mb-3">At least 2 players are required to start.</p>
|
<p v-if="players.length < 2" class="text-sm text-yellow-600 dark:text-yellow-400 mb-3">At least 2 players are required to start.</p>
|
||||||
|
|
||||||
<div v-if="players.length > 0" class="space-y-3 mb-4">
|
<div v-if="players.length > 0" class="space-y-3 mb-4">
|
||||||
<div v-for="(player, index) in players" :key="player.id" class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-600 rounded shadow-sm">
|
<div v-for="(player, index) in players" :key="player.id" class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-600 rounded shadow-sm">
|
||||||
<div class="flex items-center flex-wrap">
|
<div class="flex items-center flex-wrap">
|
||||||
<!-- *** START AVATAR LOGIC *** -->
|
|
||||||
<img
|
<img
|
||||||
v-if="player.avatar"
|
v-if="player.avatar"
|
||||||
:src="player.avatar"
|
:src="player.avatar"
|
||||||
alt="Player Avatar"
|
alt="Player Avatar"
|
||||||
class="w-10 h-10 rounded-full object-cover mr-3 flex-shrink-0"
|
class="w-10 h-10 rounded-full object-cover mr-3"
|
||||||
/>
|
/>
|
||||||
<DefaultAvatarIcon
|
<DefaultAvatarIcon
|
||||||
v-else
|
v-else
|
||||||
class="w-10 h-10 rounded-full object-cover mr-3 text-gray-400 bg-gray-200 dark:bg-gray-700 p-0.5 flex-shrink-0"
|
class="w-10 h-10 rounded-full object-cover mr-3 text-gray-400 bg-gray-200 p-0.5"
|
||||||
/>
|
/>
|
||||||
<!-- *** END AVATAR LOGIC *** -->
|
|
||||||
<span class="font-medium mr-2">{{ player.name }}</span>
|
<span class="font-medium mr-2">{{ player.name }}</span>
|
||||||
<span class="mr-2 text-xs text-gray-500 dark:text-gray-400">({{ formatTime(player.currentTimerSec) }})</span>
|
<span class="mr-2 text-xs text-gray-500 dark:text-gray-400">({{ formatTime(player.currentTimerSec) }})</span>
|
||||||
<span v-if="player.hotkey" class="text-xs px-1.5 py-0.5 bg-blue-100 dark:bg-blue-700 text-blue-700 dark:text-blue-200 rounded">Hotkey: {{ player.hotkey.toUpperCase() }}</span>
|
<span v-if="player.hotkey" class="text-xs px-1.5 py-0.5 bg-blue-100 dark:bg-blue-700 text-blue-700 dark:text-blue-200 rounded">Hotkey: {{ player.hotkey.toUpperCase() }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-1 flex items-center flex-shrink-0">
|
<div class="space-x-1 flex items-center flex-shrink-0">
|
||||||
<!-- Action buttons -->
|
<button @click="movePlayerUp(index)" :disabled="index === 0" class="btn-icon text-gray-600 dark:text-gray-300 hover:text-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
<button @click="movePlayerUp(index)" :disabled="index === 0" class="btn-icon text-gray-600 dark:text-gray-300 hover:text-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -64,51 +61,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Game Settings -->
|
<!-- Global Hotkeys Section -->
|
||||||
<section class="w-full max-w-3xl bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md mb-6">
|
<section class="w-full max-w-3xl bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md mb-6">
|
||||||
<!-- ... Global hotkey inputs ... -->
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<label for="globalHotkeyStopPause" class="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap mr-3">
|
<label for="globalHotkeyStopPauseDisplay" class="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap mr-3">
|
||||||
Global "Stop/Pause All" Hotkey:
|
Global "Stop/Pause All" Hotkey:
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<button
|
||||||
type="text"
|
type="button"
|
||||||
id="globalHotkeyStopPause"
|
id="globalHotkeyStopPauseDisplay"
|
||||||
:value="globalHotkeyStopPauseDisplay"
|
@click="startCaptureGlobalHotkey('stopPause')"
|
||||||
@keydown.prevent="captureGlobalHotkey($event, 'stopPause')"
|
class="input-base w-12 h-8 text-center font-mono text-lg p-0 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
placeholder="-"
|
|
||||||
class="input-base w-12 h-8 text-center font-mono text-lg p-0"
|
|
||||||
readonly
|
|
||||||
maxlength="1"
|
|
||||||
>
|
>
|
||||||
|
{{ globalHotkeyStopPauseDisplay || '-' }}
|
||||||
|
</button>
|
||||||
<button type="button" @click="clearGlobalHotkey('stopPause')" class="text-xs text-blue-500 hover:underline ml-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
|
<button type="button" @click="clearGlobalHotkey('stopPause')" class="text-xs text-blue-500 hover:underline ml-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<label for="globalHotkeyRunAll" class="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap mr-3">
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<label for="globalHotkeyRunAllDisplay" class="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap mr-3">
|
||||||
Global "Run All Timers" Hotkey:
|
Global "Run All Timers" Hotkey:
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<button
|
||||||
type="text"
|
type="button"
|
||||||
id="globalHotkeyRunAll"
|
id="globalHotkeyRunAllDisplay"
|
||||||
:value="globalHotkeyRunAllDisplay"
|
@click="startCaptureGlobalHotkey('runAll')"
|
||||||
@keydown.prevent="captureGlobalHotkey($event, 'runAll')"
|
class="input-base w-12 h-8 text-center font-mono text-lg p-0 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
placeholder="-"
|
|
||||||
class="input-base w-12 h-8 text-center font-mono text-lg p-0"
|
|
||||||
readonly
|
|
||||||
maxlength="1"
|
|
||||||
>
|
>
|
||||||
|
{{ globalHotkeyRunAllDisplay || '-' }}
|
||||||
|
</button>
|
||||||
<button type="button" @click="clearGlobalHotkey('runAll')" class="text-xs text-blue-500 hover:underline ml-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
|
<button type="button" @click="clearGlobalHotkey('runAll')" class="text-xs text-blue-500 hover:underline ml-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- ... Dark Mode / Mute ... -->
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
<!-- Dark Mode and Mute Audio -->
|
||||||
|
<div class="flex items-center justify-between mb-4 mt-6">
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Dark Mode</span>
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Dark Mode</span>
|
||||||
<button @click="toggleTheme" class="px-3 py-1.5 rounded-md text-sm font-medium" :class="theme === 'dark' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-800'">
|
<button @click="toggleTheme" class="px-3 py-1.5 rounded-md text-sm font-medium" :class="theme === 'dark' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-800'">
|
||||||
{{ theme === 'dark' ? 'On' : 'Off' }}
|
{{ theme === 'dark' ? 'On' : 'Off' }}
|
||||||
@@ -122,8 +116,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- ... (Action Buttons section remains the same) ... -->
|
||||||
<section class="w-full max-w-3xl mt-4 space-y-3">
|
<section class="w-full max-w-3xl mt-4 space-y-3">
|
||||||
<button @click="saveAndClose" class="w-full btn btn-primary btn-lg text-xl py-3" :disabled="players.length < 2 && players.length !==0">
|
<button @click="saveAndClose" class="w-full btn btn-primary btn-lg text-xl py-3" :disabled="players.length < 2 && players.length !==0">
|
||||||
Save & Close
|
Save & Close
|
||||||
</button>
|
</button>
|
||||||
@@ -135,13 +129,19 @@
|
|||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Player Form Modal -->
|
|
||||||
<PlayerForm
|
<PlayerForm
|
||||||
v-if="showPlayerModal"
|
v-if="showPlayerModal"
|
||||||
:player="editingPlayer"
|
:player="editingPlayer"
|
||||||
@close="closePlayerModal"
|
@close="closePlayerModal"
|
||||||
@save="savePlayer"
|
@save="savePlayer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Hotkey Capture Overlay for Global Hotkeys -->
|
||||||
|
<HotkeyCaptureOverlay
|
||||||
|
:visible="isCapturingGlobalHotkey"
|
||||||
|
@captured="handleGlobalHotkeyCaptured"
|
||||||
|
@cancel="cancelCaptureGlobalHotkey"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -152,6 +152,7 @@ import { useRouter } from 'vue-router';
|
|||||||
import PlayerForm from '../components/PlayerForm.vue';
|
import PlayerForm from '../components/PlayerForm.vue';
|
||||||
import { formatTime } from '../utils/timeFormatter';
|
import { formatTime } from '../utils/timeFormatter';
|
||||||
import DefaultAvatarIcon from '../components/DefaultAvatarIcon.vue';
|
import DefaultAvatarIcon from '../components/DefaultAvatarIcon.vue';
|
||||||
|
import HotkeyCaptureOverlay from '../components/HotkeyCaptureOverlay.vue'; // Import overlay
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -159,94 +160,29 @@ const router = useRouter();
|
|||||||
const players = computed(() => store.getters.players);
|
const players = computed(() => store.getters.players);
|
||||||
const theme = computed(() => store.getters.theme);
|
const theme = computed(() => store.getters.theme);
|
||||||
const isMuted = computed(() => store.getters.isMuted);
|
const isMuted = computed(() => store.getters.isMuted);
|
||||||
|
|
||||||
const globalHotkeyStopPause = computed(() => store.getters.globalHotkeyStopPause);
|
const globalHotkeyStopPause = computed(() => store.getters.globalHotkeyStopPause);
|
||||||
const globalHotkeyStopPauseDisplay = computed(() => globalHotkeyStopPause.value ? globalHotkeyStopPause.value.toUpperCase() : '');
|
const globalHotkeyStopPauseDisplay = computed(() => globalHotkeyStopPause.value ? globalHotkeyStopPause.value.toUpperCase() : '');
|
||||||
|
|
||||||
const globalHotkeyRunAll = computed(() => store.getters.globalHotkeyRunAll);
|
const globalHotkeyRunAll = computed(() => store.getters.globalHotkeyRunAll);
|
||||||
const globalHotkeyRunAllDisplay = computed(() => globalHotkeyRunAll.value ? globalHotkeyRunAll.value.toUpperCase() : '');
|
const globalHotkeyRunAllDisplay = computed(() => globalHotkeyRunAll.value ? globalHotkeyRunAll.value.toUpperCase() : '');
|
||||||
|
|
||||||
const showPlayerModal = ref(false);
|
const showPlayerModal = ref(false);
|
||||||
const editingPlayer = ref(null);
|
const editingPlayer = ref(null);
|
||||||
|
|
||||||
const openAddPlayerModal = () => {
|
// --- State for Global Hotkey Capture ---
|
||||||
if (players.value.length < 99) {
|
const isCapturingGlobalHotkey = ref(false);
|
||||||
editingPlayer.value = null;
|
const currentGlobalHotkeyType = ref(null); // 'stopPause' or 'runAll'
|
||||||
showPlayerModal.value = true;
|
|
||||||
} else {
|
const startCaptureGlobalHotkey = (type) => {
|
||||||
alert("Maximum player limit (99) reached.");
|
currentGlobalHotkeyType.value = type;
|
||||||
}
|
isCapturingGlobalHotkey.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditPlayerModal = (player) => {
|
const handleGlobalHotkeyCaptured = (key) => {
|
||||||
editingPlayer.value = player;
|
isCapturingGlobalHotkey.value = false;
|
||||||
showPlayerModal.value = true;
|
const type = currentGlobalHotkeyType.value;
|
||||||
};
|
if (!type) return;
|
||||||
|
|
||||||
const closePlayerModal = () => {
|
|
||||||
showPlayerModal.value = false;
|
|
||||||
editingPlayer.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const savePlayer = (playerData) => {
|
|
||||||
if (playerData.id) {
|
|
||||||
store.dispatch('updatePlayer', playerData);
|
|
||||||
} else {
|
|
||||||
store.dispatch('addPlayer', {
|
|
||||||
name: playerData.name,
|
|
||||||
avatar: playerData.avatar,
|
|
||||||
initialTimerSec: playerData.initialTimerSec,
|
|
||||||
currentTimerSec: playerData.currentTimerSec,
|
|
||||||
hotkey: playerData.hotkey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
closePlayerModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDeletePlayer = (playerId) => {
|
|
||||||
if (window.confirm('Are you sure you want to delete this player?')) {
|
|
||||||
store.dispatch('deletePlayer', playerId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const shufflePlayers = () => {
|
|
||||||
store.dispatch('shufflePlayers');
|
|
||||||
};
|
|
||||||
|
|
||||||
const reversePlayers = () => {
|
|
||||||
store.dispatch('reversePlayers');
|
|
||||||
};
|
|
||||||
|
|
||||||
const movePlayerUp = (index) => {
|
|
||||||
if (index > 0) {
|
|
||||||
const newPlayersOrder = [...players.value];
|
|
||||||
const temp = newPlayersOrder[index];
|
|
||||||
newPlayersOrder[index] = newPlayersOrder[index - 1];
|
|
||||||
newPlayersOrder[index - 1] = temp;
|
|
||||||
store.dispatch('reorderPlayers', newPlayersOrder);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const movePlayerDown = (index) => {
|
|
||||||
if (index < players.value.length - 1) {
|
|
||||||
const newPlayersOrder = [...players.value];
|
|
||||||
const temp = newPlayersOrder[index];
|
|
||||||
newPlayersOrder[index] = newPlayersOrder[index + 1];
|
|
||||||
newPlayersOrder[index + 1] = temp;
|
|
||||||
store.dispatch('reorderPlayers', newPlayersOrder);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const captureGlobalHotkey = (event, type) => {
|
|
||||||
event.preventDefault();
|
|
||||||
let key = event.key;
|
|
||||||
if (event.code === 'Space') {
|
|
||||||
key = ' ';
|
|
||||||
} else if (key.length > 1 && key !== ' ') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
key = key.toLowerCase();
|
|
||||||
|
|
||||||
|
// Validate captured key
|
||||||
const isPlayerHotkey = store.state.players.some(p => p.hotkey === key);
|
const isPlayerHotkey = store.state.players.some(p => p.hotkey === key);
|
||||||
if (isPlayerHotkey) {
|
if (isPlayerHotkey) {
|
||||||
alert(`Hotkey "${key.toUpperCase()}" is already assigned to a player.`);
|
alert(`Hotkey "${key.toUpperCase()}" is already assigned to a player.`);
|
||||||
@@ -266,8 +202,74 @@ const captureGlobalHotkey = (event, type) => {
|
|||||||
}
|
}
|
||||||
store.dispatch('setGlobalHotkeyRunAll', key);
|
store.dispatch('setGlobalHotkeyRunAll', key);
|
||||||
}
|
}
|
||||||
|
currentGlobalHotkeyType.value = null; // Reset type
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cancelCaptureGlobalHotkey = () => {
|
||||||
|
isCapturingGlobalHotkey.value = false;
|
||||||
|
currentGlobalHotkeyType.value = null;
|
||||||
|
};
|
||||||
|
// --- End Global Hotkey Capture ---
|
||||||
|
|
||||||
|
// ... (openAddPlayerModal, openEditPlayerModal, closePlayerModal, savePlayer, confirmDeletePlayer, shufflePlayers, reversePlayers, movePlayerUp, movePlayerDown remain the same)
|
||||||
|
const openAddPlayerModal = () => {
|
||||||
|
if (players.value.length < 99) {
|
||||||
|
editingPlayer.value = null;
|
||||||
|
showPlayerModal.value = true;
|
||||||
|
} else {
|
||||||
|
alert("Maximum player limit (99) reached.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const openEditPlayerModal = (player) => {
|
||||||
|
editingPlayer.value = player;
|
||||||
|
showPlayerModal.value = true;
|
||||||
|
};
|
||||||
|
const closePlayerModal = () => {
|
||||||
|
showPlayerModal.value = false;
|
||||||
|
editingPlayer.value = null;
|
||||||
|
};
|
||||||
|
const savePlayer = (playerData) => {
|
||||||
|
if (playerData.id) {
|
||||||
|
store.dispatch('updatePlayer', playerData);
|
||||||
|
} else {
|
||||||
|
store.dispatch('addPlayer', {
|
||||||
|
name: playerData.name,
|
||||||
|
avatar: playerData.avatar,
|
||||||
|
initialTimerSec: playerData.initialTimerSec,
|
||||||
|
currentTimerSec: playerData.currentTimerSec,
|
||||||
|
hotkey: playerData.hotkey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
closePlayerModal();
|
||||||
|
};
|
||||||
|
const confirmDeletePlayer = (playerId) => {
|
||||||
|
if (window.confirm('Are you sure you want to delete this player?')) {
|
||||||
|
store.dispatch('deletePlayer', playerId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const shufflePlayers = () => { store.dispatch('shufflePlayers'); };
|
||||||
|
const reversePlayers = () => { store.dispatch('reversePlayers'); };
|
||||||
|
const movePlayerUp = (index) => { /* ... as before ... */
|
||||||
|
if (index > 0) {
|
||||||
|
const newPlayersOrder = [...players.value];
|
||||||
|
const temp = newPlayersOrder[index];
|
||||||
|
newPlayersOrder[index] = newPlayersOrder[index - 1];
|
||||||
|
newPlayersOrder[index - 1] = temp;
|
||||||
|
store.dispatch('reorderPlayers', newPlayersOrder);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const movePlayerDown = (index) => { /* ... as before ... */
|
||||||
|
if (index < players.value.length - 1) {
|
||||||
|
const newPlayersOrder = [...players.value];
|
||||||
|
const temp = newPlayersOrder[index];
|
||||||
|
newPlayersOrder[index] = newPlayersOrder[index + 1];
|
||||||
|
newPlayersOrder[index + 1] = temp;
|
||||||
|
store.dispatch('reorderPlayers', newPlayersOrder);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// The old captureGlobalHotkey is replaced by startCaptureGlobalHotkey and handleGlobalHotkeyCaptured
|
||||||
const clearGlobalHotkey = (type) => {
|
const clearGlobalHotkey = (type) => {
|
||||||
if (type === 'stopPause') {
|
if (type === 'stopPause') {
|
||||||
store.dispatch('setGlobalHotkeyStopPause', null);
|
store.dispatch('setGlobalHotkeyStopPause', null);
|
||||||
@@ -276,15 +278,10 @@ const clearGlobalHotkey = (type) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleTheme = () => {
|
// ... (toggleTheme, toggleMute, saveAndClose, resetPlayerTimersConfirm, fullResetAppConfirm remain the same)
|
||||||
store.dispatch('toggleTheme');
|
const toggleTheme = () => { store.dispatch('toggleTheme'); };
|
||||||
};
|
const toggleMute = () => { store.dispatch('setMuted', !isMuted.value); };
|
||||||
|
const saveAndClose = () => { /* ... as before ... */
|
||||||
const toggleMute = () => {
|
|
||||||
store.dispatch('setMuted', !isMuted.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveAndClose = () => {
|
|
||||||
store.dispatch('saveState');
|
store.dispatch('saveState');
|
||||||
if (players.value.length >= 2) {
|
if (players.value.length >= 2) {
|
||||||
if (store.state.currentPlayerIndex >= players.value.length || store.state.currentPlayerIndex < 0) {
|
if (store.state.currentPlayerIndex >= players.value.length || store.state.currentPlayerIndex < 0) {
|
||||||
@@ -299,16 +296,15 @@ const saveAndClose = () => {
|
|||||||
alert('At least 2 players are required to start a game.');
|
alert('At least 2 players are required to start a game.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const resetPlayerTimersConfirm = () => { /* ... as before ... */
|
||||||
const resetPlayerTimersConfirm = () => {
|
|
||||||
if (window.confirm('Are you sure you want to reset all current players\' timers to their initial values? This will not delete players.')) {
|
if (window.confirm('Are you sure you want to reset all current players\' timers to their initial values? This will not delete players.')) {
|
||||||
store.dispatch('resetGame');
|
store.dispatch('resetGame');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const fullResetAppConfirm = () => { /* ... as before ... */
|
||||||
const fullResetAppConfirm = () => {
|
|
||||||
if (window.confirm('Are you sure you want to reset all app data? This includes all players, settings, and timer states. The app will revert to its default state with 2 predefined players.')) {
|
if (window.confirm('Are you sure you want to reset all app data? This includes all players, settings, and timer states. The app will revert to its default state with 2 predefined players.')) {
|
||||||
store.dispatch('fullResetApp');
|
store.dispatch('fullResetApp');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
Reference in New Issue
Block a user