Skip to content

Commit 63607ee

Browse files
committed
Added basic game version selection
1 parent d5c5720 commit 63607ee

6 files changed

Lines changed: 383 additions & 57 deletions

File tree

src-tauri/capabilities/default.json

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,35 @@
99
"dialog:default",
1010
"dialog:allow-open",
1111
"fs:default",
12-
"fs:allow-create",
13-
"fs:allow-write",
14-
"fs:allow-write-file",
15-
"fs:allow-read-file",
16-
"fs:allow-exists",
12+
{
13+
"identifier": "fs:allow-create",
14+
"allow": [{ "path": "$HOME/**" }, { "path": "$DOWNLOAD/**" }, { "path": "M:/Games/GameVault/**" }, { "path": "**" }]
15+
},
16+
{
17+
"identifier": "fs:allow-mkdir",
18+
"allow": [{ "path": "$HOME/**" }, { "path": "$DOWNLOAD/**" }, { "path": "M:/Games/GameVault/**" }, { "path": "**" }]
19+
},
20+
{
21+
"identifier": "fs:allow-write",
22+
"allow": [{ "path": "$HOME/**" }, { "path": "$DOWNLOAD/**" }, { "path": "M:/Games/GameVault/**" }, { "path": "**" }]
23+
},
24+
{
25+
"identifier": "fs:allow-write-file",
26+
"allow": [{ "path": "$HOME/**" }, { "path": "$DOWNLOAD/**" }, { "path": "M:/Games/GameVault/**" }, { "path": "**" }]
27+
},
28+
{
29+
"identifier": "fs:allow-read-file",
30+
"allow": [{ "path": "$HOME/**" }, { "path": "$DOWNLOAD/**" }, { "path": "M:/Games/GameVault/**" }, { "path": "**" }]
31+
},
32+
{
33+
"identifier": "fs:allow-exists",
34+
"allow": [{ "path": "$HOME/**" }, { "path": "$DOWNLOAD/**" }, { "path": "M:/Games/GameVault/**" }, { "path": "**" }]
35+
},
36+
{
37+
"identifier": "fs:allow-remove",
38+
"allow": [{ "path": "$HOME/**" }, { "path": "$DOWNLOAD/**" }, { "path": "M:/Games/GameVault/**" }, { "path": "**" }]
39+
},
1740
"fs:allow-home-read-recursive",
1841
"fs:allow-home-write-recursive"
19-
],
20-
"scope": {
21-
"allow": ["$HOME/**", "$DOWNLOAD/**", "**"]
22-
}
42+
]
2343
}

src/components/GameCard.tsx

