mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +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 client from "Frontend/generated/connect-client.default";
|
||||
import {ErrorHandlingMiddleware} from "Frontend/util/middleware";
|
||||
import {initializeLibraryState} from "Frontend/state/LibraryState";
|
||||
|
||||
export default function App() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
client.middlewares = [ErrorHandlingMiddleware];
|
||||
|
||||
initializeLibraryState();
|
||||
|
||||
return (
|
||||
<HeroUIProvider className="size-full" navigate={navigate} useHref={useHref}>
|
||||
<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 withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||
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 LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
||||
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) {
|
||||
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
|
||||
const libraryCreationModal = useDisclosure();
|
||||
const state = useSnapshot(libraryState);
|
||||
|
||||
useEffect(() => {
|
||||
LibraryEndpoint.getAllLibraries().then((response) => {
|
||||
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);
|
||||
});
|
||||
initializeLibraryState();
|
||||
}, []);
|
||||
|
||||
async function updateLibrary(library: LibraryUpdateDto) {
|
||||
let updatedLibrary = 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];
|
||||
});
|
||||
|
||||
await LibraryEndpoint.updateLibrary(library);
|
||||
addToast({
|
||||
title: "Library updated",
|
||||
description: `Library ${library.name} has been updated.`,
|
||||
@@ -51,9 +32,6 @@ function LibraryManagementLayout({getConfig, formik}: any) {
|
||||
|
||||
async function removeLibrary(library: LibraryDto) {
|
||||
await LibraryEndpoint.removeLibrary(library.id);
|
||||
setLibraries((prevLibraries) => {
|
||||
return prevLibraries.filter((l) => l.id !== library.id);
|
||||
});
|
||||
addToast({
|
||||
title: "Library removed",
|
||||
description: `Library ${library.name} has been removed.`,
|
||||
@@ -87,10 +65,11 @@ function LibraryManagementLayout({getConfig, formik}: any) {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Divider className="mb-4"/>
|
||||
{libraries.length > 0 ?
|
||||
{state.sorted.length > 0 ?
|
||||
// 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))]">
|
||||
{libraries.map((library) =>
|
||||
{state.sorted.map((library) =>
|
||||
// @ts-ignore
|
||||
<LibraryOverviewCard library={library} updateLibrary={updateLibrary}
|
||||
removeLibrary={removeLibrary} key={library.name}/>
|
||||
)}
|
||||
@@ -99,8 +78,8 @@ function LibraryManagementLayout({getConfig, formik}: any) {
|
||||
}
|
||||
|
||||
<LibraryCreationModal
|
||||
libraries={libraries}
|
||||
setLibraries={setLibraries}
|
||||
// @ts-ignore
|
||||
libraries={state.sorted}
|
||||
isOpen={libraryCreationModal.isOpen}
|
||||
onOpenChange={libraryCreationModal.onOpenChange}
|
||||
/>
|
||||
|
||||
@@ -20,16 +20,15 @@ import {
|
||||
TreasureChest,
|
||||
Trophy
|
||||
} 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 {randomGamesFromLibrary} from "Frontend/util/utils";
|
||||
import {useNavigate} from "react-router";
|
||||
|
||||
export function LibraryOverviewCard({library, updateLibrary, removeLibrary}: {
|
||||
library: LibraryDto,
|
||||
updateLibrary: (library: LibraryUpdateDto) => Promise<void>,
|
||||
removeLibrary: (library: LibraryDto) => Promise<void>
|
||||
}) {
|
||||
interface LibraryOverviewCardProps {
|
||||
library: LibraryDto;
|
||||
}
|
||||
|
||||
export function LibraryOverviewCard({library}: LibraryOverviewCardProps) {
|
||||
const MAX_COVER_COUNT = 5;
|
||||
const navigate = useNavigate();
|
||||
const [randomGames, setRandomGames] = useState<GameDto[]>([]);
|
||||
@@ -42,7 +41,6 @@ export function LibraryOverviewCard({library, updateLibrary, removeLibrary}: {
|
||||
|
||||
async function triggerScan() {
|
||||
await LibraryEndpoint.triggerScan(ScanType.QUICK, [library]);
|
||||
await updateLibrary({id: library.id});
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,14 +16,13 @@ interface LibraryCreationModalProps {
|
||||
|
||||
export default function LibraryCreationModal({
|
||||
libraries,
|
||||
setLibraries,
|
||||
isOpen,
|
||||
onOpenChange
|
||||
}: LibraryCreationModalProps) {
|
||||
|
||||
async function createLibrary(library: LibraryDto) {
|
||||
try {
|
||||
const newLibrary = await LibraryEndpoint.createLibrary(library as LibraryDto);
|
||||
setLibraries([...libraries, newLibrary]);
|
||||
await LibraryEndpoint.createLibrary(library as LibraryDto);
|
||||
} catch (e) {
|
||||
addToast({
|
||||
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 {GameEndpoint, LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||
import {GameEndpoint} from "Frontend/generated/endpoints";
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
import {randomGamesFromLibrary} from "Frontend/util/utils";
|
||||
import {CoverRow} from "Frontend/components/general/CoverRow";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import {libraryState} from "Frontend/state/LibraryState";
|
||||
|
||||
export default function HomeView() {
|
||||
const [recentlyAddedGames, setRecentlyAddedGames] = useState<GameDto[]>([]);
|
||||
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
|
||||
const [libraryIdToGames, setLibraryIdToGames] = useState<Map<number, GameDto[]>>(new Map());
|
||||
const state = useSnapshot(libraryState);
|
||||
|
||||
useEffect(() => {
|
||||
LibraryEndpoint.getAllLibraries().then(libraries => {
|
||||
setLibraries(libraries);
|
||||
const gamePromises = state.libraries.map((library) =>
|
||||
//@ts-ignore
|
||||
randomGamesFromLibrary(library).then((games) => [library.id, games] as [number, GameDto[]])
|
||||
);
|
||||
|
||||
const gamePromises = libraries.map((library) =>
|
||||
randomGamesFromLibrary(library).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);
|
||||
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
|
||||
@@ -38,7 +36,7 @@ export default function HomeView() {
|
||||
<div className="flex flex-col gap-2">
|
||||
<CoverRow title="Recently added" games={recentlyAddedGames}
|
||||
onPressShowMore={() => alert("show more of 'Recently added'")}/>
|
||||
{libraries.map((library) => (
|
||||
{state.libraries.map((library) => (
|
||||
<CoverRow key={library.id} title={library.name}
|
||||
games={libraryIdToGames.get(library.id) || []}
|
||||
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 React, {useEffect, useState} from "react";
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||
import React, {useEffect} from "react";
|
||||
import LibraryHeader from "Frontend/components/general/covers/LibraryHeader";
|
||||
import {Button, Tab, Tabs} from "@heroui/react";
|
||||
import {ArrowLeft} from "@phosphor-icons/react";
|
||||
import LibraryManagementDetails from "Frontend/components/general/library/LibraryManagementDetails";
|
||||
import LibraryManagementGames from "Frontend/components/general/library/LibraryManagementGames";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
||||
|
||||
|
||||
export default function LibraryManagementView() {
|
||||
const {libraryId} = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [library, setLibrary] = useState<LibraryDto>();
|
||||
const [games, setGames] = useState<GameDto[]>([]);
|
||||
const state = useSnapshot(libraryState);
|
||||
|
||||
useEffect(() => {
|
||||
if (!libraryId) return;
|
||||
LibraryEndpoint.getById(parseInt(libraryId)).then((library: LibraryDto) => {
|
||||
setLibrary(library);
|
||||
});
|
||||
LibraryEndpoint.getGamesInLibrary(parseInt(libraryId)).then((games) => {
|
||||
setGames(games);
|
||||
initializeLibraryState().then((state) => {
|
||||
if (!libraryId || !state.state[libraryId]) {
|
||||
navigate("/administration/libraries");
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
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">
|
||||
<Button isIconOnly variant="light" onPress={() => navigate("/administration/libraries")}>
|
||||
<ArrowLeft/>
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">Manage library</h1>
|
||||
</div>
|
||||
<LibraryHeader library={library} className="h-32"/>
|
||||
{/* @ts-ignore */}
|
||||
<LibraryHeader library={state.state[libraryId]} className="h-32"/>
|
||||
<Tabs color="primary" fullWidth>
|
||||
<Tab title="Details">
|
||||
<LibraryManagementDetails library={library}/>
|
||||
{/* @ts-ignore */}
|
||||
<LibraryManagementDetails library={state.state[libraryId]}/>
|
||||
</Tab>
|
||||
<Tab title="Games">
|
||||
<LibraryManagementGames library={library}/>
|
||||
{/* @ts-ignore */}
|
||||
<LibraryManagementGames library={state.state[libraryId]}/>
|
||||
</Tab>
|
||||
<Tab title="Unmatched paths">
|
||||
<p>Unmatched paths</p>
|
||||
|
||||
@@ -2,50 +2,37 @@ package de.grimsi.gameyfin.libraries
|
||||
|
||||
import com.vaadin.hilla.Endpoint
|
||||
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.LibraryEvent
|
||||
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
|
||||
import de.grimsi.gameyfin.libraries.enums.ScanType
|
||||
import jakarta.annotation.security.PermitAll
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
import reactor.core.publisher.Flux
|
||||
|
||||
@Endpoint
|
||||
@PermitAll
|
||||
class LibraryEndpoint(
|
||||
private val libraryService: LibraryService
|
||||
) {
|
||||
fun getAllLibraries(): Collection<LibraryDto> {
|
||||
return libraryService.getAllLibraries()
|
||||
fun subscribe(): Flux<LibraryEvent> {
|
||||
return libraryService.subscribe()
|
||||
}
|
||||
|
||||
fun getById(libraryId: Long): LibraryDto = libraryService.getById(libraryId)
|
||||
fun getAll() = libraryService.getAll()
|
||||
|
||||
fun getGamesInLibrary(libraryId: Long): Collection<GameDto> {
|
||||
return libraryService.getGamesInLibrary(libraryId)
|
||||
}
|
||||
fun getGamesInLibrary(libraryId: Long) = libraryService.getGamesInLibrary(libraryId)
|
||||
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
fun triggerScan(scanType: ScanType = ScanType.QUICK, libraries: Collection<LibraryDto>?) {
|
||||
return libraryService.triggerScan(scanType, libraries)
|
||||
}
|
||||
fun triggerScan(scanType: ScanType = ScanType.QUICK, libraries: Collection<LibraryDto>?) =
|
||||
libraryService.triggerScan(scanType, libraries)
|
||||
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
fun createLibrary(library: LibraryDto): LibraryDto {
|
||||
return libraryService.create(library)
|
||||
}
|
||||
fun createLibrary(library: LibraryDto) = libraryService.create(library)
|
||||
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
fun updateLibrary(library: LibraryUpdateDto): LibraryDto {
|
||||
return libraryService.update(library)
|
||||
}
|
||||
fun updateLibrary(library: LibraryUpdateDto) = libraryService.update(library)
|
||||
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
fun removeLibrary(libraryId: Long) {
|
||||
return libraryService.deleteLibrary(libraryId)
|
||||
}
|
||||
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
fun removeLibraries() {
|
||||
return libraryService.deleteAllLibraries()
|
||||
}
|
||||
fun removeLibrary(libraryId: Long) = libraryService.deleteLibrary(libraryId)
|
||||
}
|
||||
@@ -5,15 +5,14 @@ import de.grimsi.gameyfin.games.GameService
|
||||
import de.grimsi.gameyfin.games.dto.GameDto
|
||||
import de.grimsi.gameyfin.games.entities.Game
|
||||
import de.grimsi.gameyfin.games.toDto
|
||||
import de.grimsi.gameyfin.libraries.dto.DirectoryMappingDto
|
||||
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.dto.*
|
||||
import de.grimsi.gameyfin.libraries.enums.ScanType
|
||||
import de.grimsi.gameyfin.media.ImageService
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Sinks
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
@@ -33,15 +32,27 @@ class LibraryService(
|
||||
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.
|
||||
*
|
||||
* @param library: The library to create or update.
|
||||
* @return The created or updated LibraryDto object.
|
||||
*/
|
||||
fun create(library: LibraryDto): LibraryDto {
|
||||
fun create(library: LibraryDto) {
|
||||
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.
|
||||
* @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)
|
||||
?: throw IllegalArgumentException("Library with ID $libraryDto.id not found")
|
||||
|
||||
@@ -64,13 +75,14 @@ class LibraryService(
|
||||
}
|
||||
|
||||
val updatedLibrary = libraryRepository.save(existingLibrary)
|
||||
return toDto(updatedLibrary)
|
||||
val updatedLibraryDto = toDto(updatedLibrary)
|
||||
libraryEvents.tryEmitNext(LibraryEvent.Updated(updatedLibraryDto))
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all libraries from the repository.
|
||||
*/
|
||||
fun getAllLibraries(): Collection<LibraryDto> {
|
||||
fun getAll(): List<LibraryDto> {
|
||||
val entities = libraryRepository.findAll()
|
||||
return entities.map { toDto(it) }
|
||||
}
|
||||
@@ -95,13 +107,7 @@ class LibraryService(
|
||||
*/
|
||||
fun deleteLibrary(libraryId: Long) {
|
||||
libraryRepository.deleteById(libraryId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all libraries from the repository.
|
||||
*/
|
||||
fun deleteAllLibraries() {
|
||||
libraryRepository.deleteAll()
|
||||
libraryEvents.tryEmitNext(LibraryEvent.Deleted(libraryId))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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