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:
parent
e4df8fb9fb
commit
b4a89385d0
24 changed files with 334 additions and 137 deletions
|
|
@ -32,7 +32,7 @@ export async function getDevices (): Promise<Drive[]>
|
||||||
{
|
{
|
||||||
const blockDevicesRaw = await si.blockDevices();
|
const blockDevicesRaw = await si.blockDevices();
|
||||||
const layout = await si.diskLayout();
|
const layout = await si.diskLayout();
|
||||||
const blockDevices = blockDevicesRaw.filter(l => l.device && l.type === 'part' && l.mount);
|
const blockDevices = blockDevicesRaw.filter(l => l.device && (l.type === 'part' || l.type === 'disk') && l.mount);
|
||||||
const fsSizes = await si.fsSize();
|
const fsSizes = await si.fsSize();
|
||||||
const sizes = new Map(fsSizes.map(s => [s.mount, s]));
|
const sizes = new Map(fsSizes.map(s => [s.mount, s]));
|
||||||
const layoutMap = new Map(layout.map(l => [l.device, l]));
|
const layoutMap = new Map(layout.map(l => [l.device, l]));
|
||||||
|
|
@ -65,6 +65,8 @@ export async function getDevicesCurated (): Promise<Drive[]>
|
||||||
const devices = await getDevices();
|
const devices = await getDevices();
|
||||||
drives.push(...devices.filter(d => d.hasWriteAccess));
|
drives.push(...devices.filter(d => d.hasWriteAccess));
|
||||||
|
|
||||||
|
if (process.platform !== 'win32')
|
||||||
|
{
|
||||||
const homeDir = os.homedir();
|
const homeDir = os.homedir();
|
||||||
const homeDirDevice = devices.filter(d => d.mountPoint).reverse()
|
const homeDirDevice = devices.filter(d => d.mountPoint).reverse()
|
||||||
.find(d => homeDir.startsWith(d.mountPoint!));
|
.find(d => homeDir.startsWith(d.mountPoint!));
|
||||||
|
|
@ -86,6 +88,7 @@ export async function getDevicesCurated (): Promise<Drive[]>
|
||||||
hasWriteAccess
|
hasWriteAccess
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return drives;
|
return drives;
|
||||||
}
|
}
|
||||||
|
|
@ -12,7 +12,6 @@ import { getDevices, getDevicesCurated } from "./drives";
|
||||||
import getFolderSize from "get-folder-size";
|
import getFolderSize from "get-folder-size";
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
|
|
||||||
// steam://open/keyboard?XPosition=%i&YPosition=%i&Width=%i&Height=%i&Mode=%d
|
|
||||||
export const system = new Elysia({ prefix: '/api/system' })
|
export const system = new Elysia({ prefix: '/api/system' })
|
||||||
.post('/show_keyboard', async ({ body: { XPosition, YPosition, Width, Height } }) =>
|
.post('/show_keyboard', async ({ body: { XPosition, YPosition, Width, Height } }) =>
|
||||||
{
|
{
|
||||||
|
|
@ -67,12 +66,23 @@ export const system = new Elysia({ prefix: '/api/system' })
|
||||||
.get('/drives', async () =>
|
.get('/drives', async () =>
|
||||||
{
|
{
|
||||||
const drives = await getDevices();
|
const drives = await getDevices();
|
||||||
|
if (process.platform === 'win32')
|
||||||
|
return drives.map(d =>
|
||||||
|
{
|
||||||
|
d.mountPoint += '/';
|
||||||
|
return d;
|
||||||
|
});
|
||||||
return drives;
|
return drives;
|
||||||
})
|
})
|
||||||
|
// Drives that are vaiable for downloads
|
||||||
.get('/drives/download', async () =>
|
.get('/drives/download', async () =>
|
||||||
{
|
{
|
||||||
const drives = await getDevicesCurated();
|
const drives = await getDevicesCurated();
|
||||||
const downloadsPath = config.get('downloadPath');
|
let downloadsPath = config.get('downloadPath');
|
||||||
|
if (!path.isAbsolute(downloadsPath))
|
||||||
|
{
|
||||||
|
downloadsPath = path.resolve(process.cwd(), downloadsPath);
|
||||||
|
}
|
||||||
const currentDownloadsSize = await getFolderSize(downloadsPath);
|
const currentDownloadsSize = await getFolderSize(downloadsPath);
|
||||||
let used = false;
|
let used = false;
|
||||||
const drivesDownload: DownloadsDrive[] = drives
|
const drivesDownload: DownloadsDrive[] = drives
|
||||||
|
|
@ -115,6 +125,7 @@ export const system = new Elysia({ prefix: '/api/system' })
|
||||||
drives: drivesDownload,
|
drives: drivesDownload,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
// Create Folder
|
||||||
.put('/dirs', async ({ body: { dirname, name } }) =>
|
.put('/dirs', async ({ body: { dirname, name } }) =>
|
||||||
{
|
{
|
||||||
await fs.mkdir(path.join(dirname, name));
|
await fs.mkdir(path.join(dirname, name));
|
||||||
|
|
@ -123,7 +134,11 @@ export const system = new Elysia({ prefix: '/api/system' })
|
||||||
})
|
})
|
||||||
.get('/dirs', async ({ query: { path: startingPath } }) =>
|
.get('/dirs', async ({ query: { path: startingPath } }) =>
|
||||||
{
|
{
|
||||||
const currentPath = startingPath ?? dirname(Bun.main);
|
let currentPath = startingPath ?? dirname(process.cwd());
|
||||||
|
if (!path.isAbsolute(currentPath))
|
||||||
|
{
|
||||||
|
currentPath = path.resolve(process.cwd(), currentPath);
|
||||||
|
}
|
||||||
const paths = await fs.readdir(currentPath, { withFileTypes: true });
|
const paths = await fs.readdir(currentPath, { withFileTypes: true });
|
||||||
return {
|
return {
|
||||||
name: path.basename(currentPath),
|
name: path.basename(currentPath),
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export function AnimatedBackground (data: {
|
||||||
let backgroundElements: JSX.Element | undefined = undefined;
|
let backgroundElements: JSX.Element | undefined = undefined;
|
||||||
if (true)
|
if (true)
|
||||||
{
|
{
|
||||||
backgroundElements = <div id="container">
|
backgroundElements = <div id="container" className='md:visible sm:invisible'>
|
||||||
<div id="container-inside">
|
<div id="container-inside">
|
||||||
<div className={bgColor} id="circle-small"></div>
|
<div className={bgColor} id="circle-small"></div>
|
||||||
<div className={bgColor} id="circle-medium"></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>}
|
{!!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>}
|
{!!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 }}>
|
{data.animated && animateBackground && <div className="absolute overflow-hidden w-full h-full" style={{ zIndex: -1 }}>
|
||||||
{backgroundElements}
|
{backgroundElements}
|
||||||
</div>}
|
</div>}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
FocusContext,
|
FocusContext,
|
||||||
|
FocusDetails,
|
||||||
useFocusable,
|
useFocusable,
|
||||||
} from "@noriginmedia/norigin-spatial-navigation";
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { GameMeta } from "../../shared/constants";
|
import { GameMeta } from "../../shared/constants";
|
||||||
import GameCard, { GameCardParams } from "./GameCard";
|
import GameCard, { GameCardFocusHandler, GameCardParams } from "./GameCard";
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
|
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
|
||||||
|
|
@ -22,7 +23,7 @@ export function CardList (data: {
|
||||||
games: GameMetaExtra[];
|
games: GameMetaExtra[];
|
||||||
grid?: boolean;
|
grid?: boolean;
|
||||||
onSelectGame?: (id: string) => void;
|
onSelectGame?: (id: string) => void;
|
||||||
onGameFocus?: (id: string, node: HTMLElement) => void;
|
onGameFocus?: GameCardFocusHandler;
|
||||||
className?: string;
|
className?: string;
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
|
|
@ -54,10 +55,10 @@ export function CardList (data: {
|
||||||
data-index={i}
|
data-index={i}
|
||||||
title={g.title}
|
title={g.title}
|
||||||
subtitle={g.subtitle ?? ""}
|
subtitle={g.subtitle ?? ""}
|
||||||
onFocus={(id, node) =>
|
onFocus={(id, node, details) =>
|
||||||
{
|
{
|
||||||
g.onFocus?.();
|
g.onFocus?.(details);
|
||||||
data.onGameFocus?.(id, node);
|
data.onGameFocus?.(id, node, details);
|
||||||
}}
|
}}
|
||||||
onAction={handleAction}
|
onAction={handleAction}
|
||||||
preview={preview}
|
preview={preview}
|
||||||
|
|
@ -74,7 +75,7 @@ export function CardList (data: {
|
||||||
ref={ref}
|
ref={ref}
|
||||||
save-child-focus="session"
|
save-child-focus="session"
|
||||||
className={twMerge("my-6 items-center justify-center-safe h-(--game-card-height) ",
|
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
|
data.className
|
||||||
)}
|
)}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { CardList, GameMetaExtra } from "./CardList";
|
import { CardList, GameMetaExtra } from "./CardList";
|
||||||
import { SaveSource } from "../scripts/spatialNavigation";
|
import { SaveSource } from "../scripts/spatialNavigation";
|
||||||
|
import { GameCardFocusHandler } from "./GameCard";
|
||||||
|
|
||||||
export default function CollectionList (data: {
|
export default function CollectionList (data: {
|
||||||
id: string,
|
id: string,
|
||||||
setBackground: (url: string) => void;
|
setBackground: (url: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
onFocus?: (node: HTMLElement) => void;
|
onFocus?: GameCardFocusHandler;
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -42,12 +43,12 @@ export default function CollectionList (data: {
|
||||||
SaveSource('game-list');
|
SaveSource('game-list');
|
||||||
navigate({ to: `/collection/${id}`, viewTransition: { types: ['zoom-in'] } });
|
navigate({ to: `/collection/${id}`, viewTransition: { types: ['zoom-in'] } });
|
||||||
}}
|
}}
|
||||||
onGameFocus={(id, node) =>
|
onGameFocus={(id, node, details) =>
|
||||||
{
|
{
|
||||||
data.setBackground(
|
data.setBackground(
|
||||||
`https://picsum.photos/id/${10 + (id ?? 0)}/1920/1080.webp`,
|
`https://picsum.photos/id/${10 + (id ?? 0)}/1920/1080.webp`,
|
||||||
);
|
);
|
||||||
data.onFocus?.(node);
|
data.onFocus?.(id, node, details);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/
|
||||||
import { Router } from '..';
|
import { Router } from '..';
|
||||||
import { PopSource } from '../scripts/spatialNavigation';
|
import { PopSource } from '../scripts/spatialNavigation';
|
||||||
import { GameListFilterType } from '@/shared/constants';
|
import { GameListFilterType } from '@/shared/constants';
|
||||||
|
import { GameCardFocusHandler } from './GameCard';
|
||||||
|
|
||||||
export interface CollectionsDetailParams
|
export interface CollectionsDetailParams
|
||||||
{
|
{
|
||||||
|
|
@ -42,6 +43,14 @@ export function CollectionsDetail (data: CollectionsDetailParams)
|
||||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||||
const { shortcuts } = useShortcutContext();
|
const { shortcuts } = useShortcutContext();
|
||||||
|
|
||||||
|
const handleScroll: GameCardFocusHandler = (id, node, details) =>
|
||||||
|
{
|
||||||
|
if (!(details.nativeEvent instanceof MouseEvent))
|
||||||
|
{
|
||||||
|
node.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
<AnimatedBackground animated ref={ref} backgroundKey="home-background" className='flex'>
|
<AnimatedBackground animated ref={ref} backgroundKey="home-background" className='flex'>
|
||||||
|
|
@ -56,7 +65,7 @@ export function CollectionsDetail (data: CollectionsDetailParams)
|
||||||
grid
|
grid
|
||||||
setBackground={data.setBackground}
|
setBackground={data.setBackground}
|
||||||
filters={data.filters}
|
filters={data.filters}
|
||||||
onFocus={(node) => node.scrollIntoView({ block: 'center', behavior: 'smooth' })}
|
onFocus={handleScroll}
|
||||||
id={`${focusKey}-list`}>
|
id={`${focusKey}-list`}>
|
||||||
|
|
||||||
</GameList>
|
</GameList>
|
||||||
|
|
|
||||||
|
|
@ -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 handleAction = data.action ? () => data.action?.({ close: context.close, focus: focusSelf }) : undefined;
|
||||||
const { ref, focused, focusSelf, focusKey, hasFocusedChild } = useFocusable({
|
const { ref, focused, focusSelf, focusKey, hasFocusedChild } = useFocusable({
|
||||||
focusKey: `${context.id}-list-option-${data.id}`,
|
focusKey: `${context.id}-list-option-${data.id}`,
|
||||||
onEnterPress: handleAction,
|
onEnterPress: data.shortcuts ? handleAction : undefined,
|
||||||
onFocus: handleFocus,
|
onFocus: handleFocus,
|
||||||
trackChildren: typeof data.content !== 'string'
|
trackChildren: typeof data.content !== 'string'
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ function List (data: {
|
||||||
const { setCurrentPath, startingPath, allowNewFolderCreation, currentPath, isDirectoryPicker } = useContext(FilePickerContext);
|
const { setCurrentPath, startingPath, allowNewFolderCreation, currentPath, isDirectoryPicker } = useContext(FilePickerContext);
|
||||||
const { ref, focusKey } = useFocusable({ focusKey: data.id, preferredChildFocusKey: `${data.id}...` });
|
const { ref, focusKey } = useFocusable({ focusKey: data.id, preferredChildFocusKey: `${data.id}...` });
|
||||||
const handleReturn = () => setCurrentPath(data.parentPath);
|
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}>
|
return <div ref={ref}>
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
<ContextList showCloseButton={false}
|
<ContextList showCloseButton={false}
|
||||||
|
|
@ -62,21 +62,25 @@ function List (data: {
|
||||||
icon = <></>;
|
icon = <></>;
|
||||||
}
|
}
|
||||||
const shortcuts: Shortcut[] = [];
|
const shortcuts: Shortcut[] = [];
|
||||||
|
let action: () => void;
|
||||||
if (f.isDirectory)
|
if (f.isDirectory)
|
||||||
{
|
{
|
||||||
shortcuts.push({ label: "Enter", button: GamePadButtonCode.A, action: () => setCurrentPath(fullPath) });
|
shortcuts.push({ label: "Enter", button: GamePadButtonCode.A, action: () => setCurrentPath(fullPath) });
|
||||||
|
action = () => setCurrentPath(fullPath);
|
||||||
if (isDirectoryPicker)
|
if (isDirectoryPicker)
|
||||||
shortcuts.push({ label: "Select", button: GamePadButtonCode.X, action: () => data.select(fullPath) });
|
shortcuts.push({ label: "Select", button: GamePadButtonCode.X, action: () => data.select(fullPath) });
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
shortcuts.push({ label: "Select", button: GamePadButtonCode.A, action: () => data.select(fullPath) });
|
shortcuts.push({ label: "Select", button: GamePadButtonCode.A, action: () => data.select(fullPath) });
|
||||||
|
action = () => data.select(fullPath);
|
||||||
}
|
}
|
||||||
const entry: DialogEntry = {
|
const entry: DialogEntry = {
|
||||||
content: f.name,
|
content: f.name,
|
||||||
id: `${data.id}-${f.name}`,
|
id: `${data.id}-${f.name}`,
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
icon,
|
icon,
|
||||||
shortcuts
|
shortcuts,
|
||||||
|
action
|
||||||
};
|
};
|
||||||
return entry;
|
return entry;
|
||||||
}), ...(allowNewFolderCreation && currentPath ? [{
|
}), ...(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 });
|
const { ref, focused } = useFocusable({ focusKey: data.id, onEnterPress: data.onSelect });
|
||||||
return <li ref={ref} onClick={data.onSelect} className={twMerge(
|
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({
|
classNames({
|
||||||
"bg-primary text-primary-content": data.isActive,
|
"bg-primary text-primary-content": data.isActive,
|
||||||
"ring-7 ring-base-content": focused
|
"ring-7 ring-base-content": focused
|
||||||
|
|
@ -238,8 +242,8 @@ export default function FilePicker (data: {
|
||||||
{
|
{
|
||||||
const [currentPath, setCurrentPath] = useState<string | undefined>(data.startingPath);
|
const [currentPath, setCurrentPath] = useState<string | undefined>(data.startingPath);
|
||||||
|
|
||||||
const { data: files, refetch: refetchFiles } = useQuery(filesQuery(currentPath, data.id));
|
const { data: files, refetch: refetchFiles, isLoading: filesLoading } = useQuery(filesQuery(currentPath, data.id));
|
||||||
const { data: drives } = useQuery(drivesQuery);
|
const { data: drives, isLoading: drivesLoading } = useQuery(drivesQuery);
|
||||||
|
|
||||||
const fullPath = files ? path.join(files.parentPath, files.name) : '';
|
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];
|
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>
|
}>{p}</a>
|
||||||
</li>)}
|
</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
|
{(filesLoading || drivesLoading) && <span className="loading loading-spinner loading-lg"></span>}
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
<ListWithDrives
|
<ListWithDrives
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import SvgIcon from "./SvgIcon";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import useActiveControl from "../scripts/gamepads";
|
||||||
|
|
||||||
function FilterCat (
|
function FilterCat (
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -33,16 +34,19 @@ function FilterCat (
|
||||||
}
|
}
|
||||||
}, [filter]);
|
}, [filter]);
|
||||||
|
|
||||||
|
const { isMouse } = useActiveControl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={focusSelf}
|
onClick={focusSelf}
|
||||||
className={classNames(
|
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":
|
"bg-base-content px-3 text-base-300 drop-shadow cursor-default":
|
||||||
focused || data.active,
|
focused || data.active,
|
||||||
"ring-primary ring-7": focused,
|
"ring-primary ring-7": focused && !isMouse,
|
||||||
"hover:bg-base-content/40 cursor-pointer": !focused,
|
"hover:bg-base-content/40 cursor-pointer": !focused,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
|
@ -70,13 +74,13 @@ export function FilterUI (data: {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
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"
|
save-child-focus="session"
|
||||||
>
|
>
|
||||||
<FocusContext.Provider value={focusKey}>
|
<FocusContext.Provider value={focusKey}>
|
||||||
<ul className="flex flex-row bg-base-100 rounded-full p-1 drop-shadow-sm">
|
<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 h-12 items-center justify-center rounded-full">
|
<li className=" flex px-4 items-center justify-center rounded-full">
|
||||||
<SvgIcon className="size-8" icon="steamdeck_button_l1_outline" />
|
<SvgIcon className="sm:size-4 md:size-8" icon="steamdeck_button_l1_outline" />
|
||||||
</li>
|
</li>
|
||||||
{Object.entries(data.options)?.map(([id, option]) => (
|
{Object.entries(data.options)?.map(([id, option]) => (
|
||||||
<FilterCat
|
<FilterCat
|
||||||
|
|
@ -88,8 +92,8 @@ export function FilterUI (data: {
|
||||||
{...option}
|
{...option}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<li className=" flex px-4 h-12 items-center justify-center rounded-full">
|
<li className="flex px-4 items-center justify-center rounded-full">
|
||||||
<SvgIcon className="size-8" icon="steamdeck_button_r1_outline" />
|
<SvgIcon className="sm:size-4 md:size-8" icon="steamdeck_button_r1_outline" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</FocusContext.Provider>
|
</FocusContext.Provider>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { FocusDetails, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { JSX, useEffect } from "react";
|
import { JSX } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import useActiveControl from "../scripts/gamepads";
|
||||||
|
|
||||||
export function GameCardSkeleton ()
|
export function GameCardSkeleton ()
|
||||||
{
|
{
|
||||||
|
|
@ -16,6 +17,8 @@ export function GameCardSkeleton ()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GameCardFocusHandler = (id: string, node: HTMLElement, details: FocusDetails) => void;
|
||||||
|
|
||||||
export interface GameCardParams
|
export interface GameCardParams
|
||||||
{
|
{
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -27,7 +30,7 @@ export interface GameCardParams
|
||||||
id: string;
|
id: string;
|
||||||
badges?: JSX.Element[];
|
badges?: JSX.Element[];
|
||||||
className?: string;
|
className?: string;
|
||||||
onFocus?: (id: string, node: HTMLElement) => void;
|
onFocus?: GameCardFocusHandler;
|
||||||
onBlur?: (id: string) => void;
|
onBlur?: (id: string) => void;
|
||||||
onAction?: () => void;
|
onAction?: () => void;
|
||||||
clickFocuses?: boolean;
|
clickFocuses?: boolean;
|
||||||
|
|
@ -37,10 +40,11 @@ export default function GameCard (data: GameCardParams)
|
||||||
{
|
{
|
||||||
const { ref, focused, focusSelf } = useFocusable({
|
const { ref, focused, focusSelf } = useFocusable({
|
||||||
focusKey: data.focusKey,
|
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?.(),
|
onEnterPress: () => data.onAction?.(),
|
||||||
onBlur: () => data.onBlur?.(data.id)
|
onBlur: () => data.onBlur?.(data.id)
|
||||||
});
|
});
|
||||||
|
const { isPointer } = useActiveControl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
|
@ -60,21 +64,24 @@ export default function GameCard (data: GameCardParams)
|
||||||
data.onAction?.();
|
data.onAction?.();
|
||||||
}}
|
}}
|
||||||
className={twMerge(
|
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)',
|
'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",
|
"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({
|
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"
|
"h-(--game-card-height)": typeof data.preview === "string"
|
||||||
}),
|
}),
|
||||||
data.className
|
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" ? (
|
{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
|
typeof data.preview === 'function' ? data.preview({ focused }) : data.preview
|
||||||
)}</div>
|
)}</div>
|
||||||
|
|
@ -83,18 +90,21 @@ export default function GameCard (data: GameCardParams)
|
||||||
{data.badges?.map((b, i) =>
|
{data.badges?.map((b, i) =>
|
||||||
<div key={i}
|
<div key={i}
|
||||||
className={
|
className={
|
||||||
twMerge("bg-base-100 text-base-content drop-shadow-lg overflow-hidden rounded-full p-1 last:mr-4 transition-colors",
|
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 }))}
|
classNames({
|
||||||
|
"bg-primary text-primary-content": focused && !isPointer,
|
||||||
|
"group-hover:bg-primary group-hover:text-primary-content": isPointer
|
||||||
|
}))}
|
||||||
>
|
>
|
||||||
{b}
|
{b}
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col p-4">
|
<div className="flex flex-col md:p-4 sm:p-2">
|
||||||
<div className="text-xl font-bold text-nowrap text-ellipsis overflow-hidden">
|
<div className="md:text-xl sm:text-sm font-bold text-nowrap text-ellipsis overflow-hidden">
|
||||||
{data.title}
|
{data.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-s">{data.subtitle}</div>
|
<div className="sm:text-xs md:text-sm text-nowrap">{data.subtitle}</div>
|
||||||
</div>
|
</div>
|
||||||
</li >
|
</li >
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { SaveSource } from "../scripts/spatialNavigation";
|
||||||
import { rommApi } from "../scripts/clientApi";
|
import { rommApi } from "../scripts/clientApi";
|
||||||
import { HardDrive } from "lucide-react";
|
import { HardDrive } from "lucide-react";
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
|
import { GameCardFocusHandler } from "./GameCard";
|
||||||
|
|
||||||
export interface GameListParams
|
export interface GameListParams
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +15,7 @@ export interface GameListParams
|
||||||
grid?: boolean,
|
grid?: boolean,
|
||||||
setBackground?: (url: string) => void;
|
setBackground?: (url: string) => void;
|
||||||
onGameSelect?: (id: FrontEndId) => void;
|
onGameSelect?: (id: FrontEndId) => void;
|
||||||
onFocus?: (node: HTMLElement) => void;
|
onFocus?: GameCardFocusHandler;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +53,7 @@ export function GameList (data: GameListParams)
|
||||||
type="game"
|
type="game"
|
||||||
grid={data.grid}
|
grid={data.grid}
|
||||||
className={data.className}
|
className={data.className}
|
||||||
onGameFocus={(id, node) => data.onFocus?.(node)}
|
onGameFocus={data.onFocus}
|
||||||
games={games.data?.games
|
games={games.data?.games
|
||||||
.map(
|
.map(
|
||||||
(g) =>
|
(g) =>
|
||||||
|
|
@ -69,7 +70,7 @@ export function GameList (data: GameListParams)
|
||||||
title: g.name ?? "",
|
title: g.name ?? "",
|
||||||
subtitle: (
|
subtitle: (
|
||||||
<div className="flex gap-1 items-center">
|
<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>
|
<p className="opacity-80">{g.platform_display_name}</p>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,6 @@ import
|
||||||
Bell,
|
Bell,
|
||||||
Bluetooth,
|
Bluetooth,
|
||||||
Clock,
|
Clock,
|
||||||
Lock,
|
|
||||||
Power,
|
|
||||||
ShieldAlert,
|
|
||||||
Sun,
|
|
||||||
User,
|
User,
|
||||||
Wifi,
|
Wifi,
|
||||||
WifiHigh,
|
WifiHigh,
|
||||||
|
|
@ -29,9 +25,10 @@ import { useQuery } from "@tanstack/react-query";
|
||||||
import { getCurrentUserApiUsersMeGetOptions, statsApiStatsGetOptions } from "../../clients/romm/@tanstack/react-query.gen";
|
import { getCurrentUserApiUsersMeGetOptions, statsApiStatsGetOptions } from "../../clients/romm/@tanstack/react-query.gen";
|
||||||
import { RPC_URL } from "../../shared/constants";
|
import { RPC_URL } from "../../shared/constants";
|
||||||
import { JSX, useEffect, useRef } from "react";
|
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 { SaveSource } from "../scripts/spatialNavigation";
|
||||||
import { systemApi } from "../scripts/clientApi";
|
import { systemApi } from "../scripts/clientApi";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
function HeaderAvatar (data: {
|
function HeaderAvatar (data: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -116,7 +113,7 @@ function NotificationStatus ()
|
||||||
{
|
{
|
||||||
const hasUnread = false;
|
const hasUnread = false;
|
||||||
return <div className={classNames("p-2 rounded-full", { "bg-warning text-warning-content": hasUnread })}>
|
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>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,13 +167,13 @@ function BluetoothStatus ()
|
||||||
|
|
||||||
function WiFiStatus ()
|
function WiFiStatus ()
|
||||||
{
|
{
|
||||||
const { data: wifi } = useQuery({
|
const { data: wifi, isLoading } = useQuery({
|
||||||
queryKey: ['wifi'],
|
queryKey: ['wifi'],
|
||||||
queryFn: () => systemApi.api.system.info.wifi.get().then(d => d.data),
|
queryFn: () => systemApi.api.system.info.wifi.get().then(d => d.data),
|
||||||
refetchInterval: 3000
|
refetchInterval: 3000
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div>
|
return (!!wifi && wifi.length > 0) || isLoading ? <div>
|
||||||
{wifi?.map(w =>
|
{wifi?.map(w =>
|
||||||
{
|
{
|
||||||
const className = "w-6 h-6";
|
const className = "w-6 h-6";
|
||||||
|
|
@ -195,7 +192,7 @@ function WiFiStatus ()
|
||||||
</div>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
|
|
||||||
</div>;
|
</div> : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BatteryStatus ()
|
function BatteryStatus ()
|
||||||
|
|
@ -224,7 +221,7 @@ function BatteryStatus ()
|
||||||
batteryIcon = <BatteryMedium className={batteryClassName} />;
|
batteryIcon = <BatteryMedium className={batteryClassName} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <div className="flex gap-2 items-center">
|
return !!battery && battery.hasBattery && <div className="flex gap-2 items-center">
|
||||||
{batteryIcon}
|
{batteryIcon}
|
||||||
<span className="font-semibold">{battery?.percent} %</span>
|
<span className="font-semibold">{battery?.percent} %</span>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
@ -271,7 +268,9 @@ export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAcc
|
||||||
<FocusContext.Provider value={focusKey}>
|
<FocusContext.Provider value={focusKey}>
|
||||||
<header
|
<header
|
||||||
ref={ref}
|
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">
|
<div className="flex items-center gap-2 drop-shadow-sm">
|
||||||
{accounts?.map(a => <HeaderAvatar
|
{accounts?.map(a => <HeaderAvatar
|
||||||
|
|
@ -285,8 +284,8 @@ export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAcc
|
||||||
/>)}
|
/>)}
|
||||||
{data.title}
|
{data.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text drop-shadow-sm">
|
<div className="flex items-center md:gap-2 sm:gap-1 text drop-shadow-sm">
|
||||||
<div className="flex gap-5 items-center">
|
<div className="flex md:gap-5 sm:gap-2 items-center">
|
||||||
<ClockStatus />
|
<ClockStatus />
|
||||||
<WiFiStatus />
|
<WiFiStatus />
|
||||||
<BluetoothStatus />
|
<BluetoothStatus />
|
||||||
|
|
@ -297,7 +296,7 @@ export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAcc
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{data.buttonElements ?? data.buttons?.map(b => <RoundButton
|
{data.buttonElements ?? data.buttons?.map(b => <RoundButton
|
||||||
key={b.id}
|
key={b.id}
|
||||||
className="header-icon size-16"
|
className="header-icon md:size-16 sm:size-10"
|
||||||
id={b.id}
|
id={b.id}
|
||||||
icon={b.icon}
|
icon={b.icon}
|
||||||
external={b.external}
|
external={b.external}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ import { rommApi } from "../scripts/clientApi";
|
||||||
import { SaveSource } from "../scripts/spatialNavigation";
|
import { SaveSource } from "../scripts/spatialNavigation";
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
import { HardDrive } from "lucide-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 navigate = useNavigate();
|
||||||
const { data: platforms } = useSuspenseQuery(
|
const { data: platforms } = useSuspenseQuery(
|
||||||
|
|
@ -29,7 +30,7 @@ export function PlatformsList (data: { id: string, setBackground: (url: string)
|
||||||
type="platform"
|
type="platform"
|
||||||
id={data.id}
|
id={data.id}
|
||||||
className={data.className}
|
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())
|
games={platforms.sort((a, b) => a.updated_at.getTime() - b.updated_at.getTime())
|
||||||
.map((g) =>
|
.map((g) =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { MouseEventHandler } from "react";
|
import { MouseEventHandler } from "react";
|
||||||
import SvgIcon, { IconType } from "./SvgIcon";
|
import SvgIcon, { IconType } from "./SvgIcon";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export default function ShortcutPrompt (data: {
|
export default function ShortcutPrompt (data: {
|
||||||
id: string;
|
id: string;
|
||||||
icon: IconType;
|
icon?: IconType;
|
||||||
label?: string;
|
label?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: MouseEventHandler;
|
onClick?: MouseEventHandler;
|
||||||
|
|
@ -17,14 +17,15 @@ export default function ShortcutPrompt (data: {
|
||||||
style={{ viewTransitionName: data.id }}
|
style={{ viewTransitionName: data.id }}
|
||||||
className={twMerge(
|
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",
|
"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,
|
data.className,
|
||||||
classNames({
|
classNames({
|
||||||
"hover:bg-base-300 cursor-pointer": !!data.onClick,
|
"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}
|
{data.label}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { GamepadButtonEvent } from '../scripts/gamepads';
|
import useActiveControl, { GamepadButtonEvent } from '../scripts/gamepads';
|
||||||
import { GamePadButtonCode, Shortcut } from '../scripts/shortcuts';
|
import { GamePadButtonCode, Shortcut } from '../scripts/shortcuts';
|
||||||
import ShortcutPrompt from './ShortcutPrompt';
|
import ShortcutPrompt from './ShortcutPrompt';
|
||||||
import { IconType } from './SvgIcon';
|
import { IconType } from './SvgIcon';
|
||||||
|
|
@ -23,16 +23,38 @@ const iconMap: Record<GamePadButtonCode, IconType> = {
|
||||||
[GamePadButtonCode.Steam]: 'steamdeck_button_quickaccess'
|
[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[]; })
|
export default function Shortcuts (data: { shortcuts?: Shortcut[]; })
|
||||||
{
|
{
|
||||||
|
const { control } = useActiveControl();
|
||||||
|
const showKeyboard = control === 'keyboard' || control === 'mouse';
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 z-1000">
|
<div className="flex gap-2 z-1000">
|
||||||
{data.shortcuts?.filter(s => !!s.label).map((s, i) => <ShortcutPrompt
|
{data.shortcuts?.filter(s => !!s.label).map((s, i) => <ShortcutPrompt
|
||||||
key={s.button}
|
key={s.button}
|
||||||
id={`shortcut-${s.button}`}
|
id={`shortcut-${s.button}`}
|
||||||
onClick={e => s.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: s.button, isClick: true }))}
|
onClick={e => s.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: s.button, isClick: true }))}
|
||||||
icon={iconMap[s.button]}
|
icon={showKeyboard ? undefined : iconMap[s.button]}
|
||||||
label={s.label} />
|
label={showKeyboard ? `${keyboardMap[s.button]} | ${s.label}` : s.label} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export function Button (data: {
|
||||||
children?: any,
|
children?: any,
|
||||||
className?: string,
|
className?: string,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
type: "reset" | "button" | "submit" | undefined;
|
type?: "reset" | "button" | "submit";
|
||||||
shortcutLabel?: string;
|
shortcutLabel?: string;
|
||||||
focusClassName?: string;
|
focusClassName?: string;
|
||||||
} & InteractParams & FocusParams)
|
} & InteractParams & FocusParams)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export default function DownloadDirectoryOption (data: PathSettingsOptionParams)
|
||||||
type={data.type}
|
type={data.type}
|
||||||
save={setSettingMutation.mutate}
|
save={setSettingMutation.mutate}
|
||||||
allowNewFolderCreation={data.allowNewFolderCreation}
|
allowNewFolderCreation={data.allowNewFolderCreation}
|
||||||
|
requireConfirmation={data.requireConfirmation}
|
||||||
isDirectoryPicker={true}
|
isDirectoryPicker={true}
|
||||||
localValue={localValue}
|
localValue={localValue}
|
||||||
setLocalValue={(v) =>
|
setLocalValue={(v) =>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
@plugin "daisyui";
|
@plugin "daisyui";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--game-card-height: calc(var(--spacing) * 100);
|
--breakpoint-xs: 20rem;
|
||||||
--game-card-width: calc(var(--spacing) * 64);
|
|
||||||
|
|
||||||
--animate-wiggle: wiggle 0.3s ease-in-out 1;
|
--animate-wiggle: wiggle 0.3s ease-in-out 1;
|
||||||
--animate-rotate: rotate 0.3s ease-in-out 1 0.2s;
|
--animate-rotate: rotate 0.3s ease-in-out 1 0.2s;
|
||||||
|
|
@ -107,6 +106,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
@variant sm {
|
||||||
|
:root {
|
||||||
|
--game-card-height: calc(var(--spacing) * 55);
|
||||||
|
--game-card-width: calc(var(--spacing) * 35.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
@variant md {
|
||||||
|
:root {
|
||||||
|
--game-card-height: calc(var(--spacing) * 100);
|
||||||
|
--game-card-width: calc(var(--spacing) * 64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
symbol path {
|
symbol path {
|
||||||
fill: var(--color-base-content) !important;
|
fill: var(--color-base-content) !important;
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +152,7 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon svg {
|
.menu-icon svg {
|
||||||
@apply sm:size-7 md:size-9 transition-all;
|
@apply sm:size-6 md:size-9 transition-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon.focus svg {
|
.menu-icon.focus svg {
|
||||||
|
|
@ -143,7 +160,8 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-icon svg {
|
.header-icon svg {
|
||||||
@apply w-8 h-8 min-w-8 min-h-8;
|
@apply md:w-8 md:h-8 md:min-w-8 md:min-h-8;
|
||||||
|
@apply sm:w-5 sm:h-5 sm:min-w-5 sm:min-h-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-icon-small svg {
|
.header-icon-small svg {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import
|
||||||
Search,
|
Search,
|
||||||
Power,
|
Power,
|
||||||
OctagonAlert,
|
OctagonAlert,
|
||||||
|
Maximize,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
|
|
@ -19,6 +20,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
FocusContext,
|
FocusContext,
|
||||||
|
FocusDetails,
|
||||||
useFocusable,
|
useFocusable,
|
||||||
} from "@noriginmedia/norigin-spatial-navigation";
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
@ -79,9 +81,14 @@ function HomeList (data: {
|
||||||
preferredChildFocusKey: `${data.selectedFilter}-list`
|
preferredChildFocusKey: `${data.selectedFilter}-list`
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleNodeFocus = (node: HTMLElement) =>
|
const handleNodeFocus = (id: string, node: HTMLElement, details: FocusDetails) =>
|
||||||
{
|
{
|
||||||
node.scrollIntoView({ inline: 'center', behavior: initFocus ? 'smooth' : 'instant' });
|
const isMounseEvent = details.nativeEvent instanceof MouseEvent;
|
||||||
|
if (!isMounseEvent)
|
||||||
|
{
|
||||||
|
node?.scrollIntoView({ inline: 'center', behavior: initFocus ? 'smooth' : 'instant' });
|
||||||
|
}
|
||||||
|
|
||||||
setInitFocus(true);
|
setInitFocus(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -101,7 +108,7 @@ function HomeList (data: {
|
||||||
(ref.current as HTMLElement)?.scrollBy({
|
(ref.current as HTMLElement)?.scrollBy({
|
||||||
top: 0,
|
top: 0,
|
||||||
left: deltaY,
|
left: deltaY,
|
||||||
behavior: 'auto'
|
behavior: 'instant'
|
||||||
});
|
});
|
||||||
|
|
||||||
} else
|
} else
|
||||||
|
|
@ -109,7 +116,7 @@ function HomeList (data: {
|
||||||
(ref.current as HTMLElement)?.scrollBy({
|
(ref.current as HTMLElement)?.scrollBy({
|
||||||
top: 0,
|
top: 0,
|
||||||
left: deltaY,
|
left: deltaY,
|
||||||
behavior: 'auto'
|
behavior: 'instant'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -145,7 +152,9 @@ function MainMenu (data: {})
|
||||||
<ul
|
<ul
|
||||||
ref={ref}
|
ref={ref}
|
||||||
save-child-focus="session"
|
save-child-focus="session"
|
||||||
className="flex items-center justify-center gap-3"
|
className={twMerge("md:relative flex items-center justify-center md:gap-3",
|
||||||
|
"sm:gap-1 sm:absolute sm:bottom-2 sm:left-0 sm:right-0"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FocusContext.Provider value={focusKey}>
|
<FocusContext.Provider value={focusKey}>
|
||||||
<CircleIcon
|
<CircleIcon
|
||||||
|
|
@ -199,7 +208,7 @@ function CircleIcon (data: {
|
||||||
onClick={data.action}
|
onClick={data.action}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`menu-icon text-base-300 md:w-20 md:h-20 rounded-full flex items-center justify-center drop-shadow-lg cursor-pointer transition-all`,
|
`menu-icon text-base-300 md:w-20 md:h-20 rounded-full flex items-center justify-center drop-shadow-lg cursor-pointer transition-all`,
|
||||||
'sm:w-14 sm:h-14',
|
'sm:w-14 sm:h-10',
|
||||||
typeClasses[data.type ?? "none"], classNames(
|
typeClasses[data.type ?? "none"], classNames(
|
||||||
{
|
{
|
||||||
"focus ring-7 ring-primary drop-shadow-2xl animate-scale": focused,
|
"focus ring-7 ring-primary drop-shadow-2xl animate-scale": focused,
|
||||||
|
|
@ -263,18 +272,19 @@ export default function ConsoleHomeUI ()
|
||||||
<FocusContext.Provider value={focusKey}>
|
<FocusContext.Provider value={focusKey}>
|
||||||
<div className="px-3 w-full pt-2">
|
<div className="px-3 w-full pt-2">
|
||||||
<HeaderUI buttons={[
|
<HeaderUI buttons={[
|
||||||
|
{ id: "fullscreen", icon: <Maximize />, action: () => document.documentElement.requestFullscreen() },
|
||||||
{ id: "search", icon: <Search /> },
|
{ id: "search", icon: <Search /> },
|
||||||
{ id: "power-button", icon: <Power />, external: true, action: () => closeMutation.mutate() }
|
{ id: "power-button", icon: <Power />, external: true, action: () => closeMutation.mutate() }
|
||||||
]} />
|
]} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col grow justify-evenly">
|
<div className="flex w-full flex-col grow justify-evenly md:pt-0">
|
||||||
<FilterUI
|
<FilterUI
|
||||||
id="home"
|
id="home"
|
||||||
options={filters}
|
options={filters}
|
||||||
selected={filter ? filter : 'games'}
|
selected={filter ? filter : 'games'}
|
||||||
setSelected={setFilter}
|
setSelected={setFilter}
|
||||||
/>
|
/>
|
||||||
<div className="-mb-1">
|
<div className="md:-mb-1">
|
||||||
<HomeList
|
<HomeList
|
||||||
selectedFilter={filter}
|
selectedFilter={filter}
|
||||||
/>
|
/>
|
||||||
|
|
@ -283,7 +293,9 @@ export default function ConsoleHomeUI ()
|
||||||
<MainMenu />
|
<MainMenu />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer className="px-2 pb-2 flex items-center justify-between h-12">
|
<footer className={twMerge("md:relative px-2 md:pb-2 flex items-center justify-between h-12",
|
||||||
|
"sm:absolute bottom-0 left-0 right-0"
|
||||||
|
)}>
|
||||||
<div className="flex gap-2 text-sm">
|
<div className="flex gap-2 text-sm">
|
||||||
</div>
|
</div>
|
||||||
<Shortcuts shortcuts={shortcuts} />
|
<Shortcuts shortcuts={shortcuts} />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { Block, createFileRoute, useBlocker } from '@tanstack/react-router';
|
import { Block, createFileRoute } from '@tanstack/react-router';
|
||||||
import DownloadDirectoryOption from '@/mainview/components/options/DownloadDirectoryOption';
|
import DownloadDirectoryOption from '@/mainview/components/options/DownloadDirectoryOption';
|
||||||
import { useIsMutating, useMutation, useQuery } from '@tanstack/react-query';
|
import { useIsMutating, useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { changeDownloadsMutation, downloadDrivesQuery } from '@/mainview/scripts/queries';
|
import { changeDownloadsMutation, downloadDrivesQuery } from '@/mainview/scripts/queries';
|
||||||
|
|
@ -7,12 +7,12 @@ import { DownloadsDrive } from '@/shared/constants';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts';
|
import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||||
import { Download, FolderOpen, HardDrive, Usb } from 'lucide-react';
|
import { Download, FolderOpen, HardDrive, Save, Usb } from 'lucide-react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { OptionSpace } from '@/mainview/components/options/OptionSpace';
|
import { OptionSpace } from '@/mainview/components/options/OptionSpace';
|
||||||
import data from '@emulators';
|
|
||||||
import { Button } from '@/mainview/components/options/Button';
|
import { Button } from '@/mainview/components/options/Button';
|
||||||
import { systemApi } from '@/mainview/scripts/clientApi';
|
import { systemApi } from '@/mainview/scripts/clientApi';
|
||||||
|
import useActiveControl from '@/mainview/scripts/gamepads';
|
||||||
|
|
||||||
export const Route = createFileRoute('/settings/directories')({
|
export const Route = createFileRoute('/settings/directories')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|
@ -20,27 +20,33 @@ export const Route = createFileRoute('/settings/directories')({
|
||||||
|
|
||||||
function DriveComponent (data: { drive: DownloadsDrive; downloadsSize: number; refetchDrives: () => void; })
|
function DriveComponent (data: { drive: DownloadsDrive; downloadsSize: number; refetchDrives: () => void; })
|
||||||
{
|
{
|
||||||
const { ref, focused, focusKey } = useFocusable({ focusKey: data.drive.device });
|
const { ref, focused, focusKey } = useFocusable({
|
||||||
|
focusKey: data.drive.device,
|
||||||
|
onFocus: () => (ref.current as HTMLElement)?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
||||||
|
});
|
||||||
const isMoving = useIsMutating(changeDownloadsMutation);
|
const isMoving = useIsMutating(changeDownloadsMutation);
|
||||||
const usedWithoutDownlods = data.drive.used - (data.drive.isCurrentlyUsed ? data.downloadsSize : 0);
|
const usedWithoutDownlods = data.drive.used - (data.drive.isCurrentlyUsed ? data.downloadsSize : 0);
|
||||||
const usedPercent = usedWithoutDownlods / data.drive.size;
|
const usedPercent = usedWithoutDownlods / data.drive.size;
|
||||||
const usedPercentRaw = data.drive.used / data.drive.size;
|
const usedPercentRaw = data.drive.used / data.drive.size;
|
||||||
const changeDownloads = useMutation({ ...changeDownloadsMutation, onSuccess: data.refetchDrives }); data.drive.unusableReason;
|
const changeDownloads = useMutation({ ...changeDownloadsMutation, onSuccess: data.refetchDrives }); data.drive.unusableReason;
|
||||||
const shortcuts: Shortcut[] = [];
|
const shortcuts: Shortcut[] = [];
|
||||||
if (!data.drive.unusableReason && isMoving <= 0)
|
const valid = !data.drive.unusableReason && isMoving <= 0;
|
||||||
|
const handleAction = () => changeDownloads.mutate(data.drive.mountPoint);
|
||||||
|
if (valid)
|
||||||
{
|
{
|
||||||
shortcuts.push({ label: "Move Downloads", button: GamePadButtonCode.A, action: () => changeDownloads.mutate(data.drive.mountPoint) });
|
shortcuts.push({ label: "Move Downloads", button: GamePadButtonCode.A, action: handleAction });
|
||||||
}
|
}
|
||||||
useShortcuts(focusKey, () => shortcuts, [shortcuts]);
|
useShortcuts(focusKey, () => shortcuts, [shortcuts]);
|
||||||
|
const { isMouse } = useActiveControl();
|
||||||
|
|
||||||
|
return <li ref={ref} className={twMerge('flex flex-row p-4 bg-base-300 rounded-2xl gap-1 items-end',
|
||||||
return <li ref={ref} className={twMerge('flex flex-col p-4 bg-base-300 rounded-2xl gap-1',
|
|
||||||
classNames({
|
classNames({
|
||||||
"ring-7": focused,
|
"ring-7": focused,
|
||||||
"border-dashed border-primary border-7": data.drive.isCurrentlyUsed,
|
"border-dashed border-primary border-4": data.drive.isCurrentlyUsed,
|
||||||
"border-solid": data.drive.unusableReason === 'already_used',
|
"border-solid": data.drive.unusableReason === 'already_used',
|
||||||
"ring-error": data.drive.unusableReason === 'not_enough_space',
|
"ring-error": data.drive.unusableReason === 'not_enough_space',
|
||||||
}))}>
|
}))}>
|
||||||
|
<div className='flex flex-col grow gap-1'>
|
||||||
<div className='flex gap-2 font-semibold'>{data.drive.isRemovable ? <Usb /> : <HardDrive />}{data.drive.label}</div>
|
<div className='flex gap-2 font-semibold'>{data.drive.isRemovable ? <Usb /> : <HardDrive />}{data.drive.label}</div>
|
||||||
<small className='opacity-60'>{data.drive.mountPoint}</small>
|
<small className='opacity-60'>{data.drive.mountPoint}</small>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
|
|
@ -60,6 +66,8 @@ function DriveComponent (data: { drive: DownloadsDrive; downloadsSize: number; r
|
||||||
}))} style={{ width: usedPercent.toLocaleString('en-US', { style: 'percent' }) }}></div>
|
}))} style={{ width: usedPercent.toLocaleString('en-US', { style: 'percent' }) }}></div>
|
||||||
{!!data.drive.isCurrentlyUsed && <div className="h-full bg-base-content" style={{ width: usedPercentRaw.toLocaleString('en-US', { style: 'percent' }) }}></div>}
|
{!!data.drive.isCurrentlyUsed && <div className="h-full bg-base-content" style={{ width: usedPercentRaw.toLocaleString('en-US', { style: 'percent' }) }}></div>}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{valid && isMouse && <Button type="button" className='btn-circle' onAction={handleAction} id={`${data.drive.mountPoint}-select`}><Save /></Button>}
|
||||||
</li>;
|
</li>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +85,7 @@ function RouteComponent ()
|
||||||
<Block shouldBlockFn={() => isMoving} withResolver={false} />
|
<Block shouldBlockFn={() => isMoving} withResolver={false} />
|
||||||
<ul ref={ref} className="list rounded-box gap-2">
|
<ul ref={ref} className="list rounded-box gap-2">
|
||||||
<div className="divider text-2xl mt-0 md:mt-4">
|
<div className="divider text-2xl mt-0 md:mt-4">
|
||||||
<Download className='size-16' /> Downloads ({drives?.downloadsSize ? prettyBytes(drives?.downloadsSize) : '?'})
|
<Download className='size-16' /> Downloads ({drives?.downloadsSize ? prettyBytes(drives?.downloadsSize) : <span className="loading loading-spinner loading-lg size-6"></span>})
|
||||||
</div>
|
</div>
|
||||||
<ul className='p-2 grid grid-cols-2 gap-3'>
|
<ul className='p-2 grid grid-cols-2 gap-3'>
|
||||||
{drives?.drives.filter(d => d.mountPoint).map(d => <DriveComponent refetchDrives={refetch} downloadsSize={drives.downloadsSize} drive={d} />)}
|
{drives?.drives.filter(d => d.mountPoint).map(d => <DriveComponent refetchDrives={refetch} downloadsSize={drives.downloadsSize} drive={d} />)}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import { PopSource } from "../../scripts/spatialNavigation";
|
||||||
import { Router } from "../..";
|
import { Router } from "../..";
|
||||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||||
import Shortcuts from "@/mainview/components/Shortcuts";
|
import Shortcuts from "@/mainview/components/Shortcuts";
|
||||||
|
import useActiveControl from "@/mainview/scripts/gamepads";
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings")({
|
export const Route = createFileRoute("/settings")({
|
||||||
component: SettingsUI,
|
component: SettingsUI,
|
||||||
|
|
@ -68,6 +69,7 @@ function MenuItem (data: {
|
||||||
? handleNonFocusSelect
|
? handleNonFocusSelect
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
const { isMouse } = useActiveControl();
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
@ -81,13 +83,13 @@ function MenuItem (data: {
|
||||||
"group rounded-full p-3 pl-5 text-base-content/80",
|
"group rounded-full p-3 pl-5 text-base-content/80",
|
||||||
classNames({
|
classNames({
|
||||||
"bg-primary text-primary-content": acitve,
|
"bg-primary text-primary-content": acitve,
|
||||||
"font-semibold ring-7 ring-primary-content": focused,
|
"font-semibold ring-7 ring-primary-content": focused && !isMouse,
|
||||||
"bg-secondary text-secondary-content ring-primary": data.return && focused,
|
"bg-secondary text-secondary-content ring-primary": data.return && focused,
|
||||||
}),
|
}),
|
||||||
data.linkClassName,
|
data.linkClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={twMerge("flex gap-2 items-center transition-all group-hover:scale-110", classNames({
|
<div className={twMerge("flex gap-2 items-center transition-all", classNames({
|
||||||
"scale-110": focused || acitve
|
"scale-110": focused || acitve
|
||||||
}))}>
|
}))}>
|
||||||
{data.icon}
|
{data.icon}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import { getCurrentFocusKey, navigateByDirection, SpatialNavigation } from "@noriginmedia/norigin-spatial-navigation";
|
import { getCurrentFocusKey, navigateByDirection } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { dispatchFocusedEvent, GetFocusedElement } from "./spatialNavigation";
|
import { GetFocusedElement } from "./spatialNavigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
let loopStarted = false;
|
let loopStarted = false;
|
||||||
|
let isTouching = false;
|
||||||
|
type ActiveControlType = 'keyboard' | 'gamepad' | 'mouse' | 'touch' | undefined;
|
||||||
|
let activeControls: ActiveControlType = undefined;
|
||||||
|
let mouseUpdateTimeout: any | undefined = undefined;
|
||||||
|
let touchStopTimeout: any | undefined = undefined;
|
||||||
|
|
||||||
const handleLoop = () =>
|
const handleLoop = () =>
|
||||||
{
|
{
|
||||||
|
|
@ -11,8 +17,70 @@ const handleLoop = () =>
|
||||||
loopStarted = true;
|
loopStarted = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mouse needs to be delayed so that touch events can cancel it.
|
||||||
|
// This is to prevent both touch and mouse events triggering as they do on the steam deck.
|
||||||
|
const handleMouseMove = (e: MouseEvent) =>
|
||||||
|
{
|
||||||
|
if (!mouseUpdateTimeout && !isTouching)
|
||||||
|
{
|
||||||
|
mouseUpdateTimeout = setTimeout(() =>
|
||||||
|
{
|
||||||
|
focusControl('mouse');
|
||||||
|
mouseUpdateTimeout = undefined;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function clearMouseUpdate ()
|
||||||
|
{
|
||||||
|
if (mouseUpdateTimeout)
|
||||||
|
clearTimeout(mouseUpdateTimeout);
|
||||||
|
mouseUpdateTimeout = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = () =>
|
||||||
|
{
|
||||||
|
focusControl('keyboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchStart = (e: TouchEvent) =>
|
||||||
|
{
|
||||||
|
isTouching = true;
|
||||||
|
focusControl('touch');
|
||||||
|
clearMouseUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (e: TouchEvent) =>
|
||||||
|
{
|
||||||
|
setTimeout(() => isTouching = false, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('touchstart', handleTouchStart);
|
||||||
|
window.addEventListener('touchend', handleTouchEnd);
|
||||||
|
window.addEventListener('touchcancel', handleTouchEnd);
|
||||||
window.addEventListener("gamepadconnected", handleLoop);
|
window.addEventListener("gamepadconnected", handleLoop);
|
||||||
import.meta.hot.dispose(() => window.addEventListener('gamepaddisconnected', handleLoop));
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
import.meta.hot.dispose(() => window.removeEventListener('gamepaddisconnected', handleLoop));
|
||||||
|
import.meta.hot.dispose(() => window.removeEventListener('mousemove', handleMouseMove));
|
||||||
|
import.meta.hot.dispose(() => window.removeEventListener('keydown', handleKeyDown));
|
||||||
|
import.meta.hot.dispose(() => window.removeEventListener('touchstart', handleTouchStart));
|
||||||
|
import.meta.hot.dispose(() => window.removeEventListener('touchend', handleTouchEnd));
|
||||||
|
import.meta.hot.dispose(() => window.removeEventListener('touchcancel', handleTouchEnd));
|
||||||
|
|
||||||
|
export default function useActiveControl ()
|
||||||
|
{
|
||||||
|
const [c, setC] = useState<typeof activeControls>(activeControls);
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
const handler = (e: Event) => setC((e as CustomEvent).detail);
|
||||||
|
window.addEventListener('activecontrolschange', handler);
|
||||||
|
return () => window.removeEventListener('activecontrolschange', handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isMouse: c === 'mouse', isPointer: c === 'mouse' || c === 'touch', control: c };
|
||||||
|
}
|
||||||
|
|
||||||
const throttleMap = new Map<string, number>();
|
const throttleMap = new Map<string, number>();
|
||||||
const throttleAcceleration = new Map<string, number>();
|
const throttleAcceleration = new Map<string, number>();
|
||||||
|
|
@ -32,6 +100,19 @@ function throttleNav (key: string, dir: string, event: Event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focusControl (control: typeof activeControls)
|
||||||
|
{
|
||||||
|
if (activeControls != control)
|
||||||
|
{
|
||||||
|
activeControls = control;
|
||||||
|
window.dispatchEvent(new CustomEvent('activecontrolschange', { detail: control }));
|
||||||
|
if (control !== 'mouse')
|
||||||
|
{
|
||||||
|
clearMouseUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*window.addEventListener('keydown', e =>
|
/*window.addEventListener('keydown', e =>
|
||||||
{
|
{
|
||||||
if (e.key === 'Escape')
|
if (e.key === 'Escape')
|
||||||
|
|
@ -74,6 +155,7 @@ function updateStatus ()
|
||||||
if (!throttleMap.has(key))
|
if (!throttleMap.has(key))
|
||||||
{
|
{
|
||||||
window.dispatchEvent(new GamepadButtonEvent('gamepadbuttondown', { button: i, gamepad: gamepad }));
|
window.dispatchEvent(new GamepadButtonEvent('gamepadbuttondown', { button: i, gamepad: gamepad }));
|
||||||
|
focusControl('gamepad');
|
||||||
throttleMap.set(key, 0);
|
throttleMap.set(key, 0);
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
|
|
@ -81,6 +163,7 @@ function updateStatus ()
|
||||||
if (throttleMap.delete(key))
|
if (throttleMap.delete(key))
|
||||||
{
|
{
|
||||||
window.dispatchEvent(new GamepadButtonEvent('gamepadbuttonup', { button: i, gamepad: gamepad }));
|
window.dispatchEvent(new GamepadButtonEvent('gamepadbuttonup', { button: i, gamepad: gamepad }));
|
||||||
|
focusControl('gamepad');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
|
import { FocusDetails } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { JSX } from 'react';
|
import { JSX } from 'react';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
|
@ -14,7 +15,7 @@ export interface GameMeta
|
||||||
{
|
{
|
||||||
id: string,
|
id: string,
|
||||||
onSelect?: () => void,
|
onSelect?: () => void,
|
||||||
onFocus?: () => void,
|
onFocus?: (details: FocusDetails) => void,
|
||||||
title: string,
|
title: string,
|
||||||
subtitle: string | JSX.Element,
|
subtitle: string | JSX.Element,
|
||||||
previewUrl?: string;
|
previewUrl?: string;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import path from "node:path";
|
||||||
import staticAssetsPlugin from 'vite-static-assets-plugin';
|
import staticAssetsPlugin from 'vite-static-assets-plugin';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
import { host } from "@/bun/utils/host";
|
import { host } from "./src/bun/utils/host";
|
||||||
|
|
||||||
export default defineConfig(() =>
|
export default defineConfig(() =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue