mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Main page finally gets somewhat usable
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user