feat: Implemented download page for downloading roms from various sources using plugins. Added support for internet archive external plugin. feat: Added tasks page to track running tasks/downloads feat: Added tanstack caching feat: Added quick play action Fixes #6 feat: Added quick emulator launch action fix: Made task queue only support 1 task per group and task ID should now be unique
125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
import
|
|
{
|
|
FocusContext,
|
|
useFocusable,
|
|
} from "@noriginmedia/norigin-spatial-navigation";
|
|
import CardElement, { GameCardParams } from "./CardElement";
|
|
import { JSX } from "react";
|
|
import { twMerge } from "tailwind-merge";
|
|
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
|
|
import { oneShot } from "../scripts/audio/audio";
|
|
|
|
export interface GameMetaExtra extends GameMeta
|
|
{
|
|
preview?: GameCardParams['preview'];
|
|
badges?: JSX.Element[];
|
|
focusKey: string;
|
|
}
|
|
|
|
function LocalCardElement (data: { game: GameMetaExtra, i: number; onQuickAction?: (ctx: InteractParamsArgs) => void; } & FocusParams & InteractParams)
|
|
{
|
|
let preview: GameCardParams['preview'] = data.game.preview;
|
|
if (!preview && data.game.previewUrls)
|
|
{
|
|
preview = data.game.previewUrls;
|
|
}
|
|
|
|
const handleAction = (ctx: InteractParamsArgs) =>
|
|
{
|
|
data.game.onSelect?.();
|
|
data.onAction?.({ event, focusKey: data.game.focusKey });
|
|
oneShot('click');
|
|
};
|
|
|
|
const handleAltAction = (ctx: InteractParamsArgs) =>
|
|
{
|
|
data.game.onQuickAction?.();
|
|
data.onQuickAction?.({ event, focusKey: data.game.focusKey });
|
|
oneShot('click');
|
|
};
|
|
|
|
useShortcuts(data.game.focusKey, () =>
|
|
{
|
|
const options: Shortcut[] = [{
|
|
label: "Details",
|
|
button: GamePadButtonCode.A,
|
|
action: event => handleAction({ event, focusKey: data.game.focusKey })
|
|
}];
|
|
|
|
if (data.onQuickAction || data.game.onQuickAction)
|
|
{
|
|
options.push({ label: "Play", button: GamePadButtonCode.X, action: event => handleAltAction({ event, focusKey: data.game.focusKey }) });
|
|
}
|
|
|
|
return options;
|
|
}, [data.onQuickAction, data.game.onQuickAction, data.game.focusKey]);
|
|
|
|
return (
|
|
<CardElement
|
|
index={data.i}
|
|
focusKey={data.game.focusKey}
|
|
data-index={data.i}
|
|
title={data.game.title}
|
|
subtitle={data.game.subtitle}
|
|
srcset={data.game.previewSrcset}
|
|
onFocus={(focusKey, node, details) =>
|
|
{
|
|
data.game.onFocus?.(focusKey, node, details);
|
|
data.onFocus?.(focusKey, node, { ...details, id: data.game.id });
|
|
}}
|
|
onAction={handleAction}
|
|
preview={preview}
|
|
badges={data.game.badges}
|
|
id={data.game.id}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function CardList (data: {
|
|
id: string;
|
|
type?: string;
|
|
games: GameMetaExtra[];
|
|
grid?: boolean;
|
|
onSelectGame?: (id: string) => void;
|
|
focus?: string;
|
|
className?: string;
|
|
finalElement?: JSX.Element | JSX.Element[];
|
|
saveChildFocus?: 'session' | 'local';
|
|
} & FocusParams)
|
|
{
|
|
const { ref, focusKey } = useFocusable({
|
|
focusKey: data.id,
|
|
focusable: data.games.length > 0 || (!!data.finalElement && (Array.isArray(data.finalElement) ? data.finalElement.length > 0 : !!data.finalElement)),
|
|
preferredChildFocusKey: data.focus
|
|
});
|
|
|
|
return (
|
|
<ul
|
|
title="Games"
|
|
id={`card-list-${data.id}`}
|
|
ref={ref}
|
|
save-child-focus={data.saveChildFocus}
|
|
className={twMerge("items-center justify-center-safe h-full",
|
|
data.grid ? "grid h-fit sm:gap-2 md:gap-5 auto-rows-min grid-cols-[repeat(auto-fill,var(--game-card-width))]" :
|
|
'landscape:grid landscape:grid-flow-col landscape:auto-cols-min auto-rows-[1fr] sm:gap-2 md:gap-4 portrait:grid portrait:auto-rows-min portrait:grid-cols-[repeat(auto-fill,var(--game-card-width))] *:portrait:aspect-8/10 *:landscape:aspect-8/12 sm:landscape:max-h-84 md:max-h-128!',
|
|
data.className
|
|
)}
|
|
onKeyDown={(e) =>
|
|
{
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}}
|
|
>
|
|
<FocusContext.Provider value={focusKey}>
|
|
{data.games.map((g, i) => <LocalCardElement
|
|
key={g.id}
|
|
onFocus={data.onFocus}
|
|
game={g}
|
|
onAction={() => data.onSelectGame?.(g.id)}
|
|
i={i}
|
|
/>)}
|
|
{data.finalElement}
|
|
</FocusContext.Provider>
|
|
</ul>
|
|
);
|
|
}
|