From c868a3c2d5fbeb1210c994fc4997345ab6e32987 Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Sat, 16 May 2026 20:52:41 +1200 Subject: [PATCH] Add game detail view UI and data Introduce a full-screen game detail view and supporting data/model changes. Adds extensive CSS for .game-detail-view and related elements (header image, overlay, content layout, screenshots, achievements, features, actions, and scrollbars). Update renderDetailsPanel to output enriched detail markup (screenshots carousel, achievements UI, features badges, play/install button and contextual actions). Change library action handling to call openDetails for card activation. Extend normalizeLibraryItem with convertArray, screenshots and longDescription support, and populate mock items with screenshots and longDescription to enable the new detail view. --- src/views/library/library.css | 359 +++++++++++++++++++++++++ src/views/library/library.js | 2 +- src/views/library/libraryComponents.js | 176 +++++++++--- src/views/library/libraryModel.js | 33 +++ 4 files changed, 539 insertions(+), 31 deletions(-) diff --git a/src/views/library/library.css b/src/views/library/library.css index 84d7319..277aaf4 100644 --- a/src/views/library/library.css +++ b/src/views/library/library.css @@ -540,3 +540,362 @@ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } } + +/* Game Detail View Styles */ +.game-detail-view { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background: linear-gradient(180deg, rgba(17, 24, 48, 0.95), rgba(8, 12, 27, 0.98)); +} + +.game-detail-header-image { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 40%; + background-size: cover; + background-position: center; + background-color: rgba(79, 216, 255, 0.08); + z-index: 1; +} + +.game-detail-header-overlay { + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(7, 10, 20, 0.3), rgba(7, 10, 20, 0.95)); + z-index: 1; +} + +.game-detail-content { + position: relative; + z-index: 2; + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.game-detail-header { + padding: 28px 40px 20px; + display: grid; + grid-template-columns: 1fr auto; + gap: 40px; + align-items: start; +} + +.game-detail-title-section h1 { + margin: 0; + font-size: 48px; + line-height: 1.1; + letter-spacing: -0.02em; + max-width: 600px; +} + +.game-detail-title-section .library-section-kicker { + display: block; + margin-bottom: 12px; +} + +.game-detail-meta { + display: flex; + gap: 24px; + margin-top: 12px; + font-size: 14px; + color: var(--nebula-color-muted); + flex-wrap: wrap; +} + +.game-detail-play-button-container { + display: flex; + gap: 12px; +} + +.game-detail-play-button { + min-width: 160px; + padding: 16px 32px; + font-size: 18px; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.08em; + border: none; + border-radius: 12px; + background: linear-gradient(135deg, var(--nebula-color-accent), #78f0ff); + color: #04101d; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + box-shadow: 0 12px 32px rgba(79, 216, 255, 0.34); + transition: all 0.2s ease; + white-space: nowrap; +} + +.game-detail-play-button .play-icon { + font-size: 20px; +} + +.game-detail-play-button.is-focused { + transform: scale(1.08); + box-shadow: + 0 0 0 2px rgba(79, 216, 255, 0.3), + 0 16px 40px rgba(79, 216, 255, 0.42); +} + +.game-detail-main { + display: grid; + grid-template-columns: 1fr 320px; + gap: 40px; + padding: 0 40px 40px; +} + +.game-detail-left-column { + display: flex; + flex-direction: column; + gap: 32px; + max-width: 700px; +} + +.game-detail-right-column { + display: flex; + flex-direction: column; + gap: 24px; +} + +.game-detail-section-title { + margin: 0 0 16px 0; + font-size: 18px; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--nebula-color-accent); +} + +.game-detail-description { + display: flex; + flex-direction: column; + gap: 12px; +} + +.game-detail-description p { + margin: 0; + color: var(--nebula-color-muted); + line-height: 1.6; +} + +.game-detail-description .long-description { + color: var(--nebula-color-muted); + opacity: 0.85; +} + +.game-detail-info { + display: flex; + flex-direction: column; + gap: 12px; +} + +.game-detail-info-list { + display: grid; + gap: 12px; +} + +.game-detail-info-list div { + display: grid; + grid-template-columns: 120px 1fr; + gap: 12px; + padding: 12px; + border-radius: var(--nebula-radius-md); + background: rgba(79, 216, 255, 0.05); + border: 1px solid rgba(79, 216, 255, 0.08); +} + +.game-detail-info-list dt { + font-size: 12px; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--nebula-color-accent); +} + +.game-detail-info-list dd { + margin: 0; + color: var(--nebula-color-muted); + word-break: break-word; +} + +/* Screenshots Section */ +.game-detail-screenshots { + display: flex; + flex-direction: column; + gap: 16px; +} + +.screenshots-carousel { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; +} + +.screenshot-item { + aspect-ratio: 16 / 9; + border-radius: 12px; + overflow: hidden; + border: 1px solid rgba(79, 216, 255, 0.12); + background: rgba(10, 16, 34, 0.6); +} + +.screenshot-item img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Achievements Section */ +.game-detail-achievements { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; + border-radius: var(--nebula-radius-lg); + background: linear-gradient(135deg, rgba(79, 216, 255, 0.08), rgba(157, 79, 224, 0.08)); + border: 1px solid rgba(79, 216, 255, 0.12); +} + +.achievements-container { + display: flex; + flex-direction: column; + gap: 12px; +} + +.achievements-progress { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 12px; + align-items: center; +} + +.achievement-stat { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.achievement-number { + font-size: 20px; + font-weight: 900; + color: var(--nebula-color-accent); +} + +.achievement-label { + font-size: 11px; + font-weight: 800; + text-transform: uppercase; + color: var(--nebula-color-muted); + margin-top: 4px; +} + +.achievement-bar { + height: 8px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.08); + overflow: hidden; +} + +.achievement-bar-fill { + height: 100%; + background: linear-gradient(90deg, var(--nebula-color-accent), #78f0ff); + border-radius: 4px; +} + +/* Features and Badges */ +.game-detail-features { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + border-radius: var(--nebula-radius-lg); + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.07); +} + +.features-grid { + display: grid; + gap: 10px; +} + +.feature-badge { + display: inline-block; + padding: 8px 12px; + border-radius: 8px; + font-size: 12px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.04em; + background: rgba(79, 216, 255, 0.14); + color: var(--nebula-color-accent); + border: 1px solid rgba(79, 216, 255, 0.22); +} + +.feature-badge.verified { + background: rgba(79, 255, 136, 0.12); + color: #9effbc; + border-color: rgba(79, 255, 136, 0.24); +} + +/* Actions */ +.game-detail-actions { + display: grid; + gap: 10px; + padding: 16px; + border-radius: var(--nebula-radius-lg); + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.library-detail-button { + padding: 12px 16px; + border-radius: var(--nebula-radius-md); + font-weight: 900; + font-size: 14px; + text-align: left; +} + +.library-detail-button.is-focused { + border-color: var(--nebula-color-accent); + box-shadow: + 0 0 0 2px rgba(79, 216, 255, 0.14), + 0 0 26px rgba(79, 216, 255, 0.34); + transform: scale(1.03) translateZ(0); +} + +/* Scrollbar styling for content area */ +.game-detail-content::-webkit-scrollbar { + width: 8px; +} + +.game-detail-content::-webkit-scrollbar-track { + background: transparent; +} + +.game-detail-content::-webkit-scrollbar-thumb { + background: rgba(79, 216, 255, 0.24); + border-radius: 4px; +} + +.game-detail-content::-webkit-scrollbar-thumb:hover { + background: rgba(79, 216, 255, 0.36); +} + +/* Update the library-details-panel for full-screen mode */ +.library-details-panel.is-open { + display: flex; +} + +.game-detail-view { + z-index: 31; +} diff --git a/src/views/library/library.js b/src/views/library/library.js index eb8101d..5871f75 100644 --- a/src/views/library/library.js +++ b/src/views/library/library.js @@ -158,7 +158,7 @@ export const createLibraryView = ({ state, renderView }) => { if (action === "card") { runtime.focusedId = element.dataset.itemId; - return launchFocusedItem(); + return openDetails(); } if (action === "launch") return launchFocusedItem(); diff --git a/src/views/library/libraryComponents.js b/src/views/library/libraryComponents.js index 2724544..3f7ccf1 100644 --- a/src/views/library/libraryComponents.js +++ b/src/views/library/libraryComponents.js @@ -255,45 +255,161 @@ export const renderGrid = (items, focusedId) => { export const renderDetailsPanel = (item) => { if (!item) return ""; + const achievements = item.achievementsSupported ? `${item.achievementsUnlocked ?? 0} / ${item.achievementsTotal ?? "?"}` : "Not supported"; + + const screenshotsHtml = item.screenshots && item.screenshots.length > 0 + ? ` +
+

Screenshots

+ +
+ ` + : ''; + + const achievementsHtml = item.achievementsSupported + ? ` +
+

Achievements

+
+
+
+ ${item.achievementsUnlocked ?? 0} + Unlocked +
+
+
+
+
+ ${item.achievementsTotal ?? "?"} + Total +
+
+
+
+ ` + : ''; + + const featuresHtml = ` +
+

Features

+
+ ${item.supportsController ? 'Controller Support' : ''} + ${item.steamDeckVerified ? 'Steam Deck Verified' : ''} + ${item.multiplayer ? 'Multiplayer' : ''} + ${item.coOp ? 'Co-op' : ''} +
+
+ `; return `
-
-
+
- ${escapeHtml(initialsForTitle(item.title))} +
-
-

${escapeHtml(providerLabel(item.source))} · ${escapeHtml(typeLabel(item.type))}

-

${escapeHtml(item.title)}

-

${escapeHtml(item.description)}

-
-
Install location
${escapeHtml(item.installPath || "Not installed")}
-
Last played
${escapeHtml(formatShortDate(item.lastPlayed))}
-
Playtime
${escapeHtml(formatPlaytime(item.playtimeMinutes))}
-
Achievements
${escapeHtml(achievements)}
-
-
- ${["Launch", "Install", "Uninstall", "Hide", "Open Folder"] - .map( - (label, index) => ` - - `, - ) - .join("")} + +
+
+
+

${escapeHtml(providerLabel(item.source))} · ${escapeHtml(typeLabel(item.type))}

+

${escapeHtml(item.title)}

+
+ ${item.installed ? '✓ Installed' : 'Not installed'} + ${item.lastPlayed ? `Last played: ${escapeHtml(formatShortDate(item.lastPlayed))}` : ''} + ${item.playtimeMinutes > 0 ? `${escapeHtml(formatPlaytime(item.playtimeMinutes))}` : ''} +
+
+ +
+ +
+
+ +
+
+
+

About

+

${escapeHtml(item.description)}

+ ${item.longDescription ? `

${escapeHtml(item.longDescription)}

` : ''} +
+ +
+

Details

+
+
+
Genres
+
${escapeHtml(item.genre.join(', '))}
+
+
+
Source
+
${escapeHtml(providerLabel(item.source))}
+
+ ${item.installed ? `
+
Install Location
+
${escapeHtml(item.installPath)}
+
` : ''} + ${item.installedAt ? `
+
Installed
+
${escapeHtml(formatShortDate(item.installedAt))}
+
` : ''} + ${item.playtimeMinutes > 0 ? `
+
Playtime
+
${escapeHtml(formatPlaytime(item.playtimeMinutes))}
+
` : ''} +
+
+ + ${screenshotsHtml} +
+ +
+ ${featuresHtml} + ${achievementsHtml} + +
+ ${["Hide", "Open Folder"] + .filter(label => { + if (label === "Open Folder" && !item.installed) return false; + return true; + }) + .map( + (label, index) => ` + + `, + ) + .join("")} +
+
diff --git a/src/views/library/libraryModel.js b/src/views/library/libraryModel.js index af9bee4..c0a138c 100644 --- a/src/views/library/libraryModel.js +++ b/src/views/library/libraryModel.js @@ -99,6 +99,7 @@ export const normalizeLibraryItem = (raw, index = 0, convertFileSrc = null) => { const source = sourceFromBackend(raw?.platformSource ?? raw?.source); const type = typeFromBackend(raw?.appKind ?? raw?.type); const convert = (path) => (path && convertFileSrc ? convertFileSrc(path) : path || null); + const convertArray = (paths) => (Array.isArray(paths) ? paths.map(convert).filter(Boolean) : []); const installed = raw?.installed ?? Boolean(raw?.installPath || raw?.install_path); return { @@ -120,9 +121,11 @@ export const normalizeLibraryItem = (raw, index = 0, convertFileSrc = null) => { coverImage: convert(raw?.coverImage ?? raw?.cover_image), bannerImage: convert(raw?.bannerImage ?? raw?.heroImage ?? raw?.hero_image), iconImage: convert(raw?.iconImage ?? raw?.icon_image), + screenshots: convertArray(raw?.screenshots ?? []), description: raw?.description || "Scanned from your local library. Metadata can be enriched later by Steam, GOG, Epic, emulator, and local metadata providers.", + longDescription: raw?.longDescription ?? null, lastPlayed: raw?.lastPlayed ?? null, installedAt: raw?.installedAt ?? raw?.createdAt ?? null, playtimeMinutes: Number(raw?.playtimeMinutes ?? 0), @@ -150,6 +153,7 @@ export const createMockLibraryItems = () => installPath: "C:/Games/Starfall Protocol", executablePath: "C:/Games/Starfall Protocol/starfall.exe", description: "Pilot a relic fighter through collapsing gates in a neon campaign built for controller play.", + longDescription: "Experience a fast-paced action-RPG where you pilot experimental fighter craft through a collapsing interdimensional gateway. Built from the ground up with controller support, Starfall Protocol features real-time combat, intricate level design, and a gripping sci-fi narrative. Perfect for couch co-op sessions or solo playthroughs.", lastPlayed: "2026-05-15T21:15:00Z", installedAt: "2026-05-01T08:30:00Z", playtimeMinutes: 1874, @@ -160,6 +164,11 @@ export const createMockLibraryItems = () => achievementsTotal: 54, multiplayer: true, coOp: true, + screenshots: [ + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%234fd8ff' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3EStarfall Protocol - Screenshot 1%3C/text%3E%3C/svg%3E", + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%239d4fe0' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3EStarfall Protocol - Screenshot 2%3C/text%3E%3C/svg%3E", + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%231f7aff' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3EStarfall Protocol - Screenshot 3%3C/text%3E%3C/svg%3E", + ], accent: "#4fd8ff", }, { @@ -172,6 +181,7 @@ export const createMockLibraryItems = () => installPath: "D:/Games/Iron Vault Tactics", executablePath: "D:/Games/Iron Vault Tactics/ivt.exe", description: "A turn-based tactics sandbox with long-form campaigns, mod support, and couch co-op skirmishes.", + longDescription: "Command your squad through dynamic turn-based tactical battles in Iron Vault Tactics. Features mod support, deep unit customization, engaging campaign narratives, and intense couch co-op multiplayer. Each decision matters in this tactical masterpiece.", lastPlayed: "2026-05-10T11:00:00Z", installedAt: "2026-04-18T18:00:00Z", playtimeMinutes: 942, @@ -179,6 +189,11 @@ export const createMockLibraryItems = () => achievementsSupported: false, multiplayer: true, coOp: true, + screenshots: [ + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%2339ffd2' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3EIron Vault Tactics - Screenshot 1%3C/text%3E%3C/svg%3E", + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%234fd8ff' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3EIron Vault Tactics - Screenshot 2%3C/text%3E%3C/svg%3E", + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%239d4fe0' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3EIron Vault Tactics - Screenshot 3%3C/text%3E%3C/svg%3E", + ], accent: "#39ffd2", }, { @@ -191,6 +206,7 @@ export const createMockLibraryItems = () => installPath: "C:/Program Files/Nebula Paint", executablePath: "C:/Program Files/Nebula Paint/paint.exe", description: "A TV-friendly concept art tool for quick capture, markup, and launcher artwork editing.", + longDescription: "Nebula Paint Studio is a professional-grade digital painting application optimized for controller input and TV display. Create stunning concept art, edit game launcher artwork, and collaborate seamlessly with frame-perfect precision.", lastPlayed: "2026-05-11T06:20:00Z", installedAt: "2026-03-12T09:00:00Z", playtimeMinutes: 223, @@ -198,6 +214,10 @@ export const createMockLibraryItems = () => achievementsSupported: false, multiplayer: false, coOp: false, + screenshots: [ + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%239d4fe0' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3ENebula Paint Studio - Screenshot 1%3C/text%3E%3C/svg%3E", + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%234fd8ff' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3ENebula Paint Studio - Screenshot 2%3C/text%3E%3C/svg%3E", + ], accent: "#9d4fe0", }, { @@ -210,6 +230,7 @@ export const createMockLibraryItems = () => installPath: null, executablePath: null, description: "Wishlist entry from a linked store. Install support will be routed through the Epic integration later.", + longDescription: "Emberline is an indie action game with stunning visuals and engaging gameplay. Add it to your library to stay updated on new releases, sales, and updates.", lastPlayed: null, installedAt: null, playtimeMinutes: 0, @@ -220,6 +241,10 @@ export const createMockLibraryItems = () => achievementsTotal: 32, multiplayer: false, coOp: false, + screenshots: [ + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%231f7aff' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3EEmberline - Screenshot 1%3C/text%3E%3C/svg%3E", + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%239d4fe0' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3EEmberline - Screenshot 2%3C/text%3E%3C/svg%3E", + ], accent: "#1f7aff", }, { @@ -232,6 +257,7 @@ export const createMockLibraryItems = () => installPath: "D:/Emulation/RetroCore", executablePath: "D:/Emulation/RetroCore/retrocore.exe", description: "A controller-native emulator hub prepared for future ROM library scanning and save sync.", + longDescription: "RetroCore Station is your gateway to classic gaming. Play thousands of retro games with full controller support, save synchronization, and achievement tracking across multiple emulation systems.", lastPlayed: "2026-05-14T04:10:00Z", installedAt: "2026-05-03T15:10:00Z", playtimeMinutes: 517, @@ -241,6 +267,11 @@ export const createMockLibraryItems = () => achievementsTotal: 210, multiplayer: true, coOp: true, + screenshots: [ + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%23ffb84f' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3ERetroCore Station - Screenshot 1%3C/text%3E%3C/svg%3E", + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%234fd8ff' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3ERetroCore Station - Screenshot 2%3C/text%3E%3C/svg%3E", + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='540'%3E%3Crect fill='%239d4fe0' width='960' height='540'/%3E%3Ctext x='480' y='270' font-size='24' fill='white' text-anchor='middle' dominant-baseline='middle'%3ERetroCore Station - Screenshot 3%3C/text%3E%3C/svg%3E", + ], accent: "#ffb84f", }, { @@ -253,6 +284,7 @@ export const createMockLibraryItems = () => installPath: null, executablePath: null, description: "Tooling placeholder for future mod SDK detection, dependency checks, and per-game utilities.", + longDescription: "Orbit Mod Tools provides a comprehensive suite of utilities for game modding, including SDK management, dependency resolution, and per-game optimization tools. Essential for serious mod creators.", lastPlayed: null, installedAt: null, playtimeMinutes: 0, @@ -260,6 +292,7 @@ export const createMockLibraryItems = () => achievementsSupported: false, multiplayer: false, coOp: false, + screenshots: [], accent: "#ff6b9a", }, ].map((item, index) => normalizeLibraryItem(item, index));