From f60026c4c0f45ea9bebf1b8a84252aa7c7bb1af8 Mon Sep 17 00:00:00 2001 From: Dennis Ludwig Date: Thu, 21 Mar 2024 23:35:11 +0100 Subject: [PATCH] feat: make dialog controllable with gamepad --- .gitignore | 1 + app/components/Dialog/index.tsx | 12 +- app/components/ErrorDialog/index.stories.tsx | 9 +- app/components/ErrorDialog/index.tsx | 53 ++--- .../GameGrid/components/Game/index.tsx | 2 +- app/components/GameGrid/index.tsx | 3 - .../categories.$category.errorDialog.tsx | 4 +- .../categories.$category.settings.general.tsx | 205 +++++++++--------- .../__tests__/categories.server.test.ts | 35 ++- .../applications/dosbox/nameMapping/dos.json | 3 + app/server/categories.server.ts | 20 +- app/server/openDialog.server.ts | 15 -- buildConfig/remix.config.js | 2 +- package.json | 7 +- 14 files changed, 209 insertions(+), 162 deletions(-) diff --git a/.gitignore b/.gitignore index 5d1a8168..adac37be 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ coverage /playwright/.cache/ /e2eTests/*Config/ /readme/build/ +/buildConfig/ diff --git a/app/components/Dialog/index.tsx b/app/components/Dialog/index.tsx index 0663fb4a..152260a8 100644 --- a/app/components/Dialog/index.tsx +++ b/app/components/Dialog/index.tsx @@ -22,7 +22,6 @@ const DialogContent = styled("div", { boxShadow: "0px 0px 20px 10px black", borderRounded: true, borderWidth: "0.2rem", - borderColor: "sidebarBackgroundColor", width: "55rem", maxWidth: "90vw", @@ -39,6 +38,14 @@ const DialogContent = styled("div", { maxWidth: "60vw", }, }, + variant: { + default: { + borderColor: "sidebarBackgroundColor", + }, + accent: { + borderColor: "accent", + }, + }, }, }); @@ -66,6 +73,7 @@ type Props = { onClose: (event?: DialogCloseEvent) => void; closable?: boolean; smaller?: boolean; + variant?: "default" | "accent"; }; export const Dialog = ({ @@ -74,6 +82,7 @@ export const Dialog = ({ onClose, closable = true, smaller = false, + variant = "default", }: Props) => { const handleClose = (event?: DialogCloseEvent) => { event?.stopPropagation(); @@ -92,6 +101,7 @@ export const Dialog = ({ event.stopPropagation(); }} smaller={smaller} + variant={variant} > {children} {closable && ( diff --git a/app/components/ErrorDialog/index.stories.tsx b/app/components/ErrorDialog/index.stories.tsx index 8282586e..59a20606 100644 --- a/app/components/ErrorDialog/index.stories.tsx +++ b/app/components/ErrorDialog/index.stories.tsx @@ -17,7 +17,14 @@ export const Basic: Story = { }, }; -// TODO: add story with large stacktrace with scrolling +export const WithLargeStacktrace: Story = { + args: { + title: "There is a large Stacktrace", + stacktrace: + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", + onClose: () => {}, + }, +}; export const UnknownError: Story = { args: { diff --git a/app/components/ErrorDialog/index.tsx b/app/components/ErrorDialog/index.tsx index c942ec37..473f82d6 100644 --- a/app/components/ErrorDialog/index.tsx +++ b/app/components/ErrorDialog/index.tsx @@ -1,30 +1,15 @@ import { styled } from "styled-system/jsx"; import { Dialog } from "~/components/Dialog"; -import { Headline } from "~/components/Headline"; import { IconChildrenWrapper } from "~/components/IconChildrenWrapper"; import { Typography } from "~/components/Typography"; import { MdErrorOutline } from "react-icons/md"; - -const Body = styled("div", { - base: { - position: "relative", - paddingTop: "1", - paddingRight: "0.5em", - paddingBottom: "1", - paddingLeft: "2", - display: "flex", - flexDirection: "column", - gap: "1", - backgroundColor: "backgroundColor", - minWidth: "25rem", - height: "100%", - }, -}); +import { ListActionBarLayout } from "../layouts/ListActionBarLayout"; +import { SidebarMainLayout } from "~/components/layouts/SidebarMainLayout"; +import type { ForwardedRef } from "react"; const Stacktrace = styled("p", { base: { whiteSpace: "pre-wrap", - overflowY: "scroll", }, }); @@ -32,23 +17,33 @@ type Props = { title?: string; stacktrace?: string; onClose: () => void; + listRef?: ForwardedRef; }; -// TODO: add radix scrollarea export const ErrorDialog = ({ title = "An unexpected error has occurred", stacktrace = "An unexpected error has occurred", onClose, + listRef, }: Props) => ( - - - - - - {title} - - - {stacktrace} - + + + + + + {title} + + } + > + {stacktrace}} + /> + + + ); diff --git a/app/components/GameGrid/components/Game/index.tsx b/app/components/GameGrid/components/Game/index.tsx index 6ce21c97..e3141d82 100644 --- a/app/components/GameGrid/components/Game/index.tsx +++ b/app/components/GameGrid/components/Game/index.tsx @@ -36,7 +36,7 @@ const Label = styled("label", { outlineWidth: "4px", outlineStyle: "solid", outlineColor: "backgroundColor", - transition: "outline-color 0.2s ease-in-out", + transition: "outline-color 0.1s ease-in-out", "&:has(*:checked)": { outlineColor: "accent", diff --git a/app/components/GameGrid/index.tsx b/app/components/GameGrid/index.tsx index 24767634..059737ec 100644 --- a/app/components/GameGrid/index.tsx +++ b/app/components/GameGrid/index.tsx @@ -25,7 +25,6 @@ type Props = { isInFocus: boolean; onBack: () => void; onGameClick: () => void; - onSelectGameByGamepad: () => void; onExecute: () => void; "data-testid"?: string; } & ComponentPropsWithoutRef<"ul">; @@ -64,14 +63,12 @@ export const GameGrid = ({ onBack, onExecute, onGameClick, - onSelectGameByGamepad, "data-testid": dataTestid, inViewRef, }: Props & { inViewRef?: RefObject> }) => { const { getTestId } = useTestId(dataTestid); const selectEntry = (game: ElementRef<"input">) => { - onSelectGameByGamepad(); game.checked = true; game.focus(); }; diff --git a/app/customRoutes/categories.$category.errorDialog.tsx b/app/customRoutes/categories.$category.errorDialog.tsx index 262ca3be..78418e3a 100644 --- a/app/customRoutes/categories.$category.errorDialog.tsx +++ b/app/customRoutes/categories.$category.errorDialog.tsx @@ -79,7 +79,9 @@ export default function RenderComponent() { } }, [isInFocus]); - useGamepadButtonPressEvent(layout.buttons.X, handleClose); + useGamepadButtonPressEvent(layout.buttons.A, handleClose); + useGamepadButtonPressEvent(layout.buttons.DPadUp, handleScrollUp); + useGamepadButtonPressEvent(layout.buttons.DPadDown, handleScrollDown); useGamepadButtonPressEvent(layout.buttons.B, handleClose); useGamepadButtonPressEvent(layout.buttons.Start, handleClose); diff --git a/app/customRoutes/categories.$category.settings.general.tsx b/app/customRoutes/categories.$category.settings.general.tsx index 235887c7..06d8d0b7 100644 --- a/app/customRoutes/categories.$category.settings.general.tsx +++ b/app/customRoutes/categories.$category.settings.general.tsx @@ -6,6 +6,7 @@ import { IoMdDownload, IoMdRefresh } from "react-icons/io"; import { FaFolderOpen } from "react-icons/fa"; import { Form, + Outlet, useActionData, useLoaderData, useNavigation, @@ -319,48 +320,84 @@ export default function Index() { }, [disableGamepads]); return ( - - - - General - - - } - > -
- - {defaultData.isWindows && ( + <> + + + + General + + + } + > + + + {defaultData.isWindows && ( +
  • + + + + + setApplicationPath(event.target.value) + } + iconButton + /> + + + + + +
  • + )}
  • -
  • - )} -
  • - - - - - setCategoriesPath(event.target.value) - } - iconButton - /> - - - - - -
  • - - } - actions={ - <> - - {!defaultData.isWindows && defaultData.categories.length > 0 && ( + + } + actions={ + <> - )} - - } - /> - -
    + {!defaultData.isWindows && + defaultData.categories.length > 0 && ( + + )} + + } + /> + +
    + + ); } diff --git a/app/server/__tests__/categories.server.test.ts b/app/server/__tests__/categories.server.test.ts index 6737255a..d2fc6568 100644 --- a/app/server/__tests__/categories.server.test.ts +++ b/app/server/__tests__/categories.server.test.ts @@ -30,7 +30,7 @@ import { playstation, } from "../__testData__/category"; import { applications as applicationsTestData } from "../__testData__/applications"; -import type { Category, Entry } from "~/types/jsonFiles/category"; +import type { Category, Entry, MetaData } from "~/types/jsonFiles/category"; import { general } from "../__testData__/general"; import { fetchMetaData } from "~/server/igdb.server"; import { categories as categoriesDB } from "../categoriesDB.server"; @@ -107,6 +107,39 @@ describe("categories.server", () => { expect(result).toStrictEqual(expectedResult); }); + + it("Should return oldData if exist", () => { + (readFilenames as Mock).mockReturnValueOnce([ + createAbsoluteEntryPath(playstation.name, hugo.path), + createAbsoluteEntryPath(playstation.name, hugo2.path), + ]); + + const hugoMetaData: MetaData = { + imageUrl: "https://www.allImagesComeFromHere.com/hugo.webp", + expiresOn: getExpiresOn(), + }; + + const oldEntries: Entry[] = [ + { + ...hugo, + id: `${hugo.id}0`, + metaData: hugoMetaData, + }, + ]; + + const expectedResult: Entry[] = [ + { ...hugo, metaData: hugoMetaData, id: `${hugo.id}0` }, + { ...hugo2, id: `${hugo2.id}1` }, + ]; + + const result = readEntries( + playstation.name, + playstation.application.id, + oldEntries, + ); + + expect(result).toStrictEqual(expectedResult); + }); }); describe("readEntriesWithMetaData", () => { diff --git a/app/server/applicationsDB.server/applications/dosbox/nameMapping/dos.json b/app/server/applicationsDB.server/applications/dosbox/nameMapping/dos.json index 9641d02e..a88c5c11 100644 --- a/app/server/applicationsDB.server/applications/dosbox/nameMapping/dos.json +++ b/app/server/applicationsDB.server/applications/dosbox/nameMapping/dos.json @@ -1,4 +1,5 @@ { + "/ad.exe": "Archimedean Dynasty", "/calgames.exe": "California Games", "/digdogs.exe": "Dig Dogs: Streetbusters", "/dn1.exe": "Duke Nukem (Episode 1: Shrapnel City)", @@ -11,6 +12,8 @@ "/duke3d.exe": "Duke Nukem 3D", "/dune2.exe": "Dune 2", "/hospital.exe": "Theme Hospital", + "/jazz.exe": "Jazz Jackrabbit", + "HH95/jazz.exe": "Jazz Jackrabbit: Holiday Hare", "/jim.exe": "Jim Power: The lost Dimension", "/keen1.exe": "Commander Keen in Invasion of the Vorticons: Marooned on Mars", "/keen2.exe": "Commander Keen in Invasion of the Vorticons: The Earth Explodes", diff --git a/app/server/categories.server.ts b/app/server/categories.server.ts index 529a7cd6..a46d17bb 100644 --- a/app/server/categories.server.ts +++ b/app/server/categories.server.ts @@ -29,7 +29,7 @@ import { FileDataCache, MultipleFileDataCache, } from "~/server/FileDataCache.server"; -import { openErrorDialog } from "~/server/openDialog.server"; +import { setErrorDialog } from "~/server/errorDialog.server"; export const paths = { categories: "data/categories.json", @@ -125,7 +125,9 @@ export const readEntries = ( ? nodepath.basename(filename).split(extension)[0] : nodepath.basename(filename); - const oldEntryData = oldEntries?.find(({ path }) => path === filename); + const oldEntryData = oldEntries?.find( + ({ path }) => path === nodepath.relative(categoryPath, filename), + ); if (oldEntryData) { return oldEntryData; } else { @@ -202,16 +204,19 @@ export const importEntries = async (category: SystemId) => { }); }) .catch((error) => { - openErrorDialog( + setErrorDialog( + "Fetch MetaData from igdb failed", "Please try again later", - "Fetch covers from igdb failed", ); - console.log("igdb error", error); + console.log("igdb error", error, entries); + + // Write data anyway to add or remove games writeCategory({ ...oldCategoryData, application, entries, }); + throw new Error(); }); } }; @@ -319,10 +324,11 @@ export const importCategories = async () => { categorySettledResult.status === "rejected", ) ) { - openErrorDialog( + setErrorDialog( + "Fetch MetaData from igdb failed", "Please try again later", - "Fetch covers from igdb failed", ); + throw new Error(); } }); } diff --git a/app/server/openDialog.server.ts b/app/server/openDialog.server.ts index eb705f5a..5685991a 100644 --- a/app/server/openDialog.server.ts +++ b/app/server/openDialog.server.ts @@ -13,18 +13,3 @@ export const openFolderDialog = (title: string, defaultPath?: string) => { return undefined; }; - -export const openErrorDialog = (error: unknown, title: string) => { - let errorMessage; - if (error instanceof Error) { - errorMessage = error.message; - } else if (typeof error === "string") { - errorMessage = error; - } else { - console.log("unknown error type"); - } - - if (errorMessage && dialog) { - dialog.showErrorBox(title, errorMessage.slice(0, 500)); - } -}; diff --git a/buildConfig/remix.config.js b/buildConfig/remix.config.js index 1aaa9dc5..dff74ff7 100644 --- a/buildConfig/remix.config.js +++ b/buildConfig/remix.config.js @@ -43,4 +43,4 @@ var appConfig = { }, postcss: true, }; -exports.default = appConfig; +module.exports = appConfig; diff --git a/package.json b/package.json index e4c6131d..4b1f2fe4 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,11 @@ "prepare": "husky install && yarn panda:codegen", "clean": "del-cli dist build buildDesktop public/build coverage", "panda:codegen": "panda codegen --clean", - "dev": "yarn clean && yarn panda:codegen && remix dev", - "dev:electron": "yarn clean && yarn panda:codegen && yarn build:electron && remix dev --command 'electron .'", + "dev": "yarn clean && yarn panda:codegen && yarn transpileConfigs && remix dev", + "dev:electron": "yarn clean && yarn panda:codegen && yarn build:electron && yarn transpileConfigs && remix dev --command 'electron .'", + "transpileConfigs": "tsc -p ./configsToTs/tsconfig.json && cp ./buildConfig/remix.config.js .", "build": "yarn clean && cross-env NODE_ENV=production yarn panda:codegen && concurrently -c 'auto' 'yarn:build:*'", - "build:remix": "remix build", + "build:remix": "yarn transpileConfigs && remix build", "build:electron": "tsc -p ./desktop/tsconfig.json", "test": "vitest run --passWithNoTests", "ts:check": "tsc --noEmit && yarn build:electron --noEmit",