From 8f5988ecf4bfeaf1cef59a0c950c4415b7bbc70d Mon Sep 17 00:00:00 2001 From: Ryan Mercado Date: Wed, 31 Jan 2024 23:46:55 -0400 Subject: [PATCH] Merge develop (#2) * Init: enforce passing path changes thru to main, start input modal non-shifted * fix scanning deeply nested ROMs; optimize scan performance * allow opting folders out of scan w/ .eh-ignore file * fix: Game Art settings not respected in System View * fix update loop from not doing equality check on recentlyViewed props --------- Co-authored-by: Ryan Mercado --- src/preload/util/scanRoms.ts | 59 +++++++++++-------- src/renderer/src/atoms/defaults/systems.ts | 1 - src/renderer/src/atoms/games.ts | 4 +- .../components/GridScroller/GridScroller.tsx | 11 +--- .../src/components/InputModal/InputModal.tsx | 5 +- .../src/components/MediaTile/MediaTile.tsx | 8 ++- .../components/MediaTile/Presets/GameTile.tsx | 28 +++++++++ .../MediaTile/Presets/SystemTile.tsx | 26 ++++++++ .../src/components/Scroller/index.tsx | 37 ++++-------- src/renderer/src/pages/Home/index.tsx | 13 +++- src/renderer/src/pages/Init/Init.tsx | 9 +-- 11 files changed, 127 insertions(+), 74 deletions(-) create mode 100644 src/renderer/src/components/MediaTile/Presets/GameTile.tsx create mode 100644 src/renderer/src/components/MediaTile/Presets/SystemTile.tsx diff --git a/src/preload/util/scanRoms.ts b/src/preload/util/scanRoms.ts index e86e09e..6a1f733 100644 --- a/src/preload/util/scanRoms.ts +++ b/src/preload/util/scanRoms.ts @@ -1,32 +1,31 @@ import { Game, System } from "@common/types"; import path from "path"; -import { isEqual } from "lodash" import ShortUniqueId from "short-unique-id"; import { MainPaths } from "@common/types/Paths"; import { readdir, stat } from "fs/promises"; const uid = new ShortUniqueId(); +const systemExtnameMap: Record> = {} + const scanRoms = async ( - cleanupMissingGames = false, paths: MainPaths, currentSystems: System[], currentGames: Game[] ) => { + const getGameLookupKey = (romname: string, systemId: string, rompath: string[] = []) => `${romname}-${systemId}-${rompath.join("_")}` + const gameLookupMap: Record = currentGames.reduce((acc, game) => { + const key = getGameLookupKey(game.romname, game.system, game.rompath); + acc[key] = game; + return acc; + }, {}) + const { ROMs: ROM_PATH } = paths; const addedDate = new Date().toUTCString(); const newGames: Game[] = []; const romsDir = await readdir(ROM_PATH); - const compareRomPaths = (fromGame: string[] | undefined, fromScan: string[]) => { - if (!fromGame || !fromGame?.length) { - return !fromScan.length - } else { - return isEqual(fromGame, fromScan) - } - } - // remove leading periods, make lowercase const normalizeExtname = (extname: string) => { const leadingPeriodRemoved = extname.startsWith(".") @@ -38,10 +37,25 @@ const scanRoms = async ( const scanFolder = async (systemConfig: System, pathTokens: string[] = []) => { const dir = path.join(ROM_PATH, systemConfig.id, ...pathTokens); - let contents = await readdir(dir); + let contents: string[]; + + try { + contents = await readdir(dir); + } catch(e) { + console.error(`Failed to read ${systemConfig.name} directory at "${dir}"`); + return; + } // handle multi-part games by filtering out other tracks/discs contents = contents.filter(entry => !entry.match(/\((Track|Disc) [^1]\)/)); + if(!contents.length) return; + if(contents.includes('.eh-ignore')) return; + + const extnames = systemExtnameMap[systemConfig.id] || (() => { + const extnames = new Set(systemConfig.fileExtensions.map(normalizeExtname)) + systemExtnameMap[systemConfig.id] = extnames; + return extnames; + })() for (const entry of contents) { const entryPath = path.join(dir, entry); @@ -49,24 +63,17 @@ const scanRoms = async ( const entryStat = await stat(entryPath); if (entryStat.isDirectory()) { - scanFolder(systemConfig, [...pathTokens, entry]); + await scanFolder(systemConfig, [...pathTokens, entry]); continue; } - if (!systemConfig - .fileExtensions - .map(normalizeExtname) - .includes(normalizeExtname(entryExt)) - ) continue; + if (!extnames.has(normalizeExtname(entryExt))) continue; - const gameConfigEntry = currentGames.find(game => ( - game.romname === entry - && game.system === systemConfig.id - && compareRomPaths(game.rompath, pathTokens) - )) + const lookupKey = getGameLookupKey(entry, systemConfig.id, pathTokens); + const gameConfigEntry = gameLookupMap[lookupKey]; if(gameConfigEntry) { - cleanupMissingGames && newGames.push(gameConfigEntry); + newGames.push(gameConfigEntry); continue; } @@ -81,14 +88,16 @@ const scanRoms = async ( } } + const scanQueue: Promise[] = []; for (const system of romsDir) { const systemConfig = currentSystems.find(config => config.id === system); if (!systemConfig) continue; - await scanFolder(systemConfig) + scanQueue.push(scanFolder(systemConfig)) } - return [...(cleanupMissingGames ? [] : currentGames), ...newGames]; + await Promise.allSettled(scanQueue); + return newGames; } export default scanRoms; diff --git a/src/renderer/src/atoms/defaults/systems.ts b/src/renderer/src/atoms/defaults/systems.ts index 8fd47fd..2a3fc72 100644 --- a/src/renderer/src/atoms/defaults/systems.ts +++ b/src/renderer/src/atoms/defaults/systems.ts @@ -318,5 +318,4 @@ const parsedSystems: System[] = defaultSystems.map(system => { } }).filter(Boolean) as System[] -console.log(parsedSystems) export default parsedSystems diff --git a/src/renderer/src/atoms/games.ts b/src/renderer/src/atoms/games.ts index 74d1c58..b2dc35d 100644 --- a/src/renderer/src/atoms/games.ts +++ b/src/renderer/src/atoms/games.ts @@ -15,11 +15,11 @@ const mainAtoms = arrayConfigAtoms({ storageKey: 'games' }); const scanGamesAtom = atom(null, async (get, set) => { const newGames = await window.scanRoms( - true, get(pathsAtom), get(systemMainAtoms.lists.all), get(mainAtoms.lists.all) ); + set(mainAtoms.lists.all, newGames); return newGames.length; @@ -73,7 +73,7 @@ const recentlyViewedAtom = atomFamily((filter: RecentlyViewedFilters) => atom((g new Date(b.lastViewed!).valueOf() - new Date(a.lastViewed!).valueOf() ) .slice(0, 8) -})); +}), deepEqual); const launchGameAtom = atom(null, async (get, set, gameId: string) => { const systemsList = get(systemMainAtoms.lists.all); diff --git a/src/renderer/src/components/GridScroller/GridScroller.tsx b/src/renderer/src/components/GridScroller/GridScroller.tsx index 8fe69a3..e22a2f7 100644 --- a/src/renderer/src/components/GridScroller/GridScroller.tsx +++ b/src/renderer/src/components/GridScroller/GridScroller.tsx @@ -1,5 +1,4 @@ import { ScrollerProps } from "../Scroller"; -import MediaTile from "../MediaTile/MediaTile"; import { useId, useMemo } from "react"; import AutoSizer from "react-virtualized-auto-sizer"; import { FixedSizeGrid as Grid } from "react-window" @@ -12,6 +11,7 @@ import Label from "../Label/Label"; import css from "./GridScroller.module.scss" import { Game } from "@common/types/Game"; import { System } from "@common/types/System"; +import GameTile from "../MediaTile/Presets/GameTile"; const activeCellAtom = atomFamily((_id: string) => atom({ row: 0, @@ -31,16 +31,11 @@ const GameCell = ({ columnIndex, rowIndex, style, data }) => { return (
-
) diff --git a/src/renderer/src/components/InputModal/InputModal.tsx b/src/renderer/src/components/InputModal/InputModal.tsx index b07f027..09fc163 100644 --- a/src/renderer/src/components/InputModal/InputModal.tsx +++ b/src/renderer/src/components/InputModal/InputModal.tsx @@ -26,6 +26,7 @@ interface UseInputModalProps { defaultValue?: string; isPassword?: boolean; style?: CSSProperties; + shiftOnOpen?: boolean; } export const useInputModal = () => { @@ -37,14 +38,14 @@ export const useInputModal = () => { const [, setIsCaps] = useAtom(isCapsAtom); const [, setIsShift] = useAtom(isShiftAtom); - return async ({ label, defaultValue, isPassword, style }: UseInputModalProps) => { + return async ({ label, defaultValue, isPassword, style, shiftOnOpen = true }: UseInputModalProps) => { setLabel(label); setInput(defaultValue ?? ""); setOpen(true); setIsPassword(isPassword ?? false); setStyle(style ?? {}); setIsCaps(false); - setIsShift(true); + setIsShift(shiftOnOpen); let unbindCancelListener: Unsubscribe; let unbindSubmitHandler: Unsubscribe; diff --git a/src/renderer/src/components/MediaTile/MediaTile.tsx b/src/renderer/src/components/MediaTile/MediaTile.tsx index e88ec4b..5351939 100644 --- a/src/renderer/src/components/MediaTile/MediaTile.tsx +++ b/src/renderer/src/components/MediaTile/MediaTile.tsx @@ -10,10 +10,12 @@ export interface TileMedia { foregroundText?: string } -interface Props { +export type AspectRatio = "landscape" | "square" + +export interface MediaTileProps { active?: boolean activeRef?: React.RefObject - aspectRatio?: "landscape" | "square" + aspectRatio?: AspectRatio style?: CSSProperties swapTransform?: boolean className?: string @@ -29,7 +31,7 @@ const MediaTile = ({ style, swapTransform, media -}: Props) => { +}: MediaTileProps) => { return (
& { game: Game } + +const GameTile = ({ + game, + aspectRatio = "landscape", + ...props +}: Props) => { + const tileMedia: TileMedia = (() => { + if ( + game.poster + && (!game.gameTileDisplayType || game.gameTileDisplayType === "poster") + && aspectRatio === "landscape" + ) return { background: game.poster } + + return { background: game.screenshot, foreground: game.logo, foregroundText: game.name ?? game.romname } + })() + + return +} + +export default GameTile diff --git a/src/renderer/src/components/MediaTile/Presets/SystemTile.tsx b/src/renderer/src/components/MediaTile/Presets/SystemTile.tsx new file mode 100644 index 0000000..fa9abad --- /dev/null +++ b/src/renderer/src/components/MediaTile/Presets/SystemTile.tsx @@ -0,0 +1,26 @@ +import { System } from "@common/types" +import MediaTile, { MediaTileProps } from "../MediaTile" + +type Props = Omit & { system: System } + +const SystemTile = ({ + system, + aspectRatio = "landscape", + ...props +}: Props) => { + const tileMedia = { + foreground: { + resourceType: "logo", + resourceCollection: "systems", + resourceId: system.id, + } + } as const + + return +} + +export default SystemTile diff --git a/src/renderer/src/components/Scroller/index.tsx b/src/renderer/src/components/Scroller/index.tsx index ce660ca..6453c67 100644 --- a/src/renderer/src/components/Scroller/index.tsx +++ b/src/renderer/src/components/Scroller/index.tsx @@ -3,10 +3,11 @@ import { CSSProperties, Ref, useEffect, useRef, useState } from "react"; import css from "./Scroller.module.scss" import { useKeepVisible, useOnInput } from "../../hooks" import { Input, ScrollType } from "../../enums"; -import MediaTile, { TileMedia } from "../MediaTile/MediaTile"; import Label from "../Label/Label"; import { System } from "@common/types/System"; import { Game } from "@common/types"; +import SystemTile from "../MediaTile/Presets/SystemTile"; +import GameTile from "../MediaTile/Presets/GameTile"; export interface ScrollerProps { aspectRatio?: "landscape" | "square" @@ -59,32 +60,16 @@ export const Scroller = ({ const elemIsActive = i === activeIndex const isSystem = getIsSystem(elem); - const tileMedia: TileMedia = (() => { - if(isSystem) return { - foreground: { - resourceType: "logo", - resourceCollection: "systems", - resourceId: elem.id, - } - } - - if(elem.poster - && (!elem.gameTileDisplayType || elem.gameTileDisplayType === "poster") - && aspectRatio === "landscape" - ) return { background: elem.poster } - - return { background: elem.screenshot, foreground: elem.logo, foregroundText: elem.name ?? elem.romname } - })() + const tileProps = { + key: elem.id, + activeRef: activeRef, + active: elemIsActive && isActive, + aspectRatio + } - return ( - - ) + return isSystem + ? + : }) useOnInput((input) => { diff --git a/src/renderer/src/pages/Home/index.tsx b/src/renderer/src/pages/Home/index.tsx index f4c34c8..0f3ba9c 100644 --- a/src/renderer/src/pages/Home/index.tsx +++ b/src/renderer/src/pages/Home/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Showcase, ShowcaseContent } from "../../components/Showcase" import css from "./Home.module.scss" import { useAtom } from "jotai"; @@ -48,7 +48,7 @@ export const Home = () => { } })() - const scrollers = [ + const scrollers = useMemo(() => [ { id: "continue-playing", elems: recentlyPlayedGamesList, @@ -94,7 +94,14 @@ export const Home = () => { ? "landscape" as const : "square" as const })) as ScrollerConfig[] - ] + ], [ + recentlyPlayedGamesList, + recentlyAddedGamesList, + systemsList, + recentlyViewedGamesList, + collectionsList + ]) + return (
{ // lock input while we're initializing useOnInput(() => {}, { priority: 20, disabled: modalProps.open || settingsOpen }) - const main = async (paths: MainPaths = configuredPaths) => { + const main = async (paths: MainPaths) => { await window.initRomDir(paths, systems) const gamesLength = await scanRoms(); @@ -70,10 +70,11 @@ const Init = () => { break; case "change": { const input = await getInput({ - defaultValue: paths.ROMs + defaultValue: paths.ROMs, + shiftOnOpen: false }); - if(!input) return main(); + if(!input) return main(paths); const parentDir = window.path.dirname(input); const canUseDir = window.checkDir(parentDir); @@ -107,7 +108,7 @@ const Init = () => { useEffect(() => { if(!isInitializing) { isInitializing = true; - main(); + main(configuredPaths); } }, [])