@@ -12,6 +12,7 @@ import {
1212import { StarIcon as StarOutline } from "@heroicons/react/24/outline" ;
1313import { Button } from "@tw/button" ;
1414import { GameSettings } from "@/components/admin/GameSettings" ;
15+ import { VersionSelectDialog } from "@/components/VersionSelectDialog" ;
1516import { isTauriApp } from "@/utils/tauri" ;
1617import { Alert , AlertTitle } from "@tw/alert" ;
1718import {
@@ -24,6 +25,7 @@ import {
2425import clsx from "clsx" ;
2526import { useCallback , useMemo , useState , useEffect } from "react" ;
2627import { Link } from "react-router" ;
28+ import { GameVersionEntity } from "@/api/models/GameVersionEntity" ;
2729
2830export 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}
0 commit comments