diff --git a/src/api/client.ts b/src/api/client.ts index ed0df15..16c6fa0 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -33,6 +33,8 @@ export interface DetailedGame extends Game { favorite?: boolean; manualUrl?: string; platformId?: number; + videoUrl?: string; // Direct video link + youtubeId?: string; // YouTube ID for embed } export interface RommCollection { @@ -309,7 +311,6 @@ export const rommApiClient = { genres: getAll('genres').map(sanitize).filter(Boolean), franchises: getAll('franchises').map(sanitize).filter(Boolean), releaseDate: formatDate(json.release_date || getFirst('first_release_date') || getFirst('release_date')), - screenshots: (json.screenshots || []).map((s: any) => s.url ? getFullImageUrl(s.url) || '' : ''), fsName: json.fs_name, regions: regions.length > 0 ? regions : ['Global'], players: (() => { @@ -338,6 +339,13 @@ export const rommApiClient = { favorite: (json.user_collections || json.rom_collections || []).some((c: any) => c.is_favorite || c.name === 'Favorites'), manualUrl: getFullImageUrl(json.url_manual), platformId: json.platform_id, + videoUrl: getFullImageUrl(json.url_video || json.ss_metadata?.video_url), + youtubeId: json.youtube_video_id || json.igdb_metadata?.youtube_video_id || json.launchbox_metadata?.youtube_video_id, + screenshots: Array.from(new Set([ + ...(json.merged_screenshots || []).map((s: string) => getFullImageUrl(s)), + ...(json.screenshots || []).map((s: any) => s.url ? getFullImageUrl(s.url) || '' : ''), + ...(getFirst('screenshots') || []) + ])).filter(Boolean) as string[], }; }, diff --git a/src/components/GamesPage.tsx b/src/components/GamesPage.tsx index 497ea3f..33726bd 100644 --- a/src/components/GamesPage.tsx +++ b/src/components/GamesPage.tsx @@ -154,6 +154,7 @@ export const GamesPage = () => { const [playingGameId, setPlayingGameId] = useState(null); const [isCollectionModalOpen, setIsCollectionModalOpen] = useState(false); const [isManualModalOpen, setIsManualModalOpen] = useState(false); + const [activeMediaIndex, setActiveMediaIndex] = useState(0); const [activeZone, setActiveZone] = useState<'platforms' | 'games' | 'alphabet' | 'details' | null>(null); const detailTimeoutRef = React.useRef(null); @@ -308,6 +309,29 @@ export const GamesPage = () => { } }; + // Consolidate media for Galleria + const mediaItems = React.useMemo(() => { + if (!detailsQuery.data) return []; + const items: { type: 'video' | 'image', url: string, youtubeId?: string }[] = []; + + if (detailsQuery.data.youtubeId) { + items.push({ type: 'video', url: '', youtubeId: detailsQuery.data.youtubeId }); + } else if (detailsQuery.data.videoUrl) { + items.push({ type: 'video', url: detailsQuery.data.videoUrl }); + } + + if (detailsQuery.data.screenshots) { + detailsQuery.data.screenshots.forEach(s => { + items.push({ type: 'image', url: s }); + }); + } + return items; + }, [detailsQuery.data]); + + useEffect(() => { + setActiveMediaIndex(0); + }, [selectedGameId]); + const updateFocusFromScroll = (container: HTMLElement) => { const rect = container.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; @@ -660,6 +684,77 @@ export const GamesPage = () => { + + {/* Media Galleria Container */} + {mediaItems.length > 0 && ( +
+
+ {/* Active Media Slot */} +
+ {mediaItems[activeMediaIndex].type === 'video' ? ( + mediaItems[activeMediaIndex].youtubeId ? ( + + ) : ( +
+ + {/* Thumbnail Row */} +
+ {mediaItems.map((item, idx) => ( + { + if (mode === 'gamepad') setActiveZone('details'); + setActiveMediaIndex(idx); + }} + onClick={() => setActiveMediaIndex(idx)} + className="shrink-0" + focusKey={`THUMB_${idx}`} + > + {(focused) => ( +
+ {item.type === 'video' ? ( +
+ play_circle +
+ ) : ( + + )} + {activeMediaIndex === idx && ( +
+ )} +
+ )} +
+ ))} +
+
+ + {/* Placeholder for the "something else" mentioned by user */} +
+
+ Future Content Zone +
+
+
+ )} ) : selectedGameId ? (