Lines changed: 107 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { StarIcon as StarOutline } from "@heroicons/react/24/outline";
1313
import { Button } from "@tw/button";
1414
import { GameSettings } from "@/components/admin/GameSettings";
15+
import { VersionSelectDialog } from "@/components/VersionSelectDialog";
1516
import { isTauriApp } from "@/utils/tauri";
1617
import { Alert, AlertTitle } from "@tw/alert";
1718
import {
@@ -24,6 +25,7 @@ import {
2425
import clsx from "clsx";
2526
import { useCallback, useMemo, useState, useEffect } from "react";
2627
import { Link } from "react-router";
28+
import { GameVersionEntity } from "@/api/models/GameVersionEntity";
2729

2830
export function GameCard({ game }: { game: GamevaultGame }) {
2931
const { serverUrl, user, authFetch } = useAuth();
@@ -40,6 +42,11 @@ export function GameCard({ game }: { game: GamevaultGame }) {
4042
const [bookmarkBusy, setBookmarkBusy] = useState(false);
4143
const [settingsOpen, setSettingsOpen] = useState(false);
4244
const [localGame, setLocalGame] = useState<GamevaultGame>(game);
45+
const [versionDialogOpen, setVersionDialogOpen] = useState(false);
46+
const [selectableVersions, setSelectableVersions] = useState<GameVersionEntity[]>([]);
47+
const [pendingDownloadAction, setPendingDownloadAction] = useState<
48+
"direct" | "tauri" | "client" | null
49+
>(null);
4350

4451
const coverId = getGameCoverMediaId(localGame) as number | string | null;
4552

@@ -73,10 +80,6 @@ export function GameCard({ game }: { game: GamevaultGame }) {
7380

7481
const isTauri = isTauriApp();
7582

76-
const filename = (() => {
77-
return `${localGame.title}.zip`;
78-
})();
79-
8083
const rawSize = localGame.size;
8184

8285
const formatBytes = useCallback((bytes?: number) => {
@@ -96,19 +99,83 @@ export function GameCard({ game }: { game: GamevaultGame }) {
9699
typeof rawSize === "number" ? rawSize : Number(rawSize),
97100
);
98101

102+
const resolveVersions = useCallback(async (): Promise<GameVersionEntity[]> => {
103+
if (Array.isArray(localGame.versions) && localGame.versions.length > 0) {
104+
return localGame.versions;
105+
}
106+
if (!serverUrl) return [];
107+
108+
const base = serverUrl.replace(/\/+$/, "");
109+
const res = await authFetch(`${base}/api/games/${game.id}`, {
110+
method: "GET",
111+
});
112+
if (!res.ok) return [];
113+
const fullGame = (await res.json()) as GamevaultGame;
114+
const fullVersions = Array.isArray(fullGame.versions) ? fullGame.versions : [];
115+
if (fullVersions.length > 0) {
116+
setLocalGame((prev) => ({ ...prev, versions: fullVersions }));
117+
}
118+
return fullVersions;
119+
}, [localGame.versions, serverUrl, authFetch, game.id]);
120+
121+
const executeDownloadAction = useCallback(
122+
(action: "direct" | "tauri" | "client", selectedVersion: GameVersionEntity) => {
123+
const resolvedTitle = localGame.metadata?.title || localGame.title;
124+
const selectedFilename = `${resolvedTitle}.zip`;
125+
126+
if (action === "client") {
127+
const url = `gamevault://install?gameid=${game.id}&versionid=${selectedVersion.id}`;
128+
window.location.href = url;
129+
return;
130+
}
131+
132+
startDownload({
133+
gameId: game.id,
134+
versionId: selectedVersion.id,
135+
versionName: selectedVersion.version,
136+
gameTitle: resolvedTitle,
137+
filename: selectedFilename,
138+
});
139+
140+
showAlert({
141+
title: `Added ${resolvedTitle} to the download queue`,
142+
});
143+
},
144+
[game.id, localGame, showAlert, startDownload],
145+
);
146+
147+
const selectVersionAndRun = useCallback(
148+
async (action: "direct" | "tauri" | "client") => {
149+
const versions = await resolveVersions();
150+
151+
if (!versions.length) {
152+
showAlert({
153+
title: "No downloadable version found",
154+
description: "This game currently has no available version to download.",
155+
});
156+
return;
157+
}
158+
159+
if (versions.length === 1) {
160+
executeDownloadAction(action, versions[0]);
161+
return;
162+
}
163+
164+
setSelectableVersions(versions);
165+
setPendingDownloadAction(action);
166+
setVersionDialogOpen(true);
167+
},
168+
[resolveVersions, showAlert, executeDownloadAction],
169+
);
170+
99171
const handleDirectDownload = useCallback(
100-
(e: React.MouseEvent) => {
172+
async (e: React.MouseEvent) => {
101173
e.preventDefault();
102174
e.stopPropagation();
103175
if (!serverUrl) return;
104-
startDownload(game.id, filename);
105-
106-
// Show global alert notification
107-
showAlert({
108-
title: `Added ${localGame.metadata?.title || localGame.title} to the download queue`,
109-
});
176+
await selectVersionAndRun("direct");
110177
},
111-
[serverUrl, startDownload, game.id, filename, showAlert, localGame],
178+
[serverUrl, selectVersionAndRun],
112179
);
113180

114181
const handleTauriDownload = useCallback(
@@ -118,7 +185,7 @@ export function GameCard({ game }: { game: GamevaultGame }) {
118185
if (!serverUrl) return;
119186

120187
console.log("=== handleTauriDownload called ===");
121-
console.log("Game ID:", game.id, "Filename:", filename);
188+
console.log("Game ID:", game.id);
122189

123190
try {
124191
// Get download path from localStorage
@@ -129,29 +196,33 @@ export function GameCard({ game }: { game: GamevaultGame }) {
129196
return;
130197
}
131198

132-
// Start download tracking
133199
console.log("Starting download...");
134-
startDownload(game.id, filename);
135-
136-
// Show global alert notification
137-
showAlert({
138-
title: `Added ${localGame.metadata?.title || localGame.title} to the download queue`,
139-
});
200+
await selectVersionAndRun("tauri");
140201
} catch (error) {
141202
console.error("Error starting Tauri download:", error);
142203
}
143204
},
144-
[serverUrl, startDownload, game.id, filename, showAlert, localGame],
205+
[serverUrl, selectVersionAndRun],
145206
);
146207

147208
const handleClientDownload = useCallback(
148-
(e: React.MouseEvent) => {
209+
async (e: React.MouseEvent) => {
149210
e.preventDefault();
150211
e.stopPropagation();
151-
const url = `gamevault://install?gameid=${game.id}`;
152-
window.location.href = url;
212+
await selectVersionAndRun("client");
213+
},
214+
[selectVersionAndRun],
215+
);
216+
217+
const handleVersionSelect = useCallback(
218+
(selectedVersion: GameVersionEntity) => {
219+
if (!pendingDownloadAction) return;
220+
executeDownloadAction(pendingDownloadAction, selectedVersion);
221+
setVersionDialogOpen(false);
222+
setPendingDownloadAction(null);
223+
setSelectableVersions([]);
153224
},
154-
[game.id],
225+
[pendingDownloadAction, executeDownloadAction],
155226
);
156227

157228
const gameViewUrl = `/library/${game.id}`;
@@ -287,6 +358,17 @@ export function GameCard({ game }: { game: GamevaultGame }) {
287358
onGameUpdated={(updatedGame) => setLocalGame(updatedGame)}
288359
/>
289360
)}
361+
<VersionSelectDialog
362+
open={versionDialogOpen}
363+
gameTitle={localGame.metadata?.title || localGame.title || "Game"}
364+
versions={selectableVersions}
365+
onClose={() => {
366+
setVersionDialogOpen(false);
367+
setPendingDownloadAction(null);
368+
setSelectableVersions([]);
369+
}}
370+
onSelect={handleVersionSelect}
371+
/>
290372
</>
291373
);
292374
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { GameVersionEntity } from "@/api/models/GameVersionEntity";
2+
import {
3+
Dialog,
4+
DialogActions,
5+
DialogBody,
6+
DialogDescription,
7+
DialogTitle,
8+
} from "@/components/tailwind/dialog";
9+
import { Button } from "@tw/button";
10+
11+
interface VersionSelectDialogProps {
12+
open: boolean;
13+
gameTitle: string;
14+
versions: GameVersionEntity[];
15+
onSelect: (version: GameVersionEntity) => void;
16+
onClose: () => void;
17+
}
18+
19+
export function VersionSelectDialog({
20+
open,
21+
gameTitle,
22+
versions,
23+
onSelect,
24+
onClose,
25+
}: VersionSelectDialogProps) {
26+
if (!open) return null;
27+
28+
return (
29+
<Dialog open onClose={onClose} size="lg">
30+
<DialogTitle>Select Version</DialogTitle>
31+
<DialogDescription>
32+
Choose which version of {gameTitle} you want to download.
33+
</DialogDescription>
34+
<DialogBody className="pt-3">
35+
<div className="flex flex-col gap-2">
36+
{versions.map((version) => (
37+
<button
38+
key={version.id}
39+
type="button"
40+
onClick={() => onSelect(version)}
41+
className="w-full rounded-md border border-zinc-300/60 dark:border-zinc-700 px-3 py-2 text-left text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800"
42+
>
43+
({version.id}) {version.version || "Unknown Version"}
44+
</button>
45+
))}
46+
</div>
47+
</DialogBody>
48+
<DialogActions>
49+
<Button plain onClick={onClose}>
50+
Cancel
51+
</Button>
52+
</DialogActions>
53+
</Dialog>
54+
);
55+
}
56+
57+
export default VersionSelectDialog;

0 commit comments

Comments
 (0)