mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Add bulk lock/unlock metadata functionality for books
This commit is contained in:
committed by
Aditya Chandel
parent
5c50a157cd
commit
941206bdf3
@@ -4,6 +4,7 @@ import com.adityachandel.booklore.mapper.BookMetadataMapper;
|
||||
import com.adityachandel.booklore.model.dto.BookMetadata;
|
||||
import com.adityachandel.booklore.model.dto.request.FieldLockRequest;
|
||||
import com.adityachandel.booklore.model.dto.request.MetadataRefreshRequest;
|
||||
import com.adityachandel.booklore.model.dto.request.ToggleAllLockRequest;
|
||||
import com.adityachandel.booklore.quartz.JobSchedulerService;
|
||||
import com.adityachandel.booklore.service.BookMetadataService;
|
||||
import com.adityachandel.booklore.service.BookMetadataUpdater;
|
||||
@@ -57,7 +58,7 @@ public class MetadataController {
|
||||
return ResponseEntity.ok(updatedMetadata);
|
||||
}
|
||||
|
||||
@PutMapping("/{bookId}/metadata/lock")
|
||||
@PutMapping("/{bookId}/metadata/toggle-field-lock")
|
||||
@PreAuthorize("@securityUtil.canEditMetadata() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<BookMetadata> updateFieldLockState(@RequestBody FieldLockRequest request) {
|
||||
long bookId = request.getBookId();
|
||||
@@ -66,6 +67,12 @@ public class MetadataController {
|
||||
return ResponseEntity.ok(bookMetadataService.updateFieldLockState(bookId, field, isLocked));
|
||||
}
|
||||
|
||||
@PutMapping("/metadata/toggle-all-lock")
|
||||
@PreAuthorize("@securityUtil.canEditMetadata() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<List<BookMetadata>> toggleAllMetadata(@RequestBody ToggleAllLockRequest request) {
|
||||
return ResponseEntity.ok(bookMetadataService.toggleAllLock(request));
|
||||
}
|
||||
|
||||
@PostMapping("/regenerate-covers")
|
||||
@PreAuthorize("@securityUtil.canEditMetadata() or @securityUtil.isAdmin()")
|
||||
public void regenerateCovers() {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.adityachandel.booklore.model.dto.request;
|
||||
|
||||
import com.adityachandel.booklore.model.enums.Lock;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
public class ToggleAllLockRequest {
|
||||
private Set<Long> bookIds;
|
||||
private Lock lock;
|
||||
}
|
||||
@@ -150,4 +150,27 @@ public class BookMetadataEntity {
|
||||
|
||||
@OneToMany(fetch = FetchType.LAZY, mappedBy = "book")
|
||||
private List<BookAwardEntity> awards;
|
||||
|
||||
|
||||
public void applyLockToAllFields(boolean lock) {
|
||||
this.allFieldsLocked = lock;
|
||||
this.titleLocked = lock;
|
||||
this.subtitleLocked = lock;
|
||||
this.publisherLocked = lock;
|
||||
this.publishedDateLocked = lock;
|
||||
this.descriptionLocked = lock;
|
||||
this.isbn13Locked = lock;
|
||||
this.isbn10Locked = lock;
|
||||
this.pageCountLocked = lock;
|
||||
this.thumbnailLocked = lock;
|
||||
this.languageLocked = lock;
|
||||
this.ratingLocked = lock;
|
||||
this.reviewCountLocked = lock;
|
||||
this.coverLocked = lock;
|
||||
this.seriesNameLocked = lock;
|
||||
this.seriesNumberLocked = lock;
|
||||
this.seriesTotalLocked = lock;
|
||||
this.authorsLocked = lock;
|
||||
this.categoriesLocked = lock;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.adityachandel.booklore.model.enums;
|
||||
|
||||
public enum Lock {
|
||||
LOCK, UNLOCK
|
||||
}
|
||||
@@ -7,10 +7,12 @@ import com.adityachandel.booklore.model.dto.Book;
|
||||
import com.adityachandel.booklore.model.dto.BookMetadata;
|
||||
import com.adityachandel.booklore.model.dto.request.MetadataRefreshOptions;
|
||||
import com.adityachandel.booklore.model.dto.request.MetadataRefreshRequest;
|
||||
import com.adityachandel.booklore.model.dto.request.ToggleAllLockRequest;
|
||||
import com.adityachandel.booklore.model.dto.settings.AppSettings;
|
||||
import com.adityachandel.booklore.model.entity.BookEntity;
|
||||
import com.adityachandel.booklore.model.entity.BookMetadataEntity;
|
||||
import com.adityachandel.booklore.model.entity.LibraryEntity;
|
||||
import com.adityachandel.booklore.model.enums.Lock;
|
||||
import com.adityachandel.booklore.model.websocket.Topic;
|
||||
import com.adityachandel.booklore.repository.BookMetadataRepository;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
@@ -441,4 +443,15 @@ public class BookMetadataService {
|
||||
}
|
||||
log.info("{} Successfully regenerated cover for book ID {} ({})", progress, book.getId(), title);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<BookMetadata> toggleAllLock(ToggleAllLockRequest request) {
|
||||
boolean lock = request.getLock() == Lock.LOCK;
|
||||
List<BookEntity> books = bookRepository.findAllByIdIn(request.getBookIds())
|
||||
.stream()
|
||||
.peek(book -> book.getMetadata().applyLockToAllFields(lock))
|
||||
.toList();
|
||||
bookRepository.saveAll(books);
|
||||
return books.stream().map(b ->bookMetadataMapper.toBookMetadata(b.getMetadata(), false)).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -106,8 +106,7 @@
|
||||
</div>
|
||||
|
||||
<div class="book-card-container" *ngIf="bookState?.books as books">
|
||||
<app-book-table *ngIf="gridOrTable === 'table' && books.length > 0" [books]="books" (selectedBooksChange)="onSelectedBooksChange($event)"
|
||||
[sortOption]="selectedSort"></app-book-table>
|
||||
<app-book-table *ngIf="gridOrTable === 'table' && books.length > 0" [books]="books" (selectedBooksChange)="onSelectedBooksChange($event)" [sortOption]="selectedSort"></app-book-table>
|
||||
|
||||
<virtual-scroller *ngIf="gridOrTable === 'grid'" class="virtual-scroller" #scroll [items]="books">
|
||||
<div class="grid grid-cols-12 gap-4" #container>
|
||||
@@ -126,27 +125,50 @@
|
||||
<div class="book-browser-footer bg-[var(--card-background)] bg-opacity-10" *ngIf="selectedBooks.size > 0" [@slideInOut]>
|
||||
<div class="flex justify-between gap-8">
|
||||
<ng-container *ngIf="entityType$ | async as entityType">
|
||||
<p-button *ngIf="entityType === EntityType.LIBRARY || entityType === EntityType.ALL_BOOKS"
|
||||
label="Assign Shelf"
|
||||
<p-button
|
||||
*ngIf="entityType === EntityType.LIBRARY || entityType === EntityType.ALL_BOOKS"
|
||||
icon="pi pi-bookmark-fill"
|
||||
outlined="true"
|
||||
severity="info"
|
||||
(onClick)="openShelfAssigner()">
|
||||
(onClick)="openShelfAssigner()"
|
||||
pTooltip="Assign to shelf"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
<p-button *ngIf="entityType === EntityType.SHELF"
|
||||
label="Unshelf"
|
||||
<p-button
|
||||
*ngIf="entityType === EntityType.SHELF"
|
||||
icon="pi pi-bookmark"
|
||||
outlined="true"
|
||||
severity="info"
|
||||
(click)="unshelfBooks()">
|
||||
(click)="unshelfBooks()"
|
||||
pTooltip="Remove from shelf"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
<div *ngIf="userService.userData$ | async as userData">
|
||||
<p-button *ngIf="userData.permissions.canEditMetadata"
|
||||
label="Refresh Metadata"
|
||||
<p-button
|
||||
*ngIf="userData.permissions.canEditMetadata"
|
||||
icon="pi pi-database"
|
||||
outlined="true"
|
||||
severity="info"
|
||||
(click)="updateMetadata()">
|
||||
(click)="updateMetadata()"
|
||||
pTooltip="Update metadata"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
</div>
|
||||
<p-button
|
||||
label="Deselect All"
|
||||
outlined="true"
|
||||
icon="pi pi-lock"
|
||||
severity="info"
|
||||
(click)="lockUnlockMetadata()"
|
||||
pTooltip="Lock/Unlock metadata"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
<p-button
|
||||
outlined="true"
|
||||
icon="pi pi-times"
|
||||
severity="warn"
|
||||
(click)="deselectAllBooks()">
|
||||
(click)="deselectAllBooks()"
|
||||
pTooltip="Deselect all books"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
.book-browser-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20%;
|
||||
right: 20%;
|
||||
left: 30%;
|
||||
right: 30%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem 0.5rem 0.5rem;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {AfterViewInit, Component, inject, OnInit, ViewChild} from '@angular/core';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {MenuItem, MessageService, PrimeTemplate} from 'primeng/api';
|
||||
import {ConfirmationService, MenuItem, MessageService, PrimeTemplate} from 'primeng/api';
|
||||
import {LibraryService} from '../../service/library.service';
|
||||
import {BookService} from '../../service/book.service';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
import {BehaviorSubject, combineLatest, Observable, of, Subject} from 'rxjs';
|
||||
import {BehaviorSubject, Observable, of, Subject} from 'rxjs';
|
||||
import {ShelfService} from '../../service/shelf.service';
|
||||
import {ShelfAssignerComponent} from '../shelf-assigner/shelf-assigner.component';
|
||||
import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
@@ -31,6 +31,7 @@ import {BookFilterComponent} from './book-filter/book-filter.component';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
import {Fluid} from 'primeng/fluid';
|
||||
import {UserService} from '../../../user.service';
|
||||
import {LockUnlockMetadataDialogComponent} from './lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component';
|
||||
|
||||
export enum EntityType {
|
||||
LIBRARY = 'Library',
|
||||
@@ -92,6 +93,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
|
||||
private dialogService = inject(DialogService);
|
||||
private sortService = inject(SortService);
|
||||
private libraryShelfMenuService = inject(LibraryShelfMenuService);
|
||||
private confirmationService = inject(ConfirmationService);
|
||||
|
||||
protected resetFilterSubject = new Subject<void>();
|
||||
|
||||
@@ -483,4 +485,18 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
|
||||
this.selectedFilter.next(item);
|
||||
});
|
||||
}
|
||||
|
||||
lockUnlockMetadata() {
|
||||
this.dynamicDialogRef = this.dialogService.open(LockUnlockMetadataDialogComponent, {
|
||||
header: 'Toggle Metadata Lock',
|
||||
modal: true,
|
||||
closable: true,
|
||||
data: {
|
||||
bookIds: Array.from(this.selectedBooks)
|
||||
}
|
||||
});
|
||||
this.dynamicDialogRef.onClose.subscribe(() => {
|
||||
this.deselectAllBooks();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,17 @@
|
||||
</div>
|
||||
|
||||
<div class="book-info">
|
||||
<div class="book-title-container">
|
||||
<h4 class="book-title">{{ book.metadata?.title }}</h4>
|
||||
<div class="book-title-container flex items-center">
|
||||
<p class="m-0 pl-2">
|
||||
<i
|
||||
class="pi"
|
||||
[ngClass]="book.metadata?.allFieldsLocked ? 'pi-lock text-red-400' : 'pi-lock-open text-green-400'"
|
||||
[title]="book.metadata?.allFieldsLocked ? 'Locked' : 'Unlocked'"
|
||||
style="font-size: 0.75rem;">
|
||||
</i>
|
||||
</p>
|
||||
<h4 class="book-title m-0 pl-2">{{ book.metadata?.title }}</h4>
|
||||
|
||||
<p-tieredmenu #menu [model]="items" [popup]="true" appendTo="body"></p-tieredmenu>
|
||||
<p-button
|
||||
size="small"
|
||||
|
||||
@@ -80,7 +80,6 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.info-btn,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<th>
|
||||
<p-tableHeaderCheckbox/>
|
||||
</th>
|
||||
<th pResizableColumn></th>
|
||||
<th class="max-w-14 min-w-14"></th>
|
||||
<th pResizableColumn>Title</th>
|
||||
<th pResizableColumn>Authors</th>
|
||||
@@ -34,6 +35,17 @@
|
||||
<td class="max-w-16">
|
||||
<p-tableCheckbox [value]="book"></p-tableCheckbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-button
|
||||
[icon]="metadata.allFieldsLocked ? 'pi pi-lock' : 'pi pi-lock-open'"
|
||||
[severity]="metadata.allFieldsLocked ? 'danger' : 'success'"
|
||||
[text]="true"
|
||||
[title]="metadata.allFieldsLocked ? 'Locked' : 'Unlocked'"
|
||||
size="small"
|
||||
[style]="{ width: '1.5rem', height: '1.5rem', padding: '0', fontSize: '0.75rem' }"
|
||||
(click)="toggleMetadataLock(metadata)">
|
||||
</p-button>
|
||||
</td>
|
||||
<td (click)="openMetadataCenter(book.id)">
|
||||
<img [attr.src]="urlHelper.getCoverUrl(metadata.bookId, metadata?.coverUpdatedOn)" alt="Book Cover" class="size-7"/>
|
||||
</td>
|
||||
|
||||
@@ -3,10 +3,13 @@ import {TableModule} from 'primeng/table';
|
||||
import {NgIf} from '@angular/common';
|
||||
import {Rating} from 'primeng/rating';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Book} from '../../../model/book.model';
|
||||
import {Book, BookMetadata} from '../../../model/book.model';
|
||||
import {SortOption} from '../../../model/sort.model';
|
||||
import {MetadataDialogService} from '../../../../metadata/service/metadata-dialog.service';
|
||||
import {UrlHelperService} from '../../../../utilities/service/url-helper.service';
|
||||
import {Button} from 'primeng/button';
|
||||
import {BookService} from '../../../service/book.service';
|
||||
import {MessageService} from 'primeng/api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-book-table',
|
||||
@@ -16,7 +19,8 @@ import {UrlHelperService} from '../../../../utilities/service/url-helper.service
|
||||
TableModule,
|
||||
NgIf,
|
||||
Rating,
|
||||
FormsModule
|
||||
FormsModule,
|
||||
Button
|
||||
],
|
||||
styleUrl: './book-table.component.scss'
|
||||
})
|
||||
@@ -30,6 +34,8 @@ export class BookTableComponent implements OnChanges {
|
||||
|
||||
protected urlHelper = inject(UrlHelperService);
|
||||
private metadataDialogService = inject(MetadataDialogService);
|
||||
private bookService = inject(BookService);
|
||||
private messageService = inject(MessageService);
|
||||
|
||||
// Hack to set virtual-scroller height
|
||||
ngOnChanges() {
|
||||
@@ -90,4 +96,24 @@ export class BookTableComponent implements OnChanges {
|
||||
getGenres(genres: string[]) {
|
||||
return genres?.join(', ') || '';
|
||||
}
|
||||
|
||||
toggleMetadataLock(metadata: BookMetadata): void {
|
||||
const lockAction = metadata.allFieldsLocked ? 'UNLOCK' : 'LOCK';
|
||||
this.bookService.toggleAllLock(new Set([metadata.bookId]), lockAction).subscribe({
|
||||
next: () => {
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: `Metadata ${lockAction === 'LOCK' ? 'Locked' : 'Unlocked'}`,
|
||||
detail: `Book metadata has been ${lockAction === 'LOCK' ? 'locked' : 'unlocked'} successfully.`,
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: `Failed to ${lockAction === 'LOCK' ? 'Lock' : 'Unlock'}`,
|
||||
detail: `An error occurred while ${lockAction === 'LOCK' ? 'locking' : 'unlocking'} the metadata.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="flex flex-col items-center text-center space-y-6">
|
||||
<p class="pt-6 px-4">Lock or unlock all metadata for all the selected books</p>
|
||||
<div class="flex gap-4 justify-center">
|
||||
<p-button icon="pi pi-lock" label="Lock" outlined="true" severity="danger" (onClick)="toggleLock('LOCK')"></p-button>
|
||||
<p-button icon="pi pi-lock-open" label="Unlock" outlined="true" severity="success" (onClick)="toggleLock('UNLOCK')"></p-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {Button} from 'primeng/button';
|
||||
import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
import {MessageService} from 'primeng/api';
|
||||
import {BookService} from '../../../service/book.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lock-unlock-metadata-dialog',
|
||||
imports: [
|
||||
Button
|
||||
],
|
||||
templateUrl: './lock-unlock-metadata-dialog.component.html',
|
||||
styleUrl: './lock-unlock-metadata-dialog.component.scss'
|
||||
})
|
||||
export class LockUnlockMetadataDialogComponent {
|
||||
private bookService = inject(BookService);
|
||||
private dynamicDialogConfig = inject(DynamicDialogConfig);
|
||||
private dialogRef = inject(DynamicDialogRef);
|
||||
private messageService = inject(MessageService);
|
||||
|
||||
bookIds: Set<number> = this.dynamicDialogConfig.data.bookIds;
|
||||
|
||||
toggleLock(action: 'LOCK' | 'UNLOCK'): void {
|
||||
this.bookService.toggleAllLock(this.bookIds, action).subscribe({
|
||||
next: () => {
|
||||
const isLock = action === 'LOCK';
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: `Metadata ${isLock ? 'Locked' : 'Unlocked'}`,
|
||||
detail: `All selected books have been ${isLock ? 'locked' : 'unlocked'} successfully.`,
|
||||
});
|
||||
this.dialogRef.close(action.toLowerCase());
|
||||
},
|
||||
error: () => {
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: `Failed to ${action === 'LOCK' ? 'Lock' : 'Unlock'}`,
|
||||
detail: `An error occurred while ${action === 'LOCK' ? 'locking' : 'unlocking'} metadata.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ export interface BookMetadata {
|
||||
providerBookId?: string;
|
||||
thumbnailUrl?: string | null;
|
||||
|
||||
allFieldsLocked?: boolean;
|
||||
titleLocked?: boolean;
|
||||
subtitleLocked?: boolean;
|
||||
publisherLocked?: boolean;
|
||||
|
||||
@@ -210,7 +210,7 @@ export class BookService {
|
||||
|
||||
updateBookMetadata(bookId: number, bookMetadata: BookMetadata, mergeCategories: boolean): Observable<BookMetadata> {
|
||||
const params = new HttpParams().set('mergeCategories', mergeCategories.toString());
|
||||
return this.http.put<BookMetadata>(`${this.url}/${bookId}/metadata`, bookMetadata, { params }).pipe(
|
||||
return this.http.put<BookMetadata>(`${this.url}/${bookId}/metadata`, bookMetadata, {params}).pipe(
|
||||
map(updatedMetadata => {
|
||||
this.handleBookMetadataUpdate(bookId, updatedMetadata);
|
||||
return updatedMetadata;
|
||||
@@ -218,6 +218,27 @@ export class BookService {
|
||||
);
|
||||
}
|
||||
|
||||
toggleAllLock(bookIds: Set<number>, lock: string): Observable<void> {
|
||||
const requestBody = {
|
||||
bookIds: Array.from(bookIds),
|
||||
lock: lock
|
||||
};
|
||||
return this.http.put<BookMetadata[]>(`${this.url}/metadata/toggle-all-lock`, requestBody).pipe(
|
||||
tap((updatedMetadataList) => {
|
||||
const currentState = this.bookStateSubject.value;
|
||||
const updatedBooks = (currentState.books || []).map(book => {
|
||||
const updatedMetadata = updatedMetadataList.find(meta => meta.bookId === book.id);
|
||||
return updatedMetadata ? {...book, metadata: updatedMetadata} : book;
|
||||
});
|
||||
this.bookStateSubject.next({...currentState, books: updatedBooks});
|
||||
}),
|
||||
map(() => void 0),
|
||||
catchError((error) => {
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
autoRefreshMetadata(metadataRefreshRequest: MetadataRefreshRequest): Observable<any> {
|
||||
return this.http.put<void>(`${this.url}/metadata/refresh`, metadataRefreshRequest).pipe(
|
||||
map(() => {
|
||||
|
||||
@@ -151,7 +151,7 @@ export class MetadataEditorComponent implements OnInit {
|
||||
}
|
||||
|
||||
onSave(): void {
|
||||
this.bookService.updateBookMetadata(this.currentBookId, this.buildMetadata(), false).subscribe({
|
||||
this.bookService.updateBookMetadata(this.currentBookId, this.buildMetadata(undefined), false).subscribe({
|
||||
next: (response) => {
|
||||
this.messageService.add({severity: 'info', summary: 'Success', detail: 'Book metadata updated'});
|
||||
this.metadataCenterService.emit(response);
|
||||
@@ -171,7 +171,7 @@ export class MetadataEditorComponent implements OnInit {
|
||||
} else {
|
||||
this.metadataForm.get(field)?.enable();
|
||||
}
|
||||
this.updateMetadata();
|
||||
this.updateMetadata(undefined);
|
||||
}
|
||||
|
||||
lockAll(): void {
|
||||
@@ -182,7 +182,7 @@ export class MetadataEditorComponent implements OnInit {
|
||||
this.metadataForm.get(fieldName)?.disable();
|
||||
}
|
||||
});
|
||||
this.updateMetadata();
|
||||
this.updateMetadata(true);
|
||||
}
|
||||
|
||||
unlockAll(): void {
|
||||
@@ -193,10 +193,10 @@ export class MetadataEditorComponent implements OnInit {
|
||||
this.metadataForm.get(fieldName)?.enable();
|
||||
}
|
||||
});
|
||||
this.updateMetadata();
|
||||
this.updateMetadata(false);
|
||||
}
|
||||
|
||||
private buildMetadata() {
|
||||
private buildMetadata(shouldLockAllFields: boolean | undefined) {
|
||||
const updatedBookMetadata: BookMetadata = {
|
||||
bookId: this.currentBookId,
|
||||
title: this.metadataForm.get('title')?.value,
|
||||
@@ -233,17 +233,33 @@ export class MetadataEditorComponent implements OnInit {
|
||||
seriesNumberLocked: this.metadataForm.get('seriesNumberLocked')?.value,
|
||||
seriesTotalLocked: this.metadataForm.get('seriesTotalLocked')?.value,
|
||||
coverLocked: this.metadataForm.get('thumbnailUrlLocked')?.value,
|
||||
|
||||
...(shouldLockAllFields !== undefined && {allFieldsLocked: shouldLockAllFields}),
|
||||
};
|
||||
return updatedBookMetadata;
|
||||
}
|
||||
|
||||
private updateMetadata(): void {
|
||||
this.bookService.updateBookMetadata(this.currentBookId, this.buildMetadata(), false).subscribe({
|
||||
private updateMetadata(shouldLockAllFields: boolean | undefined): void {
|
||||
this.bookService.updateBookMetadata(this.currentBookId, this.buildMetadata(shouldLockAllFields), false).subscribe({
|
||||
next: (response) => {
|
||||
this.metadataCenterService.emit(response);
|
||||
|
||||
if (shouldLockAllFields !== undefined) {
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: shouldLockAllFields ? 'Metadata Locked' : 'Metadata Unlocked',
|
||||
detail: shouldLockAllFields
|
||||
? 'All fields have been successfully locked.'
|
||||
: 'All fields have been successfully unlocked.',
|
||||
});
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.messageService.add({severity: 'error', summary: 'Error', detail: 'Failed to update lock state'});
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to update lock state',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ export class MetadataPickerComponent implements OnInit {
|
||||
}
|
||||
|
||||
onSave(): void {
|
||||
const updatedBookMetadata = this.buildMetadata();
|
||||
const updatedBookMetadata = this.buildMetadata(undefined);
|
||||
this.bookService.updateBookMetadata(this.currentBookId, updatedBookMetadata, false).subscribe({
|
||||
next: (bookMetadata) => {
|
||||
Object.keys(this.copiedFields).forEach((field) => {
|
||||
@@ -197,7 +197,7 @@ export class MetadataPickerComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private buildMetadata() {
|
||||
private buildMetadata(shouldLockAllFields: boolean | undefined) {
|
||||
const updatedBookMetadata: BookMetadata = {
|
||||
bookId: this.currentBookId,
|
||||
title: this.metadataForm.get('title')?.value || this.copiedFields['title'] ? this.getValueOrCopied('title') : '',
|
||||
@@ -235,6 +235,8 @@ export class MetadataPickerComponent implements OnInit {
|
||||
seriesNumberLocked: this.metadataForm.get('seriesNumberLocked')?.value,
|
||||
seriesTotalLocked: this.metadataForm.get('seriesTotalLocked')?.value,
|
||||
coverLocked: this.metadataForm.get('thumbnailUrlLocked')?.value,
|
||||
|
||||
...(shouldLockAllFields !== undefined && {allFieldsLocked: shouldLockAllFields}),
|
||||
};
|
||||
return updatedBookMetadata;
|
||||
}
|
||||
@@ -247,13 +249,27 @@ export class MetadataPickerComponent implements OnInit {
|
||||
return this.copiedFields['thumbnailUrl'] ? this.getValueOrCopied('thumbnailUrl') : null;
|
||||
}
|
||||
|
||||
private updateMetadata(): void {
|
||||
this.bookService.updateBookMetadata(this.currentBookId, this.buildMetadata(), false).subscribe({
|
||||
private updateMetadata(shouldLockAllFields: boolean | undefined): void {
|
||||
this.bookService.updateBookMetadata(this.currentBookId, this.buildMetadata(shouldLockAllFields), false).subscribe({
|
||||
next: (response) => {
|
||||
this.metadataCenterService.emit(response);
|
||||
|
||||
if (shouldLockAllFields !== undefined) {
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: shouldLockAllFields ? 'Metadata Locked' : 'Metadata Unlocked',
|
||||
detail: shouldLockAllFields
|
||||
? 'All fields have been successfully locked.'
|
||||
: 'All fields have been successfully unlocked.',
|
||||
});
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.messageService.add({severity: 'error', summary: 'Error', detail: 'Failed to update lock state'});
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to update lock state',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -267,7 +283,7 @@ export class MetadataPickerComponent implements OnInit {
|
||||
} else {
|
||||
this.metadataForm.get(field)?.enable();
|
||||
}
|
||||
this.updateMetadata();
|
||||
this.updateMetadata(undefined);
|
||||
}
|
||||
|
||||
copyMissing(): void {
|
||||
@@ -352,7 +368,7 @@ export class MetadataPickerComponent implements OnInit {
|
||||
this.metadataForm.get(fieldName)?.disable();
|
||||
}
|
||||
});
|
||||
this.updateMetadata();
|
||||
this.updateMetadata(true);
|
||||
}
|
||||
|
||||
unlockAll(): void {
|
||||
@@ -363,7 +379,7 @@ export class MetadataPickerComponent implements OnInit {
|
||||
this.metadataForm.get(fieldName)?.enable();
|
||||
}
|
||||
});
|
||||
this.updateMetadata();
|
||||
this.updateMetadata(false);
|
||||
}
|
||||
|
||||
highlightCopiedInput(field: string): void {
|
||||
|
||||
@@ -13,7 +13,17 @@
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<p *ngIf="metadata.seriesName" class="italic">{{ metadata.seriesName }} #{{ metadata.seriesNumber }} </p>
|
||||
<p class="text-2xl font-bold">{{ metadata.title }}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-2xl font-bold m-0">{{ metadata.title }}</p>
|
||||
<i
|
||||
class="pi"
|
||||
[ngClass]="metadata.allFieldsLocked ? 'pi-lock text-red-500' : 'pi-lock-open text-green-500'"
|
||||
[title]="metadata.allFieldsLocked ? 'Metadata is locked' : 'Metadata is unlocked'"
|
||||
style="font-size: 1.25rem;"
|
||||
pTooltip="{{ metadata.allFieldsLocked ? 'This book metadata is locked.' : 'This book metadata is unlocked.' }}"
|
||||
tooltipPosition="top">
|
||||
</i>
|
||||
</div>
|
||||
<p class="text-xl">{{ getAuthorNames(metadata?.authors || []) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,6 +55,13 @@
|
||||
<p-button label="Assign Shelf" icon="pi pi-folder" severity="info" outlined (onClick)="assignShelf(metadata.bookId)"></p-button>
|
||||
<p-button *ngIf="userData.permissions.canDownload" label="Download" icon="pi pi-download" severity="info" outlined (onClick)="download(metadata.bookId)"></p-button>
|
||||
<p-splitbutton *ngIf="userData.permissions.canEmailBook" label="Quick Send" icon="pi pi-send" [model]="items" (onClick)="quickSend(metadata.bookId)" outlined severity="info"/>
|
||||
<!--<p-button
|
||||
outlined
|
||||
[label]="metadata.allFieldsLocked ? 'Unlock Metadata' : 'Lock Metadata'"
|
||||
[icon]="metadata.allFieldsLocked ? 'pi pi-lock' : 'pi pi-lock-open'"
|
||||
[severity]="metadata.allFieldsLocked ? 'danger' : 'success'"
|
||||
(onClick)="toggleMetadataLock(metadata)">
|
||||
</p-button>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {Button} from 'primeng/button';
|
||||
import {AsyncPipe, NgForOf, NgIf} from '@angular/common';
|
||||
import {AsyncPipe, NgClass, NgForOf, NgIf} from '@angular/common';
|
||||
import {first, Observable} from 'rxjs';
|
||||
import {BookService} from '../../../book/service/book.service';
|
||||
import {BookMetadataCenterService} from '../book-metadata-center.service';
|
||||
@@ -17,13 +17,14 @@ import {BookSenderComponent} from '../../../book/components/book-sender/book-sen
|
||||
import {DialogService} from 'primeng/dynamicdialog';
|
||||
import {EmailService} from '../../../settings/email/email.service';
|
||||
import {ShelfAssignerComponent} from '../../../book/components/shelf-assigner/shelf-assigner.component';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
|
||||
@Component({
|
||||
selector: 'app-metadata-viewer',
|
||||
standalone: true,
|
||||
templateUrl: './metadata-viewer.component.html',
|
||||
styleUrl: './metadata-viewer.component.scss',
|
||||
imports: [Button, NgForOf, NgIf, AsyncPipe, Rating, FormsModule, Tag, Divider, SplitButton]
|
||||
imports: [Button, NgForOf, NgIf, AsyncPipe, Rating, FormsModule, Tag, Divider, SplitButton, NgClass, Tooltip]
|
||||
})
|
||||
export class MetadataViewerComponent implements OnInit {
|
||||
|
||||
@@ -119,4 +120,24 @@ export class MetadataViewerComponent implements OnInit {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/*toggleMetadataLock(metadata: BookMetadata): void {
|
||||
const lockAction = metadata.allFieldsLocked ? 'UNLOCK' : 'LOCK';
|
||||
this.bookService.toggleAllLock(new Set([metadata.bookId]), lockAction).subscribe({
|
||||
next: () => {
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: `Metadata ${lockAction === 'LOCK' ? 'Locked' : 'Unlocked'}`,
|
||||
detail: `Book metadata has been ${lockAction === 'LOCK' ? 'locked' : 'unlocked'} successfully.`,
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: `Failed to ${lockAction === 'LOCK' ? 'Lock' : 'Unlock'}`,
|
||||
detail: `An error occurred while ${lockAction === 'LOCK' ? 'locking' : 'unlocking'} the metadata.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}*/
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user