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-aria-components": "^1.7.1",
"react-confetti-boom": "^1.0.0", "react-confetti-boom": "^1.0.0",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-player": "^2.16.0",
"react-router": "7.5.2", "react-router": "7.5.2",
"swiper": "^11.2.6", "swiper": "^11.2.6",
"yup": "^1.6.1" "yup": "^1.6.1"
@@ -13992,6 +13993,12 @@
"@types/trusted-types": "^2.0.2" "@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": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -14066,6 +14073,12 @@
"node": ">= 0.4" "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": { "node_modules/merge-source-map": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", "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==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "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": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "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-aria-components": "^1.7.1",
"react-confetti-boom": "^1.0.0", "react-confetti-boom": "^1.0.0",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-player": "^2.16.0",
"react-router": "7.5.2", "react-router": "7.5.2",
"swiper": "^11.2.6", "swiper": "^11.2.6",
"yup": "^1.6.1" "yup": "^1.6.1"
@@ -129,7 +130,9 @@
"react-aria-components": "$react-aria-components", "react-aria-components": "$react-aria-components",
"react-accessible-treeview": "$react-accessible-treeview", "react-accessible-treeview": "$react-accessible-treeview",
"rand-seed": "$rand-seed", "rand-seed": "$rand-seed",
"react-router": "$react-router" "react-router": "$react-router",
"swiper": "$swiper",
"react-player": "$react-player"
}, },
"vaadin": { "vaadin": {
"dependencies": { "dependencies": {
@@ -190,6 +193,6 @@
"workbox-core": "7.3.0", "workbox-core": "7.3.0",
"workbox-precaching": "7.3.0" "workbox-precaching": "7.3.0"
}, },
"hash": "fdf5506c7d7915b341632254a47867cd7d7007cab8d08447bd909b37cdb94cf9" "hash": "b7f2b9b343406ec77ce90fe42104642df3b7205f522d2cc60db71051097a32de"
} }
} }
@@ -1,14 +1,18 @@
import {Autoplay, Virtual} from 'swiper/modules'; import {Autoplay, Virtual} from 'swiper/modules';
import {Swiper, SwiperSlide} from "swiper/react"; 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";
import "swiper/css/navigation"; import "swiper/css/navigation";
import "swiper/css/pagination"; import "swiper/css/pagination";
import "swiper/css/autoplay"; import "swiper/css/autoplay";
import {useEffect, useState} from "react";
interface ImageCarouselProps { interface ImageCarouselProps {
imageIds: number[]; imageUrls?: string[];
videosUrls?: string[];
} }
interface SlideData { interface SlideData {
@@ -18,48 +22,108 @@ interface SlideData {
isNext: boolean; 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 ( return (
<div className="flex flex-col gap-2 bg-transparent"> <div className="flex flex-col gap-2 bg-transparent">
<Swiper <Swiper
modules={[Virtual, Autoplay]} modules={[Virtual, Autoplay]}
virtual={true} virtual={true}
slidesPerView={3} slidesPerView={SLIDES_PER_VIEW}
spaceBetween={0} spaceBetween={0}
autoplay={{ autoplay={{
delay: 5000, delay: 10000,
waitForTransition: false, waitForTransition: false,
pauseOnMouseEnter: true pauseOnMouseEnter: true
}} }}
className="w-full" className="w-full"
> >
<SwiperSlide key={imageIds[imageIds.length]} virtualIndex={0}> {elements && elements.map((e, index) => (
<Image <SwiperSlide key={index} virtualIndex={index}>
src={`/images/screenshot/${imageIds[imageIds.length - 1]}`} {({isNext}: SlideData) => {
alt={`Game screenshot slide ${imageIds.length + 2}`} if (e.type === "image") {
className="w-full h-full object-cover aspect-[16/9] scale-90" return (
/> <Image
</SwiperSlide> src={e.url}
{imageIds.map((imageId, index) => ( alt={`Game screenshot slide ${index}`}
<SwiperSlide key={imageId} virtualIndex={index + 1}> className={`w-full h-full object-cover aspect-[16/9] cursor-zoom-in ${!isNext ? "scale-90" : ""}`}
{({isNext}: SlideData) => ( onClick={() => showImagePopup(e.url)}
<Image />
src={`/images/screenshot/${imageId}`} )
alt={`Game screenshot slide ${index + 1}`} }
className={`w-full h-full object-cover aspect-[16/9] ${!isNext ? "scale-90" : ""}`} return (
/> <Card
)} className={`w-full h-full aspect-[16/9] ${!isNext ? "scale-90" : ""}`}>
<ReactPlayer
url={e.url}
width="100%"
height="100%"
/>
</Card>
)
}}
</SwiperSlide> </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> </Swiper>
<ImagePopup imageUrl={selectedImageUrl} isOpen={imagePopup.isOpen} onOpenChange={imagePopup.onOpenChange}/>
</div> </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> <p>{game.summary}</p>
</div> </div>
<div className="flex flex-col gap-2 overflow-visible"> <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 && {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> </div>
</div> </div>