From a69147a4f73cf626b92622a8ee22b54f538d41a9 Mon Sep 17 00:00:00 2001 From: Simeon Radivoev Date: Thu, 2 Apr 2026 14:20:30 +0300 Subject: [PATCH] feat: Implemented dolphin integration --- src/bun/api/cache.ts | 1 + .../api/games/services/launchGameService.ts | 22 ++++++++- src/bun/api/hooks/games.ts | 8 ++++ src/bun/api/jobs/emulator-download-job.ts | 29 +++++++++++ src/bun/api/jobs/update-store.ts | 2 +- .../dolphin.ts | 37 ++++++++++++++ .../package.json | 15 ++++++ .../pcsx2.ts | 8 ++++ .../ppsspp.ts | 9 ++++ src/bun/api/plugins/register-plugins.ts | 2 + .../api/store/services/emulatorsService.ts | 31 +++++++++--- src/bun/api/store/services/gamesService.ts | 9 +--- src/bun/api/store/store.ts | 2 +- src/mainview/components/Header.tsx | 48 ++++++++++--------- .../components/store/InvalidStoreError.tsx | 10 ++++ .../components/store/StoreEmulatorCard.tsx | 4 +- src/mainview/routes/game/$source.$id.tsx | 9 +--- src/mainview/routes/index.tsx | 4 +- src/mainview/routes/store/tab/emulators.tsx | 8 ++-- src/mainview/routes/store/tab/games.tsx | 4 +- src/mainview/scripts/queries/store.ts | 2 +- src/mainview/scripts/spatialNavigation.ts | 2 +- src/shared/constants.ts | 10 ++++ src/shared/types..d.ts | 3 +- 24 files changed, 220 insertions(+), 59 deletions(-) create mode 100644 src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts create mode 100644 src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json create mode 100644 src/mainview/components/store/InvalidStoreError.tsx diff --git a/src/bun/api/cache.ts b/src/bun/api/cache.ts index 941ba7a..6aa465a 100644 --- a/src/bun/api/cache.ts +++ b/src/bun/api/cache.ts @@ -20,6 +20,7 @@ export async function getOrCached (key: string, getter: () => Promise, opt } const data = await getter(); + if (data === undefined) return data; const expire_at = options?.expireMs ? new Date(updated_at.getTime() + options.expireMs) : new Date(updated_at.getTime() + 24 * 60 * 60 * 1000); diff --git a/src/bun/api/games/services/launchGameService.ts b/src/bun/api/games/services/launchGameService.ts index add3c59..894f6db 100644 --- a/src/bun/api/games/services/launchGameService.ts +++ b/src/bun/api/games/services/launchGameService.ts @@ -10,6 +10,8 @@ import { cores } from '../../emulatorjs/emulatorjs'; import { LaunchGameJob } from '../../jobs/launch-game-job'; import { EmulatorPackageType } from '@/shared/constants'; import { getStoreEmulatorPackage, getStoreFolder } from '../../store/services/gamesService'; +import { getOrCached } from '../../cache'; +import { getScoopPackage } from '../../store/services/emulatorsService'; export const varRegex = /%([^%]+)%/g; export const assignRegex = /(%\w+%)=(\S+) /g; @@ -285,11 +287,27 @@ export async function findStoreEmulatorExec (id: string, emulator?: { systempath const storeExecName = (await Promise.all(storeEmulator.downloads[`${process.platform}:${process.arch}`].map(async dl => { // glob file search causes issues so do manual search - const glob = new Glob(dl.pattern); if (await fs.exists(storeEmulatorFolder)) { + const glob = (dl as any).pattern ? new Glob((dl as any).pattern) : undefined; + let bin: string | undefined = (dl as any).bin; + if (!bin && dl.type === 'scoop') + { + const data = await getScoopPackage(id, dl.url); + + if (data) + { + bin = data.bin; + } + } + const files = (await fs.readdir(storeEmulatorFolder)) - .filter(f => glob.match(f)); + .filter(f => + { + if (glob && glob.match(f)) return true; + if (bin && f === bin) return true; + }); + return files.map(f => path.join(storeEmulatorFolder, f)); } return []; diff --git a/src/bun/api/hooks/games.ts b/src/bun/api/hooks/games.ts index ff3ec04..824c59c 100644 --- a/src/bun/api/hooks/games.ts +++ b/src/bun/api/hooks/games.ts @@ -18,6 +18,14 @@ export class GameHooks id: number; }; }], string[] | undefined>(['ctx']); + /** + * Is the given emulator for the given command supported + * @returns The possible value is if it can support it but not right now. To show grayed out icon. + */ + emulatorLaunchSupport = new SyncBailHook<[ctx: { + emulator: string; + source?: EmulatorSourceEntryType; + }], { id: string; possible: boolean; } | undefined>(['ctx']); /** * Fetches and returns a list of games converted to frontend. * @param ctx.localGameIds This is local game ids in the format '@' diff --git a/src/bun/api/jobs/emulator-download-job.ts b/src/bun/api/jobs/emulator-download-job.ts index f79db72..42d20b6 100644 --- a/src/bun/api/jobs/emulator-download-job.ts +++ b/src/bun/api/jobs/emulator-download-job.ts @@ -12,6 +12,7 @@ import { Downloader } from "@/bun/utils/downloader"; import { ensureDir, move } from "fs-extra"; import { simulateProgress } from "@/bun/utils"; import { path7za } from "7zip-bin"; +import { getScoopPackage } from "../store/services/emulatorsService"; type EmulatorDownloadStates = "download" | "extract"; @@ -55,6 +56,34 @@ export class EmulatorDownloadJob implements IJob await ensureDir(storeFolder); console.log("Updating Store"); - const proc = Bun.spawn([process.execPath, "add", `${this.packageName}@${this.storeVersion}`, "--production", "--registry", this.registry.href], { + const proc = Bun.spawn([process.execPath, "install", `${this.packageName}@${this.storeVersion}`, "--registry", this.registry.href], { cwd: storeFolder, stdout: 'pipe', stderr: 'pipe', diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts new file mode 100644 index 0000000..dc0e28d --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts @@ -0,0 +1,37 @@ + +import { config, db } from "@/bun/api/app"; +import { PluginContextType, PluginType } from "@/bun/types/typesc.schema"; +import path from 'node:path'; +import desc from './package.json'; + +export default class DOLPHINIntegration implements PluginType +{ + load (ctx: PluginContextType) + { + ctx.hooks.games.emulatorLaunchSupport.tap(desc.name, (ctx) => + { + if (ctx.emulator === 'DOLPHIN') + return { id: desc.name, possible: !!ctx.source }; + }); + + ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) => + { + if (ctx.autoValidCommand.emulator === 'DOLPHIN' && ctx.autoValidCommand.metadata.emulatorDir) + { + const args = ["--batch"]; + + const storageFolder = path.join(config.get('downloadPath'), "saves", 'DOLPHIN'); + + args.push(...[`--user=${storageFolder}`, `--exec=${ctx.autoValidCommand.metadata.romPath}`]); + args.push(`--config=Dolphin.Display.Fullscreen=${config.get('launchInFullscreen') ? "True" : "False"}`); + args.push(`--config=Dolphin.General.ISOPath0=${path.join(config.get('downloadPath'), 'roms', 'gc')}`); + args.push(`--config=Dolphin.General.ISOPath1=${path.join(config.get('downloadPath'), 'roms', 'wii')}`); + args.push(`--config=Dolphin.Interface.ConfirmStop=False`); + args.push(`--config=Dolphin.Interface.SkipNKitWarning=True`); + args.push(`--config=Dolphin.Analytics.PermissionAsked=True`); + + return args; + } + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json new file mode 100644 index 0000000..07fe38d --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json @@ -0,0 +1,15 @@ +{ + "name": "com.simeonradivoev.gameflow.dolphin", + "displayName": "DOLPHIN Integration", + "version": "0.0.1", + "description": "DOLPHIN Emulator Integration", + "main": "./dolphin.ts", + "icon": "https://upload.wikimedia.org/wikipedia/commons/5/53/Dolphin_Emulator_Logo_Refresh.svg", + "keywords": [ + "integration", + "emulator", + "wiiu", + "gc", + "dolphin" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts index 072752b..e4c2cbd 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts @@ -11,6 +11,14 @@ export default class PCSX2Integration implements PluginType { load (ctx: PluginContextType) { + ctx.hooks.games.emulatorLaunchSupport.tap(desc.name, (ctx) => + { + if (ctx.emulator === 'PCSX2') + { + return { id: desc.name, possible: ctx.source?.type === 'store' }; + } + }); + ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) => { if (ctx.autoValidCommand.emulator === 'PCSX2' && ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir) diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts index 8384213..7fb3fd9 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts @@ -14,6 +14,15 @@ export default class PCSX2Integration implements PluginType { load (ctx: PluginContextType) { + + ctx.hooks.games.emulatorLaunchSupport.tap(desc.name, (ctx) => + { + if (ctx.emulator === 'PPSSPP') + { + return { id: desc.name, possible: ctx.source?.type === 'store' }; + } + }); + ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) => { if (ctx.autoValidCommand.emulator === 'PPSSPP' && ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir) diff --git a/src/bun/api/plugins/register-plugins.ts b/src/bun/api/plugins/register-plugins.ts index 9f223b2..99b9d17 100644 --- a/src/bun/api/plugins/register-plugins.ts +++ b/src/bun/api/plugins/register-plugins.ts @@ -2,6 +2,7 @@ import { PluginManager } from "./plugin-manager"; import pcsx2 from './builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json'; import ppsspp from './builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json'; +import dolphin from './builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json'; import romm from './builtin/sources/com.simeonradivoev.gameflow.romm/package.json'; import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@/bun/types/typesc.schema"; @@ -11,6 +12,7 @@ export default async function register (pluginManager: PluginManager) const plugins: (PluginDescriptionType & { main: string; load: () => Promise; })[] = [ { ...pcsx2, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2') }, { ...ppsspp, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp') }, + { ...dolphin, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin') }, { ...romm, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.romm/romm') }, ]; diff --git a/src/bun/api/store/services/emulatorsService.ts b/src/bun/api/store/services/emulatorsService.ts index 1340731..d326fe6 100644 --- a/src/bun/api/store/services/emulatorsService.ts +++ b/src/bun/api/store/services/emulatorsService.ts @@ -1,8 +1,9 @@ -import { EmulatorPackageType } from "@/shared/constants"; +import { EmulatorPackageType, ScoopPackageSchema } from "@/shared/constants"; import { emulatorsDb, plugins } from "../../app"; import * as emulatorSchema from '@schema/emulators'; import { findExecs } from "../../games/services/launchGameService"; import { eq } from "drizzle-orm"; +import { getOrCached } from "../../cache"; export async function convertStoreEmulatorToFrontend (emulator: EmulatorPackageType, gameCount: number, systems: EmulatorSystem[]) { @@ -21,16 +22,32 @@ export async function convertStoreEmulatorToFrontend (emulator: EmulatorPackageT systems, gameCount, validSources: execPaths, - integration: findEmulatorPluginIntegration(emulator.name) + integration: findEmulatorPluginIntegration(emulator.name, execPaths) }; return em; } -export function findEmulatorPluginIntegration (name: string) +export function findEmulatorPluginIntegration (name: string, validSources: (EmulatorSourceEntryType | undefined)[]) { - const lowerCaseName = name.toLowerCase(); - const integration = Object.entries(plugins.plugins).find(p => p[1].description.keywords?.includes(lowerCaseName)); - if (!integration) return undefined; - return { name: integration[0], version: integration[1].description.version }; + const hasSupport = validSources.concat(undefined).map(s => plugins.hooks.games.emulatorLaunchSupport.call({ emulator: name, source: s })).filter(s => !!s); + + if (hasSupport.length <= 0) return undefined; + return { name: hasSupport[0].id, version: plugins.plugins[hasSupport[0].id]?.description.version, possible: hasSupport.some(s => s.possible) }; +} + +export async function getScoopPackage (id: string, url: string) +{ + const data = await getOrCached(`scoop-dl-${id}`, async () => + { + const res = await fetch(url); + if (res.ok) + { + return ScoopPackageSchema.parseAsync(await res.json()); + } + console.error(res.statusText); + return undefined; + }); + + return data; } \ No newline at end of file diff --git a/src/bun/api/store/services/gamesService.ts b/src/bun/api/store/services/gamesService.ts index ae0181f..99aa15a 100644 --- a/src/bun/api/store/services/gamesService.ts +++ b/src/bun/api/store/services/gamesService.ts @@ -101,14 +101,7 @@ export async function getAllStoreEmulatorPackages () const emulators = await fs.readdir(emulatorsBucket); const emulatorsRawData = await Promise.all(emulators.map(e => fs.readFile(path.join(emulatorsBucket, e), 'utf-8'))); - const emulatesParsed = emulatorsRawData.map(d => EmulatorPackageSchema.safeParse(JSON.parse(d))).filter(e => - { - if (e.error) - { - console.error(e.error); - } - return e.data; - }).map(e => e.data!); + const emulatesParsed = emulatorsRawData.map(d => EmulatorPackageSchema.parse(JSON.parse(d))); return emulatesParsed; } diff --git a/src/bun/api/store/store.ts b/src/bun/api/store/store.ts index d29746d..074d8bd 100644 --- a/src/bun/api/store/store.ts +++ b/src/bun/api/store/store.ts @@ -148,7 +148,7 @@ export const store = new Elysia({ prefix: '/api/store' }) sources: execPaths, biosRequirement: emulatorPackage.bios, bios: biosFiles, - integration: findEmulatorPluginIntegration(emulatorPackage.name) + integration: findEmulatorPluginIntegration(emulatorPackage.name, execPaths) }; return emulator; diff --git a/src/mainview/components/Header.tsx b/src/mainview/components/Header.tsx index 76b87b6..cc17aba 100644 --- a/src/mainview/components/Header.tsx +++ b/src/mainview/components/Header.tsx @@ -70,6 +70,7 @@ export interface HeaderButton icon: JSX.Element; external?: boolean; action?: () => void; + className?: string; } export interface HeaderAccount @@ -247,25 +248,28 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; }) export function HeaderStatusBar (data: { buttons?: HeaderButton[]; buttonElements?: JSX.Element[] | JSX.Element; }) { - return
-
- - - - - -
- {!!data.buttons &&
} -
- {data.buttonElements ?? data.buttons?.map(b => {b.icon})} -
+ const { ref, focusKey } = useFocusable({ focusKey: 'header-status-bar' }); + return
+ +
+ + + + + +
+ {!!data.buttons &&
} +
+ {data.buttonElements ?? data.buttons?.map(b => {b.icon})} +
+
; } @@ -296,13 +300,13 @@ export function HeaderUI (data: HeaderUIParams) > {data.title} - , id: "settings", action: goToSettings, external: true }]} /> + , id: "header-settings-btn", action: goToSettings, external: true }]} /> ); } -export function StickyHeaderUI (data: { ref: RefObject; } & HeaderUIParams) +export function StickyHeaderUI (data: { ref: RefObject; className?: string; } & HeaderUIParams) { const [isStuck, setIsStuck] = useState(false); const headerRef = useRef(null); @@ -311,7 +315,7 @@ export function StickyHeaderUI (data: { ref: RefObject; } & HeaderUIParams) return <>
-
+
; diff --git a/src/mainview/components/store/InvalidStoreError.tsx b/src/mainview/components/store/InvalidStoreError.tsx new file mode 100644 index 0000000..646721d --- /dev/null +++ b/src/mainview/components/store/InvalidStoreError.tsx @@ -0,0 +1,10 @@ +import { ErrorComponentProps } from "@tanstack/react-router"; +import { TriangleAlert } from "lucide-react"; + +export default function Error (data: ErrorComponentProps) +{ + return
+
Invalid Store. Update App.
+
{data.error.message}
+
; +} \ No newline at end of file diff --git a/src/mainview/components/store/StoreEmulatorCard.tsx b/src/mainview/components/store/StoreEmulatorCard.tsx index 202c38c..2102503 100644 --- a/src/mainview/components/store/StoreEmulatorCard.tsx +++ b/src/mainview/components/store/StoreEmulatorCard.tsx @@ -81,8 +81,8 @@ export function StoreEmulatorCard (data: {
- {!!data.emulator.integration && data.emulator.validSources.some(s => s.type === 'store') &&
-
+ {!!data.emulator.integration &&
+
} {data.emulator.validSources.slice(0, 3).map(s => { diff --git a/src/mainview/routes/game/$source.$id.tsx b/src/mainview/routes/game/$source.$id.tsx index 7147e79..ec57e71 100644 --- a/src/mainview/routes/game/$source.$id.tsx +++ b/src/mainview/routes/game/$source.$id.tsx @@ -3,7 +3,7 @@ import { RPC_URL } from "@shared/constants"; import { useEffect, useRef, useState } from "react"; import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { Calendar, Folder, Gamepad2, Image, Info, TriangleAlert, Trophy } from "lucide-react"; -import { HeaderUI } from "../../components/Header"; +import { HeaderUI, StickyHeaderUI } from "../../components/Header"; import { AnimatedBackground } from "../../components/AnimatedBackground"; import { useQuery } from "@tanstack/react-query"; import Shortcuts from "../../components/Shortcuts"; @@ -146,7 +146,6 @@ function RouteComponent () const [, setUpdate] = useState(0); const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details", forceFocus: true }); const headerRef = useRef(null); - const sentinelRef = useRef(null); const backgroundImage = data ? new URL(`${RPC_URL(__HOST__)}${data.path_cover}`) : undefined; const { data: recommendedGames } = useQuery({ ...gamesRecommendedBasedOnGameQuery(data?.id.source ?? source, data?.id.id ?? id), enabled: !!data && recommendedGamesVisible }); @@ -158,7 +157,6 @@ function RouteComponent () const { shortcuts } = useShortcutContext(); - useStickyDataAttr(headerRef, sentinelRef, ref); const recommendedEmulators = data?.emulators?.filter(e => e.validSources.some(em => em.exists)); const { ref: intersct } = useIntersectionObserver({ @@ -176,10 +174,7 @@ function RouteComponent () }} >
-
-
- -
+
diff --git a/src/mainview/routes/index.tsx b/src/mainview/routes/index.tsx index d285464..fade167 100644 --- a/src/mainview/routes/index.tsx +++ b/src/mainview/routes/index.tsx @@ -315,8 +315,8 @@ export default function ConsoleHomeUI () headerButtons.push({ id: "fullscreen", icon: , action: handleFullscreen }); headerButtons.push( { id: "search-header-button", icon: }, - { id: "power-button", icon: , external: true, action: () => close.mutate() }, - { id: "settings-header-button", icon: , external: true, action: () => Router.navigate({ to: "/settings/accounts" }) } + { id: "power-button", icon: , external: true, action: () => close.mutate(), className: "focusable-error!" }, + { id: "settings-header-button", icon: , external: true, action: () => router.navigate({ to: "/settings/accounts" }) } ); return ( diff --git a/src/mainview/routes/store/tab/emulators.tsx b/src/mainview/routes/store/tab/emulators.tsx index 7fd1e0f..7d1aafd 100644 --- a/src/mainview/routes/store/tab/emulators.tsx +++ b/src/mainview/routes/store/tab/emulators.tsx @@ -1,7 +1,7 @@ -import { createFileRoute, useSearch } from '@tanstack/react-router'; -import { Joystick } from 'lucide-react'; +import { createFileRoute, ErrorComponentProps, useSearch } from '@tanstack/react-router'; +import { Joystick, TriangleAlert } from 'lucide-react'; import { useContext, useEffect } from 'react'; import { FocusContext, getCurrentFocusKey, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; import { StoreEmulatorCard } from '@/mainview/components/store/StoreEmulatorCard'; @@ -9,9 +9,11 @@ import { StoreContext } from '@/mainview/scripts/contexts'; import { GetFocusedElement } from '@/mainview/scripts/spatialNavigation'; import { useQuery } from '@tanstack/react-query'; import { storeEmulatorsQuery } from '@queries/store'; +import InvalidStoreError from '@/mainview/components/store/InvalidStoreError'; export const Route = createFileRoute('/store/tab/emulators')({ component: RouteComponent, + errorComponent: InvalidStoreError }); function RouteComponent () @@ -22,7 +24,7 @@ function RouteComponent () preferredChildFocusKey: focus }); const storeContext = useContext(StoreContext); - const { data: emulators } = useQuery(storeEmulatorsQuery); + const { data: emulators } = useQuery({ ...storeEmulatorsQuery, retry: false, throwOnError: true }); useEffect(() => { diff --git a/src/mainview/routes/store/tab/games.tsx b/src/mainview/routes/store/tab/games.tsx index 7bbf93e..7aee585 100644 --- a/src/mainview/routes/store/tab/games.tsx +++ b/src/mainview/routes/store/tab/games.tsx @@ -8,9 +8,11 @@ import { GetFocusedElement } from '@/mainview/scripts/spatialNavigation'; import LoadMoreButton from '@/mainview/components/LoadMoreButton'; import { storeGamesInfiniteQuery } from '@queries/store'; import { StoreContext } from '@/mainview/scripts/contexts'; +import InvalidStoreError from '@/mainview/components/store/InvalidStoreError'; export const Route = createFileRoute('/store/tab/games')({ - component: RouteComponent + component: RouteComponent, + errorComponent: InvalidStoreError }); function RouteComponent () diff --git a/src/mainview/scripts/queries/store.ts b/src/mainview/scripts/queries/store.ts index e7b84f1..bdd6337 100644 --- a/src/mainview/scripts/queries/store.ts +++ b/src/mainview/scripts/queries/store.ts @@ -6,7 +6,7 @@ export const storeEmulatorsQuery = queryOptions({ queryKey: ['store-emulators'], queryFn: async () => { const { data, error } = await storeApi.api.store.emulators.get(); - if (error) throw error; + if (error) throw new Error(JSON.stringify(error.value)); return data; } }); diff --git a/src/mainview/scripts/spatialNavigation.ts b/src/mainview/scripts/spatialNavigation.ts index 3129f2a..51d212a 100644 --- a/src/mainview/scripts/spatialNavigation.ts +++ b/src/mainview/scripts/spatialNavigation.ts @@ -107,7 +107,7 @@ SpatialNavigation.setCurrentFocusedKey = (newFocusKey, focusDetails) => node: GetFocusedElement(newFocusKey) }; setCurrentFocusedKey(newFocusKey, focusDetails); - window.dispatchEvent(new CustomEvent('focuschanged', { + (GetFocusedElement(newFocusKey) ?? window).dispatchEvent(new CustomEvent('focuschanged', { bubbles: true, detail: details })); diff --git a/src/shared/constants.ts b/src/shared/constants.ts index acced4c..33531f7 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -108,12 +108,22 @@ export const EmulatorPackageSchema = z.object({ z.object({ type: z.literal('direct'), url: z.url(), + }), + z.object({ + type: z.literal('scoop'), + url: z.url(), }) ]))).optional(), systems: z.array(z.string()), bios: z.literal(["required", "optional"]).optional() }); +export const ScoopPackageSchema = z.object({ + version: z.string(), + url: z.url().optional(), + architecture: z.record(z.string(), z.object({ url: z.url(), hash: z.string().optional() })).optional() +}); + export const SystemInfoSchema = z.object({ battery: z.object({ percent: z.number(), diff --git a/src/shared/types..d.ts b/src/shared/types..d.ts index 760fc7f..7bc8ba7 100644 --- a/src/shared/types..d.ts +++ b/src/shared/types..d.ts @@ -18,7 +18,8 @@ declare interface FrontEndEmulator validSources: EmulatorSourceEntryType[]; integration?: { name: string; - version: string; + version?: string; + possible: boolean; }; }