added percentage bar
This commit is contained in:
parent
44b935e044
commit
ca5a55a612
|
@ -6,10 +6,12 @@ const {
|
||||||
buildAuthorization,
|
buildAuthorization,
|
||||||
getUserProfile,
|
getUserProfile,
|
||||||
getUserRecentAchievements,
|
getUserRecentAchievements,
|
||||||
getGame
|
getGame,
|
||||||
|
getGameInfoAndUserProgress
|
||||||
} = require('@retroachievements/api');
|
} = require('@retroachievements/api');
|
||||||
|
|
||||||
class RetroClient {
|
class RetroClient {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const RA_USER = process.env.RA_USER;
|
const RA_USER = process.env.RA_USER;
|
||||||
const RA_KEY = process.env.RA_KEY;
|
const RA_KEY = process.env.RA_KEY;
|
||||||
|
@ -22,7 +24,6 @@ class RetroClient {
|
||||||
username: RA_USER,
|
username: RA_USER,
|
||||||
webApiKey: RA_KEY
|
webApiKey: RA_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🎮 RetroAchievements API client initialized');
|
console.log('🎮 RetroAchievements API client initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +39,6 @@ class RetroClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathStr = toStr(imagePath);
|
const pathStr = toStr(imagePath);
|
||||||
|
|
||||||
// Handle full server-relative paths that start with /
|
// Handle full server-relative paths that start with /
|
||||||
if (pathStr.startsWith('/')) {
|
if (pathStr.startsWith('/')) {
|
||||||
// Ensure .png for badges
|
// Ensure .png for badges
|
||||||
|
@ -66,7 +66,9 @@ class RetroClient {
|
||||||
async getUserProfile(username) {
|
async getUserProfile(username) {
|
||||||
try {
|
try {
|
||||||
console.log(`📊 Fetching profile for user: ${username}`);
|
console.log(`📊 Fetching profile for user: ${username}`);
|
||||||
const profile = await getUserProfile(this.authorization, { username });
|
const profile = await getUserProfile(this.authorization, {
|
||||||
|
username
|
||||||
|
});
|
||||||
return profile;
|
return profile;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error fetching user profile for ${username}:`, error.message);
|
console.error(`❌ Error fetching user profile for ${username}:`, error.message);
|
||||||
|
@ -91,7 +93,9 @@ class RetroClient {
|
||||||
async getGameInfo(gameId) {
|
async getGameInfo(gameId) {
|
||||||
try {
|
try {
|
||||||
console.log(`🎮 Fetching game info for ID: ${gameId}`);
|
console.log(`🎮 Fetching game info for ID: ${gameId}`);
|
||||||
const game = await getGame(this.authorization, { gameId });
|
const game = await getGame(this.authorization, {
|
||||||
|
gameId
|
||||||
|
});
|
||||||
return game;
|
return game;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error fetching game info for ID ${gameId}:`, error.message);
|
console.error(`❌ Error fetching game info for ID ${gameId}:`, error.message);
|
||||||
|
@ -102,7 +106,6 @@ class RetroClient {
|
||||||
async getUserData(username) {
|
async getUserData(username) {
|
||||||
try {
|
try {
|
||||||
const profile = await this.getUserProfile(username);
|
const profile = await this.getUserProfile(username);
|
||||||
|
|
||||||
let lastGame = null;
|
let lastGame = null;
|
||||||
let lastAchievement = null;
|
let lastAchievement = null;
|
||||||
|
|
||||||
|
@ -117,7 +120,7 @@ class RetroClient {
|
||||||
try {
|
try {
|
||||||
const recentAchievements = await this.getUserRecentAchievements(username, 1);
|
const recentAchievements = await this.getUserRecentAchievements(username, 1);
|
||||||
if (recentAchievements && recentAchievements.length > 0) {
|
if (recentAchievements && recentAchievements.length > 0) {
|
||||||
lastAchievement = recentAchievements;
|
lastAchievement = recentAchievements[0];
|
||||||
}
|
}
|
||||||
} catch (achievementError) {
|
} catch (achievementError) {
|
||||||
console.warn(`⚠️ Could not fetch recent achievements: ${achievementError.message}`);
|
console.warn(`⚠️ Could not fetch recent achievements: ${achievementError.message}`);
|
||||||
|
@ -137,8 +140,8 @@ class RetroClient {
|
||||||
memberSince: profile.memberSince || null,
|
memberSince: profile.memberSince || null,
|
||||||
rank: profile.rank || null
|
rank: profile.rank || null
|
||||||
},
|
},
|
||||||
lastGame: lastGame
|
lastGame: lastGame ?
|
||||||
? {
|
{
|
||||||
id: lastGame.gameId,
|
id: lastGame.gameId,
|
||||||
title: lastGame.title,
|
title: lastGame.title,
|
||||||
icon: this.buildImageUrl(
|
icon: this.buildImageUrl(
|
||||||
|
@ -146,10 +149,10 @@ class RetroClient {
|
||||||
'https://media.retroachievements.org/Images'
|
'https://media.retroachievements.org/Images'
|
||||||
),
|
),
|
||||||
consoleName: lastGame.consoleName
|
consoleName: lastGame.consoleName
|
||||||
}
|
} :
|
||||||
: null,
|
null,
|
||||||
lastAchievement: lastAchievement
|
lastAchievement: lastAchievement ?
|
||||||
? {
|
{
|
||||||
id: lastAchievement.achievementId,
|
id: lastAchievement.achievementId,
|
||||||
title: lastAchievement.title,
|
title: lastAchievement.title,
|
||||||
description: lastAchievement.description,
|
description: lastAchievement.description,
|
||||||
|
@ -165,8 +168,8 @@ class RetroClient {
|
||||||
'https://media.retroachievements.org/Badge'
|
'https://media.retroachievements.org/Badge'
|
||||||
),
|
),
|
||||||
dateEarned: lastAchievement.dateEarned
|
dateEarned: lastAchievement.dateEarned
|
||||||
}
|
} :
|
||||||
: null
|
null
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error in getUserData for ${username}:`, error.message);
|
console.error(`❌ Error in getUserData for ${username}:`, error.message);
|
||||||
|
@ -177,13 +180,47 @@ class RetroClient {
|
||||||
async getOverlayData(username, count = 5) {
|
async getOverlayData(username, count = 5) {
|
||||||
try {
|
try {
|
||||||
const recentAchievements = await this.getUserRecentAchievements(username, count);
|
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) => {
|
const formattedAchievements = recentAchievements.map((achievement) => {
|
||||||
// Debug log to see what we're getting from the API
|
|
||||||
console.log(
|
|
||||||
`🔍 Processing badge for achievement "${achievement.title}": badgeName="${achievement.badgeName}"`
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: achievement.achievementId,
|
id: achievement.achievementId,
|
||||||
title: achievement.title,
|
title: achievement.title,
|
||||||
|
@ -205,6 +242,7 @@ class RetroClient {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
username,
|
username,
|
||||||
|
gameProgress,
|
||||||
recentAchievements: formattedAchievements,
|
recentAchievements: formattedAchievements,
|
||||||
lastUpdated: new Date().toISOString()
|
lastUpdated: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
} else {
|
||||||
this.gameIcon.style.display = 'none';
|
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');
|
||||||
achievements.forEach((achievement, index) => {
|
if (firstExisting && firstExisting.dataset.id == achievements[0].id) {
|
||||||
const achievementElement = this.createAchievementElement(achievement);
|
console.log('🧘 Achievements are already up-to-date.');
|
||||||
|
return;
|
||||||
// Add a small delay for staggered animation
|
|
||||||
setTimeout(() => {
|
|
||||||
this.achievementsList.appendChild(achievementElement);
|
|
||||||
}, index * 100);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createAchievementElement(achievement) {
|
console.log('✨ Updating achievements list.');
|
||||||
const item = document.createElement('div');
|
this.achievementsList.innerHTML = ''; // Clear existing
|
||||||
|
achievements.forEach(ach => {
|
||||||
|
const item = document.createElement('li');
|
||||||
item.className = 'achievement-item';
|
item.className = 'achievement-item';
|
||||||
|
item.dataset.id = ach.id; // For update checking
|
||||||
|
|
||||||
// Format the date
|
const earnedDate = new Date(ach.dateEarned).toLocaleString();
|
||||||
const dateEarned = achievement.dateEarned ?
|
|
||||||
new Date(achievement.dateEarned).toLocaleDateString() :
|
|
||||||
'Recently';
|
|
||||||
|
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<img src="${achievement.badgeName || achievement.gameIcon || ''}"
|
<img src="${ach.badgeName}" alt="Badge" class="achievement-badge">
|
||||||
alt="Achievement Badge"
|
|
||||||
class="achievement-badge"
|
|
||||||
onerror="this.style.display='none'">
|
|
||||||
<div class="achievement-details">
|
<div class="achievement-details">
|
||||||
<div class="achievement-title">${this.escapeHtml(achievement.title || 'Unknown Achievement')}</div>
|
<div class="achievement-title">${ach.title}</div>
|
||||||
<div class="achievement-description">${this.escapeHtml(achievement.description || '')}</div>
|
<div class="achievement-description">${ach.description}</div>
|
||||||
<div class="achievement-meta">
|
<div class="achievement-meta">
|
||||||
<span class="achievement-points">${achievement.points || 0} pts</span>
|
<span class="achievement-points">${ach.points} Pts</span>
|
||||||
<span class="achievement-date">${dateEarned}</span>
|
<span class="achievement-date">${earnedDate}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
this.achievementsList.appendChild(item);
|
||||||
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 = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
};
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
@ -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>
|
||||||
|
<!-- Game Progress Bar -->
|
||||||
|
<div id="game-progress-section" class="game-progress hidden">
|
||||||
|
<div class="progress-stats">
|
||||||
|
<span id="progress-text">Completion: 0/0</span>
|
||||||
|
<span id="progress-percentage">0%</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div id="progress-bar" class="progress-bar-fill"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Recent Achievements -->
|
<!-- Achievements -->
|
||||||
<div id="achievements-container" class="achievements-container">
|
<section id="achievements-container" class="achievements-container hidden">
|
||||||
<h3>Recent Achievements</h3>
|
<h3>Recent Achievements</h3>
|
||||||
<div id="achievements-list" class="achievements-list">
|
<ul id="achievements-list" class="achievements-list">
|
||||||
<!-- Achievements will be populated by JavaScript -->
|
<!-- Achievement items will be injected here -->
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</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>
|
||||||
|
|
Loading…
Reference in a new issue