feat: Implemented emulator installation

feat: Updated romm API version
feat: Updated es-de rules
feat: Added tabs to game details
refactor: returned to global query definitions to help with typescript performance
This commit is contained in:
Simeon Radivoev 2026-03-22 01:11:21 +02:00
parent cf6fff6fac
commit 3750e9ed8f
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
103 changed files with 4888 additions and 1632 deletions

View file

@ -69,7 +69,7 @@ export function EmulatorsSection (data: {
scrollIntoNearestParent(node, { behavior: details.instant ? 'instant' : 'smooth' });
}} />
)) ?? Array.from({ length: 8 }).map((_, i) => <div key={i} className="skeleton h-38 w-full rounded-4xl" />)}
<SeeAllCard id={`${FOCUS_KEYS.EMULATOR_SECTION}-see-all`} onAction={() => Router.navigate({ to: '/store/tab/emulators' })} onFocus={({ node, instant }) => scrollIntoNearestParent(node, { behavior: instant ? 'instant' : 'smooth' })} />
<SeeAllCard id={`${FOCUS_KEYS.EMULATOR_SECTION}-see-all`} onAction={() => Router.navigate({ to: '/store/tab/emulators', viewTransition: { types: ['zoom-in'] } })} onFocus={({ node, instant }) => scrollIntoNearestParent(node, { behavior: instant ? 'instant' : 'smooth' })} />
</Carousel>
</section>

View file

