retropulse/public/js/overlay.js
2025-09-02 22:27:25 +05:30

243 lines
8.1 KiB
JavaScript

// overlay.js - WebSocket client for real-time achievement updates
class RetroPulseOverlay {
constructor() {
this.ws = null;
this.username = null;
this.reconnectInterval = null;
this.reconnectDelay = 3000; // 3 seconds
this.lastUpdateTime = null;
// DOM elements
this.connectionStatus = document.getElementById('connection-status');
this.currentGame = document.getElementById('current-game');
this.gameIcon = document.getElementById('game-icon');
this.gameTitle = document.getElementById('game-title');
this.consoleName = document.getElementById('console-name');
this.achievementsList = document.getElementById('achievements-list');
this.errorDisplay = document.getElementById('error-display');
this.errorMessage = document.getElementById('error-message');
this.init();
}
init() {
// Extract username from URL path
const pathParts = window.location.pathname.split('/');
this.username = 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');
this.updateConnectionStatus('connected');
// Clear reconnect interval if it exists
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);
this.updateConnectionStatus('disconnected');
// Don't reconnect if it was a clean close
if (event.code !== 1000) {
this.scheduleReconnect();
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.updateConnectionStatus('disconnected');
};
} catch (error) {
console.error('Error creating WebSocket connection:', error);
this.showError(`Connection failed: ${error.message}`);
}
}
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);
// Update last update time
this.lastUpdateTime = new Date();
// Hide error display
this.errorDisplay.classList.add('hidden');
// Update achievements
if (data.recentAchievements && data.recentAchievements.length > 0) {
this.updateAchievements(data.recentAchievements);
// Update game info from the latest achievement
const latestAchievement = data.recentAchievements[0];
if (latestAchievement.gameTitle) {
this.updateGameInfo({
title: latestAchievement.gameTitle,
icon: latestAchievement.gameIcon
});
}
} else {
// No achievements yet
this.achievementsList.innerHTML = '<div class="no-achievements">No recent achievements</div>';
}
}
updateGameInfo(gameData) {
if (!gameData) return;
this.gameTitle.textContent = gameData.title || 'Unknown Game';
if (gameData.icon) {
this.gameIcon.src = gameData.icon;
this.gameIcon.onerror = () => {
this.gameIcon.style.display = 'none';
};
}
this.currentGame.classList.remove('hidden');
}
updateAchievements(achievements) {
// Clear existing achievements
this.achievementsList.innerHTML = '';
achievements.forEach((achievement, index) => {
const achievementElement = this.createAchievementElement(achievement);
// Add a small delay for staggered animation
setTimeout(() => {
this.achievementsList.appendChild(achievementElement);
}, index * 100);
});
}
createAchievementElement(achievement) {
const item = document.createElement('div');
item.className = 'achievement-item';
// Format the date
const dateEarned = achievement.dateEarned ?
new Date(achievement.dateEarned).toLocaleDateString() :
'Recently';
item.innerHTML = `
<img src="${achievement.badgeName || achievement.gameIcon || ''}"
alt="Achievement Badge"
class="achievement-badge"
onerror="this.style.display='none'">
<div class="achievement-details">
<div class="achievement-title">${this.escapeHtml(achievement.title || 'Unknown Achievement')}</div>
<div class="achievement-description">${this.escapeHtml(achievement.description || '')}</div>
<div class="achievement-meta">
<span class="achievement-points">${achievement.points || 0} pts</span>
<span class="achievement-date">${dateEarned}</span>
</div>
</div>
`;
return item;
}
updateConnectionStatus(status) {
this.connectionStatus.className = `connection-status ${status}`;
const statusTexts = {
connecting: 'Connecting...',
connected: 'Live',
disconnected: 'Disconnected'
};
this.connectionStatus.querySelector('.status-text').textContent = statusTexts[status] || status;
}
scheduleReconnect() {
if (this.reconnectInterval) return;
console.log(`⏰ Scheduling reconnect in ${this.reconnectDelay}ms`);
this.reconnectInterval = setTimeout(() => {
console.log('🔄 Attempting to reconnect...');
this.connect();
}, this.reconnectDelay);
// Increase delay for next reconnect (max 30 seconds)
this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, 30000);
}
showError(message) {
console.error('❌ Overlay error:', message);
this.errorMessage.textContent = message;
this.errorDisplay.classList.remove('hidden');
this.updateConnectionStatus('disconnected');
}
// Utility function to escape HTML
escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
}
// Initialize overlay when page loads
document.addEventListener('DOMContentLoaded', () => {
new RetroPulseOverlay();
});
// Handle page visibility for OBS
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
console.log('🔄 Page became visible, overlay active');
}
});