// Import the audio manager import audioManager from './audio.js'; import deepLinkManager from './deeplinks.js'; // Initialize variables let players = []; let currentPlayerIndex = 0; let gameState = 'setup'; // setup, running, paused, over let carouselPosition = 0; let startX = 0; let currentX = 0; let pushSubscription = null; const PUBLIC_VAPID_KEY = 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E'; const BACKEND_URL = 'https://webpush.virtonline.eu'; const BUTTON_ID = 'your_button1_serial'; // DOM Elements const carousel = document.getElementById('carousel'); const gameButton = document.getElementById('gameButton'); const setupButton = document.getElementById('setupButton'); const addPlayerButton = document.getElementById('addPlayerButton'); const resetButton = document.getElementById('resetButton'); const playerModal = document.getElementById('playerModal'); const resetModal = document.getElementById('resetModal'); const playerForm = document.getElementById('playerForm'); const cancelButton = document.getElementById('cancelButton'); const deletePlayerButton = document.getElementById('deletePlayerButton'); const resetCancelButton = document.getElementById('resetCancelButton'); const resetConfirmButton = document.getElementById('resetConfirmButton'); const playerImage = document.getElementById('playerImage'); const imagePreview = document.getElementById('imagePreview'); const playerTimeContainer = document.getElementById('playerTimeContainer'); const remainingTimeContainer = document.getElementById('remainingTimeContainer'); const playerRemainingTime = document.getElementById('playerRemainingTime'); const cameraButton = document.getElementById('cameraButton'); const cameraContainer = document.getElementById('cameraContainer'); const cameraView = document.getElementById('cameraView'); const cameraCanvas = document.getElementById('cameraCanvas'); const cameraCaptureButton = document.getElementById('cameraCaptureButton'); const cameraCancelButton = document.getElementById('cameraCancelButton'); let stream = null; async function subscribeToPushNotifications() { if ('serviceWorker' in navigator && 'PushManager' in window) { try { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY) }); // Send subscription to your server await fetch(`${BACKEND_URL}/subscribe`, { method: 'POST', body: JSON.stringify({ button_id: BACKEND_URL, subscription: subscription // The PushSubscription object }), headers: { 'Content-Type': 'application/json' }, }); pushSubscription = subscription; console.log('Push subscription successful'); } catch (error) { console.error('Error subscribing to push:', error); } } } function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } // Add sound toggle button const createSoundToggleButton = () => { const headerButtons = document.querySelector('.header-buttons'); const soundButton = document.createElement('button'); soundButton.id = 'soundToggleButton'; soundButton.className = 'header-button'; soundButton.title = 'Toggle Sound'; soundButton.innerHTML = ''; soundButton.addEventListener('click', () => { const isMuted = audioManager.toggleMute(); soundButton.innerHTML = isMuted ? '' : ''; // Play feedback sound if unmuting if (!isMuted) { audioManager.play('buttonClick'); } }); headerButtons.prepend(soundButton); // Set initial icon state based on mute setting if (audioManager.muted) { soundButton.innerHTML = ''; } }; // Create the sound toggle button when page loads createSoundToggleButton(); // Load data from localStorage or use defaults function loadData() { const savedData = localStorage.getItem('gameTimerData'); if (savedData) { const parsedData = JSON.parse(savedData); players = parsedData.players; gameState = parsedData.gameState || 'setup'; currentPlayerIndex = parsedData.currentPlayerIndex || 0; } else { // Default players if no saved data players = [ { id: 1, name: 'Player 1', timeInSeconds: 300, remainingTime: 300, image: null }, { id: 2, name: 'Player 2', timeInSeconds: 300, remainingTime: 300, image: null } ]; saveData(); } renderPlayers(); updateGameButton(); } // Save data to localStorage function saveData() { const dataToSave = { players, gameState, currentPlayerIndex }; localStorage.setItem('gameTimerData', JSON.stringify(dataToSave)); } // Render players to carousel function renderPlayers() { carousel.innerHTML = ''; players.forEach((player, index) => { const card = document.createElement('div'); card.className = `player-card ${index === currentPlayerIndex ? 'active-player' : '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')}`; // Create timer element with appropriate classes const timerClasses = []; // Add timer-active class if this is current player and game is running if (index === currentPlayerIndex && gameState === 'running') { timerClasses.push('timer-active'); } // Add timer-finished class if player has no time left if (player.remainingTime <= 0) { timerClasses.push('timer-finished'); } card.innerHTML = `
${player.image ? `${player.name}` : ''}
${player.name}
${timeString}
`; carousel.appendChild(card); }); // Update carousel position carousel.style.transform = `translateX(${-100 * currentPlayerIndex}%)`; } // Update game button text based on game state function updateGameButton() { switch (gameState) { case 'setup': gameButton.textContent = 'Start Game'; break; case 'running': gameButton.textContent = 'Pause Game'; break; case 'paused': gameButton.textContent = 'Resume Game'; break; case 'over': gameButton.textContent = 'Game Over'; break; } } // Handle game button click gameButton.addEventListener('click', () => { // Play button click sound audioManager.play('buttonClick'); if (players.length < 2) { alert('You need at least 2 players to start a game.'); return; } switch (gameState) { case 'setup': gameState = 'running'; audioManager.play('gameStart'); startTimer(); break; case 'running': gameState = 'paused'; audioManager.play('gamePause'); stopTimer(); break; case 'paused': gameState = 'running'; audioManager.play('gameResume'); startTimer(); break; case 'over': // Reset timers and start new game players.forEach(player => { player.remainingTime = player.timeInSeconds; }); gameState = 'setup'; // No specific sound for this state change break; } updateGameButton(); renderPlayers(); // Make sure to re-render after state change saveData(); }); // Timer variables let timerInterval = null; // Check if all timers have reached zero function areAllTimersFinished() { return players.every(player => player.remainingTime <= 0); } // Find a player who still has time left function findNextPlayerWithTime() { const startIndex = (currentPlayerIndex + 1) % players.length; let index = startIndex; do { if (players[index].remainingTime > 0) { return index; } index = (index + 1) % players.length; } while (index !== startIndex); return -1; // No player has time left } // Start the timer for current player function startTimer() { if (timerInterval) clearInterval(timerInterval); // Stop any ongoing sounds when starting timer audioManager.stopAllSounds(); // Immediately render to show the active timer effect renderPlayers(); timerInterval = setInterval(() => { const currentPlayer = players[currentPlayerIndex]; // Only decrease time if the current player has time left if (currentPlayer.remainingTime > 0) { currentPlayer.remainingTime--; // Play appropriate timer sounds based on remaining time audioManager.playTimerSound(currentPlayer.remainingTime); } // Check if current player's time is up if (currentPlayer.remainingTime <= 0) { currentPlayer.remainingTime = 0; // Play time expired sound audioManager.playTimerExpired(); // Check if all timers are at zero if (areAllTimersFinished()) { gameState = 'over'; audioManager.play('gameOver'); updateGameButton(); stopTimer(); } else { // Find the next player who still has time const nextPlayerIndex = findNextPlayerWithTime(); if (nextPlayerIndex !== -1) { currentPlayerIndex = nextPlayerIndex; // Play switch player sound audioManager.play('playerSwitch'); } } } renderPlayers(); saveData(); }, 1000); } // Stop the timer function stopTimer() { clearInterval(timerInterval); timerInterval = null; renderPlayers(); // Make sure to re-render after stopping timer } // Carousel touch events let isDragging = false; carousel.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; currentX = startX; isDragging = true; }); carousel.addEventListener('touchmove', (e) => { if (!isDragging) return; currentX = e.touches[0].clientX; const diff = currentX - startX; const currentTranslate = -100 * currentPlayerIndex + (diff / carousel.offsetWidth * 100); carousel.style.transform = `translateX(${currentTranslate}%)`; }); carousel.addEventListener('touchend', (e) => { if (!isDragging) return; isDragging = false; const diff = currentX - startX; // If dragged more than 10% of width, change player if (Math.abs(diff) > carousel.offsetWidth * 0.1) { const previousIndex = currentPlayerIndex; // Only change players that have remaining time during a running game if (gameState === 'running') { let newIndex; if (diff < 0) { // Try to go to next player with time newIndex = findNextPlayerWithTimeCircular(currentPlayerIndex, 1); } else { // Try to go to previous player with time newIndex = findNextPlayerWithTimeCircular(currentPlayerIndex, -1); } if (newIndex !== -1) { currentPlayerIndex = newIndex; } } else { // Normal navigation when game not running if (diff < 0) { currentPlayerIndex = (currentPlayerIndex + 1) % players.length; } else if (diff > 0) { currentPlayerIndex = (currentPlayerIndex - 1 + players.length) % players.length; } } // Play player switch sound if player actually changed if (previousIndex !== currentPlayerIndex) { audioManager.play('playerSwitch'); } } // Reset carousel to proper position carousel.style.transform = `translateX(${-100 * currentPlayerIndex}%)`; renderPlayers(); saveData(); }); // Find next player with time in specified direction function findNextPlayerWithTimeCircular(startIndex, direction) { let index = startIndex; for (let i = 0; i < players.length; i++) { index = (index + direction + players.length) % players.length; if (players[index].remainingTime > 0) { return index; } } return -1; // No player has time left } // Setup button setupButton.addEventListener('click', () => { audioManager.play('buttonClick'); if (gameState === 'running') { alert('Please pause the game before editing players.'); return; } const currentPlayer = players[currentPlayerIndex]; document.getElementById('modalTitle').textContent = 'Edit Player'; document.getElementById('playerName').value = currentPlayer.name; document.getElementById('playerTime').value = currentPlayer.timeInSeconds / 60; // Show or hide remaining time edit field based on game state if (gameState === 'paused' || gameState === 'over') { remainingTimeContainer.style.display = 'block'; const minutes = Math.floor(currentPlayer.remainingTime / 60); const seconds = currentPlayer.remainingTime % 60; playerRemainingTime.value = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } else { remainingTimeContainer.style.display = 'none'; } if (currentPlayer.image) { imagePreview.innerHTML = `${currentPlayer.name}`; } else { imagePreview.innerHTML = ''; } playerModal.classList.add('active'); deletePlayerButton.style.display = 'block'; cleanupCameraData(); // Play modal open sound audioManager.play('modalOpen'); }); // Add player button addPlayerButton.addEventListener('click', () => { audioManager.play('buttonClick'); if (gameState === 'running') { alert('Please pause the game before adding players.'); return; } document.getElementById('modalTitle').textContent = 'Add New Player'; document.getElementById('playerName').value = `Player ${players.length + 1}`; document.getElementById('playerTime').value = 5; remainingTimeContainer.style.display = 'none'; imagePreview.innerHTML = ''; playerModal.classList.add('active'); deletePlayerButton.style.display = 'none'; cleanupCameraData(); // Play modal open sound audioManager.play('modalOpen'); }); // Reset button resetButton.addEventListener('click', () => { audioManager.play('buttonClick'); if (gameState === 'running') { alert('Please pause the game before resetting.'); return; } resetModal.classList.add('active'); audioManager.play('modalOpen'); }); // Cancel reset resetCancelButton.addEventListener('click', () => { audioManager.play('buttonClick'); resetModal.classList.remove('active'); audioManager.play('modalClose'); }); // Confirm reset resetConfirmButton.addEventListener('click', () => { audioManager.play('buttonClick'); players = [ { id: 1, name: 'Player 1', timeInSeconds: 300, remainingTime: 300, image: null }, { id: 2, name: 'Player 2', timeInSeconds: 300, remainingTime: 300, image: null } ]; gameState = 'setup'; currentPlayerIndex = 0; renderPlayers(); updateGameButton(); saveData(); resetModal.classList.remove('active'); // Play reset sound (use game over as it's a complete reset) audioManager.play('gameOver'); }); // Player image upload preview playerImage.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { imagePreview.innerHTML = `Player`; }; reader.readAsDataURL(file); } }); // Parse time string (MM:SS) to seconds function parseTimeString(timeString) { const [minutes, seconds] = timeString.split(':').map(part => parseInt(part, 10)); return (minutes * 60) + seconds; } // Camera button click handler cameraButton.addEventListener('click', async (e) => { e.preventDefault(); // Check if the browser supports getUserMedia if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { alert('Your browser does not support camera access or it is not available on this device.'); return; } try { // Get permission and access to the camera stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', // Default to front camera width: { ideal: 1280 }, height: { ideal: 720 } } }); // Show the camera UI cameraContainer.classList.add('active'); // Attach the stream to the video element cameraView.srcObject = stream; } catch (error) { console.error('Error accessing camera:', error); alert('Could not access the camera: ' + error.message); } }); // Camera capture button click handler cameraCaptureButton.addEventListener('click', () => { // Set canvas dimensions to match video cameraCanvas.width = cameraView.videoWidth; cameraCanvas.height = cameraView.videoHeight; // Draw the current video frame to the canvas const context = cameraCanvas.getContext('2d'); context.drawImage(cameraView, 0, 0, cameraCanvas.width, cameraCanvas.height); // Convert canvas to data URL const imageDataUrl = cameraCanvas.toDataURL('image/jpeg'); // Update the image preview with the captured photo imagePreview.innerHTML = `Player`; // Stop the camera stream and close the camera UI stopCameraStream(); cameraContainer.classList.remove('active'); // Since we're capturing directly, we don't need to use the file input // Instead, store the data URL to use when saving the player playerImage.dataset.capturedImage = imageDataUrl; playerImage.value = ''; // Clear the file input }); // Camera cancel button handler cameraCancelButton.addEventListener('click', () => { stopCameraStream(); cameraContainer.classList.remove('active'); }); // Function to stop the camera stream function stopCameraStream() { if (stream) { stream.getTracks().forEach(track => track.stop()); stream = null; } } // Player form submit playerForm.addEventListener('submit', (e) => { e.preventDefault(); const name = document.getElementById('playerName').value; const timeInMinutes = parseInt(document.getElementById('playerTime').value); const timeInSeconds = timeInMinutes * 60; // Get remaining time if it's visible let remainingTimeValue = timeInSeconds; if (remainingTimeContainer.style.display === 'block') { const remainingTimeString = playerRemainingTime.value; // Validate the time format if (!/^\d{2}:\d{2}$/.test(remainingTimeString)) { alert('Please enter time in MM:SS format (e.g., 05:30)'); return; } remainingTimeValue = parseTimeString(remainingTimeString); } // Check for captured image from camera let playerImageData = null; const capturedImage = playerImage.dataset.capturedImage; // Define the savePlayer function const savePlayer = () => { const isNewPlayer = document.getElementById('modalTitle').textContent === 'Add New Player'; if (isNewPlayer) { // Add new player const newId = Date.now(); players.push({ id: newId, name: name, timeInSeconds: timeInSeconds, remainingTime: timeInSeconds, image: playerImageData }); currentPlayerIndex = players.length - 1; // Play player added sound audioManager.play('playerAdded'); } else { // Update existing player const player = players[currentPlayerIndex]; player.name = name; player.timeInSeconds = timeInSeconds; // Update remaining time based on game state and form input if (gameState === 'setup') { player.remainingTime = timeInSeconds; } else if (gameState === 'paused' || gameState === 'over') { player.remainingTime = remainingTimeValue; } if (playerImageData !== null) { player.image = playerImageData; } // Play player edited sound audioManager.play('playerEdited'); } renderPlayers(); saveData(); playerModal.classList.remove('active'); document.getElementById('playerImage').value = ''; playerImage.dataset.capturedImage = ''; // Clear captured image data // Also play modal close sound audioManager.play('modalClose'); }; // Process image: either from captured image or uploaded file if (capturedImage) { playerImageData = capturedImage; savePlayer(); } else { // Check for uploaded file const imageFile = document.getElementById('playerImage').files[0]; if (imageFile) { const reader = new FileReader(); reader.onload = (event) => { playerImageData = event.target.result; savePlayer(); }; reader.readAsDataURL(imageFile); } else { // Current player's existing image or null if (document.getElementById('modalTitle').textContent !== 'Add New Player') { playerImageData = players[currentPlayerIndex].image; } savePlayer(); } } }); // Cancel button cancelButton.addEventListener('click', () => { audioManager.play('buttonClick'); playerModal.classList.remove('active'); document.getElementById('playerImage').value = ''; cleanupCameraData(); audioManager.play('modalClose'); }); // Delete player button deletePlayerButton.addEventListener('click', () => { audioManager.play('buttonClick'); if (players.length <= 2) { alert('You need at least 2 players. Add another player before deleting this one.'); return; } players.splice(currentPlayerIndex, 1); if (currentPlayerIndex >= players.length) { currentPlayerIndex = players.length - 1; } renderPlayers(); saveData(); playerModal.classList.remove('active'); cleanupCameraData(); // Play player deleted sound audioManager.play('playerDeleted'); // Also play modal close sound audioManager.play('modalClose'); }); // Function to create deep links function createDeepLink(action) { return deepLinkManager.generateDeepLink(action); } // Function to setup deep links function setupDeepLinks() { // Register handlers for each action deepLinkManager.registerHandler('start', () => { if (gameState === 'setup' || gameState === 'paused') { if (players.length < 2) { console.log('Cannot start: Need at least 2 players'); return; } gameState = 'running'; audioManager.play('gameStart'); startTimer(); updateGameButton(); renderPlayers(); saveData(); } }); deepLinkManager.registerHandler('pause', () => { if (gameState === 'running') { gameState = 'paused'; audioManager.play('gamePause'); stopTimer(); updateGameButton(); renderPlayers(); saveData(); } }); deepLinkManager.registerHandler('toggle', () => { // Simply trigger the game button click gameButton.click(); }); deepLinkManager.registerHandler('nextplayer', () => { if (gameState === 'running') { const nextIndex = findNextPlayerWithTimeCircular(currentPlayerIndex, 1); if (nextIndex !== -1 && nextIndex !== currentPlayerIndex) { currentPlayerIndex = nextIndex; audioManager.play('playerSwitch'); renderPlayers(); saveData(); } } }); deepLinkManager.registerHandler('reset', () => { // Show the reset confirmation dialog resetButton.click(); }); // Process deep links on page load deepLinkManager.processDeepLink(); } // Clean up when the modal is closed function cleanupCameraData() { // Clear any captured image data if (playerImage) { playerImage.dataset.capturedImage = ''; } // Make sure camera is stopped stopCameraStream(); // Hide camera UI if visible cameraContainer.classList.remove('active'); } // Make sure to handle rotation by adding window event listener for orientation changes window.addEventListener('orientationchange', () => { // If camera is active, adjust video dimensions if (cameraContainer.classList.contains('active') && stream) { // Give a moment for the orientation to complete setTimeout(() => { // This may cause the video to briefly reset but will ensure proper dimensions cameraView.srcObject = null; cameraView.srcObject = stream; }, 300); } }); // Listen for service worker messages (for receiving actions while app is running) navigator.serviceWorker.addEventListener('message', (event) => { if (event.data && event.data.type === 'ACTION') { console.log('Received action from service worker:', event.data.action); deepLinkManager.handleAction(event.data.action); } }); // Service Worker Registration if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('ServiceWorker registered'); // Request notification permission and subscribe Notification.requestPermission().then(permission => { if (permission === 'granted') { subscribeToPushNotifications(); } }); setupDeepLinks(); }) .catch(error => { console.log('ServiceWorker registration failed:', error); setupDeepLinks(); }); }); } else { // If service workers aren't supported, still handle deep links window.addEventListener('load', setupDeepLinks); } // Also check for hash changes (needed for handling link activation when app is already open) window.addEventListener('hashchange', () => { console.log('Hash changed, checking for actions'); deepLinkManager.processDeepLink(); }); // Also check for navigation events that might include search parameters window.addEventListener('popstate', () => { console.log('Navigation occurred, checking for actions'); deepLinkManager.processDeepLink(); }); // Initialize the app loadData();