feat: Implemented launching and downloading of roms

This is just an initial implementation lots of kings to iron out
This commit is contained in:
Simeon Radivoev 2026-02-19 16:10:29 +02:00
parent ef08fa6114
commit f15bf9a1e0
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
117 changed files with 37776 additions and 1073 deletions

View file

@ -1,15 +1,15 @@
import { keepPreviousData, useQuery, useSuspenseQuery } from "@tanstack/react-query";
import { getRomsApiRomsGetOptions } from "../../clients/romm/@tanstack/react-query.gen";
import { useSuspenseQuery } from "@tanstack/react-query";
import { GameMetaExtra, CardList } from "./CardList";
import { DefaultRommStaleTime, RPC_URL } from "../../shared/constants";
import { FrontEndId, RPC_URL } from "../../shared/constants";
import { useLocation, useNavigate } from "@tanstack/react-router";
import { Suspense, useEffect } from "react";
import { SaveSource } from "../scripts/spatialNavigation";
import { gamesQueryOptions } from "../query-options";
import { rommApi } from "../scripts/clientApi";
import { HardDrive } from "lucide-react";
import { JSX } from "react";
export interface GameListFilter
{
platformIds?: number[];
platformId?: number;
collectionId?: number;
}
@ -19,30 +19,39 @@ export interface GameListParams
filters?: GameListFilter,
grid?: boolean,
setBackground?: (url: string) => void;
onGameSelect?: (id: number) => void;
onGameSelect?: (id: FrontEndId) => void;
className?: string;
}
export function GameList (data: GameListParams)
{
const games = useSuspenseQuery(gamesQueryOptions(data.filters));
const games = useSuspenseQuery({
queryKey: ['games', data.filters ?? 'all'],
queryFn: () => rommApi.api.romm.games.get({
query: {
platform_id: data.filters?.platformId,
collection_id: data.filters?.collectionId
}
}).then(d => d.data)
});
const navigator = useNavigate();
const location = useLocation();
const handleFocus = (id: number) =>
const handleFocus = (id: FrontEndId) =>
{
const game = games.data?.items.find((g) => g.id === id);
const game = games.data?.games.find((g) => g.id === id);
if (game)
{
data.setBackground?.(
`${RPC_URL(__HOST__)}/api/romm${game.path_cover_small}`,
`${RPC_URL(__HOST__)}${game.path_cover}`,
);
}
};
function handleDefaultSelect (id: number)
function handleDefaultSelect (id: FrontEndId, source: string | null, sourceId: number | null)
{
SaveSource('details', location.pathname);
navigator({ to: '/game/$id', params: { id: String(id) }, viewTransition: { types: ['zoom-in'] } });
SaveSource('details');
navigator({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source }, viewTransition: { types: ['zoom-in'] } });
};
return (
@ -51,23 +60,34 @@ export function GameList (data: GameListParams)
id={data.id}
type="game"
grid={data.grid}
games={games.data.items.sort(
(a, b) =>
Date.parse(b.rom_user.last_played ?? b.updated_at) -
Date.parse(a.rom_user.last_played ?? a.updated_at),
)
className={data.className}
games={games.data?.games
.map(
(g) =>
({
id: g.id,
{
const badges: JSX.Element[] = [];
if (g.id.source === 'local')
{
badges.push(<HardDrive className="size-8 m-1" />);
}
return {
id: `game-${g.id.source}-${g.id.id}`,
focusKey: g.slug ?? `game-${g.id}`,
title: g.name ?? "",
subtitle: g.platform_display_name ?? "",
previewUrl: `${RPC_URL(__HOST__)}/api/romm${g.path_cover_large}`,
}) satisfies GameMetaExtra,
)}
onGameFocus={handleFocus}
onSelectGame={id => data.onGameSelect ? data.onGameSelect(id) : handleDefaultSelect(id)}
subtitle: (
<div className="flex gap-1 items-center">
{!!g.path_platform_cover && <img className="size-4" src={`${RPC_URL(__HOST__)}${g.path_platform_cover}`} />}
<p className="opacity-80">{g.platform_display_name}</p>
</div>
),
previewUrl: `${RPC_URL(__HOST__)}${g.path_cover}`,
badges: badges,
onSelect: () => data.onGameSelect ? data.onGameSelect(g.id) : handleDefaultSelect(g.id, g.source, g.source_id),
onFocus: () => handleFocus(g.id)
} satisfies GameMetaExtra;
},
) ?? []}
/>
</>
);