From 067253b30d9e715335d109721496440a440945cf Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sat, 24 May 2025 18:27:32 +0200 Subject: [PATCH] Finish SearchView implementation --- .run/UI debug.run.xml | 2 +- gameyfin/package-lock.json | 7 + gameyfin/package.json | 3 +- .../general/cards/GameOverviewCard.tsx | 8 - .../components/general/covers/CoverGrid.tsx | 16 ++ .../general/{ => covers}/CoverRow.tsx | 10 +- .../components/general/covers/GameCover.tsx | 36 ++- .../general/covers/ImageCarousel.tsx | 2 +- gameyfin/src/main/frontend/state/GameState.ts | 47 ++- gameyfin/src/main/frontend/views/HomeView.tsx | 2 +- .../src/main/frontend/views/MainLayout.tsx | 28 +- .../src/main/frontend/views/SearchView.tsx | 267 +++++++++++++++++- 12 files changed, 368 insertions(+), 60 deletions(-) delete mode 100644 gameyfin/src/main/frontend/components/general/cards/GameOverviewCard.tsx create mode 100644 gameyfin/src/main/frontend/components/general/covers/CoverGrid.tsx rename gameyfin/src/main/frontend/components/general/{ => covers}/CoverRow.tsx (88%) diff --git a/.run/UI debug.run.xml b/.run/UI debug.run.xml index 1a2afd9..ccfbe55 100644 --- a/.run/UI debug.run.xml +++ b/.run/UI debug.run.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/gameyfin/package-lock.json b/gameyfin/package-lock.json index 213c0cf..84465f9 100644 --- a/gameyfin/package-lock.json +++ b/gameyfin/package-lock.json @@ -37,6 +37,7 @@ "date-fns": "2.29.3", "formik": "^2.4.6", "framer-motion": "^12.5.0", + "fzf": "^0.5.2", "http-status-codes": "^2.3.0", "lit": "3.3.0", "moment": "^2.30.1", @@ -12998,6 +12999,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fzf": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", + "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", + "license": "BSD-3-Clause" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/gameyfin/package.json b/gameyfin/package.json index 2f94d1d..d422e7b 100644 --- a/gameyfin/package.json +++ b/gameyfin/package.json @@ -32,6 +32,7 @@ "date-fns": "2.29.3", "formik": "^2.4.6", "framer-motion": "^12.5.0", + "fzf": "^0.5.2", "http-status-codes": "^2.3.0", "lit": "3.3.0", "moment": "^2.30.1", @@ -203,4 +204,4 @@ }, "hash": "dc682332ca36d64f455f6e13888e1ffcca97e888cbad8d356973e830f7463a10" } -} \ 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 deleted file mode 100644 index 480de0a..0000000 --- a/gameyfin/src/main/frontend/components/general/cards/GameOverviewCard.tsx +++ /dev/null @@ -1,8 +0,0 @@ -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 }) { - return ( - - ); -} \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/covers/CoverGrid.tsx b/gameyfin/src/main/frontend/components/general/covers/CoverGrid.tsx new file mode 100644 index 0000000..b902c91 --- /dev/null +++ b/gameyfin/src/main/frontend/components/general/covers/CoverGrid.tsx @@ -0,0 +1,16 @@ +import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; +import {GameCover} from "Frontend/components/general/covers/GameCover"; + +interface CoverGridProps { + games: GameDto[]; +} + +export default function CoverGrid({games}: CoverGridProps) { + return ( +
+ {games.map((game) => ( + + ))} +
+ ); +} \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/CoverRow.tsx b/gameyfin/src/main/frontend/components/general/covers/CoverRow.tsx similarity index 88% rename from gameyfin/src/main/frontend/components/general/CoverRow.tsx rename to gameyfin/src/main/frontend/components/general/covers/CoverRow.tsx index cf3dfbb..9880e6d 100644 --- a/gameyfin/src/main/frontend/components/general/CoverRow.tsx +++ b/gameyfin/src/main/frontend/components/general/covers/CoverRow.tsx @@ -1,7 +1,6 @@ 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"; import {useNavigate} from "react-router"; @@ -14,7 +13,6 @@ interface CoverRowProps { 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) { @@ -47,13 +45,11 @@ export function CoverRow({games, title, onPressShowMore}: CoverRowProps) {

{title}

