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
{[..."ABCDEFGHIJKLMNOPQRSTVWXYZ"].map(c =>
data.set(c)} content={c} id={c} action={(ctx) => ctx.focus()} type="primary" />
)}
;
}
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.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) => )}
;
}