Skip to content

Commit d9ff636

Browse files
authored
Release 16.1.0
Release 16.1.0
2 parents 032888d + 6fbf6dc commit d9ff636

29 files changed

+2431
-674
lines changed

index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,19 @@
1313
<body>
1414
<div id="root"></div>
1515
<script type="module" src="/src/main.tsx"></script>
16+
<script src="https://swetrix.org/swetrix.js" defer></script>
17+
<script>
18+
document.addEventListener("DOMContentLoaded", function () {
19+
swetrix.init("dBl2xaaJ9x3M");
20+
swetrix.trackViews();
21+
});
22+
</script>
23+
<noscript>
24+
<img
25+
src="https://api.swetrix.com/log/noscript?pid=dBl2xaaJ9x3M"
26+
alt=""
27+
referrerpolicy="no-referrer-when-downgrade"
28+
/>
29+
</noscript>
1630
</body>
1731
</html>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"tauridev": "tauri dev",
1414
"build": "vite build",
1515
"lint": "prettier --write . && eslint .",
16-
"postinstall": "rimraf ./src/api && openapi-generator-cli generate -g typescript-fetch -i https://gamevault.alfagun74.de/api/docs-yaml -o ./src/api --global-property models"
16+
"postinstall": "rimraf ./src/api && openapi-generator-cli generate -g typescript-fetch -i https://gamevault.alfagun74.de/api/docs-yaml -o ./src/api --global-property models,supportingFiles -p enumPropertyNaming=snake_case -p modelPropertyNaming=snake_case"
1717
},
1818
"dependencies": {
1919
"@headlessui/react": "^2.2.8",

src/components/GameCard.tsx

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { GamevaultGame } from "@/api/models/GamevaultGame";
12
import { Media } from "@/components/Media";
23
import { useAuth } from "@/context/AuthContext";
34
import { useDownloads } from "@/context/DownloadContext";
4-
import { Game, getGameCoverMediaId } from "@/hooks/useGames";
5+
import { getGameCoverMediaId } from "@/hooks/useGames";
56
import { CloudArrowDownIcon } from "@heroicons/react/16/solid";
7+
import { StarIcon as StarSolid } from "@heroicons/react/24/solid";
8+
import { StarIcon as StarOutline } from "@heroicons/react/24/outline";
69
import { Button } from "@tw/button";
710
import {
811
Dropdown,
@@ -12,27 +15,52 @@ import {
1215
DropdownMenu,
1316
} from "@tw/dropdown";
1417
import clsx from "clsx";
15-
import { useCallback } from "react";
18+
import { useCallback, useMemo, useState } from "react";
19+
import { useNavigate } from "react-router";
1620

17-
export function GameCard({ game }: { game: Game }) {
18-
const coverId = getGameCoverMediaId(game);
19-
const { serverUrl } = useAuth();
21+
export function GameCard({ game }: { game: GamevaultGame }) {
22+
const coverId = getGameCoverMediaId(game) as number | string | null;
23+
const { serverUrl, user, authFetch } = useAuth();
24+
// Derive initial bookmarked state from raw API shape (bookmarked_users or bookmarkedUsers)
25+
const currentUserId = (user as any)?.id ?? (user as any)?.ID;
26+
const initialBookmarked = useMemo(() => {
27+
if (!currentUserId) return false;
28+
const raw = (game as any).bookmarked_users || (game as any).bookmarkedUsers;
29+
if (!Array.isArray(raw)) return false;
30+
return raw.some((u: any) => (u?.id ?? u?.ID) === currentUserId);
31+
}, [game, currentUserId]);
32+
const [bookmarked, setBookmarked] = useState<boolean>(initialBookmarked);
33+
const [bookmarkBusy, setBookmarkBusy] = useState(false);
34+
35+
const toggleBookmark = useCallback(
36+
async (e: React.MouseEvent) => {
37+
e.preventDefault();
38+
e.stopPropagation();
39+
if (!serverUrl || !currentUserId || bookmarkBusy) return;
40+
const base = serverUrl.replace(/\/+$/, "");
41+
const url = `${base}/api/users/me/bookmark/${game.id}`;
42+
const next = !bookmarked;
43+
setBookmarked(next); // optimistic
44+
setBookmarkBusy(true);
45+
try {
46+
const res = await authFetch(url, { method: next ? "POST" : "DELETE" });
47+
if (!res.ok) throw new Error(`Bookmark toggle failed (${res.status})`);
48+
} catch (err) {
49+
// rollback on error
50+
setBookmarked(!next);
51+
} finally {
52+
setBookmarkBusy(false);
53+
}
54+
},
55+
[serverUrl, currentUserId, bookmarkBusy, authFetch, game.id, bookmarked],
56+
);
2057
const { startDownload } = useDownloads() as any;
2158

2259
const filename = (() => {
23-
const p = game.path || (game as any).Path;
24-
if (!p) return `${game.title}.zip`;
25-
// Derive filename similar to Path.GetFileName
26-
try {
27-
const parts = p.split(/\\|\//);
28-
const last = parts[parts.length - 1];
29-
return last || `${game.title}.zip`;
30-
} catch {
31-
return `${game.title}.zip`;
32-
}
60+
return `${game.title}.zip`;
3361
})();
3462

35-
const rawSize = (game as any).size;
63+
const rawSize = game.size;
3664

3765
const formatBytes = useCallback((bytes?: number) => {
3866
if (bytes === undefined || bytes === null || isNaN(bytes)) return null;
@@ -71,14 +99,15 @@ export function GameCard({ game }: { game: Game }) {
7199
[game.id],
72100
);
73101

74-
const handleClientOpen = useCallback(
102+
const navigate = useNavigate();
103+
104+
const handleOpenGameView = useCallback(
75105
(e: React.MouseEvent) => {
76106
e.preventDefault();
77107
e.stopPropagation();
78-
const url = `gamevault://show?gameid=${game.id}`;
79-
window.location.href = url;
108+
navigate(`/library/${game.id}`);
80109
},
81-
[game.id],
110+
[navigate, game.id],
82111
);
83112

84113
return (
@@ -92,18 +121,43 @@ export function GameCard({ game }: { game: Game }) {
92121
<div className="relative aspect-[3/4] w-full bg-bg-muted flex items-center justify-center overflow-hidden">
93122
{coverId ? (
94123
<Media
95-
media={{ id: coverId } as any}
124+
media={{
125+
id: typeof coverId === 'number' ? coverId : Number(coverId) || 0,
126+
created_at: new Date(0),
127+
entity_version: 0,
128+
} as any}
96129
size={300}
97130
className="h-full w-full object-contain rounded-none"
98131
square
99132
alt={game.title}
100-
onClick={handleClientOpen}
133+
onClick={handleOpenGameView}
101134
/>
102135
) : (
103-
<div onClick={handleClientOpen} className="text-xs text-fg-muted">
136+
<div onClick={handleOpenGameView} className="text-xs text-fg-muted">
104137
No Cover
105138
</div>
106139
)}
140+
{/* Top-right bookmark toggle */}
141+
<button
142+
type="button"
143+
onClick={toggleBookmark}
144+
aria-label={bookmarked ? "Remove bookmark" : "Add bookmark"}
145+
aria-pressed={bookmarked}
146+
disabled={!currentUserId || bookmarkBusy}
147+
className={clsx(
148+
"absolute top-1 right-1 h-8 w-8 flex items-center justify-center rounded-md border shadow-sm backdrop-blur-sm transition-colors",
149+
"disabled:opacity-50 disabled:cursor-not-allowed",
150+
bookmarked
151+
? "bg-yellow-400/20 border-yellow-400"
152+
: "bg-zinc-900/40 dark:bg-zinc-700/50 border-white/20 hover:bg-zinc-800/60 dark:hover:bg-zinc-600/60",
153+
)}
154+
>
155+
{bookmarked ? (
156+
<StarSolid className="h-5 w-5 text-yellow-400" />
157+
) : (
158+
<StarOutline className="h-5 w-5 text-white" />
159+
)}
160+
</button>
107161
{/* Bottom-right download actions */}
108162
<div className="absolute bottom-0 right-0 p-1 z-10 flex justify-end opacity-85">
109163
<Dropdown>
@@ -130,7 +184,7 @@ export function GameCard({ game }: { game: Game }) {
130184
<h3 className="text-sm font-medium truncate" title={game.title}>
131185
{game.metadata?.title || game.title}
132186
</h3>
133-
{game.sort_title && game.sort_title !== game.title && (
187+
{(game as any).sort_title && (game as any).sort_title !== game.title && (
134188
<p
135189
className="mt-0.5 text-xs text-fg-muted truncate"
136190
title={game.title}

0 commit comments

Comments
 (0)