refactor: moved queries to their own file
This commit is contained in:
parent
364bc9d0be
commit
cf6fff6fac
83 changed files with 1107 additions and 852 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { CollectionsDetail } from '../components/CollectionsDetail';
|
||||
import { getCollectionApiCollectionsIdGetOptions, getRomsApiRomsGetOptions } from '@clients/romm/@tanstack/react-query.gen';
|
||||
import { getRomsApiRomsGetOptions } from '@clients/romm/@tanstack/react-query.gen';
|
||||
import { DefaultRommStaleTime } from '@shared/constants';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useContext } from 'react';
|
||||
import { AnimatedBackgroundContext } from '../scripts/contexts';
|
||||
import queries from '../scripts/queries';
|
||||
|
||||
export const Route = createFileRoute('/collection/$id')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -17,7 +18,7 @@ export const Route = createFileRoute('/collection/$id')({
|
|||
function RouteComponent ()
|
||||
{
|
||||
const { id } = Route.useParams();
|
||||
const { data: collection } = useQuery({ ...getCollectionApiCollectionsIdGetOptions({ path: { id: Number(id) } }) });
|
||||
const { data: collection } = useQuery(queries.romm.getCollectionQuery(Number(id)));
|
||||
const animatedBgContext = useContext(AnimatedBackgroundContext);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,27 +1,26 @@
|
|||
import { EMULATORJS_URL, RPC_URL, SERVER_URL } from '@/shared/constants';
|
||||
import { RPC_URL, SERVER_URL } from '@/shared/constants';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { gameQuery } from '../scripts/queries';
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import z from 'zod';
|
||||
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||
import { Router } from '..';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { Button, ButtonStyle } from '../components/options/Button';
|
||||
import { DoorOpen, Home, RefreshCw, Undo } from 'lucide-react';
|
||||
import { ButtonStyle } from '../components/options/Button';
|
||||
import { DoorOpen, RefreshCw, Undo } from 'lucide-react';
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||
import Shortcuts from '../components/Shortcuts';
|
||||
import { useEventListener, useTimeout } from 'usehooks-ts';
|
||||
import { GetFocusedElement, useGlobalFocus } from '../scripts/spatialNavigation';
|
||||
import { useEventListener } from 'usehooks-ts';
|
||||
import useActiveControl from '../scripts/gamepads';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { HeaderAccounts, HeaderStatusBar } from '../components/Header';
|
||||
import { RoundButton } from '../components/RoundButton';
|
||||
import queries from '../scripts/queries';
|
||||
|
||||
export const Route = createFileRoute('/embedded/$source/$id')({
|
||||
component: RouteComponent,
|
||||
loader: async (ctx) =>
|
||||
{
|
||||
const data = await ctx.context.queryClient.fetchQuery(gameQuery(ctx.params.source, ctx.params.id));
|
||||
const data = await ctx.context.queryClient.fetchQuery(queries.romm.gameQuery(ctx.params.source, ctx.params.id));
|
||||
return { data };
|
||||
},
|
||||
validateSearch: zodValidator(z.record(z.string(), z.string().optional().nullable()))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createFileRoute, ErrorComponentProps } from "@tanstack/react-router";
|
||||
import { CommandEntry, FrontEndGameTypeDetailed, GameInstallProgress, GameStatusType, RPC_URL } from "@shared/constants";
|
||||
import { twJoin, twMerge } from "tailwind-merge";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { JSX, RefObject, useEffect, useRef, useState } from "react";
|
||||
import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
|
|
@ -11,20 +11,20 @@ import { PopSource, SaveSource, useFocusEventListener } from "../../scripts/spat
|
|||
import { AnimatedBackground } from "../../components/AnimatedBackground";
|
||||
import { rommApi } from "../../scripts/clientApi";
|
||||
import toast from "react-hot-toast";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Router } from "../..";
|
||||
import { ContextDialog, ContextList, DialogEntry } from "../../components/ContextDialog";
|
||||
import Shortcuts from "../../components/Shortcuts";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { gameQuery } from "@/mainview/scripts/queries";
|
||||
import queries from "@/mainview/scripts/queries";
|
||||
import Screenshots from "@/mainview/components/Screenshots";
|
||||
import { delay, useSticky, useStickyDataAttr } from "@/mainview/scripts/utils";
|
||||
import { useStickyDataAttr } from "@/mainview/scripts/utils";
|
||||
import useActiveControl from "@/mainview/scripts/gamepads";
|
||||
|
||||
export const Route = createFileRoute("/game/$source/$id")({
|
||||
loader: async ({ params, context }) =>
|
||||
{
|
||||
const data = await context.queryClient.fetchQuery(gameQuery(params.source, params.id));
|
||||
const data = await context.queryClient.fetchQuery(queries.romm.gameQuery(params.source, params.id));
|
||||
return { data };
|
||||
},
|
||||
component: GameDetailsUI,
|
||||
|
|
@ -402,8 +402,7 @@ function ActionButtons (data: { game: FrontEndGameTypeDetailed; })
|
|||
const { ref, focusKey } = useFocusable({ focusKey: 'actions', onBlur: () => setHoverText(undefined) });
|
||||
const [open, setOpen] = useState(false);
|
||||
const deleteMutation = useMutation({
|
||||
mutationKey: ['delete', data.game.id],
|
||||
mutationFn: () => rommApi.api.romm.game({ source: data.game.id.source })({ id: data.game.id.id }).delete(),
|
||||
...queries.romm.deleteGameMutation,
|
||||
onSuccess: () =>
|
||||
{
|
||||
location.reload();
|
||||
|
|
@ -493,7 +492,7 @@ function ActionButton (data: {
|
|||
disabled?: boolean;
|
||||
})
|
||||
{
|
||||
const { ref, focused } = useFocusable({ focusKey: data.id, onFocus: data.onFocus, onEnterPress: data.onAction, focusable: data.disabled !== true });
|
||||
const { ref } = useFocusable({ focusKey: data.id, onFocus: data.onFocus, onEnterPress: data.onAction, focusable: data.disabled !== true });
|
||||
const styles = {
|
||||
primary: "bg-primary text-primary-content focused:bg-base-content focused:text-base-300 focusable focusable-primary",
|
||||
base: " text-base-content border-dashed border-base-content/20 border-2 focused:bg-base-content focused:text-base-300 focusable focusable-primary",
|
||||
|
|
|
|||
21
src/mainview/routes/games.tsx
Normal file
21
src/mainview/routes/games.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { CollectionsDetail } from '../components/CollectionsDetail';
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import z from 'zod';
|
||||
|
||||
export const Route = createFileRoute('/games')({
|
||||
component: RouteComponent,
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
|
||||
});
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { focus } = Route.useSearch();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<CollectionsDetail focus={focus} id='all-games'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { JSX, Suspense, useContext, useState } from "react";
|
||||
import { JSX, Suspense, useContext, useEffect, useState } from "react";
|
||||
import
|
||||
{
|
||||
Gamepad2,
|
||||
|
|
@ -21,7 +21,6 @@ import
|
|||
{
|
||||
FocusContext,
|
||||
FocusDetails,
|
||||
getCurrentFocusKey,
|
||||
useFocusable,
|
||||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
|
|
@ -38,7 +37,6 @@ import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";
|
|||
import { twMerge } from "tailwind-merge";
|
||||
import Shortcuts from "../components/Shortcuts";
|
||||
import { PlatformsList } from "../components/PlatformsList";
|
||||
import { systemApi } from "../scripts/clientApi";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
|
||||
import z from "zod";
|
||||
import { Router } from "..";
|
||||
|
|
@ -47,6 +45,8 @@ import { zodValidator } from '@tanstack/zod-adapter';
|
|||
import { mobileCheck, useDragScroll } from "../scripts/utils";
|
||||
import { AnimatedBackgroundContext } from "../scripts/contexts";
|
||||
import { FrontEndId } from "@/shared/constants";
|
||||
import Carousel from "../components/Carousel";
|
||||
import queries from "../scripts/queries";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: ConsoleHomeUI,
|
||||
|
|
@ -90,6 +90,16 @@ function HomeListError (data: { focused: boolean; })
|
|||
</div></div>;
|
||||
}
|
||||
|
||||
function ShowAllGamesCard ()
|
||||
{
|
||||
const handleNavigate = () =>
|
||||
{
|
||||
Router.navigate({ to: '/games', viewTransition: { types: ['zoom-in'] } });
|
||||
};
|
||||
const { ref } = useFocusable({ focusKey: 'all-games-btn', onEnterPress: handleNavigate });
|
||||
return <div ref={ref} onClick={handleNavigate} className="flex focusable focusable-primary justify-center items-center bg-base-300/80 rounded-3xl font-semibold w-(--game-card-width) h-(--game-card-height) focusable-hover cursor-pointer">All Games</div>;
|
||||
}
|
||||
|
||||
function HomeList (data: {
|
||||
selectedFilter: string;
|
||||
})
|
||||
|
|
@ -104,8 +114,8 @@ function HomeList (data: {
|
|||
|
||||
const handleNodeFocus = (id: string, node: HTMLElement, details: FocusDetails) =>
|
||||
{
|
||||
const isMounseEvent = details.nativeEvent instanceof MouseEvent;
|
||||
if (!isMounseEvent)
|
||||
const isMouseEvent = details.nativeEvent instanceof MouseEvent;
|
||||
if (!isMouseEvent)
|
||||
{
|
||||
node?.scrollIntoView({ inline: 'center', block: 'center', behavior: initFocus ? 'smooth' : 'instant' });
|
||||
}
|
||||
|
|
@ -136,19 +146,29 @@ function HomeList (data: {
|
|||
{
|
||||
case 'consoles':
|
||||
activeList = <>
|
||||
<PlatformsList onSelect={handlePlatformSelect} onFocus={handleNodeFocus} className="animate-slide-up" key="consoles-list" id="consoles-list" setBackground={bg.setBackground} />
|
||||
<PlatformsList saveChildFocus="session" onSelect={handlePlatformSelect} onFocus={handleNodeFocus} className="animate-slide-up" key="consoles-list" id="consoles-list" setBackground={bg.setBackground} />
|
||||
<AutoFocus parentKey={focusKey} focus={focusSelf} delay={10} />
|
||||
</>;
|
||||
break;
|
||||
case 'collections':
|
||||
activeList = <>
|
||||
<CollectionList onSelect={handleCollectionSelect} onFocus={handleNodeFocus} className="animate-slide-up" key="collections-list" id="collections-list" setBackground={bg.setBackground} />
|
||||
<CollectionList saveChildFocus="session" onSelect={handleCollectionSelect} onFocus={handleNodeFocus} className="animate-slide-up" key="collections-list" id="collections-list" setBackground={bg.setBackground} />
|
||||
<AutoFocus parentKey={focusKey} focus={focusSelf} delay={10} />
|
||||
</>;
|
||||
break;
|
||||
default:
|
||||
activeList = <>
|
||||
<GameList onGameSelect={handleGameSelect} onFocus={handleNodeFocus} className="animate-slide-up" key="games-list" id="games-list" setBackground={bg.setBackground} />
|
||||
<GameList
|
||||
onGameSelect={handleGameSelect}
|
||||
saveChildFocus="session"
|
||||
onFocus={handleNodeFocus}
|
||||
className="animate-slide-up"
|
||||
key="games-list"
|
||||
id="games-list"
|
||||
setBackground={bg.setBackground}
|
||||
filters={{ limit: 12 }}
|
||||
finalElement={<ShowAllGamesCard />}
|
||||
/>
|
||||
<AutoFocus parentKey={focusKey} focus={focusSelf} delay={10} />
|
||||
</>;
|
||||
break;
|
||||
|
|
@ -182,7 +202,7 @@ function HomeList (data: {
|
|||
|
||||
return (
|
||||
<FocusContext value={focusKey}>
|
||||
<div ref={ref} className="flex h-full w-full landscape:overflow-x-scroll portrait:overflow-y-scroll overflow-hidden no-scrollbar justify-center-safe sm:py-2 md:py-6 md:pb-6 md:mb-1 not-mobile:sm:pb-4" style={{
|
||||
<Carousel scrollRef={ref} rootClassName="h-full w-full" className="flex h-full w-full landscape:overflow-x-scroll portrait:overflow-y-scroll overflow-hidden no-scrollbar justify-center-safe sm:py-2 md:py-6 md:pb-6 md:mb-1 not-mobile:sm:pb-4" style={{
|
||||
mask: `linear-gradient(to right, rgba(0,0,0,0.8) 0%, black 10%, black 90%, rgba(0,0,0,0.8) 100%)`
|
||||
}}>
|
||||
<div className="landscape:flex landscape:px-16 portrait:min-h-fit portrait:h-fit portrait:pb-32 portrait:w-full landscape:h-full landscape:items-center">
|
||||
|
|
@ -193,17 +213,16 @@ function HomeList (data: {
|
|||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</Carousel>
|
||||
</FocusContext>
|
||||
);
|
||||
}
|
||||
|
||||
function MainMenu (data: {})
|
||||
function MainMenu ()
|
||||
{
|
||||
const { ref, focusKey, hasFocusedChild } = useFocusable({
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: `main-menu`,
|
||||
trackChildren: true,
|
||||
onBlur: (layout, props, details) => { },
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
|
|
@ -214,7 +233,7 @@ function MainMenu (data: {})
|
|||
>
|
||||
<FocusContext.Provider value={focusKey}>
|
||||
<CircleIcon
|
||||
action={() => navigate({ to: "/" })}
|
||||
action={() => navigate({ to: "/games", viewTransition: { types: ['zoom-in'] } })}
|
||||
icon={<Gamepad2 />}
|
||||
label="Home"
|
||||
type="secondary"
|
||||
|
|
@ -248,7 +267,7 @@ function CircleIcon (data: {
|
|||
icon?: JSX.Element;
|
||||
})
|
||||
{
|
||||
const { ref, focused, focusKey } = useFocusable({
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: `navigation-icon-${data.label}`,
|
||||
onEnterPress: data.action,
|
||||
});
|
||||
|
|
@ -275,15 +294,9 @@ export default function ConsoleHomeUI ()
|
|||
{
|
||||
const { filter } = Route.useSearch();
|
||||
|
||||
const closeMutation = useMutation({
|
||||
mutationKey: ['close'], mutationFn: async () =>
|
||||
{
|
||||
const { error } = await systemApi.api.system.exit.post();
|
||||
if (error) throw error;
|
||||
}
|
||||
});
|
||||
const close = useMutation(queries.system.closeMutation);
|
||||
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
const { ref, focusKey } = useFocusable({
|
||||
forceFocus: true,
|
||||
autoRestoreFocus: false,
|
||||
saveLastFocusedChild: false,
|
||||
|
|
@ -319,7 +332,7 @@ export default function ConsoleHomeUI ()
|
|||
const headerButtons = [];
|
||||
if (mobileCheck())
|
||||
headerButtons.push({ id: "fullscreen", icon: <Maximize />, action: handleFullscreen });
|
||||
headerButtons.push({ id: "search", icon: <Search /> }, { id: "power-button", icon: <Power />, external: true, action: () => closeMutation.mutate() });
|
||||
headerButtons.push({ id: "search", icon: <Search /> }, { id: "power-button", icon: <Power />, external: true, action: () => close.mutate() });
|
||||
|
||||
return (
|
||||
<AnimatedBackground animated ref={ref} backgroundKey="home-background" className="grid grid-cols-3 sm:landscape:grid-rows-[3rem_minmax(var(--game-card-height-safe),1fr)_4rem] md:landscape:grid-rows-[5rem_4rem_minmax(var(--game-card-height-safe),1fr)_6rem_6rem] gap-1 portrait:grid-rows-[3rem_4rem_minmax(var(--game-card-height-safe),1fr)] max-h-screen overflow-clip">
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { GameInstallProgress, RPC_URL } from '@/shared/constants';
|
|||
import DotsLoading from '../components/backgrounds/dots';
|
||||
import { Router } from '..';
|
||||
import { useEffect } from 'react';
|
||||
import { rommApi } from '../scripts/clientApi';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||
import { useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import Shortcuts from '../components/Shortcuts';
|
||||
import queries from '../scripts/queries';
|
||||
|
||||
export const Route = createFileRoute('/launcher/$source/$id')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -23,7 +23,7 @@ function RouteComponent ()
|
|||
|
||||
const { source, id } = Route.useParams();
|
||||
const { ref, focusKey } = useFocusable({ focusKey: `launching-${source}-${id}` });
|
||||
const { data } = useQuery({ queryKey: ['romm', 'game'], queryFn: () => rommApi.api.romm.game({ source })({ id }).get() });
|
||||
const { data } = useQuery(queries.romm.gameQuery(source, id));
|
||||
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
|
@ -58,7 +58,7 @@ function RouteComponent ()
|
|||
return <AnimatedBackground ref={ref} backgroundKey='game-details'>
|
||||
<div className='flex shadow-2xs shadow-black flex-col absolute w-screen h-screen overflow-hidden justify-center items-center gap-4'>
|
||||
<DotsLoading />
|
||||
<h1 className='font-semibold'>Launching {data?.data?.name} ...</h1>
|
||||
<h1 className='font-semibold'>Launching {data?.name} ...</h1>
|
||||
</div>
|
||||
<div className='absolute bot'>
|
||||
<Shortcuts shortcuts={shortcuts} />
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CollectionsDetail } from "../components/CollectionsDetail";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { DefaultRommStaleTime, RPC_URL } from "../../shared/constants";
|
||||
import { useContext } from "react";
|
||||
import { rommApi } from "../scripts/clientApi";
|
||||
import { AnimatedBackgroundContext } from "../scripts/contexts";
|
||||
import { RPC_URL } from "../../shared/constants";
|
||||
import queries from "../scripts/queries";
|
||||
|
||||
export const Route = createFileRoute("/platform/$source/$id")({
|
||||
component: RouteComponent
|
||||
|
|
@ -24,22 +22,12 @@ function PlatformTitle (data: { pathCover: string | null, platformName?: string;
|
|||
function RouteComponent ()
|
||||
{
|
||||
const { source, id } = Route.useParams();
|
||||
const { data: platform } = useQuery({
|
||||
queryKey: ['platform', source, id], queryFn: async () =>
|
||||
{
|
||||
const { data, error } = await rommApi.api.romm.platforms({ source })({ id }).get();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}, staleTime: DefaultRommStaleTime
|
||||
});
|
||||
|
||||
const animatedBgContext = useContext(AnimatedBackgroundContext);
|
||||
const { data: platform } = useQuery(queries.romm.platformQuery(source, id));
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
{!!platform && <CollectionsDetail
|
||||
title={<PlatformTitle pathCover={platform.path_cover} platformName={platform.name} />}
|
||||
setBackground={animatedBgContext.setBackground}
|
||||
filters={{ platform_id: Number(id), platform_slug: platform.slug, platform_source: source }}
|
||||
/>}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { systemApi } from '@/mainview/scripts/clientApi';
|
||||
|
||||
import queries from '@/mainview/scripts/queries';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
|
@ -9,7 +10,7 @@ export const Route = createFileRoute('/settings/about')({
|
|||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { data: systemInfo } = useQuery({ queryKey: ['system-info'], queryFn: () => systemApi.api.system.info.get() });
|
||||
const { data: systemInfo } = useQuery(queries.system.systemInfoQuery);
|
||||
return <table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -13,23 +13,17 @@ import
|
|||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { RPC_URL } from "@shared/constants";
|
||||
import
|
||||
{
|
||||
getCurrentUserApiUsersMeGetOptions,
|
||||
statsApiStatsGetOptions,
|
||||
} from "@clients/romm/@tanstack/react-query.gen";
|
||||
import { RommLoginDataSchema, RPC_URL } from "@shared/constants";
|
||||
import toast from "react-hot-toast";
|
||||
import z from "zod";
|
||||
import { OptionSpace } from "../../components/options/OptionSpace";
|
||||
import { useSettingsForm, useSettingsFormContext } from "../../components/options/SettingsAppForm";
|
||||
import { rommApi, settingsApi } from "../../scripts/clientApi";
|
||||
import { Button } from "../../components/options/Button";
|
||||
import { ContextDialog } from "@/mainview/components/ContextDialog";
|
||||
import QRCode from "react-qr-code";
|
||||
import { useJobStatus } from "@/mainview/scripts/utils";
|
||||
import { useInterval } from "usehooks-ts";
|
||||
import { TwitchIcon } from "@/mainview/scripts/brandIcons";
|
||||
import queries from "@/mainview/scripts/queries";
|
||||
|
||||
export const Route = createFileRoute("/settings/accounts")({
|
||||
component: RouteComponent,
|
||||
|
|
@ -56,44 +50,16 @@ function LoginQR (data: { id: string, isOpen: boolean, cancel: () => void, url:
|
|||
</ContextDialog>;
|
||||
}
|
||||
|
||||
function TwitchLogin (data: {})
|
||||
function TwitchLogin ()
|
||||
{
|
||||
|
||||
const loginStatus = useQuery({
|
||||
queryKey: ['twitch', 'login', 'status'],
|
||||
retry (failureCount, error)
|
||||
{
|
||||
if (error.status === 404)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return failureCount < 3;
|
||||
},
|
||||
queryFn: async () =>
|
||||
{
|
||||
const { data, error, status } = await rommApi.api.romm.login.twitch.get();
|
||||
if (error) throw { ...error, status };
|
||||
return data;
|
||||
}
|
||||
});
|
||||
const loginStatus = useQuery(queries.settings.twitchLoginVerificationQuery);
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationKey: ['twitch', 'login'],
|
||||
mutationFn: (openInBrowser: boolean) =>
|
||||
{
|
||||
return rommApi.api.romm.login.twitch.post({ openInBrowser });
|
||||
},
|
||||
...queries.settings.twitchLoginMutation,
|
||||
onSuccess: () => loginStatus.refetch()
|
||||
});
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationKey: ['twitch', 'logout'],
|
||||
mutationFn: () =>
|
||||
{
|
||||
return rommApi.api.romm.logout.twitch.post();
|
||||
},
|
||||
onSuccess: () => loginStatus.refetch()
|
||||
});
|
||||
const logoutMutation = useMutation({ ...queries.settings.twitchLogoutMutation, onSuccess: () => loginStatus.refetch() });
|
||||
|
||||
const { data: loginData, wsRef } = useJobStatus('twitch-login-job', { onEnded: () => loginStatus.refetch() });
|
||||
|
||||
|
|
@ -118,22 +84,13 @@ function TwitchLogin (data: {})
|
|||
|
||||
function LoginControls (data: { hasPassword: boolean; })
|
||||
{
|
||||
const user = useQuery({
|
||||
...getCurrentUserApiUsersMeGetOptions(),
|
||||
queryKey: ['romm', 'auth', "login"],
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 0
|
||||
});
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationKey: ['login', 'qr', 'cancel'],
|
||||
mutationFn: () => rommApi.api.romm.login.romm.post()
|
||||
});
|
||||
const { data: statusValue, error: loginError, wsRef } = useJobStatus('login-job');
|
||||
const user = useQuery(queries.romm.rommUserQuery());
|
||||
const loginMutation = useMutation(queries.romm.rommQrLoginMutation);
|
||||
const { data: statusValue, wsRef } = useJobStatus('login-job');
|
||||
const context = useSettingsFormContext({});
|
||||
const isMutatingRomm = useIsMutating({ mutationKey: ["romm", "auth"] }) > 0;
|
||||
const logoutMutation = useMutation({
|
||||
mutationKey: ["romm", "auth", "logout"], mutationFn: () => rommApi.api.romm.logout.post(),
|
||||
...queries.romm.rommLogoutMutation,
|
||||
onSuccess: async (d, v, r, c) =>
|
||||
{
|
||||
user.refetch();
|
||||
|
|
@ -171,8 +128,6 @@ function LoginControls (data: { hasPassword: boolean; })
|
|||
</div>;
|
||||
}
|
||||
|
||||
const dataSchema = z.object({ hostname: z.url(), username: z.string(), password: z.string() });
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { focus } = Route.useSearch();
|
||||
|
|
@ -181,9 +136,9 @@ function RouteComponent ()
|
|||
preferredChildFocusKey: focus
|
||||
});
|
||||
|
||||
const { data: hasPassword } = useQuery({ queryKey: ['romm', 'auth', 'passLength'], queryFn: () => rommApi.api.romm.login.get().then(d => d.data?.hasPassword as boolean) });
|
||||
const { data: hostname } = useQuery({ queryKey: ['romm', 'auth', 'hostname'], queryFn: () => settingsApi.api.settings({ id: 'rommAddress' }).get().then(d => d.data?.value as string) });
|
||||
const { data: username } = useQuery({ queryKey: ['romm', 'auth', 'username'], queryFn: () => settingsApi.api.settings({ id: 'rommUser' }).get().then(d => d.data?.value as string) });
|
||||
const { data: hasPassword } = useQuery(queries.romm.rommHasPasswordQuery);
|
||||
const { data: hostname } = useQuery(queries.romm.rommHostnameQuery);
|
||||
const { data: username } = useQuery(queries.romm.rommUsernameQuery);
|
||||
|
||||
const loginForm = useSettingsForm({
|
||||
defaultValues: {
|
||||
|
|
@ -201,15 +156,11 @@ function RouteComponent ()
|
|||
loginForm.reset();
|
||||
},
|
||||
validators: {
|
||||
onChange: dataSchema
|
||||
onChange: RommLoginDataSchema
|
||||
}
|
||||
});
|
||||
|
||||
const rommOnline = useQuery({
|
||||
...statsApiStatsGetOptions(),
|
||||
refetchInterval: 30000,
|
||||
retry: false,
|
||||
});
|
||||
const rommOnline = useQuery(queries.romm.rommGetOptionsQuery());
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
|
@ -219,22 +170,7 @@ function RouteComponent ()
|
|||
}
|
||||
}, [focus]);
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationKey: ["romm", "login"],
|
||||
mutationFn: async (data: z.infer<typeof dataSchema>) =>
|
||||
{
|
||||
const { error } = await rommApi.api.romm.login.post({ username: data.username, password: data.password, host: data.hostname });
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: (d, v, r, c) =>
|
||||
{
|
||||
c.client.invalidateQueries({ queryKey: ['romm', 'auth'] });
|
||||
},
|
||||
onError: (e) =>
|
||||
{
|
||||
console.error(e);
|
||||
},
|
||||
});
|
||||
const loginMutation = useMutation(queries.romm.rommLoginMutation);
|
||||
|
||||
let indicator = "";
|
||||
if (rommOnline.isError)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-naviga
|
|||
import { Block, createFileRoute } from '@tanstack/react-router';
|
||||
import DownloadDirectoryOption from '@/mainview/components/options/DownloadDirectoryOption';
|
||||
import { useIsMutating, useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { changeDownloadsMutation, downloadDrivesQuery } from '@/mainview/scripts/queries';
|
||||
import queries from '@/mainview/scripts/queries';
|
||||
import { DownloadsDrive } from '@/shared/constants';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import classNames from 'classnames';
|
||||
|
|
@ -24,11 +24,11 @@ function DriveComponent (data: { drive: DownloadsDrive; downloadsSize: number; r
|
|||
focusKey: data.drive.device,
|
||||
onFocus: () => (ref.current as HTMLElement)?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
||||
});
|
||||
const isMoving = useIsMutating(changeDownloadsMutation);
|
||||
const isMoving = useIsMutating(queries.settings.changeDownloadsMutation);
|
||||
const usedWithoutDownlods = data.drive.used - (data.drive.isCurrentlyUsed ? data.downloadsSize : 0);
|
||||
const usedPercent = usedWithoutDownlods / data.drive.size;
|
||||
const usedPercentRaw = data.drive.used / data.drive.size;
|
||||
const changeDownloads = useMutation({ ...changeDownloadsMutation, onSuccess: data.refetchDrives }); data.drive.unusableReason;
|
||||
const changeDownloads = useMutation({ ...queries.settings.changeDownloadsMutation, onSuccess: data.refetchDrives }); data.drive.unusableReason;
|
||||
const shortcuts: Shortcut[] = [];
|
||||
const valid = !data.drive.unusableReason && isMoving <= 0;
|
||||
const handleAction = () => changeDownloads.mutate(data.drive.mountPoint);
|
||||
|
|
@ -74,16 +74,16 @@ function DriveComponent (data: { drive: DownloadsDrive; downloadsSize: number; r
|
|||
function RouteComponent ()
|
||||
{
|
||||
const { focus } = Route.useSearch();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: "directories",
|
||||
preferredChildFocusKey: focus
|
||||
});
|
||||
|
||||
const isMoving = useIsMutating(changeDownloadsMutation);
|
||||
const { data: drives, refetch } = useQuery({ ...downloadDrivesQuery, refetchInterval: isMoving > 0 ? 1000 : undefined });
|
||||
const isMoving = useIsMutating(queries.settings.changeDownloadsMutation);
|
||||
const { data: drives, refetch } = useQuery({ ...queries.system.downloadDrivesQuery, refetchInterval: isMoving > 0 ? 1000 : undefined });
|
||||
|
||||
return <FocusContext value={focusKey}>
|
||||
<Block shouldBlockFn={() => isMoving} withResolver={false} />
|
||||
<Block shouldBlockFn={() => isMoving > 0} withResolver={false} />
|
||||
<ul ref={ref} className="list rounded-box gap-2">
|
||||
<div className="divider text-2xl mt-0 md:mt-4">
|
||||
<Download className='size-16' /> Downloads ({drives?.downloadsSize ? prettyBytes(drives?.downloadsSize) : <span className="loading loading-spinner loading-lg size-6"></span>})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { createFileRoute } from '@tanstack/react-router';
|
|||
import { OptionSpace } from '../../components/options/OptionSpace';
|
||||
import { OptionInput } from '../../components/options/OptionInput';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { settingsApi } from '../../scripts/clientApi';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Button } from '../../components/options/Button';
|
||||
import { Check, ChevronDown, FolderSearch, SearchAlert, Trash, TriangleAlert } from 'lucide-react';
|
||||
|
|
@ -15,7 +14,7 @@ import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spat
|
|||
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||
import FilePicker from '@/mainview/components/FilePicker';
|
||||
import { dirname } from 'pathe';
|
||||
import { autoEmulatorsQuery } from '@/mainview/scripts/queries';
|
||||
import queries from '@/mainview/scripts/queries';
|
||||
|
||||
export const Route = createFileRoute('/settings/emulators')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -33,7 +32,7 @@ function EmulatorsPending ()
|
|||
|
||||
function EmulatorListCat (data: { selected: string, set: (c: string) => void; })
|
||||
{
|
||||
const { ref, focused, focusKey } = useFocusable({ focusKey: 'categories' });
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'categories' });
|
||||
return <ul className='flex gap-1' ref={ref}>
|
||||
<FocusContext value={focusKey}>
|
||||
{[..."ABCDEFGHIJKLMNOPQRSTVWXYZ"].map(c =>
|
||||
|
|
@ -99,40 +98,13 @@ function EmulatorPath (data: { id: string; })
|
|||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [localValue, setLocalValue] = useState<string | undefined>();
|
||||
const { data: remoteValue } = useQuery({
|
||||
enabled: !!data.id,
|
||||
queryKey: ["emulator", data.id],
|
||||
queryFn: async () =>
|
||||
{
|
||||
const { data: value, error } = await settingsApi.api.settings.emulators.custom({ id: data.id }).get();
|
||||
if (error) throw error;
|
||||
return value;
|
||||
},
|
||||
});
|
||||
const setSettingMutation = useMutation({
|
||||
mutationKey: ["emulator", data.id, 'set'],
|
||||
mutationFn: async (value: string) => settingsApi.api.settings.emulators.custom({ id: data.id }).put({ value }),
|
||||
onSuccess: (d, v, r, ctx) =>
|
||||
{
|
||||
ctx.client.invalidateQueries({ queryKey: ["emulator", data.id] });
|
||||
ctx.client.invalidateQueries({ queryKey: ["auto-emulators"] });
|
||||
setLocalValue(v);
|
||||
setDirty(false);
|
||||
}
|
||||
});
|
||||
const deleteMutation = useMutation({
|
||||
mutationKey: ["emulator", data.id, 'delete'],
|
||||
mutationFn: async () =>
|
||||
{
|
||||
const { error } = await settingsApi.api.settings.emulators.custom({ id: data.id }).delete();
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: (d, v, r, ctx) =>
|
||||
{
|
||||
ctx.client.invalidateQueries({ queryKey: ['custom-emulators'] });
|
||||
ctx.client.invalidateQueries({ queryKey: ["auto-emulators"] });
|
||||
}
|
||||
});
|
||||
const { data: remoteValue } = useQuery(queries.settings.customEmulatorRemoveValueQuery(data.id));
|
||||
const setSettingMutation = useMutation(queries.settings.setCustomEmulatorMutation(data.id, (v) =>
|
||||
{
|
||||
setLocalValue(v);
|
||||
setDirty(false);
|
||||
}));
|
||||
const deleteMutation = useMutation(queries.settings.customEmulatorDeleteMutation(data.id));
|
||||
|
||||
const handleSave = useCallback(() =>
|
||||
{
|
||||
|
|
@ -251,11 +223,11 @@ function EmulatorBadge (data: {
|
|||
|
||||
function EmulatorBadges (data: { path?: string; addOverride: (emulator: string) => void; })
|
||||
{
|
||||
const { data: autoEmulators } = useQuery(autoEmulatorsQuery);
|
||||
const { data: autoEmulators } = useQuery(queries.settings.autoEmulatorsQuery);
|
||||
const { ref, focusKey } = useFocusable({ focusKey: `emulator-badges`, focusable: !!autoEmulators && autoEmulators.length > 0 });
|
||||
return <div ref={ref} className='grid grid-cols-[repeat(auto-fit,14rem)] auto-rows-[4rem] gap-2 justify-center-safe'>
|
||||
<FocusContext value={focusKey}>
|
||||
{autoEmulators?.map(e => <EmulatorBadge key={e.emulator} isCritical={e.isCritical} addOverride={data.addOverride} pathCover={e.path_cover ?? undefined} path={e.path?.path} exists={e.exists} emulator={e.emulator} />)}
|
||||
{autoEmulators?.map(e => <EmulatorBadge key={e.name} isCritical={e.isCritical} addOverride={data.addOverride} pathCover={e.logo} path={e.path?.path} exists={e.exists} emulator={e.name} />)}
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -263,30 +235,14 @@ function EmulatorBadges (data: { path?: string; addOverride: (emulator: string)
|
|||
function RouteComponent ()
|
||||
{
|
||||
const { focus } = Route.useSearch();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: "emulators-setting",
|
||||
preferredChildFocusKey: focus
|
||||
});
|
||||
|
||||
const { data: customEmulators } = useQuery({
|
||||
queryKey: ['custom-emulators'], queryFn: async () =>
|
||||
{
|
||||
const { data, error } = await settingsApi.api.settings.emulators.custom.get();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
});
|
||||
const { data: customEmulators } = useQuery(queries.settings.customEmulatorsQuery);
|
||||
|
||||
const addOverrideMutation = useMutation({
|
||||
mutationKey: ['emulator', 'custom', 'add'],
|
||||
mutationFn: async (id: string) =>
|
||||
{
|
||||
const { data, error } = await settingsApi.api.settings.emulators.custom({ id }).put({ value: '' });
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
onSuccess: (d, v, r, ctx) => ctx.client.invalidateQueries({ queryKey: ['custom-emulators'] })
|
||||
});
|
||||
const addOverrideMutation = useMutation(queries.settings.customEmulatorAddMutation);
|
||||
|
||||
return <FocusContext value={focusKey}>
|
||||
<ul ref={ref} className="list rounded-box gap-2">
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const Route = createFileRoute('/settings/interface')({
|
|||
function RouteComponent ()
|
||||
{
|
||||
const { focus } = Route.useSearch();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: "interface-settings",
|
||||
preferredChildFocusKey: focus
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function MenuItem (data: {
|
|||
const { to, search } = PopSource('settings');
|
||||
navigate({ to: data.return ? to ?? data.route : data.route, viewTransition: data.viewTransition, search: data.return ? search : undefined });
|
||||
};
|
||||
const { ref, focusSelf, focused } = useFocusable({
|
||||
const { ref, focusSelf } = useFocusable({
|
||||
focusKey: `menu-item-${data.route}`,
|
||||
forceFocus: !!acitve,
|
||||
onFocus: () =>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Shortcuts from "@/mainview/components/Shortcuts";
|
|||
import { AnimatedBackground } from "@/mainview/components/AnimatedBackground";
|
||||
import { PopSource } from "@/mainview/scripts/spatialNavigation";
|
||||
import { systemApi } from "@/mainview/scripts/clientApi";
|
||||
import { storeEmulatorDetailsQuery, storeEmulatorsRecommendedQuery } from "@/mainview/scripts/queries";
|
||||
import queries from "@/mainview/scripts/queries";
|
||||
import { Button } from "@/mainview/components/options/Button";
|
||||
import { ChevronDown, Download, Info, Settings } from "lucide-react";
|
||||
import { ContextDialog, ContextList, DialogEntry } from "@/mainview/components/ContextDialog";
|
||||
|
|
@ -27,7 +27,7 @@ export const Route = createFileRoute('/store/details/emulator/$id')({
|
|||
component: RouteComponent,
|
||||
async loader (ctx)
|
||||
{
|
||||
const emulator = await ctx.context.queryClient.fetchQuery(storeEmulatorDetailsQuery(ctx.params.id));
|
||||
const emulator = await ctx.context.queryClient.fetchQuery(queries.store.storeEmulatorDetailsQuery(ctx.params.id));
|
||||
return { emulator };
|
||||
}
|
||||
});
|
||||
|
|
@ -107,7 +107,7 @@ export function RouteComponent ()
|
|||
});
|
||||
|
||||
const { emulator } = Route.useLoaderData();
|
||||
const { data: recommended } = useQuery(storeEmulatorsRecommendedQuery);
|
||||
const { data: recommended } = useQuery(queries.store.storeEmulatorsRecommendedQuery);
|
||||
|
||||
useShortcuts(focusKey, () => [{
|
||||
label: "Return",
|
||||
|
|
@ -180,13 +180,7 @@ export function RouteComponent ()
|
|||
setFocus("title-area");
|
||||
Router.navigate({ to: '/store/details/emulator/$id', params: { id }, viewTransition: { types: ['zoom-in'] } });
|
||||
}}
|
||||
emulators={recommended.map(em => ({
|
||||
name: em.name,
|
||||
id: em.name,
|
||||
installed: em.exists,
|
||||
logo: em.logo,
|
||||
systems: em.systems
|
||||
} satisfies ShopFrontEndEmulator))} />}
|
||||
emulators={recommended} />}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex fixed bottom-4 left-4 right-4 justify-end z-10'>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { storeEmulatorsQuery } from '@/mainview/scripts/queries';
|
||||
|
||||
import { createFileRoute, useSearch } from '@tanstack/react-router';
|
||||
import { Joystick } from 'lucide-react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
|
|
@ -7,33 +7,13 @@ import { FocusContext, getCurrentFocusKey, useFocusable } from '@noriginmedia/no
|
|||
import { StoreEmulatorCard } from '@/mainview/components/store/StoreEmulatorCard';
|
||||
import { StoreContext } from '@/mainview/scripts/contexts';
|
||||
import { GetFocusedElement } from '@/mainview/scripts/spatialNavigation';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import queries from '@/mainview/scripts/queries';
|
||||
|
||||
export const Route = createFileRoute('/store/tab/emulators')({
|
||||
component: RouteComponent,
|
||||
pendingComponent: PendingComponent,
|
||||
async loader ({ context })
|
||||
{
|
||||
const emulators = await context.queryClient.fetchQuery(storeEmulatorsQuery);
|
||||
return { emulators };
|
||||
},
|
||||
});
|
||||
|
||||
function PendingComponent ()
|
||||
{
|
||||
return <section className="px-6 py-4">
|
||||
<div className="divider text-info">
|
||||
<Joystick className='size-12' />
|
||||
<h2 className="font-bold uppercase tracking-widest">
|
||||
Emulators
|
||||
</h2>
|
||||
</div>
|
||||
{/* Cards */}
|
||||
<div className="grid grid-cols-[repeat(auto-fill,18rem)] auto-rows-[12rem] py-2 px-4 gap-4 justify-center-safe">
|
||||
{[1, 2, 3, 4, 5, 6].map(i => <div key={i} className="skeleton h-36 rounded-2xl" />)}
|
||||
</div>
|
||||
</section>;
|
||||
}
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { focus } = useSearch({ from: '/store/tab' });
|
||||
|
|
@ -42,7 +22,7 @@ function RouteComponent ()
|
|||
preferredChildFocusKey: focus
|
||||
});
|
||||
const storeContext = useContext(StoreContext);
|
||||
const { emulators } = Route.useLoaderData();
|
||||
const { data: emulators } = useQuery(queries.store.storeEmulatorsQuery);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
|
@ -64,7 +44,7 @@ function RouteComponent ()
|
|||
</div>
|
||||
{/* Cards */}
|
||||
<div className="grid grid-cols-[repeat(auto-fill,18rem)] auto-rows-[12rem] py-2 md:px-4 gap-4 justify-center-safe">
|
||||
{emulators && emulators.map((data) => (
|
||||
{emulators?.map((data) => (
|
||||
<StoreEmulatorCard
|
||||
id={data.name}
|
||||
key={data.name}
|
||||
|
|
@ -72,7 +52,7 @@ function RouteComponent ()
|
|||
onFocus={({ node, details }) => { node.scrollIntoView({ behavior: details.instant ? 'instant' : 'smooth', block: 'center' }); }}
|
||||
onSelect={(id, focus) => storeContext.showDetails('emulator', 'store', id, focus)}
|
||||
/>
|
||||
))}
|
||||
)) ?? Array.from({ length: 10 }).map((_, i) => <div key={i} className="skeleton rounded-3xl" />)}
|
||||
</div>
|
||||
</FocusContext>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,95 +1,23 @@
|
|||
import { StoreGameCard } from '@/mainview/components/store/GamesSection';
|
||||
import { FocusContext, getCurrentFocusKey, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { FocusContext, getCurrentFocusKey, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { createFileRoute, useSearch } from '@tanstack/react-router';
|
||||
import { Gamepad, Gamepad2, HardDrive, Save } from 'lucide-react';
|
||||
import { JSX, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Gamepad2 } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { StoreContext } from '@/mainview/scripts/contexts';
|
||||
import { basename, dirname, extname } from 'pathe';
|
||||
import { rommApi } from '@/mainview/scripts/clientApi';
|
||||
import { FrontEndGameType, RPC_URL } from '@/shared/constants';
|
||||
import CardElement from '@/mainview/components/CardElement';
|
||||
import { FOCUS_KEYS } from '@/mainview/scripts/types';
|
||||
import FrontEndGameCard from '@/mainview/components/FrontEndGameCard';
|
||||
import { GetFocusedElement } from '@/mainview/scripts/spatialNavigation';
|
||||
import { useIntersectionObserver } from 'usehooks-ts';
|
||||
|
||||
const staleTime = 24 * 60 * 60 * 1000;
|
||||
import LoadMoreButton from '@/mainview/components/LoadMoreButton';
|
||||
import queries from '@/mainview/scripts/queries';
|
||||
|
||||
export const Route = createFileRoute('/store/tab/games')({
|
||||
component: RouteComponent,
|
||||
async loader (ctx)
|
||||
{
|
||||
|
||||
/*const gamesManifest = await ctx.context.queryClient.fetchQuery({
|
||||
queryKey: ['store-games-manifest'], queryFn: async () =>
|
||||
{
|
||||
const store = await fetch('https://api.github.com/repos/dragoonDorise/EmuDeck/git/trees/50261b66d69c1758efa28c6d7c54e45259a0c9c5?recursive=true').then(r => r.json());
|
||||
|
||||
return store.tree.filter((e: any) =>
|
||||
{
|
||||
if (e.type === 'blob' && e.path !== "featured.json")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}) as [];
|
||||
}, staleTime
|
||||
});
|
||||
|
||||
return { gamesManifest };*/
|
||||
},
|
||||
component: RouteComponent
|
||||
});
|
||||
|
||||
function LoadMoreButton (data: { isFetching: boolean; lastId?: string; } & FocusParams & InteractParams)
|
||||
{
|
||||
const handleAction = (e?: Event) =>
|
||||
{
|
||||
data.onAction?.(e);
|
||||
if (data.lastId && focused)
|
||||
setFocus(FOCUS_KEYS.GAME_CARD(data.lastId));
|
||||
};
|
||||
|
||||
const { ref, focusKey, focused } = useFocusable({
|
||||
focusKey: 'load-more-btn',
|
||||
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details),
|
||||
onEnterPress: handleAction
|
||||
});
|
||||
|
||||
const { ref: intersct } = useIntersectionObserver({
|
||||
onChange: (isIntersecting, entry) =>
|
||||
{
|
||||
if (isIntersecting)
|
||||
{
|
||||
handleAction();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return <div ref={(r) =>
|
||||
{
|
||||
ref.current = r;
|
||||
intersct(r);
|
||||
}} className='flex bg-base-100 game-card focusable focusable-accent focusable-hover text-2xl justify-center items-center cursor-pointer' onClick={handleAction} id='load-more-btn'>{data.isFetching ? <span className="loading loading-spinner loading-xl"></span> : "Load More"}</div>;
|
||||
}
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { focus } = useSearch({ from: '/store/tab' });
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "main-area", preferredChildFocusKey: focus });
|
||||
|
||||
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery<{ data: FrontEndGameType[], nextPage: number; }>({
|
||||
initialPageParam: 0,
|
||||
queryKey: ['store-games'],
|
||||
getNextPageParam: (lastPage, pages) => lastPage.nextPage,
|
||||
queryFn: async (data) =>
|
||||
{
|
||||
const pageParam = data.pageParam as number;
|
||||
const { data: games, error } = await rommApi.api.romm.games.get({ query: { source: 'store', offset: pageParam * 10, limit: 10 } });
|
||||
if (error) throw error;
|
||||
return { data: games.games, nextPage: pageParam + 1 };
|
||||
}
|
||||
});
|
||||
const { data, fetchNextPage, isFetchingNextPage, isFetching } = useInfiniteQuery(queries.store.storeGamesInfiniteQuery);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
|
@ -115,17 +43,21 @@ function RouteComponent ()
|
|||
Games
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,18rem)] auto-rows-[minmax(18rem,min-content)] py-2 md:px-4 gap-4 justify-center-safe">
|
||||
<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} />))
|
||||
)}
|
||||
) ?? 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>
|
||||
<div className="skeleton h-4 w-[40%]"></div>
|
||||
</div>)}
|
||||
<LoadMoreButton
|
||||
lastId={data?.pages.at(-1)?.data.at(-1)?.id.id}
|
||||
onFocus={handleFocus}
|
||||
isFetching={isFetchingNextPage}
|
||||
isFetching={isFetchingNextPage || isFetching}
|
||||
onAction={() =>
|
||||
{
|
||||
if (isFetchingNextPage)
|
||||
if (isFetchingNextPage || isFetching)
|
||||
return;
|
||||
fetchNextPage();
|
||||
}} />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { createFileRoute, ErrorComponentProps, useSearch } from '@tanstack/react-router';
|
||||
import { createFileRoute, useSearch } from '@tanstack/react-router';
|
||||
import { useFocusable, FocusContext, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { MissingEmulatorsSection } from "../../../components/store/MissingEmulatorsSection";
|
||||
import { EmulatorsSection } from "../../../components/store/EmulatorsSection";
|
||||
import { GamesSection } from "../../../components/store/GamesSection";
|
||||
import { StatsSection } from "../../../components/store/StatsSection";
|
||||
import { FrontEndGameTypeDetailed, RPC_URL } from '@/shared/constants';
|
||||
import { autoEmulatorsQuery, storeEmulatorsRecommendedQuery, storeFeaturedGamesQuery } from '@/mainview/scripts/queries';
|
||||
import queries from '@/mainview/scripts/queries';
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { scrollIntoViewHandler } from '@/mainview/scripts/utils';
|
||||
import { StoreContext } from '@/mainview/scripts/contexts';
|
||||
|
|
@ -13,66 +13,34 @@ import { useInterval } from 'usehooks-ts';
|
|||
import { Button } from '@/mainview/components/options/Button';
|
||||
import { HardDrive, Search } from 'lucide-react';
|
||||
import { GetFocusedElement } from '@/mainview/scripts/spatialNavigation';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export const Route = createFileRoute('/store/tab/')({
|
||||
component: RouteComponent,
|
||||
pendingComponent: LoadingSkeleton,
|
||||
errorComponent: ErrorComponent,
|
||||
loader: async ({ context }) =>
|
||||
{
|
||||
const autoEmulators = await context.queryClient.fetchQuery(autoEmulatorsQuery);
|
||||
const crutialEmulators = autoEmulators?.filter(e => !e.exists && e.isCritical);
|
||||
const featuredGames = await await context.queryClient.fetchQuery(storeFeaturedGamesQuery);
|
||||
const recommendedEmulators = await context.queryClient.fetchQuery(storeEmulatorsRecommendedQuery);
|
||||
return { crutialEmulators, recommendedEmulators, featuredGames };
|
||||
}
|
||||
component: RouteComponent
|
||||
});
|
||||
|
||||
function ErrorComponent (data: ErrorComponentProps)
|
||||
{
|
||||
return <div className="flex items-center justify-center h-64">
|
||||
<div role="alert" className="alert alert-error alert-soft max-w-sm">
|
||||
<span>Failed to load store data.</span>
|
||||
<p>{data.error.message}</p>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
// ── Loading skeleton ───────────────────────────────────────────────────────
|
||||
function LoadingSkeleton ()
|
||||
function Main (data: { games?: FrontEndGameTypeDetailed[]; })
|
||||
{
|
||||
return (
|
||||
<div className="flex flex-col gap-6 px-6 py-4 animate-pulse">
|
||||
{/* Missing section */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[1, 2, 3].map((i) => <div key={i} className="skeleton h-40 rounded-2xl" />)}
|
||||
</div>
|
||||
{/* Emulators */}
|
||||
<div className="grid grid-cols-6 gap-3">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => <div key={i} className="skeleton h-36 rounded-2xl" />)}
|
||||
</div>
|
||||
{/* Games */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{[1, 2, 3, 4].map((i) => <div key={i} className="skeleton h-44 rounded-2xl" />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Main (data: { children?: any; games: FrontEndGameTypeDetailed[]; })
|
||||
{
|
||||
const [selectedGame, setSelectedGame] = useState(new Date().getSeconds() % data.games.length);
|
||||
const [selectedGame, setSelectedGame] = useState(0);
|
||||
const [nextSwitch, setNextSwitch] = useState(new Date().getTime() + 10000);
|
||||
const progressRef = useRef<HTMLProgressElement>(null);
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'main-featured-area' });
|
||||
const game = data.games[selectedGame];
|
||||
const game = data.games ? data.games[selectedGame] : undefined;
|
||||
|
||||
useInterval(() =>
|
||||
{
|
||||
setSelectedGame(current => (current + 1) % data.games.length);
|
||||
if (!data.games) return;
|
||||
setSelectedGame(current => (current + 1) % data.games!.length);
|
||||
setNextSwitch(new Date().getTime() + 10000);
|
||||
}, 10000);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (!data.games) return;
|
||||
setSelectedGame(new Date().getSeconds() % data.games.length);
|
||||
}, [data.games]);
|
||||
|
||||
useInterval(() =>
|
||||
{
|
||||
var time = (nextSwitch - new Date().getTime()) / 10000;
|
||||
|
|
@ -81,18 +49,18 @@ function Main (data: { children?: any; games: FrontEndGameTypeDetailed[]; })
|
|||
}, 10);
|
||||
|
||||
const storeContext = useContext(StoreContext);
|
||||
const previewUrl = new URL(`${RPC_URL(__HOST__)}${data.games[selectedGame].path_cover}`);
|
||||
previewUrl.searchParams.set('blur', '16');
|
||||
const previewUrl = data.games ? new URL(`${RPC_URL(__HOST__)}${data.games[selectedGame].path_cover}`) : undefined;
|
||||
previewUrl?.searchParams.set('blur', '16');
|
||||
|
||||
return <div ref={ref} className='flex sm:flex-wrap md:flex-nowrap group-focusable p-4 mt-4 gap-4'>
|
||||
|
||||
<FocusContext value={focusKey}>
|
||||
<div key={selectedGame} className="flex transition-all duration-500 flex-col sm:32 md:h-64 rounded-3xl overflow-hidden shadow-black/5 shadow-xl grow">
|
||||
{game ? <div key={selectedGame} className="flex transition-all duration-500 flex-col rounded-3xl overflow-hidden shadow-black/5 shadow-xl w-full">
|
||||
<div className='flex relative h-full overflow-hidden'>
|
||||
<div className='absolute w-full h-full z-0 bg-base-200'>
|
||||
<img key={selectedGame}
|
||||
className='w-full h-full object-cover transition-all duration-500 ease-out scale-110 opacity-0 z-0 mask-l-from-0'
|
||||
src={previewUrl.href}
|
||||
src={previewUrl?.href}
|
||||
onLoad={(e) =>
|
||||
{
|
||||
e.currentTarget.classList.toggle('opacity-0', false);
|
||||
|
|
@ -101,11 +69,11 @@ function Main (data: { children?: any; games: FrontEndGameTypeDetailed[]; })
|
|||
/>
|
||||
</div>
|
||||
<div key={selectedGame} className='flex sm:flex-wrap md:flex-nowrap grow z-1 p-8 opacity-0 animate-fade-in h-full items-end gap-4 sm:justify-end md:justify-between'>
|
||||
<div className='flex gap-4 max-h-full z-1 grow'>
|
||||
<div className='flex gap-4 max-h-full z-1 grow md:h-full'>
|
||||
<div className='flex sm:portrait:flex-wrap sm:portrait:grow gap-4 max-h-full justify-center'>
|
||||
<div className='relative rounded-3xl max-w-xs overflow-hidden'>
|
||||
<div className='relative rounded-3xl max-w-xs h-48 overflow-hidden'>
|
||||
<div className='flex absolute bottom-4 left-4 size-8 bg-base-content text-base-100 rounded-full items-center justify-center shadow-lg'><HardDrive /></div>
|
||||
<img className='object-cover w-full h-full' src={`${RPC_URL(__HOST__)}${data.games[selectedGame].path_cover}`} />
|
||||
{!!data.games && <img className='object-cover w-full h-full' src={`${RPC_URL(__HOST__)}${data.games[selectedGame].path_cover}`} />}
|
||||
</div>
|
||||
<div className='flex flex-col gap-2 py-3 max-w-md'>
|
||||
<h1 className='font-semibold text-3xl'>{game.name}</h1>
|
||||
|
|
@ -117,21 +85,19 @@ function Main (data: { children?: any; games: FrontEndGameTypeDetailed[]; })
|
|||
<Button onAction={() => storeContext.showDetails('game', game.id.source, game.id.id, focusKey)} className='px-6 py-3 text-2xl! z-1 gap-2 focusable focusable-primary' id={'play-featured-btn'}> <Search /> Details</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.children}
|
||||
</div>
|
||||
</div> : <div className='skeleton w-full rounded-3xl grow sm:h-64 z-15' />}
|
||||
<div className='sm:flex sm:flex-wrap grow justify-stretch md:grid sm:landscape:grid-flow-col sm:auto-cols-[minmax(8rem,1fr)] md:grid-flow-row! auto-rows-fr landscape:min-w-xs gap-4'>
|
||||
{data.games.map((g, i) =>
|
||||
{data.games?.map((g, i) =>
|
||||
<div key={i} data-active={i === selectedGame} className='flex grow flex-col gap-1 transition-opacity duration-500 data-[active=true]:opacity-50 rounded-3xl bg-base-100 p-4 justify-center shadow-md'>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<img className='size-6' src={`${RPC_URL(__HOST__)}${game.path_platform_cover}`}></img>
|
||||
<img className='size-6' src={`${RPC_URL(__HOST__)}${g.path_platform_cover}`}></img>
|
||||
<div className='flex gap-2 items-center grow'>
|
||||
{g.name}
|
||||
</div>
|
||||
</div>
|
||||
{i === selectedGame && <progress ref={progressRef} className="progress progress-accent w-full" style={{ animationName: '' }} value={0} max="1"></progress>}
|
||||
</div>)}
|
||||
</div>) ?? Array.from({ length: 3 }).map((_, i) => <div key={i} className="skeleton rounded-3xl"></div>)}
|
||||
</div>
|
||||
</FocusContext>
|
||||
</div>;
|
||||
|
|
@ -140,7 +106,9 @@ function Main (data: { children?: any; games: FrontEndGameTypeDetailed[]; })
|
|||
export function RouteComponent ()
|
||||
{
|
||||
const { focus } = useSearch({ from: '/store/tab' });
|
||||
const { crutialEmulators, recommendedEmulators, featuredGames } = Route.useLoaderData();
|
||||
const { data: crucialEmulators, isSuccess } = useQuery({ ...queries.settings.autoEmulatorsQuery, select: (data) => data.filter(e => !e.exists && e.isCritical) });
|
||||
const { data: featuredGames } = useQuery(queries.store.storeFeaturedGamesQuery);
|
||||
const { data: recommendedEmulators } = useQuery(queries.store.storeEmulatorsRecommendedQuery);
|
||||
|
||||
const { focusKey, ref, focusSelf } = useFocusable({ focusKey: 'main-area', preferredChildFocusKey: focus ?? "recommended-emulators" });
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
|
@ -152,15 +120,15 @@ export function RouteComponent ()
|
|||
focusSelf({ instant: true });
|
||||
}
|
||||
|
||||
}, [focus]);
|
||||
}, [focus, isSuccess]);
|
||||
|
||||
return (
|
||||
<div className='animate-slide-up' ref={ref}>
|
||||
<FocusContext value={focusKey}>
|
||||
{!!featuredGames && <Main games={featuredGames} />}
|
||||
{crutialEmulators.length > 0 && <MissingEmulatorsSection
|
||||
{<Main games={featuredGames} />}
|
||||
{!!crucialEmulators && crucialEmulators?.length > 0 && <MissingEmulatorsSection
|
||||
onSelect={(id, focus) => storeContext.showDetails('emulator', 'store', id, focus)}
|
||||
emulators={crutialEmulators} />}
|
||||
emulators={crucialEmulators} />}
|
||||
<div className='pt-4'>
|
||||
<EmulatorsSection
|
||||
id="recommended-emulators"
|
||||
|
|
@ -177,7 +145,7 @@ export function RouteComponent ()
|
|||
|
||||
<StatsSection
|
||||
romCount={1240}
|
||||
missingCount={crutialEmulators.length}
|
||||
missingCount={crucialEmulators?.length ?? 0}
|
||||
/>
|
||||
</FocusContext>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue