Initial commit
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { goto, pushState, replaceState } from '$app/navigation';
|
||||
import type { Book, DynamicFilterData, FilterOption, PaginatedResponse } from '$lib/schema';
|
||||
import { page } from '$app/state';
|
||||
import { BookOperationsState } from './bookOperations.svelte';
|
||||
|
||||
export class BookCollectionState {
|
||||
public sortOrder = $state<string>('');
|
||||
public orderBy = $state<string>('');
|
||||
public filters = $state<Record<string, string[]>>({});
|
||||
|
||||
readonly hasActiveSort = $derived(this.orderBy !== 'title' || this.sortOrder !== 'asc');
|
||||
readonly hasActiveFilters = $derived(Object.values(this.filters).some((arr) => arr.length > 0));
|
||||
|
||||
public books = $state<Book[]>([]);
|
||||
public moreBooks = $state(false);
|
||||
private currentBookPage = $state(1);
|
||||
public loading = $state(false);
|
||||
|
||||
public filterOptions: FilterOption[] = $state([]);
|
||||
|
||||
private ops: BookOperationsState;
|
||||
|
||||
readonly sortOptions = [
|
||||
{
|
||||
value: 'title',
|
||||
name: 'Title'
|
||||
},
|
||||
{
|
||||
value: 'created_at',
|
||||
name: 'Date Added'
|
||||
},
|
||||
{
|
||||
value: 'published_date',
|
||||
name: 'Date Published'
|
||||
},
|
||||
{
|
||||
value: 'pages',
|
||||
name: 'Pages'
|
||||
},
|
||||
{
|
||||
value: 'last_accessed',
|
||||
name: 'Last Accessed'
|
||||
}
|
||||
];
|
||||
|
||||
constructor(
|
||||
ops: BookOperationsState,
|
||||
books: PaginatedResponse<Book>,
|
||||
filterData: DynamicFilterData
|
||||
) {
|
||||
this.ops = ops;
|
||||
this.books = books.items;
|
||||
this.moreBooks = books.total > books.items.length;
|
||||
this.filterOptions = this.buildFilterOptions(filterData);
|
||||
|
||||
// Determine sort order and direction based on page data
|
||||
this.orderBy = page.url.searchParams.get('orderBy') || 'title';
|
||||
this.sortOrder = page.url.searchParams.get('sortOrder') || 'asc';
|
||||
|
||||
// Construct initial filters based on the page data
|
||||
this.filters = {
|
||||
authors: page.url.searchParams.getAll('authors'),
|
||||
publishers: page.url.searchParams.getAll('publishers'),
|
||||
progress: page.url.searchParams.getAll('progress'),
|
||||
tags: page.url.searchParams.getAll('tags'),
|
||||
shelves: page.url.searchParams.getAll('shelves')
|
||||
};
|
||||
}
|
||||
|
||||
updateSort(sortValue: string) {
|
||||
if (sortValue === this.orderBy) {
|
||||
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.orderBy = sortValue;
|
||||
}
|
||||
this.updateSearchParams();
|
||||
}
|
||||
|
||||
toggleFilter(filter: string, value: string) {
|
||||
if (!this.filters[filter]) {
|
||||
this.filters[filter] = [];
|
||||
}
|
||||
|
||||
const index = this.filters[filter].indexOf(value);
|
||||
if (index > -1) {
|
||||
this.filters[filter].splice(index, 1);
|
||||
} else {
|
||||
this.filters[filter].push(value);
|
||||
}
|
||||
this.updateSearchParams();
|
||||
}
|
||||
|
||||
updateSearchParams() {
|
||||
// Update URL
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
Object.entries(this.filters).forEach(([filter, values]) => {
|
||||
url.searchParams.delete(filter);
|
||||
values.forEach((val) => url.searchParams.append(filter, val.toString()));
|
||||
});
|
||||
|
||||
url.searchParams.set('orderBy', this.orderBy);
|
||||
url.searchParams.set('sortOrder', this.sortOrder);
|
||||
|
||||
// pushState(url.toString(), {})
|
||||
goto(url.toString());
|
||||
|
||||
this.loadNewBooks();
|
||||
}
|
||||
|
||||
async loadNewBooks() {
|
||||
this.loading = true;
|
||||
|
||||
const result = await this.ops.listBooks({
|
||||
currentPage: 1,
|
||||
pageSize: 50,
|
||||
sortOrder: this.sortOrder,
|
||||
orderBy: this.orderBy,
|
||||
...this.filters
|
||||
});
|
||||
|
||||
this.books = [...result.items];
|
||||
this.currentBookPage = 1;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async loadMoreBooks() {
|
||||
const result = await this.ops.listBooks({
|
||||
currentPage: this.currentBookPage + 1,
|
||||
pageSize: 50,
|
||||
sortOrder: this.sortOrder,
|
||||
orderBy: this.orderBy,
|
||||
...this.filters
|
||||
});
|
||||
|
||||
this.books = [...this.books, ...result.items];
|
||||
this.moreBooks = result.total > this.books.length;
|
||||
this.currentBookPage++;
|
||||
}
|
||||
|
||||
updateBooks(books: PaginatedResponse<Book>) {
|
||||
this.books = books.items;
|
||||
this.moreBooks = books.total > books.items.length;
|
||||
}
|
||||
|
||||
resetState(books: PaginatedResponse<Book>, filterData: DynamicFilterData) {
|
||||
this.books = books.items;
|
||||
this.moreBooks = books.total > books.items.length;
|
||||
this.filterOptions = this.buildFilterOptions(filterData);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Re-read sort and filter state from URL
|
||||
this.orderBy = urlParams.get('orderBy') || 'title';
|
||||
this.sortOrder = urlParams.get('sortOrder') || 'asc';
|
||||
|
||||
this.filters = {
|
||||
authors: urlParams.getAll('authors'),
|
||||
publishers: urlParams.getAll('publishers'),
|
||||
progress: urlParams.getAll('progress'),
|
||||
tags: urlParams.getAll('tags'),
|
||||
shelves: urlParams.getAll('shelves')
|
||||
};
|
||||
}
|
||||
|
||||
buildFilterOptions(data: DynamicFilterData): FilterOption[] {
|
||||
return [
|
||||
{
|
||||
value: 'shelves',
|
||||
name: 'Bookshelves',
|
||||
items: data.bookshelves.items
|
||||
},
|
||||
{
|
||||
value: 'authors',
|
||||
name: 'Authors',
|
||||
items: data.authors.items
|
||||
},
|
||||
{
|
||||
value: 'tags',
|
||||
name: 'Tags',
|
||||
items: data.tags.items
|
||||
},
|
||||
{
|
||||
value: 'publishers',
|
||||
name: 'Publishers',
|
||||
items: data.publishers.items
|
||||
},
|
||||
{
|
||||
value: 'progress',
|
||||
name: 'Progress',
|
||||
items: [
|
||||
{ id: 'read', name: 'Read' },
|
||||
{ id: 'in_progress', name: 'In Progress' },
|
||||
{ id: 'unread', name: 'Not Started' }
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
updateFilterOptions(
|
||||
filterOptions: { value: string; name: string; items: { name: string; id: number | string }[] }[]
|
||||
) {
|
||||
this.filterOptions = filterOptions;
|
||||
}
|
||||
|
||||
clearFilters() {
|
||||
Object.values(this.filters).forEach((val) => (val.length = 0));
|
||||
this.updateSearchParams();
|
||||
}
|
||||
|
||||
isFilterSelected(filter: string, value: string) {
|
||||
return this.filters[filter]?.includes(value) || false;
|
||||
}
|
||||
}
|
||||
|
||||
const BOOK_COLLECTION_KEY = Symbol('BOOK_COLLECTION');
|
||||
|
||||
export function setBookCollectionState(
|
||||
ops: BookOperationsState,
|
||||
books: PaginatedResponse<Book>,
|
||||
filterData: DynamicFilterData
|
||||
) {
|
||||
return setContext(BOOK_COLLECTION_KEY, new BookCollectionState(ops, books, filterData));
|
||||
}
|
||||
|
||||
export function getBookCollectionState() {
|
||||
return getContext<ReturnType<typeof setBookCollectionState>>(BOOK_COLLECTION_KEY);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
deleteBookFiles,
|
||||
deleteBooks,
|
||||
listBooks,
|
||||
updateBookProgress
|
||||
} from '$lib/api';
|
||||
import {
|
||||
type Book,
|
||||
type UpdateBookProgress,
|
||||
type BookQuery,
|
||||
type PaginatedResponse,
|
||||
} from '$lib/schema';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export class BookOperationsState {
|
||||
public libraryId = $state<string>('');
|
||||
|
||||
// Delete dialog related state
|
||||
deleteDialogOpen = $state(false);
|
||||
deleteDialogTitle = $state('Delete books?');
|
||||
deleteFn = $state((deleteFiles: boolean) => {});
|
||||
|
||||
// Edit dialog related state
|
||||
editDialogOpen = $state(false);
|
||||
bookToEdit = $state<Book>();
|
||||
|
||||
// Upload dialog related state
|
||||
uploadDialogOpen = $state(false);
|
||||
|
||||
constructor(libraryId: string | number) {
|
||||
this.libraryId = libraryId.toString();
|
||||
}
|
||||
|
||||
async listBooks(queryData: BookQuery): Promise<PaginatedResponse<Book>> {
|
||||
const cleanParams = Object.fromEntries(
|
||||
Object.entries(queryData).filter(([_, value]) => {
|
||||
// Remove empty values
|
||||
if (value === undefined || value === null) return false;
|
||||
if (Array.isArray(value) && value.length === 0) return false;
|
||||
if (value === '') return false;
|
||||
return true;
|
||||
})
|
||||
);
|
||||
|
||||
return await listBooks({
|
||||
// cacheTimestamp: this.cacheTimestamp,
|
||||
libraries: [this.libraryId],
|
||||
...cleanParams
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBooks(bookIds: string[] | number[], deleteFiles: boolean, reload: boolean = true) {
|
||||
try {
|
||||
await deleteBooks({
|
||||
book_ids: bookIds,
|
||||
delete_files: deleteFiles,
|
||||
library_id: this.libraryId
|
||||
});
|
||||
|
||||
if (bookIds.length > 1) toast.success(`Deleted ${bookIds.length} books!`);
|
||||
else toast.success(`Book deleted!`);
|
||||
|
||||
this.deleteDialogOpen = false;
|
||||
|
||||
// Reload the page after deleting a book to get fresh data
|
||||
if (reload) await this.reload();
|
||||
} catch (error) {
|
||||
toast.error('Failed to delete.');
|
||||
console.error('Failed to delete book(s): ', error);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteBookFiles(bookId: number, fileIds: number[], deleteFiles: boolean) {
|
||||
try {
|
||||
await deleteBookFiles({
|
||||
book_id: bookId,
|
||||
file_ids: fileIds,
|
||||
delete_files: deleteFiles
|
||||
});
|
||||
|
||||
await invalidate('app:books');
|
||||
|
||||
if (fileIds.length > 1) toast.success(`Deleted ${fileIds.length} files!`);
|
||||
else toast.success(`Deleted file!`);
|
||||
} catch (error) {
|
||||
toast.error('Failed to delete file(s)');
|
||||
console.error('Failed to delete book files: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateBookProgress(bookIds: string[] | number[], progressRecord: UpdateBookProgress) {
|
||||
await updateBookProgress({
|
||||
book_ids: bookIds,
|
||||
...progressRecord
|
||||
});
|
||||
}
|
||||
|
||||
async markBooksAsComplete(bookIds: string[] | number[]) {
|
||||
try {
|
||||
await this.updateBookProgress(bookIds, {
|
||||
progress: 1,
|
||||
completed: true
|
||||
});
|
||||
|
||||
if (bookIds.length > 1) toast.success('Book marked as complete!');
|
||||
else toast.success(`${bookIds.length} books marked as complete!`);
|
||||
|
||||
// Get fresh data
|
||||
await this.reload();
|
||||
} catch (error) {
|
||||
toast.error('Failed to mark books as complete.');
|
||||
console.error('Mark as complete operation failed: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
async markBooksAsIncomplete(bookIds: number[]) {
|
||||
try {
|
||||
await this.updateBookProgress(bookIds, {
|
||||
progress: 0,
|
||||
completed: false
|
||||
});
|
||||
|
||||
if (bookIds.length > 1) toast.success('Book progress reset!');
|
||||
else toast.success(`Reset progress for ${bookIds.length} books!`);
|
||||
|
||||
// get fresh data
|
||||
await this.reload();
|
||||
} catch (error) {
|
||||
toast.error('Failed to reset progress.');
|
||||
console.error('Mark as incomplete operation failed: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
async downloadBooks(bookIds: string[] | number[], filename?: string) {
|
||||
// Construct the download URL
|
||||
const downloadUrl = new URL(`/api/books/download`, page.url.origin);
|
||||
downloadUrl.searchParams.set('library_id', this.libraryId);
|
||||
bookIds.forEach((id) => {
|
||||
downloadUrl.searchParams.set('book_ids', id.toString());
|
||||
});
|
||||
|
||||
this.download(downloadUrl, filename || 'download.zip');
|
||||
}
|
||||
|
||||
async downloadBookFile(bookId: number, fileId: number, filename: string) {
|
||||
// Construct the download URL
|
||||
const downloadUrl = new URL(`/api/books/download/${bookId}/${fileId}`, page.url.origin);
|
||||
this.download(downloadUrl, filename);
|
||||
}
|
||||
|
||||
private download(downloadUrl: URL, filename: string) {
|
||||
// Create a download anchor element, click it, then remove it
|
||||
const link = document.createElement('a');
|
||||
link.download = filename;
|
||||
link.href = downloadUrl.href;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
private async reload() {
|
||||
await invalidate('app:books');
|
||||
}
|
||||
}
|
||||
|
||||
const BOOK_OPS_KEY = Symbol('BOOK_OPERATIONS');
|
||||
|
||||
export function setBookOperationsState(libraryId: string | number) {
|
||||
return setContext(BOOK_OPS_KEY, new BookOperationsState(libraryId));
|
||||
}
|
||||
|
||||
export function getBookOperationsState() {
|
||||
return getContext<ReturnType<typeof setBookOperationsState>>(BOOK_OPS_KEY);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Book } from '$lib/schema';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
export class BookSelectionState {
|
||||
private selectedBooks = new SvelteMap<string, Book>();
|
||||
readonly selectionModeActive = $derived(this.selectedBooks.size !== 0);
|
||||
|
||||
toggleSelection(book: Book) {
|
||||
const bookId = book.id.toString()
|
||||
if (this.selectedBooks.has(bookId))
|
||||
this.selectedBooks.delete(bookId);
|
||||
|
||||
else
|
||||
this.selectedBooks.set(bookId, book);
|
||||
}
|
||||
|
||||
isSelected(id: number | string): boolean {
|
||||
return this.selectedBooks.has(id.toString());
|
||||
}
|
||||
|
||||
getSelectedBooks(): Book[] {
|
||||
return Array.from(this.selectedBooks.values())
|
||||
}
|
||||
|
||||
getSelectedIds(): string[] {
|
||||
return Array.from(this.selectedBooks.keys())
|
||||
}
|
||||
|
||||
numSelected() {
|
||||
return this.selectedBooks.size;
|
||||
}
|
||||
|
||||
selectAll(books: Book[]) {
|
||||
books.forEach((book) => {
|
||||
const bookId = book.id.toString()
|
||||
if (!this.selectedBooks.has(bookId))
|
||||
this.selectedBooks.set(bookId, book)
|
||||
});
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.selectedBooks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const BOOK_SELECTION_KEY = Symbol('BOOK_SELECTION');
|
||||
|
||||
export function setBookSelectionState() {
|
||||
return setContext(BOOK_SELECTION_KEY, new BookSelectionState());
|
||||
}
|
||||
|
||||
export function getBookSelectionState() {
|
||||
return getContext<ReturnType<typeof setBookSelectionState>>(BOOK_SELECTION_KEY);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { addBooksToShelf, createBookshelf, listBookshelves, removeBooksFromShelf } from '$lib/api';
|
||||
import type { Book, Bookshelf } from '$lib/schema';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
export class BookshelfState {
|
||||
readonly libraryBookshelves = new SvelteMap<string, Bookshelf[]>();
|
||||
|
||||
getBookshelves(libraryId: string | number): Bookshelf[] | undefined {
|
||||
const id = libraryId.toString();
|
||||
|
||||
if (!this.libraryBookshelves.has(id)) {
|
||||
this.fetchBookshelves(id).then(shelves => {
|
||||
this.libraryBookshelves.set(id, shelves);
|
||||
});
|
||||
}
|
||||
|
||||
return this.libraryBookshelves.get(id);
|
||||
}
|
||||
|
||||
async fetchBookshelves(libraryId: string) {
|
||||
try {
|
||||
let paginatedBookshelves = await listBookshelves({
|
||||
libraries: [libraryId]
|
||||
});
|
||||
|
||||
return paginatedBookshelves.items;
|
||||
} catch (error) {
|
||||
console.error('Failed to retrieve bookshelves: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
async addBookshelf(name: string, libraryId?: string | number, booksToAdd?: string[] | number[]) {
|
||||
try {
|
||||
let bookshelf = await createBookshelf({
|
||||
title: name,
|
||||
library_id: libraryId,
|
||||
book_ids: booksToAdd
|
||||
})
|
||||
|
||||
if (bookshelf.library_id) {
|
||||
const currentShelves = this.libraryBookshelves.get(bookshelf.library_id.toString()) || [];
|
||||
this.libraryBookshelves.set(bookshelf.library_id.toString(), [...currentShelves, bookshelf]);
|
||||
}
|
||||
|
||||
invalidate('app:books')
|
||||
|
||||
if (booksToAdd?.length)
|
||||
toast.success(`Added ${booksToAdd.length} books to '${name}'`)
|
||||
else
|
||||
toast.success(`Created shelf '${name}'`)
|
||||
|
||||
return bookshelf
|
||||
|
||||
} catch (error) {
|
||||
toast.error(`Failed to create bookshelf '${name}'`)
|
||||
console.error(`Failed to create bookshelf: `, error)
|
||||
}
|
||||
}
|
||||
|
||||
async addBooksToShelf(shelfId: number | string, bookIds: number[]) {
|
||||
try {
|
||||
const shelf = await addBooksToShelf({
|
||||
shelf_id: shelfId,
|
||||
book_ids: bookIds
|
||||
});
|
||||
|
||||
this.updateShelf(shelf)
|
||||
|
||||
if (bookIds.length === 1) toast.success(`Added book to shelf!`);
|
||||
else toast.success(`Added ${bookIds.length} books to shelf!`);
|
||||
} catch (error) {
|
||||
console.error('Failed to add book(s) to shelf: ', error);
|
||||
toast.error('Failed to add book(s) to shelf.');
|
||||
}
|
||||
}
|
||||
|
||||
async removeBooksFromShelf(shelfId: number | string, bookIds: number[]) {
|
||||
try {
|
||||
const shelf = await removeBooksFromShelf({
|
||||
shelf_id: shelfId,
|
||||
book_ids: bookIds
|
||||
});
|
||||
|
||||
this.updateShelf(shelf)
|
||||
|
||||
if (bookIds.length === 1) toast.success(`Removed book from shelf.`);
|
||||
else toast.success(`Removed ${bookIds.length} books from shelf!`);
|
||||
} catch (error) {
|
||||
console.error('Failed to remove book(s) from shelf: ', error);
|
||||
toast.error('Failed to remove book(s) from shelf.');
|
||||
}
|
||||
}
|
||||
|
||||
async updateShelf(shelf: Bookshelf) {
|
||||
if (!shelf.library_id) return;
|
||||
const bookshelves = this.libraryBookshelves.get(shelf.library_id.toString())
|
||||
if (!bookshelves) return;
|
||||
|
||||
const index = bookshelves.findIndex(bookshelf => bookshelf.id === shelf.id)
|
||||
if (index !== -1) {
|
||||
this.libraryBookshelves.set(
|
||||
shelf.library_id.toString(),
|
||||
[...bookshelves.slice(0, index), shelf, ...bookshelves.slice(index + 1)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
deletedBooks(books: Book[]) {
|
||||
// Assume all books are in the same library
|
||||
const libraryId = books[0].library_id.toString()
|
||||
const bookshelves = this.libraryBookshelves.get(libraryId);
|
||||
if (!bookshelves) return;
|
||||
|
||||
// Create a Set of shelf IDs that need updating for efficient lookup
|
||||
const shelfIdsToUpdate = new Set<number>();
|
||||
books.forEach(book => {
|
||||
book.lists.forEach(shelf => {
|
||||
shelfIdsToUpdate.add(shelf.id);
|
||||
});
|
||||
});
|
||||
|
||||
const updatedBookshelves = bookshelves.map(shelf => {
|
||||
if (shelfIdsToUpdate.has(shelf.id)) {
|
||||
// Count how many times this shelf appears across all deleted books
|
||||
let decrementBy = 0;
|
||||
books.forEach(book => {
|
||||
if (book.lists.some(s => s.id === shelf.id)) {
|
||||
decrementBy++;
|
||||
}
|
||||
});
|
||||
return { ...shelf, total: shelf.total - decrementBy };
|
||||
}
|
||||
return shelf;
|
||||
});
|
||||
|
||||
this.libraryBookshelves.set(libraryId, updatedBookshelves);
|
||||
}
|
||||
}
|
||||
|
||||
const BOOKSHELF_KEY = Symbol('BOOKSHELF');
|
||||
|
||||
export function setBookshelfState() {
|
||||
return setContext(BOOKSHELF_KEY, new BookshelfState());
|
||||
}
|
||||
|
||||
export function getBookshelfState() {
|
||||
return getContext<ReturnType<typeof setBookshelfState>>(BOOKSHELF_KEY);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { deleteLibrary } from '$lib/api';
|
||||
import type { Library } from '$lib/schema';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export class LibraryState {
|
||||
libraries = $state<Library[]>([]);
|
||||
activeLibrary = $state<Library | undefined>(undefined);
|
||||
createDialogOpen = $state(false);
|
||||
|
||||
constructor(libraries: Library[]) {
|
||||
this.libraries = libraries;
|
||||
|
||||
const activeLibrary = this.libraries.find((lib) => lib.id.toString() === page.params.libraryId);
|
||||
|
||||
let previousLibrary: Library | undefined;
|
||||
|
||||
if (browser) {
|
||||
previousLibrary = this.libraries.find(
|
||||
(lib) => lib.id.toString() === localStorage.getItem('previousLibraryId')
|
||||
);
|
||||
}
|
||||
|
||||
this.activeLibrary = activeLibrary || previousLibrary || libraries[0];
|
||||
}
|
||||
|
||||
async setActive(libraryId: number) {
|
||||
this.activeLibrary = this.libraries.find((lib) => lib.id === libraryId) || this.libraries[0];
|
||||
if (browser) {
|
||||
localStorage.setItem('previousLibraryId', this.activeLibrary.id.toString());
|
||||
await goto(`/library/${libraryId}/view`, { invalidate: ['app:libraries'] });
|
||||
}
|
||||
}
|
||||
|
||||
updateLibraries(libraries: Library[]) {
|
||||
this.libraries = libraries;
|
||||
|
||||
// Update the current library as it may have been removed
|
||||
if (!this.libraries.find((lib) => this.activeLibrary?.id === lib.id))
|
||||
this.activeLibrary = libraries[0] || undefined;
|
||||
}
|
||||
|
||||
async deleteLibrary(libraryId: number) {
|
||||
// Store library with its original index
|
||||
const libIndex = this.libraries.findIndex((lib) => lib.id === libraryId);
|
||||
const deletionSnapshot = { library: this.libraries[libIndex], index: libIndex };
|
||||
|
||||
// Optimistically delete the library
|
||||
this.libraries = this.libraries.filter((lib) => lib.id !== libraryId);
|
||||
|
||||
try {
|
||||
// Delete library via API
|
||||
await deleteLibrary(libraryId);
|
||||
toast.success(`Deleted library '${deletionSnapshot.library}'`);
|
||||
} catch (err) {
|
||||
// Delete failed; restore library at its original position
|
||||
this.libraries.splice(deletionSnapshot.index, 0, deletionSnapshot.library);
|
||||
toast.error('Failed to delete library');
|
||||
console.error('Failed to delete library: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
addLibrary(library: Library) {
|
||||
this.libraries.push(library);
|
||||
}
|
||||
|
||||
openLibraryCreateDialog() {
|
||||
this.createDialogOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
const LIBRARY_KEY = Symbol('LIBRARY');
|
||||
|
||||
export function setLibraryState(libraries: Library[]) {
|
||||
return setContext(LIBRARY_KEY, new LibraryState(libraries));
|
||||
}
|
||||
|
||||
export function getLibraryState() {
|
||||
return getContext<ReturnType<typeof setLibraryState>>(LIBRARY_KEY);
|
||||
}
|
||||
Reference in New Issue
Block a user