mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +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";
|
radius?: "none" | "sm" | "md" | "lg" | "full";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GameCover({game, size = 300, radius}: GameCoverProps) {
|
export function GameCover({game, size = 300, radius = "sm"}: GameCoverProps) {
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
alt={game.title}
|
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 React, {useEffect, useState} from "react";
|
||||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||||
import {GameCover} from "Frontend/components/general/GameCover";
|
import {GameCover} from "Frontend/components/general/GameCover";
|
||||||
import Rand from "rand-seed";
|
|
||||||
import {
|
import {
|
||||||
Alien,
|
Alien,
|
||||||
CastleTurret,
|
CastleTurret,
|
||||||
@@ -24,32 +23,21 @@ import {
|
|||||||
import LibraryDetailsModal from "Frontend/components/general/modals/LibraryDetailsModal";
|
import LibraryDetailsModal from "Frontend/components/general/modals/LibraryDetailsModal";
|
||||||
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
||||||
import ScanType from "Frontend/generated/de/grimsi/gameyfin/libraries/enums/ScanType";
|
import ScanType from "Frontend/generated/de/grimsi/gameyfin/libraries/enums/ScanType";
|
||||||
|
import {randomGamesFromLibrary} from "Frontend/util/utils";
|
||||||
|
|
||||||
export function LibraryOverviewCard({library, updateLibrary}: {
|
export function LibraryOverviewCard({library, updateLibrary}: {
|
||||||
library: LibraryDto,
|
library: LibraryDto,
|
||||||
updateLibrary: (library: LibraryUpdateDto) => void
|
updateLibrary: (library: LibraryUpdateDto) => void
|
||||||
}) {
|
}) {
|
||||||
const MAX_COVER_COUNT = 5;
|
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();
|
const libraryDetailsModal = useDisclosure();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
LibraryEndpoint.getGamesInLibrary(library.id).then(
|
randomGamesFromLibrary(library, MAX_COVER_COUNT).then((games) => {
|
||||||
(response) => {
|
setRandomGames(games);
|
||||||
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);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -73,9 +61,9 @@ export function LibraryOverviewCard({library, updateLibrary}: {
|
|||||||
<Lego size={28} className="absolute top-[30%] left-[20%] rotate-[30deg]"/>
|
<Lego size={28} className="absolute top-[30%] left-[20%] rotate-[30deg]"/>
|
||||||
<TreasureChest size={40} className="absolute top-[70%] left-[50%] rotate-[75deg]"/>
|
<TreasureChest size={40} className="absolute top-[70%] left-[50%] rotate-[75deg]"/>
|
||||||
</div>
|
</div>
|
||||||
{randomGamesFromLibrary.length > 0 &&
|
{randomGames.length > 0 &&
|
||||||
<div className="absolute flex flex-row">
|
<div className="absolute flex flex-row">
|
||||||
{randomGamesFromLibrary.map((game) => (
|
{randomGames.map((game) => (
|
||||||
<GameCover game={game} size={100} radius="none" key={game.coverId}/>
|
<GameCover game={game} size={100} radius="none" key={game.coverId}/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import {getCsrfToken} from "Frontend/util/auth";
|
import {getCsrfToken} from "Frontend/util/auth";
|
||||||
import moment from 'moment-timezone';
|
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) {
|
export function cssVar(variable: string) {
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue(`--${variable}`);
|
return getComputedStyle(document.documentElement).getPropertyValue(`--${variable}`);
|
||||||
@@ -76,3 +80,18 @@ export function timeUntil(instantString: string, timeZone: string = moment.tz.gu
|
|||||||
|
|
||||||
return "just now";
|
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 {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 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() {
|
export default function HomeView() {
|
||||||
|
const [recentlyAddedGames, setRecentlyAddedGames] = useState<GameDto[]>([]);
|
||||||
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
|
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
|
||||||
|
const [libraryIdToGames, setLibraryIdToGames] = useState<Map<number, GameDto[]>>(new Map());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
LibraryEndpoint.getAllLibraries().then(libraries => {
|
LibraryEndpoint.getAllLibraries().then(libraries => {
|
||||||
setLibraries(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 (
|
return (
|
||||||
<div className="grow justify-center mt-12">
|
<div className="w-full">
|
||||||
<div className="flex flex-col items-center gap-6">
|
<p className="text-center text-2xl font-extrabold">Welcome to Gameyfin!</p>
|
||||||
<p>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>
|
||||||
</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 me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
import org.apache.commons.io.FilenameUtils
|
import org.apache.commons.io.FilenameUtils
|
||||||
import org.pf4j.PluginManager
|
import org.pf4j.PluginManager
|
||||||
|
import org.springframework.data.domain.Limit
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
@@ -103,6 +104,16 @@ class GameService(
|
|||||||
gameRepository.deleteAll()
|
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 {
|
private fun getById(id: Long): Game {
|
||||||
return gameRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Game with id $id not found")
|
return gameRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Game with id $id not found")
|
||||||
}
|
}
|
||||||
@@ -274,11 +285,15 @@ class GameService(
|
|||||||
|
|
||||||
fun toDto(game: Game): GameDto {
|
fun toDto(game: Game): GameDto {
|
||||||
val gameId = game.id ?: throw IllegalArgumentException("Game ID is null")
|
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 gameLibraryId = game.library.id ?: throw IllegalArgumentException("Game library ID is null")
|
||||||
val gameTitle = game.title ?: throw IllegalArgumentException("Game title is null")
|
val gameTitle = game.title ?: throw IllegalArgumentException("Game title is null")
|
||||||
|
|
||||||
return GameDto(
|
return GameDto(
|
||||||
id = gameId,
|
id = gameId,
|
||||||
|
createdAt = createdAt,
|
||||||
|
updatedAt = updatedAt,
|
||||||
libraryId = gameLibraryId,
|
libraryId = gameLibraryId,
|
||||||
title = gameTitle,
|
title = gameTitle,
|
||||||
coverId = game.coverImage?.id,
|
coverId = game.coverImage?.id,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import java.time.Instant
|
|||||||
|
|
||||||
class GameDto(
|
class GameDto(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
val createdAt: Instant,
|
||||||
|
val updatedAt: Instant,
|
||||||
val libraryId: Long,
|
val libraryId: Long,
|
||||||
val title: String,
|
val title: String,
|
||||||
val coverId: Long?,
|
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.PlayerPerspective
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.Theme
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.Theme
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
|
import org.hibernate.annotations.CreationTimestamp
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@@ -16,6 +18,13 @@ class Game(
|
|||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(updatable = false)
|
||||||
|
var createdAt: Instant? = null,
|
||||||
|
|
||||||
|
@UpdateTimestamp
|
||||||
|
var updatedAt: Instant? = null,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "library_id")
|
@JoinColumn(name = "library_id")
|
||||||
val library: Library,
|
val library: Library,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package de.grimsi.gameyfin.games.repositories
|
package de.grimsi.gameyfin.games.repositories
|
||||||
|
|
||||||
import de.grimsi.gameyfin.games.entities.Game
|
import de.grimsi.gameyfin.games.entities.Game
|
||||||
|
import org.springframework.data.domain.Limit
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
interface GameRepository : JpaRepository<Game, Long> {
|
interface GameRepository : JpaRepository<Game, Long> {
|
||||||
fun findByPath(path: String): Game?
|
fun findByPath(path: String): Game?
|
||||||
fun findAllByPathIn(paths: Collection<String>): Collection<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 com.vaadin.hilla.Endpoint
|
||||||
import de.grimsi.gameyfin.core.Role
|
import de.grimsi.gameyfin.core.Role
|
||||||
import de.grimsi.gameyfin.games.GameService
|
|
||||||
import de.grimsi.gameyfin.games.dto.GameDto
|
import de.grimsi.gameyfin.games.dto.GameDto
|
||||||
import de.grimsi.gameyfin.libraries.dto.LibraryDto
|
import de.grimsi.gameyfin.libraries.dto.LibraryDto
|
||||||
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
|
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
|
||||||
@@ -13,8 +12,7 @@ import jakarta.annotation.security.RolesAllowed
|
|||||||
@Endpoint
|
@Endpoint
|
||||||
@PermitAll
|
@PermitAll
|
||||||
class LibraryEndpoint(
|
class LibraryEndpoint(
|
||||||
private val libraryService: LibraryService,
|
private val libraryService: LibraryService
|
||||||
private val gameService: GameService
|
|
||||||
) {
|
) {
|
||||||
fun getAllLibraries(): Collection<LibraryDto> {
|
fun getAllLibraries(): Collection<LibraryDto> {
|
||||||
return libraryService.getAllLibraries()
|
return libraryService.getAllLibraries()
|
||||||
@@ -48,9 +46,4 @@ class LibraryEndpoint(
|
|||||||
fun removeLibraries() {
|
fun removeLibraries() {
|
||||||
return libraryService.deleteAllLibraries()
|
return libraryService.deleteAllLibraries()
|
||||||
}
|
}
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
|
||||||
fun removeGames() {
|
|
||||||
return gameService.deleteAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user