refactoring
This commit is contained in:
286
ui.js
Normal file
286
ui.js
Normal file
@@ -0,0 +1,286 @@
|
||||
// ui.js
|
||||
import * as state from './state.js';
|
||||
import { GAME_STATES, CSS_CLASSES } 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 = state.DEFAULT_PLAYER_TIME_SECONDS / 60; // Use default time
|
||||
elements.remainingTimeContainer.style.display = 'none';
|
||||
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';
|
||||
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.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();
|
||||
}
|
||||
Reference in New Issue
Block a user