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

@ -1,6 +1,6 @@
import { AnimatedBackground } from './AnimatedBackground';
import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
import { HeaderUI } from './Header';
import { HeaderUI, StickyHeaderUI } from './Header';
import { GameList } from './GameList';
import { Search, Settings2 } from 'lucide-react';
import { JSX, Suspense, useEffect } from 'react';
@ -9,77 +9,82 @@ import { AutoFocus } from './AutoFocus';
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
import { GameListFilterType } from '@/shared/constants';
import { GameCardFocusHandler } from './CardElement';
import { HandleGoBack } from '../scripts/utils';
import { HandleGoBack, useStickyDataAttr } from '../scripts/utils';
import LoadingCardList from './LoadingCardList';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { gameQuery } from '../scripts/queries/romm';
export interface CollectionsDetailParams
{
id?: string;
setBackground?: (url: string) => void;
filters?: GameListFilterType;
builder?: () => Promise<{ filter?: GameListFilterType, title?: JSX.Element; }>;
headerTitle?: JSX.Element;
title?: JSX.Element;
footer?: JSX.Element;
focus?: string;
countHit?: number;
}
export function CollectionsDetail (data: CollectionsDetailParams)
{
const focusKey = `game-list-${data.id}-${data.filters ? Object.values(data.filters).map(f => String(f)).join(",") : ''}`;
const builtData = useQuery({
queryKey: ['filter', data.id], queryFn: async () =>
{
return data.builder?.() ?? { filter: data.filters, title: data.title };
}
});
const queryClient = useQueryClient();
const focusKey = `game-list-${data.id}-${data?.filters ? Object.values(data?.filters).map(f => String(f)).join(",") : ''}`;
const { ref, focusSelf } = useFocusable({
focusKey,
preferredChildFocusKey: `${focusKey}-list`,
preferredChildFocusKey: `${focusKey}-list`
});
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
const { shortcuts } = useShortcutContext();
const handleScroll: GameCardFocusHandler = (id, node, details) =>
const handleScroll: GameCardFocusHandler = (cardId, node, details) =>
{
const [source, id] = cardId.split('@');
queryClient.prefetchQuery(gameQuery(source, id));
if (!(details.nativeEvent instanceof MouseEvent))
{
node.scrollIntoView({ block: 'center', behavior: details.instant ? 'instant' : 'smooth' });
}
};
useEffect(() =>
{
if (data.focus)
setFocus(data.focus, { instant: true });
}, [data.focus]);
useEffect(() =>
{
return () => setFocus('');
}, []);
return (
<FocusContext value={focusKey}>
<AnimatedBackground animated ref={ref} backgroundKey="home-background" className='flex'>
<div className="px-3 w-full pt-2">
<HeaderUI title={data.headerTitle} buttons={[{ id: "search", icon: <Search /> }, { id: "filter", icon: <Settings2 /> }]} />
</div>
<div className="w-full grow mt-4 rounded-2xl px-2 overflow-y-scroll justify-center mask-alpha sm:portrait:mask-t-from-transparent md:landscape:mask-t-from-transparent mask-t-to-20 mask-t-to-black">
<div className="h-fit w-full md:px-6 pt-4 pb-32">
{data.title}
<Suspense>
<div ref={ref} className='absolute w-screen h-screen overflow-y-scroll'>
<StickyHeaderUI title={data.headerTitle} buttons={[{ id: "search", icon: <Search /> }, { id: "filter", icon: <Settings2 /> }]} ref={ref} />
<div className="w-full grow rounded-2xl justify-center mask-alpha sm:portrait:mask-t-from-transparent md:landscape:mask-t-from-transparent mask-t-to-20 mask-t-to-black">
<div className="relative h-fit w-full md:px-6 pt-4 pb-32">
{builtData.data?.filter && data.title}
{(builtData.data?.filter || (!data.filters && !data.builder)) && <Suspense fallback={<LoadingCardList grid placeholderCount={data.countHit ?? 8} id={`${focusKey}-list`} />}>
<GameList
grid
filters={data.filters}
filters={builtData.data?.filter}
onFocus={handleScroll}
id={`${focusKey}-list`}>
</GameList>
<AutoFocus focus={focusSelf} />
</Suspense>
<AutoFocus parentKey={focusKey} focus={focusSelf} />
</Suspense>}
<div className='absolute top-0 bottom-0 left-0 right-0 bg-radial from-base-100 to-base-300'></div>
<div className='mobile:hidden bg-noise z-1'></div>
<div className='mobile:hidden bg-dots z-1'></div>
</div>
</div>
<footer className="px-2 pb-2 absolute bottom-0 w-full h-12 flex items-center justify-between">
<footer className="px-2 pb-2 fixed bottom-0 w-full h-12 flex items-center justify-between">
<div>
{data.footer}
</div>
<Shortcuts shortcuts={shortcuts} />
</footer>
</AnimatedBackground>
</div>
</FocusContext>
);
}