From 049866ecd7e70474995e6c337fb724ce59b1b1d6 Mon Sep 17 00:00:00 2001 From: cpu Date: Wed, 7 May 2025 20:38:32 +0200 Subject: [PATCH] reset --- index.html | 86 ------- manifest.json | 24 -- script.js | 649 -------------------------------------------------- style.css | 345 --------------------------- sw.js | 50 ---- 5 files changed, 1154 deletions(-) delete mode 100644 index.html delete mode 100644 manifest.json delete mode 100644 script.js delete mode 100644 style.css delete mode 100644 sw.js diff --git a/index.html b/index.html deleted file mode 100644 index 07d6020..0000000 --- a/index.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - Nexus Timer - - - - - -
-
-
- Current Player Photo -
-
-

Current Player

-
00:00
-
-
- -
-
-

Next Player

-
00:00
-
-
- Next Player Photo -
-
- -
- - - - -
-
- - - - - - \ No newline at end of file diff --git a/manifest.json b/manifest.json deleted file mode 100644 index de526b8..0000000 --- a/manifest.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "Nexus Timer", - "short_name": "NexusTimer", - "description": "Dynamic multi-player timer for games and workshops.", - "start_url": "index.html", - "display": "standalone", - "background_color": "#1E1E2F", - "theme_color": "#2A2A3E", - "orientation": "portrait", - "icons": [ - { - "src": "assets/icon-192x192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "assets/icon-512x512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any maskable" - } - ] -} \ No newline at end of file diff --git a/script.js b/script.js deleted file mode 100644 index ed286aa..0000000 --- a/script.js +++ /dev/null @@ -1,649 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - // DOM Elements - const currentPlayerArea = document.getElementById('current-player-area'); - const currentPlayerNameEl = document.getElementById('current-player-name'); - const currentPlayerTimerEl = document.getElementById('current-player-timer'); - const currentPlayerPhotoEl = document.getElementById('current-player-photo'); - - const nextPlayerArea = document.getElementById('next-player-area'); - const nextPlayerNameEl = document.getElementById('next-player-name'); - const nextPlayerTimerEl = document.getElementById('next-player-timer'); - const nextPlayerPhotoEl = document.getElementById('next-player-photo'); - - const managePlayersBtn = document.getElementById('manage-players-btn'); - const gameModeBtn = document.getElementById('game-mode-btn'); - const resetGameBtn = document.getElementById('reset-game-btn'); - const muteBtn = document.getElementById('mute-btn'); - - const managePlayersModal = document.getElementById('manage-players-modal'); - const closeModalBtn = document.querySelector('.close-modal-btn'); - const playerListEditor = document.getElementById('player-list-editor'); - const addPlayerFormBtn = document.getElementById('add-player-form-btn'); // <<<< ENSURED DEFINITION - const shufflePlayersBtn = document.getElementById('shuffle-players-btn'); - const reversePlayersBtn = document.getElementById('reverse-players-btn'); - const addEditPlayerForm = document.getElementById('add-edit-player-form'); - const playerFormTitle = document.getElementById('player-form-title'); - const editPlayerIdInput = document.getElementById('edit-player-id'); - const playerNameInput = document.getElementById('player-name-input'); - const playerTimeInput = document.getElementById('player-time-input'); - // playerPhotoInput (for URL) is removed, new elements for camera: - const playerPhotoCaptureInput = document.getElementById('player-photo-capture-input'); - const playerPhotoPreviewEl = document.getElementById('player-photo-preview'); - const removePhotoBtn = document.getElementById('remove-photo-btn'); - const playerHotkeyInput = document.getElementById('player-hotkey-input'); - const playerAdminInput = document.getElementById('player-admin-input'); - const savePlayerBtn = document.getElementById('save-player-btn'); - const cancelEditPlayerBtn = document.getElementById('cancel-edit-player-btn'); - const savePlayerManagementBtn = document.getElementById('save-player-management-btn'); - - const appContainer = document.getElementById('app-container'); - - // Web Audio API - let audioContext; - let continuousTickIntervalId = null; - let shortTickIntervalId = null; - let shortTickTimeoutId = null; - - // Game State - let players = []; - let currentPlayerIndex = 0; - let gameMode = 'normal'; - let isMuted = false; - let playerTimers = {}; - let focusedPlayerIndexInAllTimersMode = 0; - let currentPhotoDataUrl = null; // Temp store for new photo in form - - const DEFAULT_PHOTO_URL = 'assets/default-avatar.svg'; - const MAX_NEGATIVE_TIME_SECONDS = -3599; - const TICK_FREQUENCY_HZ = 1200; - const TICK_DURATION_S = 0.05; - const CONTINUOUS_TICK_INTERVAL_MS = 750; - const SHORT_TICK_DURATION_MS = 3000; - - // --- Web Audio API Initialization --- - function initAudio() { - try { - audioContext = new (window.AudioContext || window.webkitAudioContext)(); - } catch (e) { - console.error("Web Audio API is not supported in this browser", e); - isMuted = true; - updateMuteButton(); - } - } - - function resumeAudioContext() { - if (audioContext && audioContext.state === 'suspended') { - audioContext.resume().then(() => { - // console.log("AudioContext resumed successfully"); // Kept for debugging if needed - }).catch(e => console.error("Error resuming AudioContext:", e)); - } - } - - // --- Persistence --- - function saveState() { - const state = { - players: players.map(p => ({ ...p, timerInstance: undefined })), - currentPlayerIndex, - gameMode, - isMuted, - focusedPlayerIndexInAllTimersMode - }; - localStorage.setItem('nexusTimerState', JSON.stringify(state)); - } - - function loadState() { - const savedState = localStorage.getItem('nexusTimerState'); - if (savedState) { - const state = JSON.parse(savedState); - players = state.players.map(p => ({ - ...p, - currentTime: parseInt(p.currentTime, 10), - initialTime: parseInt(p.initialTime, 10), - isSkipped: p.isSkipped || false, - })); - currentPlayerIndex = state.currentPlayerIndex || 0; - gameMode = state.gameMode || 'normal'; - isMuted = state.isMuted || false; - focusedPlayerIndexInAllTimersMode = state.focusedPlayerIndexInAllTimersMode || 0; - if (players.length === 0) setupDefaultPlayers(); - } else { - setupDefaultPlayers(); - } - renderPlayerManagementList(); - updateDisplay(); - updateGameModeUI(); - } - - function setupDefaultPlayers() { - players = [ - { id: Date.now(), name: 'Player 1', initialTime: 3600, currentTime: 3600, photo: DEFAULT_PHOTO_URL, hotkey: 'a', isAdmin: true, isSkipped: false }, - { id: Date.now() + 1, name: 'Player 2', initialTime: 3600, currentTime: 3600, photo: DEFAULT_PHOTO_URL, hotkey: 'b', isAdmin: false, isSkipped: false }, - ]; - currentPlayerIndex = 0; - } - - // --- Time Formatting --- - function formatTime(totalSeconds) { - const isNegative = totalSeconds < 0; - if (isNegative) totalSeconds = -totalSeconds; - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - const sign = isNegative ? '-' : ''; - return `${sign}${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; - } - - function parseTimeInput(timeStr) { - const parts = timeStr.split(':'); - if (parts.length === 2) { - const minutes = parseInt(parts[0], 10); - const seconds = parseInt(parts[1], 10); - if (!isNaN(minutes) && !isNaN(seconds) && seconds < 60 && seconds >= 0 && minutes >= 0) { - return (minutes * 60) + seconds; - } - } - return 3600; - } - - // --- UI Updates --- - function updatePlayerDisplay(player, nameEl, timerEl, photoEl, isSmallTimer = false) { - if (player) { - nameEl.textContent = player.name; - timerEl.textContent = formatTime(player.currentTime); - timerEl.className = `timer-display ${isSmallTimer ? 'small-timer' : ''} ${player.currentTime < 0 ? 'negative' : ''}`; - photoEl.src = player.photo || DEFAULT_PHOTO_URL; // player.photo can be data URL - photoEl.alt = `${player.name}'s Photo`; - const parentArea = nameEl.closest('.player-area'); - if (parentArea) { - parentArea.classList.toggle('player-skipped', !!player.isSkipped); - parentArea.classList.remove('pulsating-background', 'pulsating-text', 'pulsating-negative-text'); - if (playerTimers[player.id] && !player.isSkipped) { - if (player.currentTime >= 0) parentArea.classList.add('pulsating-background'); - else parentArea.classList.add('pulsating-negative-text'); - } - } - } else { - nameEl.textContent = '---'; - timerEl.textContent = '00:00'; - timerEl.className = `timer-display ${isSmallTimer ? 'small-timer' : ''}`; - photoEl.src = DEFAULT_PHOTO_URL; - photoEl.alt = 'Player Photo'; - const parentArea = nameEl.closest('.player-area'); - if (parentArea) parentArea.classList.remove('player-skipped', 'pulsating-background', 'pulsating-text', 'pulsating-negative-text'); - } - } - - function updateDisplay() { - if (players.length === 0) { - updatePlayerDisplay(null, currentPlayerNameEl, currentPlayerTimerEl, currentPlayerPhotoEl); - updatePlayerDisplay(null, nextPlayerNameEl, nextPlayerTimerEl, nextPlayerPhotoEl, true); - return; - } - let currentP, nextP; - if (gameMode === 'allTimersRunning') { - const activePlayers = players.filter(p => !p.isSkipped); - if (activePlayers.length > 0) { - focusedPlayerIndexInAllTimersMode = focusedPlayerIndexInAllTimersMode % activePlayers.length; - currentP = activePlayers[focusedPlayerIndexInAllTimersMode]; - nextP = activePlayers.length > 1 ? activePlayers[(focusedPlayerIndexInAllTimersMode + 1) % activePlayers.length] : null; - } else { - currentP = players[0]; nextP = players.length > 1 ? players[1] : null; - } - } else { - currentP = players[currentPlayerIndex]; - nextP = players.length > 1 ? players[(currentPlayerIndex + 1) % players.length] : null; - } - updatePlayerDisplay(currentP, currentPlayerNameEl, currentPlayerTimerEl, currentPlayerPhotoEl); - updatePlayerDisplay(nextP, nextPlayerNameEl, nextPlayerTimerEl, nextPlayerPhotoEl, true); - saveState(); - } - - function updateGameModeUI() { - if (gameMode === 'allTimersRunning') { - gameModeBtn.textContent = 'Stop All Timers'; - let anyTimerRunning = Object.values(playerTimers).some(id => id !== null); - if (anyTimerRunning) { - appContainer.classList.add('pulsating-background'); - if (!isMuted) playContinuousTick(true); - } else { - gameModeBtn.textContent = 'Start All Timers'; - appContainer.classList.remove('pulsating-background'); - playContinuousTick(false); - } - } else { - gameModeBtn.textContent = 'All Timers Mode'; - appContainer.classList.remove('pulsating-background'); - playContinuousTick(false); - } - } - - // --- Timer Logic --- - function startTimer(playerId) { - const player = players.find(p => p.id === playerId); - if (!player || player.isSkipped || playerTimers[playerId]) return; - if (gameMode === 'normal' && !isMuted) playShortTick(); - playerTimers[playerId] = setInterval(() => { - player.currentTime--; - if (player.currentTime < MAX_NEGATIVE_TIME_SECONDS) { - player.currentTime = MAX_NEGATIVE_TIME_SECONDS; - player.isSkipped = true; - pauseTimer(playerId, false); - if (gameMode === 'normal' && players[currentPlayerIndex].id === playerId) passTurn(); - } - updateDisplay(); - }, 1000); - updateDisplay(); - } - - function pauseTimer(playerId, checkAllTimersModeRevert = true) { - if (playerTimers[playerId]) { - clearInterval(playerTimers[playerId]); - playerTimers[playerId] = null; - } - if (gameMode === 'normal' && players[currentPlayerIndex]?.id === playerId) stopShortTick(); - updateDisplay(); - if (gameMode === 'allTimersRunning' && checkAllTimersModeRevert) { - const allPaused = players.every(p => p.isSkipped || !playerTimers[p.id]); - if (allPaused) switchToNormalMode(); - else updateGameModeUI(); - } - } - - function resetPlayerTimer(player) { - player.currentTime = player.initialTime; - player.isSkipped = false; - if (playerTimers[player.id]) pauseTimer(player.id); - } - - // --- Game Flow & Modes --- - function passTurn() { - if (players.length < 1 || gameMode !== 'normal') return; - - const currentP = players[currentPlayerIndex]; - const currentTimerWasActive = !!playerTimers[currentP.id]; - - pauseTimer(currentP.id); - - let nextIndex = (currentPlayerIndex + 1) % players.length; - let attempts = 0; - while (players[nextIndex].isSkipped && attempts < players.length) { - nextIndex = (nextIndex + 1) % players.length; - attempts++; - } - - if (players[nextIndex].isSkipped && attempts >= players.length) { - currentPlayerIndex = nextIndex; - console.log("All subsequent players are skipped. Turn passed to a skipped player."); - } else { - currentPlayerIndex = nextIndex; - const nextP = players[currentPlayerIndex]; - if (currentTimerWasActive) { - startTimer(nextP.id); - } - } - updateDisplay(); - } - - function switchToNormalMode() { - gameMode = 'normal'; - players.forEach(p => pauseTimer(p.id, false)); - const activePlayers = players.filter(p => !p.isSkipped); - if (activePlayers.length > 0 && focusedPlayerIndexInAllTimersMode < activePlayers.length) { - const focusedPlayer = activePlayers[focusedPlayerIndexInAllTimersMode]; - const newCurrentIndex = players.findIndex(p => p.id === focusedPlayer.id); - if (newCurrentIndex !== -1) currentPlayerIndex = newCurrentIndex; - } else if (activePlayers.length > 0) { - currentPlayerIndex = players.findIndex(p => p.id === activePlayers[0].id); - } - updateGameModeUI(); updateDisplay(); - } - - function switchToAllTimersMode(startTimers = true) { - gameMode = 'allTimersRunning'; - if (startTimers) { - players.forEach(p => { if (!p.isSkipped) startTimer(p.id); }); - const currentActualPlayer = players[currentPlayerIndex]; - const activePlayers = players.filter(p => !p.isSkipped); - const focusIdx = activePlayers.findIndex(p => p.id === currentActualPlayer.id); - focusedPlayerIndexInAllTimersMode = (focusIdx !== -1) ? focusIdx : 0; - } - updateGameModeUI(); updateDisplay(); - } - - function changeFocusInAllTimersMode() { - if (gameMode !== 'allTimersRunning' || players.length === 0) return; - const activePlayers = players.filter(p => !p.isSkipped); - if (activePlayers.length <= 1) return; - focusedPlayerIndexInAllTimersMode = (focusedPlayerIndexInAllTimersMode + 1) % activePlayers.length; - updateDisplay(); - } - - function resetGame() { - if (!confirm("Reset game? All timers will be restored to initial values.")) return; - players.forEach(resetPlayerTimer); - currentPlayerIndex = 0; focusedPlayerIndexInAllTimersMode = 0; - if (gameMode === 'allTimersRunning') switchToNormalMode(); - updateDisplay(); saveState(); - } - - // --- Player Management --- - function renderPlayerManagementList() { - playerListEditor.innerHTML = ''; - if (players.length === 0) { - playerListEditor.innerHTML = '

No players yet. Add some!

'; - } - players.forEach((player, index) => { - const entry = document.createElement('div'); - entry.className = 'player-editor-entry'; - const photoSrc = (player.photo && player.photo.startsWith('data:image')) ? player.photo : DEFAULT_PHOTO_URL; - entry.innerHTML = ` - P - ${index + 1}. ${player.name} (${formatTime(player.initialTime)}) ${player.isAdmin ? '(Admin)' : ''} ${player.hotkey ? `[${player.hotkey}]`: ''} -
- - - ${index > 0 ? `` : ''} - ${index < players.length - 1 ? `` : ''} -
- `; - playerListEditor.appendChild(entry); - }); - document.querySelectorAll('.edit-player-btn').forEach(btn => btn.addEventListener('click', handleEditPlayerForm)); - document.querySelectorAll('.delete-player-btn').forEach(btn => btn.addEventListener('click', handleDeletePlayer)); - document.querySelectorAll('.move-player-up-btn').forEach(btn => btn.addEventListener('click', handleMovePlayerUp)); - document.querySelectorAll('.move-player-down-btn').forEach(btn => btn.addEventListener('click', handleMovePlayerDown)); - } - - function openPlayerForm(playerToEdit = null) { - addEditPlayerForm.style.display = 'block'; - currentPhotoDataUrl = null; - playerPhotoCaptureInput.value = ''; - - if (playerToEdit) { - playerFormTitle.textContent = 'Edit Player'; - editPlayerIdInput.value = playerToEdit.id; - playerNameInput.value = playerToEdit.name; - playerTimeInput.value = formatTime(playerToEdit.initialTime).replace('-', ''); - playerPhotoPreviewEl.src = playerToEdit.photo || DEFAULT_PHOTO_URL; - currentPhotoDataUrl = (playerToEdit.photo && playerToEdit.photo.startsWith('data:image')) ? playerToEdit.photo : null; - playerHotkeyInput.value = playerToEdit.hotkey || ''; - playerAdminInput.checked = playerToEdit.isAdmin || false; - } else { - playerFormTitle.textContent = 'Add Player'; - editPlayerIdInput.value = ''; - playerNameInput.value = ''; - playerTimeInput.value = '60:00'; - playerPhotoPreviewEl.src = DEFAULT_PHOTO_URL; - playerHotkeyInput.value = ''; - playerAdminInput.checked = false; - } - removePhotoBtn.style.display = (playerPhotoPreviewEl.src !== DEFAULT_PHOTO_URL && playerPhotoPreviewEl.src !== '') ? 'block' : 'none'; - playerNameInput.focus(); - } - - playerPhotoCaptureInput.addEventListener('change', (event) => { - const file = event.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - currentPhotoDataUrl = e.target.result; - playerPhotoPreviewEl.src = currentPhotoDataUrl; - removePhotoBtn.style.display = 'block'; - }; - reader.readAsDataURL(file); - } - }); - - removePhotoBtn.addEventListener('click', () => { - currentPhotoDataUrl = null; - playerPhotoPreviewEl.src = DEFAULT_PHOTO_URL; - playerPhotoCaptureInput.value = ''; - removePhotoBtn.style.display = 'none'; - }); - - addPlayerFormBtn.addEventListener('click', () => { // Event listener for addPlayerFormBtn - if (players.length >= 10) { alert("Max 10 players."); return; } - openPlayerForm(); - }); - - cancelEditPlayerBtn.addEventListener('click', () => { - addEditPlayerForm.style.display = 'none'; - }); - - savePlayerBtn.addEventListener('click', () => { - const id = editPlayerIdInput.value; - const name = playerNameInput.value.trim(); - const initialTimeSeconds = parseTimeInput(playerTimeInput.value); - let photoToSave; - if (currentPhotoDataUrl) { - photoToSave = currentPhotoDataUrl; - } else if (id) { - const existingPlayer = players.find(p => p.id === parseInt(id)); - if (playerPhotoPreviewEl.src === DEFAULT_PHOTO_URL && !currentPhotoDataUrl) { - photoToSave = DEFAULT_PHOTO_URL; - } else { - photoToSave = existingPlayer.photo; - } - } else { - photoToSave = DEFAULT_PHOTO_URL; - } - const hotkey = playerHotkeyInput.value.trim().toLowerCase(); - const isAdmin = playerAdminInput.checked; - - if (!name) { alert('Name empty.'); return; } - if (hotkey.length > 1) { alert('Hotkey single char.'); return; } - if (hotkey && players.some(p => p.hotkey === hotkey && p.id !== (id ? parseInt(id) : null))) { alert(`Hotkey '${hotkey}' taken.`); return; } - if (isAdmin) { players.forEach(p => { if (p.id !== (id ? parseInt(id) : null)) p.isAdmin = false; }); } - - if (id) { - const player = players.find(p => p.id === parseInt(id)); - if (player) { - player.name = name; - player.initialTime = initialTimeSeconds; - if (player.initialTime !== initialTimeSeconds && !playerTimers[player.id]) player.currentTime = initialTimeSeconds; - player.photo = photoToSave; - player.hotkey = hotkey; - player.isAdmin = isAdmin; - } - } else { - if (players.length >= 10) { alert("Max 10 players."); return; } - players.push({ id: Date.now(), name, initialTime: initialTimeSeconds, currentTime: initialTimeSeconds, photo: photoToSave, hotkey, isAdmin, isSkipped: false }); - } - addEditPlayerForm.style.display = 'none'; - renderPlayerManagementList(); - updateDisplay(); - saveState(); - }); - - function handleEditPlayerForm(event) { openPlayerForm(players.find(p => p.id === parseInt(event.target.dataset.id))); } - - function handleDeletePlayer(event) { - const playerId = parseInt(event.target.dataset.id); - if (players.length <= 2) { alert("Min 2 players."); return; } - if (confirm("Delete player?")) { - const playerIndex = players.findIndex(p => p.id === playerId); - if (playerIndex > -1) { - if (playerIndex === currentPlayerIndex) { - if (gameMode === 'normal') pauseTimer(players[currentPlayerIndex].id); - } else if (playerIndex < currentPlayerIndex) { - currentPlayerIndex--; - } - - if (playerTimers[playerId]) { pauseTimer(playerId, false); delete playerTimers[playerId]; } - players.splice(playerIndex, 1); - - if (players.length > 0) { - currentPlayerIndex = Math.max(0, Math.min(currentPlayerIndex, players.length - 1)); - } else { - currentPlayerIndex = 0; - } - - focusedPlayerIndexInAllTimersMode = Math.min(focusedPlayerIndexInAllTimersMode, players.filter(p => !p.isSkipped).length -1); - if (focusedPlayerIndexInAllTimersMode < 0) focusedPlayerIndexInAllTimersMode = 0; - renderPlayerManagementList(); updateDisplay(); saveState(); - } - } - } - function handleMovePlayerUp(event) { - const index = parseInt(event.target.dataset.index); - if (index > 0) { - [players[index-1], players[index]] = [players[index], players[index-1]]; - if (currentPlayerIndex === index) currentPlayerIndex = index - 1; else if (currentPlayerIndex === index - 1) currentPlayerIndex = index; - renderPlayerManagementList(); updateDisplay(); saveState(); - } - } - function handleMovePlayerDown(event) { - const index = parseInt(event.target.dataset.index); - if (index < players.length - 1) { - [players[index+1], players[index]] = [players[index], players[index+1]]; - if (currentPlayerIndex === index) currentPlayerIndex = index + 1; else if (currentPlayerIndex === index + 1) currentPlayerIndex = index; - renderPlayerManagementList(); updateDisplay(); saveState(); - } - } - shufflePlayersBtn.addEventListener('click', () => { - for (let i = players.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [players[i], players[j]] = [players[j], players[i]]; } - currentPlayerIndex = 0; focusedPlayerIndexInAllTimersMode = 0; renderPlayerManagementList(); updateDisplay(); saveState(); - }); - reversePlayersBtn.addEventListener('click', () => { - players.reverse(); if (players.length > 0) currentPlayerIndex = (players.length - 1) - currentPlayerIndex; - focusedPlayerIndexInAllTimersMode = 0; renderPlayerManagementList(); updateDisplay(); saveState(); - }); - - // --- Audio (Synthesized Ticks) --- - function playSingleTick() { - if (!audioContext || isMuted) return; - resumeAudioContext(); - const time = audioContext.currentTime; - const osc = audioContext.createOscillator(); - const gain = audioContext.createGain(); - osc.type = 'sine'; - osc.frequency.setValueAtTime(TICK_FREQUENCY_HZ, time); - gain.gain.setValueAtTime(0, time); - gain.gain.linearRampToValueAtTime(0.3, time + TICK_DURATION_S / 2); - gain.gain.linearRampToValueAtTime(0, time + TICK_DURATION_S); - osc.connect(gain); - gain.connect(audioContext.destination); - osc.start(time); - osc.stop(time + TICK_DURATION_S); - } - - function playContinuousTick(play) { - if (!audioContext) return; - resumeAudioContext(); - if (continuousTickIntervalId) { clearInterval(continuousTickIntervalId); continuousTickIntervalId = null; } - if (play && !isMuted) { - playSingleTick(); - continuousTickIntervalId = setInterval(playSingleTick, CONTINUOUS_TICK_INTERVAL_MS); - } - } - - function playShortTick() { - if (!audioContext || isMuted) return; - resumeAudioContext(); - stopShortTick(); - playSingleTick(); - shortTickIntervalId = setInterval(playSingleTick, CONTINUOUS_TICK_INTERVAL_MS); - shortTickTimeoutId = setTimeout(stopShortTick, SHORT_TICK_DURATION_MS); - } - - function stopShortTick() { - if (shortTickIntervalId) { clearInterval(shortTickIntervalId); shortTickIntervalId = null; } - if (shortTickTimeoutId) { clearTimeout(shortTickTimeoutId); shortTickTimeoutId = null; } - } - - function updateMuteButton() { - muteBtn.textContent = isMuted ? '🔊 Unmute' : '🔇 Mute'; - if (isMuted) { - playContinuousTick(false); - stopShortTick(); - } else { - if (gameMode === 'allTimersRunning' && Object.values(playerTimers).some(id => id !== null)) { - playContinuousTick(true); - } - } - } - - // --- Event Listeners --- - function handleUserInteractionForAudio() { - resumeAudioContext(); - } - document.addEventListener('click', handleUserInteractionForAudio, { once: true }); - document.addEventListener('touchstart', handleUserInteractionForAudio, { once: true }); - document.addEventListener('keydown', handleUserInteractionForAudio, { once: true }); - - managePlayersBtn.addEventListener('click', () => { - managePlayersModal.style.display = 'block'; renderPlayerManagementList(); addEditPlayerForm.style.display = 'none'; - }); - closeModalBtn.addEventListener('click', () => managePlayersModal.style.display = 'none'); - savePlayerManagementBtn.addEventListener('click', () => { - if (players.length < 2) { alert("Min 2 players."); return; } - managePlayersModal.style.display = 'none'; updateDisplay(); saveState(); - }); - window.addEventListener('click', (event) => { if (event.target === managePlayersModal) managePlayersModal.style.display = 'none'; }); - gameModeBtn.addEventListener('click', () => { - if (players.length < 2) { alert("Min 2 players."); return; } - if (gameMode === 'normal') switchToAllTimersMode(true); - else { - const anyTimerRunning = Object.values(playerTimers).some(id => id !== null); - if (anyTimerRunning) switchToNormalMode(); else switchToAllTimersMode(true); - } - }); - resetGameBtn.addEventListener('click', resetGame); - muteBtn.addEventListener('click', () => { isMuted = !isMuted; updateMuteButton(); saveState(); }); - currentPlayerArea.addEventListener('click', () => { - if (players.length === 0) return; - if (gameMode === 'normal') { - const currentP = players[currentPlayerIndex]; - if (playerTimers[currentP.id]) pauseTimer(currentP.id); else if (!currentP.isSkipped) startTimer(currentP.id); - } else { - const activePlayers = players.filter(p => !p.isSkipped); - if (activePlayers.length > 0) { - const focusedPlayer = activePlayers[focusedPlayerIndexInAllTimersMode]; - if (playerTimers[focusedPlayer.id]) pauseTimer(focusedPlayer.id); else if (!focusedPlayer.isSkipped) startTimer(focusedPlayer.id); - } - } - }); - let touchStartY = 0; - nextPlayerArea.addEventListener('touchstart', (e) => { if (e.touches.length === 1) touchStartY = e.touches[0].clientY; }, { passive: true }); - nextPlayerArea.addEventListener('touchend', (e) => { - if (players.length === 0 || touchStartY === 0 || e.changedTouches.length === 0) return; - if ((touchStartY - e.changedTouches[0].clientY) > 50) { // Swipe Up - if (gameMode === 'normal') passTurn(); else changeFocusInAllTimersMode(); - } - touchStartY = 0; - }); - document.addEventListener('keydown', (event) => { - const key = event.key.toLowerCase(); - if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) return; - const triggeredPlayer = players.find(p => p.hotkey === key); - if (triggeredPlayer) { - event.preventDefault(); - if (gameMode === 'normal') { if (players[currentPlayerIndex].id === triggeredPlayer.id) passTurn(); } - else { if (playerTimers[triggeredPlayer.id]) pauseTimer(triggeredPlayer.id); else if (!triggeredPlayer.isSkipped) startTimer(triggeredPlayer.id); } - return; - } - const adminPlayer = players.find(p => p.isAdmin); - if (adminPlayer && key === 's') { - event.preventDefault(); - if (gameMode === 'normal') { - const currentP = players[currentPlayerIndex]; - if (playerTimers[currentP.id]) pauseTimer(currentP.id); else if (!currentP.isSkipped) startTimer(currentP.id); - } else { - const anyTimerRunning = Object.values(playerTimers).some(id => id !== null); - if (anyTimerRunning) switchToNormalMode(); else switchToAllTimersMode(true); - } - } - }); - - // --- Initialization --- - function init() { - initAudio(); - loadState(); - updateMuteButton(); - if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('sw.js') - .then(reg => console.log('SW registered:', reg)) - .catch(err => console.error('SW registration failed:', err)); - } - } - init(); -}); \ No newline at end of file diff --git a/style.css b/style.css deleted file mode 100644 index 0e3fff4..0000000 --- a/style.css +++ /dev/null @@ -1,345 +0,0 @@ -body, html { - margin: 0; - padding: 0; - width: 100%; - height: 100%; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - overflow: hidden; - background-color: #1E1E2F; /* Dark background for the app */ - color: #E0E0E0; - -webkit-tap-highlight-color: transparent; /* Disable tap highlight */ -} - -#app-container { - display: flex; - flex-direction: column; - height: 100vh; /* Full viewport height */ - width: 100vw; /* Full viewport width */ -} - -.player-area { - flex: 1; - display: flex; - align-items: center; - justify-content: space-around; /* Distribute photo and info */ - padding: 20px; - box-sizing: border-box; - text-align: center; - position: relative; /* For potential absolute positioned elements inside */ -} - -#current-player-area { - background-color: #2A2A3E; /* Slightly lighter dark shade for current player */ - border-bottom: 2px solid #4A4A5E; -} - -#next-player-area { - background-color: #242434; /* Slightly different shade for next player */ - cursor: pointer; /* For swipe up indication */ -} - -.player-photo-container { - flex-shrink: 0; -} - -.player-photo { - width: 100px; - height: 100px; - border-radius: 50%; - object-fit: cover; - border: 3px solid #E0E0E0; - background-color: #555; /* Placeholder if image fails */ -} - -#current-player-area .player-photo { - width: 120px; - height: 120px; -} - -.player-info { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -#current-player-name { - font-size: 2.5em; - margin: 10px 0; - font-weight: bold; -} - -#next-player-name { - font-size: 1.8em; - margin: 5px 0; -} - -.timer-display { - font-size: 3.5em; - font-weight: bold; - font-family: 'Courier New', Courier, monospace; - letter-spacing: 2px; -} - -.small-timer { - font-size: 2em; -} - -.timer-display.negative { - color: #FF6B6B; /* Red for negative time */ -} - -.pulsating-background { - animation: pulse-bg 1.5s infinite ease-in-out; -} - -@keyframes pulse-bg { - 0% { background-color: #2A2A3E; } - 50% { background-color: #3A3A4E; } - 100% { background-color: #2A2A3E; } -} - -.pulsating-text { - animation: pulse-text 1.5s infinite ease-in-out; -} - -@keyframes pulse-text { - 0% { opacity: 1; } - 50% { opacity: 0.6; } - 100% { opacity: 1; } -} - -.pulsating-negative-text .timer-display.negative { - animation: pulse-negative-text 1s infinite ease-in-out; -} -@keyframes pulse-negative-text { - 0% { color: #FF6B6B; transform: scale(1); } - 50% { color: #FF4040; transform: scale(1.05); } - 100% { color: #FF6B6B; transform: scale(1); } -} - - -.player-skipped { - opacity: 0.4; - background-color: #333 !important; /* Distinctly greyed out */ -} - -.player-skipped .player-name, .player-skipped .timer-display { - color: #888 !important; -} - - -#controls { - display: flex; - justify-content: space-around; - padding: 10px 0; - background-color: #1A1A2A; /* Darker bar for controls */ - position: fixed; - bottom: 0; - left: 0; - width: 100%; - box-shadow: 0 -2px 5px rgba(0,0,0,0.3); -} - -.control-button { - padding: 10px 15px; - font-size: 0.9em; - background-color: #4A90E2; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.2s; -} - -.control-button:hover { - background-color: #357ABD; -} -.control-button:active { - transform: scale(0.98); -} - - -/* Modal Styles */ -.modal { - display: none; /* Hidden by default */ - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0,0,0,0.6); - color: #333; /* Text color inside modal */ -} - -.modal-content { - background-color: #fefefe; - margin: 5% auto; - padding: 20px; - border: 1px solid #888; - width: 90%; - max-width: 500px; - border-radius: 8px; - box-shadow: 0 4px 8px rgba(0,0,0,0.2); - max-height: 90vh; - display: flex; - flex-direction: column; -} - -.modal-content h2, .modal-content h3 { - color: #333; - text-align: center; -} - -.close-modal-btn { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; - align-self: flex-end; -} - -.close-modal-btn:hover, -.close-modal-btn:focus { - color: black; - text-decoration: none; - cursor: pointer; -} - -#player-list-editor { - margin-bottom: 20px; - max-height: 30vh; /* Limit height and make scrollable */ - overflow-y: auto; - border: 1px solid #ccc; - padding: 10px; -} - -.player-editor-entry { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - border-bottom: 1px solid #eee; -} -.player-editor-entry:last-child { - border-bottom: none; -} - -.player-editor-entry span { - flex-grow: 1; -} - -.player-editor-entry button { - margin-left: 5px; - padding: 5px 8px; - font-size: 0.8em; -} - -#add-edit-player-form label { - display: block; - margin-top: 10px; - font-weight: bold; - color: #555; -} - -#add-edit-player-form input[type="text"], -#add-edit-player-form input[type="file"], -#add-edit-player-form input[type="checkbox"] { - width: calc(100% - 22px); - padding: 10px; - margin-top: 5px; - border: 1px solid #ccc; - border-radius: 4px; - box-sizing: border-box; -} -#add-edit-player-form input[type="checkbox"] { - width: auto; - margin-right: 5px; -} -#add-edit-player-form input[type="file"] { - padding: 3px; /* Less padding for file input appearance */ -} - -.photo-preview-modal { - display: block; - max-width: 100px; - max-height: 100px; - margin: 10px auto; - border: 1px solid #ddd; - border-radius: 4px; - object-fit: cover; -} - -#remove-photo-btn { - display: block; - margin: 5px auto 10px auto; - padding: 6px 10px; - font-size: 0.8em; - background-color: #e07070; - color: white; - border: none; - border-radius: 3px; -} - - -.player-form-buttons, .player-form-actions { - margin-top: 15px; - display: flex; - justify-content: space-around; -} -.player-form-actions button { - padding: 10px 15px; -} - -.modal-main-action { - margin-top: 20px; - padding: 12px 20px; - background-color: #4CAF50; - color: white; - border: none; - border-radius: 5px; - font-size: 1.1em; - cursor: pointer; -} -.modal-main-action:hover { - background-color: #45a049; -} - -/* Responsive adjustments */ -@media (max-width: 600px) { - #current-player-name { - font-size: 2em; - } - #next-player-name { - font-size: 1.5em; - } - .timer-display { - font-size: 2.8em; - } - .small-timer { - font-size: 1.8em; - } - .player-photo { - width: 80px; - height: 80px; - } - #current-player-area .player-photo { - width: 100px; - height: 100px; - } - .control-button { - font-size: 0.8em; - padding: 8px 10px; - } - .modal-content { - margin: 2% auto; - width: 95%; - max-height: 95vh; - } -} - -body { - padding-bottom: 60px; -} \ No newline at end of file diff --git a/sw.js b/sw.js deleted file mode 100644 index b5ae236..0000000 --- a/sw.js +++ /dev/null @@ -1,50 +0,0 @@ -const CACHE_NAME = 'nexus-timer-cache-v3'; // Increment cache version -const URLS_TO_CACHE = [ - 'index.html', - 'style.css', - 'script.js', - 'manifest.json', - 'assets/default-avatar.svg', - // MP3 files removed as they are no longer used - 'assets/icon-192x192.png', - 'assets/icon-512x512.png' -]; - -self.addEventListener('install', event => { - event.waitUntil( - caches.open(CACHE_NAME) - .then(cache => { - console.log('Opened cache'); - return cache.addAll(URLS_TO_CACHE); - }) - ); -}); - -self.addEventListener('fetch', event => { - event.respondWith( - caches.match(event.request) - .then(response => { - if (response) { - return response; - } - return fetch(event.request); - } - ) - ); -}); - -self.addEventListener('activate', event => { - const cacheWhitelist = [CACHE_NAME]; - event.waitUntil( - caches.keys().then(cacheNames => { - return Promise.all( - cacheNames.map(cacheName => { - if (cacheWhitelist.indexOf(cacheName) === -1) { - console.log('Deleting old cache:', cacheName); - return caches.delete(cacheName); - } - }) - ); - }) - ); -}); \ No newline at end of file