First WIP version of GameView

This commit is contained in:
grimsi
2025-05-12 15:11:39 +02:00
parent 0f90278e9f
commit cb0aaacfb3
7 changed files with 114 additions and 46 deletions
@@ -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<HTMLDivElement>(null);
const [visibleCount, setVisibleCount] = useState(games.length);
@@ -47,7 +49,8 @@ export function CoverRow({games, title, onPressShowMore}: CoverRowProps) {
<div className="w-full relative">
<Card ref={containerRef} className="flex flex-row gap-4 bg-transparent" radius={radius}>
{games.slice(0, visibleCount).map((game, index) => (
<div className="flex-shrink-0" key={index}>
<div className="flex-shrink-0 cursor-pointer" key={index}
onClick={() => navigate(`/game/${game.id}`)}>
<GameCover game={game} radius={radius}/>
</div>
))}
@@ -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 (
<>
<Card className="flex flex-col justify-between w-[353px]">
@@ -75,8 +80,7 @@ export function LibraryOverviewCard({library, updateLibrary, removeLibrary}: {
<div className="absolute right-0 top-0 flex flex-row">
<Tooltip content="Scan library" placement="bottom" color="foreground">
<Button isIconOnly variant="light"
onPress={() => LibraryEndpoint.triggerScan(ScanType.QUICK, [library])}>
<Button isIconOnly variant="light" onPress={triggerScan}>
<MagnifyingGlass/>
</Button>
</Tooltip>
+5
View File
@@ -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: <HomeView/>
},
{
path: 'game/:gameId',
element: <GameView/>
},
{
path: 'settings',
element: <ProfileView/>,
@@ -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<GameDto>();
useEffect(() => {
if (gameId) {
GameEndpoint.getGame(parseInt(gameId)).then((game) => setGame(game));
}
}, [gameId]);
return (game && (
<div className="flex flex-col gap-4">
{game.imageIds !== undefined && game.imageIds.length > 0 &&
<div className="overflow-hidden rounded-lg">
<img className="w-full h-96 object-cover brightness-50 blur-sm scale-110" alt="Game screenshot"
src={`/images/screenshot/${game.imageIds[0]}`}/>
</div>
}
<div className="flex flex-col gap-4 mx-24">
<div className="flex flex-row gap-4">
<div className="mt-[-16.25rem]">
<GameCover game={game} size={320} radius="none"/>
</div>
<div className="flex flex-col gap-1">
<p className="font-semibold text-3xl">{game.title}</p>
<p className="text-foreground/60">{game.release !== undefined ? new Date(game.release).getFullYear() : "unknown"}</p>
</div>
</div>
<div className="flex flex-col gap-2">
<p className="text-foreground/60">Summary</p>
<p>{game.summary}</p>
</div>
</div>
</div>
));
}
@@ -12,6 +12,10 @@ class GameEndpoint(
private val gameService: GameService
) {
fun getGame(id: Long): GameDto {
return gameService.getGame(id)
}
fun getMostRecentlyAddedGames(n: Int?): List<GameDto> {
return gameService.getMostRecentlyAdded(n ?: 10)
}
@@ -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<GameDto> {
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<GameDto> {
return gameRepository.findByOrderByCreatedAtDesc(Limit.of(count))
.map { toDto(it) }
.map { it.toDto() }
}
fun getMostRecentlyUpdated(count: Int): List<GameDto> {
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<String, FieldMetadata>): Map<String, GameMetadataDto> {
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<String, FieldMetadata>): Map<String, GameMetadataDto> {
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 }
)
}
@@ -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
}