From cb0aaacfb382852b2f9b2b15fa39588f41d84229 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 12 May 2025 15:11:39 +0200 Subject: [PATCH] First WIP version of GameView --- .../frontend/components/general/CoverRow.tsx | 5 +- .../general/cards/LibraryOverviewCard.tsx | 8 +- gameyfin/src/main/frontend/routes.tsx | 5 + gameyfin/src/main/frontend/views/GameView.tsx | 43 +++++++++ .../de/grimsi/gameyfin/games/GameEndpoint.kt | 4 + .../de/grimsi/gameyfin/games/GameService.kt | 92 ++++++++++--------- .../gameyfin/libraries/LibraryService.kt | 3 +- 7 files changed, 114 insertions(+), 46 deletions(-) create mode 100644 gameyfin/src/main/frontend/views/GameView.tsx diff --git a/gameyfin/src/main/frontend/components/general/CoverRow.tsx b/gameyfin/src/main/frontend/components/general/CoverRow.tsx index 507cf9d..a5c969c 100644 --- a/gameyfin/src/main/frontend/components/general/CoverRow.tsx +++ b/gameyfin/src/main/frontend/components/general/CoverRow.tsx @@ -3,6 +3,7 @@ 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"; interface CoverRowProps { games: GameDto[]; @@ -17,6 +18,7 @@ const radius = "sm"; export function CoverRow({games, title, onPressShowMore}: CoverRowProps) { + const navigate = useNavigate(); const containerRef = useRef(null); const [visibleCount, setVisibleCount] = useState(games.length); @@ -47,7 +49,8 @@ export function CoverRow({games, title, onPressShowMore}: CoverRowProps) {
{games.slice(0, visibleCount).map((game, index) => ( -
+
navigate(`/game/${game.id}`)}>
))} diff --git a/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx b/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx index 049fe57..802ba4f 100644 --- a/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx +++ b/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx @@ -41,6 +41,11 @@ export function LibraryOverviewCard({library, updateLibrary, removeLibrary}: { }) }, []); + async function triggerScan() { + await LibraryEndpoint.triggerScan(ScanType.QUICK, [library]); + await updateLibrary({id: library.id}); + } + return ( <> @@ -75,8 +80,7 @@ export function LibraryOverviewCard({library, updateLibrary, removeLibrary}: {
- diff --git a/gameyfin/src/main/frontend/routes.tsx b/gameyfin/src/main/frontend/routes.tsx index 516f793..42c591f 100644 --- a/gameyfin/src/main/frontend/routes.tsx +++ b/gameyfin/src/main/frontend/routes.tsx @@ -19,6 +19,7 @@ import EmailConfirmationView from "Frontend/views/EmailConfirmationView"; import InvitationRegistrationView from "Frontend/views/InvitationRegistrationView"; import PluginManagement from "Frontend/components/administration/PluginManagement"; import {SystemManagement} from "Frontend/components/administration/SystemManagement"; +import GameView from "Frontend/views/GameView"; export const routes = protectRoutes([ { @@ -32,6 +33,10 @@ export const routes = protectRoutes([ { index: true, element: }, + { + path: 'game/:gameId', + element: + }, { path: 'settings', element: , diff --git a/gameyfin/src/main/frontend/views/GameView.tsx b/gameyfin/src/main/frontend/views/GameView.tsx new file mode 100644 index 0000000..736a660 --- /dev/null +++ b/gameyfin/src/main/frontend/views/GameView.tsx @@ -0,0 +1,43 @@ +import {useEffect, useState} from "react"; +import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; +import {GameEndpoint} from "Frontend/generated/endpoints"; +import {useParams} from "react-router"; +import {GameCover} from "Frontend/components/general/covers/GameCover"; + +export default function GameView() { + const {gameId} = useParams(); + + const [game, setGame] = useState(); + + useEffect(() => { + if (gameId) { + GameEndpoint.getGame(parseInt(gameId)).then((game) => setGame(game)); + } + }, [gameId]); + + return (game && ( +
+ {game.imageIds !== undefined && game.imageIds.length > 0 && +
+ Game screenshot +
+ } +
+
+
+ +
+
+

{game.title}

+

{game.release !== undefined ? new Date(game.release).getFullYear() : "unknown"}

+
+
+
+

Summary

+

{game.summary}

+
+
+
+ )); +} \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt index 5afeb2a..2d2205e 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt @@ -12,6 +12,10 @@ class GameEndpoint( private val gameService: GameService ) { + fun getGame(id: Long): GameDto { + return gameService.getGame(id) + } + fun getMostRecentlyAddedGames(n: Int?): List { return gameService.getMostRecentlyAdded(n ?: 10) } 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 2a7cc66..d457ed7 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt @@ -59,6 +59,11 @@ class GameService( return gameRepository.saveAll(gamesToBePersisted) } + fun getGame(id: Long): GameDto { + return gameRepository.findByIdOrNull(id)?.toDto() + ?: throw IllegalArgumentException("Game with id $id not found") + } + fun matchFromFile(path: Path, library: Library): Game? { val query = FilenameUtils.removeExtension(path.fileName.toString()) @@ -88,7 +93,7 @@ class GameService( fun getAllGames(): Collection { val entities = gameRepository.findAll() - return entities.map { toDto(it) } + return entities.map { it.toDto() } } fun delete(game: Game) { @@ -101,12 +106,12 @@ class GameService( fun getMostRecentlyAdded(count: Int): List { return gameRepository.findByOrderByCreatedAtDesc(Limit.of(count)) - .map { toDto(it) } + .map { it.toDto() } } fun getMostRecentlyUpdated(count: Int): List { return gameRepository.findByOrderByCreatedAtDesc(Limit.of(count)) - .map { toDto(it) } + .map { it.toDto() } } private fun getById(id: Long): Game { @@ -289,54 +294,57 @@ class GameService( return mergedGame } - 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, - comment = game.comment, - summary = game.summary, - release = game.release, - userRating = game.userRating, - criticRating = game.criticRating, - publishers = game.publishers.map { it.name }, - developers = game.developers.map { it.name }, - genres = game.genres.map { it.name }, - themes = game.themes.map { it.name }, - keywords = game.keywords.toList(), - features = game.features.map { it.name }, - perspectives = game.perspectives?.map { it.name }, - imageIds = game.images.mapNotNull { it.id }, - videoUrls = game.videoUrls.map { it.toString() }, - path = game.path, - metadata = toDto(game.metadata), - originalIds = game.originalIds.mapKeys { it.key.pluginId } - ) + private fun String.fuzzyMatchTitle(other: String, minRatio: Int = TITLE_MATCH_MIN_RATIO): Boolean { + return FuzzySearch.ratio(this.normalizeGameTitle(), other.normalizeGameTitle()) > minRatio } - private fun toDto(metadata: Map): Map { - return metadata.mapValues { toDto(it.value) } - } + fun String.normalizeGameTitle(): String = this.alphaNumeric().replaceRomanNumerals() +} - private fun toDto(metadata: FieldMetadata): GameMetadataDto { + +fun Game.toDto(): GameDto { + // Helper functions + fun toDto(metadata: FieldMetadata): GameMetadataDto { return GameMetadataDto( source = metadata.source.pluginId, lastUpdated = metadata.lastUpdated ) } - private fun String.fuzzyMatchTitle(other: String, minRatio: Int = TITLE_MATCH_MIN_RATIO): Boolean { - return FuzzySearch.ratio(this.normalizeGameTitle(), other.normalizeGameTitle()) > minRatio + fun toDto(metadata: Map): Map { + return metadata.mapValues { toDto(it.value) } } - fun String.normalizeGameTitle(): String = this.alphaNumeric().replaceRomanNumerals() + + val thisId = this.id ?: throw IllegalArgumentException("this ID is null") + val createdAt = this.createdAt ?: throw IllegalArgumentException("this creation timestamp is null") + val updatedAt = this.updatedAt ?: throw IllegalArgumentException("this update timestamp is null") + val thisLibraryId = this.library.id ?: throw IllegalArgumentException("this library ID is null") + val thisTitle = this.title ?: throw IllegalArgumentException("this title is null") + + return GameDto( + id = thisId, + createdAt = createdAt, + updatedAt = updatedAt, + libraryId = thisLibraryId, + title = thisTitle, + coverId = this.coverImage?.id, + comment = this.comment, + summary = this.summary, + release = this.release, + userRating = this.userRating, + criticRating = this.criticRating, + publishers = this.publishers.map { it.name }, + developers = this.developers.map { it.name }, + genres = this.genres.map { it.name }, + themes = this.themes.map { it.name }, + keywords = this.keywords.toList(), + features = this.features.map { it.name }, + perspectives = this.perspectives?.map { it.name }, + imageIds = this.images.mapNotNull { it.id }, + videoUrls = this.videoUrls.map { it.toString() }, + path = this.path, + metadata = toDto(this.metadata), + originalIds = this.originalIds.mapKeys { it.key.pluginId } + ) } \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt index fb7b5dd..3c05838 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt @@ -4,6 +4,7 @@ import de.grimsi.gameyfin.core.filesystem.FilesystemService import de.grimsi.gameyfin.games.GameService import de.grimsi.gameyfin.games.dto.GameDto import de.grimsi.gameyfin.games.entities.Game +import de.grimsi.gameyfin.games.toDto import de.grimsi.gameyfin.libraries.dto.LibraryDto import de.grimsi.gameyfin.libraries.dto.LibraryStatsDto import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto @@ -95,7 +96,7 @@ class LibraryService( val library = libraryRepository.findByIdOrNull(libraryId) ?: throw IllegalArgumentException("Library with ID $libraryId not found") - val games = library.games.map { gameService.toDto(it) } + val games = library.games.map { it.toDto() } return games }