231 lines
8.3 KiB
Vue
231 lines
8.3 KiB
Vue
<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="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 -->
|
|
<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">
|
|
<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>
|
|
<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">
|
|
"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"
|
|
>
|
|
<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"
|
|
>
|
|
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>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, watch, onMounted, computed } from 'vue';
|
|
import { useStore } from 'vuex';
|
|
import { CameraService } from '../services/CameraService';
|
|
import { formatTime, parseTime } from '../utils/timeFormatter';
|
|
import DefaultAvatarIcon from './DefaultAvatarIcon.vue';
|
|
import { AudioService } from '../services/AudioService';
|
|
|
|
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 currentTimeFormatted = ref('60:00');
|
|
const currentTimeFormatError = ref('');
|
|
const cameraError = ref('');
|
|
|
|
const maxNegativeSeconds = computed(() => store.getters.maxNegativeTimeReached);
|
|
|
|
|
|
onMounted(() => {
|
|
if (isEditing.value && props.player) {
|
|
editablePlayer.id = props.player.id;
|
|
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 {
|
|
const parsedSeconds = parseTime(time);
|
|
if (isNegativeInput && parsedSeconds > 0) {
|
|
currentTimeFormatError.value = 'Negative time should parse to negative seconds.';
|
|
} else if (parsedSeconds < maxNegativeSeconds.value) {
|
|
currentTimeFormatError.value = `Time cannot be less than ${formatTime(maxNegativeSeconds.value)}.`;
|
|
}
|
|
else {
|
|
currentTimeFormatError.value = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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 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();
|
|
}
|
|
|
|
function closeModal() {
|
|
emit('close');
|
|
}
|
|
</script> |