mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +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-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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user