diff --git a/src/bun/api/games/games.ts b/src/bun/api/games/games.ts index 2e8f957..6196cc2 100644 --- a/src/bun/api/games/games.ts +++ b/src/bun/api/games/games.ts @@ -72,6 +72,10 @@ export default new Elysia() } return status('Not Found'); }, { query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() }) }) + .get('/image', async ({ query }) => + { + return processImage(query.url, query); + }, { query: z.object({ url: z.url(), blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() }) }) .get('/screenshot/:id', async ({ params: { id }, query, set }) => { const screenshot = await db.query.screenshots.findFirst({ where: eq(schema.screenshots.id, id), columns: { content: true, type: true } }); diff --git a/src/bun/api/games/platforms.ts b/src/bun/api/games/platforms.ts index 31a73a2..731b026 100644 --- a/src/bun/api/games/platforms.ts +++ b/src/bun/api/games/platforms.ts @@ -1,5 +1,5 @@ import Elysia, { status } from "elysia"; -import { getPlatformApiPlatformsIdGet, getPlatformsApiPlatformsGet } from "@clients/romm"; +import { getPlatformApiPlatformsIdGet, getPlatformsApiPlatformsGet, getRomsApiRomsGet } from "@clients/romm"; import z from "zod"; import { count, eq, getTableColumns, notInArray } from "drizzle-orm"; import { db } from "../app"; @@ -22,8 +22,9 @@ export default new Elysia() if (rommPlatforms) { - const frontEndPlatforms = rommPlatforms.map(p => + const frontEndPlatforms = await Promise.all(rommPlatforms.map(async p => { + const game = await getRomsApiRomsGet({ query: { platform_ids: [p.id] } }); const platform: FrontEndPlatformType = { slug: p.slug, name: p.display_name, @@ -32,18 +33,20 @@ export default new Elysia() game_count: p.rom_count, updated_at: new Date(p.updated_at), id: { source: 'romm', id: p.id }, - hasLocal: localPlatformSet.has(p.slug) + hasLocal: localPlatformSet.has(p.slug), + paths_screenshots: game.data?.items[0]?.merged_screenshots.map(s => `/api/romm/image/romm/${s}`) ?? [] }; return platform; - }); + })); rommPlatformsSet = new Set(rommPlatforms.map(p => p.slug)); platforms.push(...frontEndPlatforms); } - platforms.push(...localPlatforms.filter(p => !rommPlatformsSet?.has(p.slug)).map(p => + platforms.push(...await Promise.all(localPlatforms.filter(p => !rommPlatformsSet?.has(p.slug)).map(async p => { + const game = await db.query.games.findFirst({ where: eq(schema.games.platform_id, p.id), with: { screenshots: true }, columns: {} }); const platform: FrontEndPlatformType = { slug: p.slug, name: p.name, @@ -52,11 +55,13 @@ export default new Elysia() game_count: p.game_count, updated_at: p.created_at, id: { source: 'local', id: p.id }, - hasLocal: true + hasLocal: true, + paths_screenshots: game?.screenshots?.map(s => `/api/romm/screenshot/${s.id}`) ?? [] + }; return platform; - })); + }))); return { platforms }; }).get('/platforms/:source/:id', async ({ params: { source, id } }) => diff --git a/src/bun/browser.ts b/src/bun/browser.ts index 4bc6833..01744f7 100644 --- a/src/bun/browser.ts +++ b/src/bun/browser.ts @@ -24,7 +24,7 @@ export default async function init (events: EventEmitter, forceBrowser: boolean) async function runWebview (events: EventEmitter) { - const webviewWorker = new Worker(Bun.env.IS_BINARY ? new URL(`./webview/${os.platform()}`, import.meta.url).href : `./webview/${os.platform()}.ts`, { + const webviewWorker = new Worker(new URL(`./webview/${os.platform()}`, import.meta.url).href, { smol: true, ref: false }); diff --git a/src/mainview/components/AnimatedBackground.tsx b/src/mainview/components/AnimatedBackground.tsx index 97227a4..482a674 100644 --- a/src/mainview/components/AnimatedBackground.tsx +++ b/src/mainview/components/AnimatedBackground.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import { createContext, JSX, Ref, useContext, useEffect, useState } from 'react'; import { twMerge } from 'tailwind-merge'; import { useSessionStorage } from 'usehooks-ts'; +import { useLocalSetting } from '../scripts/utils'; export const AnimatedBackgroundContext = createContext({} as { setBackground: (url: string) => void; }); @@ -28,8 +29,13 @@ export function AnimatedBackground (data: { setBackgroundUrl(data.backgroundUrl ? (data.backgroundUrl instanceof URL ? data.backgroundUrl.href : data.backgroundUrl) : undefined); }, [data.backgroundUrl]); - const finalBackgroundUrl = backgroundUrl ? new URL(backgroundUrl) : undefined; - const blur = localStorage.getItem('background-blur') !== "false"; + let finalBackgroundUrl; + try + { + finalBackgroundUrl = backgroundUrl ? new URL(backgroundUrl) : undefined; + } catch { } + + const blur = useLocalSetting('backgroundBlur'); if (blur) { if (!finalBackgroundUrl?.searchParams.has('blur')) diff --git a/src/mainview/components/ContextDialog.tsx b/src/mainview/components/ContextDialog.tsx index 1690899..12e9f21 100644 --- a/src/mainview/components/ContextDialog.tsx +++ b/src/mainview/components/ContextDialog.tsx @@ -15,7 +15,7 @@ export function ContextList (data: { options?: DialogEntry[]; className?: string const context = useContext(ContextDialogContext); return ; } @@ -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: data.shortcuts ? handleAction : undefined, + onEnterPress: data.shortcuts ? undefined : handleAction, onFocus: handleFocus, trackChildren: typeof data.content !== 'string' }); @@ -52,9 +52,9 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class twMerge("flex cursor-pointer sm:text-sm md:text-base")}>
+ data.className, + colors[data.type])}> {data.icon} {data.content}
@@ -75,7 +75,8 @@ export interface DialogEntry export function ContextDialog (data: { id: string, children: any | any[], - open: boolean, close: () => void; + open: boolean, + close: () => void; className?: string; preferredChildFocusKey?: string; }) diff --git a/src/mainview/components/GameList.tsx b/src/mainview/components/GameList.tsx index c79c1e9..12c0b3e 100644 --- a/src/mainview/components/GameList.tsx +++ b/src/mainview/components/GameList.tsx @@ -8,6 +8,7 @@ import { HardDrive } from "lucide-react"; import { JSX } from "react"; import { GameCardFocusHandler } from "./GameCard"; import { gameQuery } from "../scripts/queries"; +import { useLocalSetting } from "../scripts/utils"; export interface GameListParams { @@ -30,6 +31,7 @@ export function GameList (data: GameListParams) }); const navigator = useNavigate(); const queryClient = useQueryClient(); + const blur = useLocalSetting('backgroundBlur'); const handleFocus = (id: FrontEndId, source: string | null, sourceId: number | null) => { @@ -40,10 +42,9 @@ export function GameList (data: GameListParams) { const screenshotUrl = new URL(`${RPC_URL(__HOST__)}${game.paths_screenshots[new Date().getMinutes() % game.paths_screenshots.length]}`); const coverUrl = new URL(`${RPC_URL(__HOST__)}${game.path_cover}`); - const previewUrl = localStorage.getItem('background-blur') !== "false" ? coverUrl : screenshotUrl; + const previewUrl = blur ? coverUrl : screenshotUrl; previewUrl.searchParams.delete('ts'); data.setBackground?.(previewUrl.href); - //queryClient.prefetchQuery(gameQuery(source ?? id.source, sourceId ?? id.id)); } catch { diff --git a/src/mainview/components/PlatformsList.tsx b/src/mainview/components/PlatformsList.tsx index 8b9c15a..600efc1 100644 --- a/src/mainview/components/PlatformsList.tsx +++ b/src/mainview/components/PlatformsList.tsx @@ -42,7 +42,7 @@ export function PlatformsList (data: { id: string, setBackground: (url: string) previewUrl: "", badges, onFocus: () => data.setBackground( - `https://picsum.photos/id/${10 + i}/100/100.webp?blur=10`, + g.paths_screenshots.length > 0 ? `${RPC_URL(__HOST__)}${g.paths_screenshots[new Date().getMinutes() % g.paths_screenshots.length]}` : `${RPC_URL(__HOST__)}/api/romm/image?url=https://picsum.photos/id/${10 + i}/1280/720.webp`, ), onSelect: () => { diff --git a/src/mainview/components/options/LocalOption.tsx b/src/mainview/components/options/LocalOption.tsx new file mode 100644 index 0000000..115eea7 --- /dev/null +++ b/src/mainview/components/options/LocalOption.tsx @@ -0,0 +1,59 @@ +import { HTMLInputTypeAttribute, JSX } from "react"; +import { LocalSettingsSchema, LocalSettingsType } from "../../../shared/constants"; +import { OptionSpace } from "./OptionSpace"; +import { OptionInput } from "./OptionInput"; +import { useLocalStorage } from "usehooks-ts"; +import { OptionDropdown } from "./OptionDropdown"; + +export function LocalOption (data: { + label: string; + id: keyof LocalSettingsType; + type: HTMLInputTypeAttribute | 'dropdown'; + placeholder?: string; + values?: string[]; + icon?: JSX.Element; + children?: any; +}) +{ + const [localValue, setLocalValue] = useLocalStorage(data.id, LocalSettingsSchema.shape[data.id].parse(undefined), { deserializer: (v) => LocalSettingsSchema.shape[data.id].parse(JSON.parse(v)) }); + + return ( + + {data.type === 'dropdown' && data.values && + { + if (data.type === 'checkbox') + { + setLocalValue(v); + } else + { + setLocalValue(v); + } + }} + value={localValue} />} + {data.type !== 'dropdown' && + { + if (data.type === 'checkbox') + { + setLocalValue(v); + } else + { + setLocalValue(v); + } + }} + value={localValue} + />} + {data.children} + + ); +} \ No newline at end of file diff --git a/src/mainview/components/options/OptionDropdown.tsx b/src/mainview/components/options/OptionDropdown.tsx new file mode 100644 index 0000000..6b62841 --- /dev/null +++ b/src/mainview/components/options/OptionDropdown.tsx @@ -0,0 +1,67 @@ +import classNames from "classnames"; +import { ChangeEventHandler, FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef, useState } from "react"; +import { twMerge } from "tailwind-merge"; +import { useOptionContext } from "./OptionSpace"; +import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; +import { systemApi } from "../../scripts/clientApi"; +import { ContextDialog, ContextList, DialogEntry } from "../ContextDialog"; +import { ChevronDown } from "lucide-react"; + +export function OptionDropdown (data: { + name: string; + type: HTMLInputTypeAttribute; + className?: string; + placeholder?: string; + icon?: JSX.Element; + value?: string; + values: string[]; + defaultValue?: string | boolean; + autocomplete?: HTMLInputAutoCompleteAttribute; + onBlur?: FocusEventHandler; + onChange?: (value: any) => void; +}) +{ + const [open, setOpen] = useState(false); + const handlePress = () => + { + setOpen(true); + }; + const handleClose = () => setOpen(false); + const { ref, focused, focusKey } = useFocusable({ + focusKey: data.name, onEnterPress: handlePress + }); + const inputRef = useRef(null); + const option = useOptionContext({ + onOptionEnterPress: handlePress, + }); + + const valueIndex = data.value ? data.values?.indexOf(data.value) : -1; + + return ( + <> + + {open && + ({ + content: v, + id: String(i), + type: 'primary', + action: () => + { + data.onChange?.(v); + setOpen(false); + } + } satisfies DialogEntry))} /> + } + + ); +} \ No newline at end of file diff --git a/src/mainview/components/options/OptionInput.tsx b/src/mainview/components/options/OptionInput.tsx index 2f708d8..f279466 100644 --- a/src/mainview/components/options/OptionInput.tsx +++ b/src/mainview/components/options/OptionInput.tsx @@ -4,6 +4,7 @@ import { twMerge } from "tailwind-merge"; import { useOptionContext } from "./OptionSpace"; import { useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { systemApi } from "../../scripts/clientApi"; +import { Check, CheckIcon, X } from "lucide-react"; export function OptionInput (data: { name: string; @@ -12,24 +13,28 @@ export function OptionInput (data: { placeholder?: string; icon?: JSX.Element; value?: string; - defaultValue?: string; + defaultValue?: string | boolean; autocomplete?: HTMLInputAutoCompleteAttribute; onBlur?: FocusEventHandler; - onChange?: ChangeEventHandler; + onChange?: (value: any) => void; }) { - const { ref, focused } = useFocusable({ - focusKey: data.name, onEnterPress: () => + const handlePress = () => + { + if (data.type === 'checkbox') + { + inputRef.current?.click(); + } else { inputRef.current?.focus(); } + }; + const { ref, focused } = useFocusable({ + focusKey: data.name, onEnterPress: handlePress }); const inputRef = useRef(null); const option = useOptionContext({ - onOptionEnterPress () - { - inputRef.current?.focus(); - }, + onOptionEnterPress: handlePress, }); const handleFocus = () => { @@ -44,32 +49,60 @@ export function OptionInput (data: { Height: rect.height }); } - }; return ( ); } \ No newline at end of file diff --git a/src/mainview/components/options/OptionSpace.tsx b/src/mainview/components/options/OptionSpace.tsx index 6148e3b..40764f0 100644 --- a/src/mainview/components/options/OptionSpace.tsx +++ b/src/mainview/components/options/OptionSpace.tsx @@ -43,7 +43,7 @@ export function OptionSpace (data: { className?: string; focusable?: boolean; children?: any | any[]; - label?: string | JSX.Element; + label?: string | JSX.Element | ((focused: boolean) => JSX.Element); saveLastFocusedChild?: boolean; }) { @@ -62,32 +62,37 @@ export function OptionSpace (data: { eventTarget.dispatchEvent(new CustomEvent("onEnterPress")); }, }); + let labelElement: any = data.label; + if (data.label instanceof Function) + { + labelElement = data.label(focused); + } else if (typeof data.label === 'string') + { + labelElement = ; + } return (
  • -
    - {typeof data.label === "string" ? ( - - ) : ( - data.label - )} -
    -
    + {!!labelElement &&
    + {labelElement} +
    } +
    {data.children}
  • diff --git a/src/mainview/components/options/PathSettingsOption.tsx b/src/mainview/components/options/PathSettingsOption.tsx index 4ce4a78..f441010 100644 --- a/src/mainview/components/options/PathSettingsOption.tsx +++ b/src/mainview/components/options/PathSettingsOption.tsx @@ -119,7 +119,7 @@ export function PathSettingsOptionBase (data: PathSettingsOptionParams & { onBlur={handleInputBlur} onChange={(e) => { - data.setLocalValue(e.currentTarget.value); + data.setLocalValue(e); }} value={data.localValue} /> diff --git a/src/mainview/components/options/SettingsAppForm.tsx b/src/mainview/components/options/SettingsAppForm.tsx index bb36a3e..e957103 100644 --- a/src/mainview/components/options/SettingsAppForm.tsx +++ b/src/mainview/components/options/SettingsAppForm.tsx @@ -3,7 +3,6 @@ import { HTMLInputTypeAttribute, JSX } from "react"; import { OptionInput } from "./OptionInput"; import { OptionSpace } from "./OptionSpace"; import classNames from "classnames"; -import { TriangleAlert } from "lucide-react"; // export useFieldContext for use in your custom components export const { fieldContext, formContext, useFieldContext } = @@ -30,7 +29,7 @@ function FormOption (data: { type: HTMLInputTypeAttribute, icon?: JSX.Element; l name={field.name} value={field.state.value} type={data.type} - onChange={e => field.handleChange(e.target.value)} + onChange={v => field.handleChange(v)} placeholder={data.placeholder} className={classNames({ " flex-3 ring-4 ring-accent": field.getMeta().isDirty })} /> diff --git a/src/mainview/components/options/SettingsOption.tsx b/src/mainview/components/options/SettingsOption.tsx index fab772c..de5fcb7 100644 --- a/src/mainview/components/options/SettingsOption.tsx +++ b/src/mainview/components/options/SettingsOption.tsx @@ -61,9 +61,9 @@ export function SettingsOption (data: { type={data.type} placeholder={data.placeholder} onBlur={handleSave} - onChange={(e) => + onChange={(v) => { - setLocalValue(e.currentTarget.value); + setLocalValue(v); setDirty(true); }} value={localValue} diff --git a/src/mainview/gen/routeTree.gen.ts b/src/mainview/gen/routeTree.gen.ts index 5ce734a..e78cc2d 100644 --- a/src/mainview/gen/routeTree.gen.ts +++ b/src/mainview/gen/routeTree.gen.ts @@ -11,6 +11,7 @@ import { Route as rootRouteImport } from './../routes/__root' import { Route as SettingsRouteRouteImport } from './../routes/settings/route' import { Route as IndexRouteImport } from './../routes/index' +import { Route as SettingsInterfaceRouteImport } from './../routes/settings/interface' import { Route as SettingsEmulatorsRouteImport } from './../routes/settings/emulators' import { Route as SettingsDirectoriesRouteImport } from './../routes/settings/directories' import { Route as SettingsAccountsRouteImport } from './../routes/settings/accounts' @@ -30,6 +31,11 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const SettingsInterfaceRoute = SettingsInterfaceRouteImport.update({ + id: '/interface', + path: '/interface', + getParentRoute: () => SettingsRouteRoute, +} as any) const SettingsEmulatorsRoute = SettingsEmulatorsRouteImport.update({ id: '/emulators', path: '/emulators', @@ -79,6 +85,7 @@ export interface FileRoutesByFullPath { '/settings/accounts': typeof SettingsAccountsRoute '/settings/directories': typeof SettingsDirectoriesRoute '/settings/emulators': typeof SettingsEmulatorsRoute + '/settings/interface': typeof SettingsInterfaceRoute '/game/$source/$id': typeof GameSourceIdRoute '/launcher/$source/$id': typeof LauncherSourceIdRoute '/platform/$source/$id': typeof PlatformSourceIdRoute @@ -91,6 +98,7 @@ export interface FileRoutesByTo { '/settings/accounts': typeof SettingsAccountsRoute '/settings/directories': typeof SettingsDirectoriesRoute '/settings/emulators': typeof SettingsEmulatorsRoute + '/settings/interface': typeof SettingsInterfaceRoute '/game/$source/$id': typeof GameSourceIdRoute '/launcher/$source/$id': typeof LauncherSourceIdRoute '/platform/$source/$id': typeof PlatformSourceIdRoute @@ -104,6 +112,7 @@ export interface FileRoutesById { '/settings/accounts': typeof SettingsAccountsRoute '/settings/directories': typeof SettingsDirectoriesRoute '/settings/emulators': typeof SettingsEmulatorsRoute + '/settings/interface': typeof SettingsInterfaceRoute '/game/$source/$id': typeof GameSourceIdRoute '/launcher/$source/$id': typeof LauncherSourceIdRoute '/platform/$source/$id': typeof PlatformSourceIdRoute @@ -118,6 +127,7 @@ export interface FileRouteTypes { | '/settings/accounts' | '/settings/directories' | '/settings/emulators' + | '/settings/interface' | '/game/$source/$id' | '/launcher/$source/$id' | '/platform/$source/$id' @@ -130,6 +140,7 @@ export interface FileRouteTypes { | '/settings/accounts' | '/settings/directories' | '/settings/emulators' + | '/settings/interface' | '/game/$source/$id' | '/launcher/$source/$id' | '/platform/$source/$id' @@ -142,6 +153,7 @@ export interface FileRouteTypes { | '/settings/accounts' | '/settings/directories' | '/settings/emulators' + | '/settings/interface' | '/game/$source/$id' | '/launcher/$source/$id' | '/platform/$source/$id' @@ -172,6 +184,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/settings/interface': { + id: '/settings/interface' + path: '/interface' + fullPath: '/settings/interface' + preLoaderRoute: typeof SettingsInterfaceRouteImport + parentRoute: typeof SettingsRouteRoute + } '/settings/emulators': { id: '/settings/emulators' path: '/emulators' @@ -236,6 +255,7 @@ interface SettingsRouteRouteChildren { SettingsAccountsRoute: typeof SettingsAccountsRoute SettingsDirectoriesRoute: typeof SettingsDirectoriesRoute SettingsEmulatorsRoute: typeof SettingsEmulatorsRoute + SettingsInterfaceRoute: typeof SettingsInterfaceRoute } const SettingsRouteRouteChildren: SettingsRouteRouteChildren = { @@ -243,6 +263,7 @@ const SettingsRouteRouteChildren: SettingsRouteRouteChildren = { SettingsAccountsRoute: SettingsAccountsRoute, SettingsDirectoriesRoute: SettingsDirectoriesRoute, SettingsEmulatorsRoute: SettingsEmulatorsRoute, + SettingsInterfaceRoute: SettingsInterfaceRoute, } const SettingsRouteRouteWithChildren = SettingsRouteRoute._addFileChildren( diff --git a/src/mainview/index.css b/src/mainview/index.css index a039004..644e044 100644 --- a/src/mainview/index.css +++ b/src/mainview/index.css @@ -2,9 +2,12 @@ @import 'animate.css'; @plugin "daisyui"; +@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); + @theme { --breakpoint-sm: 0px; --breakpoint-md: 1280px; + --page-scroll-bg: transparent; --animate-wiggle: wiggle 0.3s ease-in-out 1; --animate-rotate: rotate 0.3s ease-in-out 1 0.2s; diff --git a/src/mainview/index.tsx b/src/mainview/index.tsx index 4d09964..4087b63 100644 --- a/src/mainview/index.tsx +++ b/src/mainview/index.tsx @@ -49,8 +49,6 @@ export const Router = createRouter({ }, }); - - // Register things for typesafety declare module "@tanstack/react-router" { interface Register diff --git a/src/mainview/routes/__root.tsx b/src/mainview/routes/__root.tsx index 943c280..0b6cade 100644 --- a/src/mainview/routes/__root.tsx +++ b/src/mainview/routes/__root.tsx @@ -4,7 +4,7 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { RouterContext } from ".."; import Notifications from "../components/Notifications"; import { Toaster } from "react-hot-toast"; -import { mobileCheck } from "../scripts/utils"; +import { mobileCheck, useLocalSetting } from "../scripts/utils"; export const Route = createRootRouteWithContext()({ component: RootComponent, @@ -13,9 +13,10 @@ export const Route = createRootRouteWithContext()({ function RootComponent () { const isMobile = mobileCheck(); + const theme = useLocalSetting('theme'); return ( -
    +
    diff --git a/src/mainview/routes/settings/accounts.tsx b/src/mainview/routes/settings/accounts.tsx index 5ce1194..4cd0faa 100644 --- a/src/mainview/routes/settings/accounts.tsx +++ b/src/mainview/routes/settings/accounts.tsx @@ -113,6 +113,7 @@ function RouteComponent () { const { focus } = Route.useSearch(); const { ref, focusKey, focusSelf } = useFocusable({ + focusKey: "accounts", preferredChildFocusKey: focus }); diff --git a/src/mainview/routes/settings/directories.tsx b/src/mainview/routes/settings/directories.tsx index 8cb7050..d0edb9c 100644 --- a/src/mainview/routes/settings/directories.tsx +++ b/src/mainview/routes/settings/directories.tsx @@ -41,7 +41,7 @@ function DriveComponent (data: { drive: DownloadsDrive; downloadsSize: number; r return
  • void; }) { - const { ref, focusKey } = useFocusable({ focusKey: 'categories' }); + const { ref, focused, focusKey } = useFocusable({ focusKey: 'categories' }); return
      {[..."ABCDEFGHIJKLMNOPQRSTVWXYZ"].map(c => - data.set(c)} content={c} id={c} action={(ctx) => ctx.focus()} type="primary" /> + data.set(c)} content={c} id={c} action={(ctx) => ctx.focus()} type="primary" /> )}
    ; @@ -47,7 +50,7 @@ function EmulatorListType (data: { category: string, action: (e: string) => void const { ref, focusKey } = useFocusable({ focusKey: 'list-section' }); return
    - e.startsWith(data.category)).map(e => ({ + e.startsWith(data.category)).map(e => ({ id: e, action: (ctx) => { @@ -152,7 +155,12 @@ function EmulatorPath (data: { id: string; }) }; return ( -

    {data.id}

    {emulators[data.id]}}> + <> +

    {data.id}

    + {emulators[data.id]} + + }>
    + onChange={(v) => { - setLocalValue(e.currentTarget.value); + setLocalValue(v); setDirty(true); }} value={localValue} @@ -223,8 +231,9 @@ function EmulatorBadge (data: {
    @@ -253,6 +262,7 @@ function RouteComponent () { const { focus } = Route.useSearch(); const { ref, focusKey, focusSelf } = useFocusable({ + focusKey: "emulators-setting", preferredChildFocusKey: focus }); diff --git a/src/mainview/routes/settings/interface.tsx b/src/mainview/routes/settings/interface.tsx new file mode 100644 index 0000000..a6c74c5 --- /dev/null +++ b/src/mainview/routes/settings/interface.tsx @@ -0,0 +1,23 @@ +import { LocalOption } from '@/mainview/components/options/LocalOption'; +import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/settings/interface')({ + component: RouteComponent, +}); + +function RouteComponent () +{ + const { focus } = Route.useSearch(); + const { ref, focusKey, focusSelf } = useFocusable({ + focusKey: "interface-settings", + preferredChildFocusKey: focus + }); + + return
      + + + + +
    ; +} diff --git a/src/mainview/routes/settings/route.tsx b/src/mainview/routes/settings/route.tsx index d3e130d..91f8d01 100644 --- a/src/mainview/routes/settings/route.tsx +++ b/src/mainview/routes/settings/route.tsx @@ -83,7 +83,7 @@ function MenuItem (data: { "group rounded-full p-3 md:pl-5 text-base-content/80", classNames({ "bg-primary text-primary-content": acitve, - "font-semibold sm:ring-4 md:ring-7 ring-primary-content": focused && !isPointer, + "font-semibold sm:ring-4 md:ring-7 ring-accent": focused && !isPointer, "bg-secondary text-secondary-content ring-primary": data.return && focused, }), data.linkClassName, @@ -110,7 +110,7 @@ function SettingsMenu (data: {}) return
      -
      +