class RetroPulseOverlay { constructor() { this.ws = null; this.username = null; this.reconnectInterval = null; this.reconnectDelay = 3000; // 3 seconds this.lastUpdateTime = null; // DOM elements this.errorDisplay = document.getElementById('error-display'); this.errorMessage = document.getElementById('error-message'); this.waitingForActivity = document.getElementById('waiting-for-activity'); // Game elements this.currentGame = document.getElementById('current-game'); this.gameIcon = document.getElementById('game-icon'); this.gameTitle = document.getElementById('game-title'); this.consoleName = document.getElementById('console-name'); // Progress bar elements this.gameProgressSection = document.getElementById('game-progress-section'); this.progressText = document.getElementById('progress-text'); this.progressPercentage = document.getElementById('progress-percentage'); this.progressBar = document.getElementById('progress-bar'); // Achievements elements this.achievementsContainer = document.getElementById('achievements-container'); this.achievementsList = document.getElementById('achievements-list'); this.init(); } init() { // Extract username from URL path const pathParts = window.location.pathname.split('/'); this.username = decodeURIComponent(pathParts[pathParts.length - 1]); if (!this.username || this.username === 'overlay') { this.showError('No username specified in URL'); return; } console.log(`Initializing RetroPulse overlay for user: ${this.username}`); this.connect(); } connect() { try { // Determine WebSocket URL (ws:// for http, wss:// for https) const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws?user=${encodeURIComponent(this.username)}`; console.log(`Connecting to WebSocket: ${wsUrl}`); this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket connected'); if (this.reconnectInterval) { clearInterval(this.reconnectInterval); this.reconnectInterval = null; } }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); this.handleMessage(message); } catch (error) { console.error('Error parsing WebSocket message:', error); } }; this.ws.onclose = (event) => { console.log('WebSocket disconnected:', event.code, event.reason); if (event.code !== 1000) { // Don't reconnect on clean close this.scheduleReconnect(); } }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; } catch (error) { console.error('Error creating WebSocket connection:', error); this.showError(`Connection failed: ${error.message}`); } } scheduleReconnect() { if (this.reconnectInterval) return; // Reconnect already scheduled console.log(`Scheduling reconnect in ${this.reconnectDelay / 1000} seconds...`); this.reconnectInterval = setInterval(() => { console.log('Reconnecting...'); this.connect(); }, this.reconnectDelay); } handleMessage(message) { console.log('Received message:', message); if (message.error) { this.showError(message.error); return; } if (message.type === 'initial' || message.type === 'update') { this.updateOverlay(message.data); } } updateOverlay(data) { if (!data) return; console.log('Updating overlay with data:', data); this.lastUpdateTime = new Date(); this.errorDisplay.classList.add('hidden'); const hasGameProgress = data.gameProgress !== null; const hasAchievements = data.recentAchievements && data.recentAchievements.length > 0; if (hasGameProgress || hasAchievements) { // We have data, so hide the waiting message this.waitingForActivity.classList.add('hidden'); // Update Game Info and Progress Bar if (hasGameProgress) { this.currentGame.classList.remove('hidden'); this.updateGameInfo(data.gameProgress); } else { this.currentGame.classList.add('hidden'); } // Update achievements list if (hasAchievements) { this.achievementsContainer.classList.remove('hidden'); this.updateAchievements(data.recentAchievements); } else { this.achievementsContainer.classList.add('hidden'); } } else { // No data to display, show the waiting message this.waitingForActivity.classList.remove('hidden'); this.currentGame.classList.add('hidden'); this.achievementsContainer.classList.add('hidden'); } } updateGameInfo(gameData) { this.currentGame.classList.remove('hidden'); this.gameTitle.textContent = gameData.title; this.consoleName.textContent = gameData.consoleName || ''; if (gameData.icon) { this.gameIcon.src = gameData.icon; this.gameIcon.style.display = 'block'; } else { this.gameIcon.style.display = 'none'; } // Update progress bar if (gameData.hasOwnProperty('completion') && gameData.totalAchievements > 0) { this.progressText.textContent = `Completion: ${gameData.numAwarded} / ${gameData.totalAchievements}`; this.progressPercentage.textContent = gameData.completion; this.progressBar.style.width = gameData.completion; this.gameProgressSection.classList.remove('hidden'); } else { this.gameProgressSection.classList.add('hidden'); } } updateAchievements(achievements) { // To avoid flickering, only update if content has changed. // A simple check on the first achievement ID can work. const firstExisting = this.achievementsList.querySelector('.achievement-item'); if (firstExisting && firstExisting.dataset.id == achievements[0].id) { console.log('Achievements are already up-to-date.'); return; } console.log('Updating achievements list.'); this.achievementsList.innerHTML = ''; // Clear existing achievements.forEach(ach => { const item = document.createElement('li'); item.className = 'achievement-item'; item.dataset.id = ach.id; // For update checking item.innerHTML = `