diff --git a/gameyfin/src/main/frontend/components/ProfileMenu.tsx b/gameyfin/src/main/frontend/components/ProfileMenu.tsx index 647d828..3024b72 100644 --- a/gameyfin/src/main/frontend/components/ProfileMenu.tsx +++ b/gameyfin/src/main/frontend/components/ProfileMenu.tsx @@ -60,7 +60,7 @@ export default function ProfileMenu() { - +

Signed in as {auth.state.user?.username}

{profileMenuItems.filter(item => item.showIf !== false).map(({label, icon, onClick, color}) => { @@ -72,6 +72,7 @@ export default function ProfileMenu() { /* @ts-ignore */ color={color ? color : ""} className={`text-${color} hover:bg-primary/20`} + textValue={label} > {label}
diff --git a/gameyfin/src/main/frontend/components/general/CoverRow.tsx b/gameyfin/src/main/frontend/components/general/CoverRow.tsx new file mode 100644 index 0000000..507cf9d --- /dev/null +++ b/gameyfin/src/main/frontend/components/general/CoverRow.tsx @@ -0,0 +1,72 @@ +import React, {useEffect, useRef, useState} from "react"; +import {GameCover} from "Frontend/components/general/covers/GameCover"; +import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; +import {Card} from "@heroui/react"; +import {ArrowRight} from "@phosphor-icons/react"; + +interface CoverRowProps { + games: GameDto[]; + title: string; + onPressShowMore: () => void; +} + +const aspectRatio = 12 / 17; // aspect ratio of the game cover +const defaultImageHeight = 300; // default height for the image +const defaultImageWidth = aspectRatio * defaultImageHeight; // default width for the image +const radius = "sm"; + +export function CoverRow({games, title, onPressShowMore}: CoverRowProps) { + + const containerRef = useRef(null); + const [visibleCount, setVisibleCount] = useState(games.length); + + useEffect(() => { + const calculateVisible = () => { + if (containerRef.current) { + const containerWidth = containerRef.current.offsetWidth; + const maxFit = Math.floor((containerWidth - defaultImageWidth) / defaultImageWidth) + 1; + setVisibleCount(maxFit < games.length ? maxFit : games.length); + } + }; + + const resizeObserver = new ResizeObserver(calculateVisible); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + calculateVisible(); // initial calculation + + return () => resizeObserver.disconnect(); + }, [games.length]); + + const showMore = visibleCount < games.length; + + return ( +
+

{title}

+
+ + {games.slice(0, visibleCount).map((game, index) => ( +
+ +
+ ))} +
+ + {showMore && ( +
+
+
+

Show more

+ +
+
+ )} +
+
+ ); +} diff --git a/gameyfin/src/main/frontend/components/general/GameCover.tsx b/gameyfin/src/main/frontend/components/general/GameCover.tsx deleted file mode 100644 index 18a512c..0000000 --- a/gameyfin/src/main/frontend/components/general/GameCover.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; -import {Image} from "@heroui/react"; - -interface GameCoverProps { - game: GameDto; - size?: number; - radius?: "none" | "sm" | "md" | "lg" | "full"; -} - -export function GameCover({game, size = 300, radius = "sm"}: GameCoverProps) { - return ( - {game.title} - ); -} \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/HorizontalGameList.tsx b/gameyfin/src/main/frontend/components/general/HorizontalGameList.tsx deleted file mode 100644 index a312dbf..0000000 --- a/gameyfin/src/main/frontend/components/general/HorizontalGameList.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; -import Section from "Frontend/components/general/Section"; -import {GameCover} from "Frontend/components/general/GameCover"; -import {Card} from "@heroui/react"; - -interface HorizontalGameListProps { - title: string; - games: GameDto[]; -} - -export function HorizontalGameList({title, games}: HorizontalGameListProps) { - return ( -
-
-
- {games.length > 0 ? - games.map((game) => ( - - )) - : -
-

No content

-
-
- } -
-
- ); -} \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/cards/GameOverviewCard.tsx b/gameyfin/src/main/frontend/components/general/cards/GameOverviewCard.tsx index ad46140..480de0a 100644 --- a/gameyfin/src/main/frontend/components/general/cards/GameOverviewCard.tsx +++ b/gameyfin/src/main/frontend/components/general/cards/GameOverviewCard.tsx @@ -1,4 +1,4 @@ -import {GameCover} from "Frontend/components/general/GameCover"; +import {GameCover} from "Frontend/components/general/covers/GameCover"; import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; export function GameOverviewCard({game}: { game: GameDto }) { diff --git a/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx b/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx index 8247948..0747aac 100644 --- a/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx +++ b/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx @@ -3,7 +3,7 @@ import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/Libr import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; import React, {useEffect, useState} from "react"; import {LibraryEndpoint} from "Frontend/generated/endpoints"; -import {GameCover} from "Frontend/components/general/GameCover"; +import {GameCover} from "Frontend/components/general/covers/GameCover"; import { Alien, CastleTurret, diff --git a/gameyfin/src/main/frontend/components/general/covers/GameCover.tsx b/gameyfin/src/main/frontend/components/general/covers/GameCover.tsx new file mode 100644 index 0000000..9b19e39 --- /dev/null +++ b/gameyfin/src/main/frontend/components/general/covers/GameCover.tsx @@ -0,0 +1,24 @@ +import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; +import {Image} from "@heroui/react"; +import {GameCoverFallback} from "Frontend/components/general/covers/GameCoverFallback"; + +interface GameCoverProps { + game: GameDto; + size?: number; + radius?: "none" | "sm" | "md" | "lg"; +} + +export function GameCover({game, size = 300, radius = "sm"}: GameCoverProps) { + return ( + Number.isInteger(game.coverId) ? ( + {game.title}} + /> + ) : + ); +} \ 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 new file mode 100644 index 0000000..4f06da2 --- /dev/null +++ b/gameyfin/src/main/frontend/components/general/covers/GameCoverFallback.tsx @@ -0,0 +1,18 @@ +import {Card} from "@heroui/react"; + +interface GameCoverFallbackProps { + title: string; + size?: number; + radius?: "none" | "sm" | "md" | "lg"; +} + +export function GameCoverFallback({title, size = 300, radius = "sm"}: GameCoverFallbackProps) { + return ( + +
+ {title} +
+
+ ); +} \ No newline at end of file diff --git a/gameyfin/src/main/frontend/util/utils.ts b/gameyfin/src/main/frontend/util/utils.ts index 6f93133..f96747c 100644 --- a/gameyfin/src/main/frontend/util/utils.ts +++ b/gameyfin/src/main/frontend/util/utils.ts @@ -87,11 +87,11 @@ export function timeUntil(instantString: string, timeZone: string = moment.tz.gu * @param count * @returns {GameDto[]} */ -export async function randomGamesFromLibrary(library: LibraryDto, count: number): Promise { +export async function randomGamesFromLibrary(library: LibraryDto, count?: number): Promise { const rand = new Rand(library.id.toString()); const games = await LibraryEndpoint.getGamesInLibrary(library.id); return games .sort((a: GameDto, b: GameDto) => a.id - b.id) .sort(() => rand.next() - 0.5) - .slice(0, count); + .slice(0, count ?? games.length); } \ No newline at end of file diff --git a/gameyfin/src/main/frontend/views/HomeView.tsx b/gameyfin/src/main/frontend/views/HomeView.tsx index 5ec0c7c..0d99d10 100644 --- a/gameyfin/src/main/frontend/views/HomeView.tsx +++ b/gameyfin/src/main/frontend/views/HomeView.tsx @@ -1,9 +1,9 @@ import {useEffect, useState} from "react"; import {GameEndpoint, LibraryEndpoint} from "Frontend/generated/endpoints"; import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto"; -import {HorizontalGameList} from "Frontend/components/general/HorizontalGameList"; import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; import {randomGamesFromLibrary} from "Frontend/util/utils"; +import {CoverRow} from "Frontend/components/general/CoverRow"; export default function HomeView() { const [recentlyAddedGames, setRecentlyAddedGames] = useState([]); @@ -15,7 +15,7 @@ export default function HomeView() { setLibraries(libraries); const gamePromises = libraries.map((library) => - randomGamesFromLibrary(library, 10).then((games) => [library.id, games] as [number, GameDto[]]) + randomGamesFromLibrary(library).then((games) => [library.id, games] as [number, GameDto[]]) ); Promise.all(gamePromises).then((results) => { @@ -35,12 +35,14 @@ export default function HomeView() { return (
-

Welcome to Gameyfin!

- + alert("show more of 'Recently added'")}/> {libraries.map((library) => ( - + alert(`show more of library '${library.name}'`)} + /> ))}
diff --git a/gameyfin/src/main/frontend/views/MainLayout.tsx b/gameyfin/src/main/frontend/views/MainLayout.tsx index 6102e22..63e1dc6 100644 --- a/gameyfin/src/main/frontend/views/MainLayout.tsx +++ b/gameyfin/src/main/frontend/views/MainLayout.tsx @@ -57,35 +57,34 @@ export default function MainLayout() { } return ( -
+
{isExploding ? : <>} -
- - navigate('/')}> - - - - {auth.state.user?.emailConfirmed === false ? - - Please confirm your email - - : - "" - } - - - - - -
- -
+ + navigate('/')}> + + + + {auth.state.user?.emailConfirmed === false ? + + Please confirm your email + + : + "" + } + + + + + + +
+
-
-