without footer
This commit is contained in:
@@ -1,4 +0,0 @@
|
|||||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="12" cy="7" r="4" stroke="#000000" stroke-width="2"/>
|
|
||||||
<path d="M4 21C4 17.134 7.58172 14 12 14C16.4183 14 20 17.134 20 21" stroke="#000000" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 273 B |
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = 'nexus-timer-cache-v1';
|
const CACHE_NAME = 'nexus-timer-cache-v2';
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
|
|||||||
103
src/App.vue
103
src/App.vue
@@ -1,38 +1,39 @@
|
|||||||
// src/App.vue
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[theme, 'min-h-screen flex flex-col no-select']">
|
<div :class="[theme, 'min-h-screen flex flex-col no-select']">
|
||||||
<router-view />
|
<router-view class="flex-grow" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, onUnmounted, watch } from 'vue'; // Added onUnmounted
|
import { computed, onMounted, watch } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
import { useRouter } from 'vue-router'; // Import useRouter
|
||||||
import { AudioService } from './services/AudioService';
|
import { AudioService } from './services/AudioService';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const router = useRouter(); // Get router instance
|
||||||
|
|
||||||
const theme = computed(() => store.getters.theme);
|
const theme = computed(() => store.getters.theme);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// store.dispatch('loadState'); // <-- REMOVE THIS LINE
|
store.dispatch('loadState').then(() => {
|
||||||
applyTheme();
|
// console.log("App.vue: Store has finished loading state.");
|
||||||
|
applyTheme();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("App.vue: Error during store.dispatch('loadState'):", error);
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', handleGlobalKeyDown);
|
document.addEventListener('keydown', handleGlobalKeyDown);
|
||||||
|
|
||||||
const resumeAudio = () => {
|
const resumeAudio = () => {
|
||||||
AudioService.resumeContext();
|
AudioService.resumeContext();
|
||||||
// document.body.removeEventListener('click', resumeAudio); // These are fine with {once: true}
|
document.body.removeEventListener('click', resumeAudio);
|
||||||
// document.body.removeEventListener('touchstart', resumeAudio);
|
document.body.removeEventListener('touchstart', resumeAudio);
|
||||||
};
|
};
|
||||||
document.body.addEventListener('click', resumeAudio, { once: true });
|
document.body.addEventListener('click', resumeAudio, { once: true });
|
||||||
document.body.addEventListener('touchstart', resumeAudio, { once: true });
|
document.body.addEventListener('touchstart', resumeAudio, { once: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add onUnmounted to clean up the global event listener
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('keydown', handleGlobalKeyDown);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
watch(theme, () => {
|
watch(theme, () => {
|
||||||
applyTheme();
|
applyTheme();
|
||||||
});
|
});
|
||||||
@@ -41,7 +42,6 @@ watch(() => store.state.isMuted, (newMutedState) => {
|
|||||||
AudioService.setMuted(newMutedState);
|
AudioService.setMuted(newMutedState);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const applyTheme = () => {
|
const applyTheme = () => {
|
||||||
if (store.getters.theme === 'dark') {
|
if (store.getters.theme === 'dark') {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
@@ -51,48 +51,67 @@ const applyTheme = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGlobalKeyDown = (event) => {
|
const handleGlobalKeyDown = (event) => {
|
||||||
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA' || event.target.isContentEditable) {
|
// Prevent hotkeys from firing if an input, textarea, or contenteditable element is focused.
|
||||||
|
const targetElement = event.target;
|
||||||
|
if (targetElement.tagName === 'INPUT' ||
|
||||||
|
targetElement.tagName === 'TEXTAREA' ||
|
||||||
|
targetElement.isContentEditable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyPressed = event.key.toLowerCase();
|
const keyPressed = event.key.toLowerCase();
|
||||||
|
const currentRouteName = router.currentRoute.value.name; // Use router instance
|
||||||
|
|
||||||
|
// Global Stop/Pause All Hotkey
|
||||||
if (keyPressed === store.getters.globalHotkeyStopPause && store.getters.globalHotkeyStopPause) {
|
if (keyPressed === store.getters.globalHotkeyStopPause && store.getters.globalHotkeyStopPause) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
store.dispatch('globalStopPauseAll');
|
if (currentRouteName === 'Game') { // Only active on GameView
|
||||||
return;
|
store.dispatch('globalStopPauseAll');
|
||||||
}
|
|
||||||
|
|
||||||
const currentPlayerFromStore = store.getters.currentPlayer; // Renamed to avoid conflict
|
|
||||||
const gameMode = store.getters.gameMode;
|
|
||||||
|
|
||||||
if (gameMode === 'normal' && currentPlayerFromStore && keyPressed === currentPlayerFromStore.hotkey) {
|
|
||||||
event.preventDefault();
|
|
||||||
const wasRunning = currentPlayerFromStore.isTimerRunning;
|
|
||||||
store.dispatch('passTurn').then(() => {
|
|
||||||
const newCurrentPlayer = store.getters.currentPlayer; // Get updated player
|
|
||||||
if (wasRunning && newCurrentPlayer && newCurrentPlayer.isTimerRunning) {
|
|
||||||
AudioService.playPassTurnAlert();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gameMode === 'allTimers') {
|
|
||||||
const playerToToggle = store.state.players.find(p => p.hotkey === keyPressed && !p.isSkipped);
|
|
||||||
if (playerToToggle) {
|
|
||||||
event.preventDefault();
|
|
||||||
const playerIndex = store.state.players.indexOf(playerToToggle);
|
|
||||||
store.dispatch('togglePlayerTimerAllTimersMode', playerIndex);
|
|
||||||
}
|
}
|
||||||
|
return; // Important to return after handling
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Global "Run All Timers" Hotkey
|
||||||
|
if (keyPressed === store.getters.globalHotkeyRunAll && store.getters.globalHotkeyRunAll) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (currentRouteName === 'Game' && store.getters.gameMode === 'normal') { // Only on GameView & in normal mode
|
||||||
|
store.dispatch('switchToAllTimersMode');
|
||||||
|
}
|
||||||
|
return; // Important to return after handling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player specific "Pass Turn / My Pause" hotkeys
|
||||||
|
if (currentRouteName === 'Game') { // Only active on GameView
|
||||||
|
const currentPlayerInStore = store.getters.currentPlayer;
|
||||||
|
const gameModeInStore = store.getters.gameMode;
|
||||||
|
|
||||||
|
if (gameModeInStore === 'normal' && currentPlayerInStore && keyPressed === currentPlayerInStore.hotkey) {
|
||||||
|
event.preventDefault();
|
||||||
|
const wasRunning = currentPlayerInStore.isTimerRunning;
|
||||||
|
store.dispatch('passTurn').then(() => {
|
||||||
|
const newCurrentPlayer = store.getters.currentPlayer;
|
||||||
|
if (wasRunning && newCurrentPlayer && newCurrentPlayer.isTimerRunning) {
|
||||||
|
AudioService.playPassTurnAlert();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// No return here, as this is the last check in this block
|
||||||
|
} else if (gameModeInStore === 'allTimers') {
|
||||||
|
const playerToToggle = store.state.players.find(p => p.hotkey === keyPressed && !p.isSkipped);
|
||||||
|
if (playerToToggle) {
|
||||||
|
event.preventDefault();
|
||||||
|
const playerIndex = store.state.players.indexOf(playerToToggle);
|
||||||
|
store.dispatch('togglePlayerTimerAllTimersMode', playerIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html, body, #app {
|
html, body, #app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
9
src/components/DefaultAvatarIcon.vue
Normal file
9
src/components/DefaultAvatarIcon.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M18.685 19.097A9.723 9.723 0 0021.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 003.065 7.097A9.716 9.716 0 0012 21.75a9.716 9.716 0 006.685-2.653zm-12.54-1.285A7.486 7.486 0 0112 15a7.486 7.486 0 015.855 2.812A8.224 8.224 0 0112 20.25a8.224 8.224 0 01-5.855-2.438zM15.75 9a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// No script needed for a simple SVG icon component
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="['player-area p-4 flex flex-col items-center justify-center text-center relative h-1/2 no-select',
|
:class="['player-area p-2 md:p-4 flex flex-col items-center justify-center text-center relative no-select',
|
||||||
areaClass,
|
areaClass,
|
||||||
{ 'opacity-50': player.isSkipped,
|
{ 'opacity-50': player.isSkipped,
|
||||||
'bg-green-100 dark:bg-green-800 animate-pulsePositive': player.isTimerRunning && player.currentTimerSec >=0 && !isNextPlayerArea,
|
'bg-green-100 dark:bg-green-800 animate-pulsePositive': player.isTimerRunning && player.currentTimerSec >=0 && !isNextPlayerArea,
|
||||||
@@ -9,31 +9,48 @@
|
|||||||
@click="handleTap"
|
@click="handleTap"
|
||||||
v-touch:swipe.up="handleSwipeUp"
|
v-touch:swipe.up="handleSwipeUp"
|
||||||
>
|
>
|
||||||
<img
|
<!-- Avatar -->
|
||||||
:src="player.avatar || defaultAvatar"
|
<div
|
||||||
alt="Player Avatar"
|
class="mb-4 md:mb-4 relative"
|
||||||
class="rounded-full object-cover mb-4 border-4 shadow-lg"
|
:style="{
|
||||||
style="width: 60vmin; height: 60vmin; max-width: 280px; max-height: 280px;"
|
width: avatarSize.width,
|
||||||
:class="player.isSkipped ? 'border-gray-500 filter grayscale' : 'border-blue-500 dark:border-blue-400'"
|
height: avatarSize.height,
|
||||||
/>
|
}"
|
||||||
<h2 class="text-3xl md:text-4xl lg:text-5xl font-semibold mb-2 mt-2">{{ player.name }}</h2>
|
>
|
||||||
|
<img
|
||||||
|
v-if="player.avatar"
|
||||||
|
:src="player.avatar"
|
||||||
|
alt="Player Avatar"
|
||||||
|
class="rounded-full object-cover border-2 md:border-4 shadow-lg w-full h-full"
|
||||||
|
:class="player.isSkipped ? 'border-gray-500 filter grayscale' : 'border-blue-500 dark:border-blue-400'"
|
||||||
|
/>
|
||||||
|
<DefaultAvatarIcon
|
||||||
|
v-else
|
||||||
|
class="rounded-full object-cover border-2 md:border-4 shadow-lg text-gray-400 dark:text-gray-500 bg-gray-200 dark:bg-gray-700 p-1 md:p-2 w-full h-full"
|
||||||
|
:class="player.isSkipped ? 'border-gray-500 filter grayscale !text-gray-300 dark:!text-gray-600' : 'border-blue-500 dark:border-blue-400'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Player Name -->
|
||||||
|
<h2 class="font-semibold mb-6 md:mb-6 text-5xl sm:text-2xl md:text-3xl lg:text-5xl">
|
||||||
|
{{ player.name }}
|
||||||
|
</h2>
|
||||||
<TimerDisplay
|
<TimerDisplay
|
||||||
:seconds="player.currentTimerSec"
|
:seconds="player.currentTimerSec"
|
||||||
:is-negative="player.currentTimerSec < 0"
|
:is-negative="player.currentTimerSec < 0"
|
||||||
:is-pulsating="player.isTimerRunning"
|
:is-pulsating="player.isTimerRunning"
|
||||||
class="text-6xl md:text-8xl lg:text-9xl xl:text-[10rem]"
|
class="text-4xl sm:text-5xl md:text-6xl lg:text-6xl xl:text-5xl"
|
||||||
/>
|
/>
|
||||||
<p v-if="player.isSkipped" class="text-red-500 dark:text-red-400 mt-2 font-semibold text-lg">SKIPPED</p>
|
<p v-if="player.isSkipped" class="text-red-500 dark:text-red-400 mt-1 md:mt-2 font-semibold text-sm md:text-base lg:text-lg">SKIPPED</p>
|
||||||
<p v-if="isNextPlayerArea && !player.isSkipped" class="mt-3 text-base text-gray-600 dark:text-gray-400">(Swipe up to pass turn)</p>
|
<p v-if="isNextPlayerArea && !player.isSkipped" class="mt-1 md:mt-2 text-xs md:text-sm text-gray-600 dark:text-gray-400">(Swipe up to pass turn)</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||||
import TimerDisplay from './TimerDisplay.vue';
|
import TimerDisplay from './TimerDisplay.vue';
|
||||||
import defaultAvatar from '../assets/default-avatar.png';
|
import DefaultAvatarIcon from './DefaultAvatarIcon.vue';
|
||||||
|
|
||||||
// Basic touch directive
|
|
||||||
const vTouch = {
|
const vTouch = {
|
||||||
mounted: (el, binding) => {
|
mounted: (el, binding) => {
|
||||||
if (binding.arg === 'swipe' && binding.modifiers.up) {
|
if (binding.arg === 'swipe' && binding.modifiers.up) {
|
||||||
@@ -41,7 +58,7 @@ const vTouch = {
|
|||||||
let touchstartY = 0;
|
let touchstartY = 0;
|
||||||
let touchendX = 0;
|
let touchendX = 0;
|
||||||
let touchendY = 0;
|
let touchendY = 0;
|
||||||
const swipeThreshold = 50; // Min pixels for swipe
|
const swipeThreshold = 50;
|
||||||
|
|
||||||
el.addEventListener('touchstart', function(event) {
|
el.addEventListener('touchstart', function(event) {
|
||||||
touchstartX = event.changedTouches[0].screenX;
|
touchstartX = event.changedTouches[0].screenX;
|
||||||
@@ -68,6 +85,7 @@ const vTouch = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
player: {
|
player: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -80,6 +98,40 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['tapped', 'swiped-up']);
|
const emit = defineEmits(['tapped', 'swiped-up']);
|
||||||
|
|
||||||
|
const avatarSize = ref({ width: '120px', height: '120px' });
|
||||||
|
|
||||||
|
const calculateAvatarSize = () => {
|
||||||
|
const screenWidth = window.innerWidth;
|
||||||
|
const screenHeight = window.innerHeight;
|
||||||
|
const isNormalModeContext = document.querySelector('.player-area')?.parentElement?.classList.contains('flex-col');
|
||||||
|
|
||||||
|
if (isNormalModeContext) {
|
||||||
|
const availableHeight = screenHeight / 2;
|
||||||
|
let size = Math.min(availableHeight * 0.5, screenWidth * 0.4, 175);
|
||||||
|
|
||||||
|
if (screenWidth < 960) {
|
||||||
|
size = Math.min(availableHeight * 0.7, screenWidth * 0.6, 250);
|
||||||
|
} else if (screenWidth < 1024) {
|
||||||
|
size = Math.min(availableHeight * 0.55, screenWidth * 0.45, 180);
|
||||||
|
}
|
||||||
|
size = Math.max(size, 100);
|
||||||
|
|
||||||
|
avatarSize.value = { width: `${size}px`, height: `${size}px` };
|
||||||
|
} else {
|
||||||
|
avatarSize.value = { width: '120px', height: '120px' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
calculateAvatarSize();
|
||||||
|
window.addEventListener('resize', calculateAvatarSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', calculateAvatarSize);
|
||||||
|
});
|
||||||
|
|
||||||
const handleTap = () => {
|
const handleTap = () => {
|
||||||
if (props.isCurrentPlayerArea && !props.player.isSkipped) {
|
if (props.isCurrentPlayerArea && !props.player.isSkipped) {
|
||||||
emit('tapped');
|
emit('tapped');
|
||||||
@@ -91,10 +143,4 @@ const handleSwipeUp = () => {
|
|||||||
emit('swiped-up');
|
emit('swiped-up');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.player-area {
|
|
||||||
transition: background-color 0.3s ease, opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,177 +1,231 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 flex justify-center items-center p-4" @click.self="closeModal">
|
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 flex justify-center items-center p-4" @click.self="closeModal">
|
||||||
<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">
|
||||||
<div class="mb-4">
|
<!-- Name -->
|
||||||
<label for="playerName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
|
<div class="mb-4">
|
||||||
<input type="text" id="playerName" v-model="editablePlayer.name" required class="input-base">
|
<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">
|
||||||
|
<img
|
||||||
|
v-if="editablePlayer.avatar"
|
||||||
|
:src="editablePlayer.avatar"
|
||||||
|
alt="Avatar"
|
||||||
|
class="w-16 h-16 rounded-full object-cover mr-4 border"
|
||||||
|
/>
|
||||||
|
<DefaultAvatarIcon
|
||||||
|
v-else
|
||||||
|
class="w-16 h-16 rounded-full object-cover mr-4 border text-gray-400 bg-gray-200 p-1"
|
||||||
|
/>
|
||||||
|
<button type="button" @click="capturePhoto" class="btn btn-secondary text-sm mr-2">Take Photo</button>
|
||||||
|
<button type="button" @click="useDefaultAvatar" class="btn btn-secondary text-sm">Default</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="cameraError" class="text-red-500 text-xs mt-1">{{ cameraError }}</p>
|
||||||
<div class="mb-4">
|
</div>
|
||||||
<label for="initialTime" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Initial Timer (MM:SS)</label>
|
|
||||||
<input type="text" id="initialTime" v-model="initialTimeFormatted" @blur="validateTimeFormat" placeholder="e.g., 60:00" required class="input-base">
|
<!-- "Pass Turn / My Pause" Hotkey -->
|
||||||
<p v-if="timeFormatError" class="text-red-500 text-xs mt-1">{{ timeFormatError }}</p>
|
<div class="flex items-center justify-between mb-6"> <!-- Added flex container and margin -->
|
||||||
</div>
|
<label for="playerHotkey" class="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap mr-3">
|
||||||
|
"Pass Turn / My Pause" Hotkey:
|
||||||
<div class="mb-4">
|
</label>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Avatar</label>
|
<div class="flex items-center"> <!-- Group input and clear button -->
|
||||||
<div class="mt-1 flex items-center">
|
<input
|
||||||
<img :src="editablePlayer.avatar || defaultAvatar" alt="Avatar" class="w-16 h-16 rounded-full object-cover mr-4 border">
|
type="text"
|
||||||
<button type="button" @click="capturePhoto" class="btn btn-secondary text-sm mr-2">Take Photo</button>
|
id="playerHotkey"
|
||||||
<button type="button" @click="useDefaultAvatar" class="btn btn-secondary text-sm">Default</button>
|
:value="editablePlayer.hotkey ? editablePlayer.hotkey.toUpperCase() : ''"
|
||||||
</div>
|
@keydown.prevent="captureHotkey($event, 'player')"
|
||||||
<p v-if="cameraError" class="text-red-500 text-xs mt-1">{{ cameraError }}</p>
|
placeholder="-"
|
||||||
</div>
|
class="input-base w-12 h-8 text-center font-mono text-lg p-0"
|
||||||
|
readonly
|
||||||
<div class="mb-4">
|
maxlength="1"
|
||||||
<label for="playerHotkey" class="block text-sm font-medium text-gray-700 dark:text-gray-300">"Pass Turn / My Pause" Hotkey</label>
|
>
|
||||||
<input
|
<button
|
||||||
type="text"
|
type="button"
|
||||||
id="playerHotkey"
|
@click="clearHotkey('player')"
|
||||||
v-model="editablePlayer.hotkey"
|
class="text-xs text-blue-500 hover:underline ml-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-600"
|
||||||
@keydown.prevent="captureHotkey($event, 'player')"
|
>
|
||||||
placeholder="Press a key"
|
Clear
|
||||||
class="input-base"
|
</button>
|
||||||
readonly
|
</div>
|
||||||
>
|
</div>
|
||||||
<button type="button" @click="clearHotkey('player')" class="text-xs text-blue-500 hover:underline mt-1">Clear Hotkey</button>
|
|
||||||
</div>
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex justify-end space-x-3 mt-6">
|
||||||
<div class="flex justify-end space-x-3">
|
<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="!!timeFormatError">{{ isEditing ? 'Save Changes' : 'Add Player' }}</button>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
</template>
|
||||||
<script setup>
|
|
||||||
// **** FIX: Add 'computed' to the import statement ****
|
<script setup>
|
||||||
import { ref, reactive, watch, onMounted, computed } from 'vue';
|
import { ref, reactive, watch, onMounted, computed } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
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 defaultAvatarPath from '../assets/default-avatar.png';
|
import DefaultAvatarIcon from './DefaultAvatarIcon.vue';
|
||||||
import { AudioService } from '../services/AudioService'; // Added import for AudioService
|
import { AudioService } from '../services/AudioService';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
player: Object, // Player object for editing, null for adding
|
player: Object,
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['close', 'save']);
|
const emit = defineEmits(['close', 'save']);
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const defaultAvatar = defaultAvatarPath;
|
const DEFAULT_AVATAR_MARKER = null;
|
||||||
|
|
||||||
const isEditing = computed(() => !!props.player); // This line was causing the error
|
const isEditing = computed(() => !!props.player);
|
||||||
const editablePlayer = reactive({
|
const editablePlayer = reactive({
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
avatar: defaultAvatar,
|
avatar: DEFAULT_AVATAR_MARKER,
|
||||||
initialTimerSec: 3600, // 60:00
|
initialTimerSec: 3600,
|
||||||
hotkey: '',
|
currentTimerSec: 3600,
|
||||||
});
|
hotkey: '',
|
||||||
|
});
|
||||||
const initialTimeFormatted = ref('60:00');
|
|
||||||
const timeFormatError = ref('');
|
const currentTimeFormatted = ref('60:00');
|
||||||
const cameraError = ref('');
|
const currentTimeFormatError = ref('');
|
||||||
|
const cameraError = ref('');
|
||||||
onMounted(() => {
|
|
||||||
if (isEditing.value && props.player) {
|
const maxNegativeSeconds = computed(() => store.getters.maxNegativeTimeReached);
|
||||||
editablePlayer.id = props.player.id;
|
|
||||||
editablePlayer.name = props.player.name;
|
|
||||||
editablePlayer.avatar = props.player.avatar || defaultAvatar;
|
onMounted(() => {
|
||||||
editablePlayer.initialTimerSec = props.player.initialTimerSec;
|
if (isEditing.value && props.player) {
|
||||||
editablePlayer.hotkey = props.player.hotkey || '';
|
editablePlayer.id = props.player.id;
|
||||||
initialTimeFormatted.value = formatTime(props.player.initialTimerSec);
|
editablePlayer.name = props.player.name;
|
||||||
|
editablePlayer.avatar = props.player.avatar;
|
||||||
|
editablePlayer.initialTimerSec = props.player.initialTimerSec;
|
||||||
|
editablePlayer.currentTimerSec = props.player.currentTimerSec;
|
||||||
|
editablePlayer.hotkey = props.player.hotkey || '';
|
||||||
|
currentTimeFormatted.value = formatTime(props.player.currentTimerSec);
|
||||||
|
} else {
|
||||||
|
editablePlayer.avatar = DEFAULT_AVATAR_MARKER;
|
||||||
|
editablePlayer.initialTimerSec = 3600;
|
||||||
|
editablePlayer.currentTimerSec = 3600;
|
||||||
|
currentTimeFormatted.value = formatTime(editablePlayer.currentTimerSec);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(currentTimeFormatted, (newTime) => {
|
||||||
|
validateCurrentTimeFormat();
|
||||||
|
if (!currentTimeFormatError.value) {
|
||||||
|
editablePlayer.currentTimerSec = parseTime(newTime);
|
||||||
|
if (!isEditing.value) {
|
||||||
|
editablePlayer.initialTimerSec = editablePlayer.currentTimerSec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateCurrentTimeFormat() {
|
||||||
|
const time = currentTimeFormatted.value;
|
||||||
|
const isNegativeInput = time.startsWith('-');
|
||||||
|
if (!/^-?(?:[0-9]+:[0-5]\d)$/.test(time)) {
|
||||||
|
currentTimeFormatError.value = 'Invalid format. Use MM:SS or -MM:SS (e.g., 05:30, -02:15).';
|
||||||
} else {
|
} else {
|
||||||
initialTimeFormatted.value = formatTime(editablePlayer.initialTimerSec);
|
const parsedSeconds = parseTime(time);
|
||||||
}
|
if (isNegativeInput && parsedSeconds > 0) {
|
||||||
});
|
currentTimeFormatError.value = 'Negative time should parse to negative seconds.';
|
||||||
|
} else if (parsedSeconds < maxNegativeSeconds.value) {
|
||||||
watch(initialTimeFormatted, (newTime) => {
|
currentTimeFormatError.value = `Time cannot be less than ${formatTime(maxNegativeSeconds.value)}.`;
|
||||||
validateTimeFormat();
|
|
||||||
if (!timeFormatError.value) {
|
|
||||||
editablePlayer.initialTimerSec = parseTime(newTime);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function validateTimeFormat() {
|
|
||||||
const time = initialTimeFormatted.value;
|
|
||||||
if (!/^(?:[0-5]?\d:[0-5]\d)$/.test(time) && !/^(?:[0-9]+:[0-5]\d)$/.test(time)) { // Allow more than 59 minutes for setup
|
|
||||||
timeFormatError.value = 'Invalid time format. Use MM:SS (e.g., 05:30 or 70:00).';
|
|
||||||
} else {
|
|
||||||
const parsed = parseTime(time);
|
|
||||||
if (parsed < 1) { // Minimum 1 second
|
|
||||||
timeFormatError.value = 'Timer must be at least 00:01.';
|
|
||||||
} else {
|
|
||||||
timeFormatError.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function capturePhoto() {
|
|
||||||
cameraError.value = '';
|
|
||||||
try {
|
|
||||||
AudioService.resumeContext(); // Ensure audio context is active for camera sounds if any
|
|
||||||
const photoDataUrl = await CameraService.getPhoto();
|
|
||||||
editablePlayer.avatar = photoDataUrl;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to capture photo:', error);
|
|
||||||
cameraError.value = error.message || 'Could not capture photo.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useDefaultAvatar() {
|
|
||||||
editablePlayer.avatar = defaultAvatar;
|
|
||||||
}
|
|
||||||
|
|
||||||
function captureHotkey(event, type) {
|
|
||||||
event.preventDefault();
|
|
||||||
const key = event.key.toLowerCase();
|
|
||||||
// Avoid modifier keys alone, allow combinations if needed, but spec says single keypresses
|
|
||||||
if (key && !['control', 'alt', 'shift', 'meta'].includes(key)) {
|
|
||||||
if (type === 'player') {
|
|
||||||
// Check if hotkey is already used by another player or global hotkey
|
|
||||||
const existingPlayerHotkey = store.state.players.some(p => p.hotkey === key && p.id !== editablePlayer.id);
|
|
||||||
const globalHotkeyInUse = store.state.globalHotkeyStopPause === key;
|
|
||||||
if (existingPlayerHotkey) {
|
|
||||||
alert(`Hotkey "${key.toUpperCase()}" is already assigned to another player.`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (globalHotkeyInUse) {
|
else {
|
||||||
alert(`Hotkey "${key.toUpperCase()}" is already assigned as the Global Stop/Pause hotkey.`);
|
currentTimeFormatError.value = '';
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
editablePlayer.hotkey = key;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function capturePhoto() {
|
||||||
|
cameraError.value = '';
|
||||||
|
try {
|
||||||
|
AudioService.resumeContext();
|
||||||
|
const photoDataUrl = await CameraService.getPhoto();
|
||||||
|
editablePlayer.avatar = photoDataUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to capture photo:', error);
|
||||||
|
cameraError.value = error.message || 'Could not capture photo.';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function clearHotkey(type) {
|
|
||||||
if (type === 'player') {
|
function useDefaultAvatar() {
|
||||||
editablePlayer.hotkey = '';
|
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();
|
||||||
function submitForm() {
|
|
||||||
validateTimeFormat();
|
if (type === 'player') {
|
||||||
if (timeFormatError.value) return;
|
const existingPlayerHotkey = store.state.players.some(p => p.hotkey === key && p.id !== editablePlayer.id);
|
||||||
|
const globalHotkeyInUseStopPause = store.state.globalHotkeyStopPause === key;
|
||||||
const playerPayload = { ...editablePlayer };
|
const globalHotkeyInUseRunAll = store.state.globalHotkeyRunAll === key;
|
||||||
// Ensure currentTimerSec is also updated if initialTimerSec changes and it's a new player or reset is implied
|
|
||||||
if (!isEditing.value || (props.player && playerPayload.initialTimerSec !== props.player.initialTimerSec)) {
|
if (existingPlayerHotkey) {
|
||||||
playerPayload.currentTimerSec = playerPayload.initialTimerSec;
|
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.`);
|
||||||
emit('save', playerPayload);
|
return;
|
||||||
closeModal();
|
}
|
||||||
|
if (globalHotkeyInUseRunAll) {
|
||||||
|
alert(`Hotkey "${key.toUpperCase()}" is already assigned as the Global Run All Timers hotkey.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editablePlayer.hotkey = key;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function closeModal() {
|
|
||||||
emit('close');
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
if (!isEditing.value) {
|
||||||
|
playerPayload.initialTimerSec = playerPayload.currentTimerSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('save', playerPayload);
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -9,11 +9,17 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<img
|
<img
|
||||||
:src="player.avatar || defaultAvatar"
|
v-if="player.avatar"
|
||||||
|
:src="player.avatar"
|
||||||
alt="Player Avatar"
|
alt="Player Avatar"
|
||||||
class="w-12 h-12 rounded-full object-cover mr-3 border-2"
|
class="w-12 h-12 rounded-full object-cover mr-3 border-2 flex-shrink-0"
|
||||||
:class="player.isSkipped ? 'border-gray-500 filter grayscale' : 'border-blue-400 dark:border-blue-300'"
|
:class="player.isSkipped ? 'border-gray-500 filter grayscale' : 'border-blue-400 dark:border-blue-300'"
|
||||||
/>
|
/>
|
||||||
|
<DefaultAvatarIcon
|
||||||
|
v-else
|
||||||
|
class="w-12 h-12 rounded-full object-cover mr-3 border-2 text-gray-400 dark:text-gray-500 bg-gray-200 dark:bg-gray-700 p-1 flex-shrink-0"
|
||||||
|
:class="player.isSkipped ? 'border-gray-500 filter grayscale !text-gray-300 dark:!text-gray-600' : 'border-blue-400 dark:border-blue-300'"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-medium">{{ player.name }}</h3>
|
<h3 class="text-lg font-medium">{{ player.name }}</h3>
|
||||||
<p v-if="player.isSkipped" class="text-xs text-red-500 dark:text-red-400">SKIPPED</p>
|
<p v-if="player.isSkipped" class="text-xs text-red-500 dark:text-red-400">SKIPPED</p>
|
||||||
@@ -26,16 +32,14 @@
|
|||||||
:is-pulsating="player.isTimerRunning"
|
:is-pulsating="player.isTimerRunning"
|
||||||
class="text-2xl"
|
class="text-2xl"
|
||||||
/>
|
/>
|
||||||
<!-- Removed the "Running"/"Paused" text span -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// ... (script remains the same)
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import TimerDisplay from './TimerDisplay.vue';
|
import TimerDisplay from './TimerDisplay.vue';
|
||||||
import defaultAvatar from '../assets/default-avatar.png';
|
import DefaultAvatarIcon from './DefaultAvatarIcon.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
player: {
|
player: {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { createStore } from 'vuex';
|
import { createStore } from 'vuex';
|
||||||
import { StorageService } from '../services/StorageService';
|
import { StorageService } from '../services/StorageService';
|
||||||
import { parseTime, formatTime } from '../utils/timeFormatter';
|
import { parseTime, formatTime } from '../utils/timeFormatter';
|
||||||
import defaultAvatar from '../assets/default-avatar.png';
|
|
||||||
|
|
||||||
const MAX_NEGATIVE_SECONDS = -(59 * 60 + 59); // -59:59
|
const MAX_NEGATIVE_SECONDS = -(59 * 60 + 59); // -59:59
|
||||||
|
const DEFAULT_AVATAR_MARKER = null; // Ensure this is defined
|
||||||
|
|
||||||
// Define predefined players
|
// Define predefined players
|
||||||
const predefinedPlayers = [
|
const predefinedPlayers = [
|
||||||
{
|
{
|
||||||
id: 'predefined-1', // Unique ID for predefined player 1
|
id: 'predefined-1', // Unique ID for predefined player 1
|
||||||
name: 'Player 1',
|
name: 'Player 1',
|
||||||
avatar: defaultAvatar, // Or a specific avatar path if you have one
|
avatar: DEFAULT_AVATAR_MARKER, // Or a specific avatar path if you have one
|
||||||
initialTimerSec: 60 * 60, // 60:00
|
initialTimerSec: 60 * 60, // 60:00
|
||||||
currentTimerSec: 60 * 60,
|
currentTimerSec: 60 * 60,
|
||||||
hotkey: '1', // Hotkey '1'
|
hotkey: '1', // Hotkey '1'
|
||||||
@@ -20,7 +20,7 @@ const predefinedPlayers = [
|
|||||||
{
|
{
|
||||||
id: 'predefined-2', // Unique ID for predefined player 2
|
id: 'predefined-2', // Unique ID for predefined player 2
|
||||||
name: 'Player 2',
|
name: 'Player 2',
|
||||||
avatar: defaultAvatar, // Or a specific avatar path
|
avatar: DEFAULT_AVATAR_MARKER, // Or a specific avatar path
|
||||||
initialTimerSec: 60 * 60, // 60:00
|
initialTimerSec: 60 * 60, // 60:00
|
||||||
currentTimerSec: 60 * 60,
|
currentTimerSec: 60 * 60,
|
||||||
hotkey: '2', // Hotkey '2'
|
hotkey: '2', // Hotkey '2'
|
||||||
@@ -32,6 +32,7 @@ const predefinedPlayers = [
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
players: JSON.parse(JSON.stringify(predefinedPlayers)), // Start with predefined players (deep copy)
|
players: JSON.parse(JSON.stringify(predefinedPlayers)), // Start with predefined players (deep copy)
|
||||||
globalHotkeyStopPause: null,
|
globalHotkeyStopPause: null,
|
||||||
|
globalHotkeyRunAll: null,
|
||||||
currentPlayerIndex: 0,
|
currentPlayerIndex: 0,
|
||||||
gameMode: 'normal',
|
gameMode: 'normal',
|
||||||
isMuted: false,
|
isMuted: false,
|
||||||
@@ -40,45 +41,36 @@ const initialState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
state: () => {
|
state: () => { // This function already loads from storage ONCE during store creation
|
||||||
const persistedState = StorageService.getState();
|
const persistedState = StorageService.getState();
|
||||||
if (persistedState) {
|
if (persistedState) {
|
||||||
let playersToUse = persistedState.players || JSON.parse(JSON.stringify(predefinedPlayers)); // Use persisted or default
|
let playersToUse = persistedState.players;
|
||||||
|
|
||||||
|
if (!playersToUse || (playersToUse.length === 0 && !persistedState.hasOwnProperty('players'))) {
|
||||||
|
playersToUse = JSON.parse(JSON.stringify(predefinedPlayers));
|
||||||
|
} else if (persistedState.hasOwnProperty('players') && playersToUse.length === 0) {
|
||||||
|
playersToUse = [];
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure avatar defaults and correct timer parsing if missing or in old format
|
|
||||||
playersToUse = playersToUse.map(p => ({
|
playersToUse = playersToUse.map(p => ({
|
||||||
...p,
|
...p,
|
||||||
id: p.id || Date.now().toString() + Math.random(), // Ensure ID if missing
|
id: p.id || Date.now().toString() + Math.random(),
|
||||||
avatar: p.avatar || defaultAvatar,
|
avatar: p.avatar === undefined ? DEFAULT_AVATAR_MARKER : p.avatar,
|
||||||
initialTimerSec: typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.initialTimer || "60:00"),
|
initialTimerSec: typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.initialTimer || "60:00"),
|
||||||
currentTimerSec: typeof p.currentTimerSec === 'number' ? p.currentTimerSec : (typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.currentTimer || p.initialTimer || "60:00")),
|
currentTimerSec: typeof p.currentTimerSec === 'number' ? p.currentTimerSec : (typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.currentTimer || p.initialTimer || "60:00")),
|
||||||
isSkipped: p.isSkipped || false,
|
isSkipped: p.isSkipped || false,
|
||||||
isTimerRunning: false, // Reset running state on load
|
isTimerRunning: false,
|
||||||
hotkey: p.hotkey || null, // Ensure hotkey exists
|
hotkey: p.hotkey || null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// If persisted state had no players, but we want predefined, use them.
|
|
||||||
// This logic could be refined: if the user explicitly deleted all players,
|
|
||||||
// we might not want to re-add predefined ones.
|
|
||||||
// For now, if persisted players array is empty, we'll use predefined ones.
|
|
||||||
if (persistedState.players && persistedState.players.length === 0) {
|
|
||||||
// User might have deleted all players, respect that if a 'players' key exists and is empty.
|
|
||||||
// If they want a clean slate without predefined, they delete them from UI.
|
|
||||||
// If 'players' key itself is missing from persistedState, then use predefined.
|
|
||||||
} else if (!persistedState.players) {
|
|
||||||
playersToUse = JSON.parse(JSON.stringify(predefinedPlayers));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...initialState, // Base defaults
|
...initialState,
|
||||||
...persistedState, // Persisted values override defaults
|
...persistedState,
|
||||||
players: playersToUse, // Specifically use the processed players
|
players: playersToUse,
|
||||||
gameRunning: false, // Always start with game not running
|
gameRunning: false,
|
||||||
currentPlayerIndex: persistedState.currentPlayerIndex !== undefined ? persistedState.currentPlayerIndex : 0, // Ensure valid index
|
currentPlayerIndex: persistedState.currentPlayerIndex !== undefined && persistedState.currentPlayerIndex < playersToUse.length ? persistedState.currentPlayerIndex : 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// If no persisted state, return a deep copy of the initial state (which includes predefined players)
|
|
||||||
return JSON.parse(JSON.stringify(initialState));
|
return JSON.parse(JSON.stringify(initialState));
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
@@ -86,7 +78,7 @@ export default createStore({
|
|||||||
state.players = players.map(p => ({
|
state.players = players.map(p => ({
|
||||||
...p,
|
...p,
|
||||||
id: p.id || Date.now().toString() + Math.random(),
|
id: p.id || Date.now().toString() + Math.random(),
|
||||||
avatar: p.avatar || defaultAvatar,
|
avatar: p.avatar || DEFAULT_AVATAR_MARKER,
|
||||||
initialTimerSec: typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.initialTimer || "60:00"),
|
initialTimerSec: typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.initialTimer || "60:00"),
|
||||||
currentTimerSec: typeof p.currentTimerSec === 'number' ? p.currentTimerSec : (typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.currentTimer || p.initialTimer || "60:00")),
|
currentTimerSec: typeof p.currentTimerSec === 'number' ? p.currentTimerSec : (typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.currentTimer || p.initialTimer || "60:00")),
|
||||||
isSkipped: p.isSkipped || false,
|
isSkipped: p.isSkipped || false,
|
||||||
@@ -98,15 +90,18 @@ export default createStore({
|
|||||||
const newPlayer = {
|
const newPlayer = {
|
||||||
id: Date.now().toString() + Math.random(), // More robust unique ID
|
id: Date.now().toString() + Math.random(), // More robust unique ID
|
||||||
name: player.name || `Player ${state.players.length + 1}`,
|
name: player.name || `Player ${state.players.length + 1}`,
|
||||||
avatar: player.avatar || defaultAvatar,
|
avatar: player.avatar || DEFAULT_AVATAR_MARKER,
|
||||||
initialTimerSec: player.initialTimerSec || 3600,
|
initialTimerSec: player.initialTimerSec || 3600,
|
||||||
currentTimerSec: player.initialTimerSec || 3600,
|
currentTimerSec: player.initialTimerSec || 3600,
|
||||||
hotkey: player.hotkey || null,
|
hotkey: player.hotkey || null,
|
||||||
isSkipped: false,
|
isSkipped: false,
|
||||||
isTimerRunning: false,
|
isTimerRunning: false,
|
||||||
};
|
};
|
||||||
if (state.players.length < 7) {
|
if (state.players.length < 99) {
|
||||||
state.players.push(newPlayer);
|
state.players.push(newPlayer);
|
||||||
|
} else {
|
||||||
|
console.warn("Maximum player limit (99) reached.");
|
||||||
|
alert("Maximum player limit (99) reached.");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
UPDATE_PLAYER(state, updatedPlayer) {
|
UPDATE_PLAYER(state, updatedPlayer) {
|
||||||
@@ -150,6 +145,12 @@ export default createStore({
|
|||||||
SET_GLOBAL_HOTKEY_STOP_PAUSE(state, key) {
|
SET_GLOBAL_HOTKEY_STOP_PAUSE(state, key) {
|
||||||
state.globalHotkeyStopPause = key;
|
state.globalHotkeyStopPause = key;
|
||||||
},
|
},
|
||||||
|
SET_GLOBAL_HOTKEY_RUN_ALL(state, key) {
|
||||||
|
state.globalHotkeyRunAll = key;
|
||||||
|
},
|
||||||
|
SET_THEME(state, theme) {
|
||||||
|
state.theme = theme;
|
||||||
|
},
|
||||||
DECREMENT_TIMER(state, { playerIndex }) {
|
DECREMENT_TIMER(state, { playerIndex }) {
|
||||||
const player = state.players[playerIndex];
|
const player = state.players[playerIndex];
|
||||||
if (player && player.isTimerRunning && !player.isSkipped) {
|
if (player && player.isTimerRunning && !player.isSkipped) {
|
||||||
@@ -184,8 +185,6 @@ export default createStore({
|
|||||||
state.gameMode = 'normal';
|
state.gameMode = 'normal';
|
||||||
state.gameRunning = false;
|
state.gameRunning = false;
|
||||||
},
|
},
|
||||||
// ... other mutations, actions, getters remain the same
|
|
||||||
// Make sure PAUSE_ALL_TIMERS, START_PLAYER_TIMER, PAUSE_PLAYER_TIMER update gameRunning
|
|
||||||
START_PLAYER_TIMER(state, playerIndex) {
|
START_PLAYER_TIMER(state, playerIndex) {
|
||||||
if(state.players[playerIndex] && !state.players[playerIndex].isSkipped) {
|
if(state.players[playerIndex] && !state.players[playerIndex].isSkipped) {
|
||||||
state.players[playerIndex].isTimerRunning = true;
|
state.players[playerIndex].isTimerRunning = true;
|
||||||
@@ -209,31 +208,41 @@ export default createStore({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
loadState({ commit }) {
|
loadState({ commit, state }) {
|
||||||
// This action is effectively handled by the state initializer function now.
|
// The state initializer already did the main loading from localStorage.
|
||||||
// We can keep it for explicitness or if we add more complex loading logic later.
|
// This action can be used for any *additional* setup after initial hydration
|
||||||
// For now, the state initializer is doing the heavy lifting.
|
// or to re-apply certain defaults if needed.
|
||||||
// If there are other things to initialize that aren't covered by state(), do them here.
|
// For now, it's mainly a confirmation that persisted state is used.
|
||||||
|
|
||||||
|
// Example: ensure theme is applied if it was loaded
|
||||||
|
// This is already handled by App.vue's watcher, but could be centralized.
|
||||||
|
// if (state.theme === 'dark') {
|
||||||
|
// document.documentElement.classList.add('dark');
|
||||||
|
// } else {
|
||||||
|
// document.documentElement.classList.remove('dark');
|
||||||
|
// }
|
||||||
|
console.log("Store state loaded/initialized.");
|
||||||
|
// It's good practice for actions to return a Promise if they are async
|
||||||
|
// or if other parts of the app expect to chain .then()
|
||||||
|
return Promise.resolve(); // Resolve immediately
|
||||||
},
|
},
|
||||||
saveState({ state }) {
|
saveState({ state }) {
|
||||||
StorageService.saveState({
|
StorageService.saveState({
|
||||||
players: state.players.map(p => ({ // Save a clean version of players
|
players: state.players.map(p => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
avatar: p.avatar === defaultAvatar ? null : p.avatar, // Don't save default avatar string if it's the default
|
avatar: p.avatar,
|
||||||
initialTimerSec: p.initialTimerSec,
|
initialTimerSec: p.initialTimerSec,
|
||||||
// currentTimerSec: p.currentTimerSec, // Don't save currentTimerSec to always start fresh? Or save it. Spec: "timer states ... are saved"
|
|
||||||
currentTimerSec: p.currentTimerSec,
|
currentTimerSec: p.currentTimerSec,
|
||||||
hotkey: p.hotkey,
|
hotkey: p.hotkey,
|
||||||
isSkipped: p.isSkipped,
|
isSkipped: p.isSkipped,
|
||||||
// isTimerRunning is transient, should not be saved as running
|
|
||||||
})),
|
})),
|
||||||
globalHotkeyStopPause: state.globalHotkeyStopPause,
|
globalHotkeyStopPause: state.globalHotkeyStopPause,
|
||||||
|
globalHotkeyRunAll: state.globalHotkeyRunAll,
|
||||||
currentPlayerIndex: state.currentPlayerIndex,
|
currentPlayerIndex: state.currentPlayerIndex,
|
||||||
gameMode: state.gameMode,
|
gameMode: state.gameMode,
|
||||||
isMuted: state.isMuted,
|
isMuted: state.isMuted,
|
||||||
theme: state.theme,
|
theme: state.theme,
|
||||||
// gameRunning is transient, should not be saved as true
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addPlayer({ commit, dispatch }, player) {
|
addPlayer({ commit, dispatch }, player) {
|
||||||
@@ -249,8 +258,8 @@ export default createStore({
|
|||||||
dispatch('saveState');
|
dispatch('saveState');
|
||||||
},
|
},
|
||||||
reorderPlayers({commit, dispatch}, players) {
|
reorderPlayers({commit, dispatch}, players) {
|
||||||
commit('REORDER_PLAYERS', players);
|
commit('REORDER_PLAYERS', players);
|
||||||
dispatch('saveState');
|
dispatch('saveState');
|
||||||
},
|
},
|
||||||
shufflePlayers({commit, dispatch}) {
|
shufflePlayers({commit, dispatch}) {
|
||||||
commit('SHUFFLE_PLAYERS');
|
commit('SHUFFLE_PLAYERS');
|
||||||
@@ -276,25 +285,31 @@ export default createStore({
|
|||||||
commit('RESET_ALL_TIMERS');
|
commit('RESET_ALL_TIMERS');
|
||||||
dispatch('saveState');
|
dispatch('saveState');
|
||||||
},
|
},
|
||||||
// This action is for the full reset from SetupView
|
setGlobalHotkeyStopPause({ commit, dispatch }, key) { // <-- New action
|
||||||
fullResetApp({ commit, dispatch }) {
|
commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', key);
|
||||||
StorageService.clearState();
|
dispatch('saveState');
|
||||||
// Re-initialize state to default, which includes predefined players
|
},
|
||||||
// This is tricky because the store is already created.
|
setGlobalHotkeyRunAll({ commit, dispatch }, key) { // <-- New action
|
||||||
// Easiest is to rely on a page reload after clearing storage,
|
commit('SET_GLOBAL_HOTKEY_RUN_ALL', key);
|
||||||
// or manually reset each piece of state to initialState.
|
dispatch('saveState');
|
||||||
commit('SET_PLAYERS', JSON.parse(JSON.stringify(predefinedPlayers)));
|
},
|
||||||
commit('SET_CURRENT_PLAYER_INDEX', initialState.currentPlayerIndex);
|
fullResetApp({ commit, dispatch, state: currentGlobalState }) {
|
||||||
commit('SET_GAME_MODE', initialState.gameMode);
|
StorageService.clearState();
|
||||||
commit('SET_IS_MUTED', initialState.isMuted);
|
const freshInitialState = JSON.parse(JSON.stringify(initialState));
|
||||||
commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', initialState.globalHotkeyStopPause);
|
|
||||||
// Manually set theme if it needs to be reset from initialState too
|
commit('SET_PLAYERS', freshInitialState.players);
|
||||||
if (store.state.theme !== initialState.theme) {
|
commit('SET_CURRENT_PLAYER_INDEX', freshInitialState.currentPlayerIndex);
|
||||||
commit('TOGGLE_THEME'); // This assumes toggle flips it, adjust if direct set is better
|
commit('SET_GAME_MODE', freshInitialState.gameMode);
|
||||||
}
|
commit('SET_IS_MUTED', freshInitialState.isMuted);
|
||||||
commit('SET_GAME_RUNNING', false);
|
commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', freshInitialState.globalHotkeyStopPause);
|
||||||
dispatch('saveState'); // Save this fresh state
|
commit('SET_GLOBAL_HOTKEY_RUN_ALL', freshInitialState.globalHotkeyRunAll);
|
||||||
},
|
|
||||||
|
if (currentGlobalState.theme !== freshInitialState.theme) {
|
||||||
|
commit('SET_THEME', freshInitialState.theme);
|
||||||
|
}
|
||||||
|
commit('SET_GAME_RUNNING', false);
|
||||||
|
dispatch('saveState');
|
||||||
|
},
|
||||||
tick({ commit, state }) {
|
tick({ commit, state }) {
|
||||||
if (state.gameMode === 'normal') {
|
if (state.gameMode === 'normal') {
|
||||||
if (state.players[state.currentPlayerIndex]?.isTimerRunning) {
|
if (state.players[state.currentPlayerIndex]?.isTimerRunning) {
|
||||||
@@ -437,6 +452,7 @@ export default createStore({
|
|||||||
isMuted: state => state.isMuted,
|
isMuted: state => state.isMuted,
|
||||||
theme: state => state.theme,
|
theme: state => state.theme,
|
||||||
globalHotkeyStopPause: state => state.globalHotkeyStopPause,
|
globalHotkeyStopPause: state => state.globalHotkeyStopPause,
|
||||||
|
globalHotkeyRunAll: state => state.globalHotkeyRunAll,
|
||||||
totalPlayers: state => state.players.length,
|
totalPlayers: state => state.players.length,
|
||||||
gameRunning: state => state.gameRunning,
|
gameRunning: state => state.gameRunning,
|
||||||
maxNegativeTimeReached: () => MAX_NEGATIVE_SECONDS,
|
maxNegativeTimeReached: () => MAX_NEGATIVE_SECONDS,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-screen overflow-hidden" :class="{'dark': theme === 'dark'}">
|
<div class="flex flex-col h-screen overflow-hidden" :class="{'dark': theme === 'dark'}">
|
||||||
<!-- Header Bar -->
|
|
||||||
<header class="p-3 bg-gray-100 dark:bg-gray-800 shadow-md flex justify-between items-center shrink-0">
|
<header class="p-3 bg-gray-100 dark:bg-gray-800 shadow-md flex justify-between items-center shrink-0">
|
||||||
|
<!-- ... header content ... -->
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<button @click="navigateToSetup" class="btn btn-secondary text-sm">
|
<button @click="navigateToSetup" class="btn btn-secondary text-sm">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-1" viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" /></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 d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" /></svg>
|
||||||
@@ -22,21 +22,22 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleMute" class="btn-icon">
|
<button @click="toggleMute" class="btn-icon">
|
||||||
<!-- Mute/Unmute SVG -->
|
|
||||||
<svg v-if="!isMuted" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" /></svg>
|
<svg v-if="!isMuted" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" /></svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15zm9.949-9.949a1 1 0 00-1.414 0L12 7.05l-2.085-2.085a1 1 0 00-1.414 1.414L10.586 8.5l-2.085 2.085a1 1 0 001.414 1.414L12 9.914l2.085 2.085a1 1 0 001.414-1.414L13.414 8.5l2.085-2.085a1 1 0 000-1.414z" /></svg>
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15zm9.949-9.949a1 1 0 00-1.414 0L12 7.05l-2.085-2.085a1 1 0 00-1.414 1.414L10.586 8.5l-2.085 2.085a1 1 0 001.414 1.414L12 9.914l2.085 2.085a1 1 0 001.414-1.414L13.414 8.5l2.085-2.085a1 1 0 000-1.414z" /></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-grow overflow-y-auto" ref="gameArea">
|
<!-- Main content area should allow its children to use its full height -->
|
||||||
<!-- Normal Mode -->
|
<main class="flex-grow overflow-hidden flex flex-col" ref="gameArea">
|
||||||
|
<!-- Normal Mode: This div itself needs to take full height of 'main' -->
|
||||||
<div v-if="gameMode === 'normal' && currentPlayer && nextPlayer" class="h-full flex flex-col">
|
<div v-if="gameMode === 'normal' && currentPlayer && nextPlayer" class="h-full flex flex-col">
|
||||||
<PlayerDisplay
|
<PlayerDisplay
|
||||||
:player="currentPlayer"
|
:player="currentPlayer"
|
||||||
is-current-player-area
|
is-current-player-area
|
||||||
area-class="bg-gray-100 dark:bg-gray-800"
|
area-class="bg-gray-100 dark:bg-gray-800"
|
||||||
@tapped="handleCurrentPlayerTap"
|
@tapped="handleCurrentPlayerTap"
|
||||||
|
class="flex-1 min-h-0"
|
||||||
/>
|
/>
|
||||||
<div class="shrink-0 h-1 bg-blue-500"></div>
|
<div class="shrink-0 h-1 bg-blue-500"></div>
|
||||||
<PlayerDisplay
|
<PlayerDisplay
|
||||||
@@ -44,13 +45,13 @@
|
|||||||
is-next-player-area
|
is-next-player-area
|
||||||
area-class="bg-gray-200 dark:bg-gray-700"
|
area-class="bg-gray-200 dark:bg-gray-700"
|
||||||
@swiped-up="handlePassTurn"
|
@swiped-up="handlePassTurn"
|
||||||
|
class="flex-1 min-h-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- All Timers Running Mode -->
|
<!-- All Timers Running Mode -->
|
||||||
<div v-if="gameMode === 'allTimers'" class="p-4 h-full flex flex-col">
|
<div v-if="gameMode === 'allTimers'" class="p-4 h-full flex flex-col">
|
||||||
<!-- Removed the "Pause All / Resume All" button from here -->
|
<div class="mb-4 flex justify-start items-center">
|
||||||
<div class="mb-4 flex justify-start items-center"> <!-- Changed justify-between to justify-start -->
|
|
||||||
<h2 class="text-2xl font-semibold">All Timers Running</h2>
|
<h2 class="text-2xl font-semibold">All Timers Running</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow overflow-y-auto space-y-2">
|
<div class="flex-grow overflow-y-auto space-y-2">
|
||||||
@@ -60,10 +61,10 @@
|
|||||||
:player="player"
|
:player="player"
|
||||||
@tapped="() => handlePlayerTapAllTimers(indexInFullList(player.id))"
|
@tapped="() => handlePlayerTapAllTimers(indexInFullList(player.id))"
|
||||||
/>
|
/>
|
||||||
<p v-if="playersInAllTimersView.length === 0 && players.length > 0 && anyTimerCouldRun()" class="text-center text-gray-500 dark:text-gray-400 py-6">
|
<p v-if="playersInAllTimersView.length === 0 && players.length > 0 && anyTimerCouldRun" class="text-center text-gray-500 dark:text-gray-400 py-6">
|
||||||
All active timers paused.
|
All active timers paused.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="playersInAllTimersView.length === 0 && players.length > 0 && !anyTimerCouldRun()" class="text-center text-gray-500 dark:text-gray-400 py-6">
|
<p v-if="playersInAllTimersView.length === 0 && players.length > 0 && !anyTimerCouldRun" class="text-center text-gray-500 dark:text-gray-400 py-6">
|
||||||
No players eligible to run. (All skipped or issue)
|
No players eligible to run. (All skipped or issue)
|
||||||
</p>
|
</p>
|
||||||
<p v-if="players.length === 0" class="text-center text-gray-500 dark:text-gray-400 py-6">
|
<p v-if="players.length === 0" class="text-center text-gray-500 dark:text-gray-400 py-6">
|
||||||
@@ -72,10 +73,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="p-3 bg-gray-100 dark:bg-gray-800 shadow-inner shrink-0 text-center">
|
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">Nexus Timer</span>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -96,32 +93,36 @@ const currentPlayer = computed(() => store.getters.currentPlayer);
|
|||||||
const nextPlayer = computed(() => store.getters.nextPlayer);
|
const nextPlayer = computed(() => store.getters.nextPlayer);
|
||||||
const gameMode = computed(() => store.getters.gameMode);
|
const gameMode = computed(() => store.getters.gameMode);
|
||||||
const isMuted = computed(() => store.getters.isMuted);
|
const isMuted = computed(() => store.getters.isMuted);
|
||||||
const gameRunning = computed(() => store.getters.gameRunning);
|
// const gameRunning = computed(() => store.getters.gameRunning);
|
||||||
|
|
||||||
let timerInterval = null;
|
let timerInterval = null;
|
||||||
|
|
||||||
const playersInAllTimersView = computed(() => {
|
const playersInAllTimersView = computed(() => {
|
||||||
if (gameMode.value === 'allTimers') {
|
if (gameMode.value === 'allTimers') {
|
||||||
|
if (!players.value) return [];
|
||||||
return players.value.filter(p => p.isTimerRunning && !p.isSkipped);
|
return players.value.filter(p => p.isTimerRunning && !p.isSkipped);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const anyTimerRunningInAllMode = computed(() => {
|
const anyTimerRunningInAllMode = computed(() => {
|
||||||
|
if (!players.value) return false;
|
||||||
return players.value.some(p => p.isTimerRunning && !p.isSkipped);
|
return players.value.some(p => p.isTimerRunning && !p.isSkipped);
|
||||||
});
|
});
|
||||||
|
|
||||||
const anyTimerCouldRun = computed(() => {
|
const anyTimerCouldRun = computed(() => {
|
||||||
|
if (!players.value) return false;
|
||||||
return players.value.some(p => !p.isSkipped);
|
return players.value.some(p => !p.isSkipped);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const indexInFullList = (playerId) => {
|
const indexInFullList = (playerId) => {
|
||||||
|
if (!players.value) return -1;
|
||||||
return players.value.findIndex(p => p.id === playerId);
|
return players.value.findIndex(p => p.id === playerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (players.value.length < 2) {
|
if (!players.value || players.value.length < 2) {
|
||||||
router.push({ name: 'Setup' });
|
router.push({ name: 'Setup' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -137,26 +138,23 @@ onUnmounted(() => {
|
|||||||
AudioService.cancelPassTurnSound();
|
AudioService.cancelPassTurnSound();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch gameMode for audio changes
|
|
||||||
watch(gameMode, (newMode) => {
|
watch(gameMode, (newMode) => {
|
||||||
// Always stop any ongoing sounds when mode changes
|
|
||||||
AudioService.stopContinuousTick();
|
AudioService.stopContinuousTick();
|
||||||
AudioService.cancelPassTurnSound();
|
AudioService.cancelPassTurnSound();
|
||||||
|
|
||||||
if (newMode === 'allTimers') {
|
if (newMode === 'allTimers') {
|
||||||
if (anyTimerRunningInAllMode.value) { // If timers are already running when switching TO this mode
|
if (anyTimerRunningInAllMode.value) {
|
||||||
AudioService.startContinuousTick();
|
AudioService.startContinuousTick();
|
||||||
}
|
}
|
||||||
} else { // normal mode
|
} else {
|
||||||
if (currentPlayer.value && currentPlayer.value.isTimerRunning) {
|
if (currentPlayer.value && currentPlayer.value.isTimerRunning) {
|
||||||
AudioService.playPassTurnAlert();
|
AudioService.playPassTurnAlert();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch specific timer states for audio in AllTimers mode (for ticking)
|
|
||||||
watch(anyTimerRunningInAllMode, (isRunning) => {
|
watch(anyTimerRunningInAllMode, (isRunning) => {
|
||||||
if (gameMode.value === 'allTimers') { // Only act if in allTimers mode
|
if (gameMode.value === 'allTimers') {
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
AudioService.startContinuousTick();
|
AudioService.startContinuousTick();
|
||||||
} else {
|
} else {
|
||||||
@@ -165,14 +163,12 @@ watch(anyTimerRunningInAllMode, (isRunning) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch current player for pass turn alert in Normal mode
|
|
||||||
watch(currentPlayer, (newPlayer, oldPlayer) => {
|
watch(currentPlayer, (newPlayer, oldPlayer) => {
|
||||||
if (gameMode.value === 'normal' && newPlayer && newPlayer.isTimerRunning && oldPlayer && newPlayer.id !== oldPlayer.id) {
|
if (gameMode.value === 'normal' && newPlayer && newPlayer.isTimerRunning && oldPlayer && newPlayer.id !== oldPlayer.id) {
|
||||||
AudioService.playPassTurnAlert();
|
AudioService.playPassTurnAlert();
|
||||||
}
|
}
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
// Watch current player's timer running state for audio in Normal mode (manual tap)
|
|
||||||
watch(() => currentPlayer.value?.isTimerRunning, (isRunning, wasRunning) => {
|
watch(() => currentPlayer.value?.isTimerRunning, (isRunning, wasRunning) => {
|
||||||
if (gameMode.value === 'normal' && currentPlayer.value) {
|
if (gameMode.value === 'normal' && currentPlayer.value) {
|
||||||
if (isRunning === true && wasRunning === false) {
|
if (isRunning === true && wasRunning === false) {
|
||||||
@@ -216,7 +212,8 @@ const handlePassTurn = () => {
|
|||||||
AudioService.cancelPassTurnSound();
|
AudioService.cancelPassTurnSound();
|
||||||
const wasRunning = currentPlayer.value.isTimerRunning;
|
const wasRunning = currentPlayer.value.isTimerRunning;
|
||||||
store.dispatch('passTurn').then(() => {
|
store.dispatch('passTurn').then(() => {
|
||||||
if (wasRunning && gameMode.value === 'normal' && currentPlayer.value && currentPlayer.value.isTimerRunning) {
|
const newCurrentPlayer = store.getters.currentPlayer;
|
||||||
|
if (wasRunning && gameMode.value === 'normal' && newCurrentPlayer && newCurrentPlayer.isTimerRunning) {
|
||||||
AudioService.playPassTurnAlert();
|
AudioService.playPassTurnAlert();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -227,20 +224,16 @@ const handlePlayerTapAllTimers = (playerIndex) => {
|
|||||||
store.dispatch('togglePlayerTimerAllTimersMode', playerIndex);
|
store.dispatch('togglePlayerTimerAllTimersMode', playerIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
// handleStopStartAllTimers button was removed from the template for AllTimersMode
|
|
||||||
|
|
||||||
const switchToAllTimersMode = () => {
|
const switchToAllTimersMode = () => {
|
||||||
store.dispatch('switchToAllTimersMode');
|
store.dispatch('switchToAllTimersMode');
|
||||||
};
|
};
|
||||||
|
|
||||||
const switchToNormalMode = () => {
|
const switchToNormalMode = () => {
|
||||||
store.dispatch('switchToNormalMode'); // This action already pauses all timers
|
store.dispatch('switchToNormalMode');
|
||||||
// Audio for normal mode (if current player starts) is handled by watchers
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watcher for auto-reverting from All Timers mode
|
|
||||||
watch(anyTimerRunningInAllMode, (anyRunning) => {
|
watch(anyTimerRunningInAllMode, (anyRunning) => {
|
||||||
if (gameMode.value === 'allTimers' && !anyRunning && players.value.length > 0) {
|
if (gameMode.value === 'allTimers' && !anyRunning && players.value && players.value.length > 0) {
|
||||||
const nonSkippedPlayersExist = players.value.some(p => !p.isSkipped);
|
const nonSkippedPlayersExist = players.value.some(p => !p.isSkipped);
|
||||||
if (nonSkippedPlayersExist) {
|
if (nonSkippedPlayersExist) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -253,5 +246,4 @@ watch(anyTimerRunningInAllMode, (anyRunning) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
<!-- Player Management -->
|
<!-- 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 }}/7)</h2>
|
<h2 class="text-2xl font-semibold">Players ({{ players.length }})</h2>
|
||||||
<button @click="openAddPlayerModal" class="btn btn-primary" :disabled="players.length >= 7">
|
<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
|
||||||
</button>
|
</button>
|
||||||
@@ -17,14 +17,35 @@
|
|||||||
|
|
||||||
<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"> <!-- Added flex-wrap for smaller screens -->
|
<div class="flex items-center flex-wrap">
|
||||||
<img :src="player.avatar" alt="Avatar" class="w-10 h-10 rounded-full object-cover mr-3">
|
<!-- *** 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"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- *** 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.initialTimerSec) }})</span>
|
<span class="mr-2 text-xs text-gray-500 dark:text-gray-400">({{ formatTime(player.currentTimerSec) }})</span>
|
||||||
<!-- Changed HK to Hotkey -->
|
|
||||||
<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-2 flex-shrink-0"> <!-- Added 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">
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
<button @click="movePlayerDown(index)" :disabled="index === players.length - 1" 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="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button @click="openEditPlayerModal(player)" class="btn-icon text-yellow-500 hover:text-yellow-600">
|
<button @click="openEditPlayerModal(player)" class="btn-icon text-yellow-500 hover:text-yellow-600">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" /><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" /><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" /></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -45,20 +66,48 @@
|
|||||||
|
|
||||||
<!-- Game Settings -->
|
<!-- Game Settings -->
|
||||||
<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">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Game Settings</h2>
|
<!-- ... Global hotkey inputs ... -->
|
||||||
<div class="mb-4">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<label for="globalHotkey" class="block text-sm font-medium text-gray-700 dark:text-gray-300">"Global Stop/Pause All" Hotkey</label>
|
<label for="globalHotkeyStopPause" class="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap mr-3">
|
||||||
<input
|
Global "Stop/Pause All" Hotkey:
|
||||||
type="text"
|
</label>
|
||||||
id="globalHotkey"
|
<div class="flex items-center">
|
||||||
:value="globalHotkeyDisplay"
|
<input
|
||||||
@keydown.prevent="captureGlobalHotkey"
|
type="text"
|
||||||
placeholder="Press a key"
|
id="globalHotkeyStopPause"
|
||||||
class="input-base w-full sm:w-1/2"
|
:value="globalHotkeyStopPauseDisplay"
|
||||||
readonly
|
@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" @click="clearGlobalHotkey" class="text-xs text-blue-500 hover:underline mt-1 ml-2">Clear Hotkey</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>
|
||||||
|
<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">
|
||||||
|
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" @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">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<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'">
|
||||||
@@ -86,6 +135,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Player Form Modal -->
|
||||||
<PlayerForm
|
<PlayerForm
|
||||||
v-if="showPlayerModal"
|
v-if="showPlayerModal"
|
||||||
:player="editingPlayer"
|
:player="editingPlayer"
|
||||||
@@ -96,12 +146,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// ... (script setup remains the same)
|
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { useRouter } from 'vue-router';
|
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';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -109,16 +159,22 @@ 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 globalHotkey = computed(() => store.getters.globalHotkeyStopPause);
|
|
||||||
const globalHotkeyDisplay = computed(() => globalHotkey.value ? globalHotkey.value.toUpperCase() : '');
|
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 showPlayerModal = ref(false);
|
||||||
const editingPlayer = ref(null);
|
const editingPlayer = ref(null);
|
||||||
|
|
||||||
const openAddPlayerModal = () => {
|
const openAddPlayerModal = () => {
|
||||||
if (players.value.length < 7) {
|
if (players.value.length < 99) {
|
||||||
editingPlayer.value = null;
|
editingPlayer.value = null;
|
||||||
showPlayerModal.value = true;
|
showPlayerModal.value = true;
|
||||||
|
} else {
|
||||||
|
alert("Maximum player limit (99) reached.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,6 +196,7 @@ const savePlayer = (playerData) => {
|
|||||||
name: playerData.name,
|
name: playerData.name,
|
||||||
avatar: playerData.avatar,
|
avatar: playerData.avatar,
|
||||||
initialTimerSec: playerData.initialTimerSec,
|
initialTimerSec: playerData.initialTimerSec,
|
||||||
|
currentTimerSec: playerData.currentTimerSec,
|
||||||
hotkey: playerData.hotkey
|
hotkey: playerData.hotkey
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -160,21 +217,63 @@ const reversePlayers = () => {
|
|||||||
store.dispatch('reversePlayers');
|
store.dispatch('reversePlayers');
|
||||||
};
|
};
|
||||||
|
|
||||||
const captureGlobalHotkey = (event) => {
|
const movePlayerUp = (index) => {
|
||||||
event.preventDefault();
|
if (index > 0) {
|
||||||
const key = event.key.toLowerCase();
|
const newPlayersOrder = [...players.value];
|
||||||
if (key && !['control', 'alt', 'shift', 'meta'].includes(key)) {
|
const temp = newPlayersOrder[index];
|
||||||
const existingPlayerHotkey = store.state.players.some(p => p.hotkey === key);
|
newPlayersOrder[index] = newPlayersOrder[index - 1];
|
||||||
if (existingPlayerHotkey) {
|
newPlayersOrder[index - 1] = temp;
|
||||||
alert(`Hotkey "${key.toUpperCase()}" is already assigned to a player.`);
|
store.dispatch('reorderPlayers', newPlayersOrder);
|
||||||
return;
|
|
||||||
}
|
|
||||||
store.dispatch('setGlobalHotkey', key);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearGlobalHotkey = () => {
|
const movePlayerDown = (index) => {
|
||||||
store.dispatch('setGlobalHotkey', null);
|
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 isPlayerHotkey = store.state.players.some(p => p.hotkey === key);
|
||||||
|
if (isPlayerHotkey) {
|
||||||
|
alert(`Hotkey "${key.toUpperCase()}" is already assigned to a player.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'stopPause') {
|
||||||
|
if (store.state.globalHotkeyRunAll === key) {
|
||||||
|
alert(`Hotkey "${key.toUpperCase()}" is already assigned to Global "Run All Timers".`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.dispatch('setGlobalHotkeyStopPause', key);
|
||||||
|
} else if (type === 'runAll') {
|
||||||
|
if (store.state.globalHotkeyStopPause === key) {
|
||||||
|
alert(`Hotkey "${key.toUpperCase()}" is already assigned to Global "Stop/Pause All".`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.dispatch('setGlobalHotkeyRunAll', key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearGlobalHotkey = (type) => {
|
||||||
|
if (type === 'stopPause') {
|
||||||
|
store.dispatch('setGlobalHotkeyStopPause', null);
|
||||||
|
} else if (type === 'runAll') {
|
||||||
|
store.dispatch('setGlobalHotkeyRunAll', null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
@@ -188,10 +287,9 @@ const toggleMute = () => {
|
|||||||
const saveAndClose = () => {
|
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) { // Ensure valid index
|
if (store.state.currentPlayerIndex >= players.value.length || store.state.currentPlayerIndex < 0) {
|
||||||
store.commit('SET_CURRENT_PLAYER_INDEX', 0);
|
store.commit('SET_CURRENT_PLAYER_INDEX', 0);
|
||||||
}
|
}
|
||||||
// When saving and closing from setup, always start in normal mode, paused.
|
|
||||||
store.commit('PAUSE_ALL_TIMERS');
|
store.commit('PAUSE_ALL_TIMERS');
|
||||||
store.commit('SET_GAME_MODE', 'normal');
|
store.commit('SET_GAME_MODE', 'normal');
|
||||||
router.push({ name: 'Game' });
|
router.push({ name: 'Game' });
|
||||||
|
|||||||
Reference in New Issue
Block a user