diff --git a/src/mainview/components/game/MainActions.tsx b/src/mainview/components/game/MainActions.tsx
index c70369a..96a120f 100644
--- a/src/mainview/components/game/MainActions.tsx
+++ b/src/mainview/components/game/MainActions.tsx
@@ -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 =
handlePlay(validDefaultCommand)} tooltip={validDefaultCommand?.label ?? details}
- key="primary"
- type='primary'
- id="mainAction"
- >
-
+ mainButton =
+
handlePlay(validDefaultCommand)} tooltip={validDefaultCommand?.label ?? details}
+ key="primary"
+ type='primary'
+ id="mainAction"
+ >
+
-
+
{validCommands.length > 1 &&
showAllCommands(true, 'allActionsBtn')}>
diff --git a/src/mainview/gen/static-icon-assets.gen.ts b/src/mainview/gen/static-icon-assets.gen.ts
index cb3fe1b..1d1a4aa 100644
--- a/src/mainview/gen/static-icon-assets.gen.ts
+++ b/src/mainview/gen/static-icon-assets.gen.ts
@@ -464,7 +464,7 @@ const assets = new Set([
]);
// Store basePath resolved from Vite config
-const BASE_PATH = "./";
+const BASE_PATH = "/";
/**
diff --git a/src/mainview/index.css b/src/mainview/index.css
index eb09eb3..532b330 100644
--- a/src/mainview/index.css
+++ b/src/mainview/index.css
@@ -7,7 +7,7 @@
@theme {
--breakpoint-sm: 0px;
- --breakpoint-md: 1280px;
+ --breakpoint-md: 1024px;
--page-scroll-bg: transparent;
--animation-size: 1;
diff --git a/src/mainview/routes/game/$source.$id.tsx b/src/mainview/routes/game/$source.$id.tsx
index 103d90f..d0a346e 100644
--- a/src/mainview/routes/game/$source.$id.tsx
+++ b/src/mainview/routes/game/$source.$id.tsx
@@ -166,7 +166,6 @@ function RouteComponent ()
return (
-
setUpdate(v => v + 1)
}} >
@@ -214,6 +213,7 @@ function RouteComponent ()
+
);
}
\ No newline at end of file
diff --git a/src/mainview/routes/index.tsx b/src/mainview/routes/index.tsx
index e2e6844..527fea9 100644
--- a/src/mainview/routes/index.tsx
+++ b/src/mainview/routes/index.tsx
@@ -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; })
;
}
-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
]} onAction={handleNavigate} title="Gameflow Store" subtitle="Get Free Games" 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
} focusKey='all-games-btn' index={0} id="all-games-btn" />;
+ useShortcuts(data.id, () => [{ label: data.actionLabel, button: GamePadButtonCode.A, action: handleNavigate }]);
+ return ] : undefined} onAction={handleNavigate} title={data.title} subtitle={data.subTitle} preview={
+ {typeof data.icon === 'string' ?
+
:
+
+ }
+ } 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={[, ]}
- emptyElement={[]}
+ finalElement={[
+ ,
+
+ ]}
+ emptyElement={[
+
+ ]}
/>
>;
diff --git a/src/mainview/routes/launcher.$source.$id.tsx b/src/mainview/routes/launcher.$source.$id.tsx
index 4254395..e66de07 100644
--- a/src/mainview/routes/launcher.$source.$id.tsx
+++ b/src/mainview/routes/launcher.$source.$id.tsx
@@ -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
diff --git a/src/mainview/routes/settings/about.tsx b/src/mainview/routes/settings/about.tsx
index d30b291..3351ff5 100644
--- a/src/mainview/routes/settings/about.tsx
+++ b/src/mainview/routes/settings/about.tsx
@@ -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
-
-
- | Agent |
- {navigator.userAgent} |
-
- {/* row 2 */}
-
- | Platform |
- {navigator.platform} |
-
-
- | Resolution |
- {screen.width}x{screen.height} |
-
-
- | Window |
- {window.innerWidth}x{window.innerHeight} |
-
- {/* row 3 */}
-
- | User |
- {systemInfo?.data?.user} |
-
-
- | Architecture |
- {systemInfo?.data?.arch} |
-
-
- | System |
- {systemInfo?.data?.platform} |
-
-
- | Hostname |
- {systemInfo?.data?.hostname} |
-
-
- | Machine |
- {systemInfo?.data?.machine} |
-
-
- | Sizes |
- Cache: {prettyBytes(systemInfo?.data?.cacheSize ?? 0)}, Store: {prettyBytes(systemInfo?.data?.storeSize ?? 0)} |
-
-
- | Source |
- {systemInfo?.data?.source} |
-
-
- | Steam Deck |
- {systemInfo?.data?.steamDeck ?? 'false'} |
-
-
+ 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
+
+
+
+
+ | Version |
+ {systemInfo?.data?.version} |
+
+
+ | Update |
+
+ {
+ hasUpdate && hasUpdate.hasUpdate > 0 ?
+ :
+
+ }
+ {}
+ |
+
+
+ | Agent |
+ {navigator.userAgent} |
+
+ {/* row 2 */}
+
+ | Platform |
+ {navigator.platform} |
+
+
+ | Resolution |
+ {screen.width}x{screen.height} |
+
+
+ | Window |
+ {window.innerWidth}x{window.innerHeight} |
+
+ {/* row 3 */}
+
+ | User |
+ {systemInfo?.data?.user} |
+
+
+ | Architecture |
+ {systemInfo?.data?.arch} |
+
+
+ | System |
+ {systemInfo?.data?.platform} |
+
+
+ | Hostname |
+ {systemInfo?.data?.hostname} |
+
+
+ | Machine |
+ {systemInfo?.data?.machine} |
+
+
+ | Sizes |
+ Cache: {prettyBytes(systemInfo?.data?.cacheSize ?? 0)}, Store: {prettyBytes(systemInfo?.data?.storeSize ?? 0)} |
+
+
+ | Source |
+ {systemInfo?.data?.source} |
+
+
+ | Steam Deck |
+ {systemInfo?.data?.steamDeck ?? 'false'} |
+
+
+
;
}
diff --git a/src/mainview/routes/settings/plugin.$source.tsx b/src/mainview/routes/settings/plugin.$source.tsx
index 78e0623..eedeada 100644
--- a/src/mainview/routes/settings/plugin.$source.tsx
+++ b/src/mainview/routes/settings/plugin.$source.tsx
@@ -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 <>
+
+
+ >;
+}
+
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
{!!definitions?.properties && Object.entries(Object.groupBy(Object.entries(definitions?.properties)
@@ -142,16 +165,19 @@ function RouteComponent ()
return
-
+
+

{data?.displayName}
{data?.keywords?.map((k, i) => - {k}
)}
{data?.description}
+
+
;
diff --git a/src/mainview/routes/settings/plugins.tsx b/src/mainview/routes/settings/plugins.tsx
index 2e12c78..37240e9 100644
--- a/src/mainview/routes/settings/plugins.tsx
+++ b/src/mainview/routes/settings/plugins.tsx
@@ -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 ()
;
})}
+
;
}
diff --git a/src/mainview/routes/store/tab/games.tsx b/src/mainview/routes/store/tab/games.tsx
index febe997..edf0864 100644
--- a/src/mainview/routes/store/tab/games.tsx
+++ b/src/mainview/routes/store/tab/games.tsx
@@ -30,7 +30,7 @@ function RouteComponent ()
const { focus } = Route.useSearch();
const [search] = useSessionStorage(`${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('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'} />
diff --git a/src/mainview/routes/store/tab/index.tsx b/src/mainview/routes/store/tab/index.tsx
index 2992f88..178eea0 100644
--- a/src/mainview/routes/store/tab/index.tsx
+++ b/src/mainview/routes/store/tab/index.tsx
@@ -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 ?
-
- {
- e.currentTarget.dataset.loaded = "true";
- e.currentTarget.classList.toggle('scale-110', false);
- }}
- >
- {previewUrls?.map((u, i) => )}
-
+
@@ -89,7 +81,7 @@ function Main (data: { games?: FrontEndGameTypeDetailed[]; })
-
+
: }
diff --git a/src/mainview/scripts/queries/system.ts b/src/mainview/scripts/queries/system.ts
index b46e4ea..ffc503f 100644
--- a/src/mainview/scripts/queries/system.ts
+++ b/src/mainview/scripts/queries/system.ts
@@ -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;
+ },
});
\ No newline at end of file
diff --git a/src/mainview/scripts/utils.ts b/src/mainview/scripts/utils.ts
index 9164617..37b2da1 100644
--- a/src/mainview/scripts/utils.ts
+++ b/src/mainview/scripts/utils.ts
@@ -272,6 +272,7 @@ export function useJobStatus, "completed" | "ended", 'data'>) => void;
onCompleted?: (data: ExtractField, "completed" | "ended", 'data'>) => void;
onError?: (error: string) => void;
+ onClosed?: () => void;
},
deps?: DependencyList
)
@@ -289,6 +290,7 @@ export function useJobStatus init?.onClosed?.());
sub.subscribe(({ data }) =>
{
switch (data.type)
@@ -326,7 +328,7 @@ export function useJobStatus;
declare module "@emulators" {
const data: Record;
diff --git a/src/shared/types..d.ts b/src/shared/types..d.ts
index 2a03104..b715875 100644
--- a/src/shared/types..d.ts
+++ b/src/shared/types..d.ts
@@ -341,6 +341,7 @@ declare interface SaveFileChange
isGlob?: true;
cwd: string;
shared: boolean;
+ fixedSize?: boolean;
}
declare type SaveSlots = Record;
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index 7029442..b396eae 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -102,7 +102,8 @@ export default defineConfig(({ command }) =>
},
define: {
__HOST__: JSON.stringify(host),
- __PUBLIC__: process.env.PUBLIC_ACCESS ? true : false
+ __PUBLIC__: process.env.PUBLIC_ACCESS ? true : false,
+ __FLATPAK__: process.env.FLATPAK_BUILD ? true : false
}
};
});
\ No newline at end of file