diff --git a/.vscode/settings.json b/.vscode/settings.json
index 41531a4..28b818e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,6 +5,7 @@
"src-tauri/**": true,
"dist/**": true
},
+ "typescript.preferences.autoImportSpecifierExcludeRegexes": ["@radix-ui"],
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
diff --git a/src/app.tsx b/src/app.tsx
index cc8f042..3bd9788 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -2,6 +2,7 @@ import { Suspense } from "react";
import { LoadingScreen } from "./components/loading-overlay";
import { MainPage } from "./components/main-page";
import { FolderConfigModal } from "./components/modals/folder-config-modal";
+import { GameDetailsModal } from "./components/modals/game-details-modal";
import { RunningGameModal } from "./components/modals/running-game-modal";
import { VersionManagerModal } from "./components/modals/version-manager-modal";
import { UpdateIcon } from "./components/update-icon";
@@ -19,6 +20,7 @@ export function App() {
+
diff --git a/src/components/game-box.tsx b/src/components/game-box.tsx
new file mode 100644
index 0000000..e66ee8c
--- /dev/null
+++ b/src/components/game-box.tsx
@@ -0,0 +1,179 @@
+import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
+import { CircleHelp, FrownIcon, Globe, ImageOff, Play } from "lucide-react";
+import { useEffect, useMemo, useState, useTransition } from "react";
+import { toast } from "sonner";
+import CN from "@/assets/flags/cn.svg";
+import EU from "@/assets/flags/eu.svg";
+import JP from "@/assets/flags/jp.svg";
+import US from "@/assets/flags/us.svg";
+import { startGame } from "@/handlers/run-emu";
+import type { PSF } from "@/lib/native/psf";
+import { atomShowingGameDetails } from "@/store/common";
+import type { GameEntry } from "@/store/game-library";
+import { gamepadActiveAtom } from "@/store/gamepad";
+import { atomShowingRunningGame } from "@/store/running-games";
+import { atomSelectedVersion } from "@/store/version-manager";
+import { stringifyError } from "@/utils/error";
+import GamepadIcon from "./gamepad-icon";
+import { Skeleton } from "./ui/skeleton";
+import { Spinner } from "./ui/spinner";
+
+function Flag({ sfo, className }: { sfo: PSF | null; className?: string }) {
+ const region = useMemo(() => {
+ const { CONTENT_ID } = sfo?.entries ?? {};
+ return CONTENT_ID?.Text?.[0] ?? undefined;
+ }, [sfo]);
+
+ switch (region) {
+ case "U":
+ return
;
+ case "E":
+ return
;
+ case "J":
+ return
;
+ case "H":
+ return
;
+ case "I":
+ return ;
+ default:
+ return ;
+ }
+}
+
+export function GameBoxSkeleton() {
+ return (
+
+ );
+}
+
+export function EmptyGameBox() {
+ return (
+
+ );
+}
+
+export function GameBox({
+ game,
+ isFirst,
+}: {
+ game: GameEntry;
+ isFirst?: boolean;
+}) {
+ const [isPending, startTransaction] = useTransition();
+
+ const valueData = useAtomValue(game.dataLoadable);
+ const isGamepad = useAtom(gamepadActiveAtom);
+ const store = useStore();
+ const setShowingDetails = useSetAtom(atomShowingGameDetails);
+
+ const [clickCount, setClickCount] = useState(0);
+
+ useEffect(() => {
+ if (clickCount >= 3) {
+ setClickCount(0);
+ toast.info("Do a double click to start the game");
+ }
+ }, [clickCount]);
+
+ const openGame = () =>
+ startTransaction(async () => {
+ try {
+ setClickCount(0);
+ const selectEmu = store.get(atomSelectedVersion);
+ if (!selectEmu) {
+ toast.warning("No emulator selected");
+ return;
+ }
+ const r = await startGame(selectEmu, game);
+ store.set(atomShowingRunningGame, r);
+ } catch (e: unknown) {
+ toast.error("Unknown error: " + stringifyError(e));
+ }
+ });
+
+ const onClick = () => {
+ setClickCount((prev) => prev + 1);
+ };
+
+ const onBlur = () => {
+ setClickCount(0);
+ };
+
+ if (valueData.state === "loading") {
+ return ;
+ }
+
+ if (valueData.state === "hasError") {
+ return (
+
+
+
+
+ Error: {stringifyError(valueData.error)}
+
+
+
+ );
+ }
+
+ const data = valueData.data;
+
+ return (
+
+ {isPending && (
+
+
+
+ )}
+ {data.cover ? (
+

+ ) : (
+
+
+
+ )}
+
+
+
+ {/* TODO: scroll text on overflow */}
+ {data.title}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/game-library.tsx b/src/components/game-library.tsx
index 124dad5..836b524 100644
--- a/src/components/game-library.tsx
+++ b/src/components/game-library.tsx
@@ -1,186 +1,14 @@
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { useVirtualizer } from "@tanstack/react-virtual";
import { exists, mkdir } from "@tauri-apps/plugin-fs";
-import { useAtom, useAtomValue, useStore } from "jotai";
-import { CircleHelp, FrownIcon, Globe, ImageOff, Play } from "lucide-react";
-import {
- Suspense,
- useEffect,
- useMemo,
- useRef,
- useState,
- useTransition,
-} from "react";
-import { toast } from "sonner";
-import CN from "@/assets/flags/cn.svg";
-import EU from "@/assets/flags/eu.svg";
-import JP from "@/assets/flags/jp.svg";
-import US from "@/assets/flags/us.svg";
-import { startGame } from "@/handlers/run-emu";
+import { useAtom, useStore } from "jotai";
+import { Suspense, useEffect, useRef, useState } from "react";
+
import { openPath } from "@/lib/native/common";
-import type { PSF } from "@/lib/native/psf";
-import { atomGameLibrary, type GameEntry } from "@/store/game-library";
-import { gamepadActiveAtom } from "@/store/gamepad";
+import { atomGameLibrary } from "@/store/game-library";
import { atomGamesPath } from "@/store/paths";
-import { atomShowingRunningGame } from "@/store/running-games";
-import { atomSelectedVersion } from "@/store/version-manager";
-import { stringifyError } from "@/utils/error";
-import GamepadIcon from "./gamepad-icon";
+import { EmptyGameBox, GameBox, GameBoxSkeleton } from "./game-box";
import { ScrollBar } from "./ui/scroll-area";
-import { Skeleton } from "./ui/skeleton";
-import { Spinner } from "./ui/spinner";
-
-function Flag({ sfo, className }: { sfo: PSF | null; className?: string }) {
- const region = useMemo(() => {
- const { CONTENT_ID } = sfo?.entries ?? {};
- return CONTENT_ID?.Text?.[0] ?? undefined;
- }, [sfo]);
-
- switch (region) {
- case "U":
- return
;
- case "E":
- return
;
- case "J":
- return
;
- case "H":
- return
;
- case "I":
- return ;
- default:
- return ;
- }
-}
-
-function EmptyGameBox() {
- return (
-
- );
-}
-
-function GameBoxSkeleton() {
- return (
-
- );
-}
-
-function GameBox({ game, isFirst }: { game: GameEntry; isFirst?: boolean }) {
- const [isPending, startTransaction] = useTransition();
-
- const valueData = useAtomValue(game.dataLoadable);
- const isGamepad = useAtom(gamepadActiveAtom);
- const store = useStore();
-
- const [clickCount, setClickCount] = useState(0);
-
- useEffect(() => {
- if (clickCount >= 3) {
- setClickCount(0);
- toast.info("Do a double click to start the game");
- }
- }, [clickCount]);
-
- const openGame = () =>
- startTransaction(async () => {
- try {
- setClickCount(0);
- const selectEmu = store.get(atomSelectedVersion);
- if (!selectEmu) {
- toast.warning("No emulator selected");
- return;
- }
- const r = await startGame(selectEmu, game);
- store.set(atomShowingRunningGame, r);
- } catch (e: unknown) {
- toast.error("Unknown error: " + stringifyError(e));
- }
- });
-
- const onClick = () => {
- setClickCount((prev) => prev + 1);
- };
-
- const onBlur = () => {
- setClickCount(0);
- };
-
- if (valueData.state === "loading") {
- return ;
- }
-
- if (valueData.state === "hasError") {
- return (
-
-
-
-
- Error: {stringifyError(valueData.error)}
-
-
-
- );
- }
-
- const data = valueData.data;
-
- return (
-
- {isPending && (
-
-
-
- )}
- {data.cover ? (
-

- ) : (
-
-
-
- )}
-
-
-
- {/* TODO: scroll text on overflow */}
- {data.title}
-
-
-
-
-
-
-
-
-
-
-
- );
-}
function NoGameFound() {
const store = useStore();
diff --git a/src/components/modals/game-details-modal.tsx b/src/components/modals/game-details-modal.tsx
new file mode 100644
index 0000000..06e08ab
--- /dev/null
+++ b/src/components/modals/game-details-modal.tsx
@@ -0,0 +1,198 @@
+import { useAtom } from "jotai";
+import { ImageOffIcon } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import type { PSFEntry } from "@/lib/native/psf";
+import { atomShowingGameDetails } from "@/store/common";
+import type { GameEntryData } from "@/store/game-library";
+
+function Entry({ value }: { value: PSFEntry }) {
+ if (value === null || value === undefined) {
+ return N/A;
+ }
+ if (value.Text !== undefined) {
+ return <>{value.Text}>;
+ }
+ if (value.Integer !== undefined) {
+ return <>{value.Integer.toString()}>;
+ }
+ if (value.Binary !== undefined) {
+ return (
+
+ [Binary Data ({value.Binary.length} bytes)]
+
+ );
+ }
+ return [Unknown Format];
+}
+
+type Props = {
+ gameData: GameEntryData;
+ onClose: () => void;
+};
+
+function GameDetailsDialog({ gameData, onClose }: Props) {
+ return (
+
+ );
+}
+
+export function GameDetailsModal() {
+ const [showingGame, setShowingGame] = useAtom(atomShowingGameDetails);
+
+ if (!showingGame) {
+ return <>>;
+ }
+
+ return (
+ setShowingGame(null)}
+ />
+ );
+}
diff --git a/src/components/modals/running-game-modal.tsx b/src/components/modals/running-game-modal.tsx
index 87756d9..b714186 100644
--- a/src/components/modals/running-game-modal.tsx
+++ b/src/components/modals/running-game-modal.tsx
@@ -30,7 +30,11 @@ import {
} from "../ui/dialog";
import { Skeleton } from "../ui/skeleton";
-function RunningGameDialog({ runningGame }: { runningGame: RunningGame }) {
+export function RunningGameDialog({
+ runningGame,
+}: {
+ runningGame: RunningGame;
+}) {
const store = useStore();
const setShowingGame = useSetAtom(atomShowingRunningGame);
diff --git a/src/components/modals/version-manager-modal.tsx b/src/components/modals/version-manager-modal.tsx
index ca8a1e9..f346444 100644
--- a/src/components/modals/version-manager-modal.tsx
+++ b/src/components/modals/version-manager-modal.tsx
@@ -198,7 +198,10 @@ export function VersionManagerModal() {
{isNew ? (
) : (
-
+
diff --git a/src/store/common.ts b/src/store/common.ts
index ea58178..040471d 100644
--- a/src/store/common.ts
+++ b/src/store/common.ts
@@ -1,5 +1,6 @@
import type { Update } from "@tauri-apps/plugin-updater";
import { atom } from "jotai";
+import type { GameEntryData } from "./game-library";
export const oficialRepo = "shadps4-emu/shadPS4";
@@ -17,3 +18,5 @@ export const atomDownloadingOverlay = atom<
>(null);
export const atomUpdateAvailable = atom(null);
+
+export const atomShowingGameDetails = atom(null);