feat: Implemented audio effects
This commit is contained in:
parent
fe0ab3b498
commit
edbc390d14
125 changed files with 1137 additions and 217 deletions
48
src/mainview/App.tsx
Normal file
48
src/mainview/App.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { Router } from ".";
|
||||
import { useEffect } from "react";
|
||||
import audioCallbacks from "./scripts/audio/audioCallbacks";
|
||||
import { client as rommClient } from "../clients/romm/client.gen";
|
||||
import { RPC_URL } from "@/shared/constants";
|
||||
|
||||
export const focusQueue: string[] = [];
|
||||
|
||||
export default function App (data: { children: any; })
|
||||
{
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const focusMap = new Map<number, string>();
|
||||
rommClient.setConfig({
|
||||
baseUrl: `${RPC_URL(__HOST__)}/api/romm`,
|
||||
credentials: "include",
|
||||
mode: "cors",
|
||||
});
|
||||
|
||||
const unsub = Router.history.subscribe((op) =>
|
||||
{
|
||||
if (op.action.type === 'PUSH')
|
||||
{
|
||||
focusMap.set(op.location.state.__TSR_index - 1, getCurrentFocusKey());
|
||||
} else if (op.action.type === 'BACK')
|
||||
{
|
||||
if (focusMap.has(op.location.state.__TSR_index))
|
||||
{
|
||||
focusQueue.pop();
|
||||
focusQueue.push(focusMap.get(op.location.state.__TSR_index)!);
|
||||
focusMap.delete(op.location.state.__TSR_index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const audio = audioCallbacks();
|
||||
|
||||
return () =>
|
||||
{
|
||||
unsub();
|
||||
audio.cleanup();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <>{data.children}</>;
|
||||
}
|
||||
304
src/mainview/assets/sounds.json
Normal file
304
src/mainview/assets/sounds.json
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
{
|
||||
"sprite": {
|
||||
"Classic UI SFX - Chords #1": [
|
||||
0,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #10": [
|
||||
6000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #11": [
|
||||
12000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #12": [
|
||||
18000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #13": [
|
||||
24000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #14": [
|
||||
30000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #15": [
|
||||
36000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #16": [
|
||||
42000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #17": [
|
||||
48000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #18": [
|
||||
54000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #19": [
|
||||
60000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #2": [
|
||||
66000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #20": [
|
||||
72000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #3": [
|
||||
78000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #4": [
|
||||
84000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #5": [
|
||||
90000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #6": [
|
||||
96000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #7": [
|
||||
102000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #8": [
|
||||
108000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #9": [
|
||||
114000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Short - High #1": [
|
||||
120000,
|
||||
2546.893424036284
|
||||
],
|
||||
"Classic UI SFX - Short - High #10": [
|
||||
124000,
|
||||
2552.0861678004535
|
||||
],
|
||||
"Classic UI SFX - Short - High #11": [
|
||||
128000,
|
||||
2927.0975056689394
|
||||
],
|
||||
"Classic UI SFX - Short - High #12": [
|
||||
132000,
|
||||
2927.0975056689394
|
||||
],
|
||||
"Classic UI SFX - Short - High #13": [
|
||||
136000,
|
||||
3000
|
||||
],
|
||||
"Classic UI SFX - Short - High #14": [
|
||||
140000,
|
||||
2802.0861678004394
|
||||
],
|
||||
"Classic UI SFX - Short - High #15": [
|
||||
144000,
|
||||
2723.9455782312803
|
||||
],
|
||||
"Classic UI SFX - Short - High #16": [
|
||||
148000,
|
||||
2927.0975056689394
|
||||
],
|
||||
"Classic UI SFX - Short - High #17": [
|
||||
152000,
|
||||
2880.226757369627
|
||||
],
|
||||
"Classic UI SFX - Short - High #18": [
|
||||
156000,
|
||||
2359.387755102034
|
||||
],
|
||||
"Classic UI SFX - Short - High #19": [
|
||||
160000,
|
||||
3052.0861678004394
|
||||
],
|
||||
"Classic UI SFX - Short - High #2": [
|
||||
165000,
|
||||
2843.7641723355964
|
||||
],
|
||||
"Classic UI SFX - Short - High #20": [
|
||||
169000,
|
||||
2015.6462585034092
|
||||
],
|
||||
"Classic UI SFX - Short - High #21": [
|
||||
173000,
|
||||
2005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Short - High #22": [
|
||||
177000,
|
||||
2489.5918367346894
|
||||
],
|
||||
"Classic UI SFX - Short - High #23": [
|
||||
181000,
|
||||
2458.3446712018144
|
||||
],
|
||||
"Classic UI SFX - Short - High #24": [
|
||||
185000,
|
||||
2093.7641723355964
|
||||
],
|
||||
"Classic UI SFX - Short - High #25": [
|
||||
189000,
|
||||
2005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Short - High #3": [
|
||||
193000,
|
||||
2864.6031746031613
|
||||
],
|
||||
"Classic UI SFX - Short - High #4": [
|
||||
197000,
|
||||
3031.2698412698464
|
||||
],
|
||||
"Classic UI SFX - Short - High #5": [
|
||||
202000,
|
||||
2598.9795918367236
|
||||
],
|
||||
"Classic UI SFX - Short - High #6": [
|
||||
206000,
|
||||
2427.0975056689394
|
||||
],
|
||||
"Classic UI SFX - Short - High #7": [
|
||||
210000,
|
||||
2468.752834467125
|
||||
],
|
||||
"Classic UI SFX - Short - High #8": [
|
||||
214000,
|
||||
2916.666666666657
|
||||
],
|
||||
"Classic UI SFX - Short - High #9": [
|
||||
218000,
|
||||
2250
|
||||
],
|
||||
"Classic UI SFX - Short - Low #1": [
|
||||
222000,
|
||||
2010.4308390022538
|
||||
],
|
||||
"Classic UI SFX - Short - Low #10": [
|
||||
226000,
|
||||
3020.8390022675644
|
||||
],
|
||||
"Classic UI SFX - Short - Low #11": [
|
||||
231000,
|
||||
2458.3446712018144
|
||||
],
|
||||
"Classic UI SFX - Short - Low #12": [
|
||||
235000,
|
||||
2901.0430839002197
|
||||
],
|
||||
"Classic UI SFX - Short - Low #13": [
|
||||
239000,
|
||||
2843.7641723355964
|
||||
],
|
||||
"Classic UI SFX - Short - Low #14": [
|
||||
243000,
|
||||
3135.4195011337824
|
||||
],
|
||||
"Classic UI SFX - Short - Low #15": [
|
||||
248000,
|
||||
2703.1292517006877
|
||||
],
|
||||
"Classic UI SFX - Short - Low #16": [
|
||||
252000,
|
||||
2875.011337868472
|
||||
],
|
||||
"Classic UI SFX - Short - Low #17": [
|
||||
256000,
|
||||
2927.0975056689394
|
||||
],
|
||||
"Classic UI SFX - Short - Low #18": [
|
||||
260000,
|
||||
3057.2789115646515
|
||||
],
|
||||
"Classic UI SFX - Short - Low #19": [
|
||||
265000,
|
||||
2473.9455782312803
|
||||
],
|
||||
"Classic UI SFX - Short - Low #2": [
|
||||
269000,
|
||||
2583.3333333333144
|
||||
],
|
||||
"Classic UI SFX - Short - Low #20": [
|
||||
273000,
|
||||
2515.646258503409
|
||||
],
|
||||
"Classic UI SFX - Short - Low #21": [
|
||||
277000,
|
||||
2604.172335600879
|
||||
],
|
||||
"Classic UI SFX - Short - Low #22": [
|
||||
281000,
|
||||
3031.2698412698182
|
||||
],
|
||||
"Classic UI SFX - Short - Low #23": [
|
||||
286000,
|
||||
2937.50566893425
|
||||
],
|
||||
"Classic UI SFX - Short - Low #24": [
|
||||
290000,
|
||||
2609.387755102034
|
||||
],
|
||||
"Classic UI SFX - Short - Low #25": [
|
||||
294000,
|
||||
2625.0113378685
|
||||
],
|
||||
"Classic UI SFX - Short - Low #3": [
|
||||
298000,
|
||||
2828.140589569159
|
||||
],
|
||||
"Classic UI SFX - Short - Low #4": [
|
||||
302000,
|
||||
2614.6031746031895
|
||||
],
|
||||
"Classic UI SFX - Short - Low #5": [
|
||||
306000,
|
||||
3161.4739229024735
|
||||
],
|
||||
"Classic UI SFX - Short - Low #6": [
|
||||
311000,
|
||||
2333.3333333333144
|
||||
],
|
||||
"Classic UI SFX - Short - Low #7": [
|
||||
315000,
|
||||
2536.4625850340303
|
||||
],
|
||||
"Classic UI SFX - Short - Low #8": [
|
||||
319000,
|
||||
2630.2267573695985
|
||||
],
|
||||
"Classic UI SFX - Short - Low #9": [
|
||||
323000,
|
||||
2697.936507936504
|
||||
],
|
||||
"UI_Single_Set 16_01": [
|
||||
327000,
|
||||
309.5918367346826
|
||||
],
|
||||
"UI_Single_Set 16_02": [
|
||||
329000,
|
||||
309.5918367346826
|
||||
],
|
||||
"UI_Single_Set 16_03": [
|
||||
331000,
|
||||
309.5918367346826
|
||||
],
|
||||
"UI_TwoNote_Set 15_01": [
|
||||
333000,
|
||||
335.2380952380827
|
||||
],
|
||||
"UI_TwoNote_Set 15_02": [
|
||||
335000,
|
||||
309.5918367346826
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
src/mainview/assets/sounds.ogg
(Stored with Git LFS)
Normal file
BIN
src/mainview/assets/sounds.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
import { doesFocusableExist, FocusDetails, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useLayoutEffect } from "react";
|
||||
|
||||
export function AutoFocus (data: {
|
||||
parentKey?: string;
|
||||
|
|
@ -8,7 +8,7 @@ export function AutoFocus (data: {
|
|||
delay?: number;
|
||||
})
|
||||
{
|
||||
useEffect(() =>
|
||||
useLayoutEffect(() =>
|
||||
{
|
||||
let delayTimeout: number | undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import classNames from "classnames";
|
|||
import { JSX } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import useActiveControl from "../scripts/gamepads";
|
||||
import { oneShot } from "../scripts/audio/audio";
|
||||
|
||||
export function GameCardSkeleton ()
|
||||
{
|
||||
|
|
@ -38,10 +39,15 @@ export interface GameCardParams
|
|||
|
||||
export default function CardElement (data: GameCardParams & InteractParams)
|
||||
{
|
||||
const handleAction = () =>
|
||||
{
|
||||
data.onAction?.();
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref, focused, focusSelf } = useFocusable({
|
||||
focusKey: data.focusKey,
|
||||
onFocus: (l, p, details) => data.onFocus?.(data.id, ref.current as any, details),
|
||||
onEnterPress: () => data.onAction?.(),
|
||||
onEnterPress: handleAction,
|
||||
onBlur: () => data.onBlur?.(data.id),
|
||||
});
|
||||
const { isPointer } = useActiveControl();
|
||||
|
|
@ -57,11 +63,10 @@ export default function CardElement (data: GameCardParams & InteractParams)
|
|||
scrollSnapAlign: isPointer ? "center" : "none"
|
||||
}}
|
||||
onFocus={focusSelf}
|
||||
onDoubleClick={e => data.onAction?.(e.nativeEvent)}
|
||||
onClick={() =>
|
||||
{
|
||||
focusSelf();
|
||||
data.onAction?.();
|
||||
handleAction();
|
||||
}}
|
||||
className={twMerge(
|
||||
"relative game-card light:bg-base-100 dark:bg-base-300 flex flex-col z-5 overflow-hidden transition-all duration-200 not-mobile:drop-shadow-lg cursor-pointer focusable focusable-primary focusable-hover select-none focused focused:not-control-mouse:animate-wiggle focused:not-control-mouse:bg-base-content focused:not-control-mouse:text-base-300 focused:not-control-mouse:drop-shadow-lg focused:not-control-mouse:drop-shadow-black/30 focused:not-control-mouse:scale-102 focused:not-control-mouse:z-10 group control-mouse:hover:bg-base-200 h-full [--tw-border-style:inset] border-2 border-base-content/5 backdrop-opacity-0 active:bg-base-content! active:text-base-100 active:transition-none",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useSuspenseQuery } from "@tanstack/react-query";
|
|||
import { CardList, GameMetaExtra } from "./CardList";
|
||||
import { GameCardFocusHandler } from "./CardElement";
|
||||
import { getCollectionsQuery } from "@queries/romm";
|
||||
import { Router } from "..";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
export default function CollectionList (data: {
|
||||
id: string,
|
||||
|
|
@ -14,12 +14,13 @@ export default function CollectionList (data: {
|
|||
saveChildFocus?: 'session' | 'local';
|
||||
})
|
||||
{
|
||||
const router = useRouter();
|
||||
const { data: collections } = useSuspenseQuery(getCollectionsQuery);
|
||||
|
||||
const handleDefaultSelect = (gameId: string) =>
|
||||
{
|
||||
const [source, id] = gameId.split('@');
|
||||
Router.navigate({
|
||||
router.navigate({
|
||||
to: `/collection/$source/$id`,
|
||||
params: { source, id },
|
||||
search: { countHint: collections.find(c => c.id.id === id && c.id.source === source)?.game_count }
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { AnimatedBackground } from './AnimatedBackground';
|
||||
import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { HeaderUI, StickyHeaderUI } from './Header';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { StickyHeaderUI } from './Header';
|
||||
import { GameList } from './GameList';
|
||||
import { Search, Settings2 } from 'lucide-react';
|
||||
import { JSX, Suspense, useEffect } from 'react';
|
||||
import { JSX, Suspense } from 'react';
|
||||
import Shortcuts from './Shortcuts';
|
||||
import { AutoFocus } from './AutoFocus';
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||
import { GameListFilterType } from '@/shared/constants';
|
||||
import { GameCardFocusHandler } from './CardElement';
|
||||
import { HandleGoBack, useStickyDataAttr } from '../scripts/utils';
|
||||
import { HandleGoBack } from '../scripts/utils';
|
||||
import LoadingCardList from './LoadingCardList';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { gameQuery } from '../scripts/queries/romm';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
|
||||
export interface CollectionsDetailParams
|
||||
{
|
||||
|
|
@ -29,6 +29,7 @@ export interface CollectionsDetailParams
|
|||
|
||||
export function CollectionsDetail (data: CollectionsDetailParams)
|
||||
{
|
||||
const router = useRouter();
|
||||
const builtData = useQuery({
|
||||
queryKey: ['filter', data.id], queryFn: async () =>
|
||||
{
|
||||
|
|
@ -42,7 +43,7 @@ export function CollectionsDetail (data: CollectionsDetailParams)
|
|||
preferredChildFocusKey: `${focusKey}-list`
|
||||
});
|
||||
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: () => HandleGoBack(router) }], [router]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
const handleScroll: GameCardFocusHandler = (cardId, node, details) =>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { X } from "lucide-react";
|
|||
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
|
||||
import { ContextDialogContext } from "../scripts/contexts";
|
||||
import { FOCUS_KEYS } from "../scripts/types";
|
||||
import { oneShot } from "../scripts/audio/audio";
|
||||
|
||||
export function ContextList (data: {
|
||||
options?: DialogEntry[];
|
||||
|
|
@ -34,6 +35,7 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class
|
|||
{
|
||||
if (data.disabled === true) return;
|
||||
data.action?.({ close: context.close, focus: focusSelf });
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref, focusSelf, focusKey } = useFocusable({
|
||||
focusKey: FOCUS_KEYS.CONTEXT_DIALOG_OPTION(context.id, data.id),
|
||||
|
|
@ -57,6 +59,7 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class
|
|||
onClick={handleAction}
|
||||
data-selected={data.selected}
|
||||
aria-disabled={data.disabled}
|
||||
data-sound-category={"menu"}
|
||||
className={
|
||||
twMerge("flex cursor-pointer sm:text-sm md:text-base group-focusable scroll-m-4")}>
|
||||
<FocusContext value={focusKey}>
|
||||
|
|
@ -100,10 +103,10 @@ export function useContextDialog (id: string, data: { content?: JSX.Element; cla
|
|||
data.onClose?.();
|
||||
if (newSourceFocusKey)
|
||||
{
|
||||
setFocus(newSourceFocusKey);
|
||||
setFocus(newSourceFocusKey, { instant: true });
|
||||
} else if (sourceFocusKey)
|
||||
{
|
||||
setFocus(sourceFocusKey);
|
||||
setFocus(sourceFocusKey, { instant: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,12 +140,14 @@ export function ContextDialog (data: {
|
|||
const handleClose = () =>
|
||||
{
|
||||
data.close(false);
|
||||
oneShot('closeContext');
|
||||
};
|
||||
useEffect(() =>
|
||||
{
|
||||
if (data.open)
|
||||
{
|
||||
focusSelf();
|
||||
focusSelf({ instant: true });
|
||||
oneShot('openContext');
|
||||
}
|
||||
}, [data.open]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { Home, TriangleAlert } from "lucide-react";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
|
||||
import { Router } from "..";
|
||||
import Shortcuts from "./Shortcuts";
|
||||
import { Button } from "./options/Button";
|
||||
import { useEffect } from "react";
|
||||
import { ErrorComponentProps } from "@tanstack/react-router";
|
||||
import { ErrorComponentProps, useRouter } from "@tanstack/react-router";
|
||||
|
||||
export default function Error (data: ErrorComponentProps)
|
||||
{
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "not-found" });
|
||||
const handleReturn = () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } });
|
||||
const router = useRouter();
|
||||
const handleReturn = () => router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } });
|
||||
useShortcuts(focusKey, () => [{ label: "Return Home", button: GamePadButtonCode.B, action: handleReturn }]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
useEffect(() => { focusSelf(); }, []);
|
||||
useEffect(() => { focusSelf({ instant: true }); }, []);
|
||||
|
||||
return <div ref={ref} className="absolute flex flex-col justify-center items-center w-full h-full gap-4">
|
||||
<FocusContext value={focusKey}>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import SvgIcon from "./SvgIcon";
|
|||
import { twMerge } from "tailwind-merge";
|
||||
import { useEffect } from "react";
|
||||
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
|
||||
import { oneShot } from "../scripts/audio/audio";
|
||||
|
||||
function FilterCat (
|
||||
data: {
|
||||
|
|
@ -19,7 +20,10 @@ function FilterCat (
|
|||
{
|
||||
const { ref, focusSelf } = useFocusable({
|
||||
focusKey: data.id,
|
||||
onFocus: (l, p, details) => data.onFocus?.(data.id, ref.current, details),
|
||||
onFocus: (l, p, details) =>
|
||||
{
|
||||
data.onFocus?.(data.id, ref.current, details);
|
||||
},
|
||||
onEnterPress: data.onAction
|
||||
});
|
||||
|
||||
|
|
@ -27,7 +31,8 @@ function FilterCat (
|
|||
<li
|
||||
aria-selected={data.active}
|
||||
ref={ref}
|
||||
onClick={focusSelf}
|
||||
onClick={e => focusSelf({ event: e.nativeEvent })}
|
||||
data-sound-category={data.active ? undefined : "filter"}
|
||||
className={"sm:text-sm sm:px-2 flex md:px-4 items-center justify-center rounded-full transition-all md:text-lg focusable focusable-primary hover:not-focused:not-aria-selected:bg-base-content/40 not-focused:cursor-pointer aria-selected:bg-base-content aria-selected:text-base-300 aria-selected:drop-shadow aria-selected:cursor-default active:bg-accent! active:text-accent-content! active:ring-offset-7 active:ring-offset-base-content select-none gap-1"}
|
||||
>
|
||||
{data.icon ? <><div className="sm:portrait:px-2">{data.icon}</div><div className="sm:portrait:hidden md:inline">{data.children ?? data.label}</div></> : <div>{data.children ?? data.label}</div>}
|
||||
|
|
@ -68,6 +73,10 @@ export function FilterUI (data: {
|
|||
if (!data.options[newFilter].selected)
|
||||
{
|
||||
data.setSelected(newFilter);
|
||||
oneShot('selectFilter');
|
||||
} else
|
||||
{
|
||||
oneShot('invalidNavigation');
|
||||
}
|
||||
},
|
||||
button: GamePadButtonCode.R1
|
||||
|
|
@ -80,7 +89,13 @@ export function FilterUI (data: {
|
|||
const selectedFilterIndex = Math.max(0, filterIndex - 1,);
|
||||
const newFilter = filterKeys[selectedFilterIndex];
|
||||
if (!data.options[newFilter].selected)
|
||||
{
|
||||
data.setSelected(newFilter);
|
||||
oneShot('selectFilter');
|
||||
} else
|
||||
{
|
||||
oneShot('invalidNavigation');
|
||||
}
|
||||
},
|
||||
button: GamePadButtonCode.L1
|
||||
}], [data.options]);
|
||||
|
|
@ -90,7 +105,7 @@ export function FilterUI (data: {
|
|||
{
|
||||
if (hasFocusedChild)
|
||||
{
|
||||
setFocus(`${data.id}-${defaultFocus}`);
|
||||
setFocus(`${data.id}-${defaultFocus}`, { instant: true });
|
||||
}
|
||||
}, [hasFocusedChild, defaultFocus, data.id]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { RPC_URL } from "@/shared/constants";
|
||||
import CardElement from "./CardElement";
|
||||
import { Router } from "..";
|
||||
import { FileQuestion, HardDrive, Store } from "lucide-react";
|
||||
import { JSX } from "react";
|
||||
import { FOCUS_KEYS } from "../scripts/types";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
export default function FrontEndGameCard (data: { index: number, game: FrontEndGameType; showSource?: boolean; } & FocusParams & InteractParams)
|
||||
{
|
||||
const router = useRouter();
|
||||
function handleDefaultSelect (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 } });
|
||||
};
|
||||
|
||||
const platformUrl = new URL(`${RPC_URL(__HOST__)}${data.game.path_platform_cover}`);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import
|
|||
Bell,
|
||||
Bluetooth,
|
||||
Clock,
|
||||
Plug,
|
||||
Settings,
|
||||
Wifi,
|
||||
WifiHigh,
|
||||
|
|
@ -23,17 +22,15 @@ import
|
|||
} from "lucide-react";
|
||||
import { RoundButton } from "./RoundButton";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { RPC_URL, SystemInfoType } from "../../shared/constants";
|
||||
import { JSX, RefObject, useContext, useEffect, useRef, useState } from "react";
|
||||
import { systemApi } from "../scripts/clientApi";
|
||||
import { Router } from "..";
|
||||
import { useStickyDataAttr } from "../scripts/utils";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { TwitchIcon } from "../scripts/brandIcons";
|
||||
import { rommLoggedInQuery, rommUserQuery } from "../scripts/queries/romm";
|
||||
import { rommLoggedInQuery } from "../scripts/queries/romm";
|
||||
import { twitchLoginVerificationQuery } from "../scripts/queries/settings";
|
||||
import { da } from "zod/v4/locales";
|
||||
import { SystemInfoContext } from "../scripts/contexts";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { oneShot } from "../scripts/audio/audio";
|
||||
|
||||
function HeaderAvatar (data: {
|
||||
id: string;
|
||||
|
|
@ -206,19 +203,23 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; })
|
|||
placeholderData: keepPreviousData
|
||||
});
|
||||
|
||||
const { ref } = useFocusable({ focusKey: 'accounts' });
|
||||
const handleSelect = () =>
|
||||
{
|
||||
router.navigate({ to: '/settings/accounts' });
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref } = useFocusable({
|
||||
focusKey: 'accounts', onEnterPress: handleSelect
|
||||
});
|
||||
|
||||
const accounts: HeaderAccount[] = [];
|
||||
if (data.accounts) accounts.push(...data.accounts);
|
||||
const router = useRouter();
|
||||
|
||||
if (rommUser.data?.hasLogin || rommUser.isError)
|
||||
{
|
||||
accounts.push({
|
||||
id: 'romm', preview: `https://romm.app/_ipx/q_80/images/blocks/logos/romm.svg`,
|
||||
action: () =>
|
||||
{
|
||||
Router.navigate({ to: '/settings/accounts', search: { focus: 'rommAddress' } });
|
||||
},
|
||||
className: rommUser.data?.hasLogin && !rommUser.isError ? undefined : "border-error",
|
||||
type: 'secondary'
|
||||
});
|
||||
|
|
@ -228,15 +229,11 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; })
|
|||
{
|
||||
accounts.push({
|
||||
id: 'twitch', preview: TwitchIcon,
|
||||
action: () =>
|
||||
{
|
||||
Router.navigate({ to: '/settings/accounts', search: { focus: 'rommAddress' } });
|
||||
},
|
||||
type: 'secondary'
|
||||
});
|
||||
}
|
||||
|
||||
return <div ref={ref} className="avatar-group cursor-pointer -space-x-6 w-fit flex items-center gap-2 drop-shadow-sm overflow-visible rounded-3xl focusable focusable-hover ">
|
||||
return <div onClick={handleSelect} ref={ref} style={{ viewTimelineName: "header-accounts" }} className="avatar-group cursor-pointer -space-x-6 w-fit flex items-center gap-2 drop-shadow-sm overflow-visible rounded-3xl focusable focusable-hover ">
|
||||
{accounts?.map(a => <HeaderAvatar
|
||||
key={`header-avatar-${a.id}`}
|
||||
id={`account-${a.id}`}
|
||||
|
|
@ -285,9 +282,10 @@ interface HeaderUIParams
|
|||
export function HeaderUI (data: HeaderUIParams)
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({ focusKey: "header-elements", focusable: data.focusable, preferredChildFocusKey: data.preferredChildFocusKey });
|
||||
const router = useRouter();
|
||||
const goToSettings = () =>
|
||||
{
|
||||
Router.navigate({ to: '/settings/accounts' });
|
||||
router.navigate({ to: '/settings/accounts' });
|
||||
};
|
||||
return (
|
||||
<FocusContext.Provider value={focusKey}>
|
||||
|
|
@ -296,7 +294,7 @@ export function HeaderUI (data: HeaderUIParams)
|
|||
className="flex items-center justify-between text-base-content"
|
||||
style={{ viewTimelineName: 'header' }}
|
||||
>
|
||||
<HeaderAccounts accounts={data.accounts} />
|
||||
<HeaderAccounts key={"header-accounts"} accounts={data.accounts} />
|
||||
{data.title}
|
||||
<HeaderStatusBar key={"header-status-bar"} buttonElements={data.buttonElements} buttons={[...data.buttons ?? [], { icon: <Settings />, id: "settings", action: goToSettings, external: true }]} />
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export default function LoadMoreButton (data: { isFetching: boolean; lastId?: Fr
|
|||
};
|
||||
|
||||
const { ref, focusKey, focused } = useFocusable({
|
||||
focusable: !data.isFetching,
|
||||
focusKey: 'load-more-btn',
|
||||
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details),
|
||||
onEnterPress: handleAction
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { Home, TriangleAlert } from "lucide-react";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
|
||||
import { Router } from "..";
|
||||
import Shortcuts from "./Shortcuts";
|
||||
import { Button } from "./options/Button";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
export default function NotFound ()
|
||||
{
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "not-found" });
|
||||
const handleReturn = () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } });
|
||||
const router = useRouter();
|
||||
const handleReturn = () => router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } });
|
||||
useShortcuts(focusKey, () => [{ label: "Return Home", button: GamePadButtonCode.B, action: handleReturn }]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
useEffect(() => { focusSelf(); }, []);
|
||||
useEffect(() => { focusSelf({ instant: true }); }, []);
|
||||
|
||||
return <div ref={ref} className="absolute flex flex-col justify-center items-center w-full h-full gap-4">
|
||||
<FocusContext value={focusKey}>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export default function Screenshots (data: { screenshots?: string[]; className?:
|
|||
const closest = findClosestElementToCenter(scrollRef.current);
|
||||
if (!closest) return;
|
||||
const closestIndex = Array.from(scrollRef.current.children).indexOf(closest);
|
||||
setFocus(`screenshot-${closestIndex}`);
|
||||
setFocus(`screenshot-${closestIndex}`, { instant: true });
|
||||
}
|
||||
}, [focused, hasFocusedChild, scrollRef.current]);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ import MainActions from "./MainActions";
|
|||
import ActionButton from "./ActionButton";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import FocusTooltip from "../FocusTooltip";
|
||||
import { Router } from "@/mainview";
|
||||
import { useBlocker } from "@tanstack/react-router";
|
||||
import { useBlocker, useRouter } from "@tanstack/react-router";
|
||||
|
||||
function AchievementsInfo (data: { game: FrontEndGameTypeDetailed; } & InteractParams)
|
||||
{
|
||||
|
|
@ -35,11 +34,12 @@ export default function ActionButtons (data: { game?: FrontEndGameTypeDetailed,
|
|||
const [, setDetailsSection] = useLocalStorage('details-section', 'screenshots');
|
||||
|
||||
const { ref, focusKey, hasFocusedChild } = useFocusable({ focusKey: 'actions', forceFocus: true, trackChildren: true, preferredChildFocusKey: 'mainAction' });
|
||||
const router = useRouter();
|
||||
const deleteMutation = useMutation({
|
||||
...deleteGameMutation({ id: data.id, source: data.source }),
|
||||
onSuccess: (d, v, r, ctx) =>
|
||||
{
|
||||
ctx.client.invalidateQueries(gameInvalidationQuery(data.id, data.source)).then(() => Router.history.back());
|
||||
ctx.client.invalidateQueries(gameInvalidationQuery(data.id, data.source)).then(() => router.history.back());
|
||||
},
|
||||
onError (error)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Router } from "@/mainview";
|
||||
import { rommApi } from "@/mainview/scripts/clientApi";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { JSX, useEffect, useRef, useState } from "react";
|
||||
|
|
@ -9,10 +8,12 @@ import { ContextList, DialogEntry, useContextDialog } from "../ContextDialog";
|
|||
import { Clock, Download, EllipsisVertical, Import, PackageOpen, Play, TriangleAlert } from "lucide-react";
|
||||
import { gameInvalidationQuery, installMutation, playMutation } from "@/mainview/scripts/queries/romm";
|
||||
import ActionButton from "./ActionButton";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
export default function MainActions (data: { game?: FrontEndGameTypeDetailed, source: string, id: string; })
|
||||
{
|
||||
const installMut = useMutation(installMutation(data.source, data.id));
|
||||
const router = useRouter();
|
||||
const playMut = useMutation({
|
||||
...playMutation, onError (error)
|
||||
{
|
||||
|
|
@ -20,7 +21,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
},
|
||||
onSuccess (data, { source, id }, onMutateResult, context)
|
||||
{
|
||||
Router.navigate({ to: '/launcher/$source/$id', params: { source: source, id: id }, replace: true });
|
||||
router.navigate({ to: '/launcher/$source/$id', params: { source: source, id: id }, replace: true });
|
||||
},
|
||||
});
|
||||
const ws = useRef<{ send: (data: string) => void; }>(undefined);
|
||||
|
|
@ -58,10 +59,10 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
{
|
||||
if (localId)
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { id: String(localId), source: 'local' }, replace: true });
|
||||
router.navigate({ to: '/game/$source/$id', params: { id: String(localId), source: 'local' }, replace: true });
|
||||
} else
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { id: data.id, source: data.source }, replace: true });
|
||||
router.navigate({ to: '/game/$source/$id', params: { id: data.id, source: data.source }, replace: true });
|
||||
}
|
||||
});
|
||||
} else if (e.data.status === 'error')
|
||||
|
|
@ -78,7 +79,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
sub.close();
|
||||
ws.current = undefined;
|
||||
};
|
||||
}, [data.source, data.id]);
|
||||
}, [data.source, data.id, router]);
|
||||
|
||||
let progressIcon: JSX.Element | undefined = undefined;
|
||||
switch (status)
|
||||
|
|
@ -107,7 +108,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
if (cmd.emulator === 'EMULATORJS')
|
||||
{
|
||||
const params = new URLSearchParams(cmd.command);
|
||||
Router.navigate({ to: '/embedded/$source/$id', params: { source: data.source, id: data.id }, search: Object.fromEntries(params.entries()), replace: true });
|
||||
router.navigate({ to: '/embedded/$source/$id', params: { source: data.source, id: data.id }, search: Object.fromEntries(params.entries()), replace: true });
|
||||
} else
|
||||
{
|
||||
playMut.mutate({ source: data.source, id: data.id, command_id: cmd.id });
|
||||
|
|
@ -142,7 +143,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
{
|
||||
if (status === 'missing-emulator')
|
||||
{
|
||||
Router.navigate({ to: '/settings/directories' });
|
||||
router.navigate({ to: '/settings/directories' });
|
||||
}
|
||||
}}
|
||||
id="mainAction">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import
|
|||
import classNames from "classnames";
|
||||
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { CSSProperties } from "react";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
export type ButtonStyle = 'base' | 'accent' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
|
|
@ -35,9 +36,14 @@ export function Button (data: {
|
|||
tooltipType?: "base" | "accent" | "error";
|
||||
} & InteractParams & FocusParams)
|
||||
{
|
||||
const handleAction = (e?: any) =>
|
||||
{
|
||||
data.onAction?.(e);
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref, focused, focusKey } = useFocusable({
|
||||
focusKey: data.id,
|
||||
onEnterPress: data.onAction,
|
||||
onEnterPress: () => handleAction(),
|
||||
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details),
|
||||
focusable: !data.disabled
|
||||
});
|
||||
|
|
@ -49,7 +55,7 @@ export function Button (data: {
|
|||
|
||||
return <button
|
||||
ref={ref}
|
||||
onClick={e => data.onAction?.(e.nativeEvent)}
|
||||
onClick={handleAction}
|
||||
disabled={data.disabled}
|
||||
data-tooltip={data.tooltip}
|
||||
data-tooltip_type={data.tooltipType}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
|||
import { ContextDialog, ContextList, DialogEntry } from "../ContextDialog";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
export function OptionDropdown (data: {
|
||||
name: string;
|
||||
|
|
@ -23,6 +24,7 @@ export function OptionDropdown (data: {
|
|||
const handlePress = () =>
|
||||
{
|
||||
setOpen(true);
|
||||
oneShot('click');
|
||||
};
|
||||
const handleClose = () => setOpen(false);
|
||||
const { ref } = useFocusable({
|
||||
|
|
@ -33,11 +35,7 @@ export function OptionDropdown (data: {
|
|||
<>
|
||||
<label ref={ref} className={twMerge("flex group-focusable items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent")}>
|
||||
{!!data.icon && <span className={"text-base-content/80 is-focused:text-primary-content"}>{data.icon}</span>}
|
||||
<button onClick={() =>
|
||||
{
|
||||
console.log("Open");
|
||||
setOpen(true);
|
||||
}} className={'flex items-center justify-center border h-10 border-base-content/30 px-4 py-2 rounded-full cursor-pointer grow not-in-focused:bg-base-200 focusable focusable-accent hover:border-base-content hover:bg-base-content hover:text-base-300'}>{data.value}<ChevronDown /></button>
|
||||
<button onClick={handlePress} className={'flex items-center justify-center border h-10 border-base-content/30 px-4 py-2 rounded-full cursor-pointer grow not-in-focused:bg-base-200 focusable focusable-accent hover:border-base-content hover:bg-base-content hover:text-base-300 active:bg-primary active:text-primary-content active:border-0'}>{data.value}<ChevronDown /></button>
|
||||
</label>
|
||||
{open && <ContextDialog id={`${data.name}-context`} preferredChildFocusKey={FOCUS_KEYS.CONTEXT_DIALOG_OPTION(`${data.name}-context`, String(data.values.indexOf(data.value ?? '')))} open={true} close={handleClose}>
|
||||
<ContextList options={data.values.map((v, i) => ({
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useOptionContext } from "./OptionSpace";
|
|||
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { systemApi } from "../../scripts/clientApi";
|
||||
import { CheckIcon, X } from "lucide-react";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
export function OptionInput (data: {
|
||||
name: string;
|
||||
|
|
@ -27,6 +28,7 @@ export function OptionInput (data: {
|
|||
{
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref } = useFocusable({
|
||||
focusKey: data.name, onEnterPress: handlePress
|
||||
|
|
@ -79,12 +81,14 @@ export function OptionInput (data: {
|
|||
name={data.name}
|
||||
checked={Boolean(data.value)}
|
||||
type={data.type}
|
||||
onClick={() => { oneShot("click"); }}
|
||||
autoComplete={data.autocomplete}
|
||||
onFocus={handleFocus}
|
||||
placeholder={data.placeholder}
|
||||
onChange={e => data.onChange?.(e.target.checked)}
|
||||
onBlur={data.onBlur}
|
||||
className={twMerge(
|
||||
"active:bg-base-content rounded-full",
|
||||
data.className
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export function PathSettingsOptionBase (data: PathSettingsOptionParams & {
|
|||
const handleCloseSeatch = () =>
|
||||
{
|
||||
setIsBrowsing(false);
|
||||
setFocus(`${data.id}-browse`);
|
||||
setFocus(`${data.id}-browse`, { instant: true });
|
||||
};
|
||||
|
||||
const handleInputBlur = () =>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ import { ChevronRight, Joystick } from "lucide-react";
|
|||
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { scrollIntoNearestParent, useDragScroll } from "@/mainview/scripts/utils";
|
||||
import FocusDots from "../FocusDots";
|
||||
import { Router } from "@/mainview";
|
||||
import { StoreEmulatorCard } from "./StoreEmulatorCard";
|
||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import Carousel from "../Carousel";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
function SeeAllCard (data: { id: string; onAction: () => void; onFocus?: (details: { node: HTMLElement, instant: boolean; }) => void; })
|
||||
function SeeAllCard (data: { id: string; onAction: () => void; onFocus?: (details: { node: HTMLElement, instant?: boolean; }) => void; })
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: data.id,
|
||||
|
|
@ -39,6 +39,7 @@ export function EmulatorsSection (data: {
|
|||
header?: any;
|
||||
} & FocusParams)
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: FOCUS_KEYS.EMULATOR_SECTION(data.id),
|
||||
trackChildren: true,
|
||||
|
|
@ -68,7 +69,7 @@ export function EmulatorsSection (data: {
|
|||
scrollIntoNearestParent(node, { behavior: details.instant ? 'instant' : 'smooth' });
|
||||
}} />
|
||||
)) ?? Array.from({ length: 8 }).map((_, i) => <div key={i} className="skeleton h-38 w-full rounded-4xl" />)}
|
||||
<SeeAllCard id={`${FOCUS_KEYS.EMULATOR_SECTION}-see-all`} onAction={() => Router.navigate({ to: '/store/tab/emulators', viewTransition: { types: ['zoom-in'] } })} onFocus={({ node, instant }) => scrollIntoNearestParent(node, { behavior: instant ? 'instant' : 'smooth' })} />
|
||||
<SeeAllCard id={`${FOCUS_KEYS.EMULATOR_SECTION}-see-all`} onAction={() => router.navigate({ to: '/store/tab/emulators', viewTransition: { types: ['zoom-in'] } })} onFocus={({ node, instant }) => scrollIntoNearestParent(node, { behavior: instant ? 'instant' : 'smooth' })} />
|
||||
</Carousel>
|
||||
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function GamesSection (data: {
|
|||
useEffect(() =>
|
||||
{
|
||||
if (focused)
|
||||
focusSelf();
|
||||
focusSelf({ instant: true });
|
||||
}, [!!data.games]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { ChevronRight, CircleQuestionMark, SearchAlert } from "lucide-react";
|
|||
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { RPC_URL } from "@/shared/constants";
|
||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
// ── Single missing-emulator card ───────────────────────────────────────────
|
||||
interface MissingCardProps
|
||||
|
|
@ -19,7 +20,11 @@ interface MissingCardProps
|
|||
|
||||
function MissingCard ({ emulator: em, onSelect }: MissingCardProps)
|
||||
{
|
||||
const handleSelect = () => onSelect?.(em.name, focusKey);
|
||||
const handleSelect = () =>
|
||||
{
|
||||
onSelect?.(em.name, focusKey);
|
||||
oneShot('click');
|
||||
};
|
||||
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: FOCUS_KEYS.MISSING_CARD(em.name),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { BadgeCheck, ChevronRight, EllipsisVertical, FileQuestion, IceCream2, Pa
|
|||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import { FlatpackIcon } from "@/mainview/scripts/brandIcons";
|
||||
import { JSX } from "react";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
export const emulatorStatusIcons: Record<string, JSX.Element> = {
|
||||
store: <Store />,
|
||||
|
|
@ -26,7 +27,11 @@ export function StoreEmulatorCard (data: {
|
|||
className?: string;
|
||||
})
|
||||
{
|
||||
const handleSelect = () => data.onSelect?.(data.emulator.name, focusKey);
|
||||
const handleSelect = () =>
|
||||
{
|
||||
data.onSelect?.(data.emulator.name, focusKey);
|
||||
oneShot('click');
|
||||
};
|
||||
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: FOCUS_KEYS.EMULATOR_CARD(data.id),
|
||||
|
|
@ -45,6 +50,7 @@ export function StoreEmulatorCard (data: {
|
|||
ref={ref}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
data-sound-category="emulator"
|
||||
data-installed={data.emulator.validSources.some(s => s.exists)}
|
||||
onClick={isTouch ? handleSelect : undefined}
|
||||
className={twMerge("relative focusable focusable-info bg-base-100 rounded-4xl transition-shadow focused:not-control-mouse:animate-scale-small shadow-lg border border-base-content/10 active:ring-4 active:ring-base-content active:transition-none", data.className)}
|
||||
|
|
@ -87,7 +93,7 @@ export function StoreEmulatorCard (data: {
|
|||
</div>;
|
||||
})}
|
||||
{isMouse && <>
|
||||
<Button onAction={handleSelect} style="base" className="grow text-base-content/40" id={`${data.emulator.name}-details`} >Details<ChevronRight /></Button>
|
||||
<Button onAction={e => data.onSelect?.(data.emulator.name, focusKey)} style="base" className="grow text-base-content/40" id={`${data.emulator.name}-details`} >Details<ChevronRight /></Button>
|
||||
</>}
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -464,7 +464,7 @@ const assets = new Set<string>([
|
|||
]);
|
||||
|
||||
// Store basePath resolved from Vite config
|
||||
const BASE_PATH = "./";
|
||||
const BASE_PATH = "/";
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,15 +9,13 @@ import
|
|||
} from "@tanstack/react-router";
|
||||
import { routeTree } from "./gen/routeTree.gen";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RPC_URL } from "../shared/constants";
|
||||
import "./scripts/gamepads";
|
||||
import "./scripts/windowEvents";
|
||||
import { client as rommClient } from "../clients/romm/client.gen";
|
||||
import "./scripts/spatialNavigation";
|
||||
import NotFound from "./components/NotFound";
|
||||
import Error from "./components/Error";
|
||||
import serviceWorker from './scripts/serviceWorker?worker&url';
|
||||
import { getCurrentFocusKey, setFocus } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import App from "./App";
|
||||
|
||||
if ('serviceWorker' in navigator)
|
||||
{
|
||||
|
|
@ -26,12 +24,6 @@ if ('serviceWorker' in navigator)
|
|||
|
||||
const hashHistory = createHashHistory({});
|
||||
|
||||
rommClient.setConfig({
|
||||
baseUrl: `${RPC_URL(__HOST__)}/api/romm`,
|
||||
credentials: "include",
|
||||
mode: "cors",
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export interface RouterContext
|
||||
|
|
@ -66,25 +58,6 @@ export const Router = createRouter({
|
|||
}
|
||||
});
|
||||
|
||||
const focusMap = new Map<number, string>();
|
||||
export const focusQueue: string[] = [];
|
||||
|
||||
Router.history.subscribe((op) =>
|
||||
{
|
||||
if (op.action.type === 'PUSH')
|
||||
{
|
||||
focusMap.set(op.location.state.__TSR_index - 1, getCurrentFocusKey());
|
||||
} else if (op.action.type === 'BACK')
|
||||
{
|
||||
if (focusMap.has(op.location.state.__TSR_index))
|
||||
{
|
||||
focusQueue.pop();
|
||||
focusQueue.push(focusMap.get(op.location.state.__TSR_index)!);
|
||||
focusMap.delete(op.location.state.__TSR_index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register things for typesafety
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register
|
||||
|
|
@ -100,9 +73,11 @@ if (!rootElement.innerHTML)
|
|||
const root = createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={Router} />
|
||||
</QueryClientProvider>
|
||||
<App>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={Router} />
|
||||
</QueryClientProvider>
|
||||
</App>
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 >;
|
||||
}
|
||||
|
|
|
|||
70
src/mainview/scripts/audio/audio.ts
Normal file
70
src/mainview/scripts/audio/audio.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { Howl } from 'howler';
|
||||
import sounds from '../../assets/sounds.ogg';
|
||||
import soundSprites from '../../assets/sounds.json';
|
||||
import { getLocalSetting } from '../utils';
|
||||
|
||||
const timingMap = new Map<string, Date>();
|
||||
|
||||
const sound = new Howl({
|
||||
src: [sounds],
|
||||
sprite: soundSprites.sprite as any,
|
||||
volume: 0.5,
|
||||
});
|
||||
import.meta.hot?.dispose(() => { sound.unload(); });
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface StaticDataRouteOption
|
||||
{
|
||||
enterSound?: keyof typeof soundMap | null;
|
||||
goBackSound?: keyof typeof soundMap | null;
|
||||
}
|
||||
}
|
||||
|
||||
const volumeVariation = 0.05;
|
||||
const rateVariation = 0.01;
|
||||
|
||||
export const soundMap = {
|
||||
openDetails: { key: 'Classic UI SFX - Chords #1' },
|
||||
returnGeneric: { key: 'Classic UI SFX - Short - Low #2' },
|
||||
returnDetails: { key: 'Classic UI SFX - Short - Low #5' },
|
||||
openGeneric: { key: 'Classic UI SFX - Short - High #9' },
|
||||
select: { key: 'Classic UI SFX - Short - High #5', rateVariation, volumeVariation },
|
||||
selectAlt: { key: "Classic UI SFX - Short - High #6", rateVariation, volumeVariation },
|
||||
selectMenu: { key: 'Classic UI SFX - Short - High #7', rateVariation, volumeVariation },
|
||||
selectFilter: { key: 'Classic UI SFX - Short - High #3', volumeVariation },
|
||||
closeContext: { key: 'Classic UI SFX - Short - High #19' },
|
||||
openContext: { key: 'Classic UI SFX - Short - High #22' },
|
||||
openStore: { key: 'Classic UI SFX - Chords #16' },
|
||||
openSettings: { key: 'Classic UI SFX - Short - High #8' },
|
||||
click: { key: "UI_Single_Set 16_03", rateVariation, volumeVariation },
|
||||
clickAlt: { key: "UI_Single_Set 16_01", rateVariation, volumeVariation },
|
||||
invalidNavigation: { key: "Classic UI SFX - Short - Low #6", rateVariation, volumeVariation },
|
||||
} satisfies Record<string, { key: keyof typeof soundSprites.sprite, rateVariation?: number; volumeVariation?: number; }>;
|
||||
|
||||
function sinRanom ()
|
||||
{
|
||||
return Math.sin(new Date().getMilliseconds() / 1000 * Math.PI);
|
||||
}
|
||||
|
||||
function cosRandom ()
|
||||
{
|
||||
return Math.sin(new Date().getMilliseconds() / 1000 * Math.PI);
|
||||
}
|
||||
|
||||
function random ()
|
||||
{
|
||||
return Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
export function oneShot (id: keyof typeof soundMap)
|
||||
{
|
||||
const currentDate = timingMap.get(id);
|
||||
if (!getLocalSetting('soundEffects')) return;
|
||||
if (currentDate && new Date().getTime() - currentDate.getTime() <= 100) return;
|
||||
const soundValue = soundMap[id] as { key: keyof typeof soundSprites.sprite, rateVariation?: number; volumeVariation?: number; };
|
||||
const instanceId = sound.play(soundValue.key);
|
||||
sound.volume(sound.volume() + random() * (soundValue.volumeVariation ?? 0), instanceId);
|
||||
sound.rate(1 + random() * (soundValue.rateVariation ?? 0), instanceId);
|
||||
timingMap.set(id, new Date());
|
||||
}
|
||||
|
||||
90
src/mainview/scripts/audio/audioCallbacks.ts
Normal file
90
src/mainview/scripts/audio/audioCallbacks.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { Router } from "@/mainview";
|
||||
import { oneShot, soundMap } from "./audio";
|
||||
export default function load ()
|
||||
{
|
||||
let lastLocationPath: string | undefined;
|
||||
const unsub = Router.history.subscribe((op) =>
|
||||
{
|
||||
if (op.action.type === 'PUSH')
|
||||
{
|
||||
lastLocationPath = op.location.pathname;
|
||||
|
||||
const routes = Router.matchRoutes(op.location.pathname);
|
||||
const soundRoute = routes.find(r => r.staticData.enterSound !== undefined);
|
||||
if (soundRoute)
|
||||
{
|
||||
if (soundRoute.staticData.enterSound) oneShot(soundRoute.staticData.enterSound);
|
||||
} else
|
||||
{
|
||||
oneShot("openGeneric");
|
||||
}
|
||||
|
||||
} else if (op.action.type === 'BACK')
|
||||
{
|
||||
if (lastLocationPath)
|
||||
{
|
||||
const soundRoutes = Router.matchRoutes(lastLocationPath);
|
||||
const soundRoute = soundRoutes.find(r => r.staticData.goBackSound !== undefined);
|
||||
if (soundRoute)
|
||||
{
|
||||
if (soundRoute.staticData.goBackSound) oneShot(soundRoute.staticData.goBackSound);
|
||||
} else
|
||||
{
|
||||
oneShot("returnGeneric");
|
||||
}
|
||||
} else
|
||||
{
|
||||
oneShot("returnGeneric");
|
||||
}
|
||||
|
||||
lastLocationPath = op.location.state.key;
|
||||
}
|
||||
});
|
||||
|
||||
let focusChangeDebounced: undefined | NodeJS.Timeout;
|
||||
|
||||
const focuschangedHandler = (e: CustomEvent<FocusEventDetails>) =>
|
||||
{
|
||||
clearTimeout(focusChangeDebounced);
|
||||
if (!e.detail.focusKeyChanged) return;
|
||||
|
||||
if (e.detail.nativeEvent || e.detail.event)
|
||||
{
|
||||
let sound: keyof typeof soundMap;
|
||||
if (e.detail.node && e.detail.node.matches('[data-sound-category="menu"]'))
|
||||
{
|
||||
sound = 'selectMenu';
|
||||
|
||||
} else if (e.detail.node && e.detail.node.matches('[data-sound-category="filter"]'))
|
||||
{
|
||||
sound = "selectFilter";
|
||||
}
|
||||
else if (e.detail.node && e.detail.node.matches('[data-sound-category="emulator"]'))
|
||||
{
|
||||
sound = "selectAlt";
|
||||
}
|
||||
else if (!e.detail.node || !e.detail.node.matches('[data-sound-disable="focus"]'))
|
||||
{
|
||||
sound = e.detail.sound as any ?? 'select';
|
||||
}
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (e.detail.nativeEvent || e.detail.event)
|
||||
{
|
||||
oneShot(sound);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('focuschanged', focuschangedHandler as any);
|
||||
|
||||
return {
|
||||
cleanup: () =>
|
||||
{
|
||||
unsub();
|
||||
window.removeEventListener('focuschanged', focuschangedHandler as any);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { getCurrentFocusKey, navigateByDirection } from "@noriginmedia/norigin-s
|
|||
import { GetFocusedElement } from "./spatialNavigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { mobileCheck } from "./utils";
|
||||
import { oneShot } from "./audio/audio";
|
||||
|
||||
let loopStarted = false;
|
||||
let isTouching = false;
|
||||
|
|
@ -104,7 +105,10 @@ function throttleNav (key: string, dir: string, event: Event)
|
|||
const speed = Math.max(maxSpeed - (maxSpeed - minSpeed) * (acceleration / 6), minSpeed);
|
||||
if ((currentDate.getTime() - (lastTime ?? 0) > speed))
|
||||
{
|
||||
const currentFocusKey = getCurrentFocusKey();
|
||||
navigateByDirection(dir, { event });
|
||||
if (currentFocusKey === getCurrentFocusKey())
|
||||
oneShot('invalidNavigation');
|
||||
throttleMap.set(key, currentDate.getTime());
|
||||
throttleAcceleration.set(key, acceleration + 1);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import
|
||||
{
|
||||
FocusDetails,
|
||||
getCurrentFocusKey,
|
||||
init,
|
||||
SpatialNavigation,
|
||||
|
|
@ -9,7 +8,7 @@ import
|
|||
UseFocusableResult,
|
||||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { RefObject, useEffect, useState } from "react";
|
||||
import { focusQueue, Router } from "..";
|
||||
import { focusQueue } from "../App";
|
||||
|
||||
init({
|
||||
shouldFocusDOMNode: false,
|
||||
|
|
@ -97,13 +96,21 @@ SpatialNavigation.updateLayout = (focusKey) =>
|
|||
SpatialNavigation.setFocus = (newFocusKey, focusDetails) =>
|
||||
{
|
||||
setFocus(newFocusKey, focusDetails);
|
||||
dispatchFocusedEvent(new CustomEvent<FocusDetails>('focuschanged', { bubbles: true, detail: focusDetails }));
|
||||
};
|
||||
|
||||
SpatialNavigation.setCurrentFocusedKey = (newFocusKey, focusDetails) =>
|
||||
{
|
||||
const details: FocusEventDetails = {
|
||||
...focusDetails,
|
||||
focusKey: newFocusKey,
|
||||
focusKeyChanged: newFocusKey !== getCurrentFocusKey(),
|
||||
node: GetFocusedElement(newFocusKey)
|
||||
};
|
||||
setCurrentFocusedKey(newFocusKey, focusDetails);
|
||||
window.dispatchEvent(new CustomEvent<FocusDetails>('focuschanged', { bubbles: true, detail: focusDetails }));
|
||||
window.dispatchEvent(new CustomEvent<FocusEventDetails>('focuschanged', {
|
||||
bubbles: true,
|
||||
detail: details
|
||||
}));
|
||||
};
|
||||
|
||||
SpatialNavigation.updateFocusable = (key, data) =>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { RefObject, useEffect, useRef, useState } from "react";
|
|||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { jobsApi } from "./clientApi";
|
||||
import { JobsAPIType } from "@/bun/api/rpc";
|
||||
import { Router } from "..";
|
||||
import { AnyRouter, Router, useRouter } from "@tanstack/react-router";
|
||||
import { soundMap } from "./audio/audio";
|
||||
|
||||
export type ScrollSaveParams = {
|
||||
id: string;
|
||||
|
|
@ -59,6 +60,13 @@ export function mobileCheck ()
|
|||
return check;
|
||||
};
|
||||
|
||||
export function getLocalSetting<TKey extends keyof LocalSettingsType> (key: TKey)
|
||||
{
|
||||
const localValueRaw = localStorage.getItem(key);
|
||||
if (!localValueRaw) return LocalSettingsSchema.shape[key].parse(undefined);
|
||||
return LocalSettingsSchema.shape[key].parse(JSON.parse(localValueRaw));
|
||||
}
|
||||
|
||||
export function useLocalSetting<TKey extends keyof LocalSettingsType> (key: TKey)
|
||||
{
|
||||
const [localValue] = useLocalStorage(key, LocalSettingsSchema.shape[key].parse(undefined), { deserializer: (value) => LocalSettingsSchema.shape[key].parse(JSON.parse(value)) });
|
||||
|
|
@ -218,7 +226,7 @@ export function scrollIntoViewHandler (params?: ScrollIntoViewOptions)
|
|||
return (focusKey: string, node: HTMLElement, details: any) =>
|
||||
{
|
||||
if (details.nativeEvent instanceof PointerEvent) return;
|
||||
node.scrollIntoView({ ...params, behavior: details.instant ? 'instant' : 'smooth' });
|
||||
node.scrollIntoView({ ...params, behavior: details.instant || !details.event ? 'instant' : 'smooth' });
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -315,13 +323,37 @@ export function useJobStatus<const JOB extends keyof JobsAPIType['~Routes']['api
|
|||
return { data, state, error, wsRef: ref };
|
||||
}
|
||||
|
||||
export function HandleGoBack ()
|
||||
export function HandleGoBack (router: AnyRouter)
|
||||
{
|
||||
if (Router.history.canGoBack())
|
||||
if (router.history.canGoBack())
|
||||
{
|
||||
Router.history.back();
|
||||
router.history.back();
|
||||
} else
|
||||
{
|
||||
Router.navigate({ to: '/', viewTransition: { types: ['zoom-out'] } });
|
||||
router.navigate({ to: '/', viewTransition: { types: ['zoom-out'] } });
|
||||
}
|
||||
}
|
||||
|
||||
export function useOnNavigateBack (callback: (state: { sound?: keyof typeof soundMap; } & Record<string, any>) => void)
|
||||
{
|
||||
const router = useRouter();
|
||||
const prevIndex = useRef(router.history.location.state.__TSR_index);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const unsub = router.history.subscribe(() =>
|
||||
{
|
||||
const currentIndex = router.history.location.state.__TSR_index;
|
||||
const isBack = currentIndex < prevIndex.current;
|
||||
|
||||
if (isBack)
|
||||
{
|
||||
callback(router.history.location.state);
|
||||
}
|
||||
|
||||
prevIndex.current = currentIndex;
|
||||
});
|
||||
|
||||
return unsub;
|
||||
}, [router]);
|
||||
}
|
||||
19
src/mainview/types.d.ts
vendored
19
src/mainview/types.d.ts
vendored
|
|
@ -16,6 +16,25 @@ declare global
|
|||
"save-scroll"?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
module "@noriginmedia/norigin-spatial-navigation" {
|
||||
declare interface FocusDetails
|
||||
{
|
||||
instant?: boolean;
|
||||
sound?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare interface FocusEventDetails
|
||||
{
|
||||
focusKey: string;
|
||||
instant?: boolean;
|
||||
sound?: string;
|
||||
nativeEvent?: any;
|
||||
event?: Event;
|
||||
node: HTMLElement | undefined;
|
||||
focusKeyChanged: boolean;
|
||||
}
|
||||
|
||||
declare interface FocusParams
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue