feat: Bundled NW.js with appimages
feat: Implemented self update feat: Added rclone saves for emulators fix: Fixed auto focus in builds feat: Added helper cards on empty library
This commit is contained in:
parent
587956c792
commit
813785f4f3
59 changed files with 1210 additions and 480 deletions
|
|
@ -25,17 +25,13 @@ export function AnimatedBackground (data: {
|
|||
)
|
||||
: useState<string | undefined>();
|
||||
|
||||
const [lastBackgroundUrl, setLastBackgroundUrl] = useState<string | undefined>(undefined);
|
||||
const backgroundElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const lastBg = backgroundUrl;
|
||||
|
||||
if (data.backgroundUrl != backgroundUrl)
|
||||
{
|
||||
setBackgroundUrl(data.backgroundUrl ? (data.backgroundUrl instanceof URL ? data.backgroundUrl.href : data.backgroundUrl) : undefined);
|
||||
setLastBackgroundUrl(lastBg);
|
||||
}
|
||||
}, [data.backgroundUrl]);
|
||||
|
||||
|
|
@ -44,13 +40,6 @@ export function AnimatedBackground (data: {
|
|||
{
|
||||
finalBackgroundUrl = backgroundUrl ? new URL(backgroundUrl) : undefined;
|
||||
} catch { }
|
||||
|
||||
let finalLastBackgroundUrl: URL | undefined;
|
||||
try
|
||||
{
|
||||
finalLastBackgroundUrl = lastBackgroundUrl ? new URL(lastBackgroundUrl) : undefined;
|
||||
} catch { }
|
||||
|
||||
const blur = useLocalSetting('backgroundBlur');
|
||||
if (blur)
|
||||
{
|
||||
|
|
@ -59,13 +48,7 @@ export function AnimatedBackground (data: {
|
|||
finalBackgroundUrl?.searchParams.set('blur', String(24));
|
||||
}
|
||||
|
||||
if (!finalLastBackgroundUrl?.searchParams.has('blur'))
|
||||
{
|
||||
finalLastBackgroundUrl?.searchParams.set('blur', String(24));
|
||||
}
|
||||
|
||||
finalBackgroundUrl?.searchParams.set('height', String(320));
|
||||
finalLastBackgroundUrl?.searchParams.set('height', String(320));
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
|
|
@ -90,8 +73,6 @@ export function AnimatedBackground (data: {
|
|||
|
||||
function handleSetBackground (url: string)
|
||||
{
|
||||
|
||||
setLastBackgroundUrl(backgroundUrl);
|
||||
setBackgroundUrl(url);
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +101,7 @@ export function AnimatedBackground (data: {
|
|||
>
|
||||
{!data.scrolling && <div className='absolute top-0 left-0 right-0 bottom-0 overflow-hidden'>
|
||||
<div className='absolute w-full h-full bg-radial from-base-100 to-base-300 -z-5'></div>
|
||||
{blur && finalLastBackgroundUrl && <img className='absolute w-full h-full object-cover object-center -z-4 mask-radial-at-center mask-radial-from-0 mask-radial-farthest-corner' src={finalLastBackgroundUrl.href}></img>}
|
||||
|
||||
{finalBackgroundUrl ? <img
|
||||
decoding="async"
|
||||
key={finalBackgroundUrl?.href}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ export default function AppCommunication (data: { children: any; })
|
|||
});
|
||||
|
||||
document.documentElement.dataset.loaded = "true";
|
||||
return () =>
|
||||
{
|
||||
sub.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <SystemInfoContext value={systemInfo}>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { doesFocusableExist, FocusDetails, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { useLayoutEffect } from "react";
|
||||
import { useEffect, useLayoutEffect } from "react";
|
||||
|
||||
export function AutoFocus (data: {
|
||||
parentKey?: string;
|
||||
|
|
@ -8,11 +8,15 @@ export function AutoFocus (data: {
|
|||
delay?: number;
|
||||
})
|
||||
{
|
||||
useLayoutEffect(() =>
|
||||
useEffect(() =>
|
||||
{
|
||||
let delayTimeout: number | undefined;
|
||||
|
||||
if (data.force || !getCurrentFocusKey() || getCurrentFocusKey() === data.parentKey || !doesFocusableExist(getCurrentFocusKey()))
|
||||
const focusDoesntExist = !doesFocusableExist(getCurrentFocusKey());
|
||||
const parentFocus = getCurrentFocusKey() === data.parentKey;
|
||||
const noFocus = !getCurrentFocusKey();
|
||||
|
||||
if (data.force || noFocus || parentFocus || focusDoesntExist)
|
||||
{
|
||||
if (data.delay)
|
||||
{
|
||||
|
|
@ -21,8 +25,8 @@ export function AutoFocus (data: {
|
|||
{
|
||||
data.focus({ instant: true });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return () =>
|
||||
{
|
||||
if (delayTimeout)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function LocalCardElement (data: { game: GameMetaExtra, i: number; } & FocusPara
|
|||
oneShot('click');
|
||||
};
|
||||
|
||||
useShortcuts(data.game.focusKey, () => [{ label: "Select", button: GamePadButtonCode.A, action: event => handleAction({ event, focusKey: data.game.focusKey }) }]);
|
||||
useShortcuts(data.game.focusKey, () => [{ label: "Details", button: GamePadButtonCode.A, action: event => handleAction({ event, focusKey: data.game.focusKey }) }]);
|
||||
|
||||
return (
|
||||
<CardElement
|
||||
|
|
@ -69,7 +69,7 @@ export function CardList (data: {
|
|||
{
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: data.id,
|
||||
focusable: data.games.length > 0,
|
||||
focusable: data.games.length > 0 || (!!data.finalElement && (Array.isArray(data.finalElement) ? data.finalElement.length > 0 : !!data.finalElement)),
|
||||
preferredChildFocusKey: data.focus
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { TwitchIcon } from "../scripts/brandIcons";
|
|||
import { rommLoggedInQuery } from "../scripts/queries/romm";
|
||||
import { twitchLoginVerificationQuery } from "../scripts/queries/settings";
|
||||
import { SystemInfoContext } from "../scripts/contexts";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { oneShot } from "../scripts/audio/audio";
|
||||
import { hasUpdateQuery } from "../scripts/queries/system";
|
||||
|
||||
|
|
@ -87,16 +87,24 @@ export interface HeaderAccount
|
|||
|
||||
function UpdateStatus ()
|
||||
{
|
||||
const handleSelect = () =>
|
||||
{
|
||||
navigate({ to: '/settings/about' });
|
||||
};
|
||||
const hasUnread = false;
|
||||
return <div className={classNames("tooltip tooltip-bottom tooltip-warning p-2 rounded-full", { "bg-warning text-warning-content": hasUnread })} data-tip="Update Available">
|
||||
<CircleFadingArrowUp className="sm:size-4 md:size-8 text-warning" />
|
||||
const navigate = useNavigate();
|
||||
const { ref } = useFocusable({
|
||||
focusKey: 'update-bt', onEnterPress: handleSelect
|
||||
});
|
||||
return <div onClick={handleSelect} ref={ref} className={classNames("tooltip tooltip-bottom tooltip-warning p-2 rounded-full focusable focusable-primary focusable-hover focused:bg-warning cursor-pointer", { "bg-warning text-warning-content ": hasUnread })} data-tip="Update Available">
|
||||
<CircleFadingArrowUp className="sm:size-4 md:size-8 text-warning in-focused:text-warning-content" />
|
||||
</div>;
|
||||
}
|
||||
|
||||
function NotificationStatus ()
|
||||
{
|
||||
const hasUnread = false;
|
||||
return <div className={classNames("p-2 rounded-full", { "bg-warning text-warning-content": hasUnread })}>
|
||||
return <div className={classNames("p-2 rounded-full focused:bg-base-300", { "bg-warning text-warning-content": hasUnread })}>
|
||||
<Bell className="sm:size-4 md:size-8" />
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -219,14 +227,17 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; })
|
|||
router.navigate({ to: '/settings/accounts' });
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref } = useFocusable({
|
||||
focusKey: 'accounts', onEnterPress: handleSelect
|
||||
});
|
||||
|
||||
const accounts: HeaderAccount[] = [];
|
||||
if (data.accounts) accounts.push(...data.accounts);
|
||||
const router = useRouter();
|
||||
|
||||
const { ref } = useFocusable({
|
||||
focusKey: 'accounts',
|
||||
onEnterPress: handleSelect,
|
||||
focusable: accounts.length > 0
|
||||
});
|
||||
|
||||
if (rommUser.data?.hasLogin || rommUser.isError)
|
||||
{
|
||||
accounts.push({
|
||||
|
|
@ -259,7 +270,7 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; })
|
|||
export function HeaderStatusBar (data: { buttons?: HeaderButton[]; buttonElements?: JSX.Element[] | JSX.Element; })
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'header-status-bar' });
|
||||
const { data: hasUpdate } = useQuery(hasUpdateQuery);
|
||||
const { data: update } = useQuery(hasUpdateQuery);
|
||||
return <div ref={ref} className="flex items-center sm:gap-1 md:gap-2 text drop-shadow-sm">
|
||||
<FocusContext value={focusKey}>
|
||||
<div className="flex gap-2 items-center" style={{ viewTransitionName: 'status-bar-icons' }}>
|
||||
|
|
@ -267,7 +278,7 @@ export function HeaderStatusBar (data: { buttons?: HeaderButton[]; buttonElement
|
|||
<WiFiStatus />
|
||||
<BluetoothStatus />
|
||||
<NotificationStatus />
|
||||
{!!hasUpdate && hasUpdate >= 1 && <UpdateStatus />}
|
||||
{!!update && update.hasUpdate >= 1 && <UpdateStatus />}
|
||||
<BatteryStatus />
|
||||
</div>
|
||||
{!!data.buttons && <div className="divider divider-horizontal mx-0"></div>}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,20 @@ export default function ImageWithFallbacks (data: {
|
|||
{
|
||||
img.dataset.index = String(nextIndex);
|
||||
img.src = data.src[nextIndex].href;
|
||||
|
||||
}
|
||||
};
|
||||
return <img draggable={data.draggable} className={data.className} src={data.src[0].href} data-index={0} onError={handleError}></img>;
|
||||
return <img
|
||||
draggable={data.draggable}
|
||||
className={data.className}
|
||||
src={data.src[0].href}
|
||||
data-index={0}
|
||||
onError={handleError}
|
||||
onLoad={e =>
|
||||
{
|
||||
e.currentTarget.dataset.loaded = "true";
|
||||
}}
|
||||
>
|
||||
|
||||
</img>;
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { Ref, RefObject } from 'react';
|
||||
import './dots.css';
|
||||
|
||||
export default function DotsLoading ()
|
||||
export default function DotsLoading (data: { ref?: Ref<any>; })
|
||||
{
|
||||
return <div className="flex gap-3 justify-center animation_alternate items-center pt-8">
|
||||
return <div ref={data.ref} className="flex gap-3 justify-center animation_alternate items-center pt-8">
|
||||
<div className="ball size-6"></div>
|
||||
<div className="ball size-6"></div>
|
||||
<div className="ball size-6"></div>
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
if (!cmd) return;
|
||||
if (cmd.emulator === 'EMULATORJS')
|
||||
{
|
||||
const params = new URLSearchParams(cmd.command);
|
||||
const params = new URLSearchParams(Array.isArray(cmd.command) ? cmd.command[0] : cmd.command);
|
||||
router.navigate({ to: '/embedded/$source/$id', params: { source: data.source, id: data.id }, search: Object.fromEntries(params.entries()) });
|
||||
} else
|
||||
{
|
||||
|
|
@ -120,14 +120,15 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
let mainButton: any | undefined = undefined;
|
||||
if (status === 'installed')
|
||||
{
|
||||
mainButton = <div className="flex gap-2"><ActionButton onAction={() => handlePlay(validDefaultCommand)} tooltip={validDefaultCommand?.label ?? details}
|
||||
key="primary"
|
||||
type='primary'
|
||||
id="mainAction"
|
||||
>
|
||||
<Play />
|
||||
mainButton = <div className="flex gap-2">
|
||||
<ActionButton onAction={() => handlePlay(validDefaultCommand)} tooltip={validDefaultCommand?.label ?? details}
|
||||
key="primary"
|
||||
type='primary'
|
||||
id="mainAction"
|
||||
>
|
||||
<Play />
|
||||
|
||||
</ActionButton>
|
||||
</ActionButton>
|
||||
|
||||
{validCommands.length > 1 &&
|
||||
<ActionButton className="size-11! header-icon-small" tooltip={"All Commands"} type="base" id="allActionsBtn" onAction={() => showAllCommands(true, 'allActionsBtn')}>
|
||||
|
|
|
|||
|
|
@ -464,7 +464,7 @@ const assets = new Set<string>([
|
|||
]);
|
||||
|
||||
// Store basePath resolved from Vite config
|
||||
const BASE_PATH = "./";
|
||||
const BASE_PATH = "/";
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
@theme {
|
||||
--breakpoint-sm: 0px;
|
||||
--breakpoint-md: 1280px;
|
||||
--breakpoint-md: 1024px;
|
||||
--page-scroll-bg: transparent;
|
||||
--animation-size: 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -166,7 +166,6 @@ function RouteComponent ()
|
|||
|
||||
return (
|
||||
<AnimatedBackground ref={ref} backgroundKey="game-details" backgroundUrl={backgroundImage} scrolling>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
<GameDetailsContext value={{
|
||||
update: () => setUpdate(v => v + 1)
|
||||
}} >
|
||||
|
|
@ -214,6 +213,7 @@ function RouteComponent ()
|
|||
</div>
|
||||
<FloatingShortcuts />
|
||||
</GameDetailsContext>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</AnimatedBackground>
|
||||
);
|
||||
}
|
||||
|
|
@ -13,10 +13,13 @@ import
|
|||
LayoutGrid,
|
||||
PlusCircle,
|
||||
Plus,
|
||||
LucideIcon,
|
||||
} from "lucide-react";
|
||||
import
|
||||
{
|
||||
createFileRoute,
|
||||
PathParamOptions,
|
||||
ToPathOption,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -52,6 +55,7 @@ import { FloatingShortcuts } from "../components/Shortcuts";
|
|||
import SelectMenu from "../components/SelectMenu";
|
||||
import HeaderSearchField from "../components/HeaderSearchField";
|
||||
import CardElement from "../components/CardElement";
|
||||
import { Router } from "..";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: ConsoleHomeUI,
|
||||
|
|
@ -114,24 +118,30 @@ function Preview (data: { index: number; children?: any; })
|
|||
</div>;
|
||||
}
|
||||
|
||||
function GetStoreGamesCard ()
|
||||
function AdditionalCard (data: {
|
||||
id: string,
|
||||
route: keyof typeof Router.routesByPath,
|
||||
title: string,
|
||||
subTitle: string,
|
||||
index: number,
|
||||
actionLabel: string;
|
||||
icon: LucideIcon | string;
|
||||
badgeIcon?: LucideIcon;
|
||||
})
|
||||
{
|
||||
const router = useRouter();
|
||||
const handleNavigate = () =>
|
||||
{
|
||||
router.navigate({ to: '/store/tab/games' });
|
||||
};
|
||||
return <CardElement onFocus={scrollIntoViewHandler({ behavior: "smooth", inline: "center" })} badges={[<Search className="size-8" />]} onAction={handleNavigate} title="Gameflow Store" subtitle="Get Free Games" preview={<Preview index={43} ><Store className="not-mobile:drop-shadow-md in-focus:animate-rotate size-32" /></Preview>} focusKey='store-games-btn' index={0} id="store-games-btn" />;
|
||||
}
|
||||
|
||||
function ShowAllGamesCard ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const handleNavigate = () =>
|
||||
{
|
||||
router.navigate({ to: '/games' });
|
||||
router.navigate({ to: data.route as any });
|
||||
};
|
||||
return <CardElement onFocus={scrollIntoViewHandler({ behavior: "smooth", inline: "center" })} onAction={handleNavigate} title="All Games" preview={<Preview index={17} ><LayoutGrid className="not-mobile:drop-shadow-md in-focus:animate-rotate size-32" /></Preview>} focusKey='all-games-btn' index={0} id="all-games-btn" />;
|
||||
useShortcuts(data.id, () => [{ label: data.actionLabel, button: GamePadButtonCode.A, action: handleNavigate }]);
|
||||
return <CardElement onFocus={scrollIntoViewHandler({ behavior: "smooth", inline: "center" })} badges={data.badgeIcon ? [<data.badgeIcon className="size-8" />] : undefined} onAction={handleNavigate} title={data.title} subtitle={data.subTitle} preview={<Preview index={data.index} >
|
||||
{typeof data.icon === 'string' ?
|
||||
<img className="not-mobile:drop-shadow-md" src={data.icon} /> :
|
||||
<data.icon className="not-mobile:drop-shadow-md in-focus:animate-rotate size-32" />
|
||||
}
|
||||
</Preview>} focusKey={data.id} index={0} id={data.id} />;
|
||||
}
|
||||
|
||||
function HomeList (data: {
|
||||
|
|
@ -197,8 +207,13 @@ function HomeList (data: {
|
|||
id="games-list"
|
||||
setBackground={bg.setBackground}
|
||||
filters={{ limit: 12, orderBy: 'activity' }}
|
||||
finalElement={[<GetStoreGamesCard />, <ShowAllGamesCard />]}
|
||||
emptyElement={[]}
|
||||
finalElement={[
|
||||
<AdditionalCard key='store-games-btn' icon={Store} badgeIcon={Search} route='/store/tab/games' id='store-games-btn' title="Gameflow Store" subTitle="Get Free Games" index={43} actionLabel="Go To Store" />,
|
||||
<AdditionalCard key='all-games-btn' icon={LayoutGrid} route='/games' id='all-games-btn' title="All Games" subTitle="All Owned Games" index={17} actionLabel="All Games" />
|
||||
]}
|
||||
emptyElement={[
|
||||
<AdditionalCard key='romm-setup-btn' icon={'https://romm.app/_ipx/q_80/images/blocks/logos/romm.svg'} route='/settings/accounts' id='romm-setup-btn' title="Setup Romm" subTitle="To Import Games" index={18} actionLabel="Setup Romm" />
|
||||
]}
|
||||
/>
|
||||
<AutoFocus parentKey={focusKey} focus={focusSelf} delay={10} />
|
||||
</>;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ 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';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { rommApi } from '../scripts/clientApi';
|
||||
|
||||
export const Route = createFileRoute('/launcher/$source/$id')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -39,7 +40,7 @@ function RouteComponent ()
|
|||
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||
|
||||
const { data, state } = useJobStatus('launch-game', {
|
||||
const { state, data } = useJobStatus('launch-game', {
|
||||
onProgress (process, data)
|
||||
{
|
||||
if (progressRef.current)
|
||||
|
|
@ -55,6 +56,7 @@ function RouteComponent ()
|
|||
},
|
||||
}, [progressRef.current, HandleGoBack]);
|
||||
|
||||
|
||||
useBlocker({ shouldBlockFn: () => !!data });
|
||||
|
||||
return <AnimatedBackground ref={ref} backgroundKey='game-details'>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
|
||||
|
||||
import { systemInfoQuery } from '@queries/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@/mainview/components/options/Button';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { checkUpdateMutation, hasUpdateQuery, systemInfoQuery, updateMutation } from '@queries/system';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { ArrowUpCircle, CircleFadingArrowUp, RefreshCcw } from 'lucide-react';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
export const Route = createFileRoute('/settings/about')({
|
||||
|
|
@ -12,58 +15,87 @@ export const Route = createFileRoute('/settings/about')({
|
|||
function RouteComponent ()
|
||||
{
|
||||
const { data: systemInfo } = useQuery(systemInfoQuery);
|
||||
return <table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Agent</th>
|
||||
<td>{navigator.userAgent}</td>
|
||||
</tr>
|
||||
{/* row 2 */}
|
||||
<tr>
|
||||
<th>Platform</th>
|
||||
<td>{navigator.platform}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Resolution</th>
|
||||
<td>{screen.width}x{screen.height}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Window</th>
|
||||
<td>{window.innerWidth}x{window.innerHeight}</td>
|
||||
</tr>
|
||||
{/* row 3 */}
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<td>{systemInfo?.data?.user}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Architecture</th>
|
||||
<td>{systemInfo?.data?.arch}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>System</th>
|
||||
<td>{systemInfo?.data?.platform}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<td>{systemInfo?.data?.hostname}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Machine</th>
|
||||
<td>{systemInfo?.data?.machine}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Sizes</th>
|
||||
<td>Cache: {prettyBytes(systemInfo?.data?.cacheSize ?? 0)}, Store: {prettyBytes(systemInfo?.data?.storeSize ?? 0)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<td>{systemInfo?.data?.source}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Steam Deck</th>
|
||||
<td>{systemInfo?.data?.steamDeck ?? 'false'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'about-section' });
|
||||
const { data: hasUpdate, refetch: refetchHasUpdate } = useQuery(hasUpdateQuery);
|
||||
const update = useMutation(updateMutation);
|
||||
const forceCheckUpdate = useMutation({
|
||||
...checkUpdateMutation,
|
||||
onSuccess (data, variables, onMutateResult, context)
|
||||
{
|
||||
refetchHasUpdate();
|
||||
},
|
||||
});
|
||||
|
||||
return <table ref={ref} className="table">
|
||||
|
||||
<FocusContext value={focusKey}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<td>{systemInfo?.data?.version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Update</th>
|
||||
<td className='flex flex-flex-wrap gap-2'>
|
||||
{
|
||||
hasUpdate && hasUpdate.hasUpdate > 0 ?
|
||||
<Button className='gap-3' style='warning' id='update-btn' onAction={() => update.mutate()}><CircleFadingArrowUp /> Update to {hasUpdate?.version}</Button> :
|
||||
<Button className='gap-3' id='update-btn' onAction={() => forceCheckUpdate.mutate()}>{forceCheckUpdate.isPending ? <span className="loading loading-spinner loading-lg"></span> : <RefreshCcw />}Check for Update</Button>
|
||||
}
|
||||
{<Button className='gap-3' id='force-update-btn' onAction={() => update.mutate()}><CircleFadingArrowUp /> Force Update</Button>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Agent</th>
|
||||
<td>{navigator.userAgent}</td>
|
||||
</tr>
|
||||
{/* row 2 */}
|
||||
<tr>
|
||||
<th>Platform</th>
|
||||
<td>{navigator.platform}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Resolution</th>
|
||||
<td>{screen.width}x{screen.height}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Window</th>
|
||||
<td>{window.innerWidth}x{window.innerHeight}</td>
|
||||
</tr>
|
||||
{/* row 3 */}
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<td>{systemInfo?.data?.user}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Architecture</th>
|
||||
<td>{systemInfo?.data?.arch}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>System</th>
|
||||
<td>{systemInfo?.data?.platform}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<td>{systemInfo?.data?.hostname}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Machine</th>
|
||||
<td>{systemInfo?.data?.machine}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Sizes</th>
|
||||
<td>Cache: {prettyBytes(systemInfo?.data?.cacheSize ?? 0)}, Store: {prettyBytes(systemInfo?.data?.storeSize ?? 0)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<td>{systemInfo?.data?.source}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Steam Deck</th>
|
||||
<td>{systemInfo?.data?.steamDeck ?? 'false'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</FocusContext>
|
||||
</table>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { AutoFocus } from '@/mainview/components/AutoFocus';
|
||||
import DotsLoading from '@/mainview/components/backgrounds/dots';
|
||||
import { Button } from '@/mainview/components/options/Button';
|
||||
import { OptionDropdown } from '@/mainview/components/options/OptionDropdown';
|
||||
import { OptionInput } from '@/mainview/components/options/OptionInput';
|
||||
|
|
@ -7,16 +8,34 @@ import { RoundButton } from '@/mainview/components/RoundButton';
|
|||
import { getAllPluginsQuery, getPluginDetailsQuery } from '@/mainview/scripts/queries/plugins';
|
||||
import { getPluginActionsQuery, getPluginSettingQuery, getPluginSettingsDefinitionQuery, pluginActionMutation, setPluginSettingMutation } from '@/mainview/scripts/queries/settings';
|
||||
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||
import { scrollIntoViewHandler } from '@/mainview/scripts/utils';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { ArrowLeft, CirclePlay, Play, Settings2, SettingsIcon } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
export const Route = createFileRoute('/settings/plugin/$source')({
|
||||
component: RouteComponent,
|
||||
pendingComponent: Loading,
|
||||
async loader (ctx)
|
||||
{
|
||||
const definitions = await ctx.context.queryClient.fetchQuery(getPluginSettingsDefinitionQuery(ctx.params.source));
|
||||
const actions = await ctx.context.queryClient.fetchQuery(getPluginActionsQuery(ctx.params.source));
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return { definitions, actions };
|
||||
},
|
||||
});
|
||||
|
||||
function Loading ()
|
||||
{
|
||||
const { ref, focusSelf } = useFocusable({ focusKey: 'plugins' });
|
||||
return <>
|
||||
<DotsLoading ref={ref} />
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</>;
|
||||
}
|
||||
|
||||
function PluginAction (data: { id: string, title: string | undefined, description: string | undefined; action: string; reload: () => void; })
|
||||
{
|
||||
const { source } = Route.useParams();
|
||||
|
|
@ -91,15 +110,19 @@ function PluginOption (data: { name: string, title?: string, prop: JSONSchema7;
|
|||
|
||||
function Settings ()
|
||||
{
|
||||
const { definitions, actions } = Route.useLoaderData();
|
||||
const { source } = Route.useParams();
|
||||
const { data: definitions, refetch: refetchDefinitions } = useQuery(getPluginSettingsDefinitionQuery(source));
|
||||
const { data: actions, refetch: referchActions } = useQuery(getPluginActionsQuery(source));
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleReload = () =>
|
||||
{
|
||||
referchActions();
|
||||
refetchDefinitions();
|
||||
queryClient.refetchQueries(getPluginSettingsDefinitionQuery(source));
|
||||
queryClient.refetchQueries(getPluginActionsQuery(source));
|
||||
};
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'plugin-settings' });
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: 'plugin-settings',
|
||||
focusable: (definitions?.properties && Object.keys(definitions?.properties).length > 0) || actions.length > 0
|
||||
});
|
||||
return <div ref={ref}>
|
||||
<FocusContext value={focusKey}>
|
||||
{!!definitions?.properties && Object.entries(Object.groupBy(Object.entries(definitions?.properties)
|
||||
|
|
@ -142,16 +165,19 @@ function RouteComponent ()
|
|||
|
||||
return <div ref={ref}>
|
||||
<FocusContext value={focusKey}>
|
||||
<RoundButton className='absolute' id='return-to-plugins' onAction={handleReturn}><ArrowLeft /></RoundButton>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex text-2xl font-bold gap-2 grow items-center justify-center'>
|
||||
<RoundButton onFocus={scrollIntoViewHandler({ inline: 'end' })} id='return-to-plugins' onAction={handleReturn}><ArrowLeft /></RoundButton>
|
||||
<img className='h-12' src={data?.icon}></img>
|
||||
{data?.displayName}
|
||||
</div>
|
||||
<ul className='flex gap-2 justify-center'>{data?.keywords?.map((k, i) => <li key={i} className='bg-base-200 rounded-full p-2 px-4'>{k}</li>)}</ul>
|
||||
<div className='bg-base-200 p-4 rounded-2xl'>{data?.description}</div>
|
||||
</div>
|
||||
|
||||
<Settings />
|
||||
|
||||
</FocusContext>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</div>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AutoFocus } from '@/mainview/components/AutoFocus';
|
||||
import { pluginCategoryIcons, pluginCategoryPriorities } from '@/mainview/components/Constants';
|
||||
import { Button } from '@/mainview/components/options/Button';
|
||||
import { OptionInput } from '@/mainview/components/options/OptionInput';
|
||||
|
|
@ -62,7 +63,7 @@ function Plugin (data: {
|
|||
function RouteComponent ()
|
||||
{
|
||||
const { data: plugins, refetch: refetchPlugins } = useQuery(getAllPluginsQuery);
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'plugins' });
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'plugins' });
|
||||
const pluginMutation = useMutation({
|
||||
...enablePluginMutation, onSuccess (data, variables, onMutateResult, context)
|
||||
{
|
||||
|
|
@ -84,6 +85,7 @@ function RouteComponent ()
|
|||
</div>
|
||||
</div>;
|
||||
})}
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function RouteComponent ()
|
|||
const { focus } = Route.useSearch();
|
||||
const [search] = useSessionStorage<string | undefined>(`${Route.to}-search`, undefined);
|
||||
const navigator = useNavigate();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "main-area", preferredChildFocusKey: focus });
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "main-area", preferredChildFocusKey: focus ?? 'store-games' });
|
||||
const [filter, setFilter] = useSessionStorage<GameListFilterType>('store-games-filters', {});
|
||||
const { data, fetchNextPage, isFetchingNextPage, isFetching } = useInfiniteQuery(storeGamesInfiniteQuery(filter));
|
||||
const { data: gameFilters } = useQuery(gameFiltersQuery({ source: 'store' }));
|
||||
|
|
@ -80,7 +80,8 @@ function RouteComponent ()
|
|||
if (isFetchingNextPage || isFetching)
|
||||
return;
|
||||
fetchNextPage();
|
||||
}} />} games={data?.pages.flatMap((page) => page.data.map((g) =>
|
||||
}} />}
|
||||
games={data?.pages.flatMap((page) => page.data.map((g) =>
|
||||
{
|
||||
const badges: JSX.Element[] = [];
|
||||
if (g.id.source === 'local')
|
||||
|
|
@ -119,7 +120,8 @@ function RouteComponent ()
|
|||
onFocus: (k, n, d) => handleFocus(k, n, d)
|
||||
} satisfies GameMetaExtra as GameMetaExtra;
|
||||
})
|
||||
) ?? []} id={'store-games'} />
|
||||
) ?? []}
|
||||
id={'store-games'} />
|
||||
</div>
|
||||
<div className='fixed left-2 top-52 bottom-0 sm:w-10 md:w-14 z-10'>
|
||||
<SideFilters id='filter-btns' localFilter={filter} setLocalFilter={setFilter} filterValues={gameFilters} filters={{ source: 'store' }} />
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { GetFocusedElement } from '@/mainview/scripts/spatialNavigation';
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { autoEmulatorsQuery } from '@queries/settings';
|
||||
import { storeEmulatorsRecommendedQuery, storeFeaturedGamesQuery } from '@queries/store';
|
||||
import ImageWithFallbacks from '@/mainview/components/ImageWithFallbacks';
|
||||
|
||||
export const Route = createFileRoute('/store/tab/')({
|
||||
component: RouteComponent
|
||||
|
|
@ -64,16 +65,7 @@ function Main (data: { games?: FrontEndGameTypeDetailed[]; })
|
|||
{game ? <div key={selectedGame} className="flex transition-all duration-500 flex-col rounded-3xl overflow-hidden shadow-black/5 shadow-md w-full ring-6 ring-base-200 border-6 border-base-200">
|
||||
<div className='flex relative h-full overflow-hidden'>
|
||||
<div className='absolute w-full h-full z-0 bg-base-200'>
|
||||
<picture key={selectedGame}
|
||||
className='w-full h-full object-cover transition-all duration-500 ease-out scale-110 opacity-0 light:data-loaded:opacity-40 dark:data-loaded:opacity-80 z-0'
|
||||
onLoad={(e) =>
|
||||
{
|
||||
e.currentTarget.dataset.loaded = "true";
|
||||
e.currentTarget.classList.toggle('scale-110', false);
|
||||
}}
|
||||
>
|
||||
{previewUrls?.map((u, i) => <source key={i} src={u.href} />)}
|
||||
</picture>
|
||||
<ImageWithFallbacks src={previewUrls ?? []} className='w-full h-full transition-all duration-500 ease-out scale-110 opacity-0 light:data-loaded:opacity-40 dark:data-loaded:opacity-40 z-0 object-cover' />
|
||||
</div>
|
||||
<div key={selectedGame} className='flex sm:flex-wrap md:flex-nowrap grow z-1 p-8 opacity-0 animate-fade-in h-full items-end gap-4 sm:justify-end md:justify-between'>
|
||||
<div className='flex gap-4 max-h-full z-1 grow md:h-full'>
|
||||
|
|
@ -89,7 +81,7 @@ function Main (data: { games?: FrontEndGameTypeDetailed[]; })
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Button onAction={() => storeContext.showDetails('game', game.id.source, game.id.id, focusKey)} className='px-6 py-3 text-2xl! z-1 gap-2 focusable focusable-primary' id={'play-featured-btn'}> <Search /> Details</Button>
|
||||
<Button onAction={() => storeContext.showDetails('game', game.id.source, game.id.id, focusKey)} className='px-6 py-3 text-2xl! z-1 gap-2 drop-shadow-md focusable focusable-primary' id={'play-featured-btn'}> <Search /> Details</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div> : <div className='skeleton w-full rounded-3xl grow sm:h-64 z-15' />}
|
||||
|
|
|
|||
|
|
@ -56,4 +56,24 @@ export const hasUpdateQuery = queryOptions({
|
|||
return data;
|
||||
},
|
||||
staleTime: 1000 * 60 * 30
|
||||
});
|
||||
|
||||
export const checkUpdateMutation = mutationOptions({
|
||||
mutationKey: ['update', 'check'],
|
||||
mutationFn: async () =>
|
||||
{
|
||||
const { data, error } = await systemApi.api.system.update.check.post();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
export const updateMutation = mutationOptions({
|
||||
mutationKey: ['update'],
|
||||
mutationFn: async () =>
|
||||
{
|
||||
const { data, error } = await systemApi.api.system.update.post();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
|
@ -272,6 +272,7 @@ 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;
|
||||
onClosed?: () => void;
|
||||
},
|
||||
deps?: DependencyList
|
||||
)
|
||||
|
|
@ -289,6 +290,7 @@ export function useJobStatus<const JOB extends keyof JobsAPIType['~Routes']['api
|
|||
const sub = jobsApi.api.jobs[id].subscribe({ query: init?.query });
|
||||
ref.current = sub as any;
|
||||
|
||||
sub.on('close', () => init?.onClosed?.());
|
||||
sub.subscribe(({ data }) =>
|
||||
{
|
||||
switch (data.type)
|
||||
|
|
@ -326,7 +328,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, ...(deps ?? [])]);
|
||||
}, [id, init?.query]);
|
||||
|
||||
return { data, state, error, wsRef: ref };
|
||||
}
|
||||
|
|
|
|||
1
src/mainview/types.d.ts
vendored
1
src/mainview/types.d.ts
vendored
|
|
@ -1,5 +1,6 @@
|
|||
declare const __HOST__: string;
|
||||
declare const __PUBLIC__: boolean;
|
||||
declare const __FLATPAK__: boolean;
|
||||
declare const __EMULATORS__: Record<string, string>;
|
||||
declare module "@emulators" {
|
||||
const data: Record<string, string>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue