fix: Fixed romm login, now uses token

feat: Moved romm to internal plugin
fix: Made focusing and navigation more reliable
fix: Loading errors on first time launch
This commit is contained in:
Simeon Radivoev 2026-03-28 17:32:51 +02:00
parent 7c10f4e4c2
commit 816d50ae4d
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
81 changed files with 1659 additions and 1097 deletions

View file

@ -22,7 +22,7 @@ export default function ActionButton (data: {
primary: "bg-primary text-primary-content focused:bg-base-content focused:text-base-300 focusable focusable-primary",
base: " text-base-content border-dashed border-base-content/20 border-2 focused:bg-base-content focused:text-base-300 focusable focusable-primary",
accent: "bg-accent text-accent-content focusable focusable-primary focusable:bg-base-content focusable:text-base-300",
error: "bg-error text-error-content focused:bg-error focused:text-error-content",
error: "bg-error text-error-content focused:bg-error focused:text-error-content focusable focusable-primary",
};
return (
<div className="tooltip tooltip-accent tooltip-right" data-tip={data.tooltip}>

View file

@ -28,13 +28,13 @@ function AchievementsInfo (data: { game: FrontEndGameTypeDetailed; } & InteractP
</ActionButton>;
}
export default function ActionButtons (data: { game: FrontEndGameTypeDetailed, source: string, id: string; })
export default function ActionButtons (data: { game?: FrontEndGameTypeDetailed, source: string, id: string; })
{
const [, setDetailsSection] = useLocalStorage('details-section', 'screenshots');
const { ref, focusKey, hasFocusedChild } = useFocusable({ focusKey: 'actions', trackChildren: true });
const { ref, focusKey, hasFocusedChild } = useFocusable({ focusKey: 'actions', forceFocus: true, trackChildren: true, preferredChildFocusKey: 'mainAction' });
const deleteMutation = useMutation({
...deleteGameMutation(data.game.id),
...deleteGameMutation({ id: data.id, source: data.source }),
onSuccess: () =>
{
location.reload();
@ -47,7 +47,7 @@ export default function ActionButtons (data: { game: FrontEndGameTypeDetailed, s
});
const contextOptions: DialogEntry[] = [];
if (data.game.local)
if (data.game?.local)
{
contextOptions.push({
id: 'delete',
@ -66,15 +66,15 @@ export default function ActionButtons (data: { game: FrontEndGameTypeDetailed, s
return <div ref={ref} className="flex sm:gap-2 md:gap-4 sm:h-16 md:h-32 overflow-hidden p-2 items-center shrink-0">
<FocusContext value={focusKey}>
<MainActions game={data.game} source={data.source} id={data.id} />
<AchievementsInfo game={data.game} onAction={() =>
{data.game && <AchievementsInfo game={data.game} onAction={() =>
{
setDetailsSection("achievements");
if (data.game.achievements?.entires[0])
if (data.game?.achievements?.entires[0])
{
setFocus(data.game.achievements.entires[0].id);
}
}} />
}} />}
<ActionButton tooltip="Settings" onAction={() => setOpen(true, 'settings')} type="base" id="settings" icon={<Settings />} >
</ActionButton >
{settingsDialog}

View file

@ -27,8 +27,9 @@ export default function Details (data: {
const { ref, focusKey } = useFocusable({
focusKey: 'main-details',
onFocus: (l, p, d) => scrollIntoViewHandler({ block: 'end', behavior: 'smooth' })(focusKey, ref.current, d),
preferredChildFocusKey: "play-btn",
saveLastFocusedChild: false
preferredChildFocusKey: "actions",
saveLastFocusedChild: false,
forceFocus: true
});
const platformCoverImg = data.game?.path_platform_cover ? new URL(`${RPC_URL(__HOST__)}${data.game?.path_platform_cover}`) : undefined;
@ -87,7 +88,7 @@ export default function Details (data: {
<div className="skeleton h-4 w-[80%]"></div>
</div>}
</div>
{!!data.game && <ActionButtons source={data.source} id={data.id} game={data.game} key="actions" />}
<ActionButtons source={data.source} id={data.id} game={data.game} key="actions" />
</div>
</section>
</FocusContext>

View file

@ -6,11 +6,11 @@ import { getErrorMessage } from "react-error-boundary";
import toast from "react-hot-toast";
import { useLocalStorage } from "usehooks-ts";
import { ContextList, DialogEntry, useContextDialog } from "../ContextDialog";
import { Clock, Download, EllipsisVertical, PackageOpen, Play, TriangleAlert } from "lucide-react";
import { Clock, Download, EllipsisVertical, Import, PackageOpen, Play, TriangleAlert } from "lucide-react";
import { installMutation, playMutation } from "@/mainview/scripts/queries/romm";
import ActionButton from "./ActionButton";
export default function MainActions (data: { game: FrontEndGameTypeDetailed, source: string, id: string; })
export default function MainActions (data: { game?: FrontEndGameTypeDetailed, source: string, id: string; })
{
const installMut = useMutation(installMutation(data.source, data.id));
const playMut = useMutation({
@ -29,7 +29,7 @@ export default function MainActions (data: { game: FrontEndGameTypeDetailed, sou
const [error, setError] = useState<string | undefined>(undefined);
const [details, setDetails] = useState<string | undefined>(undefined);
const [commands, setCommands] = useState<CommandEntry[] | undefined>(undefined);
const [preferredCommand, setPreferredCommand] = useLocalStorage<string | number | undefined>(`${data.game.source ?? data.game.id.source}-${data.game.source_id ?? data.game.id.id}-preferred-command`, undefined);
const [preferredCommand, setPreferredCommand] = useLocalStorage<string | number | undefined>(`${data.game?.source ?? data.game?.id.source}-${data.game?.source_id ?? data.game?.id.id}-preferred-command`, undefined);
const queryClient = useQueryClient();
const validCommands = commands ? commands.filter(c => c.valid) : [];
const validDefaultCommand = commands?.find(c =>
@ -41,7 +41,7 @@ export default function MainActions (data: { game: FrontEndGameTypeDetailed, sou
useEffect(() =>
{
const sub = rommApi.api.romm.status({ source: data.game.id.source })({ id: data.game.id.id }).subscribe();
const sub = rommApi.api.romm.status({ source: data.source })({ id: data.id }).subscribe();
ws.current = sub.ws;
sub.subscribe((e) =>
@ -69,7 +69,7 @@ export default function MainActions (data: { game: FrontEndGameTypeDetailed, sou
sub.close();
ws.current = undefined;
};
}, [data.game.id]);
}, [data.source, data.id]);
let progressIcon: JSX.Element | undefined = undefined;
switch (status)
@ -101,7 +101,7 @@ export default function MainActions (data: { game: FrontEndGameTypeDetailed, sou
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.game.id.source, id: data.game.id.id, command_id: cmd.id });
playMut.mutate({ source: data.source, id: data.id, command_id: cmd.id });
}
};
@ -142,20 +142,31 @@ export default function MainActions (data: { game: FrontEndGameTypeDetailed, sou
}
else
{
let icon = <span className="loading loading-spinner loading-lg"></span>;
if (status === 'install')
{
icon = <Download />;
} else if (status === 'present')
{
icon = <Import />;
}
mainButton = <ActionButton
key={status ?? 'unknown'}
disabled={installMut.isPending}
onAction={() =>
{
if (status === 'install')
switch (status)
{
installMut.mutate();
case 'present':
case 'install':
installMut.mutate();
break;
}
}}
tooltip={details ?? status}
type='primary'
id="mainAction">
{status === 'install' ? <Download /> : <span className="loading loading-spinner loading-lg"></span>}
{icon}
</ActionButton>;
}