feat: Implemented public plugin system accessible from the store.
feat: Implemented external ryujinx integration plugin refactor: moved sdk types and schemas to own workspace package fix: Fixed emulator launch with no game
This commit is contained in:
parent
9051834ace
commit
38cb752552
124 changed files with 1918 additions and 1067 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { SystemInfoContext } from "../scripts/contexts";
|
||||
import { systemApi } from "../scripts/clientApi";
|
||||
import { SystemInfoType } from "@/shared/constants";
|
||||
import { SystemInfoType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import LoadingScreen from "./LoadingScreen";
|
||||
import { GamepadKeyboard } from "./GamepadKeyboard";
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { JSX, Suspense } from 'react';
|
|||
import { FloatingShortcuts } from './Shortcuts';
|
||||
import { AutoFocus } from './AutoFocus';
|
||||
import { GamePadButtonCode, useShortcuts } from '../scripts/shortcuts';
|
||||
import { GameListFilterType } from '@/shared/constants';
|
||||
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { HandleGoBack } from '../scripts/utils';
|
||||
import LoadingCardList from './LoadingCardList';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { FocusEventHandler, useContext, useRef, useState } from "react";
|
|||
import path from "pathe";
|
||||
import { Check, File, FileInput, Folder, FolderInput, FolderOutput, FolderPlus, HardDrive, Usb, X } from "lucide-react";
|
||||
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { DirType } from "@/shared/constants";
|
||||
import { DirType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import classNames from "classnames";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { FileQuestion, HardDrive, Store } from "lucide-react";
|
|||
import { JSX } from "react";
|
||||
import { FOCUS_KEYS } from "../scripts/types";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { FrontEndGameType, FrontEndId } from "@/shared/types";
|
||||
import { FrontEndGameType, FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export default function FrontEndGameCard (data: { index: number, game: FrontEndGameType; showSource?: boolean; } & FocusParams & InteractParams)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { GameMetaExtra, CardList } from "./CardList";
|
||||
import { DefaultRommStaleTime, GameListFilterType, RPC_URL } from "@shared/constants";
|
||||
import { DefaultRommStaleTime, RPC_URL } from "@shared/constants";
|
||||
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { HardDrive } from "lucide-react";
|
||||
import { JSX, useContext } from "react";
|
||||
import { useLocalSetting } from "../scripts/utils";
|
||||
import { AnimatedBackgroundContext } from "../scripts/contexts";
|
||||
import { allGamesQuery } from "@queries/romm";
|
||||
import { FrontEndGameType, FrontEndId } from "@/shared/types";
|
||||
import { FrontEndGameType, FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export interface GameListParams extends FocusParams
|
||||
{
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ function buildWheel (side: 0 | 1, shift: boolean, characters: boolean)
|
|||
const elements: JSX.Element[] = [];
|
||||
const refs: RefObject<HTMLSpanElement | null>[] = [];
|
||||
const positions: { left: string; top: string; }[] = [];
|
||||
const W = 258, C = 129, R2 = 107, R1 = 42, n = GetKeys(characters)[side].length, GAP = 0.028;
|
||||
const n = GetKeys(characters)[side].length, GAP = 0.028;
|
||||
|
||||
for (let i = 0; i < n; i++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { oneShot } from "../scripts/audio/audio";
|
|||
import { Search } from "lucide-react";
|
||||
import { RoundButton } from "./RoundButton";
|
||||
import { useEventListener } from "usehooks-ts";
|
||||
import useActiveControl from "../scripts/gamepads";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
function SearchInput (data: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { FOCUS_KEYS } from "../scripts/types";
|
||||
import { useIntersectionObserver } from "usehooks-ts";
|
||||
import { FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export default function LoadMoreButton (data: { isFetching: boolean; hidden?: boolean, lastId?: FrontEndId; } & FocusParams & InteractParams)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { RPC_URL } from "@/shared/constants";
|
||||
import { FrontendNotification } from "@/shared/types";
|
||||
import { FrontendNotification } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
import { Clock, CloudUpload, Save } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import toast, { ToastOptions } from "react-hot-toast";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GameListFilterType } from "@/shared/constants";
|
||||
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { RoundButton } from "./RoundButton";
|
||||
import classNames from "classnames";
|
||||
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
|
||||
|
|
@ -6,7 +6,7 @@ import { useFocusable, FocusContext } from "@noriginmedia/norigin-spatial-naviga
|
|||
import { ArrowDownAz, ClockArrowDown, CalendarArrowDown, Rocket, HardDrive, SortDesc, User, Drama, FunnelX, Store } from "lucide-react";
|
||||
import { sourceIconMap } from "./Constants";
|
||||
import { useContextDialog, ContextList, DialogEntry } from "./ContextDialog";
|
||||
import { FrontEndFilterLists } from "@/shared/types";
|
||||
import { FrontEndFilterLists } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
function FilterButton (data: {
|
||||
id: string,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { FrontEndGameTypeDetailed, FrontEndGameTypeDetailedAchievement } from "@/shared/types";
|
||||
import { FrontEndGameTypeDetailed, FrontEndGameTypeDetailedAchievement } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { Medal } from "lucide-react";
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import ActionButton from "./ActionButton";
|
|||
import { useLocalStorage } from "usehooks-ts";
|
||||
import FocusTooltip from "../FocusTooltip";
|
||||
import { useBlocker, useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { FrontEndGameTypeDetailed } from "@/shared/types";
|
||||
import { FrontEndGameTypeDetailed } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
function AchievementsInfo (data: { game: FrontEndGameTypeDetailed; } & InteractParams)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import prettyMilliseconds from 'pretty-ms';
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { validateSourceQuery } from "@/mainview/scripts/queries/romm";
|
||||
import { sourceIconMap } from "../Constants";
|
||||
import { FrontEndGameTypeDetailed } from "@/shared/types";
|
||||
import { FrontEndGameTypeDetailed } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export function DetailElement (data: { icon: JSX.Element; tooltip?: string | null, children?: any | any[]; })
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import HeaderSearchField from "../HeaderSearchField";
|
|||
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { scrollIntoViewHandler } from "@/mainview/scripts/utils";
|
||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import { FrontEndId, GameLookup } from "@/shared/types";
|
||||
import { FrontEndId, GameLookup } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
import { gameLookupQuery } from "@/mainview/scripts/queries/romm";
|
||||
import { Button } from "../options/Button";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ import { Clock, Crosshair, Download, EllipsisVertical, Import, PackageOpen, Play
|
|||
import { gameInvalidationQuery, installMutation, playMutation } from "@/mainview/scripts/queries/romm";
|
||||
import ActionButton from "./ActionButton";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { DownloadSourceType } from "@/shared/constants";
|
||||
import { GamePadButtonCode, Shortcut, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { CommandEntry, FrontEndGameTypeDetailed } from "@/shared/types";
|
||||
import { CommandEntry, FrontEndGameTypeDetailed, DownloadSourceType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export default function MainActions (data: { game?: FrontEndGameTypeDetailed, source: string, id: string; })
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import { useState } from "react";
|
|||
import { PathSettingsOptionBase, PathSettingsOptionParams } from "./PathSettingsOption";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { changeDownloadsMutation, getSettingQuery } from "@queries/settings";
|
||||
import { SettingsType } from "@/shared/constants";
|
||||
import { KeysWithValueAssignableTo } from "@/shared/types";
|
||||
import { KeysWithValueAssignableTo, SettingsType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export default function DownloadDirectoryOption (data: PathSettingsOptionParams & { id: KeysWithValueAssignableTo<SettingsType, string>; })
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { JSX } from "react";
|
||||
import { LocalSettingsSchema, LocalSettingsType } from "@shared/constants";
|
||||
import { LocalSettingsSchema, LocalSettingsType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { OptionSpace } from "./OptionSpace";
|
||||
import { OptionInput } from "./OptionInput";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { HTMLInputTypeAttribute, JSX, useEffect, useState } from "react";
|
||||
import { SettingsType } from "../../../shared/constants";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { OptionSpace } from "./OptionSpace";
|
||||
import { OptionInput } from "./OptionInput";
|
||||
|
|
@ -9,7 +8,7 @@ import { ContextDialog } from "../ContextDialog";
|
|||
import FilePicker from "../FilePicker";
|
||||
import { setFocus } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { getSettingQuery, setSettingMutation } from "@queries/settings";
|
||||
import { KeysWithValueAssignableTo } from "@/shared/types";
|
||||
import { KeysWithValueAssignableTo, SettingsType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export interface PathSettingsOptionParams
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { JSX, useCallback, useEffect, useState } from "react";
|
||||
import { SettingsType } from "../../../shared/constants";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { OptionSpace } from "./OptionSpace";
|
||||
import { getSettingQuery, setSettingMutation } from "@queries/settings";
|
||||
import { OptionDropdown } from "./OptionDropdown";
|
||||
import { KeysWithValueAssignableTo } from "@/shared/types";
|
||||
import { KeysWithValueAssignableTo, SettingsType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export function SettingsDropdown (data: {
|
||||
label: string;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { HTMLInputTypeAttribute, JSX, useCallback, useEffect, useState } from "react";
|
||||
import { SettingsType } from "../../../shared/constants";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { OptionSpace } from "./OptionSpace";
|
||||
import { OptionInput } from "./OptionInput";
|
||||
import { getSettingQuery, setSettingMutation } from "@queries/settings";
|
||||
import { KeysWithValueAssignableTo } from "@/shared/types";
|
||||
import { KeysWithValueAssignableTo, SettingsType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export function SettingsOption (data: {
|
||||
label: string;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { StoreEmulatorCard } from "./StoreEmulatorCard";
|
|||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import Carousel from "../Carousel";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { FrontEndEmulator } from "@/shared/types";
|
||||
import { FrontEndEmulator } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
function SeeAllCard (data: { id: string; onAction: () => void; onFocus?: (details: { node: HTMLElement, instant?: boolean; }) => void; })
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import FrontEndGameCard from "../FrontEndGameCard";
|
|||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import Carousel from "../Carousel";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { FrontEndGameType, FrontEndId } from "@/shared/types";
|
||||
import { FrontEndGameType, FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export function GamesSection (data: {
|
||||
games?: FrontEndGameType[];
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
|||
import { RPC_URL } from "@/shared/constants";
|
||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
import { FrontEndEmulator } from "@/shared/types";
|
||||
import { FrontEndEmulator } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
// ── Single missing-emulator card ───────────────────────────────────────────
|
||||
interface MissingCardProps
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { JSX } from "react";
|
|||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getUpdateInfoForEmulator } from "@/mainview/scripts/queries/store";
|
||||
import { FrontEndEmulator } from "@/shared/types";
|
||||
import { FrontEndEmulator } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export const emulatorStatusIcons: Record<string, JSX.Element> = {
|
||||
store: <Store />,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { Route as SettingsAboutRouteImport } from './../routes/settings/about'
|
|||
import { Route as GameAddRouteImport } from './../routes/game/add'
|
||||
import { Route as StoreTabRouteRouteImport } from './../routes/store/tab/route'
|
||||
import { Route as StoreTabIndexRouteImport } from './../routes/store/tab/index'
|
||||
import { Route as StoreTabPluginsRouteImport } from './../routes/store/tab/plugins'
|
||||
import { Route as StoreTabGamesRouteImport } from './../routes/store/tab/games'
|
||||
import { Route as StoreTabEmulatorsRouteImport } from './../routes/store/tab/emulators'
|
||||
import { Route as SettingsPluginSourceRouteImport } from './../routes/settings/plugin.$source'
|
||||
|
|
@ -30,6 +31,7 @@ import { Route as LauncherSourceIdRouteImport } from './../routes/launcher.$sour
|
|||
import { Route as GameSourceIdRouteImport } from './../routes/game/$source.$id'
|
||||
import { Route as EmbeddedSourceIdRouteImport } from './../routes/embedded.$source.$id'
|
||||
import { Route as CollectionSourceIdRouteImport } from './../routes/collection.$source.$id'
|
||||
import { Route as StoreDetailsPluginIdRouteImport } from './../routes/store/details.plugin.$id'
|
||||
import { Route as StoreDetailsEmulatorIdRouteImport } from './../routes/store/details.emulator.$id'
|
||||
import { Route as GameUpdateSourceIdRouteImport } from './../routes/game/update.$source.$id'
|
||||
|
||||
|
|
@ -98,6 +100,11 @@ 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',
|
||||
|
|
@ -138,6 +145,11 @@ 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',
|
||||
|
|
@ -170,9 +182,11 @@ export interface FileRoutesByFullPath {
|
|||
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
|
||||
'/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
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
|
|
@ -194,9 +208,11 @@ export interface FileRoutesByTo {
|
|||
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
|
||||
'/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
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
|
|
@ -220,9 +236,11 @@ export interface FileRoutesById {
|
|||
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
|
||||
'/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
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
|
|
@ -247,9 +265,11 @@ export interface FileRouteTypes {
|
|||
| '/settings/plugin/$source'
|
||||
| '/store/tab/emulators'
|
||||
| '/store/tab/games'
|
||||
| '/store/tab/plugins'
|
||||
| '/store/tab/'
|
||||
| '/game/update/$source/$id'
|
||||
| '/store/details/emulator/$id'
|
||||
| '/store/details/plugin/$id'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
|
|
@ -271,9 +291,11 @@ export interface FileRouteTypes {
|
|||
| '/settings/plugin/$source'
|
||||
| '/store/tab/emulators'
|
||||
| '/store/tab/games'
|
||||
| '/store/tab/plugins'
|
||||
| '/store/tab'
|
||||
| '/game/update/$source/$id'
|
||||
| '/store/details/emulator/$id'
|
||||
| '/store/details/plugin/$id'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
|
|
@ -296,9 +318,11 @@ export interface FileRouteTypes {
|
|||
| '/settings/plugin/$source'
|
||||
| '/store/tab/emulators'
|
||||
| '/store/tab/games'
|
||||
| '/store/tab/plugins'
|
||||
| '/store/tab/'
|
||||
| '/game/update/$source/$id'
|
||||
| '/store/details/emulator/$id'
|
||||
| '/store/details/plugin/$id'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
|
|
@ -314,6 +338,7 @@ export interface RootRouteChildren {
|
|||
PlatformSourceIdRoute: typeof PlatformSourceIdRoute
|
||||
GameUpdateSourceIdRoute: typeof GameUpdateSourceIdRoute
|
||||
StoreDetailsEmulatorIdRoute: typeof StoreDetailsEmulatorIdRoute
|
||||
StoreDetailsPluginIdRoute: typeof StoreDetailsPluginIdRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
|
|
@ -409,6 +434,13 @@ 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'
|
||||
|
|
@ -465,6 +497,13 @@ 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'
|
||||
|
|
@ -511,12 +550,14 @@ const SettingsRouteRouteWithChildren = SettingsRouteRoute._addFileChildren(
|
|||
interface StoreTabRouteRouteChildren {
|
||||
StoreTabEmulatorsRoute: typeof StoreTabEmulatorsRoute
|
||||
StoreTabGamesRoute: typeof StoreTabGamesRoute
|
||||
StoreTabPluginsRoute: typeof StoreTabPluginsRoute
|
||||
StoreTabIndexRoute: typeof StoreTabIndexRoute
|
||||
}
|
||||
|
||||
const StoreTabRouteRouteChildren: StoreTabRouteRouteChildren = {
|
||||
StoreTabEmulatorsRoute: StoreTabEmulatorsRoute,
|
||||
StoreTabGamesRoute: StoreTabGamesRoute,
|
||||
StoreTabPluginsRoute: StoreTabPluginsRoute,
|
||||
StoreTabIndexRoute: StoreTabIndexRoute,
|
||||
}
|
||||
|
||||
|
|
@ -537,6 +578,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||
PlatformSourceIdRoute: PlatformSourceIdRoute,
|
||||
GameUpdateSourceIdRoute: GameUpdateSourceIdRoute,
|
||||
StoreDetailsEmulatorIdRoute: StoreDetailsEmulatorIdRoute,
|
||||
StoreDetailsPluginIdRoute: StoreDetailsPluginIdRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||
import { getRomApiRomsIdGetOptions, getRomsApiRomsGetOptions } from "../clients/romm/@tanstack/react-query.gen";
|
||||
import { DefaultRommStaleTime, GameListFilterType } from "../shared/constants";
|
||||
import { DefaultRommStaleTime } from "../shared/constants";
|
||||
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
|
||||
export function gamesQueryOptions (filter?: GameListFilterType)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 '@/shared/constants';
|
||||
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { useLocalStorage } from 'usehooks-ts';
|
||||
|
||||
export const Route = createFileRoute('/collection/$source/$id')({
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ 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 "@/shared/types";
|
||||
import { FrontEndGameTypeDetailed } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export const Route = createFileRoute("/game/$source/$id")({
|
||||
loader: async ({ params, context }) =>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
|
||||
import { AutoFocus } from '@/mainview/components/AutoFocus';
|
||||
import GameLookupElement from '@/mainview/components/game/GameLookup';
|
||||
import { HeaderUI, StickyHeaderUI } from '@/mainview/components/Header';
|
||||
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, useNavigate, useRouter } from '@tanstack/react-router';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
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')({
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|||
import { CollectionsDetail } from '../components/CollectionsDetail';
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import z from 'zod';
|
||||
import { GameListFilterType } from '@/shared/constants';
|
||||
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { useSessionStorage } from 'usehooks-ts';
|
||||
import HeaderSearchField from '../components/HeaderSearchField';
|
||||
import { useEffect } from 'react';
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ import SelectMenu from "../components/SelectMenu";
|
|||
import HeaderSearchField from "../components/HeaderSearchField";
|
||||
import CardElement from "../components/CardElement";
|
||||
import { Router } from "..";
|
||||
import { FrontEndId } from "@/shared/types";
|
||||
import { FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: ConsoleHomeUI,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
||||
import { CollectionsDetail } from "../components/CollectionsDetail";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { GameListFilterType, RPC_URL } from "../../shared/constants";
|
||||
import { RPC_URL } from "../../shared/constants";
|
||||
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { deletePlatformMutation, localPlatformFilter, platformQuery, updatePlatformMutation } from "@queries/romm";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import z from "zod";
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ import
|
|||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { RommLoginDataSchema, RPC_URL } from "@shared/constants";
|
||||
import { RPC_URL } from "@shared/constants";
|
||||
import { RommLoginDataSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import toast from "react-hot-toast";
|
||||
import { OptionSpace } from "../../components/options/OptionSpace";
|
||||
import { useSettingsForm, useSettingsFormContext } from "../../components/options/SettingsAppForm";
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@ 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 '@/shared/types';
|
||||
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; })
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import { Check, ChevronDown, FolderSearch, HardDrive, Plug, SearchAlert, Store,
|
|||
import { ContextDialog, ContextList, DialogEntry, OptionElement } from '../../components/ContextDialog';
|
||||
import classNames from 'classnames';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { RPC_URL, SettingsSchema } from '../../../shared/constants';
|
||||
import { RPC_URL } from '../../../shared/constants';
|
||||
import { SettingsSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import emulators from '@emulators';
|
||||
import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||
|
|
@ -20,11 +21,14 @@ 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 '@/shared/types';
|
||||
import { FrontEndEmulator } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import z from 'zod';
|
||||
|
||||
export const Route = createFileRoute('/settings/emulators')({
|
||||
component: RouteComponent,
|
||||
pendingComponent: EmulatorsPending,
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
|
||||
});
|
||||
|
||||
function EmulatorsPending ()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import { LocalOption } from '@/mainview/components/options/LocalOption';
|
||||
import { LocalSettingsSchema, settingRegistry } from '@/shared/constants';
|
||||
import { settingRegistry } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { LocalSettingsSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Terminal } from 'lucide-react';
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import z from 'zod';
|
||||
|
||||
export const Route = createFileRoute('/settings/interface')({
|
||||
component: RouteComponent,
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
|
||||
});
|
||||
|
||||
function RouteComponent ()
|
||||
|
|
|
|||
|
|
@ -5,23 +5,25 @@ import { OptionDropdown } from '@/mainview/components/options/OptionDropdown';
|
|||
import { OptionInput } from '@/mainview/components/options/OptionInput';
|
||||
import { OptionSpace } from '@/mainview/components/options/OptionSpace';
|
||||
import { RoundButton } from '@/mainview/components/RoundButton';
|
||||
import { getPluginDetailsQuery } from '@/mainview/scripts/queries/plugins';
|
||||
import { allPluginsFilter, getPluginDetailsQuery, updatePluginMutation } from '@/mainview/scripts/queries/plugins';
|
||||
import { getPluginActionsQuery, getPluginSettingQuery, getPluginSettingsDefinitionQuery, pluginActionMutation, setPluginSettingMutation } from '@/mainview/scripts/queries/settings';
|
||||
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||
import { scrollIntoViewHandler } from '@/mainview/scripts/utils';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { PluginUpdateCheck } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { ArrowLeft, CirclePlay, Settings2 } from 'lucide-react';
|
||||
import { ArrowLeft, ArrowRight, CircleFadingArrowUp, CirclePlay, Settings2 } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
export const Route = createFileRoute('/settings/plugin/$source')({
|
||||
component: RouteComponent,
|
||||
pendingComponent: Loading,
|
||||
async loader (ctx)
|
||||
{
|
||||
const definitions = await ctx.context.queryClient.fetchQuery(getPluginSettingsDefinitionQuery(ctx.params.source));
|
||||
const actions = await ctx.context.queryClient.fetchQuery(getPluginActionsQuery(ctx.params.source));
|
||||
const source = decodeURIComponent(ctx.params.source);
|
||||
const definitions = await ctx.context.queryClient.fetchQuery(getPluginSettingsDefinitionQuery(source));
|
||||
const actions = await ctx.context.queryClient.fetchQuery(getPluginActionsQuery(source));
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return { definitions, actions };
|
||||
},
|
||||
|
|
@ -38,7 +40,8 @@ function Loading ()
|
|||
|
||||
function PluginAction (data: { id: string, title: string | undefined, description: string | undefined; action: string; reload: () => void; })
|
||||
{
|
||||
const { source } = Route.useParams();
|
||||
const { source: sourceRaw } = Route.useParams();
|
||||
const source = decodeURIComponent(sourceRaw);
|
||||
const action = useMutation({
|
||||
...pluginActionMutation(source, data.id),
|
||||
onSuccess (acitonData, variables, onMutateResult, context)
|
||||
|
|
@ -67,7 +70,8 @@ function PluginAction (data: { id: string, title: string | undefined, descriptio
|
|||
|
||||
function PluginOption (data: { name: string, title?: string, prop: JSONSchema7; })
|
||||
{
|
||||
const { source } = Route.useParams();
|
||||
const { source: sourceRaw } = Route.useParams();
|
||||
const source = decodeURIComponent(sourceRaw);
|
||||
const { data: value, refetch: refetchValue } = useQuery(getPluginSettingQuery(source, data.name));
|
||||
const setValue = useMutation({
|
||||
...setPluginSettingMutation(source, data.name),
|
||||
|
|
@ -108,12 +112,21 @@ function PluginOption (data: { name: string, title?: string, prop: JSONSchema7;
|
|||
</OptionSpace>;
|
||||
}
|
||||
|
||||
function Settings ()
|
||||
function Settings (data: { update: PluginUpdateCheck | undefined; })
|
||||
{
|
||||
const { definitions, actions } = Route.useLoaderData();
|
||||
const { source } = Route.useParams();
|
||||
const { source: sourceRaw } = Route.useParams();
|
||||
const source = decodeURIComponent(sourceRaw);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const update = useMutation({
|
||||
...updatePluginMutation(source),
|
||||
onSuccess (data, variables, onMutateResult, context)
|
||||
{
|
||||
context.client.invalidateQueries(allPluginsFilter);
|
||||
navigate({ to: '/settings/plugin/$source', params: { source: encodeURIComponent(source) }, replace: true });
|
||||
},
|
||||
});
|
||||
const handleReload = () =>
|
||||
{
|
||||
queryClient.refetchQueries(getPluginSettingsDefinitionQuery(source));
|
||||
|
|
@ -121,7 +134,7 @@ function Settings ()
|
|||
};
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: 'plugin-settings',
|
||||
focusable: (definitions?.properties && Object.keys(definitions?.properties).length > 0) || actions.length > 0
|
||||
focusable: (definitions?.properties && Object.keys(definitions?.properties).length > 0) || actions.length > 0 || !!data.update
|
||||
});
|
||||
return <div ref={ref}>
|
||||
<FocusContext value={focusKey}>
|
||||
|
|
@ -148,6 +161,15 @@ function Settings ()
|
|||
|
||||
})}
|
||||
<div className="divider"><CirclePlay className='size-14' /> Actions</div>
|
||||
{!!data.update && <OptionSpace
|
||||
id="update-option-space"
|
||||
label={
|
||||
<div className='flex flex-col'>
|
||||
<div>Update</div>
|
||||
<div className='flex gap-2 text-sm text-base-content/40 text-wrap'>{data?.update?.current} {'>'} {data?.update?.new}</div>
|
||||
</div>}>
|
||||
<Button style='warning' id='update-plugin-btn' onAction={e => update.mutate()} >{update.isPending ? <span className="loading loading-spinner loading-lg"></span> : <CircleFadingArrowUp />}Update</Button>
|
||||
</OptionSpace>}
|
||||
{actions?.map(a => <PluginAction key={a.id} id={a.id} title={a.title} description={a.description} action={a.action} reload={handleReload} />)}
|
||||
</FocusContext>
|
||||
</div>;
|
||||
|
|
@ -155,7 +177,8 @@ function Settings ()
|
|||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { source } = Route.useParams();
|
||||
const { source: sourceRaw } = Route.useParams();
|
||||
const source = decodeURIComponent(sourceRaw);
|
||||
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'plugins' });
|
||||
const { data } = useQuery(getPluginDetailsQuery(source));
|
||||
|
|
@ -167,17 +190,17 @@ function RouteComponent ()
|
|||
<FocusContext value={focusKey}>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex text-2xl font-bold gap-2 grow items-center justify-center'>
|
||||
<div className='flex gap-2 grow items-center justify-center'>
|
||||
<RoundButton onFocus={scrollIntoViewHandler({ inline: 'end' })} id='return-to-plugins' onAction={handleReturn}><ArrowLeft /></RoundButton>
|
||||
<img className='h-12' src={data?.icon}></img>
|
||||
{data?.displayName}
|
||||
<div className='text-2xl font-bold'>{data?.displayName}</div>
|
||||
<div className='px-3 bg-base-300 rounded-full font-semibold'>{data?.version}</div>
|
||||
{!!data?.update && <div className='flex gap-2'> <ArrowRight /><div className='px-3 bg-warning text-warning-content rounded-full font-semibold'>{data?.update.new}</div></div>}
|
||||
</div>
|
||||
<ul className='flex gap-2 justify-center'>{data?.keywords?.map((k, i) => <li key={i} className='bg-base-200 rounded-full p-2 px-4'>{k}</li>)}</ul>
|
||||
<div className='bg-base-200 p-4 rounded-2xl'>{data?.description}</div>
|
||||
</div>
|
||||
|
||||
<Settings />
|
||||
|
||||
<Settings update={data?.update} />
|
||||
</FocusContext>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</div>;
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { pluginCategoryIcons, pluginCategoryPriorities } from '@/mainview/compon
|
|||
import { OptionInput } from '@/mainview/components/options/OptionInput';
|
||||
import { OptionSpace } from '@/mainview/components/options/OptionSpace';
|
||||
import { RoundButton } from '@/mainview/components/RoundButton';
|
||||
import { enablePluginMutation, getAllPluginsQuery } from '@/mainview/scripts/queries/plugins';
|
||||
import { enablePluginMutation, getAllPluginsQuery, uninstallPluginMutation } from '@/mainview/scripts/queries/plugins';
|
||||
import { GamePadButtonCode, Shortcut } from '@/mainview/scripts/shortcuts';
|
||||
import { FrontendPlugin } from '@/shared/types';
|
||||
import { FrontendPlugin } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { Eye, Puzzle, Search, Settings2 } from 'lucide-react';
|
||||
import { CircleFadingArrowUp, Eye, Puzzle, Settings2, Trash } from 'lucide-react';
|
||||
|
||||
export const Route = createFileRoute('/settings/plugins')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -33,7 +33,9 @@ function Plugin (data: {
|
|||
|
||||
},
|
||||
});
|
||||
const handleDetails = () => navigate({ to: '/settings/plugin/$source', params: { source: data.plugin.name }, replace: true, viewTransition: { types: ['slide-up'] } });
|
||||
const uninstall = useMutation(uninstallPluginMutation(data.plugin.name));
|
||||
const handleUninstall = () => uninstall.mutate();
|
||||
const handleDetails = () => navigate({ to: '/settings/plugin/$source', params: { source: encodeURIComponent(data.plugin.name) }, replace: true, viewTransition: { types: ['slide-up'] } });
|
||||
|
||||
return <OptionSpace
|
||||
label={
|
||||
|
|
@ -42,10 +44,13 @@ function Plugin (data: {
|
|||
{data.plugin.icon ? <img src={data.plugin.icon}></img> : <Puzzle />}
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<div>{data.plugin.displayName}</div>
|
||||
<div>{data.plugin.displayName ?? data.plugin.name}</div>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<div className=' text-sm text-base-content/40'>{data.plugin.name} ({data.plugin.version})</div>
|
||||
{data.plugin.hasSettings && <Settings2 className='bg-base-300 rounded-full p-1 size-6' />}
|
||||
{data.plugin.update && <div className={data.plugin.update.new} data-tip="hello">
|
||||
<CircleFadingArrowUp className='bg-warning text-warning-content rounded-full p-1 size-6' />
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -55,6 +60,7 @@ function Plugin (data: {
|
|||
>
|
||||
<div className='flex gap-4'>
|
||||
<RoundButton className='size-12 p-1' onAction={handleDetails} id={`${data.plugin.name}-details`} >{data.plugin.hasSettings ? <Settings2 /> : <Eye />}</RoundButton>
|
||||
{data.plugin.canUninstall && <RoundButton className='size-12 p-1' onAction={handleUninstall} id={`${data.plugin.name}-uninstall`} >{uninstall.isPending ? <span className="loading loading-spinner loading-lg"></span> : <Trash />}</RoundButton>}
|
||||
{data.plugin.canDisable && <OptionInput compact onChange={v => data.setEnabled(!!v)} value={data.plugin.enabled} name={data.plugin.name} type="checkbox" />}
|
||||
</div>
|
||||
</OptionSpace>;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import DotsLoading from '@/mainview/components/backgrounds/dots';
|
|||
import { Button } from '@/mainview/components/options/Button';
|
||||
import { checkUpdateMutation, hasUpdateQuery, updateMutation } from '@/mainview/scripts/queries/system';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { CircleFadingArrowUp, RefreshCcw } from 'lucide-react';
|
||||
import { MarkdownAsync } from 'react-markdown';
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import FocusTooltip from "@/mainview/components/FocusTooltip";
|
|||
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
||||
import { FilterUI } from "@/mainview/components/Filters";
|
||||
import Markdown from "react-markdown";
|
||||
import { FrontEndEmulatorDetailed } from "@/shared/types";
|
||||
import { FrontEndEmulatorDetailed } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export const Route = createFileRoute('/store/details/emulator/$id')({
|
||||
component: RouteComponent,
|
||||
|
|
|
|||
161
src/mainview/routes/store/details.plugin.$id.tsx
Normal file
161
src/mainview/routes/store/details.plugin.$id.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import { AutoFocus } from '@/mainview/components/AutoFocus';
|
||||
import DotsLoading from '@/mainview/components/backgrounds/dots';
|
||||
import { StickyHeaderUI } from '@/mainview/components/Header';
|
||||
import LoadingScreen from '@/mainview/components/LoadingScreen';
|
||||
import { Button } from '@/mainview/components/options/Button';
|
||||
import { FloatingShortcuts } from '@/mainview/components/Shortcuts';
|
||||
import StatList, { StatEntry } from '@/mainview/components/StatList';
|
||||
import { installPluginMutation, pluginFilter, uninstallPluginMutation, updatePluginMutation } from '@/mainview/scripts/queries/plugins';
|
||||
import { pluginDetailsQuery } from '@/mainview/scripts/queries/store';
|
||||
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||
import { HandleGoBack } from '@/mainview/scripts/utils';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { QueryClient, useMutation } from '@tanstack/react-query';
|
||||
import { createFileRoute, useNavigate, useRouter } from '@tanstack/react-router';
|
||||
import { ArrowRight, CircleFadingArrowUp, Download, Settings, Trash } from 'lucide-react';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/store/details/plugin/$id')({
|
||||
component: RouteComponent,
|
||||
pendingComponent: Loading,
|
||||
async loader (ctx)
|
||||
{
|
||||
const id = decodeURIComponent(ctx.params.id);
|
||||
const data = await ctx.context.queryClient.fetchQuery(pluginDetailsQuery(id));
|
||||
return { data };
|
||||
},
|
||||
});
|
||||
|
||||
function Loading ()
|
||||
{
|
||||
const { ref, focusSelf } = useFocusable({ focusKey: 'plugin-details' });
|
||||
return <>
|
||||
<DotsLoading ref={ref} />
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</>;
|
||||
}
|
||||
|
||||
function Details ()
|
||||
{
|
||||
const { id } = Route.useParams();
|
||||
const plugin = decodeURIComponent(id);
|
||||
const { data } = Route.useLoaderData();
|
||||
const navigate = useNavigate();
|
||||
const handleRefresh = (client: QueryClient) =>
|
||||
{
|
||||
client.invalidateQueries(pluginFilter(plugin));
|
||||
navigate({ to: '/store/details/plugin/$id', params: { id: encodeURIComponent(id) }, replace: true });
|
||||
};
|
||||
const update = useMutation({
|
||||
...updatePluginMutation(plugin),
|
||||
onSuccess (data, variables, onMutateResult, context)
|
||||
{
|
||||
handleRefresh(context.client);
|
||||
},
|
||||
});
|
||||
const install = useMutation({
|
||||
...installPluginMutation(plugin),
|
||||
onSuccess (data, variables, onMutateResult, context)
|
||||
{
|
||||
handleRefresh(context.client);
|
||||
},
|
||||
});
|
||||
const uninstall = useMutation({
|
||||
...uninstallPluginMutation(plugin),
|
||||
onSuccess (data, variables, onMutateResult, context)
|
||||
{
|
||||
handleRefresh(context.client);
|
||||
},
|
||||
});
|
||||
|
||||
const stats: StatEntry[] = [];
|
||||
if (data.devDependencies)
|
||||
{
|
||||
stats.push({ content: Object.keys(data.devDependencies), label: "Dev Dependecies" });
|
||||
}
|
||||
if (data.dependencies)
|
||||
{
|
||||
stats.push({ content: Object.keys(data.dependencies), label: "Dependecies" });
|
||||
}
|
||||
if (data.maintainers)
|
||||
{
|
||||
stats.push({ content: data.maintainers.map(m => m.name), label: "Maintainers" });
|
||||
}
|
||||
if (data.dist)
|
||||
{
|
||||
stats.push({ content: prettyBytes(data.dist.unpackedSize), label: "Size" });
|
||||
}
|
||||
if (data.license)
|
||||
{
|
||||
stats.push({ content: data.license, label: "License" });
|
||||
}
|
||||
return <>
|
||||
|
||||
<div className='flex justify-between p-8'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-3xl font-bold'>{data.name}</div>
|
||||
<div className='flex gap-2'>
|
||||
<div className='flex gap-1'>
|
||||
{data.update ? <>
|
||||
<div className='bg-base-300 px-2 rounded-full'>{data.update.from}</div>
|
||||
<ArrowRight />
|
||||
<div className='bg-warning text-warning-content px-2 rounded-full'>{data.version}</div>
|
||||
</> :
|
||||
<div className='bg-base-300 px-2 rounded-full'>{data.version}</div>}
|
||||
|
||||
</div>
|
||||
by {data.author?.name ?? data._npmUser?.name}</div>
|
||||
</div>
|
||||
<div className='flex gap-2 items-center'>
|
||||
{data.installed && <>
|
||||
{!!data.update && <Button onAction={e => update.mutate()} className='gap-2' style='warning' id='install-btn' >
|
||||
{update.isPending ? <span className="loading loading-spinner loading-lg"></span> : <CircleFadingArrowUp />} Update
|
||||
</Button>}
|
||||
<Button onAction={e => uninstall.mutate()} className='gap-2' style='accent' id='install-btn' >
|
||||
{uninstall.isPending ? <span className="loading loading-spinner loading-lg"></span> : <Trash />} Uninstall
|
||||
</Button>
|
||||
<Button external onAction={e => { navigate({ to: '/settings/plugin/$source', params: { source: encodeURIComponent(plugin) } }); }} className='gap-2' style='info' id='install-btn' >
|
||||
<Settings /> Settings
|
||||
</Button>
|
||||
|
||||
</>}
|
||||
{!data.installed && <Button onAction={e => install.mutate()} className='gap-2' style='accent' id='install-btn' >
|
||||
{install.isPending ? <span className="loading loading-spinner loading-lg"></span> : <Download />} Install
|
||||
</Button>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="divider">Details</div>
|
||||
<div className='px-8'>
|
||||
<div className='p-4 bg-base-200 rounded-2xl'>{data.description}</div>
|
||||
<StatList id={'plugin-stats'} stats={stats} />
|
||||
</div>
|
||||
<div className="divider">Keywords</div>
|
||||
<div className='flex gap-2 px-8'>
|
||||
{data.keywords.map(k => <li className='flex px-2 bg-base-300 rounded-full'>{k}</li>)}
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'plugin-details' });
|
||||
useShortcuts(focusKey, () => [{
|
||||
label: "Return", button: GamePadButtonCode.B, action (e)
|
||||
{
|
||||
HandleGoBack(router, e);
|
||||
},
|
||||
}]);
|
||||
return <div ref={ref} className='absolute w-full h-full overflow-y-scroll overflow-x-hidden'>
|
||||
<FocusContext value={focusKey}>
|
||||
<StickyHeaderUI ref={ref} />
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<Details />
|
||||
</Suspense>
|
||||
<FloatingShortcuts />
|
||||
</FocusContext>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -8,13 +8,13 @@ import LoadMoreButton from '@/mainview/components/LoadMoreButton';
|
|||
import { storeGamesInfiniteQuery } from '@queries/store';
|
||||
import InvalidStoreError from '@/mainview/components/store/InvalidStoreError';
|
||||
import { CardList, GameMetaExtra } from '@/mainview/components/CardList';
|
||||
import { GameListFilterType, RPC_URL } from '@/shared/constants';
|
||||
import { RPC_URL } from '@/shared/constants';
|
||||
import { GameListFilterType, FrontEndGameType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { useSessionStorage } from 'usehooks-ts';
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import z from 'zod';
|
||||
import SideFilters from '@/mainview/components/SideFilters';
|
||||
import { gameFiltersQuery } from '@/mainview/scripts/queries/romm';
|
||||
import { FrontEndGameType } from '@/shared/types';
|
||||
|
||||
export const Route = createFileRoute('/store/tab/games')({
|
||||
component: RouteComponent,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ 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 '@/shared/types';
|
||||
import { FrontEndGameTypeDetailed } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
|
||||
export const Route = createFileRoute('/store/tab/')({
|
||||
component: RouteComponent
|
||||
|
|
|
|||
151
src/mainview/routes/store/tab/plugins.tsx
Normal file
151
src/mainview/routes/store/tab/plugins.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import { allPluginsFilter, installPluginMutation, uninstallPluginMutation, updatePluginMutation } from '@/mainview/scripts/queries/plugins';
|
||||
import { pluginsQuery } from '@/mainview/scripts/queries/store';
|
||||
import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||
import { FOCUS_KEYS } from '@/mainview/scripts/types';
|
||||
import { PluginEntryType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { QueryClient, useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import { CircleFadingArrowUp, Dot, Download, HardDrive, Puzzle } from 'lucide-react';
|
||||
import prettyMilliseconds from 'pretty-ms';
|
||||
import { useSessionStorage } from 'usehooks-ts';
|
||||
import z from 'zod';
|
||||
|
||||
export const Route = createFileRoute('/store/tab/plugins')({
|
||||
component: RouteComponent,
|
||||
validateSearch: zodValidator(z.object({
|
||||
search: z.string().optional()
|
||||
}))
|
||||
});
|
||||
|
||||
function PluginCard (data: { plugin: PluginEntryType; })
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const onAction = () =>
|
||||
{
|
||||
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 <div ref={ref} onClick={onAction} data-installed={data.plugin.installed} className='flex flex-wrap bg-base-100 p-4 rounded-2xl focusable focusable-secondary focusable-hover justify-between cursor-pointer'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex gap-2 font-bold text-xl in-data-[installed=true]:text-info'>
|
||||
{data.plugin.installed && <HardDrive className='p-1 bg-base-300 rounded-full size-8 text-base-content' />}
|
||||
{data.plugin.update && <CircleFadingArrowUp className='p-1 bg-warning text-warning-content rounded-full size-8' />}
|
||||
{data.plugin.package.name}
|
||||
{(install.isPending || uninstall.isPending) && <span className="loading loading-spinner loading-lg"></span>}
|
||||
</div>
|
||||
<div className='text-base-content/40'>{data.plugin.package.description}</div>
|
||||
<ul className='flex flex-wrap gap-2'>{data.plugin.package.keywords.concat(...data.plugin.installed ? ["installed"] : []).map(k => <li className='bg-base-300 px-2 rounded-full'>{k}</li>)}</ul>
|
||||
<ul className='flex flex-wrap gap-2'>
|
||||
<li>{data.plugin.package.publisher.username}</li>
|
||||
<Dot />
|
||||
<li>{data.plugin.package.version}</li>
|
||||
<Dot />
|
||||
<li>{prettyMilliseconds(new Date().getTime() - data.plugin.package.date.getTime(), { hideSeconds: true })}</li>
|
||||
<Dot />
|
||||
<li>{data.plugin.package.license}</li>
|
||||
{install.isPending && <>
|
||||
<Dot />
|
||||
<li><span className="loading loading-spinner loading-md"></span>installing</li>
|
||||
</>}
|
||||
{uninstall.isPending && <>
|
||||
<Dot />
|
||||
<li><span className="loading loading-spinner loading-md"></span>uninstalling</li>
|
||||
</>}
|
||||
</ul>
|
||||
</div>
|
||||
<div className='flex justify-center items-center'>
|
||||
<div className='flex gap-2 bg-base-300 rounded-3xl px-3 py-2'>
|
||||
<Download />
|
||||
{data.plugin.downloads.monthly}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const [search] = useSessionStorage<string | undefined>(`${Route.to}-search`, undefined);
|
||||
const { data: plugins } = useQuery(pluginsQuery(search));
|
||||
const { ref, focusKey } = useFocusable({ focusKey: "plugins-store" });
|
||||
return <div ref={ref}>
|
||||
<FocusContext value={focusKey}>
|
||||
<div className="divider"><Puzzle className='size-12' /> {plugins?.total} Plugins</div>
|
||||
<div className='flex flex-col gap-2 p-8'>
|
||||
{plugins?.objects.map((p, i) => <PluginCard key={i} plugin={p} />)}
|
||||
</div>
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ 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 { Gamepad2, Home, Joystick, Puzzle } from 'lucide-react';
|
||||
import { useRef } from 'react';
|
||||
import { useSessionStorage } from 'usehooks-ts';
|
||||
import z from 'zod';
|
||||
|
|
@ -93,9 +94,10 @@ function RouteComponent ()
|
|||
const headerRef = useRef(null);
|
||||
const sentinelRef = useRef(null);
|
||||
const filters: Record<string, FilterOption> = {
|
||||
home: { label: "Home", selected: useIsSettings(''), },
|
||||
emulators: { label: "Emulators", selected: useIsSettings('emulators') },
|
||||
games: { label: "Games", selected: useIsSettings('games') }
|
||||
home: { label: "Home", icon: <Home />, selected: useIsSettings(''), },
|
||||
emulators: { label: "Emulators", icon: <Joystick />, selected: useIsSettings('emulators') },
|
||||
games: { label: "Games", icon: <Gamepad2 />, selected: useIsSettings('games') },
|
||||
plugins: { label: "Plugins", icon: <Puzzle />, selected: useIsSettings('plugins') }
|
||||
};
|
||||
const [search, setSearch] = useSessionStorage<string | undefined>(`${router.history.location.pathname}-search`, undefined);
|
||||
const [, setGamesSearch] = useSessionStorage<string | undefined>(`/store/tab/games-search`, undefined);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { SystemInfoType } from "@/shared/constants";
|
||||
import { SystemInfoType, Drive } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { Direction, FocusDetails } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { createContext } from "react";
|
||||
import { Shortcut } from "./shortcuts";
|
||||
import { Drive } from "@/shared/types";
|
||||
|
||||
export const StoreContext = createContext({} as {
|
||||
showDetails: (type: 'emulator' | 'game', source: string, id: string, focusSource: string) => void;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { mutationOptions, queryOptions } from "@tanstack/react-query";
|
||||
import { mutationOptions, QueryFilters, 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: source }).get();
|
||||
const { data, error } = await pluginsApi.plugins({ id: encodeURIComponent(source) }).get();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
|
@ -24,7 +24,51 @@ export const enablePluginMutation = mutationOptions({
|
|||
mutationKey: ['plugin', 'enable'],
|
||||
mutationFn: async (vars: { id: string, enabled: boolean; }) =>
|
||||
{
|
||||
const { error } = await pluginsApi.plugins({ id: vars.id }).post({ enabled: vars.enabled });
|
||||
const { error } = await pluginsApi.plugins({ id: encodeURIComponent(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');
|
||||
},
|
||||
});
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { DefaultRommStaleTime, GameListFilterType, RommLoginDataSchema } from "@/shared/constants";
|
||||
import { DefaultRommStaleTime } from "@/shared/constants";
|
||||
import { GameListFilterType, RommLoginDataSchema, FrontEndId } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { rommApi, settingsApi } from "../clientApi";
|
||||
import { InvalidateQueryFilters, mutationOptions, QueryClient, QueryFilters, queryOptions } from "@tanstack/react-query";
|
||||
import z from "zod";
|
||||
import { statsApiStatsGetOptions } from "@/clients/romm/@tanstack/react-query.gen";
|
||||
import { FrontEndId } from "@/shared/types";
|
||||
|
||||
export const allGamesQuery = (filter?: GameListFilterType) => queryOptions({
|
||||
queryKey: ['games', filter ?? 'all'],
|
||||
|
|
|
|||
|
|
@ -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 }).get();
|
||||
const { data: value, error } = await settingsApi.api.settings.definitions({ source: encodeURIComponent(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 })({ id }).get();
|
||||
const { data, error } = await settingsApi.api.settings({ source: encodeURIComponent(source) })({ id: encodeURIComponent(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 })({ id }).put({ value });
|
||||
const { data, error } = await settingsApi.api.settings({ source: encodeURIComponent(source) })({ id: encodeURIComponent(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 }).get();
|
||||
const { data, error } = await settingsApi.api.settings.actions({ source: encodeURIComponent(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 })({ id }).post();
|
||||
const { data, error, response } = await settingsApi.api.settings.actions({ source: encodeURIComponent(source) })({ id: encodeURIComponent(id) }).post();
|
||||
if (error) throw error;
|
||||
|
||||
return { data: data as any, response };
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { infiniteQueryOptions, mutationOptions, queryOptions } from "@tanstack/react-query";
|
||||
import { rommApi, storeApi } from "../clientApi";
|
||||
import { GameListFilterType } from "@/shared/constants";
|
||||
import { FrontEndGameType } from "@/shared/types";
|
||||
|
||||
import { GameListFilterType, FrontEndGameType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
|
||||
export const storeEmulatorsQuery = (filters: { search?: string; }) => queryOptions({
|
||||
queryKey: ['store-emulators', filters], queryFn: async () =>
|
||||
|
|
@ -97,4 +95,22 @@ 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;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { FrontEndId } from "@/shared/types";
|
||||
import { FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||
|
||||
export const FOCUS_KEYS = {
|
||||
NAV_CATEGORIES: "NAV_CATEGORIES",
|
||||
|
|
@ -14,4 +14,5 @@ export const FOCUS_KEYS = {
|
|||
GAME_CARD: (id: FrontEndId) => `GAME_${id.source}_${id.id}`,
|
||||
GAME_MATCH: (id: FrontEndId) => `GAME_${id.source}_${id.id}`,
|
||||
STATS_SECTION: "STATS_SECTION",
|
||||
PLUGIN_ENTRY: (id: string) => `PLUGIN_${id}`
|
||||
} as const;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { LocalSettingsSchema, LocalSettingsType } from "@/shared/constants";
|
||||
import { LocalSettingsSchema, LocalSettingsType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||
import { DependencyList, RefObject, useEffect, useRef, useState } from "react";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { jobsApi, systemApi } from "./clientApi";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue