import { JSX, Suspense, useContext, useState } from "react";
import
{
Gamepad2,
Settings,
Search,
Power,
OctagonAlert,
Maximize,
Store,
LayoutGrid,
LucideIcon,
} from "lucide-react";
import
{
createFileRoute,
useRouter,
} from "@tanstack/react-router";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import
{
FocusContext,
FocusDetails,
useFocusable,
} from "@noriginmedia/norigin-spatial-navigation";
import classNames from "classnames";
import { useEventListener } from "usehooks-ts";
import { HeaderAccounts, HeaderButton, HeaderStatusBar } from "../components/Header";
import { FilterUI } from "../components/Filters";
import { AnimatedBackground } from "../components/AnimatedBackground";
import { GameList } from "../components/GameList";
import LoadingCardList from "../components/LoadingCardList";
import { AutoFocus } from "../components/AutoFocus";
import SaveScroll from "../components/SaveScroll";
import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";
import { twMerge } from "tailwind-merge";
import { PlatformsList } from "../components/PlatformsList";
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
import z from "zod";
import CollectionList from "../components/CollectionList";
import { zodValidator } from '@tanstack/zod-adapter';
import { mobileCheck, scrollIntoViewHandler, useDragScroll } from "../scripts/utils";
import { AnimatedBackgroundContext } from "../scripts/contexts";
import Carousel from "../components/Carousel";
import { closeMutation } from "@queries/system";
import { gameQuery } from "../scripts/queries/romm";
import { oneShot } from "../scripts/audio/audio";
import { FloatingShortcuts } from "../components/Shortcuts";
import SelectMenu from "../components/SelectMenu";
import HeaderSearchField from "../components/HeaderSearchField";
import CardElement from "../components/CardElement";
import { Router } from "..";
import { FrontEndId } from "@/shared/types";
export const Route = createFileRoute("/")({
component: ConsoleHomeUI,
validateSearch: zodValidator(z.object({ filter: z.string().optional().default('games') }))
});
const filters = {
consoles: {
label: "Consoles",
},
games: {
label: "Games",
},
collections: {
label: "Collections",
},
};
let screenLock: WakeLockSentinel | undefined = undefined;
async function handleFullscreen ()
{
if (document.fullscreenElement)
{
await document.exitFullscreen();
if (screenLock)
screenLock.release();
} else
{
await document.documentElement.requestFullscreen();
screenLock = await navigator.wakeLock.request('screen');
return screenLock;
}
}
function HomeListError (data: { focused: boolean; })
{
const error = useErrorBoundary();
return
{(error.error as any).detail}
;
}
function Preview (data: { index: number; children?: any; })
{
const isMobile = mobileCheck();
return
{data.children}
;
}
function AdditionalCard (data: {
id: string,
route: keyof typeof Router.routesByPath,
title: string,
subTitle: string,
index: number,
actionLabel: string;
icon: LucideIcon | string;
badgeIcon?: LucideIcon;
})
{
const router = useRouter();
const handleNavigate = () =>
{
router.navigate({ to: data.route as any });
};
useShortcuts(data.id, () => [{ label: data.actionLabel, button: GamePadButtonCode.A, action: handleNavigate }]);
return ] : undefined} onAction={handleNavigate} title={data.title} subtitle={data.subTitle} preview={
{typeof data.icon === 'string' ?
:
}
} focusKey={data.id} index={0} id={data.id} />;
}
function HomeList (data: {
selectedFilter: string;
})
{
const router = useRouter();
const queryClient = useQueryClient();
const [initFocus, setInitFocus] = useState(false);
const bg = useContext(AnimatedBackgroundContext);
const { } = Route.useSearch;
const { ref, focused, focusKey, focusSelf } = useFocusable({
focusKey: "home-list",
preferredChildFocusKey: `${data.selectedFilter}-list`
});
const handleNodeFocus = (id: string, node: HTMLElement, details: FocusDetails) =>
{
const isMouseEvent = details.nativeEvent instanceof MouseEvent;
if (!isMouseEvent)
{
node?.scrollIntoView({ inline: 'center', block: 'center', behavior: initFocus ? 'smooth' : 'instant' });
}
setInitFocus(true);
};
function handleGameSelect (id: FrontEndId, source: string | null, sourceId: string | null)
{
router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } });
};
let activeList: JSX.Element;
switch (data.selectedFilter)
{
case 'consoles':
activeList = <>
}>
>;
break;
case 'collections':
activeList = <>
>;
break;
default:
activeList = <>
{
const [source, id] = d.id?.split('@', 2);
queryClient.prefetchQuery(gameQuery(source, id));
handleNodeFocus(l, n, d);
}}
className="animate-slide-up"
key="games-list"
id="games-list"
setBackground={bg.setBackground}
filters={{ limit: 12, orderBy: 'activity' }}
finalElement={[
,
]}
emptyElement={[
]}
/>
>;
break;
}
useEventListener('wheel', e =>
{
const deltaY = e.deltaY;
const deltaYSign = Math.sign(e.deltaY);
if (deltaYSign == -1)
{
(ref.current as HTMLElement)?.scrollBy({
top: 0,
left: deltaY,
behavior: 'instant'
});
} else
{
(ref.current as HTMLElement)?.scrollBy({
top: 0,
left: deltaY,
behavior: 'instant'
});
}
});
useDragScroll(ref);
return (
}>
}>
{activeList}
);
}
function MainMenu ()
{
const router = useRouter();
const { ref, focusKey } = useFocusable({
focusKey: `main-menu`,
trackChildren: true,
focusBoundaryDirections: ['up', 'down']
});
return (
router.navigate({ to: "/games", state: { eventType: e?.event?.type } })}
icon={}
label="Home"
type="secondary"
/>
} onAction={(e) => router.navigate({ to: "/store/tab", state: { eventType: e?.event?.type } })} label="Shop" />
{
router.navigate({ to: '/settings/interface', state: { eventType: e?.event?.type } });
}}
icon={}
label="Settings"
type="accent"
/>
);
}
function CircleIcon (data: {
type?: "secondary" | "accent" | "info";
label?: string;
icon?: JSX.Element;
} & InteractParams)
{
const handleAction = (event?: Event) =>
{
data.onAction?.({ event, focusKey });
oneShot('click');
};
const { ref, focusKey } = useFocusable({
focusKey: `menu-navigation-icon-${data.label}`,
onEnterPress: handleAction,
});
useShortcuts(focusKey, () => [{ label: data.label, action: handleAction, button: GamePadButtonCode.A }]);
const typeClasses = {
secondary: "bg-secondary text-secondary-content",
accent: "bg-accent text-accent-content",
info: "bg-info text-info-content",
none: "bg-base-content",
};
return (
handleAction(e.nativeEvent)}
className={twMerge(
`portrait:sm:size-12 sm:w-14 sm:h-10 menu-icon text-base-300 md:w-20 md:h-20 rounded-full flex items-center justify-center drop-shadow-lg cursor-pointer transition-all focusable focusable-primary focused:drop-shadow-2xl focused:animate-scale focusable-hover bg-base-content border-6 md:border-12 border-base-content focused:border-0 hover:border-0 z-1 active:border-0 active:bg-base-300 active:text-base-content active:transition-none`, typeClasses[data.type ?? 'none'])}
>
{data.icon}
);
}
export default function ConsoleHomeUI ()
{
const { filter } = Route.useSearch();
const close = useMutation(closeMutation);
const router = useRouter();
const { ref, focusKey } = useFocusable({
forceFocus: true,
autoRestoreFocus: false,
saveLastFocusedChild: false,
focusKey: "HomePage",
preferredChildFocusKey: `home-list`,
});
const setFilter = (filter: string) => router.navigate({ to: '/', search: { filter }, viewTransition: false, replace: true });
const headerButtons: HeaderButton[] = [];
if (mobileCheck())
headerButtons.push({ id: "fullscreen", icon: , action: handleFullscreen });
headerButtons.push(
{ id: "power-button", icon: , external: true, action: () => close.mutate(), className: "focusable-error!" },
{ id: "settings-header-button", icon: , external: true, action: () => router.navigate({ to: "/settings/accounts" }) }
);
const handleSearch = (search: string | undefined) =>
{
router.navigate({ to: '/games', search: { search } });
};
return (
[key, { ...value, selected: key === filter }]))}
setSelected={setFilter}
/>
} />
);
}