SettingsRouteRoute,
} as any)
-const SettingsTasksRoute = SettingsTasksRouteImport.update({
- id: '/tasks',
- path: '/tasks',
- getParentRoute: () => SettingsRouteRoute,
-} as any)
const SettingsPluginsRoute = SettingsPluginsRouteImport.update({
id: '/plugins',
path: '/plugins',
@@ -93,11 +81,6 @@ const SettingsAboutRoute = SettingsAboutRouteImport.update({
path: '/about',
getParentRoute: () => SettingsRouteRoute,
} as any)
-const GameAddRoute = GameAddRouteImport.update({
- id: '/game/add',
- path: '/game/add',
- getParentRoute: () => rootRouteImport,
-} as any)
const StoreTabRouteRoute = StoreTabRouteRouteImport.update({
id: '/store/tab',
path: '/store/tab',
@@ -108,11 +91,6 @@ const StoreTabIndexRoute = StoreTabIndexRouteImport.update({
path: '/',
getParentRoute: () => StoreTabRouteRoute,
} as any)
-const StoreTabPluginsRoute = StoreTabPluginsRouteImport.update({
- id: '/plugins',
- path: '/plugins',
- getParentRoute: () => StoreTabRouteRoute,
-} as any)
const StoreTabGamesRoute = StoreTabGamesRouteImport.update({
id: '/games',
path: '/games',
@@ -123,11 +101,6 @@ const StoreTabEmulatorsRoute = StoreTabEmulatorsRouteImport.update({
path: '/emulators',
getParentRoute: () => StoreTabRouteRoute,
} as any)
-const StoreTabDownloadRoute = StoreTabDownloadRouteImport.update({
- id: '/download',
- path: '/download',
- getParentRoute: () => StoreTabRouteRoute,
-} as any)
const SettingsPluginSourceRoute = SettingsPluginSourceRouteImport.update({
id: '/plugin/$source',
path: '/plugin/$source',
@@ -158,41 +131,23 @@ const CollectionSourceIdRoute = CollectionSourceIdRouteImport.update({
path: '/collection/$source/$id',
getParentRoute: () => rootRouteImport,
} as any)
-const StoreDetailsPluginIdRoute = StoreDetailsPluginIdRouteImport.update({
- id: '/store/details/plugin/$id',
- path: '/store/details/plugin/$id',
- getParentRoute: () => rootRouteImport,
-} as any)
const StoreDetailsEmulatorIdRoute = StoreDetailsEmulatorIdRouteImport.update({
id: '/store/details/emulator/$id',
path: '/store/details/emulator/$id',
getParentRoute: () => rootRouteImport,
} as any)
-const GameUpdateSourceIdRoute = GameUpdateSourceIdRouteImport.update({
- id: '/game/update/$source/$id',
- path: '/game/update/$source/$id',
- getParentRoute: () => rootRouteImport,
-} as any)
-const StoreDetailsDownloadSourceIdRoute =
- StoreDetailsDownloadSourceIdRouteImport.update({
- id: '/store/details/download/$source/$id',
- path: '/store/details/download/$source/$id',
- getParentRoute: () => rootRouteImport,
- } as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/settings': typeof SettingsRouteRouteWithChildren
'/games': typeof GamesRoute
'/store/tab': typeof StoreTabRouteRouteWithChildren
- '/game/add': typeof GameAddRoute
'/settings/about': typeof SettingsAboutRoute
'/settings/accounts': typeof SettingsAccountsRoute
'/settings/directories': typeof SettingsDirectoriesRoute
'/settings/emulators': typeof SettingsEmulatorsRoute
'/settings/interface': typeof SettingsInterfaceRoute
'/settings/plugins': typeof SettingsPluginsRoute
- '/settings/tasks': typeof SettingsTasksRoute
'/settings/update': typeof SettingsUpdateRoute
'/collection/$source/$id': typeof CollectionSourceIdRoute
'/embedded/$source/$id': typeof EmbeddedSourceIdRoute
@@ -200,28 +155,21 @@ export interface FileRoutesByFullPath {
'/launcher/$source/$id': typeof LauncherSourceIdRoute
'/platform/$source/$id': typeof PlatformSourceIdRoute
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
- '/store/tab/download': typeof StoreTabDownloadRoute
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
'/store/tab/games': typeof StoreTabGamesRoute
- '/store/tab/plugins': typeof StoreTabPluginsRoute
'/store/tab/': typeof StoreTabIndexRoute
- '/game/update/$source/$id': typeof GameUpdateSourceIdRoute
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
- '/store/details/plugin/$id': typeof StoreDetailsPluginIdRoute
- '/store/details/download/$source/$id': typeof StoreDetailsDownloadSourceIdRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/settings': typeof SettingsRouteRouteWithChildren
'/games': typeof GamesRoute
- '/game/add': typeof GameAddRoute
'/settings/about': typeof SettingsAboutRoute
'/settings/accounts': typeof SettingsAccountsRoute
'/settings/directories': typeof SettingsDirectoriesRoute
'/settings/emulators': typeof SettingsEmulatorsRoute
'/settings/interface': typeof SettingsInterfaceRoute
'/settings/plugins': typeof SettingsPluginsRoute
- '/settings/tasks': typeof SettingsTasksRoute
'/settings/update': typeof SettingsUpdateRoute
'/collection/$source/$id': typeof CollectionSourceIdRoute
'/embedded/$source/$id': typeof EmbeddedSourceIdRoute
@@ -229,15 +177,10 @@ export interface FileRoutesByTo {
'/launcher/$source/$id': typeof LauncherSourceIdRoute
'/platform/$source/$id': typeof PlatformSourceIdRoute
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
- '/store/tab/download': typeof StoreTabDownloadRoute
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
'/store/tab/games': typeof StoreTabGamesRoute
- '/store/tab/plugins': typeof StoreTabPluginsRoute
'/store/tab': typeof StoreTabIndexRoute
- '/game/update/$source/$id': typeof GameUpdateSourceIdRoute
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
- '/store/details/plugin/$id': typeof StoreDetailsPluginIdRoute
- '/store/details/download/$source/$id': typeof StoreDetailsDownloadSourceIdRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -245,14 +188,12 @@ export interface FileRoutesById {
'/settings': typeof SettingsRouteRouteWithChildren
'/games': typeof GamesRoute
'/store/tab': typeof StoreTabRouteRouteWithChildren
- '/game/add': typeof GameAddRoute
'/settings/about': typeof SettingsAboutRoute
'/settings/accounts': typeof SettingsAccountsRoute
'/settings/directories': typeof SettingsDirectoriesRoute
'/settings/emulators': typeof SettingsEmulatorsRoute
'/settings/interface': typeof SettingsInterfaceRoute
'/settings/plugins': typeof SettingsPluginsRoute
- '/settings/tasks': typeof SettingsTasksRoute
'/settings/update': typeof SettingsUpdateRoute
'/collection/$source/$id': typeof CollectionSourceIdRoute
'/embedded/$source/$id': typeof EmbeddedSourceIdRoute
@@ -260,15 +201,10 @@ export interface FileRoutesById {
'/launcher/$source/$id': typeof LauncherSourceIdRoute
'/platform/$source/$id': typeof PlatformSourceIdRoute
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
- '/store/tab/download': typeof StoreTabDownloadRoute
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
'/store/tab/games': typeof StoreTabGamesRoute
- '/store/tab/plugins': typeof StoreTabPluginsRoute
'/store/tab/': typeof StoreTabIndexRoute
- '/game/update/$source/$id': typeof GameUpdateSourceIdRoute
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
- '/store/details/plugin/$id': typeof StoreDetailsPluginIdRoute
- '/store/details/download/$source/$id': typeof StoreDetailsDownloadSourceIdRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -277,14 +213,12 @@ export interface FileRouteTypes {
| '/settings'
| '/games'
| '/store/tab'
- | '/game/add'
| '/settings/about'
| '/settings/accounts'
| '/settings/directories'
| '/settings/emulators'
| '/settings/interface'
| '/settings/plugins'
- | '/settings/tasks'
| '/settings/update'
| '/collection/$source/$id'
| '/embedded/$source/$id'
@@ -292,28 +226,21 @@ export interface FileRouteTypes {
| '/launcher/$source/$id'
| '/platform/$source/$id'
| '/settings/plugin/$source'
- | '/store/tab/download'
| '/store/tab/emulators'
| '/store/tab/games'
- | '/store/tab/plugins'
| '/store/tab/'
- | '/game/update/$source/$id'
| '/store/details/emulator/$id'
- | '/store/details/plugin/$id'
- | '/store/details/download/$source/$id'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/settings'
| '/games'
- | '/game/add'
| '/settings/about'
| '/settings/accounts'
| '/settings/directories'
| '/settings/emulators'
| '/settings/interface'
| '/settings/plugins'
- | '/settings/tasks'
| '/settings/update'
| '/collection/$source/$id'
| '/embedded/$source/$id'
@@ -321,29 +248,22 @@ export interface FileRouteTypes {
| '/launcher/$source/$id'
| '/platform/$source/$id'
| '/settings/plugin/$source'
- | '/store/tab/download'
| '/store/tab/emulators'
| '/store/tab/games'
- | '/store/tab/plugins'
| '/store/tab'
- | '/game/update/$source/$id'
| '/store/details/emulator/$id'
- | '/store/details/plugin/$id'
- | '/store/details/download/$source/$id'
id:
| '__root__'
| '/'
| '/settings'
| '/games'
| '/store/tab'
- | '/game/add'
| '/settings/about'
| '/settings/accounts'
| '/settings/directories'
| '/settings/emulators'
| '/settings/interface'
| '/settings/plugins'
- | '/settings/tasks'
| '/settings/update'
| '/collection/$source/$id'
| '/embedded/$source/$id'
@@ -351,15 +271,10 @@ export interface FileRouteTypes {
| '/launcher/$source/$id'
| '/platform/$source/$id'
| '/settings/plugin/$source'
- | '/store/tab/download'
| '/store/tab/emulators'
| '/store/tab/games'
- | '/store/tab/plugins'
| '/store/tab/'
- | '/game/update/$source/$id'
| '/store/details/emulator/$id'
- | '/store/details/plugin/$id'
- | '/store/details/download/$source/$id'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -367,16 +282,12 @@ export interface RootRouteChildren {
SettingsRouteRoute: typeof SettingsRouteRouteWithChildren
GamesRoute: typeof GamesRoute
StoreTabRouteRoute: typeof StoreTabRouteRouteWithChildren
- GameAddRoute: typeof GameAddRoute
CollectionSourceIdRoute: typeof CollectionSourceIdRoute
EmbeddedSourceIdRoute: typeof EmbeddedSourceIdRoute
GameSourceIdRoute: typeof GameSourceIdRoute
LauncherSourceIdRoute: typeof LauncherSourceIdRoute
PlatformSourceIdRoute: typeof PlatformSourceIdRoute
- GameUpdateSourceIdRoute: typeof GameUpdateSourceIdRoute
StoreDetailsEmulatorIdRoute: typeof StoreDetailsEmulatorIdRoute
- StoreDetailsPluginIdRoute: typeof StoreDetailsPluginIdRoute
- StoreDetailsDownloadSourceIdRoute: typeof StoreDetailsDownloadSourceIdRoute
}
declare module '@tanstack/react-router' {
@@ -409,13 +320,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsUpdateRouteImport
parentRoute: typeof SettingsRouteRoute
}
- '/settings/tasks': {
- id: '/settings/tasks'
- path: '/tasks'
- fullPath: '/settings/tasks'
- preLoaderRoute: typeof SettingsTasksRouteImport
- parentRoute: typeof SettingsRouteRoute
- }
'/settings/plugins': {
id: '/settings/plugins'
path: '/plugins'
@@ -458,13 +362,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsAboutRouteImport
parentRoute: typeof SettingsRouteRoute
}
- '/game/add': {
- id: '/game/add'
- path: '/game/add'
- fullPath: '/game/add'
- preLoaderRoute: typeof GameAddRouteImport
- parentRoute: typeof rootRouteImport
- }
'/store/tab': {
id: '/store/tab'
path: '/store/tab'
@@ -479,13 +376,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof StoreTabIndexRouteImport
parentRoute: typeof StoreTabRouteRoute
}
- '/store/tab/plugins': {
- id: '/store/tab/plugins'
- path: '/plugins'
- fullPath: '/store/tab/plugins'
- preLoaderRoute: typeof StoreTabPluginsRouteImport
- parentRoute: typeof StoreTabRouteRoute
- }
'/store/tab/games': {
id: '/store/tab/games'
path: '/games'
@@ -500,13 +390,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof StoreTabEmulatorsRouteImport
parentRoute: typeof StoreTabRouteRoute
}
- '/store/tab/download': {
- id: '/store/tab/download'
- path: '/download'
- fullPath: '/store/tab/download'
- preLoaderRoute: typeof StoreTabDownloadRouteImport
- parentRoute: typeof StoreTabRouteRoute
- }
'/settings/plugin/$source': {
id: '/settings/plugin/$source'
path: '/plugin/$source'
@@ -549,13 +432,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof CollectionSourceIdRouteImport
parentRoute: typeof rootRouteImport
}
- '/store/details/plugin/$id': {
- id: '/store/details/plugin/$id'
- path: '/store/details/plugin/$id'
- fullPath: '/store/details/plugin/$id'
- preLoaderRoute: typeof StoreDetailsPluginIdRouteImport
- parentRoute: typeof rootRouteImport
- }
'/store/details/emulator/$id': {
id: '/store/details/emulator/$id'
path: '/store/details/emulator/$id'
@@ -563,20 +439,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof StoreDetailsEmulatorIdRouteImport
parentRoute: typeof rootRouteImport
}
- '/game/update/$source/$id': {
- id: '/game/update/$source/$id'
- path: '/game/update/$source/$id'
- fullPath: '/game/update/$source/$id'
- preLoaderRoute: typeof GameUpdateSourceIdRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/store/details/download/$source/$id': {
- id: '/store/details/download/$source/$id'
- path: '/store/details/download/$source/$id'
- fullPath: '/store/details/download/$source/$id'
- preLoaderRoute: typeof StoreDetailsDownloadSourceIdRouteImport
- parentRoute: typeof rootRouteImport
- }
}
}
@@ -587,7 +449,6 @@ interface SettingsRouteRouteChildren {
SettingsEmulatorsRoute: typeof SettingsEmulatorsRoute
SettingsInterfaceRoute: typeof SettingsInterfaceRoute
SettingsPluginsRoute: typeof SettingsPluginsRoute
- SettingsTasksRoute: typeof SettingsTasksRoute
SettingsUpdateRoute: typeof SettingsUpdateRoute
SettingsPluginSourceRoute: typeof SettingsPluginSourceRoute
}
@@ -599,7 +460,6 @@ const SettingsRouteRouteChildren: SettingsRouteRouteChildren = {
SettingsEmulatorsRoute: SettingsEmulatorsRoute,
SettingsInterfaceRoute: SettingsInterfaceRoute,
SettingsPluginsRoute: SettingsPluginsRoute,
- SettingsTasksRoute: SettingsTasksRoute,
SettingsUpdateRoute: SettingsUpdateRoute,
SettingsPluginSourceRoute: SettingsPluginSourceRoute,
}
@@ -609,18 +469,14 @@ const SettingsRouteRouteWithChildren = SettingsRouteRoute._addFileChildren(
)
interface StoreTabRouteRouteChildren {
- StoreTabDownloadRoute: typeof StoreTabDownloadRoute
StoreTabEmulatorsRoute: typeof StoreTabEmulatorsRoute
StoreTabGamesRoute: typeof StoreTabGamesRoute
- StoreTabPluginsRoute: typeof StoreTabPluginsRoute
StoreTabIndexRoute: typeof StoreTabIndexRoute
}
const StoreTabRouteRouteChildren: StoreTabRouteRouteChildren = {
- StoreTabDownloadRoute: StoreTabDownloadRoute,
StoreTabEmulatorsRoute: StoreTabEmulatorsRoute,
StoreTabGamesRoute: StoreTabGamesRoute,
- StoreTabPluginsRoute: StoreTabPluginsRoute,
StoreTabIndexRoute: StoreTabIndexRoute,
}
@@ -633,16 +489,12 @@ const rootRouteChildren: RootRouteChildren = {
SettingsRouteRoute: SettingsRouteRouteWithChildren,
GamesRoute: GamesRoute,
StoreTabRouteRoute: StoreTabRouteRouteWithChildren,
- GameAddRoute: GameAddRoute,
CollectionSourceIdRoute: CollectionSourceIdRoute,
EmbeddedSourceIdRoute: EmbeddedSourceIdRoute,
GameSourceIdRoute: GameSourceIdRoute,
LauncherSourceIdRoute: LauncherSourceIdRoute,
PlatformSourceIdRoute: PlatformSourceIdRoute,
- GameUpdateSourceIdRoute: GameUpdateSourceIdRoute,
StoreDetailsEmulatorIdRoute: StoreDetailsEmulatorIdRoute,
- StoreDetailsPluginIdRoute: StoreDetailsPluginIdRoute,
- StoreDetailsDownloadSourceIdRoute: StoreDetailsDownloadSourceIdRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
diff --git a/src/mainview/index.css b/src/mainview/index.css
index 332862e..4c82b71 100644
--- a/src/mainview/index.css
+++ b/src/mainview/index.css
@@ -9,7 +9,6 @@
@theme {
--breakpoint-sm: 0px;
--breakpoint-md: 1024px;
- --breakpoint-lg: 1280px;
--page-scroll-bg: transparent;
--animation-size: 1;
diff --git a/src/mainview/index.tsx b/src/mainview/index.tsx
index 166cc4f..f5639f9 100644
--- a/src/mainview/index.tsx
+++ b/src/mainview/index.tsx
@@ -8,7 +8,7 @@ import
RouterProvider,
} from "@tanstack/react-router";
import { routeTree } from "./gen/routeTree.gen";
-import { QueryClient } from "@tanstack/react-query";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./scripts/gamepads";
import "./scripts/windowEvents";
import "./scripts/spatialNavigation";
@@ -16,16 +16,6 @@ import NotFound from "./components/NotFound";
import Error from "./components/Error";
import serviceWorker from './scripts/serviceWorker?worker&url';
import App from "./App";
-import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
-import { createStore, get, set, del } from "idb-keyval";
-import
-{
- PersistedClient,
- Persister,
-} from '@tanstack/react-query-persist-client';
-import pkg from '../../package.json';
-
-const idbStore = createStore("tanstack-query", "cache");
if ('serviceWorker' in navigator)
{
@@ -34,31 +24,7 @@ if ('serviceWorker' in navigator)
const hashHistory = createHashHistory({});
-const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- gcTime: 1000 * 60 * 60 * 24 * 5, // 5 days
- }
- }
-});
-
-export function createIDBPersister (idbValidKey: IDBValidKey = 'reactQuery'): Persister
-{
- return {
- persistClient: async (client: PersistedClient) =>
- {
- await set(idbValidKey, client, idbStore);
- },
- restoreClient: async () =>
- {
- return await get
(idbValidKey, idbStore);
- },
- removeClient: async () =>
- {
- await del(idbValidKey, idbStore);
- },
- } satisfies Persister;
-}
+const queryClient = new QueryClient();
export interface RouterContext
{
@@ -108,9 +74,9 @@ if (!rootElement.innerHTML)
root.render(
-
+
-
+
,
);
diff --git a/src/mainview/query-options.ts b/src/mainview/query-options.ts
index 879d632..a52c649 100644
--- a/src/mainview/query-options.ts
+++ b/src/mainview/query-options.ts
@@ -1,7 +1,6 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { getRomApiRomsIdGetOptions, getRomsApiRomsGetOptions } from "../clients/romm/@tanstack/react-query.gen";
-import { DefaultRommStaleTime } from "../shared/constants";
-import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
+import { DefaultRommStaleTime, GameListFilterType } from "../shared/constants";
export function gamesQueryOptions (filter?: GameListFilterType)
{
diff --git a/src/mainview/routes/__root.tsx b/src/mainview/routes/__root.tsx
index cafbab4..fbe2f26 100644
--- a/src/mainview/routes/__root.tsx
+++ b/src/mainview/routes/__root.tsx
@@ -8,7 +8,6 @@ import { useEffect } from "react";
import AppCommunication from "../components/AppCommunication";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
-import GlobalContextDialog from "../components/GlobalContextDialog";
export const Route = createRootRouteWithContext()({
component: RootComponent,
@@ -40,11 +39,9 @@ function RootComponent ()
return (
-
-
-
-
-
+
+
+
{queryDevOptions &&
}
diff --git a/src/mainview/routes/collection.$source.$id.tsx b/src/mainview/routes/collection.$source.$id.tsx
index a08c164..3b73d25 100644
--- a/src/mainview/routes/collection.$source.$id.tsx
+++ b/src/mainview/routes/collection.$source.$id.tsx
@@ -6,7 +6,7 @@ import { AnimatedBackgroundContext } from '../scripts/contexts';
import { getCollectionQuery } from '@queries/romm';
import { zodValidator } from '@tanstack/zod-adapter';
import z from 'zod';
-import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
+import { GameListFilterType } from '@/shared/constants';
import { useLocalStorage } from 'usehooks-ts';
export const Route = createFileRoute('/collection/$source/$id')({
diff --git a/src/mainview/routes/embedded.$source.$id.tsx b/src/mainview/routes/embedded.$source.$id.tsx
index b9a39b6..3222280 100644
--- a/src/mainview/routes/embedded.$source.$id.tsx
+++ b/src/mainview/routes/embedded.$source.$id.tsx
@@ -5,7 +5,7 @@ import z from 'zod';
import { RefObject, useEffect, useRef, useState } from 'react';
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
import { ButtonStyle } from '../components/options/Button';
-import { CloudDownload, DoorOpen, RefreshCw, Undo } from 'lucide-react';
+import { CloudDownload, DoorOpen, RefreshCw, Save, Undo } from 'lucide-react';
import { GamePadButtonCode, useShortcuts } from '../scripts/shortcuts';
import { FloatingShortcuts } from '../components/Shortcuts';
import { useEventListener } from 'usehooks-ts';
diff --git a/src/mainview/routes/game/$source.$id.tsx b/src/mainview/routes/game/$source.$id.tsx
index 13ea8ac..d0a346e 100644
--- a/src/mainview/routes/game/$source.$id.tsx
+++ b/src/mainview/routes/game/$source.$id.tsx
@@ -6,8 +6,8 @@ import { Calendar, Folder, Gamepad2, Image, Info, TriangleAlert, Trophy } from "
import { HeaderUI, StickyHeaderUI } from "../../components/Header";
import { AnimatedBackground } from "../../components/AnimatedBackground";
import { useQuery } from "@tanstack/react-query";
-import { FloatingShortcuts } from "../../components/Shortcuts";
-import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
+import Shortcuts, { FloatingShortcuts } from "../../components/Shortcuts";
+import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
import Screenshots from "@/mainview/components/Screenshots";
import { HandleGoBack, scrollIntoViewHandler, useOnNavigateBack } from "@/mainview/scripts/utils";
import { FilterUI } from "@/mainview/components/Filters";
@@ -23,8 +23,7 @@ import { GamesSection } from "@/mainview/components/store/GamesSection";
import Details from "@/mainview/components/game/Details";
import { AutoFocus } from "@/mainview/components/AutoFocus";
import SelectMenu from "@/mainview/components/SelectMenu";
-import { IGDBIcon } from "@/mainview/scripts/brandIcons";
-import { FrontEndGameTypeDetailed } from "@simeonradivoev/gameflow-sdk/shared";
+import { en } from "zod/v4/locales";
export const Route = createFileRoute("/game/$source/$id")({
loader: async ({ params, context }) =>
@@ -33,9 +32,7 @@ export const Route = createFileRoute("/game/$source/$id")({
},
component: RouteComponent,
errorComponent: Error,
- validateSearch: zodValidator(z.object({
- focus: z.string().optional(),
- })),
+ validateSearch: zodValidator(z.object({ focus: z.string().optional() })),
staticData: {
enterSound: 'openDetails',
goBackSound: "returnDetails"
@@ -108,8 +105,6 @@ function Stats (data: { game: FrontEndGameTypeDetailed | undefined; })
stats.push({ label: "Release Date", content: data.game.metadata.first_release_date.toLocaleDateString(), icon:
});
if (data.game.emulators)
stats.push({ label: "Emulators", content: data.game.emulators.map(e => e.name) });
- if (data.game.igdb_id)
- stats.push({ label: "IGDB", icon: IGDBIcon, content: String(data.game.igdb_id) });
if (data.game.source)
stats.push({ label: "Source", content: `${data.game.source} - ${data.game.source_id}` });
const integrations = new Set
(data.game.emulators?.flatMap(e => e.integrations).flatMap(i => i.capabilities).filter(c => !!c));
diff --git a/src/mainview/routes/game/add.tsx b/src/mainview/routes/game/add.tsx
deleted file mode 100644
index 6399cd0..0000000
--- a/src/mainview/routes/game/add.tsx
+++ /dev/null
@@ -1,425 +0,0 @@
-import { AutoFocus } from '@/mainview/components/AutoFocus';
-import { OptionElement } from '@/mainview/components/ContextDialog';
-import GameLookupElement from '@/mainview/components/game/GameLookup';
-import { StickyHeaderUI } from '@/mainview/components/Header';
-import LoadingScreen from '@/mainview/components/LoadingScreen';
-import { Button } from '@/mainview/components/options/Button';
-import { PathSettingsOptionBase } from '@/mainview/components/options/PathSettingsOption';
-import SelectMenu from '@/mainview/components/SelectMenu';
-import { FloatingShortcuts } from '@/mainview/components/Shortcuts';
-import { oneShot } from '@/mainview/scripts/audio/audio';
-import { rommApi } from '@/mainview/scripts/clientApi';
-import { addManualGameMutation, allGamesInvalidateQuery, gameLookupDetails, platformLookupMatchQuery } from '@/mainview/scripts/queries/romm';
-import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
-import { HandleGoBack } from '@/mainview/scripts/utils';
-import { isUrl } from '@/shared/utils';
-import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { createFileRoute, useNavigate, useRouter } from '@tanstack/react-router';
-import { zodValidator } from '@tanstack/zod-adapter';
-import { ArrowBigRightDash, Check, CirclePlus, CircleQuestionMark, CircleX, File, FileSearch, FolderOpen, Globe, HardDrive, Link, Save } from 'lucide-react';
-import { basename } from 'pathe';
-import prettyBytes from 'pretty-bytes';
-import { JSX, useState } from 'react';
-import toast from 'react-hot-toast';
-import { twMerge } from 'tailwind-merge';
-import z from 'zod';
-
-
-const StateSchema = z.object({
- step: z.number().default(0),
- gameLocation: z.string().optional(),
- selectedGame: z.object({ source: z.string(), id: z.string() }).optional(),
- platformId: z.number().optional(),
- search: z.string().optional()
-});
-
-export const Route = createFileRoute('/game/add')({
- component: RouteComponent,
- validateSearch: zodValidator(StateSchema)
-});
-
-function FileSelectionField (data: { location: string | undefined, setLocation: (location: string | undefined) => void; })
-{
- const [localLocation, setLocalLocation] = useState(data.location);
- const navigate = useNavigate();
- return
-
- ;
-}
-
-const TAG_REGEX = /\(([^)]+)\)|\[([^\]]+)\]/g;
-const EXTENSION_REGEX = /\.(([a-z]+\.)*\w+)$/g;
-const LEADING_ARTICLE_PATTERN = /^(a|an|the)\b/g;
-const COMMA_ARTICLE_PATTERN = /,\s(a|an|the)\b(?=\s*[^\w\s]|$)/g;
-const NON_WORD_SPACE_PATTERN = /[^\w\s]/g;
-const MULTIPLE_SPACE_PATTERN = /\s+/g;
-
-function BuildSearch (filePath: string)
-{
- const name = basename(filePath);
- const nameWithoutExt = name.replace(EXTENSION_REGEX, "").trim();
- if (!nameWithoutExt) return undefined;
- const nameWithoutTags = nameWithoutExt.replaceAll(TAG_REGEX, "").trim();
- if (TAG_REGEX.test(nameWithoutExt)) console.log("match");
- if (!nameWithoutTags) return undefined;
-
- // Lower and replace underscores with spaces
- let finalSearch = nameWithoutTags.toLowerCase().replace("_", " ");
-
- // Remove articles (combined if possible)
- finalSearch = finalSearch.replaceAll(LEADING_ARTICLE_PATTERN, '');
- finalSearch = finalSearch.replaceAll(COMMA_ARTICLE_PATTERN, '');
-
- // Remove punctuation and normalize spaces in one step
- finalSearch = finalSearch.replaceAll(NON_WORD_SPACE_PATTERN, '');
- finalSearch = finalSearch.replaceAll(MULTIPLE_SPACE_PATTERN, '');
-
- return nameWithoutTags;
-}
-
-const typeIconMap: Record = {
- new: ,
- existing: ,
- unknown:
-};
-
-function Overview (data: {})
-{
- const navigate = useNavigate();
- const router = useRouter();
- const state = Route.useSearch();
- const linkInfo = useQuery({
- enabled (query)
- {
- return isUrl(query.queryKey[1]);
- },
- queryKey: ['dl-link-info', state.gameLocation],
- queryFn: async () =>
- {
- return rommApi.api.romm.download.file.info.get({ query: { file_url: state.gameLocation! } });
- }
- });
- const { data: game } = useQuery(gameLookupDetails(state.selectedGame?.source, state.selectedGame?.id));
- const { data: platform } = useQuery(platformLookupMatchQuery(state.selectedGame?.source, state.platformId));
- const addGame = useMutation({
- ...addManualGameMutation,
- onError (error, variables, onMutateResult, context)
- {
- toast.error(error.message);
- },
- async onSuccess (data, variables, onMutateResult, context)
- {
- if (data.id === null || isUrl(state.gameLocation)) return;
- await context.client.invalidateQueries(allGamesInvalidateQuery);
- navigate({
- to: '/game/$source/$id', params: {
- source: data.source, id: String(data.id)
- }, replace: true
- });
- },
- });
-
- if (!game) return Select A Game
;
-
- return
-
Preview
-
-
{!!game[0].coverUrl &&

}
-
-
{game[0].name}
-
{game[0].summary}
-
-
{platform?.details.name}
-
-
- {!!platform?.match.coverUrl &&

}
-
{platform?.match.name}
-
{platform?.match.family_name}
-
- {!!platform?.match.type && typeIconMap[platform?.match.type]}
-
{platform?.match.type}
-
-
-
{isUrl(state.gameLocation) ? : }{state.gameLocation}
-
-
- {linkInfo.isFetching ? : (linkInfo.data?.data?.size && prettyBytes(linkInfo.data.data.size))}
-
- {linkInfo.isFetching ? : (linkInfo.data?.data?.content_type && linkInfo.data.data.content_type)}
-
-
-
-
Actions
-
-
-
-
-
;
-}
-
-function PlatformEntry (data: {
- id: string,
- displayName: string,
- platformSource: string,
- platformId: number;
-})
-{
- const state = Route.useSearch();
- const { data: match, isFetching: matchIsFetching } = useQuery({ ...platformLookupMatchQuery(data.platformSource, data.platformId), staleTime: 1000 * 60 * 60 });
- const navigate = useNavigate();
- const handleAction = () =>
- {
- navigate({ to: '/game/add', search: { ...state, platformId: data.platformId, step: 3 }, replace: true });
- oneShot('openGeneric');
- };
-
- return
- {data.displayName}
-
- {matchIsFetching ? : match && <>
-
- {match.match.coverUrl ?
: }
- {match.match.name} - {!!match.match.type && typeIconMap[match.match.type]} {match.match.type}
- >}
-
- } type={'primary'} />;
-}
-
-function PlatformSelection (data: {})
-{
- const state = Route.useSearch();
- const { data: game, isFetching } = useQuery({ ...gameLookupDetails(state.selectedGame?.source, state.selectedGame?.id), staleTime: 1000 * 60 * 60 });
- if (isFetching) return ;
- if (!game) return Select A Game
;
- return
- {game[0].platforms.map((p, i) => )}
-
;
-}
-
-function Lookup ()
-{
- const state = Route.useSearch();
- const [search, setSearch] = useState(state.search);
- const navigate = useNavigate();
- const handleSetSelectedGame = (source: string, id: string) =>
- {
- navigate({ to: '/game/add', search: { ...state, selectedGame: { source, id }, platformId: undefined, search, step: 2 }, replace: true });
- oneShot('openGeneric');
- };
- return
- {
- handleSetSelectedGame(l.source, l.id);
- }} />;
-}
-
-const StepDetails = [{ label: "Select Location" }, { label: "Find Match" }, { label: "Select Platform" }, { label: "Confirm" }];
-
-function Location ()
-{
- const state = Route.useSearch();
- const navigate = useNavigate();
- const handleSetLocation = (location: string | undefined) =>
- {
- if (!location) return;
- navigate({
- to: '/game/add', search: {
- ...state,
- gameLocation: location,
- search: BuildSearch(location),
- selectedGame: undefined,
- platformId: undefined,
- step: 1
- }, replace: true
- });
- oneShot('openGeneric');
- };
- return
-
Select Game Rom
-
-
- Select The Rom File from your local storage or use a link
-
-
;
-}
-
-function Details (data: {})
-{
- const { ref, focusKey } = useFocusable({ focusKey: 'add-game-details-section' });
- const state = Route.useSearch();
- const step = state.step ?? 0;
- return
-
- {step === 0 && }
- {step === 1 && }
- {step === 2 && }
- {step === 3 && }
-
-
-
;
-}
-
-function getStepDetails (index: number, state: z.infer)
-{
- let completed = index < state.step;
- if (index === 0 && state.gameLocation) completed = true;
- if (index === 1 && state.selectedGame) completed = true;
- if (index === 2 && state.platformId) completed = true;
- if (index === 3 && state.gameLocation && state.selectedGame && state.platformId) completed = true;
- let canNavigate = index <= state.step;
- if (index === 1 && state.gameLocation) canNavigate = true;
- if (index === 2 && state.selectedGame) canNavigate = true;
- if (index === 3 && state.platformId) canNavigate = true;
- return { completed, canNavigate };
-}
-
-function Step (data: { index: number; label: string; })
-{
- const navigate = useNavigate();
- const handleGoToStep = (step: number) =>
- {
- navigate({ to: '/game/add', search: { ...state, step: step }, replace: true });
- oneShot('openGeneric');
- };
- const state = Route.useSearch();
- const step = state.step ?? 0;
- const { canNavigate, completed } = getStepDetails(data.index, state);
-
- const { ref } = useFocusable({
- focusKey: `step-${data.index}`,
- focusable: canNavigate,
- onFocus: () =>
- {
- if (step === data.index) return;
- navigate({ to: '/game/add', search: { ...state, step: data.index }, replace: true });
- oneShot('openGeneric');
- }
- });
- return
- {
- if (!canNavigate) return;
- handleGoToStep(data.index);
- }} className={twMerge("step not-aria-disabled:cursor-pointer", data.index <= step ? "step-primary" : "")}>
- {completed ? : }
- {data.label}
- ;
-}
-
-function Steps ()
-{
- const state = Route.useSearch();
- const step = state.step ?? 0;
- const { ref, focusKey } = useFocusable({ focusKey: "steps", preferredChildFocusKey: `step-${step}`, saveLastFocusedChild: false });
- return
-
-
-
- {StepDetails.map((s, i) => )}
-
-
;
-}
-
-function RouteComponent ()
-{
- const navigate = useNavigate();
- const state = Route.useSearch();
- const step = state.step ?? 0;
- const router = useRouter();
- const queryClient = useQueryClient();
- const isAddingGame = queryClient.isMutating(addManualGameMutation) > 0;
-
- const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'add-game-page', preferredChildFocusKey: 'steps' });
-
- const handleReturnStep = (e: Event) =>
- {
- if (step <= 0)
- {
- HandleGoBack(router, e);
- } else
- {
- const newStep = step - 1;
- navigate({ to: '/game/add', search: { ...state, step: newStep }, replace: true });
- }
- };
-
- const handleStepNavigation = (newStep: number) =>
- {
- if (step === newStep) return;
- const { canNavigate } = getStepDetails(newStep, state);
- if (!canNavigate) return;
- navigate({ to: '/game/add', search: { ...state, step: newStep }, replace: true });
- oneShot('openGeneric');
- };
-
- useShortcuts(focusKey, () => [
- { button: GamePadButtonCode.B, label: step === 0 ? "Cancel" : "Prev Step", action: handleReturnStep },
- { button: GamePadButtonCode.Y, label: "Cancel", action: e => HandleGoBack(router, e) },
- {
- button: GamePadButtonCode.L1, label: "Prev Step", action (e)
- {
- handleStepNavigation(Math.max(step - 1, 0));
- },
- },
- {
- button: GamePadButtonCode.R1, label: "Next Step", action (e)
- {
- handleStepNavigation(Math.min(step + 1, 3));
- },
- }
- ], [step]);
-
- return
-
-
-
-
-
-
-
-
-
- {isAddingGame &&
-
- }
-
-
-
;
-}
diff --git a/src/mainview/routes/game/update.$source.$id.tsx b/src/mainview/routes/game/update.$source.$id.tsx
deleted file mode 100644
index 0a6ef83..0000000
--- a/src/mainview/routes/game/update.$source.$id.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
-import { AutoFocus } from '@/mainview/components/AutoFocus';
-import GameLookupElement from '@/mainview/components/game/GameLookup';
-import { HeaderUI } from '@/mainview/components/Header';
-import { FloatingShortcuts } from '@/mainview/components/Shortcuts';
-import { customUpdateMutation, gameInvalidationQuery, gameQuery } from '@/mainview/scripts/queries/romm';
-import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
-import { HandleGoBack } from '@/mainview/scripts/utils';
-import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
-import { useMutation, useQuery } from '@tanstack/react-query';
-import { createFileRoute, useRouter } from '@tanstack/react-router';
-import { useEffect, useState } from 'react';
-import toast from 'react-hot-toast';
-
-export const Route = createFileRoute('/game/update/$source/$id')({
- component: RouteComponent,
-});
-
-function RouteComponent ()
-{
- const { source, id } = Route.useParams();
- const [search, setSearch] = useState(undefined);
-
- const router = useRouter();
- const { data: game } = useQuery(gameQuery(source, id));
- const update = useMutation({
- ...customUpdateMutation,
- async onSuccess (data, variables, onMutateResult, context)
- {
- toast.success("Updated Metadata");
- await context.client.invalidateQueries(gameInvalidationQuery(source, id));
- router.history.back();
- },
- });
-
- const { ref, focusKey, focusSelf } = useFocusable({ focusKey: `custom-update-page`, preferredChildFocusKey: 'search-field-section' });
-
- useShortcuts(focusKey, () => [{ button: GamePadButtonCode.B, label: "Return", action (e) { HandleGoBack(router, e); }, }]);
- useEffect(() =>
- {
- if (search) return;
- setSearch(game?.name ?? undefined);
- }, [game]);
-
- return
-
-
-
-
-
- update.mutate({ source, id, destination: l.source, destinationId: l.id })}
- />
-
-
-
-
- ;
-}
diff --git a/src/mainview/routes/games.tsx b/src/mainview/routes/games.tsx
index bd0fc8e..3742e83 100644
--- a/src/mainview/routes/games.tsx
+++ b/src/mainview/routes/games.tsx
@@ -1,13 +1,12 @@
-import { createFileRoute, useNavigate } from '@tanstack/react-router';
+import { createFileRoute } from '@tanstack/react-router';
import { CollectionsDetail } from '../components/CollectionsDetail';
import { zodValidator } from '@tanstack/zod-adapter';
import z from 'zod';
-import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
+import { GameListFilterType } from '@/shared/constants';
import { useSessionStorage } from 'usehooks-ts';
import HeaderSearchField from '../components/HeaderSearchField';
-import { useEffect } from 'react';
-import { RoundButton } from '../components/RoundButton';
-import { Plus } from 'lucide-react';
+import { useEffect, useState } from 'react';
+import { setFocus } from '@noriginmedia/norigin-spatial-navigation';
export const Route = createFileRoute('/games')({
component: RouteComponent,
@@ -22,7 +21,6 @@ function RouteComponent ()
const { focus } = Route.useSearch();
const { search } = Route.useSearch();
const [filter, setFilter] = useSessionStorage('all-games-filters', {});
- const navigate = useNavigate();
useEffect(() =>
{
@@ -30,13 +28,7 @@ function RouteComponent ()
}, [search]);
return
- {
- navigate({ to: '/game/add' });
- }} >,
- setFilter({ ...filter, search: v })} search={filter.search} id='search-filter' />]
- }
+ headerButtonElements={ setFilter({ ...filter, search: v })} search={filter.search} id='search-filter' />}
localFilter={filter}
setLocalFilter={setFilter}
focus={focus}
diff --git a/src/mainview/routes/index.tsx b/src/mainview/routes/index.tsx
index 8ae562a..527fea9 100644
--- a/src/mainview/routes/index.tsx
+++ b/src/mainview/routes/index.tsx
@@ -3,18 +3,23 @@ import
{
Gamepad2,
Settings,
+ MessageSquare,
+ Image,
Search,
Power,
OctagonAlert,
Maximize,
Store,
LayoutGrid,
+ PlusCircle,
+ Plus,
LucideIcon,
} from "lucide-react";
import
{
createFileRoute,
- useNavigate,
+ PathParamOptions,
+ ToPathOption,
useRouter,
} from "@tanstack/react-router";
import { useMutation, useQueryClient } from "@tanstack/react-query";
@@ -36,12 +41,12 @@ import SaveScroll from "../components/SaveScroll";
import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";
import { twMerge } from "tailwind-merge";
import { PlatformsList } from "../components/PlatformsList";
-import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
+import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
import z from "zod";
import CollectionList from "../components/CollectionList";
import { zodValidator } from '@tanstack/zod-adapter';
-import { mobileCheck, scrollIntoViewHandler, useDragScroll } from "../scripts/utils";
-import { AnimatedBackgroundContext, GlobalDialogContext } from "../scripts/contexts";
+import { mobileCheck, scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from "../scripts/utils";
+import { AnimatedBackgroundContext } from "../scripts/contexts";
import Carousel from "../components/Carousel";
import { closeMutation } from "@queries/system";
import { gameQuery } from "../scripts/queries/romm";
@@ -51,11 +56,6 @@ import SelectMenu from "../components/SelectMenu";
import HeaderSearchField from "../components/HeaderSearchField";
import CardElement from "../components/CardElement";
import { Router } from "..";
-import { FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
-import { playGame, usePlayMutation } from "../components/game/MainActions";
-import { rommApi } from "../scripts/clientApi";
-import { ContextList, DialogEntry } from "../components/ContextDialog";
-import { FOCUS_KEYS } from "../scripts/types";
export const Route = createFileRoute("/")({
component: ConsoleHomeUI,
@@ -157,9 +157,6 @@ function HomeList (data: {
focusKey: "home-list",
preferredChildFocusKey: `${data.selectedFilter}-list`
});
- const navigate = useNavigate();
- const playGameMut = usePlayMutation(navigate);
- const globalDialog = useContext(GlobalDialogContext);
const handleNodeFocus = (id: string, node: HTMLElement, details: FocusDetails) =>
{
@@ -177,52 +174,6 @@ function HomeList (data: {
router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } });
};
- async function handleGamePlay (id: FrontEndId, source: string | null, sourceId: string | null)
- {
- const finalSource = source ?? id.source;
- const finalId = String(sourceId ?? id.id);
-
- const validCommands = await rommApi.api.romm.game({ source: finalSource })({ id: finalId }).commands.get();
- if (validCommands.data)
- {
- const preferredCommand = localStorage.getItem(`${finalSource}-${finalId}-preferred-command`);
- if (preferredCommand)
- {
- playGame(finalSource, finalId, validCommands.data.commands[JSON.parse(preferredCommand)], navigate, playGameMut.mutate);
- } else
- {
- if (validCommands.data.commands.length > 1)
- {
- globalDialog.openContext({
- content:
- {
- const option: DialogEntry = {
- id: String(c.id),
- content: c.label ?? String(c.id),
- type: "primary",
- action (ctx)
- {
- localStorage.setItem(`${finalSource}-${finalId}-preferred-command`, JSON.stringify(i));
- ctx.close();
- playGame(finalSource, finalId, validCommands.data.commands[0], navigate, playGameMut.mutate);
- },
- };
-
- return option;
- })
- } />
- }, FOCUS_KEYS.GAME_LIST_CARD('games-list', id));
- } else if (validCommands.data.commands.length === 1)
- {
- playGame(finalSource, finalId, validCommands.data.commands[0], navigate, playGameMut.mutate);
- }
-
- }
- }
-
- }
-
let activeList: JSX.Element;
switch (data.selectedFilter)
{
@@ -244,7 +195,6 @@ function HomeList (data: {
activeList = <>
{
@@ -258,7 +208,7 @@ function HomeList (data: {
setBackground={bg.setBackground}
filters={{ limit: 12, orderBy: 'activity' }}
finalElement={[
- ,
+ ,
]}
emptyElement={[
diff --git a/src/mainview/routes/launcher.$source.$id.tsx b/src/mainview/routes/launcher.$source.$id.tsx
index 78df354..e66de07 100644
--- a/src/mainview/routes/launcher.$source.$id.tsx
+++ b/src/mainview/routes/launcher.$source.$id.tsx
@@ -1,11 +1,12 @@
import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
import { createFileRoute, useBlocker, useRouter } from '@tanstack/react-router';
import DotsLoading from '../components/backgrounds/dots';
-import { GamePadButtonCode, useShortcuts } from '../scripts/shortcuts';
+import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
import { useFocusable } from '@noriginmedia/norigin-spatial-navigation';
-import { FloatingShortcuts } from '../components/Shortcuts';
+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,
diff --git a/src/mainview/routes/platform.$source.$id.tsx b/src/mainview/routes/platform.$source.$id.tsx
index bc35faf..e9feb92 100644
--- a/src/mainview/routes/platform.$source.$id.tsx
+++ b/src/mainview/routes/platform.$source.$id.tsx
@@ -1,17 +1,14 @@
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { CollectionsDetail } from "../components/CollectionsDetail";
import { useMutation, useQuery } from "@tanstack/react-query";
-import { RPC_URL } from "../../shared/constants";
-import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
+import { GameListFilterSchema, GameListFilterType, RPC_URL } from "../../shared/constants";
import { deletePlatformMutation, localPlatformFilter, platformQuery, updatePlatformMutation } from "@queries/romm";
import { zodValidator } from "@tanstack/zod-adapter";
import z from "zod";
import { useLocalStorage } from "usehooks-ts";
import { RefreshCcw, Settings2 } from "lucide-react";
-import { ContextList, DialogEntry } from "../components/ContextDialog";
+import { ContextList, DialogEntry, useContextDialog } from "../components/ContextDialog";
import toast from "react-hot-toast";
-import { useContext } from "react";
-import { GlobalDialogContext } from "../scripts/contexts";
export const Route = createFileRoute("/platform/$source/$id")({
component: RouteComponent,
@@ -25,7 +22,7 @@ function PlatformTitle (data: {})
const { source, id } = Route.useParams();
const { data: platform } = useQuery(platformQuery(source, id));
- return
+ return
{!!platform &&
}${platform.path_cover}`})
}
@@ -39,15 +36,13 @@ function RouteComponent ()
const { source, id } = Route.useParams();
const router = useRouter();
const { countHint } = Route.useSearch();
- const { data: platform } = useQuery(platformQuery(source, id));
const [filter, setFilter] = useLocalStorage
("platforms-filters", {});
const updatePlatform = useMutation({
- ...updatePlatformMutation(source, id), onSuccess (data, variables, onMutateResult, context)
+ ...updatePlatformMutation(id), onSuccess (data, variables, onMutateResult, context)
{
context.client.invalidateQueries(localPlatformFilter(id));
},
});
- const globalDialog = useContext(GlobalDialogContext);
const deletePlatform = useMutation({
...deletePlatformMutation(id),
onError (error, variables, onMutateResult, context)
@@ -61,7 +56,7 @@ function RouteComponent ()
},
});
const settingsOptions: DialogEntry[] = [];
- if (source === 'local' || platform?.hasLocal)
+ if (source === 'local')
{
settingsOptions.push({
id: 'update-platform',
@@ -75,12 +70,9 @@ function RouteComponent ()
router.navigate({ replace: true });
},
});
- }
- if (source === 'local')
- {
settingsOptions.push({
- id: 'delete-platform',
+ id: 'update-platform',
type: "error",
content: "Delete",
icon: deletePlatform.isPending ? : ,
@@ -91,6 +83,10 @@ function RouteComponent ()
});
}
+ const { dialog: platformSettingsDialog, setOpen: setPlatformSettingsOpen } = useContextDialog('platform-settings-dialog', {
+ content:
+ });
+
return (
,
action ()
{
- globalDialog.openContext({ content:
}, 'open-platform-settings-btn');
+ setPlatformSettingsOpen(true);
},
}]}
countHint={countHint}
title={
}
filters={{ platform_id: Number(id), platform_source: source }}
/>
+ {platformSettingsDialog}
);
}
diff --git a/src/mainview/routes/settings/about.tsx b/src/mainview/routes/settings/about.tsx
index da450d4..b6db34f 100644
--- a/src/mainview/routes/settings/about.tsx
+++ b/src/mainview/routes/settings/about.tsx
@@ -1,9 +1,11 @@
+import { Button } from '@/mainview/components/options/Button';
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
-import { systemInfoQuery } from '@queries/system';
-import { useQuery } from '@tanstack/react-query';
+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')({
diff --git a/src/mainview/routes/settings/accounts.tsx b/src/mainview/routes/settings/accounts.tsx
index eacd432..0e63a80 100644
--- a/src/mainview/routes/settings/accounts.tsx
+++ b/src/mainview/routes/settings/accounts.tsx
@@ -7,14 +7,13 @@ import
import { useIsMutating, useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, useRouter } from "@tanstack/react-router";
import classNames from "classnames";
-import { Info, Key, Link, Lock, LogIn, LogOut, ScanQrCode, User, X } from "lucide-react";
+import { Key, Link, Lock, LogIn, LogOut, ScanQrCode, User, X } from "lucide-react";
import
{
useEffect,
useRef,
} from "react";
-import { RPC_URL } from "@shared/constants";
-import { RommLoginDataSchema } from '@simeonradivoev/gameflow-sdk/shared';
+import { RommLoginDataSchema, RPC_URL } from "@shared/constants";
import toast from "react-hot-toast";
import { OptionSpace } from "../../components/options/OptionSpace";
import { useSettingsForm, useSettingsFormContext } from "../../components/options/SettingsAppForm";
@@ -27,13 +26,9 @@ import { TwitchIcon } from "@/mainview/scripts/brandIcons";
import { twitchLoginMutation, twitchLoginVerificationQuery, twitchLogoutMutation } from "@queries/settings";
import { rommGetOptionsQuery, rommLoggedInQuery, rommHostnameQuery, rommLoginMutation, rommLogoutMutation, rommQrLoginMutation, rommUsernameQuery, rommUserQuery, invalidateLogin } from "@queries/romm";
import { systemApi } from "@/mainview/scripts/clientApi";
-import z from "zod";
export const Route = createFileRoute("/settings/accounts")({
component: RouteComponent,
- validateSearch: z.object({
- focus: z.string().optional()
- }),
});
function LoginQR (data: { id: string, isOpen: boolean, cancel: () => void, url: string; endsAt: Date; startedAt: Date; code?: string; })
@@ -226,8 +221,6 @@ function RouteComponent ()
} type="text" />} />
} type="password" placeholder="Password" />} />
-
- For Romm Client API Token open plugin settings
} />
diff --git a/src/mainview/routes/settings/directories.tsx b/src/mainview/routes/settings/directories.tsx
index 5adee26..5a32eec 100644
--- a/src/mainview/routes/settings/directories.tsx
+++ b/src/mainview/routes/settings/directories.tsx
@@ -13,13 +13,9 @@ import { systemApi } from '@/mainview/scripts/clientApi';
import useActiveControl from '@/mainview/scripts/gamepads';
import { changeDownloadsMutation } from '@queries/settings';
import { downloadDrivesQuery } from '@/mainview/scripts/queries/system';
-import { DownloadsDrive } from '@simeonradivoev/gameflow-sdk/shared';
-import { zodValidator } from '@tanstack/zod-adapter';
-import z from 'zod';
export const Route = createFileRoute('/settings/directories')({
component: RouteComponent,
- validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
});
function DriveComponent (data: { drive: DownloadsDrive; downloadsSize: number; refetchDrives: () => void; })
diff --git a/src/mainview/routes/settings/emulators.tsx b/src/mainview/routes/settings/emulators.tsx
index 17344df..363ef45 100644
--- a/src/mainview/routes/settings/emulators.tsx
+++ b/src/mainview/routes/settings/emulators.tsx
@@ -4,12 +4,11 @@ import { OptionInput } from '../../components/options/OptionInput';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useState } from 'react';
import { Button } from '../../components/options/Button';
-import { Check, ChevronDown, FolderSearch, HardDrive, Plug, SearchAlert, Store, Trash } from 'lucide-react';
+import { Check, ChevronDown, FileQuestion, FolderSearch, HardDrive, Plug, SearchAlert, Store, Trash } from 'lucide-react';
import { ContextDialog, ContextList, DialogEntry, OptionElement } from '../../components/ContextDialog';
import classNames from 'classnames';
import { twMerge } from 'tailwind-merge';
-import { RPC_URL } from '../../../shared/constants';
-import { SettingsSchema } from '@simeonradivoev/gameflow-sdk/shared';
+import { RPC_URL, SettingsSchema } from '../../../shared/constants';
import emulators from '@emulators';
import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts';
@@ -21,15 +20,10 @@ import { FOCUS_KEYS } from '@/mainview/scripts/types';
import { scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from '@/mainview/scripts/utils';
import { SettingsOption } from '@/mainview/components/options/SettingsOption';
import { SettingsDropdown } from '@/mainview/components/options/SettingsDropdown';
-import { FrontEndEmulator } from '@simeonradivoev/gameflow-sdk/shared';
-import { zodValidator } from '@tanstack/zod-adapter';
-import z from 'zod';
-import { isUrl } from '@/shared/utils';
export const Route = createFileRoute('/settings/emulators')({
component: RouteComponent,
pendingComponent: EmulatorsPending,
- validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
});
function EmulatorsPending ()
@@ -86,10 +80,7 @@ function NewEmulatorPath (data: { addOverride: (emulator: string) => void; isAdd
};
- return
- Custom Emulator Path
- Manually Pick a path to an emulator if not automatically found.
- }>
+ return
@@ -60,7 +55,6 @@ function Plugin (data: {
>
{data.plugin.hasSettings ? : }
- {data.plugin.canUninstall && {uninstall.isPending ? : }}
{data.plugin.canDisable && data.setEnabled(!!v)} value={data.plugin.enabled} name={data.plugin.name} type="checkbox" />}
;
diff --git a/src/mainview/routes/settings/route.tsx b/src/mainview/routes/settings/route.tsx
index 625e884..5df28ac 100644
--- a/src/mainview/routes/settings/route.tsx
+++ b/src/mainview/routes/settings/route.tsx
@@ -7,6 +7,7 @@ import
{
Outlet,
createFileRoute,
+ useMatch,
useMatchRoute,
useRouter,
useRouterState,
@@ -16,7 +17,6 @@ import classNames from "classnames";
import
{
ArrowBigLeft,
- Cog,
FingerprintPattern,
HardDrive,
Info,
@@ -27,8 +27,10 @@ import
} from "lucide-react";
import { JSX, useMemo } from "react";
import { twMerge } from "tailwind-merge";
-import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
-import Shortcuts from "@/mainview/components/Shortcuts";
+import z from "zod";
+import { SettingsSchema } from "../../../shared/constants";
+import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
+import Shortcuts, { FloatingShortcuts } from "@/mainview/components/Shortcuts";
import { HandleGoBack } from "@/mainview/scripts/utils";
import { AutoFocus } from "@/mainview/components/AutoFocus";
import { oneShot } from "@/mainview/scripts/audio/audio";
@@ -36,6 +38,9 @@ import SelectMenu from "@/mainview/components/SelectMenu";
export const Route = createFileRoute("/settings")({
component: SettingsUI,
+ validateSearch: z.object({
+ focus: z.keyof(SettingsSchema).optional()
+ }),
staticData: {
enterSound: 'openSettings'
}
@@ -156,12 +161,6 @@ function SettingsMenu (data: {})
label="Plugins"
icon={
}
/>
-
}
- />
-
+
{
- const url = isUrl(c) ? new URL(c) : new URL(`${RPC_URL(__HOST__)}${c}`);
+ const url = new URL(`${RPC_URL(__HOST__)}${c}`);
url.searchParams.delete('ts');
return url;
});
diff --git a/src/mainview/routes/store/tab/index.tsx b/src/mainview/routes/store/tab/index.tsx
index dcb53c8..178eea0 100644
--- a/src/mainview/routes/store/tab/index.tsx
+++ b/src/mainview/routes/store/tab/index.tsx
@@ -16,7 +16,6 @@ import { useQuery } from '@tanstack/react-query';
import { autoEmulatorsQuery } from '@queries/settings';
import { storeEmulatorsRecommendedQuery, storeFeaturedGamesQuery } from '@queries/store';
import ImageWithFallbacks from '@/mainview/components/ImageWithFallbacks';
-import { FrontEndGameTypeDetailed } from '@simeonradivoev/gameflow-sdk/shared';
export const Route = createFileRoute('/store/tab/')({
component: RouteComponent
@@ -127,7 +126,7 @@ export function RouteComponent ()
{}
{!!crucialEmulators && crucialEmulators?.length > 0 && storeContext.showDetails('emulator', em.source, em.name, focus)}
+ onSelect={(id, focus) => storeContext.showDetails('emulator', 'store', id, focus)}
emulators={crucialEmulators} />}
- {
- navigate({ to: '/store/details/plugin/$id', params: { id: decodeURIComponent(data.plugin.package.name) } });
- };
- const { ref, focusKey } = useFocusable({ focusKey: FOCUS_KEYS.PLUGIN_ENTRY(data.plugin.package.sanitized_name), onEnterPress: onAction });
- const handleRefresh = (client: QueryClient) =>
- {
- client.invalidateQueries(allPluginsFilter);
- navigate({ to: '/store/tab/plugins', replace: true });
- };
- const update = useMutation({
- ...updatePluginMutation(data.plugin.package.name),
- onSuccess (data, variables, onMutateResult, context)
- {
- handleRefresh(context.client);
- },
- });
- const install = useMutation({
- ...installPluginMutation(data.plugin.package.name),
- onSuccess (f, variables, onMutateResult, context)
- {
- handleRefresh(context.client);
- }
- });
- const uninstall = useMutation({
- ...uninstallPluginMutation(data.plugin.package.name),
- onSuccess (f, variables, onMutateResult, context)
- {
- handleRefresh(context.client);
- }
- });
- useShortcuts(focusKey, () =>
- {
- const shortcuts: Shortcut[] = [{
- label: "Details", button: GamePadButtonCode.A, action (e)
- {
- onAction();
- },
- }];
-
- if (data.plugin.installed)
- {
- shortcuts.push({
- label: "Uninstall",
- button: GamePadButtonCode.X,
- action (e)
- {
- uninstall.mutate();
- },
- });
-
- if (data.plugin.update)
- {
- shortcuts.push({
- label: "Update",
- button: GamePadButtonCode.Y,
- action (e)
- {
- update.mutate();
- },
- });
- }
-
- } else
- {
- shortcuts.push({
- label: "Install",
- button: GamePadButtonCode.X,
- action (e)
- {
- install.mutate();
- },
- });
- }
- return shortcuts;
- }, [data.plugin.installed, install.isPending, uninstall.isPending]);
- return
-
-
- {data.plugin.installed && }
- {data.plugin.update && }
- {data.plugin.package.name}
- {(install.isPending || uninstall.isPending) && }
-
-
{data.plugin.package.description}
-
{data.plugin.package.keywords.concat(...data.plugin.installed ? ["installed"] : []).map((k, i) => - {k}
)}
-
- - {data.plugin.package.publisher.username}
-
- - {data.plugin.package.version}
-
- - {prettyMilliseconds(new Date().getTime() - data.plugin.package.date.getTime(), { hideSeconds: true })}
-
- - {data.plugin.package.license}
- {install.isPending && <>
-
- - installing
- >}
- {uninstall.isPending && <>
-
- - uninstalling
- >}
-
-
-
-
-
- {data.plugin.downloads.monthly}
-
-
-
;
-}
-
-function RouteComponent ()
-{
- const [search] = useSessionStorage(`${Route.to}-search`, undefined);
- const { data: plugins } = useQuery(pluginsQuery(search));
- const { ref, focusKey } = useFocusable({ focusKey: "plugins-store" });
- return
-
-
-
- {plugins?.objects.map((p, i) =>
)}
-
-
-
;
-}
diff --git a/src/mainview/routes/store/tab/route.tsx b/src/mainview/routes/store/tab/route.tsx
index 81a5a01..d771fa6 100644
--- a/src/mainview/routes/store/tab/route.tsx
+++ b/src/mainview/routes/store/tab/route.tsx
@@ -3,19 +3,18 @@ import { FilterUI } from '@/mainview/components/Filters';
import { HeaderUI } from '@/mainview/components/Header';
import HeaderSearchField from '@/mainview/components/HeaderSearchField';
import SelectMenu from '@/mainview/components/SelectMenu';
-import { FloatingShortcuts } from '@/mainview/components/Shortcuts';
+import Shortcuts, { FloatingShortcuts } from '@/mainview/components/Shortcuts';
import { StoreContext } from '@/mainview/scripts/contexts';
import { gameQuery } from '@/mainview/scripts/queries/romm';
import { storeEmulatorDetailsQuery } from '@/mainview/scripts/queries/store';
-import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
+import { GamePadButtonCode, useShortcutContext, useShortcuts } from '@/mainview/scripts/shortcuts';
import { HandleGoBack, mobileCheck, useStickyDataAttr } from '@/mainview/scripts/utils';
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
import { useQueryClient } from '@tanstack/react-query';
import { useMatchRoute, useRouter } from '@tanstack/react-router';
import { createFileRoute, Outlet } from '@tanstack/react-router';
import { zodValidator } from '@tanstack/zod-adapter';
-import { DownloadCloud, Gamepad2, Home, Joystick, Puzzle } from 'lucide-react';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
import { useSessionStorage } from 'usehooks-ts';
import z from 'zod';
@@ -94,11 +93,9 @@ function RouteComponent ()
const headerRef = useRef(null);
const sentinelRef = useRef(null);
const filters: Record = {
- home: { label: "Home", icon: , selected: useIsSettings(''), },
- emulators: { label: "Emulators", icon: , selected: useIsSettings('emulators') },
- games: { label: "Games", icon: , selected: useIsSettings('games') },
- download: { label: "Download", icon: , selected: useIsSettings('download') },
- plugins: { label: "Plugins", icon: , selected: useIsSettings('plugins') }
+ home: { label: "Home", selected: useIsSettings(''), },
+ emulators: { label: "Emulators", selected: useIsSettings('emulators') },
+ games: { label: "Games", selected: useIsSettings('games') }
};
const [search, setSearch] = useSessionStorage(`${router.history.location.pathname}-search`, undefined);
const [, setGamesSearch] = useSessionStorage(`/store/tab/games-search`, undefined);
@@ -107,7 +104,7 @@ function RouteComponent ()
{
if (type === 'emulator')
{
- if (!source || source === 'local') return;
+ if (source === 'local') return;
router.navigate({ to: '/store/details/emulator/$id', params: { id } });
}
else if (type === 'game')
diff --git a/src/mainview/scripts/audio/audio.ts b/src/mainview/scripts/audio/audio.ts
index 430e4de..743b4ea 100644
--- a/src/mainview/scripts/audio/audio.ts
+++ b/src/mainview/scripts/audio/audio.ts
@@ -3,7 +3,7 @@ import sounds from '../../assets/sounds.ogg';
import soundSprites from '../../assets/sounds.json';
import { getLocalSetting } from '../utils';
import { hapticMap } from '../gamepads';
-import { soundMap, SoundMapEntry } from './audioConstants';
+import { soundMap } from './audioConstants';
const timingMap = new Map();
@@ -37,23 +37,25 @@ function sinRandom ()
return Math.sin(new Date().getMilliseconds() / 1000 * Math.PI);
}
+function cosRandom ()
+{
+ return Math.sin(new Date().getMilliseconds() / 1000 * Math.PI);
+}
function random ()
{
return Math.random() * 2 - 1;
}
-export function oneShot (id: keyof typeof soundMap, options?: { volume?: number; })
+export function oneShot (id: keyof typeof soundMap)
{
const currentDate = timingMap.get(id);
if (!getLocalSetting('soundEffects')) return;
- const soundValue = soundMap[id] as SoundMapEntry;
- const maxDelay = soundValue.maxDelay ?? 100;
- if (currentDate && new Date().getTime() - currentDate.getTime() <= maxDelay) return;
-
+ if (currentDate && new Date().getTime() - currentDate.getTime() <= 100) return;
+ const soundValue = soundMap[id] as { key: keyof typeof soundSprites.sprite, rateVariation?: number; volumeVariation?: number; };
const instanceId = sound.play(soundValue.key);
const baseVolume = getLocalSetting("soundEffectsVolume") / 100;
- sound.volume(Math.min(baseVolume * (soundValue.volume ?? 1) * (options?.volume ?? 1) * (1 + random() * (soundValue.volumeVariation ?? 0), 1)), instanceId);
+ sound.volume(Math.min(baseVolume * (1 + random() * (soundValue.volumeVariation ?? 0), 1)), instanceId);
sound.rate(1 + sinRandom() * (soundValue.rateVariation ?? 0), instanceId);
timingMap.set(id, new Date());
}
diff --git a/src/mainview/scripts/audio/audioConstants.ts b/src/mainview/scripts/audio/audioConstants.ts
index 25c85c6..a877e12 100644
--- a/src/mainview/scripts/audio/audioConstants.ts
+++ b/src/mainview/scripts/audio/audioConstants.ts
@@ -3,15 +3,6 @@ import soundSprites from '../../assets/sounds.json';
const volumeVariation = 0.05;
const rateVariation = 0.02;
-export interface SoundMapEntry
-{
- key: keyof typeof soundSprites.sprite;
- rateVariation?: number;
- volumeVariation?: number;
- volume?: number;
- maxDelay?: number;
-}
-
export const soundMap = {
openDetails: { key: 'Classic UI SFX - Chords #2' },
returnGeneric: { key: 'Classic UI SFX - Short - Low #2' },
@@ -23,16 +14,10 @@ export const soundMap = {
selectFilter: { key: 'Classic UI SFX - Short - High #3', volumeVariation },
closeContext: { key: 'Classic UI SFX - Short - High #19' },
openContext: { key: 'Classic UI SFX - Short - High #22' },
- openKeyboard: { key: 'Classic UI SFX - Short - High #25' },
openStore: { key: 'Classic UI SFX - Chords #16' },
openSettings: { key: 'Classic UI SFX - Short - High #8' },
click: { key: "UI_Single_Set 16_03", rateVariation, volumeVariation },
clickAlt: { key: "UI_Single_Set 16_01", rateVariation, volumeVariation },
- keyPress: { key: "UI_Single_Set 5_02", rateVariation, volumeVariation },
- keyPressReturn: { key: "UI_Single_Set 5_04", rateVariation, volumeVariation },
- keyPressSpace: { key: "UI_Single_Set 5_03", rateVariation, volumeVariation },
- keyPressBackspace: { key: "UI_Single_Set 5_01", rateVariation, volumeVariation },
- keyHover: { key: "UI_Single_Set 11_02", rateVariation, volumeVariation, volume: 0.5, maxDelay: 60 },
invalidNavigation: { key: "Classic UI SFX - Short - Low #6", rateVariation, volumeVariation },
launch: { key: "UI SFX_InGameMenu_Open" }
-} satisfies Record;
\ No newline at end of file
+} satisfies Record;
\ No newline at end of file
diff --git a/src/mainview/scripts/brandIcons.tsx b/src/mainview/scripts/brandIcons.tsx
index bf2e576..ef35534 100644
--- a/src/mainview/scripts/brandIcons.tsx
+++ b/src/mainview/scripts/brandIcons.tsx
@@ -3,8 +3,4 @@ export const TwitchIcon =
;
-export const IGDBIcon = ;
-
-export const Rclone = ;
-
export const FlatpackIcon = ;
\ No newline at end of file
diff --git a/src/mainview/scripts/contexts.ts b/src/mainview/scripts/contexts.ts
index 0c958b1..d987199 100644
--- a/src/mainview/scripts/contexts.ts
+++ b/src/mainview/scripts/contexts.ts
@@ -1,4 +1,4 @@
-import { SystemInfoType, Drive, AppInfoContext } from '@simeonradivoev/gameflow-sdk/shared';
+import { SystemInfoType } from "@/shared/constants";
import { Direction, FocusDetails } from "@noriginmedia/norigin-spatial-navigation";
import { createContext } from "react";
import { Shortcut } from "./shortcuts";
@@ -45,16 +45,6 @@ export const ShortcutsContext = createContext({} as {
export const SystemInfoContext = createContext({} as SystemInfoType | undefined);
-export const AppContext = createContext({} as AppInfoContext);
-
-export const GlobalDialogContext = createContext({} as {
- openContext: (options: {
- content: any;
- preferredChildFocusKey?: string;
- onClose?: () => void;
- }, focusKey: string) => void;
-});
-
export const GameDetailsContext = createContext<{
update: () => void;
}>({} as any);
\ No newline at end of file
diff --git a/src/mainview/scripts/gamepads.ts b/src/mainview/scripts/gamepads.ts
index 5959d90..251f000 100644
--- a/src/mainview/scripts/gamepads.ts
+++ b/src/mainview/scripts/gamepads.ts
@@ -1,7 +1,7 @@
import { getCurrentFocusKey, navigateByDirection } from "@noriginmedia/norigin-spatial-navigation";
import { GetFocusedElement } from "./spatialNavigation";
import { useEffect, useState } from "react";
-import { getLocalSetting, isTextInputFocused, mobileCheck } from "./utils";
+import { getLocalSetting, mobileCheck } from "./utils";
import { oneShot } from "./audio/audio";
import { Router } from "@/mainview";
@@ -98,11 +98,6 @@ const throttleMap = new Map();
const throttleAcceleration = new Map();
function throttleNav (key: string, dir: string, event: Event)
{
- if (isTextInputFocused())
- {
- return false;
- }
-
const minSpeed = 150;
const maxSpeed = 300;
const currentDate = new Date();
diff --git a/src/mainview/scripts/queries/plugins.ts b/src/mainview/scripts/queries/plugins.ts
index 8e6e948..323b168 100644
--- a/src/mainview/scripts/queries/plugins.ts
+++ b/src/mainview/scripts/queries/plugins.ts
@@ -1,4 +1,4 @@
-import { mutationOptions, QueryFilters, queryOptions } from "@tanstack/react-query";
+import { mutationOptions, queryOptions } from "@tanstack/react-query";
import { pluginsApi } from "../clientApi";
export const getAllPluginsQuery = queryOptions({
@@ -14,7 +14,7 @@ export const getAllPluginsQuery = queryOptions({
export const getPluginDetailsQuery = (source: string) => queryOptions({
queryKey: ['plugins', source], queryFn: async () =>
{
- const { data, error } = await pluginsApi.plugins({ id: encodeURIComponent(source) }).get();
+ const { data, error } = await pluginsApi.plugins({ id: source }).get();
if (error) throw error;
return data;
}
@@ -24,51 +24,7 @@ export const enablePluginMutation = mutationOptions({
mutationKey: ['plugin', 'enable'],
mutationFn: async (vars: { id: string, enabled: boolean; }) =>
{
- const { error } = await pluginsApi.plugins({ id: encodeURIComponent(vars.id) }).post({ enabled: vars.enabled });
+ const { error } = await pluginsApi.plugins({ id: vars.id }).post({ enabled: vars.enabled });
if (error) throw error;
}
-});
-
-export const installPluginMutation = (id: string) => mutationOptions({
- mutationKey: ['plugin', 'install', id],
- mutationFn: async () =>
- {
- const { data, error } = await pluginsApi.plugins.install.post({ id });
- if (error) throw error;
- return data;
- }
-});
-
-export const updatePluginMutation = (id: string) => mutationOptions({
- mutationKey: ['plugin', 'update', id],
- mutationFn: async () =>
- {
- const { data, error } = await pluginsApi.plugins.update.post({ id });
- if (error) throw error;
- return data;
- }
-});
-
-export const uninstallPluginMutation = (id: string) => mutationOptions({
- mutationKey: ['plugin', 'uninstall', id],
- mutationFn: async () =>
- {
- const { data, error } = await pluginsApi.plugins.uninstall.post({ id: id });
- if (error) throw error;
- return data;
- }
-});
-
-export const pluginFilter = (id: string): QueryFilters => ({
- predicate (query)
- {
- return query.queryKey.includes(id);
- },
-});
-
-export const allPluginsFilter: QueryFilters = ({
- predicate (query)
- {
- return query.queryKey.includes('plugin') || query.queryKey.includes('plugins');
- },
});
\ No newline at end of file
diff --git a/src/mainview/scripts/queries/romm.ts b/src/mainview/scripts/queries/romm.ts
index 5c76d2b..7ebf4db 100644
--- a/src/mainview/scripts/queries/romm.ts
+++ b/src/mainview/scripts/queries/romm.ts
@@ -1,7 +1,6 @@
-import { DefaultRommStaleTime } from "@/shared/constants";
-import { GameListFilterType, RommLoginDataSchema, FrontEndId, DownloadLookupEntry, DownloadsLookupFilter } from '@simeonradivoev/gameflow-sdk/shared';
+import { DefaultRommStaleTime, GameListFilterType, RommLoginDataSchema } from "@/shared/constants";
import { rommApi, settingsApi } from "../clientApi";
-import { infiniteQueryOptions, InvalidateQueryFilters, mutationOptions, QueryClient, QueryFilters, queryOptions } from "@tanstack/react-query";
+import { InvalidateQueryFilters, mutationOptions, QueryClient, QueryFilters, queryOptions, useMutation } from "@tanstack/react-query";
import z from "zod";
import { statsApiStatsGetOptions } from "@/clients/romm/@tanstack/react-query.gen";
@@ -167,9 +166,6 @@ export const gamesRecommendedBasedOnGameQuery = (source: string, id: string) =>
return data;
}
});
-export const allGamesInvalidateQuery: QueryFilters = {
- queryKey: ['games']
-};
export const gameInvalidationQuery = (source: string, id: string): QueryFilters => ({
predicate (query)
{
@@ -181,7 +177,7 @@ export const gameInvalidationQuery = (source: string, id: string): QueryFilters
export const validateSourceQuery = (source: string, id: string) => queryOptions({
queryKey: ["game", source, id, "validate"], queryFn: async () =>
{
- const { data } = await rommApi.api.romm.game({ source })({ id }).validate.get();
+ const { data, error } = await rommApi.api.romm.game({ source })({ id }).validate.get();
return data;
}
});
@@ -196,19 +192,16 @@ export const fixSourceMutation = mutationOptions({
export const updateSourceMutation = mutationOptions({
mutationKey: ['game', "update_source"], mutationFn: async ({ source, id }: { source: string, id: string; }) =>
{
- const { data, error } = await rommApi.api.romm.game({ source })({ id }).update.post({
- source: source,
- id: id
- });
+ const { data, error } = await rommApi.api.romm.game({ source })({ id }).update.post();
if (error) throw error;
return data;
}
});
-export const updatePlatformMutation = (source: string, id: string) => mutationOptions({
- mutationKey: ['platform', source, 'update', id],
+export const updatePlatformMutation = (id: string) => mutationOptions({
+ mutationKey: ['platform', 'local', 'update', id],
mutationFn: async () =>
{
- const { data, error } = await rommApi.api.romm.platform({ source })({ id }).update.post();
+ const { data, error } = await rommApi.api.romm.platform.local({ id }).update.post();
if (error) throw error;
return data;
}
@@ -236,93 +229,4 @@ export const gameFiltersQuery = (filters: { source?: string; }) => queryOptions(
if (error) throw error;
return data;
}
-});
-
-export const gameLookupQuery = (search: string | undefined) => queryOptions({
- queryKey: ['game', 'lookup', search],
- queryFn: async () =>
- {
- if (!search) return [];
- const { data, error } = await rommApi.api.romm.lookup.get({ query: { search } });
- if (error) throw error;
- return data;
- }
-});
-
-export const gameLookupDetails = (source: string | undefined, id: string | undefined) => queryOptions({
- enabled: !!source && !!id,
- queryKey: ['game', 'lookup', source, id],
- queryFn: async () =>
- {
- const { data, error } = await rommApi.api.romm.lookup({ source: source! })({ id: id! }).get();
- if (error) throw error;
- return data;
- }
-});
-
-export const platformLookupMatchQuery = (source: string | undefined, id: number | undefined) => queryOptions({
- enabled: !!source && !!id,
- queryKey: ['platform', 'lookup', 'match', source, id],
- queryFn: async () =>
- {
- const { data, error } = await rommApi.api.romm.platform.lookup.match({ source: source! })({ id: id! }).get();
- if (error) throw error;
- return data;
- }
-});
-
-export const customUpdateMutation = mutationOptions({
- mutationKey: ['game', 'custom-update'], mutationFn: async (args: { source: string, id: string, destination: string, destinationId: string; }) =>
- {
- const { data, error } = await rommApi.api.romm.game({ source: args.source })({ id: args.id }).update.post({ source: args.destination, id: args.destinationId });
- if (error) throw error;
- return data;
- }
-});
-
-export const addManualGameMutation = mutationOptions({
- mutationKey: ['game', 'custom-add'],
- mutationFn: async (args: { source: string, id: string, gamePath: string, platformId: number; }) =>
- {
- const { data, error } = await rommApi.api.romm.add.custom.post({
- source: args.source,
- id: args.id,
- gamePath: args.gamePath,
- platformId: args.platformId
- });
- if (error) throw error;
- return data;
- }
-});
-
-export const downloadsLookupQuery = (filter: DownloadsLookupFilter) => infiniteQueryOptions<{ data: DownloadLookupEntry[], totalCount: number, nextPage: number; }>({
- initialPageParam: 1,
- queryKey: ["downloads", filter],
- getNextPageParam: (lastPage, pages) => lastPage.nextPage,
- queryFn: async (params) =>
- {
- const pageParam = params.pageParam as number;
- const { data, error } = await rommApi.api.romm.downloads.lookup.get({ query: { ...filter, page: pageParam } });
- if (error) throw error;
- return { data: data.matches, totalCount: data.totalCount, nextPage: pageParam + 1 };
- }
-});
-
-export const downloadLookupQuery = (source: string, id: string) => queryOptions({
- queryKey: ["downloads", source, id],
- queryFn: async () =>
- {
- const { data, error } = await rommApi.api.romm.download.lookup({ source: encodeURIComponent(source) })({ id: encodeURIComponent(id) }).get();
- if (error) throw error;
- return data;
- }
-});
-
-export const downloadLookupFiltersQuery = queryOptions({
- queryKey: ['game', 'filters'], queryFn: async () =>
- {
- const { data, error } = await rommApi.api.romm.download.lookup.filters.get();
- if (error) throw error;
- return data;
- }
});
\ No newline at end of file
diff --git a/src/mainview/scripts/queries/settings.ts b/src/mainview/scripts/queries/settings.ts
index 03956af..e0f605e 100644
--- a/src/mainview/scripts/queries/settings.ts
+++ b/src/mainview/scripts/queries/settings.ts
@@ -138,7 +138,7 @@ export const getPluginSettingsDefinitionQuery = (source: string) => queryOptions
queryKey: ['settings', source, 'definitions'],
queryFn: async () =>
{
- const { data: value, error } = await settingsApi.api.settings.definitions({ source: encodeURIComponent(source) }).get();
+ const { data: value, error } = await settingsApi.api.settings.definitions({ source }).get();
if (error) throw error;
return value;
@@ -148,7 +148,7 @@ export const getPluginSettingQuery = (source: string, id: string) => queryOption
queryKey: ["setting", source, id],
queryFn: async () =>
{
- const { data, error } = await settingsApi.api.settings({ source: encodeURIComponent(source) })({ id: encodeURIComponent(id) }).get();
+ const { data, error } = await settingsApi.api.settings({ source })({ id }).get();
if (error) throw error;
return data;
@@ -158,7 +158,7 @@ export const setPluginSettingMutation = (source: string, id: string) => mutation
mutationKey: ["setting", source, id],
mutationFn: async (value: any) =>
{
- const { data, error } = await settingsApi.api.settings({ source: encodeURIComponent(source) })({ id: encodeURIComponent(id) }).put({ value });
+ const { data, error } = await settingsApi.api.settings({ source })({ id }).put({ value });
if (error) throw error;
return data;
@@ -167,7 +167,7 @@ export const setPluginSettingMutation = (source: string, id: string) => mutation
export const getPluginActionsQuery = (source: string) => queryOptions({
queryKey: ['plugin', source, 'actions'], queryFn: async () =>
{
- const { data, error } = await settingsApi.api.settings.actions({ source: encodeURIComponent(source) }).get();
+ const { data, error } = await settingsApi.api.settings.actions({ source }).get();
if (error) throw error;
return data;
@@ -177,7 +177,7 @@ export const pluginActionMutation = (source: string, id: string) => mutationOpti
mutationKey: ["plugin", source, "action"],
mutationFn: async () =>
{
- const { data, error, response } = await settingsApi.api.settings.actions({ source: encodeURIComponent(source) })({ id: encodeURIComponent(id) }).post();
+ const { data, error, response } = await settingsApi.api.settings.actions({ source })({ id }).post();
if (error) throw error;
return { data: data as any, response };
diff --git a/src/mainview/scripts/queries/store.ts b/src/mainview/scripts/queries/store.ts
index a428f40..9e506a9 100644
--- a/src/mainview/scripts/queries/store.ts
+++ b/src/mainview/scripts/queries/store.ts
@@ -1,6 +1,7 @@
import { infiniteQueryOptions, mutationOptions, queryOptions } from "@tanstack/react-query";
import { rommApi, storeApi } from "../clientApi";
-import { GameListFilterType, FrontEndGameType } from '@simeonradivoev/gameflow-sdk/shared';
+import { GameListFilterType } from "@/shared/constants";
+
export const storeEmulatorsQuery = (filters: { search?: string; }) => queryOptions({
queryKey: ['store-emulators', filters], queryFn: async () =>
@@ -95,22 +96,4 @@ export const getUpdateInfoForEmulator = (id: string) => queryOptions({
if (error) throw error;
return data;
}
-});
-export const pluginsQuery = (search?: string) => queryOptions({
- queryKey: ['plugins', 'store', search ?? 'all'],
- queryFn: async () =>
- {
- const { data, error } = await storeApi.api.store.plugins.get({ query: { search } });
- if (error) throw error;
- return data;
- }
-});
-export const pluginDetailsQuery = (id: string) => queryOptions({
- queryKey: ['plugin', 'store', id],
- queryFn: async () =>
- {
- const { data, error } = await storeApi.api.store.plugin.get({ query: { plugin: id } });
- if (error) throw error;
- return data;
- }
});
\ No newline at end of file
diff --git a/src/mainview/scripts/shortcuts.ts b/src/mainview/scripts/shortcuts.ts
index c947d23..35316b7 100644
--- a/src/mainview/scripts/shortcuts.ts
+++ b/src/mainview/scripts/shortcuts.ts
@@ -2,7 +2,6 @@ import { DependencyList, useEffect, useState } from "react";
import { GamepadButtonEvent } from "./gamepads";
import { dispatchFocusedEvent, GetFocusedTree } from "./spatialNavigation";
import { getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
-import { isTextInputFocused } from "./utils";
const shortcutMap = new Map Shortcut[])[]>();
const conflictSet = new Set();
@@ -124,21 +123,12 @@ export function useShortcutContext ()
if (e.key === 'Escape')
{
shortcuts.get(GamePadButtonCode.B)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.B }));
- } else
+ } else if (e.key === 'Backspace')
{
- // We use backspace and space in typing
- if (isTextInputFocused())
- {
- return false;
- }
-
- if (e.key === 'Backspace')
- {
- shortcuts.get(GamePadButtonCode.X)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.X }));
- } else if (e.key === ' ')
- {
- shortcuts.get(GamePadButtonCode.Y)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.Y }));
- }
+ shortcuts.get(GamePadButtonCode.X)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.X }));
+ } else if (e.key === ' ')
+ {
+ shortcuts.get(GamePadButtonCode.Y)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.Y }));
}
};
diff --git a/src/mainview/scripts/types.ts b/src/mainview/scripts/types.ts
index fbfcb36..e7630dc 100644
--- a/src/mainview/scripts/types.ts
+++ b/src/mainview/scripts/types.ts
@@ -1,5 +1,3 @@
-import { FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
-
export const FOCUS_KEYS = {
NAV_CATEGORIES: "NAV_CATEGORIES",
NAV_CATEGORY: (cat: string) => `NAV_CAT_${cat}`,
@@ -12,9 +10,5 @@ export const FOCUS_KEYS = {
EMULATOR_CARD: (id: string) => `EMULATOR_${id}`,
GAME_SECTION: "GAME_SECTION",
GAME_CARD: (id: FrontEndId) => `GAME_${id.source}_${id.id}`,
- GAME_LIST_CARD: (list: string, id: FrontEndId) => `LIST_${list}_GAME_${id.source}_${id.id}`,
- GAME_MATCH: (id: FrontEndId) => `GAME_${id.source}_${id.id}`,
STATS_SECTION: "STATS_SECTION",
- PLUGIN_ENTRY: (id: string) => `PLUGIN_${id}`,
- DOWNLOAD_ENTRY: (source: string, id: string) => `DOWNLOAD_${source}_${id}`
} as const;
\ No newline at end of file
diff --git a/src/mainview/scripts/utils.ts b/src/mainview/scripts/utils.ts
index 6635bd6..37b2da1 100644
--- a/src/mainview/scripts/utils.ts
+++ b/src/mainview/scripts/utils.ts
@@ -1,5 +1,5 @@
-import { LocalSettingsSchema, LocalSettingsType } from '@simeonradivoev/gameflow-sdk/shared';
-import { DependencyList, RefObject, useEffect, useRef, useState } from "react";
+import { LocalSettingsSchema, LocalSettingsType } from "@/shared/constants";
+import { DependencyList, FocusEventHandler, RefObject, useEffect, useRef, useState } from "react";
import { useLocalStorage } from "usehooks-ts";
import { jobsApi, systemApi } from "./clientApi";
import { JobsAPIType } from "@/bun/api/rpc";
@@ -13,12 +13,6 @@ export type ScrollSaveParams = {
storage?: "session" | "local";
shouldSave?: boolean;
};
-
-export function isTextInputFocused ()
-{
- return document.activeElement && document.activeElement instanceof HTMLInputElement;
-}
-
export function useScrollSave (data: ScrollSaveParams)
{
useEffect(() =>
@@ -378,7 +372,7 @@ export function useOnNavigateBack (callback: (state: { sound?: keyof typeof soun
}, [router]);
}
-export function showKeyboardHandler (activeControl: string | undefined, node?: HTMLInputElement)
+export function showKeyboardHandler (activeControl: string, node?: HTMLInputElement)
{
if (node && node.type !== 'checkbox' && (activeControl === 'gamepad' || activeControl === 'touch'))
{
diff --git a/src/mainview/scripts/windowEvents.ts b/src/mainview/scripts/windowEvents.ts
index 88dd926..c6f7ab8 100644
--- a/src/mainview/scripts/windowEvents.ts
+++ b/src/mainview/scripts/windowEvents.ts
@@ -2,7 +2,7 @@ import { settingsApi } from "./clientApi";
const handleResize = () =>
{
- settingsApi.api.settings.local({ id: 'windowSize' }).post({ value: { width: window.innerWidth, height: window.innerHeight } });
+ settingsApi.api.settings({ id: 'windowSize' }).post({ value: { width: window.innerWidth, height: window.innerHeight } });
};
window.addEventListener("resize", handleResize);
import.meta.hot?.dispose(() => window.removeEventListener('resize', handleResize));
@@ -13,7 +13,7 @@ var screenPositionInternal: NodeJS.Timeout = setInterval(() =>
{
if (lastWindowPosX != window.screenX || lastWindowPosY != window.screenY)
{
- settingsApi.api.settings.local({ id: 'windowPosition' }).post({ value: { x: window.screenX, y: window.screenY } });
+ settingsApi.api.settings({ id: 'windowPosition' }).post({ value: { x: window.screenX, y: window.screenY } });
}
lastWindowPosX = window.screenX;
diff --git a/src/mainview/types.d.ts b/src/mainview/types.d.ts
index 2a0c101..0ac95d6 100644
--- a/src/mainview/types.d.ts
+++ b/src/mainview/types.d.ts
@@ -46,17 +46,6 @@ declare interface FocusEventDetails
focusKeyChanged: boolean;
}
-declare interface GameMeta extends FocusParams
-{
- id: string,
- onSelect?: () => void,
- onQuickAction?: () => void,
- title: string,
- subtitle?: any,
- previewUrls?: string | URL[];
- previewSrcset?: string;
-};
-
declare interface FocusParams
{
onFocus?: (focusKey: string, node: HTMLElement, details: Record) => void;
diff --git a/src/packages/gameflow-sdk/README.md b/src/packages/gameflow-sdk/README.md
deleted file mode 100644
index 8338233..0000000
--- a/src/packages/gameflow-sdk/README.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Gameflow Deck SDK
-
-This is the type definitions for Gameflow Deck plugins.
-
-## Developing a plugin
-
-The plugin must have a default export class of type `PluginType`. It exposes the context and all the hooks to be tapped.
-Gameflow uses the [Tapable Hooks](https://github.com/webpack/tapable).
-
-The package must expose a main script gameflow will import and validate. It must implement the type fields on `PluginDescriptionType`.
-
-## Publishing
-
-For the plugin to show up in the UI for download. It must be published to NPM with the `gameflow-plugin` keyword. Gameflow uses bun to install plugins as packages from npmjs.
-Follow publishing instruction check the [NPM Docs](https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry)
-
-## Dependencies
-
-Peer dependencies will not be installed when the run adds the plugin package. They are provided by gameflow.
-All peer dependencies can be marked as external as gameflow provides it. There is a helper build script that does all that for you, to run it use.
-
-`bunx gameflow-build --entry=index.ts`
-
-supported arguments are
-`--entry` the entry of the app to build
-`--outdir` Where to build. Default is 'dist'
-`--minify` Minify the code. Default is 'false'
-`--sourcemap` Include a source map. Default is 'none'
-
-If you want to include dependencies that gameflow does not provide you have to bundle them in. Gameflow does not load dependencies for you.
diff --git a/src/packages/gameflow-sdk/build.ts b/src/packages/gameflow-sdk/build.ts
deleted file mode 100644
index 9f18b88..0000000
--- a/src/packages/gameflow-sdk/build.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bun
-
-import pkg from './package.json';
-
-import { parseArgs } from "util";
-
-const { values } = parseArgs({
- args: Bun.argv.slice(2),
- options: {
- outdir: { type: "string", default: "dist" },
- minify: { type: "boolean", default: false },
- sourcemap: { type: "string", default: "none" }, // "none" | "inline" | "external"
- entry: { type: "string", default: "src/index.ts" },
- },
- allowPositionals: true,
-});
-
-await Bun.build({
- entrypoints: [values.entry],
- outdir: values.outdir,
- minify: values.minify,
- sourcemap: values.sourcemap as any,
- external: [...Object.keys(pkg.peerDependencies), pkg.name],
- target: "bun",
-});
-
-console.log(`✅ Built to ${values.outdir}`);
\ No newline at end of file
diff --git a/src/packages/gameflow-sdk/hooks/app.ts b/src/packages/gameflow-sdk/hooks/app.ts
deleted file mode 100644
index 1b73daa..0000000
--- a/src/packages/gameflow-sdk/hooks/app.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { AsyncSeriesBailHook } from "tapable";
-import AuthHooks from "./auth";
-import EmulatorHooks from "./emulators";
-import GameHooks from "./games";
-import StoreHooks from "./store";
-import { DownloadFileEntry, ProgressStats } from "../shared";
-
-export class GameflowHooks
-{
- games = new GameHooks();
- emulators = new EmulatorHooks();
- auth = new AuthHooks();
- store = new StoreHooks();
- /** Download the given files and return their final paths. */
- downloadFiles = new AsyncSeriesBailHook<[ctx: {
- /** Unique ID of the download */
- id: string,
- /** The root download path. Each file has it's own download sub path */
- downloadPath: string,
- abortSignal?: AbortSignal,
- /** Authentication needed for download. Should be put in the headers. */
- auth?: string,
- /** The files to download */
- files: DownloadFileEntry[];
- /** Call it to update progress in the UI */
- updateProgress: (stats: ProgressStats) => void;
-
- }], {
- /** What downloaded the files. Will be passed to {@link postDownloadFiles} files hook. */
- source: string,
- /** The file paths ot the downloaded files. */
- files: string[];
- } | undefined>(['ctx']);
- /** Called after {@link downloadFiles} has finished downloading.
- * @returns The modified file paths.
- */
- postDownloadFiles = new AsyncSeriesBailHook<[ctx: {
- /** Who downloaded the files. Passed from the {@link downloadFiles} hook. */
- source: string;
- /** Can be directories or files */
- files: string[];
- /** The root downloads folder. */
- downloadPath: string,
- /** The sub path where the archive should be extracted to. This will be a sub path of `path_fs` */
- extract_path?: string;
- /** This is the parent path for the extracted files. */
- path_fs?: string;
- }], string[] | undefined>(['ctx']);
-}
\ No newline at end of file
diff --git a/src/packages/gameflow-sdk/index.ts b/src/packages/gameflow-sdk/index.ts
deleted file mode 100644
index c78c757..0000000
--- a/src/packages/gameflow-sdk/index.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import z from "zod";
-import { GameflowHooks } from "./hooks/app";
-import { EmulatorDownloadInfoSchema, EmulatorPackageSchema, FrontendNotification, SettingsType } from "./shared";
-import { $ZodRegistry } from "zod/v4/core";
-import Conf from "conf";
-import { EventEmitter } from 'node:events';
-import { TaskQueue } from "./task-queue";
-
-export * from "./hooks/app";
-export * from "./task-queue";
-
-export interface AppEventMap
-{
- exitapp: [];
- notification: [FrontendNotification];
- focus: [];
-}
-
-export const PluginContextSchema = z.object({
- hooks: z.instanceof(GameflowHooks)
-});
-
-export const PluginLoadingContextSchema = z.object({
- setProgress: z.function().input([z.number(), z.string()]).output(z.void()),
- config: z.instanceof(Conf).describe("Per plugin config. It will use the settings schema defined in the plugin class"),
- zodRegistry: z.instanceof($ZodRegistry).describe("Used by the settings to register metadata for the UI"),
- app: z.object({
- config: z.instanceof(Conf),
- events: z.instanceof(EventEmitter),
- taskQueue: z.instanceof(TaskQueue)
- })
-}).extend(PluginContextSchema.shape);
-
-export const PluginDescriptionSchema = z.object({
- name: z.string(),
- displayName: z.string().optional(),
- version: z.string(),
- description: z.string().optional(),
- icon: z.url().optional().describe("Can be an external URL to an image or a data url"),
- keywords: z.array(z.string()).optional(),
- peerDependencies: z.record(z.string(), z.string()).optional(),
- category: z.string().default("other"),
- main: z.string().describe("The main entry. It must export a default class implementing PluginType"),
- canDisable: z.boolean().default(true).optional().describe("Can the plugin be disabled or enabled by the user"),
- autoUpdate: z.boolean().optional().describe("Should the plugin auto update to latest version")
-});
-
-export const PluginSchema = z.object({
- load: z.function().input([PluginLoadingContextSchema]).output(z.promise(z.void())).describe("Called when the plugin is loaded or reloaded"),
- cleanup: z.function().output(z.promise(z.void())).optional().describe("Called when the plugin is unloaded or before it's reloaded"),
- settingsSchema: z.instanceof(z.ZodObject).optional().describe("The settings schema. Gameflow will show settings in the UI."),
- settingsMigrations: z.record(z.string(), z.function().input([z.instanceof(Conf)]).output(z.void())).optional(),
- eventsNames: z.object({
- id: z.string(),
- title: z.string().optional(),
- description: z.string().optional(),
- action: z.string()
- }).array().optional().describe("Events will be called when the user presses the button in plugin settings. Each event creates a button."),
- onEvent: z.function().input([z.string()]).output(z.object({
- openTab: z.string().optional(),
- reload: z.boolean().optional()
- }).or(z.record(z.string(), z.any()))).optional()
-});
-
-export const ActiveGameSchema = z.object({
- process: z.any().optional(),
- gameId: z.object({ id: z.string(), source: z.string() }),
- source: z.string().optional(),
- sourceId: z.string().optional(),
- name: z.string(),
- command: z.object({ command: z.string().or(z.string().array()), startDir: z.string().optional() })
-});
-
-export const EmulatorPostInstallContextSchema = z.object({
- emulator: z.string(),
- emulatorPackage: EmulatorPackageSchema.optional(),
- path: z.string(),
- update: z.boolean(),
- info: EmulatorDownloadInfoSchema,
-});
-
-export type ActiveGameType = z.infer;
-export type PluginDescriptionType = z.infer;
-export type PluginContextType = z.infer;
-export type PluginLoadingContextType = Record> = z.infer & {
- config: Conf;
-};
-export type PluginType = Record> = Omit, "load" | 'settingsMigrations'> & {
- load: (ctx: PluginLoadingContextType) => Promise;
- settingsMigrations?: Record) => void>;
-};
-export type EmulatorPostInstallContextType = z.infer;
-
diff --git a/src/packages/gameflow-sdk/package.json b/src/packages/gameflow-sdk/package.json
deleted file mode 100644
index 4e4ce28..0000000
--- a/src/packages/gameflow-sdk/package.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "name": "@simeonradivoev/gameflow-sdk",
- "version": "1.6.0",
- "types": "index.d.ts",
- "description": "plugin SDK for the Gameflow Deck Launcher",
- "exports": {
- ".": "./index.ts",
- "./shared": "./shared.ts"
- },
- "bin": {
- "gameflow-build": "build.ts"
- },
- "peerDependencies": {
- "7zip-bin": "^5.2.0",
- "@auth/core": "^0.34.3",
- "cheerio": "^1.2.0",
- "conf": "^15.1.0",
- "drizzle-orm": "^0.45.2",
- "elysia": "^1.4.28",
- "fs-extra": "^11.3.5",
- "get-folder-size": "^5.0.0",
- "ini": "^6.0.0",
- "jimp": "^1.6.1",
- "mustache": "^4.2.0",
- "node-7z": "^3.0.0",
- "node-disk-info": "^1.3.0",
- "node-downloader-helper": "^2.1.11",
- "node-stream-zip": "^1.15.0",
- "node-unrar-js": "^2.0.2",
- "open": "^11.0.0",
- "p-queue": "^9.2.0",
- "pathe": "^2.0.3",
- "slugify": "^1.6.9",
- "smol-toml": "^1.6.1",
- "tapable": "^2.3.3",
- "unzip-stream": "^0.3.4",
- "zod": "^4.4.3"
- },
- "keywords": [
- "gameflow",
- "sdk"
- ]
-}
\ No newline at end of file
diff --git a/src/packages/gameflow-sdk/sdk.tsconfig.json b/src/packages/gameflow-sdk/sdk.tsconfig.json
deleted file mode 100644
index 707544a..0000000
--- a/src/packages/gameflow-sdk/sdk.tsconfig.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2022",
- "useDefineForClassFields": true,
- "lib": [
- "ES2024"
- ],
- "module": "ESNext",
- "skipLibCheck": true,
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "emitDeclarationOnly": true,
- "declaration": true,
- "strict": true,
- "outDir": "../../dist-sdk",
- "types": [
- "node"
- ]
- }
-}
\ No newline at end of file
diff --git a/src/packages/gameflow-sdk/shared.ts b/src/packages/gameflow-sdk/shared.ts
deleted file mode 100644
index 96ddb01..0000000
--- a/src/packages/gameflow-sdk/shared.ts
+++ /dev/null
@@ -1,710 +0,0 @@
-import * as z from "zod";
-
-export const settingRegistry = z.registry<{
- dev?: boolean;
-}>();
-
-export const SettingsSchema = z.object({
- rommAddress: z.url().optional(),
- rommUser: z.string().default('admin').optional(),
- windowSize: z.object({ width: z.number(), height: z.number() }).optional(),
- windowPosition: z.object({ x: z.number(), y: z.number() }).optional(),
- downloadPath: z.string(),
- launchInFullscreen: z.boolean().default(true),
- disabledPlugins: z.array(z.string()).default([]),
- emulatorResolution: z.enum(['720p', '1080p', '1440p', '4k']).default('720p'),
- emulatorWidescreen: z.boolean().default(true)
-}); export const LocalSettingsSchema = z.object({
- backgroundBlur: z.boolean().default(true).meta({ title: "Background Blur" }),
- backgroundAnimation: z.boolean().default(true).meta({ title: "Background Animation" }),
- theme: z.enum(['dark', 'light', 'auto']).default('auto').meta({ title: "Theme" }),
- soundEffects: z.boolean().default(true).meta({ title: "Sounds" }),
- soundEffectsVolume: z.number().min(0).max(100).default(50).meta({ title: "Sound Volume" }),
- hapticsEffects: z.boolean().default(true).meta({ title: "Haptics" }),
- showRouterDevOptions: z.boolean().default(false).meta({ title: "Show Router Options" }).register(settingRegistry, { dev: true }),
- showQueryDevOptions: z.boolean().default(false).meta({ title: "Show Query Options" }).register(settingRegistry, { dev: true }),
- useGameflowKeyboard: z.boolean().default(true).describe("Show the gameflow on screen keyboard when using a controller").meta({ title: "Use Gameflow Keyboard" }),
- autoKeybaord: z.boolean().default(true).describe("Open on screen keybaord automatically").meta({ title: "Auto Keyboard" })
-});
-export const GameListFilterSchema = z.object({
- platform_source: z.string().optional(),
- platform_slug: z.string().optional(),
- platform_id: z.coerce.number().optional(),
- collection_id: z.coerce.number().optional(),
- collection_source: z.string().optional(),
- limit: z.coerce.number().optional(),
- search: z.string().optional(),
- offset: z.coerce.number().optional(),
- source: z.string().optional(),
- localOnly: z.coerce.boolean().optional(),
- orderBy: z.literal(['added', 'activity', 'name', 'release']).optional(),
- age_ratings: z.union([z.string().array(), z.string().transform(v => [v])]).optional(),
- genres: z.union([z.string().array(), z.string().transform(v => [v])]).optional(),
- keywords: z.union([z.string().array(), z.string().transform(v => [v])]).optional(),
-});
-export const DownloadSourceSchema = z.object({
- id: z.string(),
- name: z.string()
-});
-export const RommLoginDataSchema = z.object({ hostname: z.url(), username: z.string(), password: z.string() });
-export type GameListFilterType = z.infer;
-export const DirSchema = z.object({ name: z.string(), parentPath: z.string(), isDirectory: z.boolean() });
-export type DirType = z.infer;
-export const CustomEmulatorSchema = z.record(z.string(), z.string());
-export const GithubManifestSchema = z.object({
- sha: z.hash('sha1'),
- url: z.url(),
- tree: z.array(z.object({
- path: z.string(),
- mode: z.string(),
- type: z.enum(['blob', 'tree']),
- sha: z.hash('sha1'),
- url: z.url()
- }))
-});
-export const StoreGameSaveSchema = z.object({
- cwd: z.string(),
- globs: z.string().array()
-});
-export const StoreDownloadSchema = z.discriminatedUnion('type', [
- z.object({
- type: z.literal('direct'),
- url: z.url(),
- name: z.string().optional(),
- system: z.string(),
- main: z.string().optional(),
- saves: z.record(z.string(), StoreGameSaveSchema).optional()
- }),
- z.object({
- type: z.literal("itch"),
- path: z.string(),
- name: z.string().optional(),
- system: z.string(),
- saves: z.record(z.string(), StoreGameSaveSchema).optional()
- })
-]);
-export const NewGameSchema = z.object({
- name: z.string(),
- summary: z.string(),
- genres: z.string().regex(/^$|^(\s*\S[^,]*)(\s*,\s*\S[^,]*)*\s*$/, {
- message: "Must be a comma-separated list",
- })
-});
-export const StoreGameSchema = z.object({
- name: z.string(),
- description: z.string(),
- version: z.string(),
- homepage: z.string().optional(),
- keywords: z.string().array().optional(),
- genres: z.string().array().optional(),
- companies: z.string().array().optional(),
- screenshots: z.string().array().optional(),
- covers: z.string().array().optional(),
- igdb_id: z.number().optional(),
- ra_id: z.number().optional(),
- sgdb_id: z.number().optional(),
- first_release_date: z.union([z.number(), z.date()]).optional(),
- player_count: z.string().optional(),
- saves: z.record(z.string(), z.record(z.string(), StoreGameSaveSchema)).optional(),
- downloads: z.record(z.string(), StoreDownloadSchema)
-});
-export const EmulatorPackageSchema = z.object({
- name: z.string(),
- description: z.string(),
- homepage: z.url(),
- logo: z.url(),
- type: z.enum(['emulator']),
- os: z.array(z.enum(['darwin', 'linux', 'win32', 'android'])),
- keywords: z.array(z.string()).optional(),
- downloads: z.record(z.string(), z.array(z.discriminatedUnion('type', [
- z.object({
- type: z.literal(['github', 'gitlab']),
- pattern: z.string(),
- path: z.string(),
- bin: z.string().optional()
- }),
- z.object({
- type: z.literal('direct'),
- url: z.url(),
- bin: z.string().optional()
- }),
- z.object({
- type: z.literal('scoop'),
- url: z.url(),
- bin: z.string().optional()
- })
- ]))).optional(),
- systems: z.array(z.string()),
- bios: z.literal(["required", "optional"]).optional()
-});
-export const ScoopPackageSchema = z.object({
- version: z.string(),
- url: z.url().optional(),
- description: z.string(),
- bin: z.string().optional(),
- architecture: z.record(z.string(), z.object({
- url: z.url(),
- hash: z.string().optional(),
- extract_dir: z.string().optional()
- })).optional()
-});
-export const SystemInfoSchema = z.object({
- battery: z.object({
- percent: z.number(),
- isCharging: z.boolean(),
- acConnected: z.boolean(),
- hasBattery: z.boolean()
- }),
- wifiConnections: z.array(z.object({ signalLevel: z.number() })),
- bluetoothDevices: z.array(z.object({ connected: z.boolean() }))
-});
-export const GithubReleaseSchema = z.object({
- id: z.number(),
- tag_name: z.string().optional(),
- url: z.url(),
- body: z.string(),
- assets: z.array(z.object({
- name: z.string(),
- browser_download_url: z.url(),
- content_type: z.string().optional()
- }))
-});
-export const EmulatorDownloadInfoSchema = z.object({
- id: z.string(),
- version: z.string().optional(),
- url: z.url().optional(),
- description: z.string().optional(),
- downloadDate: z.coerce.date(),
- type: z.string()
-});
-export const PluginEntrySchema = z.object({
- downloads: z.object({
- monthly: z.number(),
- weekly: z.number()
- }),
- searchScore: z.number(),
- installed: z.boolean(),
- update: z.object({ from: z.string() }).optional(),
- package: z.object({
- name: z.string(),
- keywords: z.string().array(),
- version: z.string(),
- description: z.string().optional(),
- sanitized_name: z.string(),
- license: z.string().optional(),
- publisher: z.object({
- email: z.string(),
- username: z.string(),
- trustedPublisher: z.object({
- id: z.string(),
- oidcConfigId: z.string()
- }).optional()
- }),
- date: z.coerce.date(),
- links: z.object({
- homepage: z.string().optional(),
- repository: z.string().optional(),
- bugs: z.string().optional(),
- npm: z.url()
- })
- })
-});
-export const PluginBunDetailsSchema = z.object({
- name: z.string(),
- keywords: z.string().array(),
- version: z.string(),
- author: z.object({ name: z.string().optional() }).optional(),
- license: z.string().optional(),
- devDependencies: z.record(z.string(), z.string()).optional(),
- dependencies: z.record(z.string(), z.string()).optional(),
- maintainers: z.object({ name: z.string() }).array().optional(),
- dist: z.object({ unpackedSize: z.number() }),
- description: z.string().optional(),
- _npmUser: z.object({ name: z.string() }).optional()
-});
-export type EmulatorPackageType = z.infer;
-export type StoreGameType = z.infer;
-export type StoreDownloadType = z.infer;
-export type SettingsType = z.infer;
-export type LocalSettingsType = z.infer;
-export const PlatformSchema = z.object({ slug: z.string() });
-export type SystemInfoType = z.infer;
-export type EmulatorDownloadInfoType = z.infer;
-export type DownloadSourceType = z.infer;
-export type PluginEntryType = z.infer;
-export type PluginBunDetailsType = z.infer;
-
-export interface SaveFileChange
-{
- subPath: string | string[];
- isGlob?: true;
- cwd: string;
- shared: boolean;
- fixedSize?: boolean;
-}
-
-export type EmulatorSourceType = 'custom' | 'store' | 'registry' | 'system' | 'static' | 'embedded';
-
-export interface EmulatorSourceEntryType
-{
- binPath: string;
- rootPath?: string;
- type: EmulatorSourceType;
- /** Does the emulator exist in the file system */
- exists: boolean;
-}
-
-export interface FrontEndEmulator
-{
- name: string;
- source: string;
- logo: string;
- systems: EmulatorSystem[];
- description?: string;
- gameCount: number;
- validSources: EmulatorSourceEntryType[];
- integrations: EmulatorSupport[];
-}
-
-export interface EmulatorSystem { id: string, romm_slug?: string, name: string, iconUrl: string; }
-
-export interface FrontEndEmulatorDetailedDownload
-{
- name: string;
- type: string | undefined;
- version?: string;
-}
-
-export interface FrontEndEmulatorDetailed extends FrontEndEmulator
-{
- homepage: string;
- description: string;
- downloads: FrontEndEmulatorDetailedDownload[];
- keywords?: string[];
- screenshots: string[];
- biosRequirement?: "required" | "optional";
- bios?: string[];
- storeDownloadInfo?: { hasUpdate: boolean; version?: string, type: string; description?: string; };
-}
-
-export interface FrontEndGameTypeDetailedAchievement
-{
- id: string;
- title: string;
- description?: string;
- date?: Date;
- date_hardcode?: Date;
- badge_url?: string;
- display_order: number;
- type?: string;
-}
-
-export interface FrontEndGameTypeDetailedEmulator extends FrontEndEmulator
-{
-
-}
-
-export interface FrontEndGameTypeDetailed extends Exclude
-{
- summary: string | null;
- fs_size_bytes: number | null;
- missing: boolean;
- local: boolean;
- version?: string | null;
- version_system?: string | null;
- version_source?: string | null;
- metadata: FrontEndGameMetadataDetailed,
- emulators?: FrontEndGameTypeDetailedEmulator[],
- achievements?: {
- unlocked: number;
- total: number;
- entires: FrontEndGameTypeDetailedAchievement[];
- };
-};
-
-export interface Drive
-{
- parent: string | null;
- device: string;
- label: string;
- mountPoint: string | null;
- type: string;
- size: number;
- used: number;
- isRemovable: boolean;
- interfaceType: string | null;
- hasWriteAccess: boolean;
- hasReadAccess: boolean;
-}
-
-export interface DownloadsDrive
-{
- device: string;
- label: string;
- mountPoint: string | null;
- isRemovable: boolean;
- size: number;
- used: number;
- isCurrentlyUsed: boolean;
- unusableReason: 'not_enough_space' | 'already_used' | null;
-}
-
-export interface FrontendNotification
-{
- title?: string;
- message: string;
- type: 'success' | 'error' | 'info' | 'custom';
- icon?: "save" | "upload" | "clock";
- duration?: number;
-}
-
-export interface CommandEntry
-{
- /** The ID of the command. Could be just an index or a string */
- id: string | number;
- /** The front end label for the command. Mainly gotten from ES-DE list */
- label?: string;
- /** Compiled command to be executed */
- command: string | string[];
- /** Environment variables */
- env?: Record,
- /** The path the spawned process will start at */
- startDir?: string;
- /** Is the command valid, for example does the executable exists */
- valid: boolean;
- /** Run the command as shell. Defaults is true */
- shell?: boolean;
- /** For what emulator is the command */
- emulator?: string;
- /** Where the emulator came from */
- emulatorSource?: EmulatorSourceType;
- /** Metadata for the command */
- metadata: {
- romPath?: string;
- emulatorBin?: string;
- /** The root directory of the emulator */
- emulatorDir?: string;
- };
-}
-
-export interface FrontEndId
-{
- id: string;
- source: string;
-}
-
-// Stuff stored in the local sqlite metadata field
-export interface LocalGameMetadata
-{
- genres?: string[],
- companies?: string[],
- game_modes?: string[],
- age_ratings?: string[];
- player_count?: string;
- first_release_date?: number;
- average_rating?: number;
-}
-
-export interface FrontEndPlatformType
-{
- id: FrontEndId;
- slug: string;
- name: string;
- family_name?: string | null;
- path_cover: string | null;
- game_count: number;
- updated_at: Date;
- hasLocal: boolean;
- paths_screenshots: string[];
-}
-
-export interface FrontEndGameTypeWithIds extends FrontEndGameType
-{
- igdb_id: number | null;
- ra_id: number | null;
-}
-
-export interface FrontEndFilterSets
-{
- age_ratings: Set,
- player_counts: Set,
- languages: Set,
- companies: Set,
- genres: Set;
-}
-
-export interface FrontEndFilterLists
-{
- age_ratings: string[],
- player_counts: string[],
- languages: string[],
- companies: string[],
- genres: string[];
-}
-
-export interface FrontEndGameMetadata
-{
- first_release_date: Date | null;
-}
-
-export interface FrontEndGameMetadataDetailed extends FrontEndGameMetadata
-{
- genres: string[],
- companies: string[],
- game_modes: string[],
- age_ratings: string[];
- player_count: string | null;
- average_rating: number | null;
-}
-
-export interface FrontEndGameType
-{
- platform_display_name: string | null,
- path_platform_cover: string | null;
- id: FrontEndId,
- source: string | null,
- source_id: string | null,
- path_fs: string | null,
- path_covers: string[],
- last_played: Date | null,
- updated_at: Date,
- metadata: FrontEndGameMetadata,
- slug: string | null,
- name: string | null,
- platform_id: number | null,
- platform_slug: string | null,
- paths_screenshots: string[];
-};
-
-export type GameStatusType = 'installed' | 'missing-emulator' | 'error' | 'install' | 'download' | 'extract' | 'playing' | 'queued';
-
-export interface GameInstallProgress
-{
- progress?: number;
- status?: GameStatusType;
- details?: string;
- commands?: CommandEntry[];
- error?: any;
-}
-
-export type JobStatus = 'completed' | 'error' | 'running' | 'queued' | 'aborted';
-export type GameInstallProgressEvent = 'refresh';
-
-export interface FrontEndJob
-{
- id: string;
- data: any;
- progress: number;
- state?: string;
- status: string;
-}
-
-export interface FrontendPlugin
-{
- name: string;
- displayName?: string;
- description?: string;
- category: string;
- enabled: boolean;
- canDisable: boolean;
- canUninstall: boolean;
- source: PluginSourceType;
- hasSettings: boolean;
- version: string;
- icon?: string;
- update?: PluginUpdateCheck;
-}
-
-export interface PluginUpdateCheck
-{
- current: string;
- new: string;
-}
-
-export type PluginSourceType = "builtin" | "store";
-
-export type KeysWithValueAssignableTo = {
- [K in keyof T]: Exclude extends Value ? K : never;
-}[keyof T];
-
-export interface DownloadInfo
-{
- id: string;
- screenshotUrls: string[];
- coverUrl: string;
- platform?: DownloadPlatform;
- slug?: string;
- path_fs?: string;
- main_glob?: string;
- summary?: string;
- name: string;
- last_played?: Date;
- igdb_id?: number;
- ra_id?: number;
- source_id: string;
- system_slug: string;
- extract_path?: string;
- metadata?: any;
- files: DownloadFileEntry[];
- auth?: string;
- version?: string;
- version_source?: string;
- version_system?: string;
-}
-
-export interface DownloadPlatform
-{
- id: string;
- source: string;
- igdb_id?: number;
- igdb_slug?: string;
- ra_id?: number;
- moby_id?: number;
- slug: string;
- name: string;
- /** Like Sony or Nintendo */
- family_name?: string;
-}
-
-export interface DownloadFileEntry
-{
- url: URL;
- /** The path of the file, excluding the name */
- file_path: string;
- /** Just the name of the file including the extension */
- file_name: string;
- /** Checksum of the file */
- sha1?: string;
- /** Size in bytes */
- size?: number;
-}
-
-export interface LocalDownloadFileEntry extends DownloadFileEntry
-{
- /** Exists on the file system */
- exists: boolean;
- /** Matches the checksum */
- matches: boolean;
-}
-
-export interface FrontEndCollection
-{
- id: FrontEndId;
- name: string;
- description: string;
- path_platform_cover: string | null;
- game_count: number;
-}
-
-export type EmulatorCapabilities = "saves" | "fullscreen" | "resolution" | "batch" | "states" | "config";
-
-export interface EmulatorSupport
-{
- id: string;
- source?: EmulatorSourceEntryType;
- supportLevel?: "partial" | "full";
- capabilities?: EmulatorCapabilities[];
-}
-
-export interface GameLookup
-{
- source: string;
- id: string;
- coverUrl: string | null | undefined;
- slug: string | null | undefined;
- screenshotUrls: string[];
- name: string;
- summary: string | null | undefined;
- genres: string[];
- companies: string[];
- game_modes: string[];
- age_ratings: string[];
- player_count: string | undefined;
- first_release_date: number | undefined;
- average_rating: number | undefined;
- keywords: string[];
- igdb_id: number | undefined;
- platforms: {
- id: number;
- name?: string | null;
- displayName: string;
- slug: string;
- }[];
-}
-
-export interface DownloadLookupEntry
-{
- source: string;
- id: string;
- cover_url: string | null | undefined;
- name: string;
- summary: string | null | undefined;
- size: number | null | undefined;
- date: Date | null | undefined;
- rating: number | null | undefined;
- view_count: number | null | undefined;
- download_count: number | null | undefined;
- comment_count: number | null | undefined;
-}
-
-export interface DownloadLookupDetailsFile
-{
- id: string;
- format: string | null | undefined;
- mtime: Date | null | undefined;
- size: number | null | undefined;
- download_url: string;
-}
-
-export interface DownloadLookupDetails
-{
- source: string;
- id: string;
- cover_url: string | null | undefined;
- name: string;
- summary: string | null | undefined;
- date: Date | null | undefined;
- files: DownloadLookupDetailsFile[];
-}
-
-export interface AutoSaveChange
-{
- subPath: string;
- cwd: string;
-}
-
-export interface AppInfoContext
-{
- activeTaskProgress: number | null;
-}
-
-export type SaveSlots = Record;
-
-/** Jobs that are downloading stuff can implement this data interface to show up in the downloads screen */
-export interface DownloadJobData extends Partial>
-{
- preview_url?: string | null;
- name?: string;
-}
-
-export interface ProgressStats
-{
- progress: number;
- speed: number;
- total: number;
- downloaded: number;
-}
-
-export interface DownloadsLookupFilter
-{
- source?: string,
- orderBy?: string,
- search?: string;
- sortDirection?: "desc" | "asc";
-}
-
-export interface DownloadsLookupFilterValues
-{
- orderBy: string[],
- source: string[];
-}
\ No newline at end of file
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
index 3c9d776..e21bb54 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.ts
@@ -1,3 +1,8 @@
+
+
+import { JSX } from 'react';
+import * as z from 'zod';
+
export const LOGIN_PORT = 5196;
export const OAUTH_REDIRECT_PORT = 5194;
export const SERVER_PORT = 5173;
@@ -10,4 +15,205 @@ export const EMULATORJS_URL = (host: string) => `http://${host}:${EMULATORJS_POR
export const SOCKETS_URL = (host: string) => `ws://${host}:${RPC_PORT}`;
export const DefaultRommStaleTime = 60 * 1000; // A minute
-export const PluginRegistry = process.env.STORE_REGISTRY ?? "https://registry.npmjs.org";
\ No newline at end of file
+export interface GameMeta extends FocusParams
+{
+ id: string,
+ onSelect?: () => void,
+ title: string,
+ subtitle?: string | JSX.Element,
+ previewUrls?: string | URL[];
+ previewSrcset?: string;
+};
+
+export const SettingsSchema = z.object({
+ rommAddress: z.url().optional(),
+ rommUser: z.string().default('admin').optional(),
+ windowSize: z.object({ width: z.number(), height: z.number() }).optional(),
+ windowPosition: z.object({ x: z.number(), y: z.number() }).optional(),
+ downloadPath: z.string(),
+ launchInFullscreen: z.boolean().default(true),
+ disabledPlugins: z.array(z.string()).default([]),
+ emulatorResolution: z.enum(['720p', '1080p', '1440p', '4k']).default('720p'),
+ emulatorWidescreen: z.boolean().default(true)
+});
+
+export const LocalSettingsSchema = z.object({
+ backgroundBlur: z.stringbool().or(z.boolean()).default(true),
+ backgroundAnimation: z.stringbool().or(z.boolean()).default(true),
+ theme: z.enum(['dark', 'light', 'auto']).default('auto'),
+ soundEffects: z.boolean().default(true),
+ soundEffectsVolume: z.number().min(0).max(100).default(50),
+ hapticsEffects: z.boolean().default(true),
+ showRouterDevOptions: z.boolean().default(false),
+ showQueryDevOptions: z.boolean().default(false),
+});
+
+export const GameListFilterSchema = z.object({
+ platform_source: z.string().optional(),
+ platform_slug: z.string().optional(),
+ platform_id: z.coerce.number().optional(),
+ collection_id: z.coerce.number().optional(),
+ collection_source: z.string().optional(),
+ limit: z.coerce.number().optional(),
+ search: z.string().optional(),
+ offset: z.coerce.number().optional(),
+ source: z.string().optional(),
+ localOnly: z.coerce.boolean().optional(),
+ orderBy: z.literal(['added', 'activity', 'name', 'release']).optional(),
+ age_ratings: z.union([z.string().array(), z.string().transform(v => [v])]).optional(),
+ genres: z.union([z.string().array(), z.string().transform(v => [v])]).optional(),
+ keywords: z.union([z.string().array(), z.string().transform(v => [v])]).optional(),
+});
+
+export const DownloadSourceSchema = z.object({
+ id: z.string(),
+ name: z.string()
+});
+
+export const RommLoginDataSchema = z.object({ hostname: z.url(), username: z.string(), password: z.string() });
+
+export type GameListFilterType = z.infer;
+
+export const DirSchema = z.object({ name: z.string(), parentPath: z.string(), isDirectory: z.boolean() });
+export type DirType = z.infer;
+
+export const CustomEmulatorSchema = z.record(z.string(), z.string());
+
+export const GithubManifestSchema = z.object({
+ sha: z.hash('sha1'),
+ url: z.url(),
+ tree: z.array(z.object({
+ path: z.string(),
+ mode: z.string(),
+ type: z.enum(['blob', 'tree']),
+ sha: z.hash('sha1'),
+ url: z.url()
+ }))
+});
+
+export const StoreGameSaveSchema = z.object({
+ cwd: z.string(),
+ globs: z.string().array()
+});
+
+export const StoreDownloadSchema = z.discriminatedUnion('type', [
+ z.object({
+ type: z.literal('direct'),
+ url: z.url(),
+ name: z.string().optional(),
+ system: z.string(),
+ main: z.string().optional(),
+ saves: z.record(z.string(), StoreGameSaveSchema).optional()
+ }),
+ z.object({
+ type: z.literal("itch"),
+ path: z.string(),
+ name: z.string().optional(),
+ system: z.string(),
+ saves: z.record(z.string(), StoreGameSaveSchema).optional()
+ })
+]);
+
+export const StoreGameSchema = z.object({
+ name: z.string(),
+ description: z.string(),
+ version: z.string(),
+ homepage: z.string().optional(),
+ keywords: z.string().array().optional(),
+ genres: z.string().array().optional(),
+ companies: z.string().array().optional(),
+ screenshots: z.string().array().optional(),
+ covers: z.string().array().optional(),
+ igdb_id: z.number().optional(),
+ ra_id: z.number().optional(),
+ sgdb_id: z.number().optional(),
+ first_release_date: z.union([z.number(), z.date()]).optional(),
+ player_count: z.string().optional(),
+ saves: z.record(z.string(), z.record(z.string(), StoreGameSaveSchema)).optional(),
+ downloads: z.record(z.string(), StoreDownloadSchema)
+});
+
+export const EmulatorPackageSchema = z.object({
+ name: z.string(),
+ description: z.string(),
+ homepage: z.url(),
+ logo: z.url(),
+ type: z.enum(['emulator']),
+ os: z.array(z.enum(['darwin', 'linux', 'win32', 'android'])),
+ keywords: z.array(z.string()).optional(),
+ downloads: z.record(z.string(), z.array(z.discriminatedUnion('type', [
+ z.object({
+ type: z.literal(['github', 'gitlab']),
+ pattern: z.string(),
+ path: z.string(),
+ bin: z.string().optional()
+ }),
+ z.object({
+ type: z.literal('direct'),
+ url: z.url(),
+ bin: z.string().optional()
+ }),
+ z.object({
+ type: z.literal('scoop'),
+ url: z.url(),
+ bin: z.string().optional()
+ })
+ ]))).optional(),
+ systems: z.array(z.string()),
+ bios: z.literal(["required", "optional"]).optional()
+});
+
+export const ScoopPackageSchema = z.object({
+ version: z.string(),
+ url: z.url().optional(),
+ description: z.string(),
+ bin: z.string().optional(),
+ architecture: z.record(z.string(), z.object({
+ url: z.url(),
+ hash: z.string().optional(),
+ extract_dir: z.string().optional()
+ })).optional()
+});
+
+export const SystemInfoSchema = z.object({
+ battery: z.object({
+ percent: z.number(),
+ isCharging: z.boolean(),
+ acConnected: z.boolean(),
+ hasBattery: z.boolean()
+
+ }),
+ wifiConnections: z.array(z.object({ signalLevel: z.number() })),
+ bluetoothDevices: z.array(z.object({ connected: z.boolean() }))
+});
+
+export const GithubReleaseSchema = z.object({
+ id: z.number(),
+ tag_name: z.string().optional(),
+ url: z.url(),
+ body: z.string(),
+ assets: z.array(z.object({
+ name: z.string(),
+ browser_download_url: z.url(),
+ content_type: z.string().optional()
+ }))
+});
+
+export const EmulatorDownloadInfoSchema = z.object({
+ id: z.string(),
+ version: z.string().optional(),
+ url: z.url().optional(),
+ description: z.string().optional(),
+ downloadDate: z.coerce.date(),
+ type: z.string()
+});
+
+export type EmulatorPackageType = z.infer;
+export type StoreGameType = z.infer;
+export type StoreDownloadType = z.infer