feat: Implemented emulator versions and updating
This commit is contained in:
parent
a69147a4f7
commit
34db717ec5
22 changed files with 434 additions and 212 deletions
|
|
@ -12,7 +12,7 @@ export default function FocusTooltip (data: { parentRef: RefObject<any>; visible
|
|||
{
|
||||
const dataTooltip = e.getAttribute('data-tooltip');
|
||||
setHoverText(dataTooltip ?? undefined);
|
||||
setHoverTextType(e.getAttribute('data-tooltip_type') ?? 'accent');
|
||||
setHoverTextType(e.getAttribute('data-tooltip-type') ?? 'accent');
|
||||
};
|
||||
|
||||
const { isPointer } = useActiveControl();
|
||||
|
|
@ -29,7 +29,10 @@ export default function FocusTooltip (data: { parentRef: RefObject<any>; visible
|
|||
const tooltipStyles = {
|
||||
base: 'bg-base-100 text-base-content',
|
||||
accent: 'bg-accent text-accent-content',
|
||||
error: 'bg-error text-error-content'
|
||||
error: 'bg-error text-error-content',
|
||||
warning: 'bg-warning text-warning-content',
|
||||
info: 'bg-info text-info-content',
|
||||
success: 'bg-success text-success-content'
|
||||
};
|
||||
|
||||
return !!hoverText && (data.visible ?? true) && !isPointer && <p className={twMerge("flex sm:hidden md:inline py-1 md:py-2 md:px-4 rounded-4xl text-wrap wrap-anywhere text-base", (tooltipStyles as any)[hoverTextType])}>{hoverText}</p>;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function StatList (data: {
|
|||
|
||||
return <ul ref={ref} className="grid md:grid-cols-[8rem_1fr] sm:px-8 md:px-16 py-4 gap-2 focused:border-y focused:border-dashed focused:border-base-content/40">
|
||||
<FocusContext value={focusKey}>
|
||||
{data.stats.map((s, i) =>
|
||||
{data.stats.flatMap((s, i) =>
|
||||
{
|
||||
let content: any = undefined;
|
||||
if (s.content instanceof Array)
|
||||
|
|
@ -37,13 +37,9 @@ export default function StatList (data: {
|
|||
content = <div key={`label-items-${i}`} className="flex flex-wrap gap-2">{s.content.map((c, ci) => <span key={`label-items-${i}-${ci}`} className={twMerge("rounded-3xl bg-base-200 px-3 py-1", data.elementClassName)}>{c}</span>)}</div>;
|
||||
} else
|
||||
{
|
||||
content = <div key={`label-element-${i}`} className={twMerge("flex gap-2 rounded-3xl bg-base-200 px-3 py-1", data.elementClassName)}>{s.icon}{s.content}</div>;
|
||||
content = <div key={`label-element-${i}`} className={twMerge("flex gap-2 rounded-2xl bg-base-200 px-3 py-2", data.elementClassName)}>{s.icon}{s.content}</div>;
|
||||
}
|
||||
const element = <>
|
||||
<Label id={`${data.id}-label-${i}`} key={`label-${i}`} label={s.label} />
|
||||
{content}
|
||||
</>;
|
||||
return element;
|
||||
return [<Label key={`label-${i}`} id={`${data.id}-label-${i}`} label={s.label} />, <div key={`content-${i}`}>{content}</div>];
|
||||
})}
|
||||
</FocusContext>
|
||||
</ul>;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export default function ActionButton (data: {
|
|||
ref={ref}
|
||||
onClick={data.onAction}
|
||||
data-tooltip={data.tooltip}
|
||||
data-tooltip_type={data.tooltip_type}
|
||||
data-tooltip-type={data.tooltip_type}
|
||||
className={twMerge("header-icon flex flex-col gap-2 md:px-5 md:py-4 rounded-3xl md:text-2xl justify-center items-center cursor-pointer disabled:opacity-30 active:bg-base-100 active:transition-none active:text-base-content",
|
||||
"hover:ring-7 hover:ring-primary", styles[data.type], classNames({ "rounded-full sm:size-14 md:size-21 hover:bg-base-content hover:text-base-300 hover:ring-7 hover:ring-primary": !data.square }), data.className)}>
|
||||
{data.icon}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
mainButton = <ActionButton
|
||||
key="error"
|
||||
tooltip={error}
|
||||
tooltip_type="error"
|
||||
tooltip-type="error"
|
||||
type='error'
|
||||
onAction={() =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export function Button (data: {
|
|||
focusClassName?: string;
|
||||
cssStyle?: CSSProperties;
|
||||
tooltip?: string;
|
||||
tooltipType?: "base" | "accent" | "error";
|
||||
tooltipType?: "base" | "accent" | "error" | "warning";
|
||||
} & InteractParams & FocusParams)
|
||||
{
|
||||
const handleAction = (e?: any) =>
|
||||
|
|
@ -58,7 +58,7 @@ export function Button (data: {
|
|||
onClick={handleAction}
|
||||
disabled={data.disabled}
|
||||
data-tooltip={data.tooltip}
|
||||
data-tooltip_type={data.tooltipType}
|
||||
data-tooltip-type={data.tooltipType}
|
||||
style={data.cssStyle}
|
||||
className={twMerge("flex items-center justify-center px-4 py-2 disabled:bg-base-200/40 disabled:text-base-content/40 not-disabled:cursor-pointer rounded-3xl md:text-lg not-control-mouse:focused:drop-shadow-lg border border-base-content/5 not-control-mouse:focused:bg-base-content not-control-mouse:focused:text-base-100 control-mouse:hover:not-disabled:bg-base-content control-mouse:hover:not-disabled:text-base-100 active:not-disabled:transition-none active:not-disabled:ring-offset-4",
|
||||
styles[data.style ?? 'base'],
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import { Button } from "../options/Button";
|
|||
import useActiveControl from "@/mainview/scripts/gamepads";
|
||||
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { BadgeCheck, ChevronRight, EllipsisVertical, FileQuestion, IceCream2, Package, Sparkles, Store, WandSparkles } from "lucide-react";
|
||||
import { BadgeCheck, ChevronRight, CircleFadingArrowUp, EllipsisVertical, FileQuestion, IceCream2, Package, Sparkles, Store, WandSparkles } from "lucide-react";
|
||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import { FlatpackIcon } from "@/mainview/scripts/brandIcons";
|
||||
import { JSX } from "react";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getUpdateInfoForEmulator } from "@/mainview/scripts/queries/store";
|
||||
|
||||
export const emulatorStatusIcons: Record<string, JSX.Element> = {
|
||||
store: <Store />,
|
||||
|
|
@ -42,8 +44,9 @@ export function StoreEmulatorCard (data: {
|
|||
}
|
||||
});
|
||||
|
||||
const { data: updateInfo } = useQuery(getUpdateInfoForEmulator(data.emulator.name));
|
||||
|
||||
useShortcuts(focusKey, () => [{ button: GamePadButtonCode.A, label: "Details", action: handleSelect }], [handleSelect]);
|
||||
const { isMouse, isTouch } = useActiveControl();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -52,8 +55,8 @@ export function StoreEmulatorCard (data: {
|
|||
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)}
|
||||
onClick={handleSelect}
|
||||
className={twMerge("relative focusable focusable-info focusable-hover 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 cursor-pointer", data.className)}
|
||||
>
|
||||
<div className="flex flex-col justify-between p-4 gap-2 h-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
|
|
@ -81,21 +84,27 @@ export function StoreEmulatorCard (data: {
|
|||
</div>
|
||||
|
||||
<div className="flex gap-1 mt-1 h-10 items-center">
|
||||
{!!data.emulator.integration && <div aria-disabled={!data.emulator.integration.possible} className="tooltip not-aria-disabled:tooltip-primary" data-tip={data.emulator.integration.possible ? "Has Integration" : "Can Integrate"}>
|
||||
<div className="bg-primary in-aria-disabled:bg-base-200 text-primary-content rounded-full p-1.5"><WandSparkles className="size-5" /></div>
|
||||
{updateInfo?.hasUpdate && <div className="tooltip" data-tip="Has Update">
|
||||
<div className="flex items-center justify-center rounded-full p-1 size-8 bg-warning text-warning-content">
|
||||
<CircleFadingArrowUp />
|
||||
</div>
|
||||
</div>}
|
||||
{data.emulator.integrations.length > 0 && <div
|
||||
aria-disabled={!data.emulator.integrations.some(i => i.supportLevel)}
|
||||
data-full-support={data.emulator.integrations.some(i => i.supportLevel === 'full')}
|
||||
className="tooltip not-aria-disabled:tooltip-primary"
|
||||
data-tip={data.emulator.integrations.some(i => i.supportLevel) ? data.emulator.integrations.some(i => i.supportLevel === 'full') ? "Full Support" : "Partial SUpport" : "Can Integrate"}
|
||||
>
|
||||
<div className="bg-primary in-data-[full-support=false]:bg-warning in-data-[full-support=false]:text-warning-content in-aria-disabled:bg-base-200 in-aria-disabled:text-base-content text-primary-content rounded-full p-1.5"><WandSparkles className="size-5" /></div>
|
||||
</div>}
|
||||
{data.emulator.validSources.slice(0, 3).map(s =>
|
||||
{
|
||||
return <div className="tooltip" data-tip={s.type}>
|
||||
<div data-source={s.type} className="flex items-center justify-center rounded-full p-1 size-8 bg-warning text-warning-content data-[source=store]:bg-success data-[source=store]:text-success-content">
|
||||
<div data-source={s.type} className="flex items-center justify-center rounded-full p-1 size-8 bg-base-300 text-base-content data-[source=store]:bg-success data-[source=store]:text-success-content">
|
||||
{emulatorStatusIcons[s.type]}
|
||||
</div>
|
||||
</div>;
|
||||
})}
|
||||
{isMouse && <>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import Shortcuts from "@/mainview/components/Shortcuts";
|
|||
import { AnimatedBackground } from "@/mainview/components/AnimatedBackground";
|
||||
import { systemApi } from "@/mainview/scripts/clientApi";
|
||||
import { Button } from "@/mainview/components/options/Button";
|
||||
import { ChevronDown, Cpu, Download, Gamepad2, Info, Puzzle, Settings, Trash2, TriangleAlert, WandSparkles } from "lucide-react";
|
||||
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";
|
||||
|
|
@ -59,6 +59,7 @@ function HomePageLink (data: { homepage?: string; })
|
|||
function TitleArea (data: {
|
||||
emulator?: FrontEndEmulatorDetailed;
|
||||
onInstall: (source: string) => void;
|
||||
onUpdate: (source: string) => void;
|
||||
})
|
||||
{
|
||||
const queryClient = useQueryClient();
|
||||
|
|
@ -70,6 +71,7 @@ function TitleArea (data: {
|
|||
},
|
||||
});
|
||||
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)
|
||||
|
|
@ -122,7 +124,7 @@ function TitleArea (data: {
|
|||
const isInstalling = !!installJob || !!biosInstallJob;
|
||||
|
||||
const options: DialogEntry[] = [];
|
||||
const installedFromStore = !!data.emulator?.sources.find(s => s.type === 'store' && s.exists);
|
||||
const installedFromStore = !!data.emulator?.validSources.find(s => s.type === 'store' && s.exists);
|
||||
if (data.emulator)
|
||||
{
|
||||
if (!isInstalling && !installedFromStore)
|
||||
|
|
@ -155,6 +157,22 @@ function TitleArea (data: {
|
|||
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: <CircleFadingArrowUp />,
|
||||
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({
|
||||
|
|
@ -183,7 +201,6 @@ function TitleArea (data: {
|
|||
id: "download-bios"
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -253,13 +270,16 @@ function TitleArea (data: {
|
|||
{!!data.emulator?.bios?.[0] && <div className="tooltip" data-tip="Has BIOS">
|
||||
<div className="flex items-center justify-center bg-base-200 p-2 rounded-full"><Cpu className="size-5" /></div>
|
||||
</div>}
|
||||
{data.emulator && !!data.emulator.integration && data.emulator.validSources.some(s => s.type === 'store') && <div className="tooltip" data-tip="Has Integration">
|
||||
{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>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex relative sm:portrait:grow md:grow-0 justify-center gap-4 items-center">
|
||||
<FocusTooltip visible={hasFocusedChild} parentRef={ref} />
|
||||
{(data.emulator?.storeDownloadInfo?.hasUpdate || !data.emulator?.storeDownloadInfo) && installedFromStore && !!updateToVersion && <div className="tooltip tooltip-warning" data-tip="Update Available">
|
||||
<Button id="update-warning-bt" tooltipType="warning" tooltip="Update Available" style="warning" className="rounded-full size-14 focusable focusable-warning shadow-lg" onAction={() => setOpen(true, 'update-warning-bt')}><CircleFadingArrowUp /></Button>
|
||||
</div>}
|
||||
{(!data.emulator?.bios || data.emulator.bios.length <= 0) && (data.emulator?.biosRequirement === 'required') && installedFromStore && <div className="tooltip tooltip-error" data-tip="Missing BIOS">
|
||||
<Button id="bios-warning-bt" tooltipType="error" tooltip="Missing BIOS" style="error" className="rounded-full size-14 focusable focusable-error shadow-lg" onAction={() => setOpen(true, 'bios-warning-bt')}><TriangleAlert /></Button>
|
||||
</div>}
|
||||
|
|
@ -310,7 +330,8 @@ export function RouteComponent ()
|
|||
}], [router]);
|
||||
|
||||
const installMutation = useMutation({
|
||||
...installEmulatorMutation(id), onSuccess: (data, variables, onMutateResult, context) => context.client.refetchQueries(storeEmulatorDetailsQuery(id)),
|
||||
...installEmulatorMutation(id),
|
||||
onSuccess: (data, variables, onMutateResult, context) => context.client.refetchQueries(storeEmulatorDetailsQuery(id)),
|
||||
});
|
||||
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
|
@ -320,21 +341,33 @@ export function RouteComponent ()
|
|||
{
|
||||
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.sources.flatMap(s => [{
|
||||
label: "Source", content: <div className="flex flex-wrap gap-1 p-1">
|
||||
<div className="flex gap-1 flex-1">{emulatorStatusIcons[s.type]}{s.type}:</div>
|
||||
<div className="grow text-base-content/40">{s.binPath}</div>
|
||||
stats.push(...emulator.validSources.flatMap(s => [{
|
||||
label: "Source", content: <div className="flex flex-col grow">
|
||||
<div className="flex grow flex-wrap justify-between gap-1">
|
||||
<div className="flex gap-1">{emulatorStatusIcons[s.type]}{s.type}</div>
|
||||
<div className="text-base-content/40">{s.binPath}</div>
|
||||
</div>
|
||||
{emulator.integrations.some(i => i.source?.type === s.type) && <div className="divider m-0"></div>}
|
||||
{emulator.integrations.filter(i => i.source?.type === s.type).map(i =>
|
||||
{
|
||||
return <div key={i.id} className="flex flex-wrap justify-between gap-1">
|
||||
<div className="flex gap-2">
|
||||
<Puzzle />
|
||||
<div>{i.id}</div>
|
||||
</div>
|
||||
<div className="text-base-content/40">{`${i.capabilities?.join(", ")}`}</div>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
}]));
|
||||
if (emulator.bios)
|
||||
stats.push({
|
||||
label: "Bios", content: emulator.bios && emulator.bios.length > 0 ? emulator.bios : <div className="text-warning font-semibold">Missing</div>
|
||||
});
|
||||
if (emulator.integration)
|
||||
{
|
||||
stats.push({ label: "Integration", icon: <Puzzle />, content: `${emulator.integration.name} (${emulator.integration.version})` });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -344,7 +377,7 @@ export function RouteComponent ()
|
|||
<StickyHeaderUI ref={ref} />
|
||||
<div className="flex flex-col z-10">
|
||||
<div className="w-full sm:px-8 md:px-16 pb-8 pt-12">
|
||||
<TitleArea emulator={emulator} onInstall={installMutation.mutate} />
|
||||
<TitleArea emulator={emulator} onInstall={s => installMutation.mutate({ source: s, isUpdate: false })} onUpdate={s => installMutation.mutate({ source: s, isUpdate: true })} />
|
||||
|
||||
<div className='mobile:hidden left-0 top-0 absolute bg-gradient'></div>
|
||||
<div className='mobile:hidden left-0 top-0 absolute bg-noise'></div>
|
||||
|
|
|
|||
|
|
@ -64,9 +64,9 @@ export const storeGetStatsQuery = queryOptions({
|
|||
});
|
||||
export const installEmulatorMutation = (id: string) => mutationOptions({
|
||||
mutationKey: ['install', 'emulator', id],
|
||||
mutationFn: async (source: string) =>
|
||||
mutationFn: async (ctx: { source: string, isUpdate: boolean; }) =>
|
||||
{
|
||||
const { data, error } = await storeApi.api.store.install.emulator({ id })({ source }).post();
|
||||
const { data, error } = await storeApi.api.store.install.emulator({ id })({ source: ctx.source }).post({ isUpdate: ctx.isUpdate });
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
|
@ -85,4 +85,12 @@ export const deleteBiosMutation = mutationOptions({
|
|||
const { error } = await storeApi.api.store.bios({ id }).delete();
|
||||
if (error) throw error;
|
||||
}
|
||||
});
|
||||
export const getUpdateInfoForEmulator = (id: string) => queryOptions({
|
||||
queryKey: ['emulator', 'update'], queryFn: async () =>
|
||||
{
|
||||
const { data, error } = await storeApi.api.store.emulator({ id }).update.get();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue