From e45585227e387dbd496d4680920811e99e5fecc6 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Fri, 9 May 2025 16:51:00 +0200 Subject: [PATCH] Main page finally gets somewhat usable --- .../frontend/components/general/GameCover.tsx | 2 +- .../components/general/HorizontalGameList.tsx | 29 +++++++++++++++ .../general/cards/LibraryOverviewCard.tsx | 26 ++++--------- gameyfin/src/main/frontend/util/utils.ts | 19 ++++++++++ gameyfin/src/main/frontend/views/HomeView.tsx | 37 ++++++++++++++++--- .../de/grimsi/gameyfin/games/GameEndpoint.kt | 27 ++++++++++++++ .../de/grimsi/gameyfin/games/GameService.kt | 15 ++++++++ .../de/grimsi/gameyfin/games/dto/GameDto.kt | 2 + .../de/grimsi/gameyfin/games/entities/Game.kt | 9 +++++ .../games/repositories/GameRepository.kt | 3 ++ .../gameyfin/libraries/LibraryEndpoint.kt | 9 +---- 11 files changed, 145 insertions(+), 33 deletions(-) create mode 100644 gameyfin/src/main/frontend/components/general/HorizontalGameList.tsx create mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt diff --git a/gameyfin/src/main/frontend/components/general/GameCover.tsx b/gameyfin/src/main/frontend/components/general/GameCover.tsx index ce62f66..18a512c 100644 --- a/gameyfin/src/main/frontend/components/general/GameCover.tsx +++ b/gameyfin/src/main/frontend/components/general/GameCover.tsx @@ -7,7 +7,7 @@ interface GameCoverProps { radius?: "none" | "sm" | "md" | "lg" | "full"; } -export function GameCover({game, size = 300, radius}: GameCoverProps) { +export function GameCover({game, size = 300, radius = "sm"}: GameCoverProps) { return ( {game.title} +
+
+ {games.length > 0 ? + games.map((game) => ( + + )) + : +
+

No content

+
+
+ } +
+ + ); +} \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx b/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx index a1eff51..8247948 100644 --- a/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx +++ b/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx @@ -4,7 +4,6 @@ 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 Rand from "rand-seed"; import { Alien, CastleTurret, @@ -24,32 +23,21 @@ import { import LibraryDetailsModal from "Frontend/components/general/modals/LibraryDetailsModal"; import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto"; import ScanType from "Frontend/generated/de/grimsi/gameyfin/libraries/enums/ScanType"; +import {randomGamesFromLibrary} from "Frontend/util/utils"; export function LibraryOverviewCard({library, updateLibrary}: { library: LibraryDto, updateLibrary: (library: LibraryUpdateDto) => void }) { const MAX_COVER_COUNT = 5; - const rand = new Rand(library.id.toString()); - const [randomGamesFromLibrary, setRandomGamesFromLibrary] = useState([]); + const [randomGames, setRandomGames] = useState([]); const libraryDetailsModal = useDisclosure(); useEffect(() => { - LibraryEndpoint.getGamesInLibrary(library.id).then( - (response) => { - if (response === undefined) return; - const count = Math.min(response.length, MAX_COVER_COUNT) - - let gamesFromLibrary: GameDto[] = response - .filter(g => !!g) - .sort((a: GameDto, b: GameDto) => a.id - b.id) - .sort(() => rand.next() - 0.5) - .slice(0, count) - - setRandomGamesFromLibrary(gamesFromLibrary); - } - ) + randomGamesFromLibrary(library, MAX_COVER_COUNT).then((games) => { + setRandomGames(games); + }) }, []); return ( @@ -73,9 +61,9 @@ export function LibraryOverviewCard({library, updateLibrary}: { - {randomGamesFromLibrary.length > 0 && + {randomGames.length > 0 &&
- {randomGamesFromLibrary.map((game) => ( + {randomGames.map((game) => ( ))}
diff --git a/gameyfin/src/main/frontend/util/utils.ts b/gameyfin/src/main/frontend/util/utils.ts index dec4cda..6f93133 100644 --- a/gameyfin/src/main/frontend/util/utils.ts +++ b/gameyfin/src/main/frontend/util/utils.ts @@ -1,5 +1,9 @@ import {getCsrfToken} from "Frontend/util/auth"; import moment from 'moment-timezone'; +import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto"; +import Rand from "rand-seed"; +import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; +import {LibraryEndpoint} from "Frontend/generated/endpoints"; export function cssVar(variable: string) { return getComputedStyle(document.documentElement).getPropertyValue(`--${variable}`); @@ -75,4 +79,19 @@ export function timeUntil(instantString: string, timeZone: string = moment.tz.gu } return "just now"; +} + +/** + * Select a random number of games from the library based on the library ID. + * @param library + * @param count + * @returns {GameDto[]} + */ +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); } \ 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 af1d70e..5ec0c7c 100644 --- a/gameyfin/src/main/frontend/views/HomeView.tsx +++ b/gameyfin/src/main/frontend/views/HomeView.tsx @@ -1,20 +1,47 @@ import {useEffect, useState} from "react"; -import {LibraryEndpoint} from "Frontend/generated/endpoints"; +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"; export default function HomeView() { + const [recentlyAddedGames, setRecentlyAddedGames] = useState([]); const [libraries, setLibraries] = useState([]); + const [libraryIdToGames, setLibraryIdToGames] = useState>(new Map()); useEffect(() => { LibraryEndpoint.getAllLibraries().then(libraries => { setLibraries(libraries); + + const gamePromises = libraries.map((library) => + randomGamesFromLibrary(library, 10).then((games) => [library.id, games] as [number, GameDto[]]) + ); + + Promise.all(gamePromises).then((results) => { + const libraryGamesMap = new Map(); + results.forEach(([libraryId, games]) => { + libraryGamesMap.set(libraryId, games); + }); + setLibraryIdToGames(libraryGamesMap); + }); }); - }, []) + + // TODO: see https://github.com/vaadin/hilla/issues/3470 + GameEndpoint.getMostRecentlyAddedGames(undefined).then(games => { + setRecentlyAddedGames(games); + }); + }, []); return ( -
-
-

Welcome to Gameyfin!

+
+

Welcome to Gameyfin!

+
+ + {libraries.map((library) => ( + + ))}
); diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt new file mode 100644 index 0000000..5afeb2a --- /dev/null +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt @@ -0,0 +1,27 @@ +package de.grimsi.gameyfin.games + +import com.vaadin.hilla.Endpoint +import de.grimsi.gameyfin.core.Role +import de.grimsi.gameyfin.games.dto.GameDto +import jakarta.annotation.security.PermitAll +import jakarta.annotation.security.RolesAllowed + +@Endpoint +@PermitAll +class GameEndpoint( + private val gameService: GameService +) { + + fun getMostRecentlyAddedGames(n: Int?): List { + return gameService.getMostRecentlyAdded(n ?: 10) + } + + fun getMostRecentlyUpdatedGames(n: Int?): List { + return gameService.getMostRecentlyUpdated(n ?: 10) + } + + @RolesAllowed(Role.Names.ADMIN) + fun removeGames() { + return gameService.deleteAll() + } +} \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt index 3824324..6ae511a 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.runBlocking import me.xdrop.fuzzywuzzy.FuzzySearch import org.apache.commons.io.FilenameUtils import org.pf4j.PluginManager +import org.springframework.data.domain.Limit import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -103,6 +104,16 @@ class GameService( gameRepository.deleteAll() } + fun getMostRecentlyAdded(count: Int): List { + return gameRepository.findByOrderByCreatedAtDesc(Limit.of(count)) + .map { toDto(it) } + } + + fun getMostRecentlyUpdated(count: Int): List { + return gameRepository.findByOrderByCreatedAtDesc(Limit.of(count)) + .map { toDto(it) } + } + private fun getById(id: Long): Game { return gameRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Game with id $id not found") } @@ -274,11 +285,15 @@ class GameService( fun toDto(game: Game): GameDto { val gameId = game.id ?: throw IllegalArgumentException("Game ID is null") + val createdAt = game.createdAt ?: throw IllegalArgumentException("Game creation timestamp is null") + val updatedAt = game.updatedAt ?: throw IllegalArgumentException("Game update timestamp is null") val gameLibraryId = game.library.id ?: throw IllegalArgumentException("Game library ID is null") val gameTitle = game.title ?: throw IllegalArgumentException("Game title is null") return GameDto( id = gameId, + createdAt = createdAt, + updatedAt = updatedAt, libraryId = gameLibraryId, title = gameTitle, coverId = game.coverImage?.id, diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt index 8c03036..df420ff 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt @@ -4,6 +4,8 @@ import java.time.Instant class GameDto( val id: Long, + val createdAt: Instant, + val updatedAt: Instant, val libraryId: Long, val title: String, val coverId: Long?, diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt index b474e18..9487907 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt @@ -7,6 +7,8 @@ import de.grimsi.gameyfin.pluginapi.gamemetadata.Genre import de.grimsi.gameyfin.pluginapi.gamemetadata.PlayerPerspective import de.grimsi.gameyfin.pluginapi.gamemetadata.Theme import jakarta.persistence.* +import org.hibernate.annotations.CreationTimestamp +import org.hibernate.annotations.UpdateTimestamp import java.net.URI import java.time.Instant @@ -16,6 +18,13 @@ class Game( @GeneratedValue(strategy = GenerationType.AUTO) var id: Long? = null, + @CreationTimestamp + @Column(updatable = false) + var createdAt: Instant? = null, + + @UpdateTimestamp + var updatedAt: Instant? = null, + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "library_id") val library: Library, diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/repositories/GameRepository.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/repositories/GameRepository.kt index e907ea8..9e2d111 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/repositories/GameRepository.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/repositories/GameRepository.kt @@ -1,9 +1,12 @@ package de.grimsi.gameyfin.games.repositories import de.grimsi.gameyfin.games.entities.Game +import org.springframework.data.domain.Limit import org.springframework.data.jpa.repository.JpaRepository interface GameRepository : JpaRepository { fun findByPath(path: String): Game? fun findAllByPathIn(paths: Collection): Collection + fun findByOrderByCreatedAtDesc(limit: Limit): List + fun findByOrderByUpdatedAtDesc(limit: Limit): List } \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt index d466f25..03ddeac 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt @@ -2,7 +2,6 @@ package de.grimsi.gameyfin.libraries import com.vaadin.hilla.Endpoint import de.grimsi.gameyfin.core.Role -import de.grimsi.gameyfin.games.GameService import de.grimsi.gameyfin.games.dto.GameDto import de.grimsi.gameyfin.libraries.dto.LibraryDto import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto @@ -13,8 +12,7 @@ import jakarta.annotation.security.RolesAllowed @Endpoint @PermitAll class LibraryEndpoint( - private val libraryService: LibraryService, - private val gameService: GameService + private val libraryService: LibraryService ) { fun getAllLibraries(): Collection { return libraryService.getAllLibraries() @@ -48,9 +46,4 @@ class LibraryEndpoint( fun removeLibraries() { return libraryService.deleteAllLibraries() } - - @RolesAllowed(Role.Names.ADMIN) - fun removeGames() { - return gameService.deleteAll() - } } \ No newline at end of file