mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
First WIP version of GameView
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user