Files
game-timer/js/ui/ui.js
2025-03-31 23:33:18 +02:00

289 lines
11 KiB
JavaScript

// ui.js
import * as state from '../core/state.js';
import { GAME_STATES, CSS_CLASSES, DEFAULT_PLAYER_TIME_SECONDS } from '../config.js';
import audioManager from './audio.js';
// --- DOM Elements ---
export const elements = {
carousel: document.getElementById('carousel'),
gameButton: document.getElementById('gameButton'),
setupButton: document.getElementById('setupButton'),
addPlayerButton: document.getElementById('addPlayerButton'),
resetButton: document.getElementById('resetButton'),
playerModal: document.getElementById('playerModal'),
resetModal: document.getElementById('resetModal'),
playerForm: document.getElementById('playerForm'),
modalTitle: document.getElementById('modalTitle'),
playerNameInput: document.getElementById('playerName'),
playerTimeInput: document.getElementById('playerTime'),
playerImageInput: document.getElementById('playerImage'),
imagePreview: document.getElementById('imagePreview'),
playerTimeContainer: document.getElementById('playerTimeContainer'), // Parent of playerTimeInput
remainingTimeContainer: document.getElementById('remainingTimeContainer'),
playerRemainingTimeInput: document.getElementById('playerRemainingTime'),
deletePlayerButton: document.getElementById('deletePlayerButton'),
cancelButton: document.getElementById('cancelButton'), // Modal cancel
resetCancelButton: document.getElementById('resetCancelButton'),
resetConfirmButton: document.getElementById('resetConfirmButton'),
cameraButton: document.getElementById('cameraButton'),
// Camera elements needed by camera.js, but listed here for central management if desired
cameraContainer: document.getElementById('cameraContainer'),
cameraView: document.getElementById('cameraView'),
cameraCanvas: document.getElementById('cameraCanvas'),
cameraCaptureButton: document.getElementById('cameraCaptureButton'),
cameraCancelButton: document.getElementById('cameraCancelButton'),
// Header buttons container for sound toggle
headerButtons: document.querySelector('.header-buttons')
};
let isDragging = false;
let startX = 0;
let currentX = 0;
let carouselSwipeHandler = null; // To store the bound function for removal
// --- Rendering Functions ---
export function renderPlayers() {
const players = state.getPlayers();
const currentIndex = state.getCurrentPlayerIndex();
const currentGameState = state.getGameState();
elements.carousel.innerHTML = '';
if (players.length === 0) {
// Optionally display a message if there are no players
elements.carousel.innerHTML = '<p style="text-align: center; width: 100%;">Add players to start</p>';
return;
}
players.forEach((player, index) => {
const card = document.createElement('div');
const isActive = index === currentIndex;
card.className = `player-card ${isActive ? CSS_CLASSES.ACTIVE_PLAYER : CSS_CLASSES.INACTIVE_PLAYER}`;
const minutes = Math.floor(player.remainingTime / 60);
const seconds = player.remainingTime % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
const timerClasses = [];
if (isActive && currentGameState === GAME_STATES.RUNNING) {
timerClasses.push(CSS_CLASSES.TIMER_ACTIVE);
}
if (player.remainingTime <= 0) {
timerClasses.push(CSS_CLASSES.TIMER_FINISHED);
}
card.innerHTML = `
<div class="player-image">
${player.image ? `<img src="${player.image}" alt="${player.name}">` : '<i class="fas fa-user"></i>'}
</div>
<div class="player-name">${player.name}</div>
<div class="player-timer ${timerClasses.join(' ')}">${timeString}</div>
`;
elements.carousel.appendChild(card);
});
updateCarouselPosition();
}
export function updateCarouselPosition() {
const currentIndex = state.getCurrentPlayerIndex();
elements.carousel.style.transform = `translateX(${-100 * currentIndex}%)`;
}
export function updateGameButton() {
const currentGameState = state.getGameState();
switch (currentGameState) {
case GAME_STATES.SETUP:
elements.gameButton.textContent = 'Start Game';
break;
case GAME_STATES.RUNNING:
elements.gameButton.textContent = 'Pause Game';
break;
case GAME_STATES.PAUSED:
elements.gameButton.textContent = 'Resume Game';
break;
case GAME_STATES.OVER:
elements.gameButton.textContent = 'Game Over';
break;
}
// Disable button if less than 2 players in setup
elements.gameButton.disabled = currentGameState === GAME_STATES.SETUP && state.getPlayers().length < 2;
}
// --- Modal Functions ---
export function showPlayerModal(isNewPlayer, player = null) {
const currentGameState = state.getGameState();
if (isNewPlayer) {
elements.modalTitle.textContent = 'Add New Player';
elements.playerNameInput.value = `Player ${state.getPlayers().length + 1}`;
elements.playerTimeInput.value = DEFAULT_PLAYER_TIME_SECONDS / 60; // Use default time from config
elements.remainingTimeContainer.style.display = 'none';
elements.playerTimeContainer.style.display = 'block'; // Ensure Time field is visible for new players
elements.imagePreview.innerHTML = '<i class="fas fa-user"></i>';
elements.deletePlayerButton.style.display = 'none';
} else if (player) {
elements.modalTitle.textContent = 'Edit Player';
elements.playerNameInput.value = player.name;
elements.playerTimeInput.value = player.timeInSeconds / 60;
if (currentGameState === GAME_STATES.PAUSED || currentGameState === GAME_STATES.OVER) {
elements.remainingTimeContainer.style.display = 'block';
elements.playerTimeContainer.style.display = 'none'; // Hide Time field when Remaining Time is shown
const minutes = Math.floor(player.remainingTime / 60);
const seconds = player.remainingTime % 60;
elements.playerRemainingTimeInput.value = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} else {
elements.remainingTimeContainer.style.display = 'none';
elements.playerTimeContainer.style.display = 'block'; // Ensure Time field is visible otherwise
}
elements.imagePreview.innerHTML = player.image ? `<img src="${player.image}" alt="${player.name}">` : '<i class="fas fa-user"></i>';
elements.deletePlayerButton.style.display = 'block';
}
// Reset file input and captured image data
elements.playerImageInput.value = '';
elements.playerImageInput.dataset.capturedImage = '';
elements.playerModal.classList.add(CSS_CLASSES.MODAL_ACTIVE);
audioManager.play('modalOpen');
}
export function hidePlayerModal() {
elements.playerModal.classList.remove(CSS_CLASSES.MODAL_ACTIVE);
audioManager.play('modalClose');
// Potentially call camera cleanup here if it wasn't done elsewhere
}
export function showResetModal() {
elements.resetModal.classList.add(CSS_CLASSES.MODAL_ACTIVE);
audioManager.play('modalOpen');
}
export function hideResetModal() {
elements.resetModal.classList.remove(CSS_CLASSES.MODAL_ACTIVE);
audioManager.play('modalClose');
}
export function updateImagePreviewFromFile(file) {
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
elements.imagePreview.innerHTML = `<img src="${event.target.result}" alt="Player Preview">`;
// Clear any previously captured image data if a file is selected
elements.playerImageInput.dataset.capturedImage = '';
};
reader.readAsDataURL(file);
}
}
export function updateImagePreviewFromDataUrl(dataUrl) {
elements.imagePreview.innerHTML = `<img src="${dataUrl}" alt="Player Preview">`;
// Store data URL and clear file input
elements.playerImageInput.dataset.capturedImage = dataUrl;
elements.playerImageInput.value = '';
}
// --- Carousel Touch Handling ---
function handleTouchStart(e) {
startX = e.touches[0].clientX;
currentX = startX;
isDragging = true;
// Optional: Add a class to the carousel for visual feedback during drag
elements.carousel.style.transition = 'none'; // Disable transition during drag
}
function handleTouchMove(e) {
if (!isDragging) return;
currentX = e.touches[0].clientX;
const diff = currentX - startX;
const currentIndex = state.getCurrentPlayerIndex();
const currentTranslate = -100 * currentIndex + (diff / elements.carousel.offsetWidth * 100);
elements.carousel.style.transform = `translateX(${currentTranslate}%)`;
}
function handleTouchEnd(e) {
if (!isDragging) return;
isDragging = false;
elements.carousel.style.transition = ''; // Re-enable transition
const diff = currentX - startX;
const threshold = elements.carousel.offsetWidth * 0.1; // 10% swipe threshold
if (Math.abs(diff) > threshold) {
// Call the handler passed during initialization
if (carouselSwipeHandler) {
carouselSwipeHandler(diff < 0 ? 1 : -1); // Pass direction: 1 for next, -1 for prev
}
} else {
// Snap back if swipe wasn't enough
updateCarouselPosition();
}
}
// --- UI Initialization ---
// Add sound toggle button
function createSoundToggleButton() {
const soundButton = document.createElement('button');
soundButton.id = 'soundToggleButton';
soundButton.className = 'header-button';
soundButton.title = 'Toggle Sound';
soundButton.innerHTML = audioManager.muted ? '<i class="fas fa-volume-mute"></i>' : '<i class="fas fa-volume-up"></i>';
soundButton.addEventListener('click', () => {
const isMuted = audioManager.toggleMute();
soundButton.innerHTML = isMuted ? '<i class="fas fa-volume-mute"></i>' : '<i class="fas fa-volume-up"></i>';
if (!isMuted) audioManager.play('buttonClick'); // Feedback only when unmuting
});
elements.headerButtons.prepend(soundButton); // Add to the beginning
}
// Parse time string (MM:SS) to seconds - Helper needed for form processing
export function parseTimeString(timeString) {
if (!/^\d{1,2}:\d{2}$/.test(timeString)) {
console.error('Invalid time format:', timeString);
return null; // Indicate error
}
const parts = timeString.split(':');
const minutes = parseInt(parts[0], 10);
const seconds = parseInt(parts[1], 10);
if (isNaN(minutes) || isNaN(seconds) || seconds > 59) {
console.error('Invalid time value:', timeString);
return null;
}
return (minutes * 60) + seconds;
}
// Sets up basic UI elements and listeners that primarily affect the UI itself
export function initUI(options) {
// Store the swipe handler provided by app.js
carouselSwipeHandler = options.onCarouselSwipe;
createSoundToggleButton();
// Carousel touch events
elements.carousel.addEventListener('touchstart', handleTouchStart, { passive: true });
elements.carousel.addEventListener('touchmove', handleTouchMove, { passive: true });
elements.carousel.addEventListener('touchend', handleTouchEnd);
// Image file input preview
elements.playerImageInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
updateImagePreviewFromFile(file);
}
});
// Initial render
renderPlayers();
updateGameButton();
}