243 lines
8.1 KiB
JavaScript
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 = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
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');
|
|
}
|
|
});
|