import { useRef } from "react"; import { useFocusable, FocusContext, } from "@noriginmedia/norigin-spatial-navigation"; import { createFileRoute, useNavigate, useRouter } from "@tanstack/react-router"; import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts"; import Shortcuts 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 { ContextList, DialogEntry, useContextDialog } from "@/mainview/components/ContextDialog"; import { RPC_URL } from "@/shared/constants"; 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 toast from "react-hot-toast"; import { getErrorMessage } from "react-error-boundary"; import { emulatorStatusIcons } from "@/mainview/components/store/StoreEmulatorCard"; import StatList, { StatEntry } from "@/mainview/components/StatList"; 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, async loader (ctx) { 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" } }); function HomePageLink (data: { homepage?: string; }) { const { ref } = useFocusable({ focusKey: 'homepage-link' }); return { if (data.homepage) systemApi.api.system.open.post({ url: data.homepage }); }}> {data.homepage ?? } ; } function TitleArea (data: { emulator?: FrontEndEmulatorDetailed; onInstall: (source: string) => void; onUpdate: (source: string) => void; }) { const navigation = useNavigate(); const queryClient = useQueryClient(); const deleteMutation = useMutation({ ...storeEmulatorDeleteMutation, onSuccess (data, variables, onMutateResult, context) { context.client.refetchQueries(storeEmulatorDetailsQuery(variables)); }, }); const downloadBios = useMutation(downloadBiosMutation(data.emulator?.name ?? '')); const updateToVersion = data.emulator?.downloads.find(d => d.version === data.emulator!.storeDownloadInfo?.type)?.version ?? data.emulator?.downloads[0]?.version; const deleteBios = useMutation({ ...deleteBiosMutation, onSuccess (data, variables, onMutateResult, context) { context.client.refetchQueries(storeEmulatorDetailsQuery(variables)); toast.success("BIOS Deleted", { icon: }); }, }); const installProgressRef = useRef(null); const { data: biosInstallJob, state: biosDownloadState } = useJobStatus('bios-download-job', { query: { id: data.emulator?.name }, onError (error) { console.log(error); toast.error(getErrorMessage(error) ?? "Error During Bios Download"); }, onProgress (process) { if (installProgressRef.current) installProgressRef.current.value = process; }, onCompleted (data) { toast.success("BIOS Downloaded", { icon: }); }, onEnded (data) { queryClient.refetchQueries(storeEmulatorDetailsQuery(data.emulator)); }, }); const { data: installJob, state: installState } = useJobStatus('download-emulator', { onError (error) { console.log(error); toast.error(getErrorMessage(error) ?? "Error During Download"); }, onProgress (process) { if (installProgressRef.current) installProgressRef.current.value = process; }, onEnded (data) { console.log("Finished Install", data.emulator); if (data.emulator) queryClient.refetchQueries(storeEmulatorDetailsQuery(data.emulator)); }, }); const isInstalling = !!installJob || !!biosInstallJob; const options: DialogEntry[] = []; const installedFromStore = !!data.emulator?.validSources.find(s => s.type === 'store' && s.exists); if (data.emulator) { if (!isInstalling && !installedFromStore) { options.push(...data.emulator.downloads.map(d => { const entry: DialogEntry = { content: `Install From: ${d.name} (${d.type})`, type: 'primary', id: d.name, action: (ctx) => { data.onInstall(d.name); ctx.close(); } }; return entry; })); } else if (installedFromStore) { options.push({ content: "Delete", type: 'error', icon: , action (ctx) { if (data.emulator) deleteMutation.mutate(data.emulator.name); ctx.close(); }, id: "delete" }); if ((!data.emulator.storeDownloadInfo || data.emulator.storeDownloadInfo.hasUpdate)) { options.push({ content: `Update ${data.emulator.storeDownloadInfo?.type}: ${data.emulator.storeDownloadInfo?.version ?? "Unknown"} > ${updateToVersion}`, type: 'warning', icon: , action (ctx) { const source = data.emulator?.storeDownloadInfo?.type ?? data.emulator?.downloads[0]?.type; if (source) data.onUpdate(source); ctx.close(); }, id: 'update' }); } if (!data.emulator.bios || data.emulator.bios.length <= 0) { options.push({ content: "Download BIOS", type: "primary", icon: , action (ctx) { downloadBios.mutate(); ctx.close(); }, id: "download-bios" }); } else { options.push({ content: "Delete BIOS", type: "error", icon: , action (ctx) { if (!data.emulator) return; deleteBios.mutate(data.emulator.name); ctx.close(); }, id: "download-bios" }); } } options.push(...data.emulator.validSources.filter(s => s.exists).map(s => ({ content: `Launch: ${s.type}`, type: 'primary', icon: emulatorStatusIcons[s.type], action (ctx) { if (!data.emulator) return; rommApi.api.romm.game({ source: 'emulator' })({ id: data.emulator.name }).play.post({ command_id: s.type }); navigation({ to: '/launcher/$source/$id', params: { source: 'emulator', id: data.emulator.name } }); }, id: `open-${s.type}` } satisfies DialogEntry))); } const { ref, focusKey, hasFocusedChild } = useFocusable({ focusKey: 'title-area', preferredChildFocusKey: "install-btn", trackChildren: true, onFocus: () => { (ref.current as HTMLElement).scrollIntoView({ behavior: "smooth", block: 'end' }); } }); let installButtonContent = <>>; if (!data.emulator) { installButtonContent = ; } else if (isInstalling) { const status: any = { bios: { download: "Downloading BIOS" }, install: { download: "Downloading", extract: "Extracting" } }; installButtonContent = <>{installState ? status.install[installState] : biosDownloadState ? status.bios[biosDownloadState] : undefined}>; } else if (data.emulator.validSources.some(s => s.exists)) { installButtonContent = <> Options>; } else if (data.emulator.downloads.length > 0) { installButtonContent = <>Install>; } else { installButtonContent = <>Unsupported>; } const { dialog: installOptionsDialog, setOpen } = useContextDialog("install-context-menu", { content: }); const handleOptionsOpen = () => { if (isInstalling || !data.emulator) return false; setOpen(true, 'install-btn'); }; return {data.emulator ? : } {data.emulator?.name ?? } {data.emulator?.systems.map(({ id, name, iconUrl }) => { return {!!iconUrl && } {name} ; }) ?? <>>} {!!data.emulator?.bios?.[0] && } {data.emulator && data.emulator.integrations.length > 0 && } {(data.emulator?.storeDownloadInfo?.hasUpdate || !data.emulator?.storeDownloadInfo) && installedFromStore && !!updateToVersion && setOpen(true, 'update-warning-bt')}> } {(!data.emulator?.bios || data.emulator.bios.length <= 0) && (data.emulator?.biosRequirement === 'required') && installedFromStore && setOpen(true, 'bios-warning-bt')}> } {installButtonContent} {isInstalling && } {installOptionsDialog} ; } function Description (data: { emulator?: FrontEndEmulatorDetailed; }) { return {data.emulator?.description ?? } ; } export function RouteComponent () { const { id } = Route.useParams(); const router = useRouter(); const { ref, focusKey, focusSelf } = useFocusable({ focusKey: `GAME_DETAIL_${id}`, trackChildren: true, preferredChildFocusKey: 'title-area' }); const { data: emulator, isPending: isEmulatorPending } = useQuery(storeEmulatorDetailsQuery(id)); const { data: recommendedEmulators } = useQuery(storeEmulatorsRecommendedQuery(id)); const { data: recommendedGames } = useQuery(gamesRecommendedBasedOnEmulatorQuery(id)); useShortcuts(focusKey, () => [{ label: "Return", action: () => HandleGoBack(router), button: GamePadButtonCode.B }], [router]); const installMutation = useMutation({ ...installEmulatorMutation(id), onSuccess: (data, variables, onMutateResult, context) => context.client.refetchQueries(storeEmulatorDetailsQuery(id)), }); const { shortcuts } = useShortcutContext(); const stats: StatEntry[] = []; if (emulator) { if (emulator.keywords) stats.push({ label: "Tags", content: emulator.keywords }); if (emulator.storeDownloadInfo) stats.push({ label: "Version", content: `${emulator.storeDownloadInfo.version ?? "Unknown"} (${emulator.storeDownloadInfo.type})` }); stats.push({ label: "Systems", content: emulator.systems.map(s => s.name) }); stats.push(...emulator.validSources.flatMap(s => [{ label: "Source", content: {emulatorStatusIcons[s.type]}{s.type} {s.binPath} {emulator.integrations.some(i => i.source?.type === s.type) && } {emulator.integrations.filter(i => i.source?.type === s.type).map(i => { return {i.id} {`${i.capabilities?.join(", ")}`} ; })} }])); if (emulator.bios) stats.push({ label: "Bios", content: emulator.bios && emulator.bios.length > 0 ? emulator.bios : Missing }); } return ( installMutation.mutate({ source: s, isUpdate: false })} onUpdate={s => installMutation.mutate({ source: s, isUpdate: true })} /> {isEmulatorPending || (!!emulator && emulator?.screenshots.length > 0) && } Stats {recommendedEmulators && More Emulators >} onFocus={scrollIntoViewHandler({ block: 'center' })} onSelect={(id, focus) => { router.navigate({ to: '/store/details/emulator/$id', params: { id } }); }} emulators={recommendedEmulators} /> } {recommendedGames && recommendedGames.length > 0 && Related Games { router.navigate({ to: '/game/$source/$id', params: { id: id.id, source: id.source } }); }} games={recommendedGames} />} ); }
{name}