feat: Implemented audio effects
This commit is contained in:
parent
fe0ab3b498
commit
edbc390d14
125 changed files with 1137 additions and 217 deletions
|
|
@ -4,10 +4,7 @@ import Notifications from "../components/Notifications";
|
|||
import { Toaster } from "react-hot-toast";
|
||||
import { mobileCheck, useLocalSetting } from "../scripts/utils";
|
||||
import useActiveControl from "../scripts/gamepads";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SystemInfoContext } from "../scripts/contexts";
|
||||
import { SystemInfoType } from "@/shared/constants";
|
||||
import { systemApi } from "../scripts/clientApi";
|
||||
import { useEffect } from "react";
|
||||
import AppCommunication from "../components/AppCommunication";
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { RPC_URL, SERVER_URL } from '@/shared/constants';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||
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 { ButtonStyle } from '../components/options/Button';
|
||||
import { DoorOpen, RefreshCw, Undo } from 'lucide-react';
|
||||
|
|
@ -57,7 +56,7 @@ function Overlay (data: {
|
|||
{
|
||||
if (data.open)
|
||||
{
|
||||
focusSelf();
|
||||
focusSelf({ instant: true });
|
||||
}
|
||||
}, [data.open]);
|
||||
|
||||
|
|
@ -122,6 +121,7 @@ function Frame (data: { ref: RefObject<HTMLIFrameElement | null>; })
|
|||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusSelf, focusKey } = useFocusable({
|
||||
focusKey: 'emulatorjs',
|
||||
preferredChildFocusKey: 'frame',
|
||||
|
|
@ -133,7 +133,7 @@ function RouteComponent ()
|
|||
|
||||
function HandleGoBack ()
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { source, id }, replace: true });
|
||||
router.navigate({ to: '/game/$source/$id', params: { source, id }, replace: true });
|
||||
}
|
||||
|
||||
useEventListener('message', e =>
|
||||
|
|
@ -173,7 +173,7 @@ function RouteComponent ()
|
|||
};
|
||||
useEffect(() => setPaused(overlayOpen), [overlayOpen]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
useEffect(() => { if (!overlayOpen) focusSelf(); }, [overlayOpen]);
|
||||
useEffect(() => { if (!overlayOpen) focusSelf({ instant: true }); }, [overlayOpen]);
|
||||
function handleClose ()
|
||||
{
|
||||
setOverlayOpen(false);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { createFileRoute, ErrorComponentProps } from "@tanstack/react-router";
|
||||
import { createFileRoute, ErrorComponentProps, useRouter, useRouterState } from "@tanstack/react-router";
|
||||
import { RPC_URL } from "@shared/constants";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { Calendar, Clock, Folder, Gamepad2, Image, Info, Store, TriangleAlert, Trophy } from "lucide-react";
|
||||
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { Calendar, Folder, Gamepad2, Image, Info, TriangleAlert, Trophy } from "lucide-react";
|
||||
import { HeaderUI } from "../../components/Header";
|
||||
import { AnimatedBackground } from "../../components/AnimatedBackground";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Router } from "../..";
|
||||
import Shortcuts from "../../components/Shortcuts";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import Screenshots from "@/mainview/components/Screenshots";
|
||||
import { HandleGoBack, scrollIntoViewHandler, useStickyDataAttr } from "@/mainview/scripts/utils";
|
||||
import { HandleGoBack, scrollIntoViewHandler, useOnNavigateBack, useStickyDataAttr } from "@/mainview/scripts/utils";
|
||||
import { FilterUI } from "@/mainview/components/Filters";
|
||||
import StatList, { StatEntry } from "@/mainview/components/StatList";
|
||||
import { useIntersectionObserver, useLocalStorage } from "usehooks-ts";
|
||||
|
|
@ -21,7 +20,7 @@ import Achievements from "@/mainview/components/game/Achievements";
|
|||
import { GameDetailsContext } from "@/mainview/scripts/contexts";
|
||||
import { gameQuery, gamesRecommendedBasedOnGameQuery } from "@queries/romm";
|
||||
import { GamesSection } from "@/mainview/components/store/GamesSection";
|
||||
import Details, { DetailElement } from "@/mainview/components/game/Details";
|
||||
import Details from "@/mainview/components/game/Details";
|
||||
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
||||
|
||||
export const Route = createFileRoute("/game/$source/$id")({
|
||||
|
|
@ -31,7 +30,11 @@ export const Route = createFileRoute("/game/$source/$id")({
|
|||
},
|
||||
component: RouteComponent,
|
||||
errorComponent: Error,
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() })),
|
||||
staticData: {
|
||||
enterSound: 'openDetails',
|
||||
goBackSound: "returnDetails"
|
||||
},
|
||||
});
|
||||
|
||||
function useDetailsSection ()
|
||||
|
|
@ -45,10 +48,6 @@ function Error (data: ErrorComponentProps)
|
|||
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
useEffect(() =>
|
||||
{
|
||||
focusSelf();
|
||||
}, []);
|
||||
|
||||
return <AnimatedBackground ref={ref} backgroundKey="game-details">
|
||||
<div className="relative z-10 h-full">
|
||||
|
|
@ -68,6 +67,7 @@ function Error (data: ErrorComponentProps)
|
|||
</div>
|
||||
</FocusContext>
|
||||
</div>
|
||||
<AutoFocus force focus={focusSelf} />
|
||||
</AnimatedBackground>;
|
||||
}
|
||||
|
||||
|
|
@ -139,10 +139,10 @@ function Divider (data: { rootFocusKey: string; showShortcuts: boolean; game: Fr
|
|||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const [recommendedGamesVisible, setRecommendedGamesVisible] = useState(false);
|
||||
const { source, id } = Route.useParams();
|
||||
const { data } = useQuery(gameQuery(source, id));
|
||||
const { focus } = Route.useSearch();
|
||||
const [, setUpdate] = useState(0);
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details", forceFocus: true });
|
||||
const headerRef = useRef(null);
|
||||
|
|
@ -150,7 +150,12 @@ function RouteComponent ()
|
|||
const backgroundImage = data ? new URL(`${RPC_URL(__HOST__)}${data.path_cover}`) : undefined;
|
||||
const { data: recommendedGames } = useQuery({ ...gamesRecommendedBasedOnGameQuery(data?.id.source ?? source, data?.id.id ?? id), enabled: !!data && recommendedGamesVisible });
|
||||
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||
useShortcuts(focusKey, () => [{
|
||||
label: "Back", button: GamePadButtonCode.B, action: () => HandleGoBack(router)
|
||||
}], [router]);
|
||||
|
||||
useOnNavigateBack((s) => s.sound = 'returnDetails');
|
||||
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
useStickyDataAttr(headerRef, sentinelRef, ref);
|
||||
|
|
@ -190,7 +195,7 @@ function RouteComponent ()
|
|||
onFocus={scrollIntoViewHandler({ block: 'center' })}
|
||||
onSelect={(id, focus) =>
|
||||
{
|
||||
Router.navigate({ to: '/store/details/emulator/$id', params: { id } });
|
||||
router.navigate({ to: '/store/details/emulator/$id', params: { id } });
|
||||
}}
|
||||
emulators={recommendedEmulators} />}
|
||||
|
||||
|
|
@ -206,7 +211,7 @@ function RouteComponent ()
|
|||
</div>
|
||||
<GamesSection ref={intersct} showSources onSelect={(id, focus) =>
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { id: id.id, source: id.source } });
|
||||
router.navigate({ to: '/game/$source/$id', params: { id: id.id, source: id.source } });
|
||||
}} onFocus={scrollIntoViewHandler({ block: 'center', inline: 'nearest' })} games={recommendedGames} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import
|
|||
import
|
||||
{
|
||||
createFileRoute,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import
|
||||
|
|
@ -37,7 +38,6 @@ import Shortcuts from "../components/Shortcuts";
|
|||
import { PlatformsList } from "../components/PlatformsList";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
|
||||
import z from "zod";
|
||||
import { Router } from "..";
|
||||
import CollectionList from "../components/CollectionList";
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import { mobileCheck, useDragScroll } from "../scripts/utils";
|
||||
|
|
@ -45,6 +45,7 @@ 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";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: ConsoleHomeUI,
|
||||
|
|
@ -90,9 +91,10 @@ function HomeListError (data: { focused: boolean; })
|
|||
|
||||
function ShowAllGamesCard ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const handleNavigate = () =>
|
||||
{
|
||||
Router.navigate({ to: '/games', viewTransition: { types: ['zoom-in'] } });
|
||||
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>;
|
||||
|
|
@ -102,6 +104,7 @@ function HomeList (data: {
|
|||
selectedFilter: string;
|
||||
})
|
||||
{
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const [initFocus, setInitFocus] = useState(false);
|
||||
const bg = useContext(AnimatedBackgroundContext);
|
||||
|
|
@ -124,7 +127,7 @@ function HomeList (data: {
|
|||
|
||||
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 } });
|
||||
router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } });
|
||||
};
|
||||
|
||||
let activeList: JSX.Element;
|
||||
|
|
@ -213,9 +216,11 @@ function HomeList (data: {
|
|||
|
||||
function MainMenu ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: `main-menu`,
|
||||
trackChildren: true,
|
||||
focusBoundaryDirections: ['up', 'down']
|
||||
});
|
||||
return (
|
||||
<ul
|
||||
|
|
@ -226,13 +231,13 @@ function MainMenu ()
|
|||
>
|
||||
<FocusContext.Provider value={focusKey}>
|
||||
<CircleIcon
|
||||
action={() => Router.navigate({ to: "/games" })}
|
||||
action={() => router.navigate({ to: "/games" })}
|
||||
icon={<Gamepad2 />}
|
||||
label="Home"
|
||||
type="secondary"
|
||||
/>
|
||||
<CircleIcon icon={<MessageSquare />} label="News" />
|
||||
<CircleIcon type="info" icon={<Store />} action={() => Router.navigate({ to: "/store/tab" })} label="Shop" />
|
||||
<CircleIcon type="info" icon={<Store />} action={() => router.navigate({ to: "/store/tab" })} label="Shop" />
|
||||
<CircleIcon icon={<Image />} label="Album" />
|
||||
<CircleIcon
|
||||
icon={<Gamepad2 />}
|
||||
|
|
@ -241,7 +246,7 @@ function MainMenu ()
|
|||
<CircleIcon
|
||||
action={() =>
|
||||
{
|
||||
Router.navigate({ to: '/settings/accounts' });
|
||||
router.navigate({ to: '/settings/accounts' });
|
||||
}}
|
||||
icon={<Settings />}
|
||||
label="Settings"
|
||||
|
|
@ -259,11 +264,16 @@ function CircleIcon (data: {
|
|||
icon?: JSX.Element;
|
||||
})
|
||||
{
|
||||
const handleAction = () =>
|
||||
{
|
||||
data.action?.();
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: `navigation-icon-${data.label}`,
|
||||
onEnterPress: data.action,
|
||||
focusKey: `menu-navigation-icon-${data.label}`,
|
||||
onEnterPress: handleAction,
|
||||
});
|
||||
useShortcuts(focusKey, () => [{ label: data.label, action: (e) => data.action?.(), button: GamePadButtonCode.A }]);
|
||||
useShortcuts(focusKey, () => [{ label: data.label, action: handleAction, button: GamePadButtonCode.A }]);
|
||||
const typeClasses = {
|
||||
secondary: "bg-secondary text-secondary-content",
|
||||
accent: "bg-accent text-accent-content",
|
||||
|
|
@ -273,7 +283,8 @@ function CircleIcon (data: {
|
|||
return (
|
||||
<li
|
||||
ref={ref}
|
||||
onClick={data.action}
|
||||
data-sound-category={"menu"}
|
||||
onClick={handleAction}
|
||||
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'])}
|
||||
>
|
||||
|
|
@ -287,7 +298,7 @@ export default function ConsoleHomeUI ()
|
|||
const { filter } = Route.useSearch();
|
||||
|
||||
const close = useMutation(closeMutation);
|
||||
|
||||
const router = useRouter();
|
||||
const { ref, focusKey } = useFocusable({
|
||||
forceFocus: true,
|
||||
autoRestoreFocus: false,
|
||||
|
|
@ -296,7 +307,7 @@ export default function ConsoleHomeUI ()
|
|||
preferredChildFocusKey: `home-list`,
|
||||
});
|
||||
|
||||
const setFilter = (filter: string) => Router.navigate({ to: '/', search: { filter }, viewTransition: false, replace: true });
|
||||
const setFilter = (filter: string) => router.navigate({ to: '/', search: { filter }, viewTransition: false, replace: true });
|
||||
|
||||
const { shortcuts } = useShortcutContext();
|
||||
const headerButtons: HeaderButton[] = [];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||
import DotsLoading from '../components/backgrounds/dots';
|
||||
import { Router } from '..';
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||
|
|
@ -16,9 +15,10 @@ export const Route = createFileRoute('/launcher/$source/$id')({
|
|||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const router = useRouter();
|
||||
function HandleGoBack ()
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id }, replace: true });
|
||||
router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id }, replace: true });
|
||||
}
|
||||
|
||||
const { source, id } = Route.useParams();
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ function RouteComponent ()
|
|||
{
|
||||
if (focus)
|
||||
{
|
||||
focusSelf();
|
||||
focusSelf({ instant: true });
|
||||
}
|
||||
}, [focus]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||
import { OptionSpace } from '../../components/options/OptionSpace';
|
||||
import { OptionInput } from '../../components/options/OptionInput';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
|
@ -19,7 +19,6 @@ import Carousel from '@/mainview/components/Carousel';
|
|||
import { FOCUS_KEYS } from '@/mainview/scripts/types';
|
||||
import { scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from '@/mainview/scripts/utils';
|
||||
import { SettingsOption } from '@/mainview/components/options/SettingsOption';
|
||||
import { Router } from '@/mainview';
|
||||
|
||||
export const Route = createFileRoute('/settings/emulators')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -76,7 +75,7 @@ function NewEmulatorPath (data: { addOverride: (emulator: string) => void; isAdd
|
|||
const handleCloseContext = () =>
|
||||
{
|
||||
setNewEmulatorTypeOpen(false);
|
||||
setFocus('emulator');
|
||||
setFocus('emulator', { instant: true });
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -123,7 +122,7 @@ function EmulatorPath (data: { id: string; })
|
|||
const handleCloseSearch = () =>
|
||||
{
|
||||
setIsSearching(false);
|
||||
setFocus(`search-${data.id}`);
|
||||
setFocus(`search-${data.id}`, { instant: true });
|
||||
};
|
||||
|
||||
const handleSelectPath = (path: string) =>
|
||||
|
|
@ -192,6 +191,7 @@ function EmulatorBadge (data: {
|
|||
addOverride: (emulator: string) => void;
|
||||
} & FocusParams)
|
||||
{
|
||||
const router = useRouter();
|
||||
const { focusKey, ref, focused } = useFocusable({
|
||||
focusKey: FOCUS_KEYS.EMULATOR_CARD(data.emulator.name),
|
||||
onFocus (l, p, details) { data.onFocus?.(focusKey, ref.current, details); }
|
||||
|
|
@ -212,12 +212,12 @@ function EmulatorBadge (data: {
|
|||
label: "Visit Store",
|
||||
action ()
|
||||
{
|
||||
Router.navigate({ to: '/store/details/emulator/$id', params: { id: data.emulator.name } });
|
||||
router.navigate({ to: '/store/details/emulator/$id', params: { id: data.emulator.name } });
|
||||
},
|
||||
});
|
||||
}
|
||||
return shortcuts;
|
||||
}, [data.addOverride]);
|
||||
}, [data.addOverride, router]);
|
||||
|
||||
|
||||
let statusIcon = <SearchAlert className={data.emulator.isCritical ? 'text-warning' : 'text-base-content/40'} />;
|
||||
|
|
@ -255,7 +255,7 @@ function EmulatorBadge (data: {
|
|||
case 'store':
|
||||
icon = <Store />;
|
||||
className = "hover:bg-base-content hover:text-base-100 cursor-pointer bg-accent text-accent-content";
|
||||
action = () => { Router.navigate({ to: '/store/details/emulator/$id', params: { id: data.emulator.name } }); };
|
||||
action = () => { router.navigate({ to: '/store/details/emulator/$id', params: { id: data.emulator.name } }); };
|
||||
break;
|
||||
case 'embedded':
|
||||
icon = <Plug />;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ function RouteComponent ()
|
|||
<LocalOption id="backgroundBlur" label="Background Blur" type='checkbox'></LocalOption>
|
||||
<LocalOption id="backgroundAnimation" label="Background Animation" type='checkbox'></LocalOption>
|
||||
<LocalOption id="theme" label="Theme" type='dropdown' values={['dark', 'light', 'auto']}></LocalOption>
|
||||
<LocalOption id='soundEffects' label="Sounds" type='checkbox'></LocalOption>
|
||||
</FocusContext>
|
||||
</ul>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import
|
|||
Outlet,
|
||||
createFileRoute,
|
||||
useMatch,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { ViewTransitionOptions } from "@tanstack/router-core";
|
||||
import classNames from "classnames";
|
||||
|
|
@ -21,20 +22,24 @@ import
|
|||
MonitorCog,
|
||||
Puzzle,
|
||||
} from "lucide-react";
|
||||
import { JSX, useEffect } from "react";
|
||||
import { JSX } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import z from "zod";
|
||||
import { SettingsSchema } from "../../../shared/constants";
|
||||
import { Router } from "../..";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import Shortcuts from "@/mainview/components/Shortcuts";
|
||||
import { HandleGoBack } from "@/mainview/scripts/utils";
|
||||
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
export const Route = createFileRoute("/settings")({
|
||||
component: SettingsUI,
|
||||
validateSearch: z.object({
|
||||
focus: z.keyof(SettingsSchema).optional()
|
||||
})
|
||||
}),
|
||||
staticData: {
|
||||
enterSound: 'openSettings'
|
||||
}
|
||||
});
|
||||
|
||||
function MenuItem (data: {
|
||||
|
|
@ -48,17 +53,18 @@ function MenuItem (data: {
|
|||
label: string;
|
||||
})
|
||||
{
|
||||
const router = useRouter();
|
||||
const acitve = !!useMatch({ from: data.route as any, shouldThrow: false });;
|
||||
const handleNonFocusSelect = () =>
|
||||
{
|
||||
if (data.return)
|
||||
{
|
||||
HandleGoBack();
|
||||
HandleGoBack(router);
|
||||
} else if (!acitve)
|
||||
{
|
||||
Router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
}
|
||||
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref, focusSelf } = useFocusable({
|
||||
focusKey: `menu-item-${data.route}`,
|
||||
|
|
@ -67,7 +73,7 @@ function MenuItem (data: {
|
|||
{
|
||||
if (data.focusSelect && !acitve)
|
||||
{
|
||||
Router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
}
|
||||
(ref.current as HTMLElement).scrollIntoView({ inline: 'center' });
|
||||
},
|
||||
|
|
@ -81,6 +87,7 @@ function MenuItem (data: {
|
|||
<li
|
||||
ref={ref}
|
||||
key={data.route}
|
||||
data-sound-category={"menu"}
|
||||
onClick={data.focusSelect ? focusSelf : handleNonFocusSelect}
|
||||
onFocus={focusSelf}
|
||||
className={twMerge("flex group-focusable cursor-pointer", data.className)}
|
||||
|
|
@ -167,17 +174,13 @@ function SettingsMenu (data: {})
|
|||
|
||||
export function SettingsUI ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
focusKey: "settings-page-layout",
|
||||
preferredChildFocusKey: 'settings-menu'
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
focusSelf();
|
||||
}, []);
|
||||
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: () => HandleGoBack(router) }], [router]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
return (
|
||||
|
|
@ -196,6 +199,7 @@ export function SettingsUI ()
|
|||
<Shortcuts shortcuts={shortcuts} />
|
||||
</div>
|
||||
</div>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</FocusContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import
|
|||
useFocusable,
|
||||
FocusContext,
|
||||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { Router } from "@/mainview";
|
||||
import Shortcuts from "@/mainview/components/Shortcuts";
|
||||
import { AnimatedBackground } from "@/mainview/components/AnimatedBackground";
|
||||
import { systemApi } from "@/mainview/scripts/clientApi";
|
||||
|
|
@ -18,7 +17,7 @@ import Screenshots from "@/mainview/components/Screenshots";
|
|||
import { StickyHeaderUI } from "@/mainview/components/Header";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { EmulatorsSection } from "@/mainview/components/store/EmulatorsSection";
|
||||
import { HandleGoBack, scrollIntoViewHandler, useJobStatus } from "@/mainview/scripts/utils";
|
||||
import { HandleGoBack, scrollIntoViewHandler, useJobStatus, useOnNavigateBack } from "@/mainview/scripts/utils";
|
||||
import toast from "react-hot-toast";
|
||||
import { getErrorMessage } from "react-error-boundary";
|
||||
import { emulatorStatusIcons } from "@/mainview/components/store/StoreEmulatorCard";
|
||||
|
|
@ -27,6 +26,7 @@ import { GamesSection } from "@/mainview/components/store/GamesSection";
|
|||
import { deleteBiosMutation, downloadBiosMutation, installEmulatorMutation, storeEmulatorDeleteMutation, storeEmulatorDetailsQuery, storeEmulatorsRecommendedQuery } from "@queries/store";
|
||||
import { gamesRecommendedBasedOnEmulatorQuery } from "@queries/romm";
|
||||
import FocusTooltip from "@/mainview/components/FocusTooltip";
|
||||
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
||||
|
||||
export const Route = createFileRoute('/store/details/emulator/$id')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -35,6 +35,10 @@ export const Route = createFileRoute('/store/details/emulator/$id')({
|
|||
ctx.context.queryClient.prefetchQuery(storeEmulatorDetailsQuery(ctx.params.id));
|
||||
ctx.context.queryClient.prefetchQuery(storeEmulatorsRecommendedQuery(ctx.params.id));
|
||||
ctx.context.queryClient.prefetchQuery(gamesRecommendedBasedOnEmulatorQuery(ctx.params.id));
|
||||
},
|
||||
staticData: {
|
||||
enterSound: "openDetails",
|
||||
goBackSound: "returnDetails"
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -288,7 +292,7 @@ function Description (data: { emulator?: FrontEndEmulatorDetailed; })
|
|||
export function RouteComponent ()
|
||||
{
|
||||
const { id } = Route.useParams();
|
||||
|
||||
const router = useRouter();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
focusKey: `GAME_DETAIL_${id}`,
|
||||
trackChildren: true,
|
||||
|
|
@ -301,22 +305,16 @@ export function RouteComponent ()
|
|||
|
||||
useShortcuts(focusKey, () => [{
|
||||
label: "Return",
|
||||
action: HandleGoBack,
|
||||
action: () => HandleGoBack(router),
|
||||
button: GamePadButtonCode.B
|
||||
}]);
|
||||
}], [router]);
|
||||
|
||||
const installMutation = useMutation({
|
||||
...installEmulatorMutation(id), onSuccess: (data, variables, onMutateResult, context) => context.client.refetchQueries(storeEmulatorDetailsQuery(id)),
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
focusSelf();
|
||||
}, []);
|
||||
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
|
||||
const stats: StatEntry[] = [];
|
||||
if (emulator)
|
||||
{
|
||||
|
|
@ -341,6 +339,7 @@ export function RouteComponent ()
|
|||
|
||||
return (
|
||||
<AnimatedBackground ref={ref} className="" scrolling>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
<FocusContext.Provider value={focusKey}>
|
||||
<StickyHeaderUI ref={ref} />
|
||||
<div className="flex flex-col z-10">
|
||||
|
|
@ -370,7 +369,7 @@ export function RouteComponent ()
|
|||
onFocus={scrollIntoViewHandler({ block: 'center' })}
|
||||
onSelect={(id, focus) =>
|
||||
{
|
||||
Router.navigate({
|
||||
router.navigate({
|
||||
to: '/store/details/emulator/$id', params: { id }
|
||||
});
|
||||
}}
|
||||
|
|
@ -386,7 +385,7 @@ export function RouteComponent ()
|
|||
</div>
|
||||
<GamesSection showSources={true} onFocus={scrollIntoViewHandler({ behavior: 'smooth', block: 'center' })} onSelect={(id) =>
|
||||
{
|
||||
Router.navigate({
|
||||
router.navigate({
|
||||
to: '/game/$source/$id', params: { id: id.id, source: id.source }
|
||||
});
|
||||
}} games={recommendedGames} /></div>}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Router } from '@/mainview';
|
||||
import { AutoFocus } from '@/mainview/components/AutoFocus';
|
||||
import { FilterUI } from '@/mainview/components/Filters';
|
||||
import { HeaderUI } from '@/mainview/components/Header';
|
||||
import Shortcuts from '@/mainview/components/Shortcuts';
|
||||
|
|
@ -6,19 +6,21 @@ 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 { HandleGoBack, 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 { useMatchRoute, useRouter } from '@tanstack/react-router';
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import z from 'zod';
|
||||
|
||||
export const Route = createFileRoute('/store/tab')({
|
||||
component: RouteComponent,
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() })),
|
||||
staticData: {
|
||||
enterSound: 'openStore'
|
||||
}
|
||||
});
|
||||
|
||||
function useIsSettings (subPath: string)
|
||||
|
|
@ -33,6 +35,7 @@ function useIsSettings (subPath: string)
|
|||
|
||||
function TopArea (data: { filters: Record<string, FilterOption>; })
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: 'top-area',
|
||||
preferredChildFocusKey: `store-tabs`,
|
||||
|
|
@ -44,13 +47,13 @@ function TopArea (data: { filters: Record<string, FilterOption>; })
|
|||
|
||||
useShortcuts("STORE_ROOT", () => [{
|
||||
label: "Return",
|
||||
action: () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-out'] } }),
|
||||
action: () => HandleGoBack(router),
|
||||
button: GamePadButtonCode.B
|
||||
}], []);
|
||||
}], [router]);
|
||||
|
||||
const handleNavigate = (s: string) =>
|
||||
{
|
||||
Router.navigate({ to: `/store/tab/${s === 'home' ? '' : s}`, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
router.navigate({ to: `/store/tab/${s === 'home' ? '' : s}`, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
};
|
||||
|
||||
return <div ref={ref}>
|
||||
|
|
@ -76,6 +79,7 @@ function StoreOutlet ()
|
|||
function RouteComponent ()
|
||||
{
|
||||
// Root spatial nav container
|
||||
const router = useRouter();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
focusKey: "STORE_ROOT",
|
||||
preferredChildFocusKey: 'top-area',
|
||||
|
|
@ -93,25 +97,16 @@ function RouteComponent ()
|
|||
const { shortcuts } = useShortcutContext();
|
||||
const { focus } = Route.useSearch();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (!focus)
|
||||
{
|
||||
focusSelf();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDetails = (type: string, source: string, id: string, focus: string) =>
|
||||
{
|
||||
if (type === 'emulator')
|
||||
{
|
||||
Router.navigate({ to: '/store/details/emulator/$id', params: { id } });
|
||||
router.navigate({ to: '/store/details/emulator/$id', params: { id } });
|
||||
}
|
||||
else if (type === 'game')
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { source: source, id: id } });
|
||||
router.navigate({ to: '/game/$source/$id', params: { source: source, id: id } });
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const handlePrefetch = (type: string, source: string, id: string) =>
|
||||
|
|
@ -150,5 +145,6 @@ function RouteComponent ()
|
|||
</div>
|
||||
</FocusContext.Provider>
|
||||
</StoreContext>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</div >;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue