fix: Fixed issues on windows

feat: Implemented mouse and gamepad automatic switching
fix: Made touch screen work better on the steam deck
This commit is contained in:
Simeon Radivoev 2026-02-24 18:58:48 +02:00
parent e4df8fb9fb
commit b4a89385d0
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
24 changed files with 334 additions and 137 deletions

View file

@ -48,7 +48,7 @@ export function AnimatedBackground (data: {
let backgroundElements: JSX.Element | undefined = undefined;
if (true)
{
backgroundElements = <div id="container">
backgroundElements = <div id="container" className='md:visible sm:invisible'>
<div id="container-inside">
<div className={bgColor} id="circle-small"></div>
<div className={bgColor} id="circle-medium"></div>
@ -66,7 +66,7 @@ export function AnimatedBackground (data: {
>
{!!lastBackgroundUrl && <div className='absolute w-full h-full' style={{ background: backgroundStyle(lastBackgroundUrl), zIndex: -4 }}></div>}
{!!backgroundUrl && <div key={backgroundUrl} className='absolute w-full h-full animate__animated animate__fadeIn' style={{ background: backgroundStyle(backgroundUrl), zIndex: -3 }}></div>}
{blurBackground && <div className={"absolute w-full h-full backdrop-blur-3xl"} style={{ zIndex: -2 }}></div>}
{blurBackground && <div className={"absolute w-full h-full backdrop-blur-3xl md:visible sm:invisible"} style={{ zIndex: -2 }}></div>}
{data.animated && animateBackground && <div className="absolute overflow-hidden w-full h-full" style={{ zIndex: -1 }}>
{backgroundElements}
</div>}

View file

@ -1,10 +1,11 @@
import
{
FocusContext,
FocusDetails,
useFocusable,
} from "@noriginmedia/norigin-spatial-navigation";
import { GameMeta } from "../../shared/constants";
import GameCard, { GameCardParams } from "./GameCard";
import GameCard, { GameCardFocusHandler, GameCardParams } from "./GameCard";
import { JSX } from "react";
import { twMerge } from "tailwind-merge";
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
@ -22,7 +23,7 @@ export function CardList (data: {
games: GameMetaExtra[];
grid?: boolean;
onSelectGame?: (id: string) => void;
onGameFocus?: (id: string, node: HTMLElement) => void;
onGameFocus?: GameCardFocusHandler;
className?: string;
})
{
@ -54,10 +55,10 @@ export function CardList (data: {
data-index={i}
title={g.title}
subtitle={g.subtitle ?? ""}
onFocus={(id, node) =>
onFocus={(id, node, details) =>
{
g.onFocus?.();
data.onGameFocus?.(id, node);
g.onFocus?.(details);
data.onGameFocus?.(id, node, details);
}}
onAction={handleAction}
preview={preview}
@ -74,7 +75,7 @@ export function CardList (data: {
ref={ref}
save-child-focus="session"
className={twMerge("my-6 items-center justify-center-safe h-(--game-card-height) ",
data.grid ? "card-grid h-fit gap-5" : 'card-list gap-6',
data.grid ? "card-grid h-fit gap-5" : 'card-list md:gap-6 sm:gap-2',
data.className
)}
onKeyDown={(e) =>

View file

@ -4,12 +4,13 @@ import { useSuspenseQuery } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { CardList, GameMetaExtra } from "./CardList";
import { SaveSource } from "../scripts/spatialNavigation";
import { GameCardFocusHandler } from "./GameCard";
export default function CollectionList (data: {
id: string,
setBackground: (url: string) => void;
className?: string;
onFocus?: (node: HTMLElement) => void;
onFocus?: GameCardFocusHandler;
})
{
const navigate = useNavigate();
@ -42,12 +43,12 @@ export default function CollectionList (data: {
SaveSource('game-list');
navigate({ to: `/collection/${id}`, viewTransition: { types: ['zoom-in'] } });
}}
onGameFocus={(id, node) =>
onGameFocus={(id, node, details) =>
{
data.setBackground(
`https://picsum.photos/id/${10 + (id ?? 0)}/1920/1080.webp`,
);
data.onFocus?.(node);
data.onFocus?.(id, node, details);
}}
/>
);

View file

@ -10,6 +10,7 @@ import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/
import { Router } from '..';
import { PopSource } from '../scripts/spatialNavigation';
import { GameListFilterType } from '@/shared/constants';
import { GameCardFocusHandler } from './GameCard';
export interface CollectionsDetailParams
{
@ -42,6 +43,14 @@ export function CollectionsDetail (data: CollectionsDetailParams)
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
const { shortcuts } = useShortcutContext();
const handleScroll: GameCardFocusHandler = (id, node, details) =>
{
if (!(details.nativeEvent instanceof MouseEvent))
{
node.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
};
return (
<FocusContext value={focusKey}>
<AnimatedBackground animated ref={ref} backgroundKey="home-background" className='flex'>
@ -56,7 +65,7 @@ export function CollectionsDetail (data: CollectionsDetailParams)
grid
setBackground={data.setBackground}
filters={data.filters}
onFocus={(node) => node.scrollIntoView({ block: 'center', behavior: 'smooth' })}
onFocus={handleScroll}
id={`${focusKey}-list`}>
</GameList>

View file

@ -30,7 +30,7 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class
const handleAction = data.action ? () => data.action?.({ close: context.close, focus: focusSelf }) : undefined;
const { ref, focused, focusSelf, focusKey, hasFocusedChild } = useFocusable({
focusKey: `${context.id}-list-option-${data.id}`,
onEnterPress: handleAction,
onEnterPress: data.shortcuts ? handleAction : undefined,
onFocus: handleFocus,
trackChildren: typeof data.content !== 'string'
});

View file

@ -36,7 +36,7 @@ function List (data: {
const { setCurrentPath, startingPath, allowNewFolderCreation, currentPath, isDirectoryPicker } = useContext(FilePickerContext);
const { ref, focusKey } = useFocusable({ focusKey: data.id, preferredChildFocusKey: `${data.id}...` });
const handleReturn = () => setCurrentPath(data.parentPath);
useShortcuts(focusKey, () => [{ label: "Directoy Up", button: GamePadButtonCode.L1, action: handleReturn }], [handleReturn]);
useShortcuts(focusKey, () => [{ label: "Directory Up", button: GamePadButtonCode.L1, action: handleReturn }], [handleReturn]);
return <div ref={ref}>
<FocusContext value={focusKey}>
<ContextList showCloseButton={false}
@ -62,21 +62,25 @@ function List (data: {
icon = <></>;
}
const shortcuts: Shortcut[] = [];
let action: () => void;
if (f.isDirectory)
{
shortcuts.push({ label: "Enter", button: GamePadButtonCode.A, action: () => setCurrentPath(fullPath) });
action = () => setCurrentPath(fullPath);
if (isDirectoryPicker)
shortcuts.push({ label: "Select", button: GamePadButtonCode.X, action: () => data.select(fullPath) });
} else
{
shortcuts.push({ label: "Select", button: GamePadButtonCode.A, action: () => data.select(fullPath) });
action = () => data.select(fullPath);
}
const entry: DialogEntry = {
content: f.name,
id: `${data.id}-${f.name}`,
type: 'primary',
icon,
shortcuts
shortcuts,
action
};
return entry;
}), ...(allowNewFolderCreation && currentPath ? [{
@ -157,7 +161,7 @@ function DriveElement (data: { id: string, isActive: boolean, label: string; onS
{
const { ref, focused } = useFocusable({ focusKey: data.id, onEnterPress: data.onSelect });
return <li ref={ref} onClick={data.onSelect} className={twMerge(
"flex bg-base-200 text-base-content rounded-full gap-2 items-center p-2 overflow-hidden max-w-xs",
"flex bg-base-200 text-base-content rounded-full gap-2 items-center p-2 px-4 overflow-hidden max-w-xs cursor-pointer text-nowrap hover:bg-primary/40",
classNames({
"bg-primary text-primary-content": data.isActive,
"ring-7 ring-base-content": focused
@ -238,8 +242,8 @@ export default function FilePicker (data: {
{
const [currentPath, setCurrentPath] = useState<string | undefined>(data.startingPath);
const { data: files, refetch: refetchFiles } = useQuery(filesQuery(currentPath, data.id));
const { data: drives } = useQuery(drivesQuery);
const { data: files, refetch: refetchFiles, isLoading: filesLoading } = useQuery(filesQuery(currentPath, data.id));
const { data: drives, isLoading: drivesLoading } = useQuery(drivesQuery);
const fullPath = files ? path.join(files.parentPath, files.name) : '';
const activeDrive = drives?.filter(d => !!d.mountPoint).sort((a, b) => b.mountPoint!.length - a.mountPoint!.length).filter(d => fullPath.startsWith(d.mountPoint!))[0];
@ -268,6 +272,7 @@ export default function FilePicker (data: {
}>{p}</a>
</li>)}
</ul>
{(filesLoading || drivesLoading) && <span className="loading loading-spinner loading-lg"></span>}
</div>}
<ListWithDrives

View file

@ -7,6 +7,7 @@ import SvgIcon from "./SvgIcon";
import classNames from "classnames";
import { useSearch } from "@tanstack/react-router";
import { useEffect } from "react";
import useActiveControl from "../scripts/gamepads";
function FilterCat (
data: {
@ -33,16 +34,19 @@ function FilterCat (
}
}, [filter]);
const { isMouse } = useActiveControl();
return (
<li
ref={ref}
onClick={focusSelf}
className={classNames(
"flex px-4 h-12 items-center justify-center rounded-full transition-all",
"flex md:px-4 items-center justify-center rounded-full transition-all md:text-lg",
"sm:text-xs sm:px-2",
{
"bg-base-content px-3 text-base-300 drop-shadow cursor-default":
focused || data.active,
"ring-primary ring-7": focused,
"ring-primary ring-7": focused && !isMouse,
"hover:bg-base-content/40 cursor-pointer": !focused,
},
)}
@ -70,13 +74,13 @@ export function FilterUI (data: {
return (
<div
ref={ref}
className="flex items-center justify-center gap-2"
className="flex items-center sm:justify-start md:justify-center sm:ml-[15%] md:ml-0 gap-2"
save-child-focus="session"
>
<FocusContext.Provider value={focusKey}>
<ul className="flex flex-row bg-base-100 rounded-full p-1 drop-shadow-sm">
<li className=" flex px-4 h-12 items-center justify-center rounded-full">
<SvgIcon className="size-8" icon="steamdeck_button_l1_outline" />
<ul className="flex flex-row bg-base-100 rounded-full p-1 drop-shadow-sm md:h-14 sm:h-8">
<li className=" flex px-4 items-center justify-center rounded-full">
<SvgIcon className="sm:size-4 md:size-8" icon="steamdeck_button_l1_outline" />
</li>
{Object.entries(data.options)?.map(([id, option]) => (
<FilterCat
@ -88,8 +92,8 @@ export function FilterUI (data: {
{...option}
/>
))}
<li className=" flex px-4 h-12 items-center justify-center rounded-full">
<SvgIcon className="size-8" icon="steamdeck_button_r1_outline" />
<li className="flex px-4 items-center justify-center rounded-full">
<SvgIcon className="sm:size-4 md:size-8" icon="steamdeck_button_r1_outline" />
</li>
</ul>
</FocusContext.Provider>

View file

@ -1,7 +1,8 @@
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { FocusDetails, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import classNames from "classnames";
import { JSX, useEffect } from "react";
import { JSX } from "react";
import { twMerge } from "tailwind-merge";
import useActiveControl from "../scripts/gamepads";
export function GameCardSkeleton ()
{
@ -16,6 +17,8 @@ export function GameCardSkeleton ()
);
}
export type GameCardFocusHandler = (id: string, node: HTMLElement, details: FocusDetails) => void;
export interface GameCardParams
{
title: string;
@ -27,7 +30,7 @@ export interface GameCardParams
id: string;
badges?: JSX.Element[];
className?: string;
onFocus?: (id: string, node: HTMLElement) => void;
onFocus?: GameCardFocusHandler;
onBlur?: (id: string) => void;
onAction?: () => void;
clickFocuses?: boolean;
@ -37,10 +40,11 @@ export default function GameCard (data: GameCardParams)
{
const { ref, focused, focusSelf } = useFocusable({
focusKey: data.focusKey,
onFocus: () => data.onFocus?.(data.id, ref.current as any),
onFocus: (l, p, detals) => data.onFocus?.(data.id, ref.current as any, detals),
onEnterPress: () => data.onAction?.(),
onBlur: () => data.onBlur?.(data.id)
});
const { isPointer } = useActiveControl();
return (
<li
@ -60,21 +64,24 @@ export default function GameCard (data: GameCardParams)
data.onAction?.();
}}
className={twMerge(
`game-card game-card-height flex flex-col justify-end z-5`,
`game-card bg-base-300 game-card-height flex flex-col justify-end z-5 ring-primary`,
'max-h-(--game-card-height) min-w-(--game-card-width) w-(--game-card-width)',
"overflow-hidden transition-all duration-200 drop-shadow-lg cursor-pointer",
focused ?
`focused animate-wiggle ring-7 bg-base-content text-base-300 ring-primary drop-shadow-xl drop-shadow-black/30 scale-102 z-10` :
"bg-base-300 hover:bg-base-100 hover:scale-102 text-base-content",
classNames({
"focused animate-wiggle ring-7 bg-base-content text-base-300 drop-shadow-xl drop-shadow-black/30 scale-102 z-10": focused && !isPointer,
"group hover:focused hover:animate-wiggle hover:ring-7 hover:bg-base-content hover:text-base-300 hover:drop-shadow-xl hover:drop-shadow-black/30 hover:scale-102 hover:z-10": isPointer,
"h-(--game-card-height)": typeof data.preview === "string"
}),
data.className
)}
>
<div className={twMerge("overflow-hidden bg-base-400 h-full rounded-t-xl rounded-b-md transition-all", focused ? "mt-2 mx-2" : "mt-2 mx-2")}>
<div className={twMerge(
"overflow-hidden bg-base-400 h-full rounded-t-xl rounded-b-md transition-all",
focused ? "md:mt-2 md:mx-2" : "md:mt-2 md:mx-2",
focused ? "sm:mt-1 sm:mx-1" : "sm:mt-1 sm:mx-1",
)}>
{typeof data.preview === "string" ? (
<img className={classNames({ "animate-rotate-small": focused })} src={data.preview} ></img>
<img className={classNames({ "animate-rotate-small": focused && !isPointer })} src={data.preview} ></img>
) : (
typeof data.preview === 'function' ? data.preview({ focused }) : data.preview
)}</div>
@ -83,18 +90,21 @@ export default function GameCard (data: GameCardParams)
{data.badges?.map((b, i) =>
<div key={i}
className={
twMerge("bg-base-100 text-base-content drop-shadow-lg overflow-hidden rounded-full p-1 last:mr-4 transition-colors",
classNames({ "bg-primary text-primary-content": focused }))}
twMerge("bg-base-100 text-base-content drop-shadow-lg overflow-hidden rounded-full p-1 md:last:mr-4 transition-colors",
classNames({
"bg-primary text-primary-content": focused && !isPointer,
"group-hover:bg-primary group-hover:text-primary-content": isPointer
}))}
>
{b}
</div>)
}
</div>
<div className="flex flex-col p-4">
<div className="text-xl font-bold text-nowrap text-ellipsis overflow-hidden">
<div className="flex flex-col md:p-4 sm:p-2">
<div className="md:text-xl sm:text-sm font-bold text-nowrap text-ellipsis overflow-hidden">
{data.title}
</div>
<div className="text-s">{data.subtitle}</div>
<div className="sm:text-xs md:text-sm text-nowrap">{data.subtitle}</div>
</div>
</li >
);

View file

@ -6,6 +6,7 @@ import { SaveSource } from "../scripts/spatialNavigation";
import { rommApi } from "../scripts/clientApi";
import { HardDrive } from "lucide-react";
import { JSX } from "react";
import { GameCardFocusHandler } from "./GameCard";
export interface GameListParams
{
@ -14,7 +15,7 @@ export interface GameListParams
grid?: boolean,
setBackground?: (url: string) => void;
onGameSelect?: (id: FrontEndId) => void;
onFocus?: (node: HTMLElement) => void;
onFocus?: GameCardFocusHandler;
className?: string;
}
@ -52,7 +53,7 @@ export function GameList (data: GameListParams)
type="game"
grid={data.grid}
className={data.className}
onGameFocus={(id, node) => data.onFocus?.(node)}
onGameFocus={data.onFocus}
games={games.data?.games
.map(
(g) =>
@ -69,7 +70,7 @@ export function GameList (data: GameListParams)
title: g.name ?? "",
subtitle: (
<div className="flex gap-1 items-center">
{!!g.path_platform_cover && <img className="size-4" src={`${RPC_URL(__HOST__)}${g.path_platform_cover}`} />}
{!!g.path_platform_cover && <img className="sm:hidden md:inline size-4" src={`${RPC_URL(__HOST__)}${g.path_platform_cover}`} />}
<p className="opacity-80">{g.platform_display_name}</p>
</div>
),

View file

@ -14,10 +14,6 @@ import
Bell,
Bluetooth,
Clock,
Lock,
Power,
ShieldAlert,
Sun,
User,
Wifi,
WifiHigh,
@ -29,9 +25,10 @@ import { useQuery } from "@tanstack/react-query";
import { getCurrentUserApiUsersMeGetOptions, statsApiStatsGetOptions } from "../../clients/romm/@tanstack/react-query.gen";
import { RPC_URL } from "../../shared/constants";
import { JSX, useEffect, useRef } from "react";
import { useLocation, useNavigate } from "@tanstack/react-router";
import { useNavigate } from "@tanstack/react-router";
import { SaveSource } from "../scripts/spatialNavigation";
import { systemApi } from "../scripts/clientApi";
import { twMerge } from "tailwind-merge";
function HeaderAvatar (data: {
id: string;
@ -116,7 +113,7 @@ function NotificationStatus ()
{
const hasUnread = false;
return <div className={classNames("p-2 rounded-full", { "bg-warning text-warning-content": hasUnread })}>
<Bell className="w-6 h-6" />
<Bell className="md:size-6 sm:size-4" />
</div>;
}
@ -170,13 +167,13 @@ function BluetoothStatus ()
function WiFiStatus ()
{
const { data: wifi } = useQuery({
const { data: wifi, isLoading } = useQuery({
queryKey: ['wifi'],
queryFn: () => systemApi.api.system.info.wifi.get().then(d => d.data),
refetchInterval: 3000
});
return <div>
return (!!wifi && wifi.length > 0) || isLoading ? <div>
{wifi?.map(w =>
{
const className = "w-6 h-6";
@ -195,7 +192,7 @@ function WiFiStatus ()
</div>;
})}
</div>;
</div> : undefined;
}
function BatteryStatus ()
@ -224,7 +221,7 @@ function BatteryStatus ()
batteryIcon = <BatteryMedium className={batteryClassName} />;
}
}
return <div className="flex gap-2 items-center">
return !!battery && battery.hasBattery && <div className="flex gap-2 items-center">
{batteryIcon}
<span className="font-semibold">{battery?.percent} %</span>
</div>;
@ -271,7 +268,9 @@ export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAcc
<FocusContext.Provider value={focusKey}>
<header
ref={ref}
className="h-14 mt-2 flex items-center justify-between text-white"
className={twMerge("md:relative md:h-14 md:mt-2 flex items-center justify-between text-white",
"sm:absolute sm:top-0 sm:right-0 sm:left-0"
)}
>
<div className="flex items-center gap-2 drop-shadow-sm">
{accounts?.map(a => <HeaderAvatar
@ -285,8 +284,8 @@ export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAcc
/>)}
{data.title}
</div>
<div className="flex items-center gap-2 text drop-shadow-sm">
<div className="flex gap-5 items-center">
<div className="flex items-center md:gap-2 sm:gap-1 text drop-shadow-sm">
<div className="flex md:gap-5 sm:gap-2 items-center">
<ClockStatus />
<WiFiStatus />
<BluetoothStatus />
@ -297,7 +296,7 @@ export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAcc
<div className="flex gap-2">
{data.buttonElements ?? data.buttons?.map(b => <RoundButton
key={b.id}
className="header-icon size-16"
className="header-icon md:size-16 sm:size-10"
id={b.id}
icon={b.icon}
external={b.external}

View file

@ -7,8 +7,9 @@ import { rommApi } from "../scripts/clientApi";
import { SaveSource } from "../scripts/spatialNavigation";
import { JSX } from "react";
import { HardDrive } from "lucide-react";
import { GameCardFocusHandler } from "./GameCard";
export function PlatformsList (data: { id: string, setBackground: (url: string) => void; className?: string; onFocus?: (node: HTMLElement) => void; })
export function PlatformsList (data: { id: string, setBackground: (url: string) => void; className?: string; onFocus?: GameCardFocusHandler; })
{
const navigate = useNavigate();
const { data: platforms } = useSuspenseQuery(
@ -29,7 +30,7 @@ export function PlatformsList (data: { id: string, setBackground: (url: string)
type="platform"
id={data.id}
className={data.className}
onGameFocus={(id, node) => data.onFocus?.(node)}
onGameFocus={data.onFocus}
games={platforms.sort((a, b) => a.updated_at.getTime() - b.updated_at.getTime())
.map((g) =>
{

View file

@ -1,11 +1,11 @@
import React, { MouseEventHandler } from "react";
import { MouseEventHandler } from "react";
import SvgIcon, { IconType } from "./SvgIcon";
import classNames from "classnames";
import { twMerge } from "tailwind-merge";
export default function ShortcutPrompt (data: {
id: string;
icon: IconType;
icon?: IconType;
label?: string;
className?: string;
onClick?: MouseEventHandler;
@ -17,14 +17,15 @@ export default function ShortcutPrompt (data: {
style={{ viewTransitionName: data.id }}
className={twMerge(
"flex md:gap-2 bg-base-100 text-base-content neutral-content md:pl-2 md:pr-3 md:py-1.5 rounded-full items-center md:text-lg drop-shadow-sm ring-[1px] ring-base-content/10 drop-shadow-black/30",
"sm:text-sm",
"sm:text-sm sm:p-1",
"xs:text-xs sm:p-1",
data.className,
classNames({
"hover:bg-base-300 cursor-pointer": !!data.onClick,
})
)}
>
<SvgIcon className="md:size-8 sm:size-6" icon={data.icon} />
{data.icon && <SvgIcon className="md:size-8 sm:size-6 xs:size-2" icon={data.icon} />}
{data.label}
</div>
);

View file

@ -1,4 +1,4 @@
import { GamepadButtonEvent } from '../scripts/gamepads';
import useActiveControl, { GamepadButtonEvent } from '../scripts/gamepads';
import { GamePadButtonCode, Shortcut } from '../scripts/shortcuts';
import ShortcutPrompt from './ShortcutPrompt';
import { IconType } from './SvgIcon';
@ -23,16 +23,38 @@ const iconMap: Record<GamePadButtonCode, IconType> = {
[GamePadButtonCode.Steam]: 'steamdeck_button_quickaccess'
};
const keyboardMap: Record<GamePadButtonCode, string> = {
[GamePadButtonCode.A]: 'ENTER',
[GamePadButtonCode.B]: 'ESC',
[GamePadButtonCode.X]: 'BACKSPACE',
[GamePadButtonCode.Y]: 'SPACE',
[GamePadButtonCode.L1]: 'Q',
[GamePadButtonCode.R1]: 'E',
[GamePadButtonCode.L2]: '',
[GamePadButtonCode.R2]: '',
[GamePadButtonCode.Select]: '',
[GamePadButtonCode.Start]: '',
[GamePadButtonCode.LJoy]: '',
[GamePadButtonCode.RJoy]: '',
[GamePadButtonCode.Up]: '',
[GamePadButtonCode.Down]: '',
[GamePadButtonCode.Left]: '',
[GamePadButtonCode.Right]: '',
[GamePadButtonCode.Steam]: ''
};
export default function Shortcuts (data: { shortcuts?: Shortcut[]; })
{
const { control } = useActiveControl();
const showKeyboard = control === 'keyboard' || control === 'mouse';
return (
<div className="flex gap-2 z-1000">
{data.shortcuts?.filter(s => !!s.label).map((s, i) => <ShortcutPrompt
key={s.button}
id={`shortcut-${s.button}`}
onClick={e => s.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: s.button, isClick: true }))}
icon={iconMap[s.button]}
label={s.label} />
icon={showKeyboard ? undefined : iconMap[s.button]}
label={showKeyboard ? `${keyboardMap[s.button]} | ${s.label}` : s.label} />
)}
</div>
);

View file

@ -12,7 +12,7 @@ export function Button (data: {
children?: any,
className?: string,
disabled?: boolean,
type: "reset" | "button" | "submit" | undefined;
type?: "reset" | "button" | "submit";
shortcutLabel?: string;
focusClassName?: string;
} & InteractParams & FocusParams)

View file

@ -22,6 +22,7 @@ export default function DownloadDirectoryOption (data: PathSettingsOptionParams)
type={data.type}
save={setSettingMutation.mutate}
allowNewFolderCreation={data.allowNewFolderCreation}
requireConfirmation={data.requireConfirmation}
isDirectoryPicker={true}
localValue={localValue}
setLocalValue={(v) =>