- +
{games.slice(0, visibleCount).map((game, index) => ( - - - + ))} - +
{showMore && (
- {game.title}} - /> -
- ) : +export function GameCover({game, size = 300, radius = "sm", interactive = false}: GameCoverProps) { + const coverContent = Number.isInteger(game.coverId) ? ( +
+ {game.title}} + /> +
+ ) : ( + ); + + return interactive ? ( + + {coverContent} + + ) : coverContent; } \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/covers/ImageCarousel.tsx b/gameyfin/src/main/frontend/components/general/covers/ImageCarousel.tsx index f9d9feb..8b4325c 100644 --- a/gameyfin/src/main/frontend/components/general/covers/ImageCarousel.tsx +++ b/gameyfin/src/main/frontend/components/general/covers/ImageCarousel.tsx @@ -48,7 +48,7 @@ export default function ImageCarousel({imageUrls, videosUrls, className}: ImageC })) || []; setElements([...images, ...videos]); - }, []) + }, [imageUrls, videosUrls]) function showImagePopup(imageUrl: string) { setSelectedImageUrl(imageUrl); diff --git a/gameyfin/src/main/frontend/state/GameState.ts b/gameyfin/src/main/frontend/state/GameState.ts index e618ac4..b3d63dc 100644 --- a/gameyfin/src/main/frontend/state/GameState.ts +++ b/gameyfin/src/main/frontend/state/GameState.ts @@ -11,6 +11,7 @@ type GameState = { state: Record; games: GameDto[]; gamesByLibraryId: Record; + sortedAlphabetically: GameDto[]; sortedByMostRecentlyAdded: GameDto[]; sortedByMostRecentlyUpdated: GameDto[]; randomlyOrderedGamesByLibraryId: Record; @@ -37,6 +38,10 @@ export const gameState = proxy({ return acc; }, {}); }, + get sortedAlphabetically() { + return this.games + .sort((a: GameDto, b: GameDto) => a.title.localeCompare(b.title, undefined, {sensitivity: 'base'})); + }, get sortedByMostRecentlyAdded() { return this.games .sort((a: GameDto, b: GameDto) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); @@ -57,25 +62,53 @@ export const gameState = proxy({ return result; }, get knownPublishers() { - return new Set(this.games.flatMap((game: GameDto) => game.publishers ? game.publishers : [])); + return new Set( + this.games + .flatMap((game: GameDto) => game.publishers ? game.publishers : []) + .sort() + ); }, get knownDevelopers() { - return new Set(this.games.flatMap((game: GameDto) => game.developers ? game.developers : [])); + return new Set( + this.games + .flatMap((game: GameDto) => game.developers ? game.developers : []) + .sort() + ); }, get knownGenres() { - return new Set(this.games.flatMap((game: GameDto) => game.genres ? game.genres : [])); + return new Set( + this.games + .flatMap((game: GameDto) => game.genres ? game.genres : []) + .sort() + ); }, get knownThemes() { - return new Set(this.games.flatMap((game: GameDto) => game.themes ? game.themes : [])); + return new Set( + this.games + .flatMap((game: GameDto) => game.themes ? game.themes : []) + .sort() + ); }, get knownKeywords() { - return new Set(this.games.flatMap((game: GameDto) => game.keywords ? game.keywords : [])); + return new Set( + this.games + .flatMap((game: GameDto) => game.keywords ? game.keywords : []) + .sort() + ); }, get knownFeatures() { - return new Set(this.games.flatMap((game: GameDto) => game.features ? game.features : [])); + return new Set( + this.games + .flatMap((game: GameDto) => game.features ? game.features : []) + .sort() + ); }, get knownPerspectives() { - return new Set(this.games.flatMap((game: GameDto) => game.perspectives ? game.perspectives : [])); + return new Set( + this.games + .flatMap((game: GameDto) => game.perspectives ? game.perspectives : []) + .sort() + ); } }); diff --git a/gameyfin/src/main/frontend/views/HomeView.tsx b/gameyfin/src/main/frontend/views/HomeView.tsx index 45e947f..e68085b 100644 --- a/gameyfin/src/main/frontend/views/HomeView.tsx +++ b/gameyfin/src/main/frontend/views/HomeView.tsx @@ -1,5 +1,5 @@ import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; -import {CoverRow} from "Frontend/components/general/CoverRow"; +import {CoverRow} from "Frontend/components/general/covers/CoverRow"; import {useSnapshot} from "valtio/react"; import {libraryState} from "Frontend/state/LibraryState"; import {gameState} from "Frontend/state/GameState"; diff --git a/gameyfin/src/main/frontend/views/MainLayout.tsx b/gameyfin/src/main/frontend/views/MainLayout.tsx index ae6eb0d..c330733 100644 --- a/gameyfin/src/main/frontend/views/MainLayout.tsx +++ b/gameyfin/src/main/frontend/views/MainLayout.tsx @@ -6,11 +6,13 @@ import GameyfinLogo from "Frontend/components/theming/GameyfinLogo"; import * as PackageJson from "../../../../package.json"; import {Outlet, useLocation, useNavigate} from "react-router"; import {useAuth} from "Frontend/util/auth"; -import {Heart, ListMagnifyingGlass} from "@phosphor-icons/react"; +import {ArrowLeft, DiceSix, Heart, House, ListMagnifyingGlass} from "@phosphor-icons/react"; import Confetti, {ConfettiProps} from "react-confetti-boom"; import {useTheme} from "next-themes"; import {UserPreferenceService} from "Frontend/util/user-preference-service"; import SearchBar from "Frontend/components/general/SearchBar"; +import {useSnapshot} from "valtio/react"; +import {gameState} from "Frontend/state/GameState"; export default function MainLayout() { const navigate = useNavigate(); @@ -19,7 +21,9 @@ export default function MainLayout() { const routeMetadata = useRouteMetadata(); const {setTheme} = useTheme(); const isSearchPage = location.pathname.startsWith("/search"); + const isHomePage = location.pathname === "/"; const [isExploding, setIsExploding] = useState(false); + const games = useSnapshot(gameState).games; useEffect(() => { let newTitle = `Gameyfin - ${routeMetadata?.title}`; @@ -55,17 +59,33 @@ export default function MainLayout() { } } + function getRandomGameId() { + return games[Math.floor(Math.random() * games.length)].id; + } + return (
{isExploding ? : <>} -
navigate('/')}> - -
+ {isHomePage ? : +
+ + +
+ }
{!isSearchPage && + + +