feat: all-video media row, clickable media selector, download button
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:
David Alvarez
2026-05-26 16:47:17 +02:00
parent 0912e43d99
commit 446a9d61b8
3 changed files with 159 additions and 42 deletions
@@ -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) {