Add screenshot preview to GameView

Add videos to GameView
This commit is contained in:
grimsi
2025-05-14 01:34:15 +02:00
parent f16e2df043
commit b906d8a77b
4 changed files with 137 additions and 32 deletions
+35
View File
@@ -48,6 +48,7 @@
"react-aria-components": "^1.7.1",
"react-confetti-boom": "^1.0.0",
"react-dom": "18.3.1",
"react-player": "^2.16.0",
"react-router": "7.5.2",
"swiper": "^11.2.6",
"yup": "^1.6.1"
@@ -13992,6 +13993,12 @@
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/load-script": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==",
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -14066,6 +14073,12 @@
"node": ">= 0.4"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
"node_modules/merge-source-map": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz",
@@ -15130,6 +15143,28 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-player": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz",
"integrity": "sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.0.0",
"load-script": "^1.0.0",
"memoize-one": "^5.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.0.1"
},
"peerDependencies": {
"react": ">=16.6.0"
}
},
"node_modules/react-player/node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+5 -2
View File
@@ -43,6 +43,7 @@
"react-aria-components": "^1.7.1",
"react-confetti-boom": "^1.0.0",
"react-dom": "18.3.1",
"react-player": "^2.16.0",
"react-router": "7.5.2",
"swiper": "^11.2.6",
"yup": "^1.6.1"
@@ -129,7 +130,9 @@
"react-aria-components": "$react-aria-components",
"react-accessible-treeview": "$react-accessible-treeview",
"rand-seed": "$rand-seed",
"react-router": "$react-router"
"react-router": "$react-router",
"swiper": "$swiper",
"react-player": "$react-player"
},
"vaadin": {
"dependencies": {
@@ -190,6 +193,6 @@
"workbox-core": "7.3.0",
"workbox-precaching": "7.3.0"
},
"hash": "fdf5506c7d7915b341632254a47867cd7d7007cab8d08447bd909b37cdb94cf9"
"hash": "b7f2b9b343406ec77ce90fe42104642df3b7205f522d2cc60db71051097a32de"
}
}
@@ -1,14 +1,18 @@
import {Autoplay, Virtual} from 'swiper/modules';
import {Swiper, SwiperSlide} from "swiper/react";
import {Image} from "@heroui/react";
import {Card, Image, Modal, ModalContent, useDisclosure} from "@heroui/react";
import ReactPlayer from 'react-player';
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/autoplay";
import {useEffect, useState} from "react";
interface ImageCarouselProps {
imageIds: number[];
imageUrls?: string[];
videosUrls?: string[];
}
interface SlideData {
@@ -18,48 +22,108 @@ interface SlideData {
isNext: boolean;
}
export default function ImageCarousel({imageIds}: ImageCarouselProps) {
export default function ImageCarousel({imageUrls, videosUrls}: ImageCarouselProps) {
interface CarouselElement {
type: "image" | "video";
url: string;
}
const SLIDES_PER_VIEW = 3;
const [elements, setElements] = useState<CarouselElement[]>();
const [selectedImageUrl, setSelectedImageUrl] = useState<string>();
const imagePopup = useDisclosure();
useEffect(() => {
const images = imageUrls?.map((imageUrl) => ({
type: "image" as const,
url: imageUrl
})) || [];
const videos = videosUrls?.map((videoUrl) => ({
type: "video" as const,
url: videoUrl
})) || [];
if ((images.length + videos.length) > SLIDES_PER_VIEW) {
let elements = [...videos, ...images];
// Add the last element to the start of the array and the first element to the end of the array to create a loop
setElements([elements[elements.length - 1], ...elements, elements[0]]);
} else {
setElements([...videos, ...images]);
}
}, [])
function showImagePopup(imageUrl: string) {
setSelectedImageUrl(imageUrl);
imagePopup.onOpen();
}
return (
<div className="flex flex-col gap-2 bg-transparent">
<Swiper
modules={[Virtual, Autoplay]}
virtual={true}
slidesPerView={3}
slidesPerView={SLIDES_PER_VIEW}
spaceBetween={0}
autoplay={{
delay: 5000,
delay: 10000,
waitForTransition: false,
pauseOnMouseEnter: true
}}
className="w-full"
>
<SwiperSlide key={imageIds[imageIds.length]} virtualIndex={0}>
<Image
src={`/images/screenshot/${imageIds[imageIds.length - 1]}`}
alt={`Game screenshot slide ${imageIds.length + 2}`}
className="w-full h-full object-cover aspect-[16/9] scale-90"
/>
</SwiperSlide>
{imageIds.map((imageId, index) => (
<SwiperSlide key={imageId} virtualIndex={index + 1}>
{({isNext}: SlideData) => (
<Image
src={`/images/screenshot/${imageId}`}
alt={`Game screenshot slide ${index + 1}`}
className={`w-full h-full object-cover aspect-[16/9] ${!isNext ? "scale-90" : ""}`}
/>
)}
{elements && elements.map((e, index) => (
<SwiperSlide key={index} virtualIndex={index}>
{({isNext}: SlideData) => {
if (e.type === "image") {
return (
<Image
src={e.url}
alt={`Game screenshot slide ${index}`}
className={`w-full h-full object-cover aspect-[16/9] cursor-zoom-in ${!isNext ? "scale-90" : ""}`}
onClick={() => showImagePopup(e.url)}
/>
)
}
return (
<Card
className={`w-full h-full aspect-[16/9] ${!isNext ? "scale-90" : ""}`}>
<ReactPlayer
url={e.url}
width="100%"
height="100%"
/>
</Card>
)
}}
</SwiperSlide>
))}
<SwiperSlide key={imageIds[0]} virtualIndex={imageIds.length + 2}>
<Image
src={`/images/screenshot/${imageIds[0]}`}
alt={`Game screenshot slide ${0}`}
className="w-full h-full object-cover aspect-[16/9] scale-90"
/>
</SwiperSlide>
</Swiper>
<ImagePopup imageUrl={selectedImageUrl} isOpen={imagePopup.isOpen} onOpenChange={imagePopup.onOpenChange}/>
</div>
);
}
function ImagePopup({imageUrl, isOpen, onOpenChange}: {
imageUrl?: string,
isOpen: boolean,
onOpenChange: (isOpen: boolean) => void
}) {
return (imageUrl &&
<Modal isOpen={isOpen} onOpenChange={onOpenChange} hideCloseButton size="full" backdrop="blur">
<ModalContent className="bg-transparent">
{(onClose) => (
<div className="flex flex-grow items-center justify-center cursor-zoom-out"
onClick={onClose}>
<Image
src={imageUrl}
alt="Game screenshot"
className="max-w-[80vw] max-h-[80vh] object-contain"
/>
</div>
)}
</ModalContent>
</Modal>
)
}
@@ -66,9 +66,12 @@ export default function GameView() {
<p>{game.summary}</p>
</div>
<div className="flex flex-col gap-2 overflow-visible">
<p className="text-foreground/60">Screenshots</p>
<p className="text-foreground/60">Media</p>
{game.imageIds !== undefined && game.imageIds.length > 0 &&
<ImageCarousel imageIds={game.imageIds}/>}
<ImageCarousel
imageUrls={game.imageIds.map(id => `/images/screenshot/${id}`)}
videosUrls={game.videoUrls}
/>}
</div>
</div>
</div>