import { createFileRoute, ErrorComponentProps } from "@tanstack/react-router";
import { RPC_URL } from "@shared/constants";
import { useEffect, useRef, useState } from "react";
import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { Calendar, Clock, Folder, Gamepad2, Image, Info, Store, TriangleAlert, Trophy } from "lucide-react";
import { HeaderUI } from "../../components/Header";
import { AnimatedBackground } from "../../components/AnimatedBackground";
import { useQuery } from "@tanstack/react-query";
import { Router } from "../..";
import Shortcuts from "../../components/Shortcuts";
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
import Screenshots from "@/mainview/components/Screenshots";
import { HandleGoBack, scrollIntoViewHandler, useStickyDataAttr } from "@/mainview/scripts/utils";
import { FilterUI } from "@/mainview/components/Filters";
import StatList, { StatEntry } from "@/mainview/components/StatList";
import { useIntersectionObserver, useLocalStorage } from "usehooks-ts";
import { EmulatorsSection } from "@/mainview/components/store/EmulatorsSection";
import { zodValidator } from "@tanstack/zod-adapter";
import z from "zod";
import Achievements from "@/mainview/components/game/Achievements";
import { GameDetailsContext } from "@/mainview/scripts/contexts";
import { gameQuery, gamesRecommendedBasedOnGameQuery } from "@queries/romm";
import { GamesSection } from "@/mainview/components/store/GamesSection";
import Details, { DetailElement } from "@/mainview/components/game/Details";
export const Route = createFileRoute("/game/$source/$id")({
loader: async ({ params, context }) =>
{
context.queryClient.prefetchQuery(gameQuery(params.source, params.id));
},
component: RouteComponent,
pendingComponent: GameDetailsUIPending,
errorComponent: Error,
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
});
function useDetailsSection ()
{
return useLocalStorage('details-section', 'screenshots');
}
function Error (data: ErrorComponentProps)
{
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details-error", preferredChildFocusKey: "main-details" });
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
const { shortcuts } = useShortcutContext();
useEffect(() =>
{
focusSelf();
}, []);
return
{JSON.stringify(data.error, null, 3)}
;
}
function MainDetailsPending ()
{
const { ref } = useFocusable({ focusKey: "main-details" });
return
;
}
function GameDetailsUIPending ()
{
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details-error", preferredChildFocusKey: "main-details" });
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
const { shortcuts } = useShortcutContext();
useEffect(() =>
{
focusSelf();
}, []);
return
{Array.from({ length: 5 }).map((s, i) =>
)}
;
}
function MoreDetails (data: { game: FrontEndGameTypeDetailed | undefined; })
{
const [details] = useDetailsSection();
const { ref, focusKey, hasFocusedChild } = useFocusable({
focusKey: "game-more-details-section",
onFocus: (l, p, d) => scrollIntoViewHandler({ block: 'start', behavior: 'smooth' })(focusKey, ref.current, d),
trackChildren: true
});
return
{details === 'screenshots' && !!data.game &&
}
{details === 'stats' &&
}
{details === 'achievements' && !!data.game &&
}
;
}
function Stats (data: { game: FrontEndGameTypeDetailed | undefined; })
{
const stats: StatEntry[] = [];
if (data.game)
{
if (data.game.path_fs)
stats.push({ label: "Location", content: data.game.path_fs, icon: });
if (data.game.companies)
stats.push({ label: "Companies", content: data.game.companies });
if (data.game.genres)
stats.push({ label: 'Genres', content: data.game.genres });
if (data.game.release_date)
stats.push({ label: "Release Date", content: data.game.release_date.toLocaleDateString(), icon: });
if (data.game.emulators)
stats.push({ label: "Emulators", content: data.game.emulators.map(e => e.name) });
}
return ;
}
function Divider (data: { rootFocusKey: string; showShortcuts: boolean; game: FrontEndGameTypeDetailed | undefined; })
{
const [details, setDetails] = useDetailsSection();
const { ref, focusKey } = useFocusable({
focusKey: "details-divider",
onFocus: (l, p, d) => scrollIntoViewHandler({ block: 'nearest', behavior: 'smooth' })(focusKey, ref.current, d),
});
const detailFilter: Record = {
stats: { label: "Stats", selected: details === 'stats', icon: },
screenshots: { label: "Screenshots", selected: details === 'screenshots', icon: },
};
if (data.game?.achievements)
{
detailFilter.achievements = { label: "Achievements", selected: details === 'achievements', icon: };
}
return
;
}
function RouteComponent ()
{
const [recommendedGamesVisible, setRecommendedGamesVisible] = useState(false);
const { source, id } = Route.useParams();
const { data } = useQuery(gameQuery(source, id));
const { focus } = Route.useSearch();
const [, setUpdate] = useState(0);
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details" });
const headerRef = useRef(null);
const sentinelRef = useRef(null);
const backgroundImage = data ? new URL(`${RPC_URL(__HOST__)}${data.path_cover}`) : undefined;
const { data: recommendedGames } = useQuery({ ...gamesRecommendedBasedOnGameQuery(data?.id.source ?? source, data?.id.id ?? id), enabled: !!data && recommendedGamesVisible });
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
const { shortcuts } = useShortcutContext();
useEffect(() =>
{
if (focus)
{
setFocus(focus, { instant: true });
} else
{
focusSelf();
}
}, []);
useStickyDataAttr(headerRef, sentinelRef, ref);
const recommendedEmulators = data?.emulators?.filter(e => e.validSource);
const { ref: intersct } = useIntersectionObserver({
onChange: (isIntersecting, entry) =>
{
setRecommendedGamesVisible(isIntersecting);
}
});
return (
setUpdate(v => v + 1)
}} >
{!!recommendedEmulators && recommendedEmulators.length > 0 &&
Related Emulators
>}
onFocus={scrollIntoViewHandler({ block: 'center' })}
onSelect={(id, focus) =>
{
Router.navigate({ to: '/store/details/emulator/$id', params: { id } });
}}
emulators={recommendedEmulators} />}
{
Router.navigate({ to: '/game/$source/$id', params: { id: id.id, source: id.source } });
}} onFocus={scrollIntoViewHandler({ block: 'center', inline: 'nearest' })} games={recommendedGames} />
);
}