From 4da717c26d9840febd48ee87a6a493a3e1acc6b9 Mon Sep 17 00:00:00 2001 From: Simeon Radivoev Date: Tue, 5 May 2026 22:24:15 +0300 Subject: [PATCH] fix: Navigation blocking now working with focuesed input fields fix: Added warning to loging with lookup provider for better UX feat: Added ROMM Client API Token in plugin settings --- .../com.simeonradivoev.gameflow-deck.desktop | 2 +- src/bun/api/games/games.ts | 12 +++---- src/bun/api/games/services/statusService.ts | 7 ++-- src/bun/api/games/services/utils.ts | 6 ++-- src/bun/api/hooks/games.ts | 7 ++-- src/bun/api/jobs/import-job.ts | 7 ++-- .../com.simeonradivoev.gameflow.igdb/igdb.ts | 12 ++++--- .../com.simeonradivoev.gameflow.romm/romm.ts | 36 ++++++++++++------- src/mainview/components/GamepadKeyboard.tsx | 17 ++++++++- src/mainview/components/Header.tsx | 36 ++++++++++--------- src/mainview/components/game/GameLookup.tsx | 24 +++++++++---- src/mainview/components/options/Button.tsx | 2 ++ .../components/options/OptionInput.tsx | 2 +- src/mainview/routes/game/add.tsx | 36 ++++++++++--------- .../routes/game/update.$source.$id.tsx | 7 ++-- src/mainview/routes/settings/accounts.tsx | 8 ++++- src/mainview/routes/settings/route.tsx | 5 --- src/mainview/scripts/gamepads.ts | 4 +-- src/mainview/scripts/shortcuts.ts | 20 ++++++++--- src/mainview/scripts/utils.ts | 6 ++++ 20 files changed, 160 insertions(+), 96 deletions(-) diff --git a/.config/flatpak/com.simeonradivoev.gameflow-deck.desktop b/.config/flatpak/com.simeonradivoev.gameflow-deck.desktop index b5e2806..bed0068 100644 --- a/.config/flatpak/com.simeonradivoev.gameflow-deck.desktop +++ b/.config/flatpak/com.simeonradivoev.gameflow-deck.desktop @@ -4,4 +4,4 @@ Comment=GameFlow Deck Exec=gameflow Icon=com.simeonradivoev.gameflow-deck Type=Application -Categories=Games; \ No newline at end of file +Categories=Game; \ No newline at end of file diff --git a/src/bun/api/games/games.ts b/src/bun/api/games/games.ts index 88e9d50..1b73e78 100644 --- a/src/bun/api/games/games.ts +++ b/src/bun/api/games/games.ts @@ -499,17 +499,17 @@ export default new Elysia() }, { body: z.object({ source: z.string(), id: z.string() }) }) .get('/lookup', async ({ query: { search } }) => { - const matches: GameLookup[] = []; - await plugins.hooks.games.gameLookup.promise({ search, matches }); - return matches; + const matches = new Map(); + await plugins.hooks.games.gameLookup.promise(matches, { search }); + return { hadMatchers: matches.size > 0, matches: Array.from(matches.values()).flatMap(m => m) }; }, { query: z.object({ search: z.string() }) }) .get('/lookup/:source/:id', async ({ params: { source, id } }) => { - const matches: GameLookup[] = []; - await plugins.hooks.games.gameLookup.promise({ source, id, matches }); - return matches; + const matches = new Map(); + await plugins.hooks.games.gameLookup.promise(matches, { source, id }); + return Array.from(matches.values()).flatMap(m => m); }) .post('/game/:source/:id/play', async ({ params: { id, source }, body, set }) => { diff --git a/src/bun/api/games/services/statusService.ts b/src/bun/api/games/services/statusService.ts index 291bf56..05bade3 100644 --- a/src/bun/api/games/services/statusService.ts +++ b/src/bun/api/games/services/statusService.ts @@ -48,9 +48,10 @@ export async function customUpdate (source: string, id: string, destination: str const localGame = await getLocalGame(source, id); if (!localGame) throw new Error("Could not find Local Game"); - const matches: GameLookup[] = []; - await plugins.hooks.games.gameLookup.promise({ source: destination, id: destinationId, matches }); - if (matches.length <= 0) throw new Error("Could not find destination"); + const matchesMap = new Map(); + await plugins.hooks.games.gameLookup.promise(matchesMap, { source: destination, id: destinationId }); + const matches = matchesMap.values().next().value; + if (!matches || matches?.length <= 0) throw new Error("Could not find destination"); const match = matches[0]; await db.transaction(async (tx) => diff --git a/src/bun/api/games/services/utils.ts b/src/bun/api/games/services/utils.ts index 0e1aba9..fd4b2d9 100644 --- a/src/bun/api/games/services/utils.ts +++ b/src/bun/api/games/services/utils.ts @@ -441,9 +441,9 @@ export async function createLocalGame (info: { if (info.screenshotUrls.length <= 0 && info.igdb_id) { - const matches: GameLookup[] = []; - await plugins.hooks.games.gameLookup.promise({ source: 'igdb', id: String(info.igdb_id), matches }); - info.screenshotUrls.push(...matches[0].screenshotUrls); + const matches = new Map(); + await plugins.hooks.games.gameLookup.promise(matches, { source: 'igdb', id: String(info.igdb_id) }); + info.screenshotUrls.push(...(matches.values().next().value?.[0].screenshotUrls ?? [])); } // pre-fetch screenshots diff --git a/src/bun/api/hooks/games.ts b/src/bun/api/hooks/games.ts index 3645d93..bb1f4bc 100644 --- a/src/bun/api/hooks/games.ts +++ b/src/bun/api/hooks/games.ts @@ -1,6 +1,6 @@ import { EmulatorPackageType, GameListFilterType } from '@/shared/constants'; import { CommandEntry, DownloadInfo, EmulatorSourceEntryType, EmulatorSupport, EmulatorSystem, FrontEndCollection, FrontEndFilterSets, FrontEndGameType, FrontEndGameTypeDetailed, FrontEndGameTypeWithIds, FrontEndId, FrontEndPlatformType, GameLookup, SaveFileChange, SaveSlots } from '@/shared/types'; -import { SyncBailHook, AsyncSeriesHook, AsyncSeriesBailHook, Hook } from 'tapable'; +import { SyncBailHook, AsyncSeriesHook, AsyncSeriesBailHook, Hook, AsyncSeriesWaterfallHook } from 'tapable'; export default class GameHooks { @@ -96,12 +96,11 @@ export default class GameHooks name?: string; family_name?: string; } | undefined>(['ctx']); - gameLookup = new AsyncSeriesHook<[ctx: { + gameLookup = new AsyncSeriesWaterfallHook<[matches: Map, ctx: { source?: string, id?: string; search?: string; - matches: GameLookup[]; - }]>(['ctx']); + }]>(['matches', 'ctx']); fetchPlatforms = new AsyncSeriesHook<[ctx: { platforms: FrontEndPlatformType[]; }]>(['ctx']); diff --git a/src/bun/api/jobs/import-job.ts b/src/bun/api/jobs/import-job.ts index a0adee6..3e608a4 100644 --- a/src/bun/api/jobs/import-job.ts +++ b/src/bun/api/jobs/import-job.ts @@ -32,9 +32,10 @@ export class ImportJob implements IJob, str async start (context: JobContext, string>, z.infer, string>): Promise { - const matches: GameLookup[] = []; - await plugins.hooks.games.gameLookup.promise({ source: this.source, id: this.id, matches }); - if (matches.length <= 0) throw Error("Could not Find Game"); + const matchesMap = new Map(); + await plugins.hooks.games.gameLookup.promise(matchesMap, { source: this.source, id: this.id }); + const matches = matchesMap.values().next().value; + if (!matches || matches.length <= 0) throw Error("Could not Find Game"); const match = matches[0]; let cover: Buffer | undefined = undefined; diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/igdb.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/igdb.ts index ce9cd6b..7c39e01 100644 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/igdb.ts +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/igdb.ts @@ -43,13 +43,13 @@ export default class IgdbIntegration implements PluginType { await checkLoginAndRefreshTwitch(); - ctx.hooks.games.gameLookup.tapPromise(desc.name, async ({ source, id, search, matches }) => + ctx.hooks.games.gameLookup.tapPromise(desc.name, async (matches, { source, id, search }) => { - if (!process.env.TWITCH_CLIENT_ID) return; + if (!process.env.TWITCH_CLIENT_ID) return matches; const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); if (!access_token) { - return; + return matches; } if ((source === 'igdb' && id) || search) @@ -62,7 +62,7 @@ export default class IgdbIntegration implements PluginType ...(source === 'igdb' && id ? [igdb.where('id', '=', Number(id))] : []), igdb.limit(10)).execute()); - matches.push(...games.filter(g => !!g.name) + matches.set(desc.name, games.filter(g => !!g.name) .map(g => { const lookup: GameLookup = { @@ -89,8 +89,10 @@ export default class IgdbIntegration implements PluginType return lookup; })); - return; + return matches; } + + return matches.set(desc.name, []); }); ctx.hooks.games.platformLookup.tapPromise(desc.name, async ({ source, id, slug }) => diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts index a627199..ac9474f 100644 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts @@ -15,9 +15,11 @@ import { validateGameSource } from "@/bun/api/games/services/statusService"; import z from "zod"; import { checkLoginAndRefreshRomm } from "@/bun/api/auth"; import { DownloadFileEntry, DownloadInfo, FrontEndCollection, FrontEndGameType, FrontEndGameTypeDetailed, FrontEndGameTypeDetailedAchievement, FrontEndGameTypeWithIds, FrontEndPlatformType } from "@/shared/types"; +import Conf from "conf"; const SettingsSchema = z.object({ - savesSync: z.boolean().default(false).describe("Experimental save sync support") + savesSync: z.boolean().default(false).describe("Experimental save sync support"), + clientApiToken: z.string().optional().describe("Generate a long lived token from the ROMM server") }); type SettingsType = z.infer; @@ -39,26 +41,34 @@ export default class RommIntegration implements PluginType return true; } - async updateClient () + async getAccessToken (config: Conf) + { + if (process.env.ROMM_CLIENT_TOKEN) return process.env.ROMM_CLIENT_TOKEN; + const client_token = await config.get('clientApiToken'); + if (client_token) return client_token; + return (await secrets.get({ service: 'gameflow', name: 'romm_access_token' })) ?? undefined; + } + + async updateClient (pluginConfig: Conf) { client.setConfig({ baseUrl: config.get('rommAddress'), - async auth (auth) + auth: (auth) => { if (auth.scheme === 'bearer') { - return (await secrets.get({ service: 'gameflow', name: 'romm_access_token' })) ?? undefined; + return this.getAccessToken(pluginConfig); } } }); } - async getAuthToken () + async getAuthToken (config: Conf) { return getAuthToken({ scheme: 'bearer', type: "http" - }, async (a) => (await secrets.get({ service: "gameflow", name: 'romm_access_token' })) ?? undefined); + }, async (a) => this.getAccessToken(config)); } async getAllRommPlatforms () @@ -146,9 +156,9 @@ export default class RommIntegration implements PluginType { this.isSteamDeck = isSteamDeckGameMode(); ctx.setProgress(0, "Logging Into Romm"); - await this.updateClient(); + await this.updateClient(ctx.config); await checkLoginAndRefreshRomm(); - await this.updateClient(); + await this.updateClient(ctx.config); ctx.hooks.games.fetchGames.tapPromise(desc.name, async ({ query, games }) => { @@ -199,7 +209,7 @@ export default class RommIntegration implements PluginType { if (!await this.checkRemote()) return; if (service !== 'romm') return; - await this.updateClient(); + await this.updateClient(ctx.config); }); ctx.hooks.games.fetchGame.tapPromise(desc.name, async ({ source, id }) => @@ -273,7 +283,7 @@ export default class RommIntegration implements PluginType system_slug: rommPlatform.slug, metadata: rom.metadatum, files, - auth: await this.getAuthToken(), + auth: await this.getAuthToken(ctx.config), extract_path, id: "romm" }; @@ -310,7 +320,7 @@ export default class RommIntegration implements PluginType } } - if (files.length > 0) return { files, auth: await this.getAuthToken() }; + if (files.length > 0) return { files, auth: await this.getAuthToken(ctx.config) }; }); ctx.hooks.games.fetchRecommendedGamesForGame.tapPromise(desc.name, async ({ game, games }) => @@ -445,7 +455,7 @@ export default class RommIntegration implements PluginType const rommSlot = saveFiles.data.slots.find(s => s.slot === 'gameflow' && s.latest.file_name_no_tags === slot); if (rommSlot) { - const auth = await this.getAuthToken(); + const auth = await this.getAuthToken(ctx.config); const headers: Record = {}; if (auth) headers['Authorization'] = auth; @@ -535,7 +545,7 @@ export default class RommIntegration implements PluginType url.searchParams.set('emulator', command.emulator); url.searchParams.set('overwrite', "true"); - const auth = await this.getAuthToken(); + const auth = await this.getAuthToken(ctx.config); const headers: Record = {}; if (auth) headers['Authorization'] = auth; diff --git a/src/mainview/components/GamepadKeyboard.tsx b/src/mainview/components/GamepadKeyboard.tsx index 4125db3..7f3b994 100644 --- a/src/mainview/components/GamepadKeyboard.tsx +++ b/src/mainview/components/GamepadKeyboard.tsx @@ -1,5 +1,5 @@ import { createRef, JSX, RefObject, useEffect, useRef, useState } from "react"; -import useActiveControl from "../scripts/gamepads"; +import useActiveControl, { GamepadButtonEvent } from "../scripts/gamepads"; import { oneShot } from "../scripts/audio/audio"; import { ArrowLeft, ArrowRight, CornerDownLeft, Delete, Space } from "lucide-react"; import { GamePadButtonCode } from "../scripts/shortcuts"; @@ -457,11 +457,26 @@ export function GamepadKeyboard () if (!disposed && !hidden) requestAnimationFrame(update); + const gamepadButtonHandler = (e: Event) => + { + if (!(e instanceof GamepadButtonEvent) || disposed || hidden) return; + if (e.button === GamePadButtonCode.L1 || e.button === GamePadButtonCode.R1 || e.button === GamePadButtonCode.L2 || e.button === GamePadButtonCode.R2) + { + e.preventDefault(); + e.stopImmediatePropagation(); + } + + }; + window.addEventListener('gamepadbuttondown', gamepadButtonHandler); + window.addEventListener('gamepadbuttonup', gamepadButtonHandler); + return () => { disposed = true; Object.values(buttonRepeatTimeout).forEach(v => clearTimeout(v)); Object.values(actionRepeatTimeout).forEach(v => clearTimeout(v)); + window.removeEventListener('gamepadbuttondown', gamepadButtonHandler); + window.removeEventListener('gamepadbuttonup', gamepadButtonHandler); }; }, [focusedInput, elements, shift, characters, hidden]); diff --git a/src/mainview/components/Header.tsx b/src/mainview/components/Header.tsx index b9e29d9..932dada 100644 --- a/src/mainview/components/Header.tsx +++ b/src/mainview/components/Header.tsx @@ -230,14 +230,6 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; }) const accounts: HeaderAccount[] = []; if (data.accounts) accounts.push(...data.accounts); - const router = useRouter(); - - const { ref } = useFocusable({ - focusKey: 'accounts', - onEnterPress: handleSelect, - focusable: accounts.length > 0 - }); - if (rommUser.data?.hasLogin || rommUser.isError) { accounts.push({ @@ -254,6 +246,16 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; }) type: 'secondary' }); } + const hasAccounts = accounts.length > 0; + const router = useRouter(); + + const { ref } = useFocusable({ + focusKey: 'accounts', + onEnterPress: handleSelect, + focusable: hasAccounts + }); + + return
{accounts?.map(a => -
+ +
+ {data.title} , id: "header-settings-btn", action: goToSettings, external: true }]} /> -
- + + +
); } diff --git a/src/mainview/components/game/GameLookup.tsx b/src/mainview/components/game/GameLookup.tsx index 15abb1c..3b15009 100644 --- a/src/mainview/components/game/GameLookup.tsx +++ b/src/mainview/components/game/GameLookup.tsx @@ -1,13 +1,15 @@ import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { useQuery } from "@tanstack/react-query"; -import { Check, Search } from "lucide-react"; +import { Check, Search, TriangleAlert } from "lucide-react"; 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 { gameLookupQuery } from "@/mainview/scripts/queries/romm"; +import { Button } from "../options/Button"; +import { useNavigate } from "@tanstack/react-router"; function Result (data: { match: GameLookup; @@ -65,18 +67,26 @@ export default function GameLookupElement (data: { }) { const { data: lookups, isFetching } = useQuery({ ...gameLookupQuery(data.search), staleTime: 1000 * 60 * 60 }); + const navigate = useNavigate(); return
{isFetching ? : }Results
    - {lookups?.map((l, i) => - { - return + {!Array.isArray(lookups) && <> + + {!isFetching && !lookups?.hadMatchers &&
    + +
    } + {lookups?.matches.map((l, i) => { - data.onSelect(l); - }} />; - })} + return + { + data.onSelect(l); + }} />; + })} + + }
; } \ No newline at end of file diff --git a/src/mainview/components/options/Button.tsx b/src/mainview/components/options/Button.tsx index 7970688..de07bdf 100644 --- a/src/mainview/components/options/Button.tsx +++ b/src/mainview/components/options/Button.tsx @@ -27,6 +27,7 @@ export function Button (data: { children?: any, className?: string, disabled?: boolean, + external?: boolean, type?: "reset" | "button" | "submit"; style?: ButtonStyle, shortcutLabel?: string; @@ -65,6 +66,7 @@ export function Button (data: { focused ? data.focusClassName : undefined, classNames({ "btn-accent": focused, + "focusable focusable-primary focusable-hover": data.external }, data.className))} type={data.type ?? 'button'} > diff --git a/src/mainview/components/options/OptionInput.tsx b/src/mainview/components/options/OptionInput.tsx index 54caee8..801e639 100644 --- a/src/mainview/components/options/OptionInput.tsx +++ b/src/mainview/components/options/OptionInput.tsx @@ -121,7 +121,7 @@ export function OptionInput (data: { step={data.step} data-focus={"input"} name={data.name} - value={String(data.value)} + value={data.value === undefined ? undefined : String(data.value)} defaultValue={typeof data.defaultValue === 'string' ? data.defaultValue : undefined} type={data.type} autoComplete={data.autocomplete} diff --git a/src/mainview/routes/game/add.tsx b/src/mainview/routes/game/add.tsx index bf5f481..3a6a2f8 100644 --- a/src/mainview/routes/game/add.tsx +++ b/src/mainview/routes/game/add.tsx @@ -5,6 +5,7 @@ import { StickyHeaderUI } from '@/mainview/components/Header'; import LoadingScreen from '@/mainview/components/LoadingScreen'; import { Button } from '@/mainview/components/options/Button'; import { PathSettingsOptionBase } from '@/mainview/components/options/PathSettingsOption'; +import SelectMenu from '@/mainview/components/SelectMenu'; import { FloatingShortcuts } from '@/mainview/components/Shortcuts'; import { oneShot } from '@/mainview/scripts/audio/audio'; import { addManualGameMutation, allGamesInvalidateQuery, gameLookupDetails, platformLookupMatchQuery } from '@/mainview/scripts/queries/romm'; @@ -252,7 +253,6 @@ function Location () function Details (data: {}) { - const { ref, focusKey } = useFocusable({ focusKey: 'add-game-details-section' }); const state = Route.useSearch(); const step = state.step ?? 0; @@ -318,11 +318,13 @@ function Steps () const state = Route.useSearch(); const step = state.step ?? 0; const { ref, focusKey } = useFocusable({ focusKey: "steps", preferredChildFocusKey: `step-${step}`, saveLastFocusedChild: false }); - return
    - - {StepDetails.map((s, i) => )} - -
; + return
+ +
    + + {StepDetails.map((s, i) => )} + +
; } function RouteComponent () @@ -374,23 +376,23 @@ function RouteComponent () } ], [step]); - return
+ return
-
- -
+
+ + + {isAddingGame && +
+ +
Adding Game
+
+
} +
- - {isAddingGame && -
- -
Adding Game
-
-
}
; } diff --git a/src/mainview/routes/game/update.$source.$id.tsx b/src/mainview/routes/game/update.$source.$id.tsx index fb9ffba..867112a 100644 --- a/src/mainview/routes/game/update.$source.$id.tsx +++ b/src/mainview/routes/game/update.$source.$id.tsx @@ -1,7 +1,7 @@ import { AnimatedBackground } from '@/mainview/components/AnimatedBackground'; import { AutoFocus } from '@/mainview/components/AutoFocus'; import GameLookupElement from '@/mainview/components/game/GameLookup'; -import { StickyHeaderUI } from '@/mainview/components/Header'; +import { HeaderUI, StickyHeaderUI } 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'; @@ -9,7 +9,7 @@ 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, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import toast from 'react-hot-toast'; export const Route = createFileRoute('/game/update/$source/$id')({ @@ -44,8 +44,9 @@ function RouteComponent () return +
- + void, url: string; endsAt: Date; startedAt: Date; code?: string; }) @@ -221,6 +225,8 @@ function RouteComponent () } type="text" />} /> } type="password" placeholder="Password" />} /> + +
For Romm Client API Token open plugin settings
} /> diff --git a/src/mainview/routes/settings/route.tsx b/src/mainview/routes/settings/route.tsx index 0283ac3..fd83578 100644 --- a/src/mainview/routes/settings/route.tsx +++ b/src/mainview/routes/settings/route.tsx @@ -26,8 +26,6 @@ import } from "lucide-react"; import { JSX, useMemo } from "react"; import { twMerge } from "tailwind-merge"; -import z from "zod"; -import { SettingsSchema } from "../../../shared/constants"; import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts"; import Shortcuts from "@/mainview/components/Shortcuts"; import { HandleGoBack } from "@/mainview/scripts/utils"; @@ -37,9 +35,6 @@ import SelectMenu from "@/mainview/components/SelectMenu"; export const Route = createFileRoute("/settings")({ component: SettingsUI, - validateSearch: z.object({ - focus: z.keyof(SettingsSchema).optional() - }), staticData: { enterSound: 'openSettings' } diff --git a/src/mainview/scripts/gamepads.ts b/src/mainview/scripts/gamepads.ts index 08321a3..5959d90 100644 --- a/src/mainview/scripts/gamepads.ts +++ b/src/mainview/scripts/gamepads.ts @@ -1,7 +1,7 @@ import { getCurrentFocusKey, navigateByDirection } from "@noriginmedia/norigin-spatial-navigation"; import { GetFocusedElement } from "./spatialNavigation"; import { useEffect, useState } from "react"; -import { getLocalSetting, mobileCheck } from "./utils"; +import { getLocalSetting, isTextInputFocused, mobileCheck } from "./utils"; import { oneShot } from "./audio/audio"; import { Router } from "@/mainview"; @@ -98,7 +98,7 @@ const throttleMap = new Map(); const throttleAcceleration = new Map(); function throttleNav (key: string, dir: string, event: Event) { - if (document.activeElement && document.activeElement instanceof HTMLInputElement) + if (isTextInputFocused()) { return false; } diff --git a/src/mainview/scripts/shortcuts.ts b/src/mainview/scripts/shortcuts.ts index 35316b7..c947d23 100644 --- a/src/mainview/scripts/shortcuts.ts +++ b/src/mainview/scripts/shortcuts.ts @@ -2,6 +2,7 @@ import { DependencyList, useEffect, useState } from "react"; import { GamepadButtonEvent } from "./gamepads"; import { dispatchFocusedEvent, GetFocusedTree } from "./spatialNavigation"; import { getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation"; +import { isTextInputFocused } from "./utils"; const shortcutMap = new Map Shortcut[])[]>(); const conflictSet = new Set(); @@ -123,12 +124,21 @@ export function useShortcutContext () if (e.key === 'Escape') { shortcuts.get(GamePadButtonCode.B)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.B })); - } else if (e.key === 'Backspace') + } else { - shortcuts.get(GamePadButtonCode.X)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.X })); - } else if (e.key === ' ') - { - shortcuts.get(GamePadButtonCode.Y)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.Y })); + // We use backspace and space in typing + if (isTextInputFocused()) + { + return false; + } + + if (e.key === 'Backspace') + { + shortcuts.get(GamePadButtonCode.X)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.X })); + } else if (e.key === ' ') + { + shortcuts.get(GamePadButtonCode.Y)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.Y })); + } } }; diff --git a/src/mainview/scripts/utils.ts b/src/mainview/scripts/utils.ts index 7f4bf68..4859ddf 100644 --- a/src/mainview/scripts/utils.ts +++ b/src/mainview/scripts/utils.ts @@ -13,6 +13,12 @@ export type ScrollSaveParams = { storage?: "session" | "local"; shouldSave?: boolean; }; + +export function isTextInputFocused () +{ + return document.activeElement && document.activeElement instanceof HTMLInputElement; +} + export function useScrollSave (data: ScrollSaveParams) { useEffect(() =>