overlay for the hotkey
This commit is contained in:
@@ -187,8 +187,7 @@ npm run dev
|
||||
Open it in your browser:
|
||||
[http://localhost:8080/](http://localhost:8080/)
|
||||
|
||||
|
||||
When done, do not forget to update the cache version in the `service-worker.js`
|
||||
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.
|
||||
```bash
|
||||
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
|
||||
@@ -212,4 +211,4 @@ View real-time logs
|
||||
```bash
|
||||
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 = [
|
||||
// '/', // Let NetworkFirst handle '/'
|
||||
'/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">
|
||||
<h2 class="text-2xl font-semibold mb-4">{{ isEditing ? 'Edit Player' : 'Add New Player' }}</h2>
|
||||
<form @submit.prevent="submitForm">
|
||||
<!-- Name -->
|
||||
<!-- ... (Name, Remaining Time, Avatar sections remain the same) ... -->
|
||||
<div class="mb-4">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<!-- Remaining Time -->
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Avatar</label>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- "Pass Turn / My Pause" Hotkey -->
|
||||
<div class="flex items-center justify-between mb-6"> <!-- Added flex container and margin -->
|
||||
<label for="playerHotkey" 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="playerHotkeyDisplay" class="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap mr-3">
|
||||
"Pass Turn / My Pause" Hotkey:
|
||||
</label>
|
||||
<div class="flex items-center"> <!-- Group input and clear button -->
|
||||
<input
|
||||
type="text"
|
||||
id="playerHotkey"
|
||||
:value="editablePlayer.hotkey ? editablePlayer.hotkey.toUpperCase() : ''"
|
||||
@keydown.prevent="captureHotkey($event, 'player')"
|
||||
placeholder="-"
|
||||
class="input-base w-12 h-8 text-center font-mono text-lg p-0"
|
||||
readonly
|
||||
maxlength="1"
|
||||
<div class="flex items-center">
|
||||
<!-- Display field - not an input anymore, triggers overlay -->
|
||||
<button
|
||||
type="button"
|
||||
id="playerHotkeyDisplay"
|
||||
@click="startCapturePlayerHotkey"
|
||||
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"
|
||||
>
|
||||
{{ editablePlayer.hotkey ? editablePlayer.hotkey.toUpperCase() : '-' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Hotkey Capture Overlay for Player Hotkey -->
|
||||
<HotkeyCaptureOverlay
|
||||
:visible="isCapturingPlayerHotkey"
|
||||
@captured="handlePlayerHotkeyCaptured"
|
||||
@cancel="cancelCapturePlayerHotkey"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -78,32 +82,65 @@ import { useStore } from 'vuex';
|
||||
import { CameraService } from '../services/CameraService';
|
||||
import { formatTime, parseTime } from '../utils/timeFormatter';
|
||||
import DefaultAvatarIcon from './DefaultAvatarIcon.vue';
|
||||
import HotkeyCaptureOverlay from './HotkeyCaptureOverlay.vue'; // Import overlay
|
||||
import { AudioService } from '../services/AudioService';
|
||||
|
||||
const props = defineProps({
|
||||
player: Object,
|
||||
});
|
||||
const props = defineProps({ player: Object });
|
||||
const emit = defineEmits(['close', 'save']);
|
||||
|
||||
const store = useStore();
|
||||
const DEFAULT_AVATAR_MARKER = null;
|
||||
|
||||
const isEditing = computed(() => !!props.player);
|
||||
const editablePlayer = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
avatar: DEFAULT_AVATAR_MARKER,
|
||||
initialTimerSec: 3600,
|
||||
currentTimerSec: 3600,
|
||||
hotkey: '',
|
||||
const editablePlayer = reactive({ /* ... same as before ... */
|
||||
id: null, name: '', avatar: DEFAULT_AVATAR_MARKER, initialTimerSec: 3600, currentTimerSec: 3600, hotkey: '',
|
||||
});
|
||||
|
||||
const currentTimeFormatted = ref('60:00');
|
||||
const currentTimeFormatError = ref('');
|
||||
const cameraError = ref('');
|
||||
|
||||
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(() => {
|
||||
if (isEditing.value && props.player) {
|
||||
@@ -167,60 +204,18 @@ function useDefaultAvatar() {
|
||||
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() {
|
||||
validateCurrentTimeFormat();
|
||||
if (currentTimeFormatError.value) return;
|
||||
|
||||
const playerPayload = { ...editablePlayer };
|
||||
|
||||
if (playerPayload.currentTimerSec <= maxNegativeSeconds.value) {
|
||||
playerPayload.isSkipped = true;
|
||||
} else if (playerPayload.isSkipped && playerPayload.currentTimerSec > maxNegativeSeconds.value) {
|
||||
playerPayload.isSkipped = false;
|
||||
}
|
||||
|
||||
if (!isEditing.value) {
|
||||
playerPayload.initialTimerSec = playerPayload.currentTimerSec;
|
||||
}
|
||||
|
||||
emit('save', playerPayload);
|
||||
closeModal();
|
||||
}
|
||||
@@ -228,4 +223,5 @@ function submitForm() {
|
||||
function closeModal() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<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">
|
||||
<h1 class="text-4xl font-bold text-blue-600 dark:text-blue-400">Nexus Timer Setup</h1>
|
||||
</header>
|
||||
|
||||
<!-- Player Management -->
|
||||
<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">
|
||||
<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">
|
||||
<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
|
||||
@@ -15,28 +15,25 @@
|
||||
</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>
|
||||
|
||||
<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 class="flex items-center flex-wrap">
|
||||
<!-- *** START AVATAR LOGIC *** -->
|
||||
<img
|
||||
v-if="player.avatar"
|
||||
:src="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
|
||||
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="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>
|
||||
</div>
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
@@ -64,51 +61,48 @@
|
||||
</div>
|
||||
</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">
|
||||
<!-- ... Global hotkey inputs ... -->
|
||||
<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:
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
id="globalHotkeyStopPause"
|
||||
:value="globalHotkeyStopPauseDisplay"
|
||||
@keydown.prevent="captureGlobalHotkey($event, 'stopPause')"
|
||||
placeholder="-"
|
||||
class="input-base w-12 h-8 text-center font-mono text-lg p-0"
|
||||
readonly
|
||||
maxlength="1"
|
||||
<button
|
||||
type="button"
|
||||
id="globalHotkeyStopPauseDisplay"
|
||||
@click="startCaptureGlobalHotkey('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"
|
||||
>
|
||||
{{ 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">
|
||||
Clear
|
||||
</button>
|
||||
</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:
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
id="globalHotkeyRunAll"
|
||||
:value="globalHotkeyRunAllDisplay"
|
||||
@keydown.prevent="captureGlobalHotkey($event, 'runAll')"
|
||||
placeholder="-"
|
||||
class="input-base w-12 h-8 text-center font-mono text-lg p-0"
|
||||
readonly
|
||||
maxlength="1"
|
||||
<button
|
||||
type="button"
|
||||
id="globalHotkeyRunAllDisplay"
|
||||
@click="startCaptureGlobalHotkey('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"
|
||||
>
|
||||
{{ 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">
|
||||
Clear
|
||||
</button>
|
||||
</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>
|
||||
<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' }}
|
||||
@@ -122,8 +116,8 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Actions -->
|
||||
<section class="w-full max-w-3xl mt-4 space-y-3">
|
||||
<!-- ... (Action Buttons section remains the same) ... -->
|
||||
<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">
|
||||
Save & Close
|
||||
</button>
|
||||
@@ -135,13 +129,19 @@
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- Player Form Modal -->
|
||||
<PlayerForm
|
||||
v-if="showPlayerModal"
|
||||
:player="editingPlayer"
|
||||
@close="closePlayerModal"
|
||||
@save="savePlayer"
|
||||
/>
|
||||
|
||||
<!-- Hotkey Capture Overlay for Global Hotkeys -->
|
||||
<HotkeyCaptureOverlay
|
||||
:visible="isCapturingGlobalHotkey"
|
||||
@captured="handleGlobalHotkeyCaptured"
|
||||
@cancel="cancelCaptureGlobalHotkey"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -152,6 +152,7 @@ import { useRouter } from 'vue-router';
|
||||
import PlayerForm from '../components/PlayerForm.vue';
|
||||
import { formatTime } from '../utils/timeFormatter';
|
||||
import DefaultAvatarIcon from '../components/DefaultAvatarIcon.vue';
|
||||
import HotkeyCaptureOverlay from '../components/HotkeyCaptureOverlay.vue'; // Import overlay
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
@@ -159,94 +160,29 @@ const router = useRouter();
|
||||
const players = computed(() => store.getters.players);
|
||||
const theme = computed(() => store.getters.theme);
|
||||
const isMuted = computed(() => store.getters.isMuted);
|
||||
|
||||
const globalHotkeyStopPause = computed(() => store.getters.globalHotkeyStopPause);
|
||||
const globalHotkeyStopPauseDisplay = computed(() => globalHotkeyStopPause.value ? globalHotkeyStopPause.value.toUpperCase() : '');
|
||||
|
||||
const globalHotkeyRunAll = computed(() => store.getters.globalHotkeyRunAll);
|
||||
const globalHotkeyRunAllDisplay = computed(() => globalHotkeyRunAll.value ? globalHotkeyRunAll.value.toUpperCase() : '');
|
||||
|
||||
const showPlayerModal = ref(false);
|
||||
const editingPlayer = ref(null);
|
||||
|
||||
const openAddPlayerModal = () => {
|
||||
if (players.value.length < 99) {
|
||||
editingPlayer.value = null;
|
||||
showPlayerModal.value = true;
|
||||
} else {
|
||||
alert("Maximum player limit (99) reached.");
|
||||
}
|
||||
// --- State for Global Hotkey Capture ---
|
||||
const isCapturingGlobalHotkey = ref(false);
|
||||
const currentGlobalHotkeyType = ref(null); // 'stopPause' or 'runAll'
|
||||
|
||||
const startCaptureGlobalHotkey = (type) => {
|
||||
currentGlobalHotkeyType.value = type;
|
||||
isCapturingGlobalHotkey.value = true;
|
||||
};
|
||||
|
||||
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) => {
|
||||
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();
|
||||
const handleGlobalHotkeyCaptured = (key) => {
|
||||
isCapturingGlobalHotkey.value = false;
|
||||
const type = currentGlobalHotkeyType.value;
|
||||
if (!type) return;
|
||||
|
||||
// Validate captured key
|
||||
const isPlayerHotkey = store.state.players.some(p => p.hotkey === key);
|
||||
if (isPlayerHotkey) {
|
||||
alert(`Hotkey "${key.toUpperCase()}" is already assigned to a player.`);
|
||||
@@ -266,8 +202,74 @@ const captureGlobalHotkey = (event, type) => {
|
||||
}
|
||||
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) => {
|
||||
if (type === 'stopPause') {
|
||||
store.dispatch('setGlobalHotkeyStopPause', null);
|
||||
@@ -276,15 +278,10 @@ const clearGlobalHotkey = (type) => {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTheme = () => {
|
||||
store.dispatch('toggleTheme');
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
store.dispatch('setMuted', !isMuted.value);
|
||||
};
|
||||
|
||||
const saveAndClose = () => {
|
||||
// ... (toggleTheme, toggleMute, saveAndClose, resetPlayerTimersConfirm, fullResetAppConfirm remain the same)
|
||||
const toggleTheme = () => { store.dispatch('toggleTheme'); };
|
||||
const toggleMute = () => { store.dispatch('setMuted', !isMuted.value); };
|
||||
const saveAndClose = () => { /* ... as before ... */
|
||||
store.dispatch('saveState');
|
||||
if (players.value.length >= 2) {
|
||||
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.');
|
||||
}
|
||||
};
|
||||
|
||||
const resetPlayerTimersConfirm = () => {
|
||||
const resetPlayerTimersConfirm = () => { /* ... as before ... */
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
const fullResetAppConfirm = () => {
|
||||
const fullResetAppConfirm = () => { /* ... as before ... */
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user