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 && (
- }
- />
-
- ) :
+export function GameCover({game, size = 300, radius = "sm", interactive = false}: GameCoverProps) {
+ const coverContent = Number.isInteger(game.coverId) ? (
+
+ }
+ />
+
+ ) : (
+
);
+
+ 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 &&
+
+
+