import { createFileRoute, useRouter } from '@tanstack/react-router'; import { OptionSpace } from '../../components/options/OptionSpace'; import { OptionInput } from '../../components/options/OptionInput'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useCallback, useEffect, useState } from 'react'; import { Button } from '../../components/options/Button'; import { Check, ChevronDown, FileQuestion, FolderSearch, HardDrive, Plug, SearchAlert, Store, Trash } from 'lucide-react'; import { ContextDialog, ContextList, DialogEntry, OptionElement } from '../../components/ContextDialog'; import classNames from 'classnames'; import { twMerge } from 'tailwind-merge'; import { RPC_URL, SettingsSchema } from '../../../shared/constants'; import emulators from '@emulators'; import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts'; import FilePicker from '@/mainview/components/FilePicker'; import { dirname } from 'pathe'; import { autoEmulatorsQuery, customEmulatorAddMutation, customEmulatorDeleteMutation, customEmulatorRemoveValueQuery, customEmulatorsQuery, setCustomEmulatorMutation } from '@queries/settings'; import Carousel from '@/mainview/components/Carousel'; import { FOCUS_KEYS } from '@/mainview/scripts/types'; import { scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from '@/mainview/scripts/utils'; import { SettingsOption } from '@/mainview/components/options/SettingsOption'; import { SettingsDropdown } from '@/mainview/components/options/SettingsDropdown'; export const Route = createFileRoute('/settings/emulators')({ component: RouteComponent, pendingComponent: EmulatorsPending, }); function EmulatorsPending () { return
; } function EmulatorListCat (data: { selected: string, set: (c: string) => void; }) { const { ref, focusKey } = useFocusable({ focusKey: 'categories' }); return ; } function EmulatorListType (data: { category: string, action: (e: string) => void, }) { const { ref, focusKey } = useFocusable({ focusKey: 'list-section' }); return
e.startsWith(data.category)).map(e => ({ id: e, action: (ctx) => { data.action(e); ctx.close(); }, type: 'primary', content: e } satisfies DialogEntry))} />
; } function NewEmulatorPath (data: { addOverride: (emulator: string) => void; isAddingOverride: boolean; }) { const [newEmulatorTypeOpen, setNewEmulatorTypeOpen] = useState(false); const [newEmulatorContextCat, setNewEmulatorContextCat] = useState('A'); const handleCloseContext = () => { setNewEmulatorTypeOpen(false); setFocus('emulator', { instant: true }); }; return
{ data.addOverride(e); }} />
; } function EmulatorPath (data: { id: string; }) { const [isSearching, setIsSearching] = useState(false); const [dirty, setDirty] = useState(false); const [localValue, setLocalValue] = useState(); const { data: remoteValue } = useQuery(customEmulatorRemoveValueQuery(data.id)); useEffect(() => { setLocalValue(remoteValue); }, [remoteValue]); const setSettingMutation = useMutation(setCustomEmulatorMutation(data.id, (v) => { setLocalValue(v); setDirty(false); })); const deleteMutation = useMutation(customEmulatorDeleteMutation(data.id)); const handleSave = useCallback(() => { if (dirty) { setSettingMutation.mutate(localValue ?? ''); } }, [dirty, setDirty, localValue]); const handleCloseSearch = () => { setIsSearching(false); setFocus(`search-${data.id}`, { instant: true }); }; const handleSelectPath = (path: string) => { setIsSearching(false); setSettingMutation.mutate(path); setFocus(`search-${data.id}`); }; return ( <>

{data.id}

{emulators[data.id]} }>
{ setLocalValue(v as string); setDirty(true); }} value={localValue} /> {isSearching && }
); } function EmulatorBadge (data: { emulator: FrontEndEmulator & { isCritical: boolean; }, addOverride: (emulator: string) => void; } & FocusParams) { const router = useRouter(); const { focusKey, ref, focused } = useFocusable({ focusKey: FOCUS_KEYS.EMULATOR_CARD(data.emulator.name), onFocus (l, p, details) { data.onFocus?.(focusKey, ref.current, details); } }); useShortcuts(focusKey, () => { const shortcuts: Shortcut[] = [{ label: 'Add Override', button: GamePadButtonCode.A, action: () => data.addOverride(data.emulator.name) }]; if (data.emulator.validSources.some(s => s.type === 'store')) { shortcuts.push({ button: GamePadButtonCode.Y, label: "Visit Store", action () { router.navigate({ to: '/store/details/emulator/$id', params: { id: data.emulator.name } }); }, }); } return shortcuts; }, [data.addOverride, router]); let statusIcon = ; if (data.emulator.validSources.some(s => s.exists)) { statusIcon = ; } return
v.exists), "border-dashed border-base-content/40 border-2": !data.emulator.validSources.some(v => v.exists) && data.emulator.isCritical && !focused, })) }>
{statusIcon} {!!data.emulator.logo && } {data.emulator.name}
{data.emulator.description ?? emulators[data.emulator.name]}
{data.emulator.validSources.length > 0 &&
{data.emulator.validSources.map(s => { let icon = ; let action: (() => void) | undefined = undefined; let className = "bg-warning text-warning-content"; switch (s.type) { case 'store': icon = ; className = "hover:bg-base-content hover:text-base-100 cursor-pointer bg-accent text-accent-content"; action = () => { router.navigate({ to: '/store/details/emulator/$id', params: { id: data.emulator.name } }); }; break; case 'embedded': icon = ; className = "bg-info text-info-content"; break; } return
{icon}
; })}
}
    {data.emulator.validSources.slice(0, 3).filter(s => s.exists).map(s =>
  • {s.binPath}
  • )}
; } function EmulatorBadges (data: { path?: string; addOverride: (emulator: string) => void; } & FocusParams) { const { data: autoEmulators } = useQuery({ ...autoEmulatorsQuery, select (data) { return data.toSorted((a, b) => { const sourceCompare = (b.validSources.some(s => s.exists) ? 1 : 0) - (a.validSources.some(s => s.exists) ? 1 : 0); if (sourceCompare !== 0) { return sourceCompare; } else { return b.name.localeCompare(b.name); } }); } }); const { ref, focusKey } = useFocusable({ focusKey: `emulator-badges`, focusable: !!autoEmulators && autoEmulators.length > 0, onFocus (l, p, details) { data.onFocus?.(focusKey, ref.current, details); } }); useDragScroll(ref); return {autoEmulators?.map(e => scrollIntoNearestParent(n)} key={e.name} addOverride={data.addOverride} emulator={e} />)} ; } function RouteComponent () { const { focus } = Route.useSearch(); const { ref, focusKey } = useFocusable({ focusKey: "emulators-setting", preferredChildFocusKey: focus }); const { data: customEmulators } = useQuery(customEmulatorsQuery); const addOverrideMutation = useMutation({ ...customEmulatorAddMutation, async onSuccess (data, variables, onMutateResult, context) { await context.client.invalidateQueries({ queryKey: ['custom-emulators'] }); setFocus(FOCUS_KEYS.EMULATOR_CUSTOM_PATH(variables)); }, }); return
    Preferences
    Overrides
    {!!customEmulators && customEmulators.map((key) => )}
; }