Main page finally gets somewhat usable

This commit is contained in:
grimsi
2025-05-09 16:51:00 +02:00
parent ccd3ebf9e8
commit e45585227e
11 changed files with 145 additions and 33 deletions
@@ -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 (
<Image
alt={game.title}
@@ -0,0 +1,29 @@
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
import Section from "Frontend/components/general/Section";
import {GameCover} from "Frontend/components/general/GameCover";
import {Card} from "@heroui/react";
interface HorizontalGameListProps {
title: string;
games: GameDto[];
}
export function HorizontalGameList({title, games}: HorizontalGameListProps) {
return (
<div className="flex flex-col gap-2">
<Section title={title}/>
<div className="flex flex-row gap-4 overflow-x-auto">
{games.length > 0 ?
games.map((game) => (
<GameCover game={game}/>
))
: <Card className="h-[300px] aspect-[12/17]">
<div className="flex flex-col items-center justify-center h-full">
<p className="text-gray-500">No content</p>
</div>
</Card>
}
</div>
</div>
);
}
@@ -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<GameDto[]>([]);
const [randomGames, setRandomGames] = useState<GameDto[]>([]);
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}: {
<Lego size={28} className="absolute top-[30%] left-[20%] rotate-[30deg]"/>
<TreasureChest size={40} className="absolute top-[70%] left-[50%] rotate-[75deg]"/>
</div>
{randomGamesFromLibrary.length > 0 &&
{randomGames.length > 0 &&
<div className="absolute flex flex-row">
{randomGamesFromLibrary.map((game) => (
{randomGames.map((game) => (
<GameCover game={game} size={100} radius="none" key={game.coverId}/>
))}
</div>
+19
View File
@@ -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<GameDto[]> {
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);
}
+32 -5
View File
@@ -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<GameDto[]>([]);
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
const [libraryIdToGames, setLibraryIdToGames] = useState<Map<number, GameDto[]>>(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<number, GameDto[]>();
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 (
<div className="grow justify-center mt-12">
<div className="flex flex-col items-center gap-6">
<p>Welcome to Gameyfin!</p>
<div className="w-full">
<p className="text-center text-2xl font-extrabold">Welcome to Gameyfin!</p>
<div className="flex flex-col gap-2">
<HorizontalGameList title="Recently added" games={recentlyAddedGames}/>
{libraries.map((library) => (
<HorizontalGameList key={library.id} title={library.name}
games={libraryIdToGames.get(library.id) || []}/>
))}
</div>
</div>
);
@@ -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<GameDto> {
return gameService.getMostRecentlyAdded(n ?: 10)
}
fun getMostRecentlyUpdatedGames(n: Int?): List<GameDto> {
return gameService.getMostRecentlyUpdated(n ?: 10)
}
@RolesAllowed(Role.Names.ADMIN)
fun removeGames() {
return gameService.deleteAll()
}
}
@@ -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<GameDto> {
return gameRepository.findByOrderByCreatedAtDesc(Limit.of(count))
.map { toDto(it) }
}
fun getMostRecentlyUpdated(count: Int): List<GameDto> {
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,
@@ -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?,
@@ -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,
@@ -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<Game, Long> {
fun findByPath(path: String): Game?
fun findAllByPathIn(paths: Collection<String>): Collection<Game>
fun findByOrderByCreatedAtDesc(limit: Limit): List<Game>
fun findByOrderByUpdatedAtDesc(limit: Limit): List<Game>
}
@@ -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<LibraryDto> {
return libraryService.getAllLibraries()
@@ -48,9 +46,4 @@ class LibraryEndpoint(
fun removeLibraries() {
return libraryService.deleteAllLibraries()
}
@RolesAllowed(Role.Names.ADMIN)
fun removeGames() {
return gameService.deleteAll()
}
}