@ -1,50 +1,58 @@
import { useRef } from "react";
import { CSSProperties, Ref, RefObject, useEffect, useRef } from "react";
import
{
useFocusable,
FocusContext,
} from "@noriginmedia/norigin-spatial-navigation";
import { Gamepad2, Star } from "lucide-react";
import { useDragScroll } from "@/mainview/scripts/utils";
import { scrollIntoNearestParent, useDragScroll } from "@/mainview/scripts/utils";
import FocusDots from "../FocusDots";
import { FrontEndGameType, FrontEndId } from "@/shared/constants";
import FrontEndGameCard from "../FrontEndGameCard";
import { FOCUS_KEYS } from "@/mainview/scripts/types";
import Carousel from "../Carousel";
import { twMerge } from "tailwind-merge";
export function GamesSection ({ games, onSelect, onFocus }: {
export function GamesSection (data: {
games?: FrontEndGameType[];
onSelect?: (id: FrontEndId, focusKey: string) => void;
className?: string;
showSources?: boolean;
ref?: Ref<any>;
} & FocusParams)
{
const { ref, focusKey } = useFocusable({
const { ref, focusKey, focused, focusSelf } = useFocusable({
focusKey: FOCUS_KEYS.GAME_SECTION,
trackChildren: true,
onFocus: (_l, _p, details) => onFocus?.(focusKey, ref.current, details)
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details)
});
const containerRef = useRef(null);
useDragScroll(containerRef);
useEffect(() =>
{
if (focused)
focusSelf();
}, [!!data.games]);
return (
<FocusContext.Provider value={focusKey}>
<section ref={ref} className="px-6 py-3 select-none">
<div className="flex items-center gap-3 mb-4">
<div className="w-2 h-5 rounded-full bg-accent shadow-sm shadow-error/40" />
<Gamepad2 className="text-accent" />
<h2 className="font-bold uppercase tracking-widest text-accent grow">
Featured Games
</h2>
<div className="flex gap-2 bg-accent text-accent-content rounded-full py-1 px-4 font-semibold opacity-80"><Star />Creator Picks</div>
</div>
<section ref={(r) =>
{
ref.current = r;
if (data.ref instanceof Function) data.ref(r);
else if (data.ref) data.ref.current = r;
}} className={twMerge("select-none", data.className)}>
<Carousel controlsClassName="z-20" scrollRef={containerRef} className="flex *:w-[18rem] *:min-w-[18rem] *:h-[21rem] overflow-y-hidden overflow-x-auto hide-scrollbar p-4 gap-4 justify-center-safe">
{games?.map((g, i) => <FrontEndGameCard
{data.games?.map((g, i) => <FrontEndGameCard
showSource={data.showSources}
key={g.id.id}
game={g}
onAction={() => onSelect?.(g.id, FOCUS_KEYS.GAME_CARD(g.id.id))}
onAction={() => data.onSelect?.(g.id, FOCUS_KEYS.GAME_CARD(g.id))}
onFocus={(key, node, details) => scrollIntoNearestParent(node, { behavior: details.instant ? 'instant' : 'smooth' })}
index={i} />) ?? Array.from({ length: 8 }).map((_, i) => <div key={i} className="skeleton h-38 w-full" />)}
</Carousel>
</section>
<FocusDots elements={games?.map(e => FOCUS_KEYS.GAME_CARD(e.id.id)) ?? []} />
<FocusDots elements={data.games?.map(e => FOCUS_KEYS.GAME_CARD(e.id)) ?? []} />
</FocusContext.Provider>
);
}

View file

@ -1,5 +1,6 @@
import queries from "@/mainview/scripts/queries";
import { storeGetStatsQuery } from "@queries/store";
import { useQuery } from "@tanstack/react-query";
import { Joystick, LibraryBig, Save, TriangleAlert } from "lucide-react";
@ -15,7 +16,7 @@ export function StatsSection ({
}: StatsSectionProps)
{
const { data: stats } = useQuery(queries.store.storeGetStatsQuery);
const { data: stats } = useQuery(storeGetStatsQuery);
return (
<section className="px-6 pt-3 pb-4">

View file

@ -5,8 +5,18 @@ import { Button } from "../options/Button";
import useActiveControl from "@/mainview/scripts/gamepads";
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
import { ChevronRight, EllipsisVertical, HardDrive } from "lucide-react";
import { ChevronRight, EllipsisVertical, FileQuestion, IceCream2, Package, Store } from "lucide-react";
import { FOCUS_KEYS } from "@/mainview/scripts/types";
import { FlatpackIcon } from "@/mainview/scripts/brandIcons";
import { JSX } from "react";
export const emulatorStatusIcons: Record<string, JSX.Element> = {
store: <Store />,
custom: <FileQuestion />,
flatpak: FlatpackIcon,
winget: <Package />,
scoop: <IceCream2 />
};
export function StoreEmulatorCard (data: {
id: string;
@ -35,7 +45,7 @@ export function StoreEmulatorCard (data: {
ref={ref}
role="button"
tabIndex={0}
data-installed={data.emulator.exists ? true : undefined}
data-installed={!!data.emulator.validSource}
onClick={isTouch ? handleSelect : undefined}
className={twMerge("relative focusable focusable-info bg-base-100 rounded-4xl transition-shadow focused:not-control-mouse:animate-scale-small shadow-lg border border-base-content/10 active:ring-4 active:ring-base-content active:transition-none", data.className)}
>
@ -44,14 +54,14 @@ export function StoreEmulatorCard (data: {
<div className="flex gap-2">
<div className="flex items-start">
<div
data-installed={data.emulator.exists}
data-installed={!!data.emulator.validSource}
className={`size-14 p-2 rounded-full bg-info flex items-center justify-center text-xl shadow-lg data-[installed=true]:bg-success`}
>
<img draggable={false} src={data.emulator.logo}></img>
</div>
</div>
<div>
<p data-installed={data.emulator.exists} className="font-bold text-base-content text-xl leading-snug data-[installed=true]:text-success">{data.emulator.name}</p>
<p data-installed={!!data.emulator.validSource} className="font-bold text-base-content text-xl leading-snug data-[installed=true]:text-success">{data.emulator.name}</p>
<ul className="flex flex-wrap gap-1">
{data.emulator.systems.map(({ id, name, icon }) =>
{
@ -66,10 +76,12 @@ export function StoreEmulatorCard (data: {
</div>
<div className="flex gap-0.5 mt-1 h-10 items-center">
{data.emulator.exists && <div className="tooltip" data-tip="Installed">
<div className="flex items-center justify-center rounded-full p-1 size-8 bg-success text-success-content"><HardDrive /></div>
{!!data.emulator.validSource && <div className="tooltip" data-tip={data.emulator.validSource.type}>
<div className="flex items-center justify-center rounded-full p-1 size-8 bg-success text-success-content">
{emulatorStatusIcons[data.emulator.validSource?.type ?? '']}
</div>
</div>}
{<div className="tooltip" data-tip="Game Count">
{data.emulator.gameCount > 0 && <div className="tooltip" data-tip="Game Count">
<div className="flex items-center justify-center rounded-full font-semibold size-9 p-2 bg-base-200 text-base-content/40">{data.emulator.gameCount}</div>
</div>}
{isMouse && <>