feat: Implemented romm saves for dolphin and xenia
feat: Implemented save backups for emulatorjs fix: Added support for rar archives fix: Moved to individual ini adjustments for pcsx2 and ppsspp to allow for user editing of configs
This commit is contained in:
parent
54dd9256e3
commit
7948bd24fa
36 changed files with 1103 additions and 243 deletions
|
|
@ -1,7 +1,15 @@
|
|||
import { RPC_URL } from "@/shared/constants";
|
||||
import { Clock, CloudUpload, Save } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import toast, { ToastOptions } from "react-hot-toast";
|
||||
|
||||
|
||||
const customIconMap = {
|
||||
save: <Save />,
|
||||
upload: <CloudUpload />,
|
||||
clock: <Clock />
|
||||
};
|
||||
|
||||
export default function Notifications (data: {})
|
||||
{
|
||||
useEffect(() =>
|
||||
|
|
@ -10,7 +18,13 @@ export default function Notifications (data: {})
|
|||
es.addEventListener('notification', (e) =>
|
||||
{
|
||||
const notification = JSON.parse(e.data) as FrontendNotification;
|
||||
const options: ToastOptions = { removeDelay: notification.duration };
|
||||
const options: ToastOptions = {
|
||||
removeDelay: notification.duration,
|
||||
style: {
|
||||
borderRadius: "64px"
|
||||
}
|
||||
};
|
||||
if (notification.icon) options.icon = customIconMap[notification.icon];
|
||||
if (notification.type === 'error')
|
||||
{
|
||||
toast.error(notification.message, options);
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ import { scrollIntoViewHandler } from "@/mainview/scripts/utils";
|
|||
import { RPC_URL } from "@/shared/constants";
|
||||
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
import { Clock, CloudDownload, HardDrive, Store, TriangleAlert } from "lucide-react";
|
||||
import { Clock, CloudBackup, CloudDownload, CloudUpload, HardDrive, Store, TriangleAlert } from "lucide-react";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { JSX } from "react";
|
||||
import ActionButtons from "./ActionButtons";
|
||||
import prettyMilliseconds from 'pretty-ms';
|
||||
|
||||
|
||||
export function DetailElement (data: { icon: JSX.Element; children?: any | any[]; })
|
||||
export function DetailElement (data: { icon: JSX.Element; tooltip?: string | null, children?: any | any[]; })
|
||||
{
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex gap-2 items-center tooltip" data-tip={data.tooltip}>
|
||||
{data.icon}
|
||||
{data.children}
|
||||
</div>
|
||||
|
|
@ -62,15 +62,14 @@ export default function Details (data: {
|
|||
}
|
||||
</div>
|
||||
<div className="flex-2 flex flex-col sm:gap-1 md:gap-6 sm:pt-2 md:pt-16 min-h-0">
|
||||
<div className="flex flex-wrap sm:gap-4 md:gap-6 shrink-0">
|
||||
<DetailElement icon={<Clock />} >{data.game?.last_played ? new Date(data.game.last_played).toDateString() : "Never"}</DetailElement>
|
||||
<div className="flex flex-wrap items-center sm:gap-4 md:gap-6 shrink-0">
|
||||
<DetailElement icon={<Clock />} >{data.game?.last_played ? `${prettyMilliseconds(new Date().getTime() - new Date(data.game.last_played).getTime(), { compact: true, verbose: true })} ago` : "Never"}</DetailElement>
|
||||
{!!data.game && (data.game.fs_size_bytes !== null || data.game.missing) &&
|
||||
<div className={classNames({ "text-error": data.game.missing })}>
|
||||
<div className="tooltip" data-tip={data.game.path_fs}>
|
||||
<DetailElement icon={fileSizeIcon} >{data.game.missing ? 'Missing' : prettyBytes(data.game.fs_size_bytes!)}</DetailElement>
|
||||
</div>
|
||||
<div className={classNames("flex items-center", { "text-error": data.game.missing })}>
|
||||
<DetailElement tooltip={data.game.path_fs} icon={fileSizeIcon} >{data.game.missing ? 'Missing' : prettyBytes(data.game.fs_size_bytes!)}</DetailElement>
|
||||
</div>}
|
||||
<DetailElement icon={platformCoverImg ? <img className="size-6" src={platformCoverImg.href}></img> : <div className="skeleton size-6 rounded-full shrink-0"></div>} >{data.game?.platform_display_name ?? <div className="skeleton h-4 w-32"></div>}</DetailElement>
|
||||
{data.game?.emulators?.some(e => e.integrations.some(i => i.capabilities?.includes('saves'))) && <DetailElement tooltip={"Save Backup"} icon={<CloudUpload />} />}
|
||||
<DetailElement icon={
|
||||
<Store />
|
||||
} >
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ Array.from(params.entries()).forEach(([key, value]) =>
|
|||
|
||||
window.addEventListener('message', (e) =>
|
||||
{
|
||||
switch (e.data.type)
|
||||
const data = e.data as EmulatorJsMessage;
|
||||
switch (data.type)
|
||||
{
|
||||
case 'pause':
|
||||
if (e.data.data === true)
|
||||
if (data.paused)
|
||||
{
|
||||
window.EJS_emulator.pause();
|
||||
} else
|
||||
|
|
@ -24,14 +25,51 @@ window.addEventListener('message', (e) =>
|
|||
case 'restart':
|
||||
window.EJS_emulator.elements.bottomBar.restart[0].click();
|
||||
break;
|
||||
case 'requestSave':
|
||||
window.EJS_emulator.elements.bottomBar.saveSavFiles[0].click();
|
||||
break;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function postMessage (m: EmulatorJsMessage)
|
||||
{
|
||||
window.parent.postMessage(
|
||||
m,
|
||||
"*"
|
||||
);
|
||||
}
|
||||
|
||||
export function loadEmulatorJSSave (save: Uint8Array)
|
||||
{
|
||||
const FS = window.EJS_emulator.gameManager.FS;
|
||||
const path = window.EJS_emulator.gameManager.getSaveFilePath();
|
||||
const paths = path.split("/");
|
||||
let cp = "";
|
||||
for (let i = 0; i < paths.length - 1; i++)
|
||||
{
|
||||
if (paths[i] === "") continue;
|
||||
cp += "/" + paths[i];
|
||||
if (!FS.analyzePath(cp).exists) FS.mkdir(cp);
|
||||
}
|
||||
if (FS.analyzePath(path).exists) FS.unlink(path);
|
||||
FS.writeFile(path, save);
|
||||
window.EJS_emulator.gameManager.loadSaveFiles();
|
||||
}
|
||||
|
||||
window.EJS_threads = !__PUBLIC__;
|
||||
window.EJS_player = "#game";
|
||||
window.EJS_lightgun = false;
|
||||
window.EJS_startOnLoaded = true;
|
||||
window.EJS_onGameStart = async () =>
|
||||
{
|
||||
const savesResponse = await fetch(`${RPC_URL(__HOST__)}/api/romm/emulatorjs/load?filePath=${encodeURIComponent(window.EJS_emulator.gameManager.getSaveFilePath())}`);
|
||||
if (savesResponse.ok)
|
||||
{
|
||||
loadEmulatorJSSave(new Uint8Array(await savesResponse.arrayBuffer()));
|
||||
postMessage({ type: "loaded" });
|
||||
}
|
||||
};
|
||||
// For core downloads, it either redirects to CDN or uses local if downloaded
|
||||
window.EJS_pathtodata = `${RPC_URL(__HOST__)}/api/romm/emulatorjs/data`;
|
||||
window.EJS_Buttons = {
|
||||
|
|
@ -40,10 +78,8 @@ window.EJS_Buttons = {
|
|||
displayName: "Exit",
|
||||
callback: () =>
|
||||
{
|
||||
window.parent.postMessage(
|
||||
{ type: "exit" },
|
||||
"*"
|
||||
);
|
||||
const saveFile = window.EJS_emulator.gameManager.getSaveFile(false);
|
||||
postMessage({ type: "exit", save: saveFile ? new File([saveFile], window.EJS_emulator.gameManager.getSaveFilePath()) : undefined });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -58,7 +94,18 @@ const moduleUrls = import.meta.glob
|
|||
import: 'default',
|
||||
});
|
||||
|
||||
function handeSave (ctx: { save: ArrayBuffer, screenshot: ArrayBuffer | undefined, format: string; })
|
||||
{
|
||||
window.parent.postMessage({ type: 'save', save: new File([ctx.save], window.EJS_emulator.gameManager.getSaveFilePath()) });
|
||||
}
|
||||
|
||||
// emulatorjs expects basenames instead of paths for some reason
|
||||
window.EJS_paths = Object.fromEntries(await Promise.all(Object.entries(moduleUrls).map(async ([key, value]) => [basename(key), await value()])));
|
||||
window.EJS_onSaveUpdate = (ctx: { hash: string, save: ArrayBuffer, screenshot: ArrayBuffer | undefined, format: string; }) => handeSave(ctx);
|
||||
window.EJS_onSaveSave = (ctx: {
|
||||
save: ArrayBuffer;
|
||||
screenshot: ArrayBuffer;
|
||||
format: string;
|
||||
}) => handeSave(ctx);
|
||||
|
||||
await import('@emulatorjs/emulatorjs/data/loader.js' as any);
|
||||
3
src/mainview/emulatorjs/types.d.ts
vendored
3
src/mainview/emulatorjs/types.d.ts
vendored
|
|
@ -14,6 +14,7 @@ export declare global
|
|||
EJS_cheats: string[][],
|
||||
EJS_fullscreenOnLoaded: boolean,
|
||||
EJS_startOnLoaded: boolean,
|
||||
EJS_onGameStart,
|
||||
EJS_core: string,
|
||||
EJS_lightgun: boolean,
|
||||
EJS_biosUrl: string,
|
||||
|
|
@ -56,7 +57,9 @@ export declare global
|
|||
EJS_browserMode,
|
||||
EJS_shaders,
|
||||
EJS_fixedSaveInterval,
|
||||
EJS_onSaveUpdate,
|
||||
EJS_disableAutoUnload,
|
||||
EJS_disableBatchBootup;
|
||||
EJS_onSaveSave;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,20 +5,24 @@ import z from 'zod';
|
|||
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { ButtonStyle } from '../components/options/Button';
|
||||
import { DoorOpen, RefreshCw, Undo } from 'lucide-react';
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||
import Shortcuts, { FloatingShortcuts } from '../components/Shortcuts';
|
||||
import { CloudDownload, DoorOpen, RefreshCw, Save, Undo } from 'lucide-react';
|
||||
import { GamePadButtonCode, useShortcuts } from '../scripts/shortcuts';
|
||||
import { FloatingShortcuts } from '../components/Shortcuts';
|
||||
import { useEventListener } from 'usehooks-ts';
|
||||
import useActiveControl from '../scripts/gamepads';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { HeaderAccounts, HeaderStatusBar } from '../components/Header';
|
||||
import { RoundButton } from '../components/RoundButton';
|
||||
import { gameQuery } from '@queries/romm';
|
||||
import { rommApi } from '../scripts/clientApi';
|
||||
import toast from 'react-hot-toast';
|
||||
import { getErrorMessage } from 'react-error-boundary';
|
||||
|
||||
export const Route = createFileRoute('/embedded/$source/$id')({
|
||||
component: RouteComponent,
|
||||
staticData: {
|
||||
enterSound: 'launch'
|
||||
enterSound: 'launch',
|
||||
missNavSound: false
|
||||
},
|
||||
loader: async (ctx) =>
|
||||
{
|
||||
|
|
@ -45,7 +49,7 @@ function OverlayButton (data: {
|
|||
|
||||
function Overlay (data: {
|
||||
open: boolean;
|
||||
iframeRef: RefObject<HTMLIFrameElement | null>;
|
||||
postMessage: (m: EmulatorJsMessage) => void;
|
||||
close: () => void;
|
||||
goBack: () => void;
|
||||
})
|
||||
|
|
@ -64,7 +68,6 @@ function Overlay (data: {
|
|||
}, [data.open]);
|
||||
|
||||
const { isPointer } = useActiveControl();
|
||||
const handleEvent = (type: string, value?: any) => data.iframeRef.current?.contentWindow?.postMessage({ type, data: value });
|
||||
|
||||
return <div data-open={data.open} className='flex group w-full flex-col gap-2 transition-opacity p-4 not-data-[open=true]:pointer-events-none not-data-[open=true]:opacity-0'>
|
||||
<div className='grid grid-cols-3 justify-between items-start'>
|
||||
|
|
@ -78,7 +81,7 @@ function Overlay (data: {
|
|||
<OverlayButton id="restart" style='secondary' tooltip='Restart' setTooltip={setTooltip} onAction={() =>
|
||||
{
|
||||
data.close();
|
||||
handleEvent('restart');
|
||||
data.postMessage({ type: 'restart' });
|
||||
}} ><RefreshCw /></OverlayButton>
|
||||
<OverlayButton id="exit" style='warning' tooltip='Exit' setTooltip={setTooltip} onAction={data.goBack} ><DoorOpen /></OverlayButton>
|
||||
</FocusContext>
|
||||
|
|
@ -132,6 +135,7 @@ function RouteComponent ()
|
|||
});
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [overlayOpen, setOverlayOpen] = useState(false);
|
||||
const postMessage = (m: EmulatorJsMessage) => iframeRef.current?.contentWindow?.postMessage(m);
|
||||
const { source, id } = Route.useParams();
|
||||
|
||||
function HandleGoBack ()
|
||||
|
|
@ -147,9 +151,23 @@ function RouteComponent ()
|
|||
|
||||
useEventListener('message', e =>
|
||||
{
|
||||
if (e.data.type === 'exit')
|
||||
const data = e.data as EmulatorJsMessage;
|
||||
switch (data.type)
|
||||
{
|
||||
HandleGoBack();
|
||||
case "exit":
|
||||
rommApi.api.romm.emulatorjs.post_play({ source })({ id }).post({ save: data.save });
|
||||
HandleGoBack();
|
||||
break;
|
||||
case "loaded":
|
||||
toast.success("Save Loaded", { icon: <CloudDownload /> });
|
||||
break;
|
||||
case "save":
|
||||
rommApi.api.romm.emulatorjs.save.put({ save: data.save }).then(r =>
|
||||
{
|
||||
if (r.error) toast.error(getErrorMessage(r.error.value) ?? "Error While Saving");
|
||||
else toast.success("Save Backed Up");
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -173,11 +191,11 @@ function RouteComponent ()
|
|||
|
||||
const setPaused = (paused: boolean) =>
|
||||
{
|
||||
if (paused) iframeRef.current?.contentWindow?.postMessage({ type: 'pause', data: true });
|
||||
if (paused) postMessage({ type: 'pause', paused: true });
|
||||
else
|
||||
{
|
||||
// we want to prevent input from closing the overlay spilling
|
||||
setTimeout(() => iframeRef.current?.contentWindow?.postMessage({ type: 'pause', data: false }), 100);
|
||||
setTimeout(() => postMessage({ type: 'pause', paused: false }), 100);
|
||||
}
|
||||
};
|
||||
useEffect(() => setPaused(overlayOpen), [overlayOpen]);
|
||||
|
|
@ -191,7 +209,7 @@ function RouteComponent ()
|
|||
<FocusContext value={focusKey}>
|
||||
<Frame ref={iframeRef} />
|
||||
<div className='flex fixed left-0 right-0 top-0'>
|
||||
<Overlay iframeRef={iframeRef} goBack={HandleGoBack} open={overlayOpen} close={handleClose} />
|
||||
<Overlay postMessage={postMessage} goBack={HandleGoBack} open={overlayOpen} close={handleClose} />
|
||||
</div>
|
||||
<FloatingShortcuts />
|
||||
</FocusContext>
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ function Stats (data: { game: FrontEndGameTypeDetailed | undefined; })
|
|||
stats.push({ label: "Release Date", content: data.game.release_date.toLocaleDateString(), icon: <Calendar /> });
|
||||
if (data.game.emulators)
|
||||
stats.push({ label: "Emulators", content: data.game.emulators.map(e => e.name) });
|
||||
const integrations = new Set<string>(data.game.emulators?.flatMap(e => e.integrations).flatMap(i => i.capabilities).filter(c => !!c));
|
||||
stats.push({ label: "Integrations", content: Array.from(integrations) });
|
||||
}
|
||||
|
||||
return <StatList elementClassName="bg-base-300" stats={stats} id="game-detail-stats" />;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/
|
|||
import { useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import Shortcuts, { FloatingShortcuts } from '../components/Shortcuts';
|
||||
import { useJobStatus } from '../scripts/utils';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/launcher/$source/$id')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -13,6 +14,10 @@ export const Route = createFileRoute('/launcher/$source/$id')({
|
|||
},
|
||||
});
|
||||
|
||||
const stateLookup: Record<string, string> = {
|
||||
saves: "Syncing Saves"
|
||||
};
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const router = useRouter();
|
||||
|
|
@ -27,12 +32,18 @@ function RouteComponent ()
|
|||
}
|
||||
}
|
||||
|
||||
const progressRef = useRef<HTMLProgressElement>(null);
|
||||
const { source, id } = Route.useParams();
|
||||
const { ref, focusKey } = useFocusable({ focusKey: `launching-${source}-${id}` });
|
||||
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||
|
||||
const { data } = useJobStatus('launch-game', {
|
||||
const { data, state } = useJobStatus('launch-game', {
|
||||
onProgress (process, data)
|
||||
{
|
||||
if (progressRef.current)
|
||||
progressRef.current.value = process;
|
||||
},
|
||||
onEnded (data)
|
||||
{
|
||||
HandleGoBack();
|
||||
|
|
@ -41,14 +52,19 @@ function RouteComponent ()
|
|||
{
|
||||
HandleGoBack();
|
||||
},
|
||||
});
|
||||
}, [progressRef.current, HandleGoBack]);
|
||||
|
||||
useBlocker({ shouldBlockFn: () => !!data });
|
||||
|
||||
return <AnimatedBackground ref={ref} backgroundKey='game-details'>
|
||||
<div className='flex shadow-2xs shadow-black flex-col absolute w-screen h-screen overflow-hidden justify-center items-center gap-4'>
|
||||
<DotsLoading />
|
||||
<h1 className='font-semibold'>Launching {data?.name} ...</h1>
|
||||
{!!state && !!stateLookup[state] ?
|
||||
<>
|
||||
<h1 className='font-semibold'>Launching {data?.name} ...</h1> <progress ref={progressRef} className="progress w-56" value={0} max="100"></progress>
|
||||
</>
|
||||
:
|
||||
<h1 className='font-semibold'>Launching {data?.name} ...</h1>}
|
||||
</div>
|
||||
<FloatingShortcuts />
|
||||
</AnimatedBackground>;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import Shortcuts, { FloatingShortcuts } from "@/mainview/components/Shortcuts";
|
|||
import { AnimatedBackground } from "@/mainview/components/AnimatedBackground";
|
||||
import { rommApi, systemApi } from "@/mainview/scripts/clientApi";
|
||||
import { Button } from "@/mainview/components/options/Button";
|
||||
import { ChevronDown, CircleFadingArrowUp, Cpu, Download, Gamepad2, Info, Puzzle, Settings, Trash2, TriangleAlert, WandSparkles } from "lucide-react";
|
||||
import { ChevronDown, CircleFadingArrowUp, CloudUpload, Cpu, Download, Fullscreen, Gamepad2, Info, Monitor, Puzzle, Save, Settings, Settings2, Terminal, Trash2, TriangleAlert, WandSparkles } from "lucide-react";
|
||||
import { ContextList, DialogEntry, useContextDialog } from "@/mainview/components/ContextDialog";
|
||||
import { RPC_URL } from "@/shared/constants";
|
||||
import Screenshots from "@/mainview/components/Screenshots";
|
||||
|
|
@ -283,6 +283,9 @@ function TitleArea (data: {
|
|||
{data.emulator && data.emulator.integrations.length > 0 && <div className="tooltip" data-tip="Has Integration">
|
||||
<div className="bg-base-200 rounded-full p-2"><WandSparkles className="size-5" /></div>
|
||||
</div>}
|
||||
{data.emulator?.integrations.some(s => s.capabilities?.includes('saves')) && <div className="tooltip" data-tip="Save Support">
|
||||
<div className="bg-base-200 rounded-full p-2"><CloudUpload className="size-5" /></div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex relative sm:portrait:grow md:grow-0 justify-center gap-4 items-center">
|
||||
|
|
@ -319,6 +322,14 @@ function Description (data: { emulator?: FrontEndEmulatorDetailed; })
|
|||
</div>;
|
||||
}
|
||||
|
||||
const capabilityIconMap: Record<string, any> = {
|
||||
saves: <CloudUpload />,
|
||||
fullscreen: <Fullscreen />,
|
||||
resolution: <Monitor />,
|
||||
config: <Settings2 />,
|
||||
batch: <Terminal />
|
||||
};
|
||||
|
||||
export function RouteComponent ()
|
||||
{
|
||||
const { id } = Route.useParams();
|
||||
|
|
@ -366,7 +377,9 @@ export function RouteComponent ()
|
|||
<Puzzle />
|
||||
<div>{i.id}</div>
|
||||
</div>
|
||||
<div className="text-base-content/40">{`${i.capabilities?.join(", ")}`}</div>
|
||||
<div className="flex flex-wrap text-base-content/40">
|
||||
{i.capabilities?.map(c => <><div className="divider divider-horizontal"></div><div className="flex gap-1">{capabilityIconMap[c]}{c}</div></>)}
|
||||
</div>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ declare module '@tanstack/react-router' {
|
|||
enterSound?: keyof typeof soundMap | null;
|
||||
enterHaptic?: keyof typeof hapticMap | null;
|
||||
goBackSound?: keyof typeof soundMap | null;
|
||||
missNavSound?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { GetFocusedElement } from "./spatialNavigation";
|
|||
import { useEffect, useState } from "react";
|
||||
import { getLocalSetting, mobileCheck } from "./utils";
|
||||
import { oneShot } from "./audio/audio";
|
||||
import { Router } from "@/mainview";
|
||||
|
||||
let loopStarted = false;
|
||||
let isTouching = false;
|
||||
|
|
@ -108,7 +109,13 @@ function throttleNav (key: string, dir: string, event: Event)
|
|||
const currentFocusKey = getCurrentFocusKey();
|
||||
navigateByDirection(dir, { event });
|
||||
if (currentFocusKey === getCurrentFocusKey())
|
||||
oneShot('invalidNavigation');
|
||||
{
|
||||
const routes = Router.matchRoutes(Router.history.location.pathname);
|
||||
if (!routes.some(r => r.staticData.missNavSound === false))
|
||||
{
|
||||
oneShot('invalidNavigation');
|
||||
}
|
||||
}
|
||||
throttleMap.set(key, currentDate.getTime());
|
||||
throttleAcceleration.set(key, acceleration + 1);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LocalSettingsSchema, LocalSettingsType } from "@/shared/constants";
|
||||
import { RefObject, useEffect, useRef, useState } from "react";
|
||||
import { DependencyList, RefObject, useEffect, useRef, useState } from "react";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { jobsApi } from "./clientApi";
|
||||
import { JobsAPIType } from "@/bun/api/rpc";
|
||||
|
|
@ -272,7 +272,8 @@ export function useJobStatus<const JOB extends keyof JobsAPIType['~Routes']['api
|
|||
onEnded?: (data: ExtractField<JobResponse<JOB>, "completed" | "ended", 'data'>) => void;
|
||||
onCompleted?: (data: ExtractField<JobResponse<JOB>, "completed" | "ended", 'data'>) => void;
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
},
|
||||
deps?: DependencyList
|
||||
)
|
||||
{
|
||||
type Response = JobResponse<JOB>;
|
||||
|
|
@ -325,7 +326,7 @@ export function useJobStatus<const JOB extends keyof JobsAPIType['~Routes']['api
|
|||
sub.close();
|
||||
ref.current = null;
|
||||
};
|
||||
}, [id, init?.query, init?.onEnded, init?.onCompleted, init?.onProgress, init?.onError]);
|
||||
}, [id, init?.query, init?.onEnded, init?.onCompleted, init?.onProgress, init?.onError, ...(deps ?? [])]);
|
||||
|
||||
return { data, state, error, wsRef: ref };
|
||||
}
|
||||
|
|
|
|||
9
src/mainview/types.d.ts
vendored
9
src/mainview/types.d.ts
vendored
|
|
@ -60,4 +60,11 @@ declare interface FilterOption extends FocusParams, InteractParams
|
|||
label: string;
|
||||
selected: boolean;
|
||||
icon?: any;
|
||||
}
|
||||
}
|
||||
|
||||
declare type EmulatorJsMessage = { type: 'restart'; } |
|
||||
{ type: 'pause'; paused: boolean; } |
|
||||
{ type: 'exit'; save?: File; } |
|
||||
{ type: 'save', save: File, screenshot?: File, type: string; } |
|
||||
{ type: 'loaded'; } |
|
||||
{ type: 'requestSave'; };
|
||||
Loading…
Add table
Add a link
Reference in a new issue