mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Add screenshot preview to GameView
Add videos to GameView
This commit is contained in:
Generated
+35
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user