fix: Fixed romm login, now uses token

feat: Moved romm to internal plugin
fix: Made focusing and navigation more reliable
fix: Loading errors on first time launch
This commit is contained in:
Simeon Radivoev 2026-03-28 17:32:51 +02:00
parent 7c10f4e4c2
commit 816d50ae4d
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
81 changed files with 1659 additions and 1097 deletions

View file

@ -11,7 +11,7 @@ import Shortcuts from "@/mainview/components/Shortcuts";
import { AnimatedBackground } from "@/mainview/components/AnimatedBackground";
import { systemApi } from "@/mainview/scripts/clientApi";
import { Button } from "@/mainview/components/options/Button";
import { ChevronDown, Cpu, Download, Gamepad2, Info, Settings, Trash2, TriangleAlert, WandSparkles } from "lucide-react";
import { ChevronDown, Cpu, Download, Gamepad2, Info, Puzzle, Settings, Trash2, TriangleAlert, WandSparkles } from "lucide-react";
import { ContextList, DialogEntry, useContextDialog } from "@/mainview/components/ContextDialog";
import { RPC_URL } from "@/shared/constants";
import Screenshots from "@/mainview/components/Screenshots";
@ -33,7 +33,7 @@ export const Route = createFileRoute('/store/details/emulator/$id')({
async loader (ctx)
{
ctx.context.queryClient.prefetchQuery(storeEmulatorDetailsQuery(ctx.params.id));
ctx.context.queryClient.prefetchQuery(storeEmulatorsRecommendedQuery);
ctx.context.queryClient.prefetchQuery(storeEmulatorsRecommendedQuery(ctx.params.id));
ctx.context.queryClient.prefetchQuery(gamesRecommendedBasedOnEmulatorQuery(ctx.params.id));
}
});
@ -208,7 +208,7 @@ function TitleArea (data: {
}
};
installButtonContent = <><span className="loading loading-spinner loading-lg"></span>{installState ? status.install[installState] : biosDownloadState ? status.bios[biosDownloadState] : undefined}</>;
} else if (data.emulator.validSource)
} else if (data.emulator.validSources.some(s => s.exists))
{
installButtonContent = <><Settings /> Options</>;
} else if (data.emulator.downloads.length > 0)
@ -235,10 +235,10 @@ function TitleArea (data: {
<div className="flex flex-col grow gap-1 sm:portrait:items-center md:items-start">
<h1 className="text-4xl font-semibold text-shadow-md">{data.emulator?.name ?? <div className="skeleton h-10 w-84" />}</h1>
<div className="flex gap-2">
{data.emulator?.systems.map(({ id, name, icon }) =>
{data.emulator?.systems.map(({ id, name, iconUrl }) =>
{
return <div key={id} className="flex gap-1 items-center text-base-content/35 mt-0.5">
{!!icon && <img className="size-6 p-1 bg-base-200 rounded-full" src={`${RPC_URL(__HOST__)}${icon}`} />}
{!!iconUrl && <img className="size-6 p-1 bg-base-200 rounded-full" src={`${RPC_URL(__HOST__)}${iconUrl}`} />}
<p className="text-nowrap text-ellipsis overflow-hidden dark:text-shadow-lg">{name}</p>
</div>;
}) ?? <><div className="skeleton h-4 w-48" /><div className="skeleton h-4 w-32" /></>}
@ -249,7 +249,7 @@ function TitleArea (data: {
{!!data.emulator?.bios?.[0] && <div className="tooltip" data-tip="Has BIOS">
<div className="flex items-center justify-center bg-base-200 p-2 rounded-full"><Cpu className="size-5" /></div>
</div>}
{data.emulator && !!data.emulator.integration && data.emulator.validSource?.type === 'store' && <div className="tooltip" data-tip="Has Integration">
{data.emulator && !!data.emulator.integration && data.emulator.validSources.some(s => s.type === 'store') && <div className="tooltip" data-tip="Has Integration">
<div className="bg-base-200 rounded-full p-2"><WandSparkles className="size-5" /></div>
</div>}
</div>
@ -296,7 +296,7 @@ export function RouteComponent ()
});
const { data: emulator, isPending: isEmulatorPending } = useQuery(storeEmulatorDetailsQuery(id));
const { data: recommendedEmulators } = useQuery(storeEmulatorsRecommendedQuery);
const { data: recommendedEmulators } = useQuery(storeEmulatorsRecommendedQuery(id));
const { data: recommendedGames } = useQuery(gamesRecommendedBasedOnEmulatorQuery(id));
useShortcuts(focusKey, () => [{
@ -323,14 +323,19 @@ export function RouteComponent ()
if (emulator.keywords)
stats.push({ label: "Tags", content: emulator.keywords });
stats.push({ label: "Systems", content: emulator.systems.map(s => s.name) });
stats.push(...emulator.sources.flatMap(s => [{ label: "Source", content: s.type, icon: emulatorStatusIcons[s.type] }, { label: "Location", content: s.binPath }]));
stats.push(...emulator.sources.flatMap(s => [{
label: "Source", content: <div className="flex flex-wrap gap-1 p-1">
<div className="flex gap-1 flex-1">{emulatorStatusIcons[s.type]}{s.type}:</div>
<div className="grow text-base-content/40">{s.binPath}</div>
</div>
}]));
if (emulator.bios)
stats.push({
label: "Bios", content: emulator.bios && emulator.bios.length > 0 ? emulator.bios : <div className="text-warning font-semibold">Missing</div>
});
if (emulator.integration)
{
stats.push({ label: "Integration", content: `${emulator.integration.name} (${emulator.integration.version})` });
stats.push({ label: "Integration", icon: <Puzzle />, content: `${emulator.integration.name} (${emulator.integration.version})` });
}
}

View file

@ -49,7 +49,11 @@ function RouteComponent ()
id={data.name}
key={data.name}
emulator={data}
onFocus={({ node, details }) => { node.scrollIntoView({ behavior: details.instant ? 'instant' : 'smooth', block: 'center' }); }}
onFocus={({ id, node, details }) =>
{
node.scrollIntoView({ behavior: details.instant ? 'instant' : 'smooth', block: 'center' });
storeContext.prefetchDetails('emulator', 'store', id);
}}
onSelect={(id, focus) => storeContext.showDetails('emulator', 'store', id, focus)}
/>
)) ?? Array.from({ length: 10 }).map((_, i) => <div key={i} className="skeleton rounded-3xl" />)}

View file

@ -1,12 +1,13 @@
import { FocusContext, getCurrentFocusKey, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
import { createFileRoute, useSearch } from '@tanstack/react-router';
import { Gamepad2 } from 'lucide-react';
import { useEffect } from 'react';
import { useContext, useEffect } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import FrontEndGameCard from '@/mainview/components/FrontEndGameCard';
import { GetFocusedElement } from '@/mainview/scripts/spatialNavigation';
import LoadMoreButton from '@/mainview/components/LoadMoreButton';
import { storeGamesInfiniteQuery } from '@queries/store';
import { StoreContext } from '@/mainview/scripts/contexts';
export const Route = createFileRoute('/store/tab/games')({
component: RouteComponent
@ -18,6 +19,7 @@ function RouteComponent ()
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "main-area", preferredChildFocusKey: focus });
const { data, fetchNextPage, isFetchingNextPage, isFetching } = useInfiniteQuery(storeGamesInfiniteQuery);
const storeContext = useContext(StoreContext);
useEffect(() =>
{
@ -45,7 +47,11 @@ function RouteComponent ()
</div>
<div className="grid grid-cols-[repeat(auto-fill,18rem)] auto-rows-[21rem] py-2 md:px-4 gap-4 justify-center-safe">
{data?.pages.flatMap((page) => (
page.data.map((g, i) => <FrontEndGameCard onFocus={handleFocus} key={g.id.id} game={g} index={i} />))
page.data.map((g, i) => <FrontEndGameCard onFocus={(k, n, d) =>
{
storeContext.prefetchDetails('game', g.id.source, g.id.id);
handleFocus(k, n, d);
}} key={g.id.id} game={g} index={i} />))
) ?? Array.from({ length: 20 }).map((_, i) => <div key={i} className="flex flex-col gap-4">
<div className="skeleton grow w-full"></div>
<div className="skeleton h-4 w-[80%]"></div>

View file

@ -107,9 +107,9 @@ function Main (data: { games?: FrontEndGameTypeDetailed[]; })
export function RouteComponent ()
{
const { focus } = useSearch({ from: '/store/tab' });
const { data: crucialEmulators, isSuccess } = useQuery({ ...autoEmulatorsQuery, select: (data) => data.filter(e => !e.validSource && e.isCritical) });
const { data: crucialEmulators, isSuccess } = useQuery({ ...autoEmulatorsQuery, select: (data) => data.filter(e => !e.validSources.some(s => s.exists) && e.isCritical) });
const { data: featuredGames } = useQuery(storeFeaturedGamesQuery);
const { data: recommendedEmulators } = useQuery(storeEmulatorsRecommendedQuery);
const { data: recommendedEmulators } = useQuery(storeEmulatorsRecommendedQuery());
const { focusKey, ref, focusSelf } = useFocusable({ focusKey: 'main-area', preferredChildFocusKey: focus ?? "recommended-emulators" });
const storeContext = useContext(StoreContext);

View file

@ -3,9 +3,12 @@ import { FilterUI } from '@/mainview/components/Filters';
import { HeaderUI } from '@/mainview/components/Header';
import Shortcuts from '@/mainview/components/Shortcuts';
import { StoreContext } from '@/mainview/scripts/contexts';
import { gameQuery } from '@/mainview/scripts/queries/romm';
import { storeEmulatorDetailsQuery } from '@/mainview/scripts/queries/store';
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '@/mainview/scripts/shortcuts';
import { mobileCheck, useStickyDataAttr } from '@/mainview/scripts/utils';
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
import { useQueryClient } from '@tanstack/react-query';
import { useMatchRoute } from '@tanstack/react-router';
import { createFileRoute, Outlet } from '@tanstack/react-router';
import { zodValidator } from '@tanstack/zod-adapter';
@ -78,6 +81,7 @@ function RouteComponent ()
preferredChildFocusKey: 'top-area',
forceFocus: true
});
const queryClient = useQueryClient();
const headerRef = useRef(null);
const sentinelRef = useRef(null);
const filters: Record<string, FilterOption> = {
@ -110,11 +114,23 @@ function RouteComponent ()
};
const handlePrefetch = (type: string, source: string, id: string) =>
{
if (type === 'emulator')
{
queryClient.prefetchQuery(storeEmulatorDetailsQuery(id));
}
else if (type === 'game')
{
queryClient.prefetchQuery(gameQuery(source, id));
}
};
const isMobile = mobileCheck();
useStickyDataAttr(headerRef, sentinelRef, ref);
return <div ref={ref} className='overflow-y-scroll w-screen h-screen' >
<StoreContext value={{ showDetails: handleDetails }} >
<StoreContext value={{ showDetails: handleDetails, prefetchDetails: handlePrefetch }} >
<FocusContext.Provider value={focusKey}>
<div className="relative flex flex-col min-h-screen text-base-content z-10" >
<div ref={sentinelRef} className="h-0" />