diff --git a/backend/retroClient.js b/backend/retroClient.js index 1d83deb..59fa967 100644 --- a/backend/retroClient.js +++ b/backend/retroClient.js @@ -1,7 +1,4 @@ -// backend/retroClient.js - 'use strict'; - const { buildAuthorization, getUserProfile, @@ -9,35 +6,27 @@ const { getGame, getGameInfoAndUserProgress } = require('@retroachievements/api'); - class RetroClient { - constructor() { 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'); + console.log('RetroAchievements API client initialized'); } - // Helper method to build correct image URLs buildImageUrl(imagePath, baseUrl) { if (!imagePath) return null; - const toStr = (v) => (typeof v === 'string' ? v : String(v)); - // If full URL provided, return as-is if (/^https?:\/\//i.test(imagePath)) { return imagePath; } - const pathStr = toStr(imagePath); // Handle full server-relative paths that start with / if (pathStr.startsWith('/')) { @@ -48,84 +37,74 @@ class RetroClient { } 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}`); + 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); + 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}`); + 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); + 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}`); + 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); + 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 { lastGame = await this.getGameInfo(profile.lastGameId); } catch (gameError) { - console.warn(`⚠️ Could not fetch last game info: ${gameError.message}`); + console.warn(`Could not fetch last game info: ${gameError.message}`); } } - try { const recentAchievements = await this.getUserRecentAchievements(username, 1); if (recentAchievements && recentAchievements.length > 0) { lastAchievement = recentAchievements[0]; } } catch (achievementError) { - console.warn(`⚠️ Could not fetch recent achievements: ${achievementError.message}`); + console.warn(`Could not fetch recent achievements: ${achievementError.message}`); } - return { user: { username: profile.user, @@ -140,65 +119,60 @@ class RetroClient { 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 - } : + 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 - } : + 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); + 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.`); + 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}`); + 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 { @@ -206,7 +180,6 @@ class RetroClient { username, gameId }); - gameProgress = { title: progressData.title, icon: this.buildImageUrl(progressData.imageIcon, 'https://media.retroachievements.org/Images'), @@ -216,10 +189,9 @@ class RetroClient { completion: progressData.userCompletion, }; } catch (progressError) { - console.warn(`⚠️ Could not fetch game progress for gameId ${gameId}: ${progressError.message}`); + console.warn(`Could not fetch game progress for gameId ${gameId}: ${progressError.message}`); } } - const formattedAchievements = recentAchievements.map((achievement) => { return { id: achievement.achievementId, @@ -239,7 +211,6 @@ class RetroClient { dateEarned: achievement.dateEarned }; }); - return { username, gameProgress, @@ -247,10 +218,9 @@ class RetroClient { lastUpdated: new Date().toISOString() }; } catch (error) { - console.error(`❌ Error in getOverlayData for ${username}:`, error.message); + console.error(`Error in getOverlayData for ${username}:`, error.message); throw error; } } } - -module.exports = new RetroClient(); +module.exports = new RetroClient(); \ No newline at end of file diff --git a/backend/routes/api.js b/backend/routes/api.js index 2ad113a..ec55a60 100644 --- a/backend/routes/api.js +++ b/backend/routes/api.js @@ -1,19 +1,16 @@ const express = require('express'); const router = express.Router(); const retroClient = require('../retroClient'); - // GET /api/userinfo?username=xxx router.get('/userinfo', async (req, res) => { try { const { username } = req.query; - if (!username) { return res.status(400).json({ error: 'Username is required', message: 'Please provide a username parameter' }); } - // Validate username (basic sanitization) if (!/^[a-zA-Z0-9_-]+$/.test(username)) { return res.status(400).json({ @@ -21,18 +18,13 @@ router.get('/userinfo', async (req, res) => { message: 'Username can only contain letters, numbers, underscores, and hyphens' }); } - - console.log(`📡 API request for user: ${username}`); - + console.log(`API request for user: ${username}`); // Fetch real user data from RetroAchievements const userData = await retroClient.getUserData(username); - - console.log(`✅ Successfully fetched data for user: ${username}`); + console.log(`Successfully fetched data for user: ${username}`); res.json(userData); - } catch (error) { - console.error('❌ Error in /api/userinfo:', error); - + console.error('Error in /api/userinfo:', error); // Handle specific API errors if (error.message.includes('User not found') || error.message.includes('Invalid user')) { return res.status(404).json({ @@ -40,7 +32,6 @@ router.get('/userinfo', async (req, res) => { message: `RetroAchievements user '${req.query.username}' not found` }); } - // Handle rate limiting if (error.message.includes('rate limit') || error.message.includes('too many requests')) { return res.status(429).json({ @@ -48,20 +39,17 @@ router.get('/userinfo', async (req, res) => { message: 'Too many API requests. Please wait a moment and try again.' }); } - res.status(500).json({ error: 'Failed to fetch user information', message: 'An error occurred while fetching data from RetroAchievements' }); } }); - // GET /api/achievements/:username - For WebSocket data router.get('/achievements/:username', async (req, res) => { try { const { username } = req.params; const { count = 5 } = req.query; - // Validate username if (!/^[a-zA-Z0-9_-]+$/.test(username)) { return res.status(400).json({ @@ -69,21 +57,15 @@ router.get('/achievements/:username', async (req, res) => { message: 'Username can only contain letters, numbers, underscores, and hyphens' }); } - // Validate count parameter const achievementCount = Math.min(Math.max(parseInt(count) || 5, 1), 25); // Between 1 and 25 - - console.log(`🏆 API request for ${username}'s recent ${achievementCount} achievements`); - + console.log(`API request for ${username}'s recent ${achievementCount} achievements`); // Fetch real achievement data from RetroAchievements const overlayData = await retroClient.getOverlayData(username, achievementCount); - - console.log(`✅ Successfully fetched ${overlayData.recentAchievements.length} achievements for user: ${username}`); + console.log(`Successfully fetched ${overlayData.recentAchievements.length} achievements for user: ${username}`); res.json(overlayData); - } catch (error) { - console.error('❌ Error in /api/achievements:', error); - + console.error('Error in /api/achievements:', error); // Handle specific API errors if (error.message.includes('User not found') || error.message.includes('Invalid user')) { return res.status(404).json({ @@ -91,14 +73,12 @@ router.get('/achievements/:username', async (req, res) => { message: `RetroAchievements user '${req.params.username}' not found` }); } - res.status(500).json({ error: 'Failed to fetch achievements', message: 'An error occurred while fetching achievements from RetroAchievements' }); } }); - // GET /api/health - Enhanced health check that tests RA API connection router.get('/health', async (req, res) => { try { @@ -111,12 +91,11 @@ router.get('/health', async (req, res) => { }); } catch (error) { res.status(503).json({ - status: 'ERROR', + status: 'ERROR', message: 'Service unavailable', retroAchievements: 'Connection failed', timestamp: new Date().toISOString() }); } }); - -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 4b469f0..5414e26 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,78 +1,60 @@ -// backend/server.js - // Load environment variables from .env file require('dotenv').config(); - const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const path = require('path'); - // Create Express app and HTTP server const app = express(); const server = http.createServer(app); const PORT = process.env.PORT || 3000; - // Middleware to parse JSON requests app.use(express.json()); app.use(express.urlencoded({ extended: true })); - // Serve static files from public directory app.use(express.static(path.join(__dirname, '../public'))); - // Basic route for the main page app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../public/index.html')); }); - // Route for overlay page with username parameter app.get('/overlay/:username', (req, res) => { res.sendFile(path.join(__dirname, '../public/overlay.html')); }); - // API Routes const apiRoutes = require('./routes/api'); app.use('/api', apiRoutes); - // Health check endpoint app.get('/health', (req, res) => { - res.json({ - status: 'OK', + res.json({ + status: 'OK', message: 'RetroPulse server is running', timestamp: new Date().toISOString() }); }); - // WebSocket Setup const retroClient = require('./retroClient'); - // Create WebSocket server -const wss = new WebSocket.Server({ - server, +const wss = new WebSocket.Server({ + server, path: '/ws' }); - // Store active connections by username const userConnections = new Map(); - wss.on('connection', async (ws, req) => { // Extract username from query parameters const url = new URL(req.url, `http://localhost:${PORT}`); const username = url.searchParams.get('user'); - if (!username) { - ws.send(JSON.stringify({ - error: 'Username parameter is required (?user=yourname)' + ws.send(JSON.stringify({ + error: 'Username parameter is required (?user=yourname)' })); ws.close(); return; } - - console.log(`🔌 WebSocket connected for user: ${username}`); - + console.log(`WebSocket connected for user: ${username}`); // Store the connection userConnections.set(username, ws); - // Send initial data immediately try { const initialData = await retroClient.getOverlayData(username, 5); @@ -86,7 +68,6 @@ wss.on('connection', async (ws, req) => { message: `Failed to load initial data: ${error.message}` })); } - // Set up polling for updates every 10 seconds const pollInterval = setInterval(async () => { if (ws.readyState !== WebSocket.OPEN) { @@ -94,7 +75,6 @@ wss.on('connection', async (ws, req) => { userConnections.delete(username); return; } - try { const updatedData = await retroClient.getOverlayData(username, 5); ws.send(JSON.stringify({ @@ -105,61 +85,53 @@ wss.on('connection', async (ws, req) => { console.error(`Error fetching updates for ${username}:`, error.message); } }, 10000); // Poll every 10 seconds - // Handle client disconnect ws.on('close', () => { clearInterval(pollInterval); userConnections.delete(username); - console.log(`🔌 WebSocket disconnected for user: ${username}`); + console.log(`WebSocket disconnected for user: ${username}`); }); - ws.on('error', (error) => { console.error(`WebSocket error for ${username}:`, error.message); }); }); - // Error handling middleware app.use((err, req, res, next) => { console.error('Server error:', err); - res.status(500).json({ + res.status(500).json({ error: 'Internal server error', message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong' }); }); - // Handle 404 for unmatched routes app.use((req, res) => { - res.status(404).json({ + res.status(404).json({ error: 'Not found', message: `Route ${req.method} ${req.path} not found` }); }); - // Start the server (HTTP and WebSocket together) server.listen(PORT, () => { - console.log(`🎮 RetroPulse server is running on http://localhost:${PORT}`); - console.log(`📊 Health check: http://localhost:${PORT}/health`); - console.log(`🎯 Main page: http://localhost:${PORT}/`); - console.log(`📺 Overlay example: http://localhost:${PORT}/overlay/your-username`); - console.log(`🔌 WebSocket endpoint: ws://localhost:${PORT}/ws?user=your-username`); + console.log(`RetroPulse server is running on http://localhost:${PORT}`); + console.log(`Health check: http://localhost:${PORT}/health`); + console.log(`Main page: http://localhost:${PORT}/`); + console.log(`Overlay example: http://localhost:${PORT}/overlay/your-username`); + console.log(`WebSocket endpoint: ws://localhost:${PORT}/ws?user=your-username`); }); - // Graceful shutdown handling process.on('SIGTERM', () => { - console.log('📴 Received SIGTERM, shutting down gracefully'); + console.log('Received SIGTERM, shutting down gracefully'); server.close(() => { - console.log('✅ Server closed'); + console.log('Server closed'); process.exit(0); }); }); - process.on('SIGINT', () => { - console.log('📴 Received SIGINT, shutting down gracefully'); + console.log('Received SIGINT, shutting down gracefully'); server.close(() => { - console.log('✅ Server closed'); + console.log('Server closed'); process.exit(0); }); }); - // Export server for potential testing -module.exports = server; +module.exports = server; \ No newline at end of file diff --git a/public/css/overlay.css b/public/css/overlay.css index 8fa38e9..e3d4872 100644 --- a/public/css/overlay.css +++ b/public/css/overlay.css @@ -1,4 +1,12 @@ -/* Overlay CSS - Designed for OBS Browser Source */ +:root { + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --accent-blue: #4a90e2; + --accent-gold: #ffd700; + --text-primary: #e0e0e0; + --text-secondary: #a0a0a0; +} + * { margin: 0; padding: 0; @@ -6,237 +14,148 @@ } body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background: transparent; /* Transparent for OBS */ - color: #fff; + font-family: 'Courier New', monospace; + background: var(--bg-primary); + color: var(--text-primary); overflow: hidden; - font-size: 14px; + font-size: 11px; + line-height: 1.2; + min-height: 100vh; } .overlay-container { - padding: 20px; width: 100vw; height: 100vh; display: flex; flex-direction: column; - gap: 15px; + background: var(--bg-primary); } -/* Connection Status */ -.connection-status { - position: absolute; - top: 10px; - right: 10px; - padding: 8px 12px; - border-radius: 20px; - font-size: 12px; - font-weight: bold; - text-transform: uppercase; - animation: pulse 2s infinite; -} - -.connection-status.connecting { - background: rgba(255, 193, 7, 0.9); - color: #333; -} - -.connection-status.connected { - background: rgba(40, 167, 69, 0.9); - color: white; - animation: none; -} - -.connection-status.disconnected { - background: rgba(220, 53, 69, 0.9); - color: white; -} - -@keyframes pulse { - 0% { opacity: 1; } - 50% { opacity: 0.5; } - 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; + background: var(--bg-secondary); + border: 2px solid var(--accent-blue); 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; + max-width: 300px; } .waiting-display h2 { - margin-bottom: 15px; - font-size: 1.5em; - text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7); + font-size: 14px; + background: var(--accent-blue); + color: var(--bg-primary); } .waiting-display p { - opacity: 0.8; - line-height: 1.5; + font-size: 11px; + color: var(--text-secondary); } -/* Current Game */ .current-game { - background: linear-gradient(135deg, rgba(30, 60, 114, 0.9), rgba(42, 82, 152, 0.9)); - padding: 20px; - border-radius: 12px; - border: 2px solid rgba(255, 255, 255, 0.2); - backdrop-filter: blur(10px); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + background: var(--bg-secondary); + border: 1px solid var(--accent-blue); } .game-header { display: flex; align-items: center; - gap: 15px; + gap: 10px; } .game-icon { - width: 64px; - height: 64px; - border-radius: 8px; - border: 2px solid rgba(255, 255, 255, 0.3); - background: rgba(255, 255, 255, 0.1); + width: 48px; + height: 48px; + border: 1px solid var(--accent-blue); + background: var(--bg-primary); } .game-info h2 { - font-size: 1.8em; - margin-bottom: 5px; - text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7); + font-size: 14px; + color: var(--text-primary); } .console-name { - opacity: 0.8; - font-size: 1.1em; + font-size: 11px; + color: var(--text-secondary); } -/* Game Progress Bar */ .game-progress { - margin-top: 15px; + border-top: 1px solid var(--accent-blue); } .progress-stats { display: flex; justify-content: space-between; align-items: center; - font-size: 1em; - margin-bottom: 8px; - opacity: 0.9; + font-size: 10px; font-weight: bold; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); + color: var(--text-primary); + background: var(--bg-primary); } .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); + background: var(--bg-primary); + border: 1px solid var(--accent-blue); + height: 8px; } .progress-bar-fill { width: 0%; height: 100%; - background: linear-gradient(90deg, #1e90ff, #00bfff); - border-radius: 20px; - transition: width 0.5s ease-in-out; + background: var(--accent-gold); } -/* Achievements Container */ .achievements-container { flex: 1; - background: linear-gradient(135deg, rgba(40, 167, 69, 0.9), rgba(32, 134, 55, 0.9)); - padding: 20px; - border-radius: 12px; - border: 2px solid rgba(255, 255, 255, 0.2); - backdrop-filter: blur(10px); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + background: var(--bg-secondary); + border: 1px solid var(--accent-blue); overflow: hidden; } .achievements-container h3 { - font-size: 1.4em; - margin-bottom: 15px; + font-size: 12px; text-align: center; - text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7); - border-bottom: 2px solid rgba(255, 255, 255, 0.3); - padding-bottom: 10px; + background: var(--accent-blue); + color: var(--bg-primary); } .achievements-list { display: flex; flex-direction: column; - gap: 12px; - max-height: 400px; /* Adjust as needed */ + max-height: 300px; overflow-y: auto; - padding-right: 10px; list-style-type: none; + background: var(--bg-primary); } -/* Custom Scrollbar */ .achievements-list::-webkit-scrollbar { - width: 6px; -} -.achievements-list::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.1); - border-radius: 3px; -} -.achievements-list::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 3px; -} -.achievements-list::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); + width: 4px; +} + +.achievements-list::-webkit-scrollbar-track { + background: var(--bg-primary); +} + +.achievements-list::-webkit-scrollbar-thumb { + background: var(--accent-blue); } -/* Achievement Item */ .achievement-item { display: flex; align-items: center; - gap: 12px; - background: rgba(255, 255, 255, 0.1); - padding: 15px; - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 0.2); - backdrop-filter: blur(5px); - transition: all 0.3s ease; - animation: slideIn 0.5s ease-out; -} - -.achievement-item:hover { - transform: translateX(5px); - background: rgba(255, 255, 255, 0.15); - border-color: rgba(255, 255, 255, 0.4); -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateX(-30px); - } - to { - opacity: 1; - transform: translateX(0); - } + gap: 8px; + background: var(--bg-primary); + border-bottom: 1px solid var(--bg-secondary); } .achievement-badge { - width: 48px; - height: 48px; - border-radius: 50%; - border: 2px solid rgba(255, 255, 255, 0.3); - background: rgba(255, 255, 255, 0.1); + width: 32px; + height: 32px; + border: 1px solid var(--accent-gold); + background: var(--bg-secondary); flex-shrink: 0; } @@ -247,96 +166,64 @@ body { .achievement-title { font-weight: bold; - font-size: 1.1em; - margin-bottom: 3px; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); + font-size: 11px; + color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .achievement-description { - opacity: 0.8; - font-size: 0.9em; - margin-bottom: 5px; - line-height: 1.3; + font-size: 10px; + color: var(--text-secondary); display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; } .achievement-meta { display: flex; - justify-content: space-between; align-items: center; - font-size: 0.8em; - opacity: 0.7; + font-size: 9px; + color: var(--text-secondary); } .achievement-points { - background: rgba(255, 215, 0, 0.2); - color: #FFD700; - padding: 2px 6px; - border-radius: 10px; - border: 1px solid rgba(255, 215, 0, 0.3); + background: var(--accent-gold); + color: var(--bg-primary); + font-weight: bold; } -.achievement-date { - font-style: italic; -} - -/* Error Display */ .error-display { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); - background: rgba(220, 53, 69, 0.95); - padding: 30px; - border-radius: 12px; + background: #4d1f1f; + border: 2px solid #cc0000; text-align: center; - border: 2px solid rgba(255, 255, 255, 0.3); - backdrop-filter: blur(10px); } .error-content h3 { - margin-bottom: 10px; - font-size: 1.3em; -} -.error-content p { - margin-bottom: 20px; - opacity: 0.9; -} -.error-content button { - background: rgba(255, 255, 255, 0.2); - color: white; - border: 1px solid rgba(255, 255, 255, 0.3); - padding: 10px 20px; - border-radius: 6px; - cursor: pointer; - transition: background 0.3s; -} -.error-content button:hover { - background: rgba(255, 255, 255, 0.3); + font-size: 12px; + color: #ff6666; +} + +.error-content p { + font-size: 11px; + color: #ff9999; +} + +.error-content button { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--accent-blue); + cursor: pointer; + font-size: 10px; + font-family: inherit; } -/* Utility Classes */ .hidden { display: none !important; -} - -/* Responsive Design for Different OBS Sizes */ -@media (max-width: 500px) { - .overlay-container { - padding: 10px; - } - .game-header { - flex-direction: column; - text-align: center; - } - .achievement-item { - flex-direction: column; - text-align: center; - } -} +} \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css index fd35dc5..dd743b7 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,282 +1,224 @@ -/* Basic styling for RetroPulse */ +/* RetroAchievements Style - Color Variables */ +:root { + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --accent-blue: #4a90e2; + --accent-gold: #ffd700; + --text-primary: #e0e0e0; + --text-secondary: #a0a0a0; +} * { margin: 0; padding: 0; box-sizing: border-box; } - body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background: linear-gradient(135deg, #1e3c72, #2a5298); - color: #fff; + font-family: 'Courier New', monospace; + background: var(--bg-primary); + color: var(--text-primary); min-height: 100vh; - line-height: 1.6; + font-size: 12px; + line-height: 1.2; } - .container { - max-width: 800px; + max-width: 600px; margin: 0 auto; - padding: 20px; } - header { text-align: center; - margin-bottom: 40px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--accent-blue); } - header h1 { - font-size: 3em; - margin-bottom: 10px; - text-shadow: 2px 2px 4px rgba(0,0,0,0.5); + font-size: 18px; + color: var(--accent-gold); } - header p { - font-size: 1.2em; - opacity: 0.9; + font-size: 11px; + color: var(--text-secondary); } - -/* Form Styling */ .user-input { - background: rgba(255,255,255,0.1); - padding: 30px; - border-radius: 15px; - text-align: center; - margin-bottom: 30px; - backdrop-filter: blur(10px); - border: 1px solid rgba(255,255,255,0.2); + background: var(--bg-secondary); + border: 1px solid var(--accent-blue); } - .user-input h2 { - margin-bottom: 20px; - font-size: 1.5em; + font-size: 14px; + text-align: center; + background: var(--accent-blue); + color: var(--bg-primary); } - #userForm { display: flex; - gap: 15px; - justify-content: center; - flex-wrap: wrap; } - #username { flex: 1; - max-width: 300px; - padding: 15px; border: none; - border-radius: 8px; - font-size: 1.1em; - background: rgba(255,255,255,0.9); - color: #333; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 12px; + font-family: inherit; + border-right: 1px solid var(--accent-blue); } - #username:focus { outline: none; - box-shadow: 0 0 0 3px rgba(255,255,255,0.3); + background: var(--bg-secondary); } - button { - padding: 15px 30px; - background: #28a745; - color: white; + background: var(--accent-blue); + color: var(--bg-primary); border: none; - border-radius: 8px; - font-size: 1.1em; + font-size: 12px; cursor: pointer; - transition: background 0.3s; + font-family: inherit; + font-weight: bold; } - -button:hover { - background: #218838; -} - -/* Section States */ .hidden { display: none; } - .loading, .error { text-align: center; - padding: 40px; - background: rgba(255,255,255,0.1); - border-radius: 15px; - backdrop-filter: blur(10px); - border: 1px solid rgba(255,255,255,0.2); + background: var(--bg-secondary); + border: 1px solid var(--accent-blue); } - .error { - background: rgba(220,53,69,0.2); - border: 1px solid rgba(220,53,69,0.3); + background: #4d1f1f; + border-color: #cc0000; + color: #ff6666; } - .error-message { - color: #ff6b6b; - font-size: 1.1em; - margin-bottom: 15px; + color: #ff6666; + font-size: 12px; } - -/* User Info Styling */ .user-info { - background: rgba(255,255,255,0.1); - padding: 30px; - border-radius: 15px; - backdrop-filter: blur(10px); - border: 1px solid rgba(255,255,255,0.2); + background: var(--bg-secondary); + border: 1px solid var(--accent-blue); } - -.profile-card { - margin-bottom: 30px; +.user-info h2 { + font-size: 14px; + background: var(--accent-blue); + color: var(--bg-primary); + text-align: center; } - .profile-header { display: flex; align-items: center; - gap: 20px; - margin-bottom: 20px; + gap: 10px; + border-bottom: 1px solid var(--accent-blue); } - .avatar { - width: 80px; - height: 80px; - border-radius: 50%; - border: 3px solid rgba(255,255,255,0.3); + width: 40px; + height: 40px; + border: 1px solid var(--accent-blue); } - .profile-details h3 { - font-size: 1.8em; - margin-bottom: 5px; + font-size: 14px; + color: var(--accent-gold); } - .motto { + font-size: 11px; + color: var(--text-secondary); font-style: italic; - opacity: 0.8; - margin-bottom: 10px; } - .stats { display: flex; - gap: 20px; - margin-bottom: 10px; + gap: 10px; } - .stat { - background: rgba(255,255,255,0.1); - padding: 8px 12px; - border-radius: 6px; - font-size: 0.9em; + background: var(--bg-primary); + font-size: 10px; + border: 1px solid var(--accent-gold); + color: var(--accent-gold); } - .member-since { - font-size: 0.9em; - opacity: 0.7; + font-size: 10px; + color: var(--text-secondary); +} +.last-game h3, .last-achievement h3 { + font-size: 12px; + background: var(--bg-primary); + color: var(--text-primary); + border-bottom: 1px solid var(--accent-blue); } - -/* Game and Achievement Cards */ .game-card, .achievement-card { display: flex; align-items: center; - gap: 15px; - background: rgba(255,255,255,0.05); - padding: 20px; - border-radius: 10px; - margin-top: 15px; + gap: 10px; + background: var(--bg-primary); + border-bottom: 1px solid var(--bg-secondary); } - .game-icon, .achievement-badge { - width: 48px; - height: 48px; - border-radius: 8px; - border: 2px solid rgba(255,255,255,0.2); + width: 32px; + height: 32px; + border: 1px solid var(--accent-blue); } - .game-details h4, .achievement-details h4 { - font-size: 1.3em; - margin-bottom: 5px; + font-size: 12px; + color: var(--text-primary); } - .console, .description { - opacity: 0.8; - font-size: 0.9em; + font-size: 10px; + color: var(--text-secondary); } - .achievement-stats { display: flex; - gap: 15px; - margin-top: 8px; - font-size: 0.9em; + gap: 10px; + font-size: 10px; } - -.points, .true-ratio { - background: rgba(255,255,255,0.1); - padding: 4px 8px; - border-radius: 4px; +.points { + color: var(--accent-gold); +} +.true-ratio { + color: var(--text-secondary); } - -/* Overlay Controls */ .overlay-controls { - margin-top: 30px; - padding: 25px; - background: rgba(40,167,69,0.1); - border-radius: 10px; - border: 1px solid rgba(40,167,69,0.3); + background: var(--bg-primary); + border: 1px solid var(--accent-gold); } - .overlay-controls h3 { - margin-bottom: 10px; - color: #28a745; + font-size: 12px; + background: var(--accent-gold); + color: var(--bg-primary); + text-align: center; +} +.overlay-controls p { + font-size: 10px; + color: var(--text-secondary); } - .overlay-url { display: flex; - gap: 10px; - margin: 15px 0; } - .overlay-url input { flex: 1; - padding: 10px; + background: var(--bg-primary); + color: var(--text-primary); border: none; - border-radius: 6px; - background: rgba(255,255,255,0.9); - color: #333; - font-family: monospace; - font-size: 0.9em; + font-family: inherit; + font-size: 10px; + border-right: 1px solid var(--accent-gold); } - .overlay-button { display: inline-block; - padding: 15px 25px; - background: #17a2b8; - color: white; + background: var(--accent-gold); + color: var(--bg-primary); text-decoration: none; - border-radius: 8px; font-weight: bold; - transition: background 0.3s; + font-size: 12px; + text-align: center; } - -.overlay-button:hover { - background: #138496; -} - -/* Responsive Design */ @media (max-width: 600px) { + .container { + max-width: 100%; + } .profile-header { flex-direction: column; - text-align: center; } - .game-card, .achievement-card { flex-direction: column; text-align: center; } - #userForm { flex-direction: column; } - - #username { - max-width: none; - } - .overlay-url { flex-direction: column; } diff --git a/public/index.html b/public/index.html index 5b9117e..4807f22 100644 --- a/public/index.html +++ b/public/index.html @@ -1,46 +1,35 @@ +
RetroAchievements Overlay for OBS