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));