mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +00:00
Implement realtime UI for libraries
This commit is contained in:
@@ -9,12 +9,15 @@ import {AuthProvider} from "Frontend/util/auth";
|
|||||||
import {IconContext, X} from "@phosphor-icons/react";
|
import {IconContext, X} from "@phosphor-icons/react";
|
||||||
import client from "Frontend/generated/connect-client.default";
|
import client from "Frontend/generated/connect-client.default";
|
||||||
import {ErrorHandlingMiddleware} from "Frontend/util/middleware";
|
import {ErrorHandlingMiddleware} from "Frontend/util/middleware";
|
||||||
|
import {initializeLibraryState} from "Frontend/state/LibraryState";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
client.middlewares = [ErrorHandlingMiddleware];
|
client.middlewares = [ErrorHandlingMiddleware];
|
||||||
|
|
||||||
|
initializeLibraryState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeroUIProvider className="size-full" navigate={navigate} useHref={useHref}>
|
<HeroUIProvider className="size-full" navigate={navigate} useHref={useHref}>
|
||||||
<NextThemesProvider attribute="class" themes={themeNames()} defaultTheme="gameyfin-violet-dark">
|
<NextThemesProvider attribute="class" themes={themeNames()} defaultTheme="gameyfin-violet-dark">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect} from "react";
|
||||||
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||||
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||||
import Section from "Frontend/components/general/Section";
|
import Section from "Frontend/components/general/Section";
|
||||||
@@ -10,38 +10,19 @@ import {LibraryOverviewCard} from "Frontend/components/general/cards/LibraryOver
|
|||||||
import LibraryCreationModal from "Frontend/components/general/modals/LibraryCreationModal";
|
import LibraryCreationModal from "Frontend/components/general/modals/LibraryCreationModal";
|
||||||
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
||||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||||
|
import {useSnapshot} from "valtio/react";
|
||||||
|
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
||||||
|
|
||||||
function LibraryManagementLayout({getConfig, formik}: any) {
|
function LibraryManagementLayout({getConfig, formik}: any) {
|
||||||
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
|
|
||||||
const libraryCreationModal = useDisclosure();
|
const libraryCreationModal = useDisclosure();
|
||||||
|
const state = useSnapshot(libraryState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
LibraryEndpoint.getAllLibraries().then((response) => {
|
initializeLibraryState();
|
||||||
if (response === undefined) return;
|
|
||||||
let sortedLibraries: LibraryDto[] = response
|
|
||||||
.filter(l => !!l)
|
|
||||||
.sort((a: LibraryDto, b: LibraryDto) => {
|
|
||||||
if (a.name === undefined || b.name === undefined) return 0;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
setLibraries(sortedLibraries);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function updateLibrary(library: LibraryUpdateDto) {
|
async function updateLibrary(library: LibraryUpdateDto) {
|
||||||
let updatedLibrary = await LibraryEndpoint.updateLibrary(library);
|
await LibraryEndpoint.updateLibrary(library);
|
||||||
if (updatedLibrary === undefined) return;
|
|
||||||
|
|
||||||
setLibraries((prevLibraries) => {
|
|
||||||
const index = prevLibraries.findIndex((l) => l.id === updatedLibrary.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
const updatedLibraries = [...prevLibraries];
|
|
||||||
updatedLibraries[index] = updatedLibrary;
|
|
||||||
return updatedLibraries;
|
|
||||||
}
|
|
||||||
return [...prevLibraries, updatedLibrary];
|
|
||||||
});
|
|
||||||
|
|
||||||
addToast({
|
addToast({
|
||||||
title: "Library updated",
|
title: "Library updated",
|
||||||
description: `Library ${library.name} has been updated.`,
|
description: `Library ${library.name} has been updated.`,
|
||||||
@@ -51,9 +32,6 @@ function LibraryManagementLayout({getConfig, formik}: any) {
|
|||||||
|
|
||||||
async function removeLibrary(library: LibraryDto) {
|
async function removeLibrary(library: LibraryDto) {
|
||||||
await LibraryEndpoint.removeLibrary(library.id);
|
await LibraryEndpoint.removeLibrary(library.id);
|
||||||
setLibraries((prevLibraries) => {
|
|
||||||
return prevLibraries.filter((l) => l.id !== library.id);
|
|
||||||
});
|
|
||||||
addToast({
|
addToast({
|
||||||
title: "Library removed",
|
title: "Library removed",
|
||||||
description: `Library ${library.name} has been removed.`,
|
description: `Library ${library.name} has been removed.`,
|
||||||
@@ -87,10 +65,11 @@ function LibraryManagementLayout({getConfig, formik}: any) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Divider className="mb-4"/>
|
<Divider className="mb-4"/>
|
||||||
{libraries.length > 0 ?
|
{state.sorted.length > 0 ?
|
||||||
// Aspect ratio of cover = 12/17 -> 5 covers = 60/17 -> 353px * 100px
|
// Aspect ratio of cover = 12/17 -> 5 covers = 60/17 -> 353px * 100px
|
||||||
<div id="library-cards" className="grid gap-4 grid-cols-[repeat(auto-fill,minmax(353px,1fr))]">
|
<div id="library-cards" className="grid gap-4 grid-cols-[repeat(auto-fill,minmax(353px,1fr))]">
|
||||||
{libraries.map((library) =>
|
{state.sorted.map((library) =>
|
||||||
|
// @ts-ignore
|
||||||
<LibraryOverviewCard library={library} updateLibrary={updateLibrary}
|
<LibraryOverviewCard library={library} updateLibrary={updateLibrary}
|
||||||
removeLibrary={removeLibrary} key={library.name}/>
|
removeLibrary={removeLibrary} key={library.name}/>
|
||||||
)}
|
)}
|
||||||
@@ -99,8 +78,8 @@ function LibraryManagementLayout({getConfig, formik}: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<LibraryCreationModal
|
<LibraryCreationModal
|
||||||
libraries={libraries}
|
// @ts-ignore
|
||||||
setLibraries={setLibraries}
|
libraries={state.sorted}
|
||||||
isOpen={libraryCreationModal.isOpen}
|
isOpen={libraryCreationModal.isOpen}
|
||||||
onOpenChange={libraryCreationModal.onOpenChange}
|
onOpenChange={libraryCreationModal.onOpenChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,16 +20,15 @@ import {
|
|||||||
TreasureChest,
|
TreasureChest,
|
||||||
Trophy
|
Trophy
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
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";
|
import {randomGamesFromLibrary} from "Frontend/util/utils";
|
||||||
import {useNavigate} from "react-router";
|
import {useNavigate} from "react-router";
|
||||||
|
|
||||||
export function LibraryOverviewCard({library, updateLibrary, removeLibrary}: {
|
interface LibraryOverviewCardProps {
|
||||||
library: LibraryDto,
|
library: LibraryDto;
|
||||||
updateLibrary: (library: LibraryUpdateDto) => Promise<void>,
|
}
|
||||||
removeLibrary: (library: LibraryDto) => Promise<void>
|
|
||||||
}) {
|
export function LibraryOverviewCard({library}: LibraryOverviewCardProps) {
|
||||||
const MAX_COVER_COUNT = 5;
|
const MAX_COVER_COUNT = 5;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [randomGames, setRandomGames] = useState<GameDto[]>([]);
|
const [randomGames, setRandomGames] = useState<GameDto[]>([]);
|
||||||
@@ -42,7 +41,6 @@ export function LibraryOverviewCard({library, updateLibrary, removeLibrary}: {
|
|||||||
|
|
||||||
async function triggerScan() {
|
async function triggerScan() {
|
||||||
await LibraryEndpoint.triggerScan(ScanType.QUICK, [library]);
|
await LibraryEndpoint.triggerScan(ScanType.QUICK, [library]);
|
||||||
await updateLibrary({id: library.id});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ interface LibraryCreationModalProps {
|
|||||||
|
|
||||||
export default function LibraryCreationModal({
|
export default function LibraryCreationModal({
|
||||||
libraries,
|
libraries,
|
||||||
setLibraries,
|
|
||||||
isOpen,
|
isOpen,
|
||||||
onOpenChange
|
onOpenChange
|
||||||
}: LibraryCreationModalProps) {
|
}: LibraryCreationModalProps) {
|
||||||
|
|
||||||
async function createLibrary(library: LibraryDto) {
|
async function createLibrary(library: LibraryDto) {
|
||||||
try {
|
try {
|
||||||
const newLibrary = await LibraryEndpoint.createLibrary(library as LibraryDto);
|
await LibraryEndpoint.createLibrary(library as LibraryDto);
|
||||||
setLibraries([...libraries, newLibrary]);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast({
|
addToast({
|
||||||
title: "Error creating library",
|
title: "Error creating library",
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {Subscription} from "@vaadin/hilla-frontend";
|
||||||
|
import {proxy} from "valtio/index";
|
||||||
|
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||||
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||||
|
import LibraryEvent from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryEvent";
|
||||||
|
|
||||||
|
type LibraryState = {
|
||||||
|
subscription?: Subscription<LibraryEvent>;
|
||||||
|
isLoaded: boolean;
|
||||||
|
state: Record<string, LibraryDto>;
|
||||||
|
libraries: LibraryDto[];
|
||||||
|
sorted: LibraryDto[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const libraryState = proxy<LibraryState>({
|
||||||
|
get isLoaded() {
|
||||||
|
return this.subscription != null;
|
||||||
|
},
|
||||||
|
state: {},
|
||||||
|
get libraries() {
|
||||||
|
return Object.values<LibraryDto>(this.state);
|
||||||
|
},
|
||||||
|
get sorted() {
|
||||||
|
return Object.values<LibraryDto>(this.state).sort((a, b) => {
|
||||||
|
if (a.name === undefined || b.name === undefined) return 0;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Subscribe to and process state updates from backend **/
|
||||||
|
export async function initializeLibraryState() {
|
||||||
|
if (libraryState.isLoaded) return libraryState;
|
||||||
|
|
||||||
|
// Fetch initial library list
|
||||||
|
const initialEntries = await LibraryEndpoint.getAll();
|
||||||
|
initialEntries.forEach((library: LibraryDto) => {
|
||||||
|
libraryState.state[library.id] = library;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to real-time updates
|
||||||
|
libraryState.subscription = LibraryEndpoint.subscribe().onNext((libraryEvent) => {
|
||||||
|
switch (libraryEvent.type) {
|
||||||
|
case "created":
|
||||||
|
case "updated":
|
||||||
|
//@ts-ignore
|
||||||
|
libraryState.state[libraryEvent.library.id] = libraryEvent.library;
|
||||||
|
break;
|
||||||
|
case "deleted":
|
||||||
|
//@ts-ignore
|
||||||
|
delete libraryState.state[libraryEvent.libraryId];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return libraryState;
|
||||||
|
}
|
||||||
@@ -1,30 +1,28 @@
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {GameEndpoint, LibraryEndpoint} from "Frontend/generated/endpoints";
|
import {GameEndpoint} from "Frontend/generated/endpoints";
|
||||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
|
||||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||||
import {randomGamesFromLibrary} from "Frontend/util/utils";
|
import {randomGamesFromLibrary} from "Frontend/util/utils";
|
||||||
import {CoverRow} from "Frontend/components/general/CoverRow";
|
import {CoverRow} from "Frontend/components/general/CoverRow";
|
||||||
|
import {useSnapshot} from "valtio/react";
|
||||||
|
import {libraryState} from "Frontend/state/LibraryState";
|
||||||
|
|
||||||
export default function HomeView() {
|
export default function HomeView() {
|
||||||
const [recentlyAddedGames, setRecentlyAddedGames] = useState<GameDto[]>([]);
|
const [recentlyAddedGames, setRecentlyAddedGames] = useState<GameDto[]>([]);
|
||||||
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
|
|
||||||
const [libraryIdToGames, setLibraryIdToGames] = useState<Map<number, GameDto[]>>(new Map());
|
const [libraryIdToGames, setLibraryIdToGames] = useState<Map<number, GameDto[]>>(new Map());
|
||||||
|
const state = useSnapshot(libraryState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
LibraryEndpoint.getAllLibraries().then(libraries => {
|
const gamePromises = state.libraries.map((library) =>
|
||||||
setLibraries(libraries);
|
//@ts-ignore
|
||||||
|
randomGamesFromLibrary(library).then((games) => [library.id, games] as [number, GameDto[]])
|
||||||
|
);
|
||||||
|
|
||||||
const gamePromises = libraries.map((library) =>
|
Promise.all(gamePromises).then((results) => {
|
||||||
randomGamesFromLibrary(library).then((games) => [library.id, games] as [number, GameDto[]])
|
const libraryGamesMap = new Map<number, GameDto[]>();
|
||||||
);
|
results.forEach(([libraryId, games]) => {
|
||||||
|
libraryGamesMap.set(libraryId, games);
|
||||||
Promise.all(gamePromises).then((results) => {
|
|
||||||
const libraryGamesMap = new Map<number, GameDto[]>();
|
|
||||||
results.forEach(([libraryId, games]) => {
|
|
||||||
libraryGamesMap.set(libraryId, games);
|
|
||||||
});
|
|
||||||
setLibraryIdToGames(libraryGamesMap);
|
|
||||||
});
|
});
|
||||||
|
setLibraryIdToGames(libraryGamesMap);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: see https://github.com/vaadin/hilla/issues/3470
|
// TODO: see https://github.com/vaadin/hilla/issues/3470
|
||||||
@@ -38,7 +36,7 @@ export default function HomeView() {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<CoverRow title="Recently added" games={recentlyAddedGames}
|
<CoverRow title="Recently added" games={recentlyAddedGames}
|
||||||
onPressShowMore={() => alert("show more of 'Recently added'")}/>
|
onPressShowMore={() => alert("show more of 'Recently added'")}/>
|
||||||
{libraries.map((library) => (
|
{state.libraries.map((library) => (
|
||||||
<CoverRow key={library.id} title={library.name}
|
<CoverRow key={library.id} title={library.name}
|
||||||
games={libraryIdToGames.get(library.id) || []}
|
games={libraryIdToGames.get(library.id) || []}
|
||||||
onPressShowMore={() => alert(`show more of library '${library.name}'`)}
|
onPressShowMore={() => alert(`show more of library '${library.name}'`)}
|
||||||
|
|||||||
@@ -1,45 +1,44 @@
|
|||||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
|
||||||
import {useNavigate, useParams} from "react-router";
|
import {useNavigate, useParams} from "react-router";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect} from "react";
|
||||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
|
||||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
|
||||||
import LibraryHeader from "Frontend/components/general/covers/LibraryHeader";
|
import LibraryHeader from "Frontend/components/general/covers/LibraryHeader";
|
||||||
import {Button, Tab, Tabs} from "@heroui/react";
|
import {Button, Tab, Tabs} from "@heroui/react";
|
||||||
import {ArrowLeft} from "@phosphor-icons/react";
|
import {ArrowLeft} from "@phosphor-icons/react";
|
||||||
import LibraryManagementDetails from "Frontend/components/general/library/LibraryManagementDetails";
|
import LibraryManagementDetails from "Frontend/components/general/library/LibraryManagementDetails";
|
||||||
import LibraryManagementGames from "Frontend/components/general/library/LibraryManagementGames";
|
import LibraryManagementGames from "Frontend/components/general/library/LibraryManagementGames";
|
||||||
|
import {useSnapshot} from "valtio/react";
|
||||||
|
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
||||||
|
|
||||||
|
|
||||||
export default function LibraryManagementView() {
|
export default function LibraryManagementView() {
|
||||||
const {libraryId} = useParams();
|
const {libraryId} = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [library, setLibrary] = useState<LibraryDto>();
|
const state = useSnapshot(libraryState);
|
||||||
const [games, setGames] = useState<GameDto[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!libraryId) return;
|
initializeLibraryState().then((state) => {
|
||||||
LibraryEndpoint.getById(parseInt(libraryId)).then((library: LibraryDto) => {
|
if (!libraryId || !state.state[libraryId]) {
|
||||||
setLibrary(library);
|
navigate("/administration/libraries");
|
||||||
});
|
}
|
||||||
LibraryEndpoint.getGamesInLibrary(parseInt(libraryId)).then((games) => {
|
|
||||||
setGames(games);
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return library && <div className="flex flex-col gap-4">
|
return libraryId && state.state[libraryId] && <div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-row gap-4 items-center">
|
<div className="flex flex-row gap-4 items-center">
|
||||||
<Button isIconOnly variant="light" onPress={() => navigate("/administration/libraries")}>
|
<Button isIconOnly variant="light" onPress={() => navigate("/administration/libraries")}>
|
||||||
<ArrowLeft/>
|
<ArrowLeft/>
|
||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-2xl font-bold">Manage library</h1>
|
<h1 className="text-2xl font-bold">Manage library</h1>
|
||||||
</div>
|
</div>
|
||||||
<LibraryHeader library={library} className="h-32"/>
|
{/* @ts-ignore */}
|
||||||
|
<LibraryHeader library={state.state[libraryId]} className="h-32"/>
|
||||||
<Tabs color="primary" fullWidth>
|
<Tabs color="primary" fullWidth>
|
||||||
<Tab title="Details">
|
<Tab title="Details">
|
||||||
<LibraryManagementDetails library={library}/>
|
{/* @ts-ignore */}
|
||||||
|
<LibraryManagementDetails library={state.state[libraryId]}/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Games">
|
<Tab title="Games">
|
||||||
<LibraryManagementGames library={library}/>
|
{/* @ts-ignore */}
|
||||||
|
<LibraryManagementGames library={state.state[libraryId]}/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Unmatched paths">
|
<Tab title="Unmatched paths">
|
||||||
<p>Unmatched paths</p>
|
<p>Unmatched paths</p>
|
||||||
|
|||||||
@@ -2,50 +2,37 @@ 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.dto.GameDto
|
|
||||||
import de.grimsi.gameyfin.libraries.dto.LibraryDto
|
import de.grimsi.gameyfin.libraries.dto.LibraryDto
|
||||||
|
import de.grimsi.gameyfin.libraries.dto.LibraryEvent
|
||||||
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
|
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
|
||||||
import de.grimsi.gameyfin.libraries.enums.ScanType
|
import de.grimsi.gameyfin.libraries.enums.ScanType
|
||||||
import jakarta.annotation.security.PermitAll
|
import jakarta.annotation.security.PermitAll
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
@Endpoint
|
@Endpoint
|
||||||
@PermitAll
|
@PermitAll
|
||||||
class LibraryEndpoint(
|
class LibraryEndpoint(
|
||||||
private val libraryService: LibraryService
|
private val libraryService: LibraryService
|
||||||
) {
|
) {
|
||||||
fun getAllLibraries(): Collection<LibraryDto> {
|
fun subscribe(): Flux<LibraryEvent> {
|
||||||
return libraryService.getAllLibraries()
|
return libraryService.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getById(libraryId: Long): LibraryDto = libraryService.getById(libraryId)
|
fun getAll() = libraryService.getAll()
|
||||||
|
|
||||||
fun getGamesInLibrary(libraryId: Long): Collection<GameDto> {
|
fun getGamesInLibrary(libraryId: Long) = libraryService.getGamesInLibrary(libraryId)
|
||||||
return libraryService.getGamesInLibrary(libraryId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun triggerScan(scanType: ScanType = ScanType.QUICK, libraries: Collection<LibraryDto>?) {
|
fun triggerScan(scanType: ScanType = ScanType.QUICK, libraries: Collection<LibraryDto>?) =
|
||||||
return libraryService.triggerScan(scanType, libraries)
|
libraryService.triggerScan(scanType, libraries)
|
||||||
}
|
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun createLibrary(library: LibraryDto): LibraryDto {
|
fun createLibrary(library: LibraryDto) = libraryService.create(library)
|
||||||
return libraryService.create(library)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun updateLibrary(library: LibraryUpdateDto): LibraryDto {
|
fun updateLibrary(library: LibraryUpdateDto) = libraryService.update(library)
|
||||||
return libraryService.update(library)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun removeLibrary(libraryId: Long) {
|
fun removeLibrary(libraryId: Long) = libraryService.deleteLibrary(libraryId)
|
||||||
return libraryService.deleteLibrary(libraryId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
|
||||||
fun removeLibraries() {
|
|
||||||
return libraryService.deleteAllLibraries()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,15 +5,14 @@ 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.games.toDto
|
||||||
import de.grimsi.gameyfin.libraries.dto.DirectoryMappingDto
|
import de.grimsi.gameyfin.libraries.dto.*
|
||||||
import de.grimsi.gameyfin.libraries.dto.LibraryDto
|
|
||||||
import de.grimsi.gameyfin.libraries.dto.LibraryStatsDto
|
|
||||||
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
|
|
||||||
import de.grimsi.gameyfin.libraries.enums.ScanType
|
import de.grimsi.gameyfin.libraries.enums.ScanType
|
||||||
import de.grimsi.gameyfin.media.ImageService
|
import de.grimsi.gameyfin.media.ImageService
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Sinks
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -33,15 +32,27 @@ class LibraryService(
|
|||||||
private val executor = Executors.newVirtualThreadPerTaskExecutor()
|
private val executor = Executors.newVirtualThreadPerTaskExecutor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val libraryEvents = Sinks.many().multicast().onBackpressureBuffer<LibraryEvent>(1024, false)
|
||||||
|
|
||||||
|
fun subscribe(): Flux<LibraryEvent> {
|
||||||
|
log.debug { "New subscription for libraryUpdates" }
|
||||||
|
return libraryEvents.asFlux()
|
||||||
|
.doOnSubscribe { log.debug { "Subscriber added to libraryUpdates [${libraryEvents.currentSubscriberCount()}]" } }
|
||||||
|
.doFinally {
|
||||||
|
log.debug { "Subscriber removed from libraryUpdates with signal type $it [${libraryEvents.currentSubscriberCount()}]" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates or updates a library in the repository.
|
* Creates or updates a library in the repository.
|
||||||
*
|
*
|
||||||
* @param library: The library to create or update.
|
* @param library: The library to create or update.
|
||||||
* @return The created or updated LibraryDto object.
|
* @return The created or updated LibraryDto object.
|
||||||
*/
|
*/
|
||||||
fun create(library: LibraryDto): LibraryDto {
|
fun create(library: LibraryDto) {
|
||||||
val entity = libraryRepository.save(toEntity(library))
|
val entity = libraryRepository.save(toEntity(library))
|
||||||
return toDto(entity)
|
val libraryDto = toDto(entity)
|
||||||
|
libraryEvents.tryEmitNext(LibraryEvent.Created(libraryDto))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +62,7 @@ class LibraryService(
|
|||||||
* @return The updated LibraryDto.
|
* @return The updated LibraryDto.
|
||||||
* @throws IllegalArgumentException if the library ID is null or the library is not found.
|
* @throws IllegalArgumentException if the library ID is null or the library is not found.
|
||||||
*/
|
*/
|
||||||
fun update(libraryDto: LibraryUpdateDto): LibraryDto {
|
fun update(libraryDto: LibraryUpdateDto) {
|
||||||
val existingLibrary = libraryRepository.findByIdOrNull(libraryDto.id)
|
val existingLibrary = libraryRepository.findByIdOrNull(libraryDto.id)
|
||||||
?: throw IllegalArgumentException("Library with ID $libraryDto.id not found")
|
?: throw IllegalArgumentException("Library with ID $libraryDto.id not found")
|
||||||
|
|
||||||
@@ -64,13 +75,14 @@ class LibraryService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val updatedLibrary = libraryRepository.save(existingLibrary)
|
val updatedLibrary = libraryRepository.save(existingLibrary)
|
||||||
return toDto(updatedLibrary)
|
val updatedLibraryDto = toDto(updatedLibrary)
|
||||||
|
libraryEvents.tryEmitNext(LibraryEvent.Updated(updatedLibraryDto))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all libraries from the repository.
|
* Retrieves all libraries from the repository.
|
||||||
*/
|
*/
|
||||||
fun getAllLibraries(): Collection<LibraryDto> {
|
fun getAll(): List<LibraryDto> {
|
||||||
val entities = libraryRepository.findAll()
|
val entities = libraryRepository.findAll()
|
||||||
return entities.map { toDto(it) }
|
return entities.map { toDto(it) }
|
||||||
}
|
}
|
||||||
@@ -95,13 +107,7 @@ class LibraryService(
|
|||||||
*/
|
*/
|
||||||
fun deleteLibrary(libraryId: Long) {
|
fun deleteLibrary(libraryId: Long) {
|
||||||
libraryRepository.deleteById(libraryId)
|
libraryRepository.deleteById(libraryId)
|
||||||
}
|
libraryEvents.tryEmitNext(LibraryEvent.Deleted(libraryId))
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes all libraries from the repository.
|
|
||||||
*/
|
|
||||||
fun deleteAllLibraries() {
|
|
||||||
libraryRepository.deleteAll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.grimsi.gameyfin.libraries.dto
|
||||||
|
|
||||||
|
sealed class LibraryEvent {
|
||||||
|
abstract val type: String
|
||||||
|
|
||||||
|
data class Created(val library: LibraryDto, override val type: String = "created") : LibraryEvent()
|
||||||
|
data class Updated(val library: LibraryDto, override val type: String = "updated") : LibraryEvent()
|
||||||
|
data class Deleted(val libraryId: Long, override val type: String = "deleted") : LibraryEvent()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user