Implement realtime UI for libraries

This commit is contained in:
grimsi
2025-05-21 20:44:00 +02:00
parent 54e8c31f9e
commit ad2fee0c3f
10 changed files with 149 additions and 114 deletions
+3
View File
@@ -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,20 +1,19 @@
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
const gamePromises = libraries.map((library) =>
randomGamesFromLibrary(library).then((games) => [library.id, games] as [number, GameDto[]]) randomGamesFromLibrary(library).then((games) => [library.id, games] as [number, GameDto[]])
); );
@@ -25,7 +24,6 @@ export default function HomeView() {
}); });
setLibraryIdToGames(libraryGamesMap); setLibraryIdToGames(libraryGamesMap);
}); });
});
// TODO: see https://github.com/vaadin/hilla/issues/3470 // TODO: see https://github.com/vaadin/hilla/issues/3470
GameEndpoint.getMostRecentlyAddedGames(undefined).then(games => { GameEndpoint.getMostRecentlyAddedGames(undefined).then(games => {
@@ -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()
}