mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +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 GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||||
import {Card} from "@heroui/react";
|
import {Card} from "@heroui/react";
|
||||||
import {ArrowRight} from "@phosphor-icons/react";
|
import {ArrowRight} from "@phosphor-icons/react";
|
||||||
|
import {useNavigate} from "react-router";
|
||||||
|
|
||||||
interface CoverRowProps {
|
interface CoverRowProps {
|
||||||
games: GameDto[];
|
games: GameDto[];
|
||||||
@@ -17,6 +18,7 @@ const radius = "sm";
|
|||||||
|
|
||||||
export function CoverRow({games, title, onPressShowMore}: CoverRowProps) {
|
export function CoverRow({games, title, onPressShowMore}: CoverRowProps) {
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [visibleCount, setVisibleCount] = useState(games.length);
|
const [visibleCount, setVisibleCount] = useState(games.length);
|
||||||
|
|
||||||
@@ -47,7 +49,8 @@ export function CoverRow({games, title, onPressShowMore}: CoverRowProps) {
|
|||||||
<div className="w-full relative">
|
<div className="w-full relative">
|
||||||
<Card ref={containerRef} className="flex flex-row gap-4 bg-transparent" radius={radius}>
|
<Card ref={containerRef} className="flex flex-row gap-4 bg-transparent" radius={radius}>
|
||||||
{games.slice(0, visibleCount).map((game, index) => (
|
{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}/>
|
<GameCover game={game} radius={radius}/>
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="flex flex-col justify-between w-[353px]">
|
<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">
|
<div className="absolute right-0 top-0 flex flex-row">
|
||||||
<Tooltip content="Scan library" placement="bottom" color="foreground">
|
<Tooltip content="Scan library" placement="bottom" color="foreground">
|
||||||
<Button isIconOnly variant="light"
|
<Button isIconOnly variant="light" onPress={triggerScan}>
|
||||||
onPress={() => LibraryEndpoint.triggerScan(ScanType.QUICK, [library])}>
|
|
||||||
<MagnifyingGlass/>
|
<MagnifyingGlass/>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import EmailConfirmationView from "Frontend/views/EmailConfirmationView";
|
|||||||
import InvitationRegistrationView from "Frontend/views/InvitationRegistrationView";
|
import InvitationRegistrationView from "Frontend/views/InvitationRegistrationView";
|
||||||
import PluginManagement from "Frontend/components/administration/PluginManagement";
|
import PluginManagement from "Frontend/components/administration/PluginManagement";
|
||||||
import {SystemManagement} from "Frontend/components/administration/SystemManagement";
|
import {SystemManagement} from "Frontend/components/administration/SystemManagement";
|
||||||
|
import GameView from "Frontend/views/GameView";
|
||||||
|
|
||||||
export const routes = protectRoutes([
|
export const routes = protectRoutes([
|
||||||
{
|
{
|
||||||
@@ -32,6 +33,10 @@ export const routes = protectRoutes([
|
|||||||
{
|
{
|
||||||
index: true, element: <HomeView/>
|
index: true, element: <HomeView/>
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'game/:gameId',
|
||||||
|
element: <GameView/>
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
element: <ProfileView/>,
|
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
|
private val gameService: GameService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun getGame(id: Long): GameDto {
|
||||||
|
return gameService.getGame(id)
|
||||||
|
}
|
||||||
|
|
||||||
fun getMostRecentlyAddedGames(n: Int?): List<GameDto> {
|
fun getMostRecentlyAddedGames(n: Int?): List<GameDto> {
|
||||||
return gameService.getMostRecentlyAdded(n ?: 10)
|
return gameService.getMostRecentlyAdded(n ?: 10)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ class GameService(
|
|||||||
return gameRepository.saveAll(gamesToBePersisted)
|
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? {
|
fun matchFromFile(path: Path, library: Library): Game? {
|
||||||
val query = FilenameUtils.removeExtension(path.fileName.toString())
|
val query = FilenameUtils.removeExtension(path.fileName.toString())
|
||||||
|
|
||||||
@@ -88,7 +93,7 @@ class GameService(
|
|||||||
|
|
||||||
fun getAllGames(): Collection<GameDto> {
|
fun getAllGames(): Collection<GameDto> {
|
||||||
val entities = gameRepository.findAll()
|
val entities = gameRepository.findAll()
|
||||||
return entities.map { toDto(it) }
|
return entities.map { it.toDto() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(game: Game) {
|
fun delete(game: Game) {
|
||||||
@@ -101,12 +106,12 @@ class GameService(
|
|||||||
|
|
||||||
fun getMostRecentlyAdded(count: Int): List<GameDto> {
|
fun getMostRecentlyAdded(count: Int): List<GameDto> {
|
||||||
return gameRepository.findByOrderByCreatedAtDesc(Limit.of(count))
|
return gameRepository.findByOrderByCreatedAtDesc(Limit.of(count))
|
||||||
.map { toDto(it) }
|
.map { it.toDto() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMostRecentlyUpdated(count: Int): List<GameDto> {
|
fun getMostRecentlyUpdated(count: Int): List<GameDto> {
|
||||||
return gameRepository.findByOrderByCreatedAtDesc(Limit.of(count))
|
return gameRepository.findByOrderByCreatedAtDesc(Limit.of(count))
|
||||||
.map { toDto(it) }
|
.map { it.toDto() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getById(id: Long): Game {
|
private fun getById(id: Long): Game {
|
||||||
@@ -289,54 +294,57 @@ class GameService(
|
|||||||
return mergedGame
|
return mergedGame
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toDto(game: Game): GameDto {
|
private fun String.fuzzyMatchTitle(other: String, minRatio: Int = TITLE_MATCH_MIN_RATIO): Boolean {
|
||||||
val gameId = game.id ?: throw IllegalArgumentException("Game ID is null")
|
return FuzzySearch.ratio(this.normalizeGameTitle(), other.normalizeGameTitle()) > minRatio
|
||||||
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 toDto(metadata: Map<String, FieldMetadata>): Map<String, GameMetadataDto> {
|
fun String.normalizeGameTitle(): String = this.alphaNumeric().replaceRomanNumerals()
|
||||||
return metadata.mapValues { toDto(it.value) }
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun toDto(metadata: FieldMetadata): GameMetadataDto {
|
|
||||||
|
fun Game.toDto(): GameDto {
|
||||||
|
// Helper functions
|
||||||
|
fun toDto(metadata: FieldMetadata): GameMetadataDto {
|
||||||
return GameMetadataDto(
|
return GameMetadataDto(
|
||||||
source = metadata.source.pluginId,
|
source = metadata.source.pluginId,
|
||||||
lastUpdated = metadata.lastUpdated
|
lastUpdated = metadata.lastUpdated
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.fuzzyMatchTitle(other: String, minRatio: Int = TITLE_MATCH_MIN_RATIO): Boolean {
|
fun toDto(metadata: Map<String, FieldMetadata>): Map<String, GameMetadataDto> {
|
||||||
return FuzzySearch.ratio(this.normalizeGameTitle(), other.normalizeGameTitle()) > minRatio
|
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.GameService
|
||||||
import de.grimsi.gameyfin.games.dto.GameDto
|
import de.grimsi.gameyfin.games.dto.GameDto
|
||||||
import de.grimsi.gameyfin.games.entities.Game
|
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.LibraryDto
|
||||||
import de.grimsi.gameyfin.libraries.dto.LibraryStatsDto
|
import de.grimsi.gameyfin.libraries.dto.LibraryStatsDto
|
||||||
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
|
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
|
||||||
@@ -95,7 +96,7 @@ class LibraryService(
|
|||||||
val library = libraryRepository.findByIdOrNull(libraryId)
|
val library = libraryRepository.findByIdOrNull(libraryId)
|
||||||
?: throw IllegalArgumentException("Library with ID $libraryId not found")
|
?: 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
|
return games
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user