added percentage bar

This commit is contained in:
Anuj K 2025-09-03 00:11:14 +05:30
parent 44b935e044
commit ca5a55a612
4 changed files with 433 additions and 337 deletions

View file

@ -3,216 +3,254 @@
'use strict'; 'use strict';
const { const {
buildAuthorization, buildAuthorization,
getUserProfile, getUserProfile,
getUserRecentAchievements, getUserRecentAchievements,
getGame getGame,
getGameInfoAndUserProgress
} = require('@retroachievements/api'); } = require('@retroachievements/api');
class RetroClient { class RetroClient {
constructor() {
const RA_USER = process.env.RA_USER;
const RA_KEY = process.env.RA_KEY;
if (!RA_USER || !RA_KEY) { constructor() {
throw new Error('RetroAchievements username and API key must be set in .env file'); const RA_USER = process.env.RA_USER;
const RA_KEY = process.env.RA_KEY;
if (!RA_USER || !RA_KEY) {
throw new Error('RetroAchievements username and API key must be set in .env file');
}
this.authorization = buildAuthorization({
username: RA_USER,
webApiKey: RA_KEY
});
console.log('🎮 RetroAchievements API client initialized');
} }
this.authorization = buildAuthorization({ // Helper method to build correct image URLs
username: RA_USER, buildImageUrl(imagePath, baseUrl) {
webApiKey: RA_KEY if (!imagePath) return null;
});
console.log('🎮 RetroAchievements API client initialized'); const toStr = (v) => (typeof v === 'string' ? v : String(v));
}
// Helper method to build correct image URLs // If full URL provided, return as-is
buildImageUrl(imagePath, baseUrl) { if (/^https?:\/\//i.test(imagePath)) {
if (!imagePath) return null; return imagePath;
}
const toStr = (v) => (typeof v === 'string' ? v : String(v)); const pathStr = toStr(imagePath);
// Handle full server-relative paths that start with /
if (pathStr.startsWith('/')) {
// Ensure .png for badges
if (pathStr.startsWith('/Badge/')) {
const name = pathStr.split('/').pop().replace(/\.(png|jpe?g|gif|webp)$/i, '');
return `https://media.retroachievements.org/Badge/${name}.png`;
}
return `https://media.retroachievements.org${pathStr}`;
}
// If full URL provided, return as-is // Normalize base URL (strip trailing slashes)
if (/^https?:\/\//i.test(imagePath)) { const normalizedBase = toStr(baseUrl || '').replace(/\/+$/, '');
return imagePath;
// Special handling for badges - always ensure .png extension
const isBadgeContext = /\/Badge(\/|$)/i.test(normalizedBase);
if (isBadgeContext) {
const cleanImagePath = pathStr.replace(/\.(png|jpe?g|gif|webp)$/i, '');
return `${normalizedBase}/${cleanImagePath}.png`;
}
// For other images (UserPic, Images), build normally
return `${normalizedBase}/${pathStr}`;
} }
const pathStr = toStr(imagePath); async getUserProfile(username) {
// Handle full server-relative paths that start with /
if (pathStr.startsWith('/')) {
// Ensure .png for badges
if (pathStr.startsWith('/Badge/')) {
const name = pathStr.split('/').pop().replace(/\.(png|jpe?g|gif|webp)$/i, '');
return `https://media.retroachievements.org/Badge/${name}.png`;
}
return `https://media.retroachievements.org${pathStr}`;
}
// Normalize base URL (strip trailing slashes)
const normalizedBase = toStr(baseUrl || '').replace(/\/+$/, '');
// Special handling for badges - always ensure .png extension
const isBadgeContext = /\/Badge(\/|$)/i.test(normalizedBase);
if (isBadgeContext) {
const cleanImagePath = pathStr.replace(/\.(png|jpe?g|gif|webp)$/i, '');
return `${normalizedBase}/${cleanImagePath}.png`;
}
// For other images (UserPic, Images), build normally
return `${normalizedBase}/${pathStr}`;
}
async getUserProfile(username) {
try {
console.log(`📊 Fetching profile for user: ${username}`);
const profile = await getUserProfile(this.authorization, { username });
return profile;
} catch (error) {
console.error(`❌ Error fetching user profile for ${username}:`, error.message);
throw new Error(`Failed to fetch user profile: ${error.message}`);
}
}
async getUserRecentAchievements(username, count = 10) {
try {
console.log(`🏆 Fetching recent achievements for user: ${username}`);
const achievements = await getUserRecentAchievements(this.authorization, {
username,
count
});
return achievements;
} catch (error) {
console.error(`❌ Error fetching recent achievements for ${username}:`, error.message);
throw new Error(`Failed to fetch recent achievements: ${error.message}`);
}
}
async getGameInfo(gameId) {
try {
console.log(`🎮 Fetching game info for ID: ${gameId}`);
const game = await getGame(this.authorization, { gameId });
return game;
} catch (error) {
console.error(`❌ Error fetching game info for ID ${gameId}:`, error.message);
throw new Error(`Failed to fetch game info: ${error.message}`);
}
}
async getUserData(username) {
try {
const profile = await this.getUserProfile(username);
let lastGame = null;
let lastAchievement = null;
if (profile.lastGameId) {
try { try {
lastGame = await this.getGameInfo(profile.lastGameId); console.log(`📊 Fetching profile for user: ${username}`);
} catch (gameError) { const profile = await getUserProfile(this.authorization, {
console.warn(`⚠️ Could not fetch last game info: ${gameError.message}`); username
});
return profile;
} catch (error) {
console.error(`❌ Error fetching user profile for ${username}:`, error.message);
throw new Error(`Failed to fetch user profile: ${error.message}`);
} }
} }
try { async getUserRecentAchievements(username, count = 10) {
const recentAchievements = await this.getUserRecentAchievements(username, 1); try {
if (recentAchievements && recentAchievements.length > 0) { console.log(`🏆 Fetching recent achievements for user: ${username}`);
lastAchievement = recentAchievements; const achievements = await getUserRecentAchievements(this.authorization, {
username,
count
});
return achievements;
} catch (error) {
console.error(`❌ Error fetching recent achievements for ${username}:`, error.message);
throw new Error(`Failed to fetch recent achievements: ${error.message}`);
} }
} catch (achievementError) {
console.warn(`⚠️ Could not fetch recent achievements: ${achievementError.message}`);
}
return {
user: {
username: profile.user,
displayName: profile.user,
avatar: this.buildImageUrl(
profile.userPic,
'https://media.retroachievements.org/UserPic'
),
motto: profile.motto || '',
totalPoints: profile.totalPoints || 0,
totalTruePoints: profile.totalTruePoints || 0,
memberSince: profile.memberSince || null,
rank: profile.rank || null
},
lastGame: lastGame
? {
id: lastGame.gameId,
title: lastGame.title,
icon: this.buildImageUrl(
lastGame.gameIcon,
'https://media.retroachievements.org/Images'
),
consoleName: lastGame.consoleName
}
: null,
lastAchievement: lastAchievement
? {
id: lastAchievement.achievementId,
title: lastAchievement.title,
description: lastAchievement.description,
points: lastAchievement.points,
trueRatio: lastAchievement.trueRatio,
gameTitle: lastAchievement.gameTitle,
gameIcon: this.buildImageUrl(
lastAchievement.gameIcon,
'https://media.retroachievements.org/Images'
),
badgeName: this.buildImageUrl(
lastAchievement.badgeName,
'https://media.retroachievements.org/Badge'
),
dateEarned: lastAchievement.dateEarned
}
: null
};
} catch (error) {
console.error(`❌ Error in getUserData for ${username}:`, error.message);
throw error;
} }
}
async getOverlayData(username, count = 5) { async getGameInfo(gameId) {
try { try {
const recentAchievements = await this.getUserRecentAchievements(username, count); console.log(`🎮 Fetching game info for ID: ${gameId}`);
const game = await getGame(this.authorization, {
const formattedAchievements = recentAchievements.map((achievement) => { gameId
// Debug log to see what we're getting from the API });
console.log( return game;
`🔍 Processing badge for achievement "${achievement.title}": badgeName="${achievement.badgeName}"` } catch (error) {
); console.error(`❌ Error fetching game info for ID ${gameId}:`, error.message);
throw new Error(`Failed to fetch game info: ${error.message}`);
return { }
id: achievement.achievementId, }
title: achievement.title,
description: achievement.description, async getUserData(username) {
points: achievement.points, try {
trueRatio: achievement.trueRatio || achievement.points, const profile = await this.getUserProfile(username);
gameTitle: achievement.gameTitle, let lastGame = null;
gameIcon: this.buildImageUrl( let lastAchievement = null;
achievement.gameIcon,
'https://media.retroachievements.org/Images' if (profile.lastGameId) {
), try {
badgeName: this.buildImageUrl( lastGame = await this.getGameInfo(profile.lastGameId);
achievement.badgeName, } catch (gameError) {
'https://media.retroachievements.org/Badge' console.warn(`⚠️ Could not fetch last game info: ${gameError.message}`);
), }
dateEarned: achievement.dateEarned }
};
}); try {
const recentAchievements = await this.getUserRecentAchievements(username, 1);
return { if (recentAchievements && recentAchievements.length > 0) {
username, lastAchievement = recentAchievements[0];
recentAchievements: formattedAchievements, }
lastUpdated: new Date().toISOString() } catch (achievementError) {
}; console.warn(`⚠️ Could not fetch recent achievements: ${achievementError.message}`);
} catch (error) { }
console.error(`❌ Error in getOverlayData for ${username}:`, error.message);
throw error; return {
user: {
username: profile.user,
displayName: profile.user,
avatar: this.buildImageUrl(
profile.userPic,
'https://media.retroachievements.org/UserPic'
),
motto: profile.motto || '',
totalPoints: profile.totalPoints || 0,
totalTruePoints: profile.totalTruePoints || 0,
memberSince: profile.memberSince || null,
rank: profile.rank || null
},
lastGame: lastGame ?
{
id: lastGame.gameId,
title: lastGame.title,
icon: this.buildImageUrl(
lastGame.gameIcon,
'https://media.retroachievements.org/Images'
),
consoleName: lastGame.consoleName
} :
null,
lastAchievement: lastAchievement ?
{
id: lastAchievement.achievementId,
title: lastAchievement.title,
description: lastAchievement.description,
points: lastAchievement.points,
trueRatio: lastAchievement.trueRatio,
gameTitle: lastAchievement.gameTitle,
gameIcon: this.buildImageUrl(
lastAchievement.gameIcon,
'https://media.retroachievements.org/Images'
),
badgeName: this.buildImageUrl(
lastAchievement.badgeName,
'https://media.retroachievements.org/Badge'
),
dateEarned: lastAchievement.dateEarned
} :
null
};
} catch (error) {
console.error(`❌ Error in getUserData for ${username}:`, error.message);
throw error;
}
}
async getOverlayData(username, count = 5) {
try {
const recentAchievements = await this.getUserRecentAchievements(username, count);
let gameProgress = null;
let gameId = null;
// First, try to get gameId from the most recent achievement. This is the most "live" data.
if (recentAchievements && recentAchievements.length > 0) {
gameId = recentAchievements[0].gameId;
} else {
// If no recent achievements, fall back to the user's profile to find their last played game.
console.log(`🤔 No recent achievements for ${username}. Checking profile for last game.`);
try {
const profile = await this.getUserProfile(username);
if (profile.lastGameId) {
gameId = profile.lastGameId;
}
} catch (profileError) {
console.warn(`⚠️ Could not fetch profile to find last game: ${profileError.message}`);
}
}
// If we found a gameId from either method, fetch its progress.
if (gameId) {
try {
const progressData = await getGameInfoAndUserProgress(this.authorization, {
username,
gameId
});
gameProgress = {
title: progressData.title,
icon: this.buildImageUrl(progressData.imageIcon, 'https://media.retroachievements.org/Images'),
consoleName: progressData.consoleName,
numAwarded: progressData.numAwardedToUser,
totalAchievements: progressData.numAchievements,
completion: progressData.userCompletion,
};
} catch (progressError) {
console.warn(`⚠️ Could not fetch game progress for gameId ${gameId}: ${progressError.message}`);
}
}
const formattedAchievements = recentAchievements.map((achievement) => {
return {
id: achievement.achievementId,
title: achievement.title,
description: achievement.description,
points: achievement.points,
trueRatio: achievement.trueRatio || achievement.points,
gameTitle: achievement.gameTitle,
gameIcon: this.buildImageUrl(
achievement.gameIcon,
'https://media.retroachievements.org/Images'
),
badgeName: this.buildImageUrl(
achievement.badgeName,
'https://media.retroachievements.org/Badge'
),
dateEarned: achievement.dateEarned
};
});
return {
username,
gameProgress,
recentAchievements: formattedAchievements,
lastUpdated: new Date().toISOString()
};
} catch (error) {
console.error(`❌ Error in getOverlayData for ${username}:`, error.message);
throw error;
}
} }
}
} }
module.exports = new RetroClient(); module.exports = new RetroClient();

View file

@ -57,6 +57,34 @@ body {
100% { opacity: 1; } 100% { opacity: 1; }
} }
/* Waiting for Activity Display */
.waiting-display {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(30, 60, 114, 0.9);
padding: 40px;
border-radius: 12px;
text-align: center;
border: 2px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
width: 80%;
max-width: 450px;
}
.waiting-display h2 {
margin-bottom: 15px;
font-size: 1.5em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
}
.waiting-display p {
opacity: 0.8;
line-height: 1.5;
}
/* Current Game */ /* Current Game */
.current-game { .current-game {
background: linear-gradient(135deg, rgba(30, 60, 114, 0.9), rgba(42, 82, 152, 0.9)); background: linear-gradient(135deg, rgba(30, 60, 114, 0.9), rgba(42, 82, 152, 0.9));
@ -92,6 +120,39 @@ body {
font-size: 1.1em; font-size: 1.1em;
} }
/* Game Progress Bar */
.game-progress {
margin-top: 15px;
}
.progress-stats {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1em;
margin-bottom: 8px;
opacity: 0.9;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
}
.progress-bar-container {
background: rgba(0, 0, 0, 0.3);
border-radius: 20px;
height: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4);
}
.progress-bar-fill {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #1e90ff, #00bfff);
border-radius: 20px;
transition: width 0.5s ease-in-out;
}
/* Achievements Container */ /* Achievements Container */
.achievements-container { .achievements-container {
flex: 1; flex: 1;
@ -117,26 +178,24 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
max-height: 400px; max-height: 400px; /* Adjust as needed */
overflow-y: auto; overflow-y: auto;
padding-right: 10px; padding-right: 10px;
list-style-type: none;
} }
/* Custom Scrollbar */ /* Custom Scrollbar */
.achievements-list::-webkit-scrollbar { .achievements-list::-webkit-scrollbar {
width: 6px; width: 6px;
} }
.achievements-list::-webkit-scrollbar-track { .achievements-list::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
border-radius: 3px; border-radius: 3px;
} }
.achievements-list::-webkit-scrollbar-thumb { .achievements-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
border-radius: 3px; border-radius: 3px;
} }
.achievements-list::-webkit-scrollbar-thumb:hover { .achievements-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.5);
} }
@ -245,12 +304,10 @@ body {
margin-bottom: 10px; margin-bottom: 10px;
font-size: 1.3em; font-size: 1.3em;
} }
.error-content p { .error-content p {
margin-bottom: 20px; margin-bottom: 20px;
opacity: 0.9; opacity: 0.9;
} }
.error-content button { .error-content button {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
color: white; color: white;
@ -260,7 +317,6 @@ body {
cursor: pointer; cursor: pointer;
transition: background 0.3s; transition: background 0.3s;
} }
.error-content button:hover { .error-content button:hover {
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
} }
@ -275,12 +331,10 @@ body {
.overlay-container { .overlay-container {
padding: 10px; padding: 10px;
} }
.game-header { .game-header {
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
} }
.achievement-item { .achievement-item {
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;

View file

@ -1,5 +1,4 @@
// overlay.js - WebSocket client for real-time achievement updates // overlay.js - WebSocket client for real-time achievement updates
class RetroPulseOverlay { class RetroPulseOverlay {
constructor() { constructor() {
this.ws = null; this.ws = null;
@ -10,13 +9,26 @@ class RetroPulseOverlay {
// DOM elements // DOM elements
this.connectionStatus = document.getElementById('connection-status'); this.connectionStatus = document.getElementById('connection-status');
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.currentGame = document.getElementById('current-game');
this.gameIcon = document.getElementById('game-icon'); this.gameIcon = document.getElementById('game-icon');
this.gameTitle = document.getElementById('game-title'); this.gameTitle = document.getElementById('game-title');
this.consoleName = document.getElementById('console-name'); 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.achievementsList = document.getElementById('achievements-list');
this.errorDisplay = document.getElementById('error-display');
this.errorMessage = document.getElementById('error-message');
this.init(); this.init();
} }
@ -24,7 +36,7 @@ class RetroPulseOverlay {
init() { init() {
// Extract username from URL path // Extract username from URL path
const pathParts = window.location.pathname.split('/'); const pathParts = window.location.pathname.split('/');
this.username = pathParts[pathParts.length - 1]; this.username = decodeURIComponent(pathParts[pathParts.length - 1]);
if (!this.username || this.username === 'overlay') { if (!this.username || this.username === 'overlay') {
this.showError('No username specified in URL'); this.showError('No username specified in URL');
@ -42,13 +54,12 @@ class RetroPulseOverlay {
const wsUrl = `${protocol}//${window.location.host}/ws?user=${encodeURIComponent(this.username)}`; const wsUrl = `${protocol}//${window.location.host}/ws?user=${encodeURIComponent(this.username)}`;
console.log(`🔌 Connecting to WebSocket: ${wsUrl}`); console.log(`🔌 Connecting to WebSocket: ${wsUrl}`);
this.updateConnectionStatus('connecting');
this.ws = new WebSocket(wsUrl); this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => { this.ws.onopen = () => {
console.log('✅ WebSocket connected'); console.log('✅ WebSocket connected');
this.updateConnectionStatus('connected'); this.updateConnectionStatus('connected');
// Clear reconnect interval if it exists
if (this.reconnectInterval) { if (this.reconnectInterval) {
clearInterval(this.reconnectInterval); clearInterval(this.reconnectInterval);
this.reconnectInterval = null; this.reconnectInterval = null;
@ -67,9 +78,7 @@ class RetroPulseOverlay {
this.ws.onclose = (event) => { this.ws.onclose = (event) => {
console.log('❌ WebSocket disconnected:', event.code, event.reason); console.log('❌ WebSocket disconnected:', event.code, event.reason);
this.updateConnectionStatus('disconnected'); this.updateConnectionStatus('disconnected');
if (event.code !== 1000) { // Don't reconnect on clean close
// Don't reconnect if it was a clean close
if (event.code !== 1000) {
this.scheduleReconnect(); this.scheduleReconnect();
} }
}; };
@ -77,6 +86,7 @@ class RetroPulseOverlay {
this.ws.onerror = (error) => { this.ws.onerror = (error) => {
console.error('WebSocket error:', error); console.error('WebSocket error:', error);
this.updateConnectionStatus('disconnected'); this.updateConnectionStatus('disconnected');
// The onclose event will fire next, which will handle reconnecting.
}; };
} catch (error) { } catch (error) {
@ -85,14 +95,21 @@ class RetroPulseOverlay {
} }
} }
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) { handleMessage(message) {
console.log('📨 Received message:', message); console.log('📨 Received message:', message);
if (message.error) { if (message.error) {
this.showError(message.error); this.showError(message.error);
return; return;
} }
if (message.type === 'initial' || message.type === 'update') { if (message.type === 'initial' || message.type === 'update') {
this.updateOverlay(message.data); this.updateOverlay(message.data);
} }
@ -100,143 +117,116 @@ class RetroPulseOverlay {
updateOverlay(data) { updateOverlay(data) {
if (!data) return; if (!data) return;
console.log('🔄 Updating overlay with data:', data); console.log('🔄 Updating overlay with data:', data);
// Update last update time
this.lastUpdateTime = new Date(); this.lastUpdateTime = new Date();
// Hide error display
this.errorDisplay.classList.add('hidden'); this.errorDisplay.classList.add('hidden');
// Update achievements const hasGameProgress = data.gameProgress !== null;
if (data.recentAchievements && data.recentAchievements.length > 0) { const hasAchievements = data.recentAchievements && data.recentAchievements.length > 0;
this.updateAchievements(data.recentAchievements);
// Update game info from the latest achievement if (hasGameProgress || hasAchievements) {
const latestAchievement = data.recentAchievements[0]; // We have data, so hide the waiting message
if (latestAchievement.gameTitle) { this.waitingForActivity.classList.add('hidden');
this.updateGameInfo({
title: latestAchievement.gameTitle, // Update Game Info and Progress Bar
icon: latestAchievement.gameIcon 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 { } else {
// No achievements yet // No data to display, show the waiting message
this.achievementsList.innerHTML = '<div class="no-achievements">No recent achievements</div>'; this.waitingForActivity.classList.remove('hidden');
this.currentGame.classList.add('hidden');
this.achievementsContainer.classList.add('hidden');
} }
} }
updateGameInfo(gameData) { updateGameInfo(gameData) {
if (!gameData) return; this.currentGame.classList.remove('hidden');
this.gameTitle.textContent = gameData.title || 'Unknown Game'; this.gameTitle.textContent = gameData.title;
this.consoleName.textContent = gameData.consoleName || '';
if (gameData.icon) { if(gameData.icon) {
this.gameIcon.src = gameData.icon; this.gameIcon.src = gameData.icon;
this.gameIcon.onerror = () => { this.gameIcon.style.display = 'block';
this.gameIcon.style.display = 'none'; } else {
}; this.gameIcon.style.display = 'none';
} }
this.currentGame.classList.remove('hidden'); // 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) { updateAchievements(achievements) {
// Clear existing achievements // To avoid flickering, only update if content has changed.
this.achievementsList.innerHTML = ''; // 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;
}
achievements.forEach((achievement, index) => { console.log('✨ Updating achievements list.');
const achievementElement = this.createAchievementElement(achievement); 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
// Add a small delay for staggered animation const earnedDate = new Date(ach.dateEarned).toLocaleString();
setTimeout(() => {
this.achievementsList.appendChild(achievementElement); item.innerHTML = `
}, index * 100); <img src="${ach.badgeName}" alt="Badge" class="achievement-badge">
<div class="achievement-details">
<div class="achievement-title">${ach.title}</div>
<div class="achievement-description">${ach.description}</div>
<div class="achievement-meta">
<span class="achievement-points">${ach.points} Pts</span>
<span class="achievement-date">${earnedDate}</span>
</div>
</div>
`;
this.achievementsList.appendChild(item);
}); });
} }
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) { updateConnectionStatus(status) {
this.connectionStatus.className = `connection-status ${status}`; this.connectionStatus.className = 'connection-status'; // Reset classes
this.connectionStatus.classList.add(status);
const statusTexts = { this.connectionStatus.textContent = status;
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) { showError(message) {
console.error('❌ Overlay error:', message); console.error('Overlay Error:', message);
this.errorMessage.textContent = message; this.errorMessage.textContent = message;
this.errorDisplay.classList.remove('hidden'); this.errorDisplay.classList.remove('hidden');
this.updateConnectionStatus('disconnected'); // Hide other elements
} this.currentGame.classList.add('hidden');
this.achievementsContainer.classList.add('hidden');
// Utility function to escape HTML this.waitingForActivity.classList.add('hidden');
escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, (m) => map[m]);
} }
} }
// Initialize overlay when page loads // Initialize the overlay logic
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new RetroPulseOverlay(); new RetroPulseOverlay();
}); });
// Handle page visibility for OBS
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
console.log('🔄 Page became visible, overlay active');
}
});

View file

@ -3,41 +3,55 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RetroPulse - Overlay</title> <title>RetroPulse Overlay</title>
<link rel="stylesheet" href="/css/overlay.css"> <link rel="stylesheet" href="/css/overlay.css">
</head> </head>
<body> <body>
<div class="overlay-container"> <div id="overlay-container" class="overlay-container">
<!-- Connection Status --> <!-- Connection Status -->
<div id="connection-status" class="connection-status connecting"> <div id="connection-status" class="connection-status connecting">Connecting...</div>
<span class="status-text">Connecting...</span>
</div>
<!-- Current Game Info --> <!-- Waiting for Activity -->
<div id="current-game" class="current-game hidden"> <section id="waiting-for-activity" class="waiting-display hidden">
<h2>Waiting for Game Activity</h2>
<p>Start playing a game with RetroAchievements enabled. New achievements will appear here automatically.</p>
</section>
<!-- Current Game -->
<section id="current-game" class="current-game hidden">
<div class="game-header"> <div class="game-header">
<img id="game-icon" src="" alt="Game Icon" class="game-icon"> <img id="game-icon" src="" alt="Game Icon" class="game-icon">
<div class="game-info"> <div class="game-info">
<h2 id="game-title">Loading...</h2> <h2 id="game-title">Loading Game...</h2>
<p id="console-name" class="console-name"></p> <p id="console-name" class="console-name"></p>
</div> </div>
</div> </div>
</div> <!-- Game Progress Bar -->
<div id="game-progress-section" class="game-progress hidden">
<!-- Recent Achievements --> <div class="progress-stats">
<div id="achievements-container" class="achievements-container"> <span id="progress-text">Completion: 0/0</span>
<h3>Recent Achievements</h3> <span id="progress-percentage">0%</span>
<div id="achievements-list" class="achievements-list"> </div>
<!-- Achievements will be populated by JavaScript --> <div class="progress-bar-container">
<div id="progress-bar" class="progress-bar-fill"></div>
</div>
</div> </div>
</div> </section>
<!-- Achievements -->
<section id="achievements-container" class="achievements-container hidden">
<h3>Recent Achievements</h3>
<ul id="achievements-list" class="achievements-list">
<!-- Achievement items will be injected here -->
</ul>
</section>
<!-- Error Display --> <!-- Error Display -->
<div id="error-display" class="error-display hidden"> <div id="error-display" class="error-display hidden">
<div class="error-content"> <div class="error-content">
<h3>Connection Error</h3> <h3>Error</h3>
<p id="error-message"></p> <p id="error-message">An error occurred.</p>
<button onclick="location.reload()">Retry</button> <button onclick="location.reload()">Reload</button>
</div> </div>
</div> </div>
</div> </div>