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:
Simeon Radivoev 2026-04-09 17:15:37 +03:00
parent 54dd9256e3
commit 7948bd24fa
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
36 changed files with 1103 additions and 243 deletions

View file

@ -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);

View file

@ -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 />
} >