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')}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue