@@ -329,7 +344,7 @@ function RouteComponent ()
Preferences
-
+
Overrides
diff --git a/src/mainview/routes/settings/interface.tsx b/src/mainview/routes/settings/interface.tsx
index ee0ec8f..f1a42c8 100644
--- a/src/mainview/routes/settings/interface.tsx
+++ b/src/mainview/routes/settings/interface.tsx
@@ -1,4 +1,5 @@
import { LocalOption } from '@/mainview/components/options/LocalOption';
+import { LocalSettingsSchema, settingRegistry } from '@/shared/constants';
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
import { createFileRoute } from '@tanstack/react-router';
import { Terminal } from 'lucide-react';
@@ -17,16 +18,14 @@ function RouteComponent ()
return
-
-
-
-
-
-
+ {Object.keys(LocalSettingsSchema.shape)
+ .filter(k => !settingRegistry.get(LocalSettingsSchema.shape[k as keyof typeof LocalSettingsSchema.shape])?.dev)
+ .map(k => )}
{import.meta.env.DEV && <>
Dev Settings
-
-
+ {Object.keys(LocalSettingsSchema.shape)
+ .filter(k => settingRegistry.get(LocalSettingsSchema.shape[k as keyof typeof LocalSettingsSchema.shape])?.dev)
+ .map(k => )}
>}
;
diff --git a/src/mainview/routes/store/tab/index.tsx b/src/mainview/routes/store/tab/index.tsx
index 178eea0..226f688 100644
--- a/src/mainview/routes/store/tab/index.tsx
+++ b/src/mainview/routes/store/tab/index.tsx
@@ -126,7 +126,7 @@ export function RouteComponent ()
{}
{!!crucialEmulators && crucialEmulators?.length > 0 && storeContext.showDetails('emulator', 'store', id, focus)}
+ onSelect={(em, focus) => storeContext.showDetails('emulator', em.source, em.name, focus)}
emulators={crucialEmulators} />}
();
@@ -47,15 +47,17 @@ function random ()
return Math.random() * 2 - 1;
}
-export function oneShot (id: keyof typeof soundMap)
+export function oneShot (id: keyof typeof soundMap, options?: { volume?: number; })
{
const currentDate = timingMap.get(id);
if (!getLocalSetting('soundEffects')) return;
- if (currentDate && new Date().getTime() - currentDate.getTime() <= 100) return;
- const soundValue = soundMap[id] as { key: keyof typeof soundSprites.sprite, rateVariation?: number; volumeVariation?: number; };
+ const soundValue = soundMap[id] as SoundMapEntry;
+ const maxDelay = soundValue.maxDelay ?? 100;
+ if (currentDate && new Date().getTime() - currentDate.getTime() <= maxDelay) return;
+
const instanceId = sound.play(soundValue.key);
const baseVolume = getLocalSetting("soundEffectsVolume") / 100;
- sound.volume(Math.min(baseVolume * (1 + random() * (soundValue.volumeVariation ?? 0), 1)), instanceId);
+ sound.volume(Math.min(baseVolume * (soundValue.volume ?? 1) * (options?.volume ?? 1) * (1 + random() * (soundValue.volumeVariation ?? 0), 1)), instanceId);
sound.rate(1 + sinRandom() * (soundValue.rateVariation ?? 0), instanceId);
timingMap.set(id, new Date());
}
diff --git a/src/mainview/scripts/audio/audioConstants.ts b/src/mainview/scripts/audio/audioConstants.ts
index a877e12..25c85c6 100644
--- a/src/mainview/scripts/audio/audioConstants.ts
+++ b/src/mainview/scripts/audio/audioConstants.ts
@@ -3,6 +3,15 @@ import soundSprites from '../../assets/sounds.json';
const volumeVariation = 0.05;
const rateVariation = 0.02;
+export interface SoundMapEntry
+{
+ key: keyof typeof soundSprites.sprite;
+ rateVariation?: number;
+ volumeVariation?: number;
+ volume?: number;
+ maxDelay?: number;
+}
+
export const soundMap = {
openDetails: { key: 'Classic UI SFX - Chords #2' },
returnGeneric: { key: 'Classic UI SFX - Short - Low #2' },
@@ -14,10 +23,16 @@ export const soundMap = {
selectFilter: { key: 'Classic UI SFX - Short - High #3', volumeVariation },
closeContext: { key: 'Classic UI SFX - Short - High #19' },
openContext: { key: 'Classic UI SFX - Short - High #22' },
+ openKeyboard: { key: 'Classic UI SFX - Short - High #25' },
openStore: { key: 'Classic UI SFX - Chords #16' },
openSettings: { key: 'Classic UI SFX - Short - High #8' },
click: { key: "UI_Single_Set 16_03", rateVariation, volumeVariation },
clickAlt: { key: "UI_Single_Set 16_01", rateVariation, volumeVariation },
+ keyPress: { key: "UI_Single_Set 5_02", rateVariation, volumeVariation },
+ keyPressReturn: { key: "UI_Single_Set 5_04", rateVariation, volumeVariation },
+ keyPressSpace: { key: "UI_Single_Set 5_03", rateVariation, volumeVariation },
+ keyPressBackspace: { key: "UI_Single_Set 5_01", rateVariation, volumeVariation },
+ keyHover: { key: "UI_Single_Set 11_02", rateVariation, volumeVariation, volume: 0.5, maxDelay: 60 },
invalidNavigation: { key: "Classic UI SFX - Short - Low #6", rateVariation, volumeVariation },
launch: { key: "UI SFX_InGameMenu_Open" }
-} satisfies Record;
\ No newline at end of file
+} satisfies Record;
\ No newline at end of file
diff --git a/src/mainview/scripts/brandIcons.tsx b/src/mainview/scripts/brandIcons.tsx
index ef35534..bf2e576 100644
--- a/src/mainview/scripts/brandIcons.tsx
+++ b/src/mainview/scripts/brandIcons.tsx
@@ -3,4 +3,8 @@ export const TwitchIcon =
;
+export const IGDBIcon = ;
+
+export const Rclone = ;
+
export const FlatpackIcon = ;
\ No newline at end of file
diff --git a/src/mainview/scripts/gamepads.ts b/src/mainview/scripts/gamepads.ts
index 251f000..08321a3 100644
--- a/src/mainview/scripts/gamepads.ts
+++ b/src/mainview/scripts/gamepads.ts
@@ -98,6 +98,11 @@ const throttleMap = new Map();
const throttleAcceleration = new Map();
function throttleNav (key: string, dir: string, event: Event)
{
+ if (document.activeElement && document.activeElement instanceof HTMLInputElement)
+ {
+ return false;
+ }
+
const minSpeed = 150;
const maxSpeed = 300;
const currentDate = new Date();
diff --git a/src/mainview/scripts/queries/romm.ts b/src/mainview/scripts/queries/romm.ts
index 7ebf4db..c81cb3b 100644
--- a/src/mainview/scripts/queries/romm.ts
+++ b/src/mainview/scripts/queries/romm.ts
@@ -1,6 +1,6 @@
import { DefaultRommStaleTime, GameListFilterType, RommLoginDataSchema } from "@/shared/constants";
import { rommApi, settingsApi } from "../clientApi";
-import { InvalidateQueryFilters, mutationOptions, QueryClient, QueryFilters, queryOptions, useMutation } from "@tanstack/react-query";
+import { InvalidateQueryFilters, mutationOptions, QueryClient, QueryFilters, queryOptions } from "@tanstack/react-query";
import z from "zod";
import { statsApiStatsGetOptions } from "@/clients/romm/@tanstack/react-query.gen";
@@ -166,6 +166,9 @@ export const gamesRecommendedBasedOnGameQuery = (source: string, id: string) =>
return data;
}
});
+export const allGamesInvalidateQuery: QueryFilters = {
+ queryKey: ['games']
+};
export const gameInvalidationQuery = (source: string, id: string): QueryFilters => ({
predicate (query)
{
@@ -192,16 +195,19 @@ export const fixSourceMutation = mutationOptions({
export const updateSourceMutation = mutationOptions({
mutationKey: ['game', "update_source"], mutationFn: async ({ source, id }: { source: string, id: string; }) =>
{
- const { data, error } = await rommApi.api.romm.game({ source })({ id }).update.post();
+ const { data, error } = await rommApi.api.romm.game({ source })({ id }).update.post({
+ source: source,
+ id: id
+ });
if (error) throw error;
return data;
}
});
-export const updatePlatformMutation = (id: string) => mutationOptions({
- mutationKey: ['platform', 'local', 'update', id],
+export const updatePlatformMutation = (source: string, id: string) => mutationOptions({
+ mutationKey: ['platform', source, 'update', id],
mutationFn: async () =>
{
- const { data, error } = await rommApi.api.romm.platform.local({ id }).update.post();
+ const { data, error } = await rommApi.api.romm.platform({ source })({ id }).update.post();
if (error) throw error;
return data;
}
@@ -229,4 +235,61 @@ export const gameFiltersQuery = (filters: { source?: string; }) => queryOptions(
if (error) throw error;
return data;
}
+});
+
+export const gameLookup = (search: string | undefined) => queryOptions({
+ queryKey: ['game', 'lookup', search],
+ queryFn: async () =>
+ {
+ if (!search) return [];
+ const { data, error } = await rommApi.api.romm.lookup.get({ query: { search } });
+ if (error) throw error;
+ return data;
+ }
+});
+
+export const gameLookupDetails = (source: string | undefined, id: string | undefined) => queryOptions({
+ enabled: !!source && !!id,
+ queryKey: ['game', 'lookup', source, id],
+ queryFn: async () =>
+ {
+ const { data, error } = await rommApi.api.romm.lookup({ source: source! })({ id: id! }).get();
+ if (error) throw error;
+ return data;
+ }
+});
+
+export const platformLookupMatchQuery = (source: string | undefined, id: number | undefined) => queryOptions({
+ enabled: !!source && !!id,
+ queryKey: ['platform', 'lookup', 'match', source, id],
+ queryFn: async () =>
+ {
+ const { data, error } = await rommApi.api.romm.platform.lookup.match({ source: source! })({ id: id! }).get();
+ if (error) throw error;
+ return data;
+ }
+});
+
+export const customUpdateMutation = mutationOptions({
+ mutationKey: ['game', 'custom-update'], mutationFn: async (args: { source: string, id: string, destination: string, destinationId: string; }) =>
+ {
+ const { data, error } = await rommApi.api.romm.game({ source: args.source })({ id: args.id }).update.post({ source: args.destination, id: args.destinationId });
+ if (error) throw error;
+ return data;
+ }
+});
+
+export const addManualGameMutation = mutationOptions({
+ mutationKey: ['game', 'custom-add'],
+ mutationFn: async (args: { source: string, id: string, gamePath: string, platformId: number; }) =>
+ {
+ const { data, error } = await rommApi.api.romm.add.custom.post({
+ source: args.source,
+ id: args.id,
+ gamePath: args.gamePath,
+ platformId: args.platformId
+ });
+ if (error) throw error;
+ return data;
+ }
});
\ No newline at end of file
diff --git a/src/mainview/scripts/types.ts b/src/mainview/scripts/types.ts
index e7630dc..1f45367 100644
--- a/src/mainview/scripts/types.ts
+++ b/src/mainview/scripts/types.ts
@@ -10,5 +10,6 @@ export const FOCUS_KEYS = {
EMULATOR_CARD: (id: string) => `EMULATOR_${id}`,
GAME_SECTION: "GAME_SECTION",
GAME_CARD: (id: FrontEndId) => `GAME_${id.source}_${id.id}`,
+ GAME_MATCH: (id: FrontEndId) => `GAME_${id.source}_${id.id}`,
STATS_SECTION: "STATS_SECTION",
} as const;
\ No newline at end of file
diff --git a/src/mainview/scripts/utils.ts b/src/mainview/scripts/utils.ts
index 37b2da1..d14fcc0 100644
--- a/src/mainview/scripts/utils.ts
+++ b/src/mainview/scripts/utils.ts
@@ -372,7 +372,7 @@ export function useOnNavigateBack (callback: (state: { sound?: keyof typeof soun
}, [router]);
}
-export function showKeyboardHandler (activeControl: string, node?: HTMLInputElement)
+export function showKeyboardHandler (activeControl: string | undefined, node?: HTMLInputElement)
{
if (node && node.type !== 'checkbox' && (activeControl === 'gamepad' || activeControl === 'touch'))
{
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
index e21bb54..639202e 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.ts
@@ -1,5 +1,3 @@
-
-
import { JSX } from 'react';
import * as z from 'zod';
@@ -13,6 +11,9 @@ export const RPC_PORT = 8787;
export const RPC_URL = (host: string) => `http://${host}:${RPC_PORT}`;
export const EMULATORJS_URL = (host: string) => `http://${host}:${EMULATORJS_PORT}`;
export const SOCKETS_URL = (host: string) => `ws://${host}:${RPC_PORT}`;
+export const settingRegistry = z.registry<{
+ dev?: boolean;
+}>();
export const DefaultRommStaleTime = 60 * 1000; // A minute
export interface GameMeta extends FocusParams
@@ -38,14 +39,16 @@ export const SettingsSchema = z.object({
});
export const LocalSettingsSchema = z.object({
- backgroundBlur: z.stringbool().or(z.boolean()).default(true),
- backgroundAnimation: z.stringbool().or(z.boolean()).default(true),
- theme: z.enum(['dark', 'light', 'auto']).default('auto'),
- soundEffects: z.boolean().default(true),
- soundEffectsVolume: z.number().min(0).max(100).default(50),
- hapticsEffects: z.boolean().default(true),
- showRouterDevOptions: z.boolean().default(false),
- showQueryDevOptions: z.boolean().default(false),
+ backgroundBlur: z.boolean().default(true).meta({ title: "Background Blur" }),
+ backgroundAnimation: z.boolean().default(true).meta({ title: "Background Animation" }),
+ theme: z.enum(['dark', 'light', 'auto']).default('auto').meta({ title: "Theme" }),
+ soundEffects: z.boolean().default(true).meta({ title: "Sounds" }),
+ soundEffectsVolume: z.number().min(0).max(100).default(50).meta({ title: "Sound Volume" }),
+ hapticsEffects: z.boolean().default(true).meta({ title: "Haptics" }),
+ showRouterDevOptions: z.boolean().default(false).meta({ title: "Show Router Options" }).register(settingRegistry, { dev: true }),
+ showQueryDevOptions: z.boolean().default(false).meta({ title: "Show Query Options" }).register(settingRegistry, { dev: true }),
+ useGameflowKeyboard: z.boolean().default(true).describe("Show the gameflow on screen keyboard when using a controller").meta({ title: "Use Gameflow Keyboard" }),
+ autoKeybaord: z.boolean().default(true).describe("Open on screen keybaord automatically").meta({ title: "Auto Keyboard" })
});
export const GameListFilterSchema = z.object({
@@ -114,6 +117,14 @@ export const StoreDownloadSchema = z.discriminatedUnion('type', [
})
]);
+export const NewGameSchema = z.object({
+ name: z.string(),
+ summary: z.string(),
+ genres: z.string().regex(/^$|^(\s*\S[^,]*)(\s*,\s*\S[^,]*)*\s*$/, {
+ message: "Must be a comma-separated list",
+ })
+});
+
export const StoreGameSchema = z.object({
name: z.string(),
description: z.string(),
diff --git a/src/shared/types..d.ts b/src/shared/types..d.ts
index a490edb..a0b3d19 100644
--- a/src/shared/types..d.ts
+++ b/src/shared/types..d.ts
@@ -147,6 +147,18 @@ declare interface FrontEndId
source: string;
}
+// Stuff stored in the local sqlite metadata field
+declare interface LocalGameMetadata
+{
+ genres?: string[],
+ companies?: string[],
+ game_modes?: string[],
+ age_ratings?: string[];
+ player_count?: string;
+ first_release_date?: number;
+ average_rating?: number;
+}
+
declare interface FrontEndPlatformType
{
id: FrontEndId;
@@ -279,6 +291,8 @@ declare interface DownloadInfo
declare interface DownloadPlatform
{
+ id: string;
+ source: string;
igdb_id?: number;
igdb_slug?: string;
ra_id?: number;
@@ -329,6 +343,32 @@ declare interface EmulatorSupport
capabilities?: EmulatorCapabilities[];
}
+declare interface GameLookup
+{
+ source: string;
+ id: string;
+ coverUrl: string | null | undefined;
+ slug: string | null | undefined;
+ screenshotUrls: string[];
+ name: string;
+ summary: string | null | undefined;
+ genres: string[];
+ companies: string[];
+ game_modes: string[];
+ age_ratings: string[];
+ player_count: string | undefined;
+ first_release_date: number | undefined;
+ average_rating: number | undefined;
+ keywords: string[];
+ igdb_id: number | undefined;
+ platforms: {
+ id: number;
+ name?: string | null;
+ displayName: string;
+ slug: string;
+ }[];
+}
+
declare interface AutoSaveChange
{
subPath: string;
diff --git a/src/sounds/UI_Single_Set 5_01.wav b/src/sounds/UI_Single_Set 5_01.wav
new file mode 100644
index 0000000..fa2df4b
--- /dev/null
+++ b/src/sounds/UI_Single_Set 5_01.wav
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7dfe353747423192f9f92c3427d4f14d79b1889f0c3396f15c3072164c7d9883
+size 155042
diff --git a/src/sounds/UI_Single_Set 5_03.wav b/src/sounds/UI_Single_Set 5_03.wav
new file mode 100644
index 0000000..07a0a76
--- /dev/null
+++ b/src/sounds/UI_Single_Set 5_03.wav
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4d7df23402238e2bb14cc7cc225d4b3997fd0647ec9119d6e08d27444039d4d3
+size 94402
diff --git a/src/sounds/UI_Single_Set 5_04.wav b/src/sounds/UI_Single_Set 5_04.wav
new file mode 100644
index 0000000..e5f50d1
--- /dev/null
+++ b/src/sounds/UI_Single_Set 5_04.wav
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5912777aae29c0ffadac28527d05699f690d10e0be634f6fedd0b1ce97954081
+size 94402