From 71a42ccf0c8692f8325aa909f59a0d2e98bc82dc Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Wed, 14 May 2025 17:33:03 +0200
Subject: [PATCH] Progress implementation of GameView
---
.../frontend/components/general/CoverRow.tsx | 4 +-
.../components/general/covers/GameCover.tsx | 7 +-
.../general/covers/GameCoverFallback.tsx | 6 +-
.../general/covers/ImageCarousel.tsx | 125 +++++++++++-------
.../components/general/input/ComboButton.tsx | 11 +-
gameyfin/src/main/frontend/main.css | 10 +-
gameyfin/src/main/frontend/util/utils.ts | 2 +-
gameyfin/src/main/frontend/views/GameView.tsx | 47 +++++--
8 files changed, 139 insertions(+), 73 deletions(-)
diff --git a/gameyfin/src/main/frontend/components/general/CoverRow.tsx b/gameyfin/src/main/frontend/components/general/CoverRow.tsx
index a5c969c..a5c959a 100644
--- a/gameyfin/src/main/frontend/components/general/CoverRow.tsx
+++ b/gameyfin/src/main/frontend/components/general/CoverRow.tsx
@@ -47,11 +47,11 @@ export function CoverRow({games, title, onPressShowMore}: CoverRowProps) {
{title}
-
+
{games.slice(0, visibleCount).map((game, index) => (
navigate(`/game/${game.id}`)}>
-
+
))}
diff --git a/gameyfin/src/main/frontend/components/general/covers/GameCover.tsx b/gameyfin/src/main/frontend/components/general/covers/GameCover.tsx
index 9b19e39..cbbb3c1 100644
--- a/gameyfin/src/main/frontend/components/general/covers/GameCover.tsx
+++ b/gameyfin/src/main/frontend/components/general/covers/GameCover.tsx
@@ -6,19 +6,20 @@ interface GameCoverProps {
game: GameDto;
size?: number;
radius?: "none" | "sm" | "md" | "lg";
+ hover?: boolean;
}
-export function GameCover({game, size = 300, radius = "sm"}: GameCoverProps) {
+export function GameCover({game, size = 300, radius = "sm", hover = false}: GameCoverProps) {
return (
Number.isInteger(game.coverId) ? (
}
/>
- ) :
+ ) :
);
}
\ No newline at end of file
diff --git a/gameyfin/src/main/frontend/components/general/covers/GameCoverFallback.tsx b/gameyfin/src/main/frontend/components/general/covers/GameCoverFallback.tsx
index 4f06da2..7c6bc1d 100644
--- a/gameyfin/src/main/frontend/components/general/covers/GameCoverFallback.tsx
+++ b/gameyfin/src/main/frontend/components/general/covers/GameCoverFallback.tsx
@@ -4,12 +4,14 @@ interface GameCoverFallbackProps {
title: string;
size?: number;
radius?: "none" | "sm" | "md" | "lg";
+ hover?: boolean;
}
-export function GameCoverFallback({title, size = 300, radius = "sm"}: GameCoverFallbackProps) {
+export function GameCoverFallback({title, size = 300, radius = "sm", hover = false}: GameCoverFallbackProps) {
return (
+ radius={radius}
+ className={hover ? "scale-95 hover:scale-100" : ""}>
{title}
diff --git a/gameyfin/src/main/frontend/components/general/covers/ImageCarousel.tsx b/gameyfin/src/main/frontend/components/general/covers/ImageCarousel.tsx
index 84247a6..f9d9feb 100644
--- a/gameyfin/src/main/frontend/components/general/covers/ImageCarousel.tsx
+++ b/gameyfin/src/main/frontend/components/general/covers/ImageCarousel.tsx
@@ -1,4 +1,4 @@
-import {Autoplay, Virtual} from 'swiper/modules';
+import {Autoplay, Navigation, Pagination} from 'swiper/modules';
import {Swiper, SwiperSlide} from "swiper/react";
import {Card, Image, Modal, ModalContent, useDisclosure} from "@heroui/react";
import ReactPlayer from 'react-player';
@@ -8,11 +8,13 @@ import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/autoplay";
import {useEffect, useState} from "react";
+import {CaretLeft, CaretRight, IconContext, Play} from "@phosphor-icons/react";
interface ImageCarouselProps {
imageUrls?: string[];
videosUrls?: string[];
+ className?: string;
}
interface SlideData {
@@ -22,14 +24,14 @@ interface SlideData {
isNext: boolean;
}
-export default function ImageCarousel({imageUrls, videosUrls}: ImageCarouselProps) {
+export default function ImageCarousel({imageUrls, videosUrls, className}: ImageCarouselProps) {
interface CarouselElement {
type: "image" | "video";
url: string;
}
- const SLIDES_PER_VIEW = 3;
+ const DEFAULT_SLIDES_PER_VIEW = 3;
const [elements, setElements] = useState();
const [selectedImageUrl, setSelectedImageUrl] = useState();
@@ -45,13 +47,7 @@ export default function ImageCarousel({imageUrls, videosUrls}: ImageCarouselProp
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]);
- }
+ setElements([...images, ...videos]);
}, [])
function showImagePopup(imageUrl: string) {
@@ -60,47 +56,74 @@ export default function ImageCarousel({imageUrls, videosUrls}: ImageCarouselProp
}
return (
-
-
- {elements && elements.map((e, index) => (
-
- {({isNext}: SlideData) => {
- if (e.type === "image") {
- return (
- showImagePopup(e.url)}
- />
- )
- }
- return (
-
-
-
- )
- }}
-
- ))}
-
-
+
+ {elements && elements.length > 0 &&
+
+
+
+
+ elements.length ? elements.length : DEFAULT_SLIDES_PER_VIEW}
+ pagination={{
+ clickable: true,
+ el: ".swiper-custom-pagination"
+ }}
+ navigation={{
+ prevEl: ".swiper-custom-button-prev",
+ nextEl: ".swiper-custom-button-next"
+ }}
+ centeredSlides={true}
+ loop={true}
+ spaceBetween={0}
+ autoplay={{
+ delay: 10000,
+ disableOnInteraction: true
+ }}
+ className="w-full"
+ >
+ {elements && elements.map((e, index) => (
+
+ {({isActive}: SlideData) => {
+ if (e.type === "image") {
+ return (
+ showImagePopup(e.url)}
+ />
+ )
+ }
+ return (
+
+ }
+ />
+
+ )
+ }}
+
+ ))}
+
+
+
+
+
+
+ {/* Wrap the pagination in a div because it gets replaced at runtime be SwiperJS and loses all styling */}
+
+
+
+ }
);
}
diff --git a/gameyfin/src/main/frontend/components/general/input/ComboButton.tsx b/gameyfin/src/main/frontend/components/general/input/ComboButton.tsx
index 55e66be..26f3fb1 100644
--- a/gameyfin/src/main/frontend/components/general/input/ComboButton.tsx
+++ b/gameyfin/src/main/frontend/components/general/input/ComboButton.tsx
@@ -19,11 +19,12 @@ export interface ComboButtonOption {
}
export interface ComboButtonProps {
+ description?: string;
options: Record;
preferredOptionKey?: string;
}
-export default function ComboButton({options, preferredOptionKey}: ComboButtonProps) {
+export default function ComboButton({options, preferredOptionKey, description}: ComboButtonProps) {
const [selectedOption, setSelectedOption] = useState(new Set([Object.keys(options)[0]]));
const [disabledOptions] = useState(getDisabledKeys(options));
const selectedOptionValue = Array.from(selectedOption)[0];
@@ -56,8 +57,12 @@ export default function ComboButton({options, preferredOptionKey}: ComboButtonPr
return (
-
-
+
-
-
Summary
-
{game.summary}
+
+
+
Summary
+
{game.summary}
+
+
+
Details
+
+
+ {Object.entries({
+ "Developed by": game.developers?.sort().join(" / "),
+ "Published by": game.publishers?.sort().join(" / "),
+ "Genres": game.genres?.sort().map(p => {toTitleCase(p)}),
+ "Themes": game.themes?.sort().map(p => {toTitleCase(p)}),
+ "Features": game.features?.sort().map(p => {toTitleCase(p)}),
+ }).map(([key, value]) => (
+
+ | {key} |
+ {value} |
+
+ ))}
+
+
+
-
+
Media
- {game.imageIds !== undefined && game.imageIds.length > 0 &&
-
`/images/screenshot/${id}`)}
- videosUrls={game.videoUrls}
- />}
+ `/images/screenshot/${id}`)}
+ videosUrls={game.videoUrls}
+ className="-mx-24"
+ />