feat: all-video media row, clickable media selector, download button
Build & Deploy / build-and-deploy (push) Failing after 44s
Build & Deploy / build-and-deploy (push) Failing after 44s
- Backend: Add GET /api/games/{id}/download endpoint (ZIP streaming)
- Frontend: All videos now shown in media row alongside screenshots
- Click any media thumbnail to load it into the media container
- Videos show YouTube thumbnail, screenshots show image preview
- Active thumbnail has green border highlight
- Download button between Edit and Delete triggers ZIP download
- Early return: if no media, the entire section is hidden
This commit is contained in:
@@ -4,8 +4,13 @@ import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.StreamingOutput;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@Path("/api/games")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -90,6 +95,56 @@ public class GameResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}/download")
|
||||
@Produces("application/zip")
|
||||
public Response download(@PathParam("id") String id) {
|
||||
try {
|
||||
var game = svc.load(id);
|
||||
String filename = (game.getTitle() != null ? game.getTitle() : id) + ".zip";
|
||||
|
||||
if ("sockdrive".equals(game.getBundleType())) {
|
||||
// ZIP all game files on the fly, excluding metadata
|
||||
StreamingOutput stream = output -> {
|
||||
Path gameDir = svc.gameDir(id);
|
||||
try (var zos = new ZipOutputStream(output)) {
|
||||
Files.walk(gameDir)
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(p -> !p.getFileName().toString().equals("game.json"))
|
||||
.filter(p -> !p.getFileName().toString().startsWith("cover."))
|
||||
.forEach(p -> {
|
||||
try {
|
||||
String entryName = gameDir.relativize(p).toString().replace('\\', '/');
|
||||
zos.putNextEntry(new ZipEntry(entryName));
|
||||
Files.copy(p, zos);
|
||||
zos.closeEntry();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return Response.ok(stream)
|
||||
.header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
|
||||
.build();
|
||||
} else {
|
||||
// Standard .jsdos bundle
|
||||
Path bundlePath = svc.getGamesDir().resolve(game.getBundleFile());
|
||||
if (!Files.exists(bundlePath)) {
|
||||
return Response.status(404).entity(Map.of("error", "Bundle file not found")).build();
|
||||
}
|
||||
StreamingOutput stream = output -> Files.copy(bundlePath, output);
|
||||
return Response.ok(stream)
|
||||
.header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
|
||||
.build();
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
return Response.status(404).entity(Map.of("error", "Game not found")).build();
|
||||
} catch (Exception e) {
|
||||
return Response.serverError().entity(Map.of("error", e.getMessage())).build();
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
public Response delete(@PathParam("id") String id) {
|
||||
|
||||
Reference in New Issue
Block a user