PWA fixed

This commit is contained in:
cpu
2025-05-08 15:36:17 +02:00
parent d741efa62d
commit 13b227cfc2
40 changed files with 5117 additions and 2 deletions

View File

@@ -0,0 +1,231 @@
<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>