Initial commit
This commit is contained in:
323
js/ui/audio.js
Normal file
323
js/ui/audio.js
Normal file
@@ -0,0 +1,323 @@
|
||||
// Audio Manager using Web Audio API
|
||||
const audioManager = {
|
||||
audioContext: null,
|
||||
muted: false,
|
||||
sounds: {},
|
||||
lowTimeThreshold: 10, // Seconds threshold for low time warning
|
||||
lastTickTime: 0, // Track when we started continuous ticking
|
||||
tickFadeoutTime: 3, // Seconds after which tick sound fades out
|
||||
|
||||
// Initialize the audio context
|
||||
init() {
|
||||
try {
|
||||
// Create AudioContext (with fallback for older browsers)
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
// Check for saved mute preference
|
||||
const savedMute = localStorage.getItem('gameTimerMuted');
|
||||
this.muted = savedMute === 'true';
|
||||
|
||||
// Create all the sounds
|
||||
this.createSounds();
|
||||
|
||||
console.log('Web Audio API initialized successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Web Audio API initialization failed:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Create all the sound generators
|
||||
createSounds() {
|
||||
// Game sounds
|
||||
this.sounds.tick = this.createTickSound();
|
||||
this.sounds.lowTime = this.createLowTimeSound();
|
||||
this.sounds.timeUp = this.createTimeUpSound();
|
||||
this.sounds.gameStart = this.createGameStartSound();
|
||||
this.sounds.gamePause = this.createGamePauseSound();
|
||||
this.sounds.gameResume = this.createGameResumeSound();
|
||||
this.sounds.gameOver = this.createGameOverSound();
|
||||
this.sounds.playerSwitch = this.createPlayerSwitchSound();
|
||||
|
||||
// UI sounds
|
||||
this.sounds.buttonClick = this.createButtonClickSound();
|
||||
this.sounds.modalOpen = this.createModalOpenSound();
|
||||
this.sounds.modalClose = this.createModalCloseSound();
|
||||
this.sounds.playerAdded = this.createPlayerAddedSound();
|
||||
this.sounds.playerEdited = this.createPlayerEditedSound();
|
||||
this.sounds.playerDeleted = this.createPlayerDeletedSound();
|
||||
},
|
||||
|
||||
// Helper function to create an oscillator
|
||||
createOscillator(type, frequency, startTime, duration, gain = 1.0, ramp = false) {
|
||||
if (this.audioContext === null) this.init();
|
||||
|
||||
const oscillator = this.audioContext.createOscillator();
|
||||
const gainNode = this.audioContext.createGain();
|
||||
|
||||
oscillator.type = type;
|
||||
oscillator.frequency.value = frequency;
|
||||
gainNode.gain.value = gain;
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(this.audioContext.destination);
|
||||
|
||||
oscillator.start(startTime);
|
||||
|
||||
if (ramp) {
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + duration);
|
||||
}
|
||||
|
||||
oscillator.stop(startTime + duration);
|
||||
|
||||
return { oscillator, gainNode };
|
||||
},
|
||||
|
||||
// Sound creators
|
||||
createTickSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
const currentTime = Date.now() / 1000;
|
||||
|
||||
// Initialize lastTickTime if it's not set
|
||||
if (this.lastTickTime === 0) {
|
||||
this.lastTickTime = currentTime;
|
||||
}
|
||||
|
||||
// Calculate how long we've been ticking continuously
|
||||
const tickDuration = currentTime - this.lastTickTime;
|
||||
|
||||
// Determine volume based on duration
|
||||
let volume = 0.1; // Default/initial volume
|
||||
|
||||
if (tickDuration <= this.tickFadeoutTime) {
|
||||
// Linear fade from 0.1 to 0 over tickFadeoutTime seconds
|
||||
volume = 0.1 * (1 - (tickDuration / this.tickFadeoutTime));
|
||||
} else {
|
||||
// After tickFadeoutTime, don't play any sound
|
||||
return; // Exit without playing sound
|
||||
}
|
||||
|
||||
// Only play if volume is significant
|
||||
if (volume > 0.001) {
|
||||
this.createOscillator('sine', 800, now, 0.03, volume);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
createLowTimeSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
// Low time warning is always audible
|
||||
this.createOscillator('triangle', 660, now, 0.1, 0.2);
|
||||
// Reset tick fade timer on low time warning
|
||||
this.lastTickTime = 0;
|
||||
};
|
||||
},
|
||||
|
||||
createTimeUpSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
|
||||
// First note
|
||||
this.createOscillator('sawtooth', 440, now, 0.2, 0.3);
|
||||
|
||||
// Second note (lower)
|
||||
this.createOscillator('sawtooth', 220, now + 0.25, 0.3, 0.4);
|
||||
|
||||
// Reset tick fade timer
|
||||
this.lastTickTime = 0;
|
||||
};
|
||||
},
|
||||
|
||||
createGameStartSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
|
||||
// Rising sequence
|
||||
this.createOscillator('sine', 440, now, 0.1, 0.3);
|
||||
this.createOscillator('sine', 554, now + 0.1, 0.1, 0.3);
|
||||
this.createOscillator('sine', 659, now + 0.2, 0.3, 0.3, true);
|
||||
|
||||
// Reset tick fade timer
|
||||
this.lastTickTime = 0;
|
||||
};
|
||||
},
|
||||
|
||||
createGamePauseSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
|
||||
// Two notes pause sound
|
||||
this.createOscillator('sine', 659, now, 0.1, 0.3);
|
||||
this.createOscillator('sine', 523, now + 0.15, 0.2, 0.3, true);
|
||||
|
||||
// Reset tick fade timer
|
||||
this.lastTickTime = 0;
|
||||
};
|
||||
},
|
||||
|
||||
createGameResumeSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
|
||||
// Rising sequence (opposite of pause)
|
||||
this.createOscillator('sine', 523, now, 0.1, 0.3);
|
||||
this.createOscillator('sine', 659, now + 0.15, 0.2, 0.3, true);
|
||||
|
||||
// Reset tick fade timer
|
||||
this.lastTickTime = 0;
|
||||
};
|
||||
},
|
||||
|
||||
createGameOverSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
|
||||
// Fanfare
|
||||
this.createOscillator('square', 440, now, 0.1, 0.3);
|
||||
this.createOscillator('square', 554, now + 0.1, 0.1, 0.3);
|
||||
this.createOscillator('square', 659, now + 0.2, 0.1, 0.3);
|
||||
this.createOscillator('square', 880, now + 0.3, 0.4, 0.3, true);
|
||||
|
||||
// Reset tick fade timer
|
||||
this.lastTickTime = 0;
|
||||
};
|
||||
},
|
||||
|
||||
createPlayerSwitchSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
this.createOscillator('sine', 1200, now, 0.05, 0.2);
|
||||
|
||||
// Reset tick fade timer on player switch
|
||||
this.lastTickTime = 0;
|
||||
};
|
||||
},
|
||||
|
||||
createButtonClickSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
this.createOscillator('sine', 700, now, 0.04, 0.1);
|
||||
};
|
||||
},
|
||||
|
||||
createModalOpenSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
|
||||
// Ascending sound
|
||||
this.createOscillator('sine', 400, now, 0.1, 0.2);
|
||||
this.createOscillator('sine', 600, now + 0.1, 0.1, 0.2);
|
||||
};
|
||||
},
|
||||
|
||||
createModalCloseSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
|
||||
// Descending sound
|
||||
this.createOscillator('sine', 600, now, 0.1, 0.2);
|
||||
this.createOscillator('sine', 400, now + 0.1, 0.1, 0.2);
|
||||
};
|
||||
},
|
||||
|
||||
createPlayerAddedSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
|
||||
// Positive ascending notes
|
||||
this.createOscillator('sine', 440, now, 0.1, 0.2);
|
||||
this.createOscillator('sine', 523, now + 0.1, 0.1, 0.2);
|
||||
this.createOscillator('sine', 659, now + 0.2, 0.2, 0.2, true);
|
||||
};
|
||||
},
|
||||
|
||||
createPlayerEditedSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
|
||||
// Two note confirmation
|
||||
this.createOscillator('sine', 440, now, 0.1, 0.2);
|
||||
this.createOscillator('sine', 523, now + 0.15, 0.15, 0.2);
|
||||
};
|
||||
},
|
||||
|
||||
createPlayerDeletedSound() {
|
||||
return () => {
|
||||
const now = this.audioContext.currentTime;
|
||||
|
||||
// Descending notes
|
||||
this.createOscillator('sine', 659, now, 0.1, 0.2);
|
||||
this.createOscillator('sine', 523, now + 0.1, 0.1, 0.2);
|
||||
this.createOscillator('sine', 392, now + 0.2, 0.2, 0.2, true);
|
||||
};
|
||||
},
|
||||
|
||||
// Play a sound if not muted
|
||||
play(soundName) {
|
||||
if (this.muted || !this.sounds[soundName]) return;
|
||||
|
||||
// Resume audio context if it's suspended (needed for newer browsers)
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
|
||||
this.sounds[soundName]();
|
||||
},
|
||||
|
||||
// Toggle mute state
|
||||
toggleMute() {
|
||||
this.muted = !this.muted;
|
||||
localStorage.setItem('gameTimerMuted', this.muted);
|
||||
return this.muted;
|
||||
},
|
||||
|
||||
// Play timer sounds based on remaining time
|
||||
playTimerSound(remainingSeconds) {
|
||||
if (remainingSeconds <= 0) {
|
||||
// Reset tick fade timer when timer stops
|
||||
this.lastTickTime = 0;
|
||||
return; // Don't play sounds for zero time
|
||||
}
|
||||
|
||||
if (remainingSeconds <= this.lowTimeThreshold) {
|
||||
// Play low time warning sound (this resets the tick fade timer)
|
||||
this.play('lowTime');
|
||||
} else if (remainingSeconds % 1 === 0) {
|
||||
// Normal tick sound on every second
|
||||
this.play('tick');
|
||||
}
|
||||
},
|
||||
|
||||
// Play timer expired sound
|
||||
playTimerExpired() {
|
||||
this.play('timeUp');
|
||||
},
|
||||
|
||||
// Stop all sounds and reset the tick fading
|
||||
stopAllSounds() {
|
||||
// Reset tick fade timer when stopping sounds
|
||||
this.lastTickTime = 0;
|
||||
},
|
||||
|
||||
// Stop timer-specific sounds (tick, low time warning)
|
||||
stopTimerSounds() {
|
||||
// Reset tick fade timer when stopping timer sounds
|
||||
this.lastTickTime = 0;
|
||||
// In this implementation, sounds are so short-lived that
|
||||
// they don't need to be explicitly stopped, just fade prevention is enough
|
||||
},
|
||||
|
||||
// Reset the tick fading (call this when timer is paused or player changes)
|
||||
resetTickFade() {
|
||||
this.lastTickTime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize audio on module load
|
||||
audioManager.init();
|
||||
|
||||
// Export the audio manager
|
||||
export default audioManager;
|
||||
116
js/ui/camera.js
Normal file
116
js/ui/camera.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// camera.js
|
||||
import { CSS_CLASSES } from '../config.js';
|
||||
|
||||
let stream = null;
|
||||
let elements = {}; // To store references to DOM elements passed during init
|
||||
let onCaptureCallback = null; // Callback when image is captured
|
||||
|
||||
export function initCamera(cameraElements, options) {
|
||||
elements = cameraElements; // Store refs like { cameraContainer, cameraView, etc. }
|
||||
onCaptureCallback = options.onCapture;
|
||||
|
||||
// Add internal listeners for capture/cancel buttons
|
||||
elements.cameraCaptureButton?.addEventListener('click', handleCapture);
|
||||
elements.cameraCancelButton?.addEventListener('click', closeCamera);
|
||||
|
||||
// Handle orientation change to potentially reset stream dimensions
|
||||
window.addEventListener('orientationchange', handleOrientationChange);
|
||||
}
|
||||
|
||||
async function openCamera() {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
alert('Camera access not supported or available on this device.');
|
||||
return false; // Indicate failure
|
||||
}
|
||||
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: 'user', // Prefer front camera
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
}
|
||||
});
|
||||
|
||||
elements.cameraContainer?.classList.add(CSS_CLASSES.CAMERA_ACTIVE);
|
||||
if (elements.cameraView) {
|
||||
elements.cameraView.srcObject = stream;
|
||||
// Wait for video metadata to load to get correct dimensions
|
||||
elements.cameraView.onloadedmetadata = () => {
|
||||
elements.cameraView.play(); // Start playing the video stream
|
||||
};
|
||||
}
|
||||
return true; // Indicate success
|
||||
} catch (error) {
|
||||
console.error('Error accessing camera:', error);
|
||||
alert('Could not access camera: ' + error.message);
|
||||
closeCamera(); // Ensure cleanup if opening failed
|
||||
return false; // Indicate failure
|
||||
}
|
||||
}
|
||||
|
||||
function handleCapture() {
|
||||
if (!elements.cameraView || !elements.cameraCanvas || !stream) return;
|
||||
|
||||
// Set canvas dimensions to match video's actual dimensions
|
||||
elements.cameraCanvas.width = elements.cameraView.videoWidth;
|
||||
elements.cameraCanvas.height = elements.cameraView.videoHeight;
|
||||
|
||||
// Draw the current video frame to the canvas
|
||||
const context = elements.cameraCanvas.getContext('2d');
|
||||
// Flip horizontally for front camera to make it mirror-like
|
||||
if (stream.getVideoTracks()[0]?.getSettings()?.facingMode === 'user') {
|
||||
context.translate(elements.cameraCanvas.width, 0);
|
||||
context.scale(-1, 1);
|
||||
}
|
||||
context.drawImage(elements.cameraView, 0, 0, elements.cameraCanvas.width, elements.cameraCanvas.height);
|
||||
|
||||
// Convert canvas to data URL (JPEG format)
|
||||
const imageDataUrl = elements.cameraCanvas.toDataURL('image/jpeg', 0.9); // Quality 0.9
|
||||
|
||||
// Call the callback provided during init with the image data
|
||||
if (onCaptureCallback) {
|
||||
onCaptureCallback(imageDataUrl);
|
||||
}
|
||||
|
||||
// Stop stream and hide UI after capture
|
||||
closeCamera();
|
||||
}
|
||||
|
||||
function stopCameraStream() {
|
||||
if (stream) {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
stream = null;
|
||||
}
|
||||
// Also clear the srcObject
|
||||
if (elements.cameraView) {
|
||||
elements.cameraView.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
function closeCamera() {
|
||||
stopCameraStream();
|
||||
elements.cameraContainer?.classList.remove(CSS_CLASSES.CAMERA_ACTIVE);
|
||||
}
|
||||
|
||||
function handleOrientationChange() {
|
||||
// If camera is active, restart stream to potentially adjust aspect ratio/resolution
|
||||
if (elements.cameraContainer?.classList.contains(CSS_CLASSES.CAMERA_ACTIVE) && stream) {
|
||||
console.log("Orientation changed, re-evaluating camera stream...");
|
||||
// Short delay to allow layout to settle
|
||||
setTimeout(async () => {
|
||||
// Stop existing stream before requesting new one
|
||||
// This might cause a flicker but ensures constraints are re-evaluated
|
||||
stopCameraStream();
|
||||
await openCamera(); // Attempt to reopen with potentially new constraints
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Public API for camera module
|
||||
export default {
|
||||
init: initCamera,
|
||||
open: openCamera,
|
||||
close: closeCamera,
|
||||
stopStream: stopCameraStream // Expose if needed externally, e.g., when modal closes
|
||||
};
|
||||
435
js/ui/pushSettingsUI.js
Normal file
435
js/ui/pushSettingsUI.js
Normal file
@@ -0,0 +1,435 @@
|
||||
// pushSettingsUI.js - UI handling for push notification settings
|
||||
import { setupPushNotifications } from '../services/serviceWorkerManager.js';
|
||||
import { FLIC_BUTTON_ID, getBackendUrl} from '../config.js';
|
||||
|
||||
// --- DOM Elements ---
|
||||
const elements = {
|
||||
pushSettingsButton: null,
|
||||
pushSettingsModal: null,
|
||||
notificationPermissionStatus: null,
|
||||
subscriptionStatus: null,
|
||||
pushUsername: null,
|
||||
pushPassword: null,
|
||||
pushSaveButton: null,
|
||||
pushCancelButton: null,
|
||||
pushUnsubscribeButton: null,
|
||||
pushResubscribeButton: null,
|
||||
// Message monitor elements
|
||||
swMessagesOutput: null,
|
||||
simulateClickButton: null
|
||||
};
|
||||
|
||||
// --- State ---
|
||||
let currentSubscription = null;
|
||||
let messageListener = null;
|
||||
let isMonitoring = false;
|
||||
|
||||
// --- Initialization ---
|
||||
export function initPushSettingsUI() {
|
||||
// Cache DOM elements
|
||||
elements.pushSettingsButton = document.getElementById('pushSettingsButton');
|
||||
elements.pushSettingsModal = document.getElementById('pushSettingsModal');
|
||||
elements.notificationPermissionStatus = document.getElementById('notificationPermissionStatus');
|
||||
elements.subscriptionStatus = document.getElementById('subscriptionStatus');
|
||||
elements.pushUsername = document.getElementById('pushUsername');
|
||||
elements.pushPassword = document.getElementById('pushPassword');
|
||||
elements.pushSaveButton = document.getElementById('pushSaveButton');
|
||||
elements.pushCancelButton = document.getElementById('pushCancelButton');
|
||||
elements.pushUnsubscribeButton = document.getElementById('pushUnsubscribeButton');
|
||||
elements.pushResubscribeButton = document.getElementById('pushResubscribeButton');
|
||||
|
||||
// Message Monitor elements
|
||||
elements.swMessagesOutput = document.getElementById('swMessagesOutput');
|
||||
elements.simulateClickButton = document.getElementById('simulateClickButton');
|
||||
|
||||
// Set up event listeners
|
||||
elements.pushSettingsButton.addEventListener('click', openPushSettingsModal);
|
||||
elements.pushCancelButton.addEventListener('click', closePushSettingsModal);
|
||||
elements.pushSaveButton.addEventListener('click', saveCredentialsAndSubscribe);
|
||||
elements.pushUnsubscribeButton.addEventListener('click', unsubscribeFromPush);
|
||||
elements.pushResubscribeButton.addEventListener('click', resubscribeToPush);
|
||||
|
||||
// Initial status check
|
||||
updateNotificationStatus();
|
||||
}
|
||||
|
||||
// --- UI Functions ---
|
||||
|
||||
// Open the push settings modal and update statuses
|
||||
function openPushSettingsModal() {
|
||||
// Update status displays
|
||||
updateNotificationStatus();
|
||||
updateSubscriptionStatus();
|
||||
|
||||
// Load saved credentials if available
|
||||
loadSavedCredentials();
|
||||
|
||||
// Start monitoring automatically when modal opens
|
||||
startMessageMonitoring();
|
||||
|
||||
// Show the modal
|
||||
elements.pushSettingsModal.classList.add('active');
|
||||
}
|
||||
|
||||
// Close the push settings modal
|
||||
function closePushSettingsModal() {
|
||||
// Stop monitoring when the modal is closed to avoid unnecessary processing
|
||||
stopMessageMonitoring();
|
||||
elements.pushSettingsModal.classList.remove('active');
|
||||
}
|
||||
|
||||
// --- Message Monitor Functions ---
|
||||
|
||||
// Start monitoring service worker messages
|
||||
function startMessageMonitoring() {
|
||||
// If already monitoring, don't set up a new listener
|
||||
if (isMonitoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
elements.swMessagesOutput.textContent = 'Service Worker not supported in this browser.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the output area
|
||||
elements.swMessagesOutput.textContent = 'Monitoring for service worker messages...';
|
||||
|
||||
// Create and register the message listener
|
||||
messageListener = function(event) {
|
||||
const now = new Date().toISOString();
|
||||
const formattedMessage = `[${now}] Message received: \n${JSON.stringify(event.data, null, 2)}\n\n`;
|
||||
elements.swMessagesOutput.textContent += formattedMessage;
|
||||
|
||||
// Auto-scroll to the bottom
|
||||
elements.swMessagesOutput.scrollTop = elements.swMessagesOutput.scrollHeight;
|
||||
};
|
||||
|
||||
// Add the listener
|
||||
navigator.serviceWorker.addEventListener('message', messageListener);
|
||||
isMonitoring = true;
|
||||
}
|
||||
|
||||
// Stop monitoring service worker messages
|
||||
function stopMessageMonitoring() {
|
||||
if (messageListener) {
|
||||
navigator.serviceWorker.removeEventListener('message', messageListener);
|
||||
messageListener = null;
|
||||
isMonitoring = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the notification permission status display
|
||||
function updateNotificationStatus() {
|
||||
if (!('Notification' in window)) {
|
||||
elements.notificationPermissionStatus.textContent = 'Not Supported';
|
||||
elements.notificationPermissionStatus.className = 'status-denied';
|
||||
// Disable subscribe button when notifications are not supported
|
||||
elements.pushResubscribeButton.disabled = true;
|
||||
elements.pushResubscribeButton.classList.add('disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const permission = Notification.permission;
|
||||
elements.notificationPermissionStatus.textContent = permission;
|
||||
|
||||
switch (permission) {
|
||||
case 'granted':
|
||||
elements.notificationPermissionStatus.className = 'status-granted';
|
||||
// Enable subscribe button when permission is granted
|
||||
elements.pushResubscribeButton.disabled = false;
|
||||
elements.pushResubscribeButton.classList.remove('disabled');
|
||||
break;
|
||||
case 'denied':
|
||||
elements.notificationPermissionStatus.className = 'status-denied';
|
||||
// Disable subscribe button when permission is denied
|
||||
elements.pushResubscribeButton.disabled = true;
|
||||
elements.pushResubscribeButton.classList.add('disabled');
|
||||
break;
|
||||
default:
|
||||
elements.notificationPermissionStatus.className = 'status-default';
|
||||
// Enable subscribe button for default state (prompt)
|
||||
elements.pushResubscribeButton.disabled = false;
|
||||
elements.pushResubscribeButton.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
// Update the subscription status display
|
||||
async function updateSubscriptionStatus() {
|
||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
||||
elements.subscriptionStatus.textContent = 'Not Supported';
|
||||
elements.subscriptionStatus.className = 'status-denied';
|
||||
// Disable unsubscribe button when not supported
|
||||
elements.pushUnsubscribeButton.disabled = true;
|
||||
// Set subscribe button text
|
||||
elements.pushResubscribeButton.textContent = 'Subscribe';
|
||||
// Disable simulate button when not supported
|
||||
if (elements.simulateClickButton) {
|
||||
elements.simulateClickButton.disabled = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
currentSubscription = await registration.pushManager.getSubscription();
|
||||
|
||||
if (currentSubscription) {
|
||||
elements.subscriptionStatus.textContent = 'active';
|
||||
elements.subscriptionStatus.className = 'status-active';
|
||||
// Enable unsubscribe button when subscription is active
|
||||
elements.pushUnsubscribeButton.disabled = false;
|
||||
// Change subscribe button text to "Re-subscribe"
|
||||
elements.pushResubscribeButton.textContent = 'Re-subscribe';
|
||||
// Enable simulate button when subscription is active
|
||||
if (elements.simulateClickButton) {
|
||||
elements.simulateClickButton.disabled = false;
|
||||
}
|
||||
} else {
|
||||
elements.subscriptionStatus.textContent = 'Not Subscribed';
|
||||
elements.subscriptionStatus.className = 'status-inactive';
|
||||
// Disable unsubscribe button when not subscribed
|
||||
elements.pushUnsubscribeButton.disabled = true;
|
||||
// Set subscribe button text
|
||||
elements.pushResubscribeButton.textContent = 'Subscribe';
|
||||
// Disable simulate button when not subscribed
|
||||
if (elements.simulateClickButton) {
|
||||
elements.simulateClickButton.disabled = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking subscription status:', error);
|
||||
elements.subscriptionStatus.textContent = 'Error';
|
||||
elements.subscriptionStatus.className = 'status-denied';
|
||||
// Disable unsubscribe button on error
|
||||
elements.pushUnsubscribeButton.disabled = true;
|
||||
// Set subscribe button text
|
||||
elements.pushResubscribeButton.textContent = 'Subscribe';
|
||||
// Disable simulate button on error
|
||||
if (elements.simulateClickButton) {
|
||||
elements.simulateClickButton.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved credentials from localStorage
|
||||
function loadSavedCredentials() {
|
||||
try {
|
||||
const storedAuth = localStorage.getItem('basicAuthCredentials');
|
||||
if (storedAuth) {
|
||||
const credentials = JSON.parse(storedAuth);
|
||||
if (credentials.username && credentials.password) {
|
||||
elements.pushUsername.value = credentials.username;
|
||||
elements.pushPassword.value = credentials.password;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading saved credentials:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Action Functions ---
|
||||
|
||||
// Save credentials and close the modal
|
||||
async function saveCredentialsAndSubscribe() {
|
||||
const username = elements.pushUsername.value.trim();
|
||||
const password = elements.pushPassword.value.trim();
|
||||
|
||||
if (!username || !password) {
|
||||
alert('Please enter both username and password');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save credentials to localStorage
|
||||
const credentials = { username, password };
|
||||
localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials));
|
||||
|
||||
// Close the modal
|
||||
closePushSettingsModal();
|
||||
}
|
||||
|
||||
// Unsubscribe from push notifications
|
||||
async function unsubscribeFromPush() {
|
||||
if (!currentSubscription) {
|
||||
alert('No active subscription to unsubscribe from');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await currentSubscription.unsubscribe();
|
||||
await updateSubscriptionStatus();
|
||||
// No success alert
|
||||
} catch (error) {
|
||||
console.error('Error unsubscribing:', error);
|
||||
alert(`Error unsubscribing: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to push notifications
|
||||
async function resubscribeToPush() {
|
||||
try {
|
||||
let username = elements.pushUsername.value.trim();
|
||||
let password = elements.pushPassword.value.trim();
|
||||
|
||||
// If fields are empty, try to use stored credentials
|
||||
if (!username || !password) {
|
||||
try {
|
||||
const storedAuth = localStorage.getItem('basicAuthCredentials');
|
||||
if (storedAuth) {
|
||||
const credentials = JSON.parse(storedAuth);
|
||||
if (credentials.username && credentials.password) {
|
||||
username = credentials.username;
|
||||
password = credentials.password;
|
||||
|
||||
// Update the form fields with stored values
|
||||
elements.pushUsername.value = username;
|
||||
elements.pushPassword.value = password;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading stored credentials:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have credentials, show alert if missing
|
||||
if (!username || !password) {
|
||||
console.log('No credentials available. Showing alert.');
|
||||
alert('Please enter your username and password to subscribe.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the credentials to localStorage
|
||||
const credentials = { username, password };
|
||||
localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials));
|
||||
console.log('Saved credentials to localStorage');
|
||||
|
||||
// Use the credentials to subscribe
|
||||
await setupPushNotifications();
|
||||
|
||||
// Wait a moment for the subscription to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Get the updated subscription
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
currentSubscription = await registration.pushManager.getSubscription();
|
||||
|
||||
// Update the UI directly
|
||||
if (currentSubscription && elements.subscriptionStatus) {
|
||||
elements.subscriptionStatus.textContent = 'active';
|
||||
elements.subscriptionStatus.className = 'status-active';
|
||||
// Enable unsubscribe button when subscription is active
|
||||
elements.pushUnsubscribeButton.disabled = false;
|
||||
// Change subscribe button text to "Re-subscribe"
|
||||
elements.pushResubscribeButton.textContent = 'Re-subscribe';
|
||||
// Enable simulate button when subscription is active
|
||||
if (elements.simulateClickButton) {
|
||||
elements.simulateClickButton.disabled = false;
|
||||
}
|
||||
} else {
|
||||
// Disable unsubscribe button when not subscribed
|
||||
elements.pushUnsubscribeButton.disabled = true;
|
||||
// Set subscribe button text
|
||||
elements.pushResubscribeButton.textContent = 'Subscribe';
|
||||
// Disable simulate button when not subscribed
|
||||
if (elements.simulateClickButton) {
|
||||
elements.simulateClickButton.disabled = true;
|
||||
}
|
||||
// Fall back to the standard update function
|
||||
await updateSubscriptionStatus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error subscribing:', error);
|
||||
alert(`Error subscribing: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Manually trigger sendSubscriptionToServer with the current subscription
|
||||
export async function sendSubscriptionToServer() {
|
||||
if (!currentSubscription) {
|
||||
await updateSubscriptionStatus();
|
||||
if (!currentSubscription) {
|
||||
// No alert, just return silently
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get stored credentials
|
||||
let credentials;
|
||||
try {
|
||||
const storedAuth = localStorage.getItem('basicAuthCredentials');
|
||||
if (storedAuth) {
|
||||
credentials = JSON.parse(storedAuth);
|
||||
if (!credentials.username || !credentials.password) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
} else {
|
||||
throw new Error('No stored credentials');
|
||||
}
|
||||
} catch (error) {
|
||||
// No alert, just open the modal to let the user set credentials
|
||||
openPushSettingsModal();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Basic Auth header
|
||||
const createBasicAuthHeader = (creds) => {
|
||||
return 'Basic ' + btoa(`${creds.username}:${creds.password}`);
|
||||
};
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': createBasicAuthHeader(credentials)
|
||||
};
|
||||
|
||||
try {
|
||||
// Make the request to the server
|
||||
const response = await fetch(`${getBackendUrl()}/subscribe`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
button_id: FLIC_BUTTON_ID,
|
||||
subscription: currentSubscription
|
||||
}),
|
||||
headers: headers,
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
// No success alert
|
||||
|
||||
// Update the currentSubscription variable
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
currentSubscription = await registration.pushManager.getSubscription();
|
||||
|
||||
// Directly update the subscription status element in the DOM
|
||||
if (currentSubscription && elements.subscriptionStatus) {
|
||||
elements.subscriptionStatus.textContent = 'active';
|
||||
elements.subscriptionStatus.className = 'status-active';
|
||||
// Enable unsubscribe button when subscription is active
|
||||
elements.pushUnsubscribeButton.disabled = false;
|
||||
// Change subscribe button text to "Re-subscribe"
|
||||
elements.pushResubscribeButton.textContent = 'Re-subscribe';
|
||||
// Enable simulate button when subscription is active
|
||||
if (elements.simulateClickButton) {
|
||||
elements.simulateClickButton.disabled = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let errorMsg = `Server error: ${response.status}`;
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
localStorage.removeItem('basicAuthCredentials');
|
||||
errorMsg = 'Authentication failed. Credentials cleared.';
|
||||
} else {
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMsg = errorData.message || errorMsg;
|
||||
} catch (e) { /* use default */ }
|
||||
}
|
||||
// No error alert, just log the error
|
||||
console.error(`Failed to send subscription: ${errorMsg}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// No error alert, just log the error
|
||||
console.error(`Network error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
289
js/ui/ui.js
Normal file
289
js/ui/ui.js
Normal file
@@ -0,0 +1,289 @@
|
||||
// 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();
|
||||
}
|
||||
Reference in New Issue
Block a user