diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts index 15af2c4f..262101a4 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts @@ -50,6 +50,7 @@ import {GroupRule} from '../../../magic-shelf/component/magic-shelf-component'; import {TaskHelperService} from '../../../settings/task-management/task-helper.service'; import {FilterLabelHelper} from './filter-label.helper'; import {LoadingService} from '../../../../core/services/loading.service'; +import {BookNavigationService} from '../../service/book-navigation.service'; export enum EntityType { LIBRARY = 'Library', @@ -118,10 +119,10 @@ export class BookBrowserComponent implements OnInit, AfterViewInit { protected confirmationService = inject(ConfirmationService); protected magicShelfService = inject(MagicShelfService); protected bookRuleEvaluatorService = inject(BookRuleEvaluatorService); + protected taskHelperService = inject(TaskHelperService); private pageTitle = inject(PageTitleService); private loadingService = inject(LoadingService); - - protected taskHelperService = inject(TaskHelperService); + private bookNavigationService = inject(BookNavigationService); bookState$: Observable | undefined; entity$: Observable | undefined; @@ -584,6 +585,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit { ) .subscribe(books => { this.currentBooks = books; + this.bookNavigationService.setAvailableBookIds(books.map(book => book.id)); }); } diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts index 6463ba6b..a3ba42b2 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts @@ -22,6 +22,7 @@ import {ResetProgressTypes} from '../../../../../shared/constants/reset-progress import {ReadStatusHelper} from '../../../helpers/read-status.helper'; import {BookDialogHelperService} from '../BookDialogHelperService'; import {TaskHelperService} from '../../../../settings/task-management/task-helper.service'; +import {BookNavigationService} from '../../../service/book-navigation.service'; @Component({ selector: 'app-book-card', @@ -61,6 +62,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { protected urlHelper = inject(UrlHelperService); private confirmationService = inject(ConfirmationService); private bookDialogHelperService = inject(BookDialogHelperService); + private bookNavigationService = inject(BookNavigationService); private userPermissions: any; private metadataCenterViewMode: 'route' | 'dialog' = 'route'; @@ -110,7 +112,6 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { get displayTitle(): string | undefined { return (this.isSeriesCollapsed && this.book.metadata?.seriesName) ? this.book.metadata?.seriesName : this.book.metadata?.title; - // return (this.isSeriesCollapsed && this.book.metadata?.seriesName) ? this.book.metadata.seriesName : this.book.metadata?.title; } onImageLoad(): void { @@ -132,7 +133,6 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { onMenuToggle(event: Event, menu: TieredMenu): void { menu.toggle(event); - // Load additional files if not already loaded and needed if (!this.additionalFilesLoaded && !this.isSubMenuLoading && this.needsAdditionalFilesData()) { this.isSubMenuLoading = true; this.bookService.getBookByIdFromAPI(this.book.id, true).subscribe({ @@ -446,6 +446,11 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { } openBookInfo(book: Book): void { + const allBookIds = this.bookNavigationService.getAvailableBookIds(); + if (allBookIds.length > 0) { + this.bookNavigationService.setNavigationContext(allBookIds, book.id); + } + if (this.metadataCenterViewMode === 'route') { this.router.navigate(['/book', book.id], { queryParams: {tab: 'view'} diff --git a/booklore-ui/src/app/features/book/service/book-navigation.service.ts b/booklore-ui/src/app/features/book/service/book-navigation.service.ts new file mode 100644 index 00000000..5fd2096a --- /dev/null +++ b/booklore-ui/src/app/features/book/service/book-navigation.service.ts @@ -0,0 +1,86 @@ +import {Injectable} from '@angular/core'; +import {BehaviorSubject, Observable} from 'rxjs'; + +export interface BookNavigationState { + bookIds: number[]; + currentIndex: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class BookNavigationService { + private navigationState$ = new BehaviorSubject(null); + private availableBookIds: number[] = []; + + setAvailableBookIds(bookIds: number[]): void { + this.availableBookIds = bookIds; + } + + getAvailableBookIds(): number[] { + return this.availableBookIds; + } + + setNavigationContext(bookIds: number[], currentBookId: number): void { + const currentIndex = bookIds.indexOf(currentBookId); + if (currentIndex !== -1) { + this.navigationState$.next({bookIds, currentIndex}); + } else { + this.navigationState$.next(null); + } + } + + getNavigationState(): Observable { + return this.navigationState$.asObservable(); + } + + canNavigatePrevious(): boolean { + const state = this.navigationState$.value; + return state !== null && state.currentIndex > 0; + } + + canNavigateNext(): boolean { + const state = this.navigationState$.value; + return state !== null && state.currentIndex < state.bookIds.length - 1; + } + + getPreviousBookId(): number | null { + const state = this.navigationState$.value; + if (state && state.currentIndex > 0) { + return state.bookIds[state.currentIndex - 1]; + } + return null; + } + + getNextBookId(): number | null { + const state = this.navigationState$.value; + if (state && state.currentIndex < state.bookIds.length - 1) { + return state.bookIds[state.currentIndex + 1]; + } + return null; + } + + updateCurrentBook(bookId: number): void { + const state = this.navigationState$.value; + if (state) { + const newIndex = state.bookIds.indexOf(bookId); + if (newIndex !== -1) { + this.navigationState$.next({ + ...state, + currentIndex: newIndex + }); + } + } + } + + getCurrentPosition(): { current: number; total: number } | null { + const state = this.navigationState$.value; + if (state) { + return { + current: state.currentIndex + 1, + total: state.bookIds.length + }; + } + return null; + } +} diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-metadata-center.component.html b/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-metadata-center.component.html index 74070163..c38cc809 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-metadata-center.component.html +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-metadata-center.component.html @@ -20,11 +20,16 @@ - + + @if (admin || canEditMetadata) { - + + } @if (admin || canEditMetadata) { diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html index 29a6f378..7e40b504 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html @@ -335,184 +335,184 @@ -
-
- -
- - @if (!book.metadata!['reviewsLocked']) { - - } - @if (book.metadata!['reviewsLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['isbn10Locked']) { - - } - @if (book.metadata!['isbn10Locked']) { - - } -
-
-
- -
- - @if (!book.metadata!['isbn13Locked']) { - - } - @if (book.metadata!['isbn13Locked']) { - - } -
-
-
- -
- - @if (!book.metadata!['asinLocked']) { - - } - @if (book.metadata!['asinLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['amazonRatingLocked']) { - - } - @if (book.metadata!['amazonRatingLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['amazonReviewCountLocked']) { - - } - @if (book.metadata!['amazonReviewCountLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['googleIdLocked']) { - - } - @if (book.metadata!['googleIdLocked']) { - - } -
-
+
+
+ +
+ + @if (!book.metadata!['reviewsLocked']) { + + } + @if (book.metadata!['reviewsLocked']) { + + }
- -
-
- -
- - @if (!book.metadata!['goodreadsIdLocked']) { - - } - @if (book.metadata!['goodreadsIdLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['goodreadsRatingLocked']) { - - } - @if (book.metadata!['goodreadsRatingLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['goodreadsReviewCountLocked']) { - - } - @if (book.metadata!['goodreadsReviewCountLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['hardcoverIdLocked']) { - - } - @if (book.metadata!['hardcoverIdLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['hardcoverRatingLocked']) { - - } - @if (book.metadata!['hardcoverRatingLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['hardcoverReviewCountLocked']) { - - } - @if (book.metadata!['hardcoverReviewCountLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['comicvineIdLocked']) { - - } - @if (book.metadata!['comicvineIdLocked']) { - - } -
-
+
+
+ +
+ + @if (!book.metadata!['isbn10Locked']) { + + } + @if (book.metadata!['isbn10Locked']) { + + }
+
+
+ +
+ + @if (!book.metadata!['isbn13Locked']) { + + } + @if (book.metadata!['isbn13Locked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['asinLocked']) { + + } + @if (book.metadata!['asinLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['amazonRatingLocked']) { + + } + @if (book.metadata!['amazonRatingLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['amazonReviewCountLocked']) { + + } + @if (book.metadata!['amazonReviewCountLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['googleIdLocked']) { + + } + @if (book.metadata!['googleIdLocked']) { + + } +
+
+
-
-
- -
+
+
+ +
+ + @if (!book.metadata!['goodreadsIdLocked']) { + + } + @if (book.metadata!['goodreadsIdLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['goodreadsRatingLocked']) { + + } + @if (book.metadata!['goodreadsRatingLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['goodreadsReviewCountLocked']) { + + } + @if (book.metadata!['goodreadsReviewCountLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['hardcoverIdLocked']) { + + } + @if (book.metadata!['hardcoverIdLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['hardcoverRatingLocked']) { + + } + @if (book.metadata!['hardcoverRatingLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['hardcoverReviewCountLocked']) { + + } + @if (book.metadata!['hardcoverReviewCountLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['comicvineIdLocked']) { + + } + @if (book.metadata!['comicvineIdLocked']) { + + } +
+
+
+ +
+
+ +
- @if (!book.metadata!['descriptionLocked']) { - - } - @if (book.metadata!['descriptionLocked']) { - - } -
-
+ @if (!book.metadata!['descriptionLocked']) { + + } + @if (book.metadata!['descriptionLocked']) { + + }
+
+
@@ -545,6 +545,35 @@
} + @if (navigationState$ | async) { +
+ + + + {{ getNavigationPosition() }} + + + +
+ } +
} - +
+ @if (navigationState$ | async) { +
+ + + + {{ getNavigationPosition() }} + + + +
+ }
+
diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts index 645df8af..7331b686 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts @@ -23,6 +23,10 @@ import {Image} from "primeng/image"; import {LazyLoadImageModule} from "ng-lazyload-image"; import {TaskHelperService} from '../../../../settings/task-management/task-helper.service'; import {BookDialogHelperService} from "../../../../book/components/book-browser/BookDialogHelperService"; +import {BookNavigationService} from '../../../../book/service/book-navigation.service'; +import {BookMetadataHostService} from '../../../../../shared/service/book-metadata-host-service'; +import {Router} from '@angular/router'; +import {UserService} from '../../../../settings/user-management/user.service'; @Component({ selector: "app-metadata-editor", @@ -61,6 +65,10 @@ export class MetadataEditorComponent implements OnInit { private taskHelperService = inject(TaskHelperService); protected urlHelper = inject(UrlHelperService); private bookDialogHelperService = inject(BookDialogHelperService); + private bookNavigationService = inject(BookNavigationService); + private metadataHostService = inject(BookMetadataHostService); + private router = inject(Router); + private userService = inject(UserService); private destroyRef = inject(DestroyRef); metadataForm: FormGroup; @@ -86,6 +94,9 @@ export class MetadataEditorComponent implements OnInit { filteredTags: string[] = []; filteredPublishers: string[] = []; filteredSeries: string[] = []; + private metadataCenterViewMode: 'route' | 'dialog' = 'route'; + + navigationState$ = this.bookNavigationService.getNavigationState(); filterCategories(event: { query: string }) { const query = event.query.toLowerCase(); @@ -206,6 +217,15 @@ export class MetadataEditorComponent implements OnInit { }); this.prepareAutoComplete(); + + this.userService.userState$ + .pipe( + filter(userState => !!userState?.user && userState.loaded), + take(1) + ) + .subscribe(userState => { + this.metadataCenterViewMode = userState.user?.userSettings.metadataCenterViewMode ?? 'route'; + }); } private prepareAutoComplete(): void { @@ -692,5 +712,43 @@ export class MetadataEditorComponent implements OnInit { }); } + canNavigatePrevious(): boolean { + return this.bookNavigationService.canNavigatePrevious(); + } + + canNavigateNext(): boolean { + return this.bookNavigationService.canNavigateNext(); + } + + navigatePrevious(): void { + const prevBookId = this.bookNavigationService.getPreviousBookId(); + if (prevBookId) { + this.navigateToBook(prevBookId); + } + } + + navigateNext(): void { + const nextBookId = this.bookNavigationService.getNextBookId(); + if (nextBookId) { + this.navigateToBook(nextBookId); + } + } + + private navigateToBook(bookId: number): void { + this.bookNavigationService.updateCurrentBook(bookId); + if (this.metadataCenterViewMode === 'route') { + this.router.navigate(['/book', bookId], { + queryParams: {tab: 'edit'} + }); + } else { + this.metadataHostService.switchBook(bookId); + } + } + + getNavigationPosition(): string { + const position = this.bookNavigationService.getCurrentPosition(); + return position ? `${position.current} of ${position.total}` : ''; + } + protected readonly sample = sample; } diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html index 326db744..149ad793 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html @@ -46,6 +46,35 @@ }
+ + @if (navigationState$ | async) { + + }
@@ -454,6 +483,35 @@ @if (userService.userState$ | async; as userState) {
+ @if (navigationState$ | async) { + + + } @if (book!.bookType === 'PDF') { @if (readMenuItems$ | async; as readItems) { @@ -462,19 +520,14 @@ @if (book!.bookType !== 'PDF' && book!.bookType !== 'FB2') { } - + @if (userState.user!.permissions.canDownload || userState.user!.permissions.admin) { @if ((book!.alternativeFormats && book!.alternativeFormats.length > 0) || (book!.supplementaryFiles && book!.supplementaryFiles.length > 0)) { @if (downloadMenuItems$ | async; as downloadItems) { - + } } @else { - - } - } - @if (userState.user!.permissions.canEmailBook || userState.user!.permissions.admin) { - @if (emailMenuItems$ | async; as emailItems) { - + } } @if (userState.user!.permissions.canEditMetadata || userState.user!.permissions.admin) { @@ -488,7 +541,8 @@ (onClick)="quickRefresh(book!.id)" [disabled]="isAutoFetching" pTooltip="Automatically fetch metadata using default sources" - tooltipPosition="top"> + tooltipPosition="top" + styleClass="mobile-icon-only"> } } diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.scss b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.scss index f7ad0a51..0bce931e 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.scss +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.scss @@ -26,17 +26,21 @@ .cover-section { display: flex; + flex-direction: column; justify-content: center; + align-items: center; flex-shrink: 0; + gap: 1rem; @media (min-width: 768px) { justify-content: flex-start; + align-items: flex-start; } } .cover-wrapper { position: relative; - width: 175px; + width: 250px; overflow: hidden; @media (min-width: 768px) { @@ -461,6 +465,60 @@ display: flex; flex-wrap: wrap; gap: 0.5rem; + align-items: center; +} + +::ng-deep .action-divider { + display: none; + + @media (min-width: 768px) { + display: block; + } +} + +@media (max-width: 767px) { + ::ng-deep .mobile-icon-only .p-button-label { + display: none !important; + padding: 10px !important; + } + + ::ng-deep .mobile-icon-only .p-button { + min-width: auto !important; + padding: 10px !important; + } + + ::ng-deep .mobile-icon-only.p-splitbutton .p-button-label { + display: none !important; + padding: 10px !important; + } + + ::ng-deep .mobile-icon-only.p-splitbutton .p-button { + min-width: auto !important; + padding: 10px !important; + } +} + +.navigation-buttons-desktop { + display: none; + align-items: center; + gap: 0.5rem; +} + +.navigation-buttons-mobile { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +@media (min-width: 768px) { + .navigation-buttons-desktop { + display: flex; + } + + .navigation-buttons-mobile { + display: none; + } } .description-section { diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts index c56659e9..462779b5 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts @@ -38,13 +38,16 @@ import { matchScoreRanges, pageCountRanges } from '../../../../book/components/book-browser/book-filter/book-filter.component'; +import {BookNavigationService} from '../../../../book/service/book-navigation.service'; +import {Divider} from 'primeng/divider'; +import {BookMetadataHostService} from '../../../../../shared/service/book-metadata-host-service'; @Component({ selector: 'app-metadata-viewer', standalone: true, templateUrl: './metadata-viewer.component.html', styleUrl: './metadata-viewer.component.scss', - imports: [Button, AsyncPipe, Rating, FormsModule, SplitButton, NgClass, Tooltip, DecimalPipe, Editor, ProgressBar, Menu, InfiniteScrollDirective, BookCardLiteComponent, DatePicker, Tab, TabList, TabPanel, TabPanels, Tabs, BookReviewsComponent, BookNotesComponent, ProgressSpinner, TieredMenu, Image, TagComponent, UpperCasePipe] + imports: [Button, AsyncPipe, Rating, FormsModule, SplitButton, NgClass, Tooltip, DecimalPipe, Editor, ProgressBar, Menu, InfiniteScrollDirective, BookCardLiteComponent, DatePicker, Tab, TabList, TabPanel, TabPanels, Tabs, BookReviewsComponent, BookNotesComponent, ProgressSpinner, TieredMenu, Image, TagComponent, UpperCasePipe, Divider] }) export class MetadataViewerComponent implements OnInit, OnChanges { @Input() book$!: Observable; @@ -65,7 +68,6 @@ export class MetadataViewerComponent implements OnInit, OnChanges { private destroyRef = inject(DestroyRef); private dialogRef?: DynamicDialogRef; - emailMenuItems$!: Observable; readMenuItems$!: Observable; refreshMenuItems$!: Observable; otherItems$!: Observable; @@ -91,20 +93,11 @@ export class MetadataViewerComponent implements OnInit, OnChanges { {value: ReadStatus.UNSET, label: 'Unset'}, ]; - ngOnInit(): void { - this.emailMenuItems$ = this.book$.pipe( - map(book => book?.metadata ?? null), - filter((metadata): metadata is BookMetadata => metadata != null), - map((metadata): MenuItem[] => [ - { - label: 'Custom Send', - command: () => { - this.bookDialogHelperService.openCustomSendDialog(metadata.bookId); - } - } - ]) - ); + private bookNavigationService = inject(BookNavigationService); + private metadataHostService = inject(BookMetadataHostService); + navigationState$ = this.bookNavigationService.getNavigationState(); + ngOnInit(): void { this.refreshMenuItems$ = this.book$.pipe( filter((book): book is Book => book !== null), map((book): MenuItem[] => [ @@ -171,95 +164,123 @@ export class MetadataViewerComponent implements OnInit, OnChanges { this.otherItems$ = this.book$.pipe( filter((book): book is Book => book !== null), - map((book): MenuItem[] => { - const items: MenuItem[] = [ - { - label: 'Upload File', - icon: 'pi pi-upload', - command: () => { - this.bookDialogHelperService.openAdditionalFileUploaderDialog(book); - }, - }, - { - label: 'Organize Files', - icon: 'pi pi-arrows-h', - command: () => { - this.openFileMoverDialog(book.id); - }, - }, - { - label: 'Delete Book', - icon: 'pi pi-trash', - command: () => { - this.confirmationService.confirm({ - message: `Are you sure you want to delete "${book.metadata?.title}"?`, - header: 'Confirm Deletion', - icon: 'pi pi-exclamation-triangle', - acceptIcon: 'pi pi-trash', - rejectIcon: 'pi pi-times', - acceptButtonStyleClass: 'p-button-danger', - accept: () => { - this.bookService.deleteBooks(new Set([book.id])).subscribe({ - next: () => { - if (this.metadataCenterViewMode === 'route') { - this.router.navigate(['/dashboard']); - } else { - this.dialogRef?.close(); - } - }, - error: () => { + switchMap(book => + this.userService.userState$.pipe( + take(1), + map(userState => { + const items: MenuItem[] = [ + { + label: 'Upload File', + icon: 'pi pi-upload', + command: () => { + this.bookDialogHelperService.openAdditionalFileUploaderDialog(book); + }, + }, + { + label: 'Organize Files', + icon: 'pi pi-arrows-h', + command: () => { + this.openFileMoverDialog(book.id); + }, + }, + ]; + + // Add Send Book submenu if user has permission + if (userState?.user?.permissions.canEmailBook || userState?.user?.permissions.admin) { + items.push({ + label: 'Send Book', + icon: 'pi pi-send', + items: [ + { + label: 'Quick Send', + icon: 'pi pi-bolt', + command: () => this.quickSend(book.id) + }, + { + label: 'Custom Send', + icon: 'pi pi-cog', + command: () => { + this.bookDialogHelperService.openCustomSendDialog(book.id); } + } + ] + }); + } + + items.push({ + label: 'Delete Book', + icon: 'pi pi-trash', + command: () => { + this.confirmationService.confirm({ + message: `Are you sure you want to delete "${book.metadata?.title}"?`, + header: 'Confirm Deletion', + icon: 'pi pi-exclamation-triangle', + acceptIcon: 'pi pi-trash', + rejectIcon: 'pi pi-times', + acceptButtonStyleClass: 'p-button-danger', + accept: () => { + this.bookService.deleteBooks(new Set([book.id])).subscribe({ + next: () => { + if (this.metadataCenterViewMode === 'route') { + this.router.navigate(['/dashboard']); + } else { + this.dialogRef?.close(); + } + }, + error: () => { + } + }); + } + }); + }, + }); + + // Add delete additional files menu if there are any additional files + if ((book.alternativeFormats && book.alternativeFormats.length > 0) || + (book.supplementaryFiles && book.supplementaryFiles.length > 0)) { + const deleteFileItems: MenuItem[] = []; + + // Add alternative formats + if (book.alternativeFormats && book.alternativeFormats.length > 0) { + book.alternativeFormats.forEach(format => { + const extension = this.getFileExtension(format.filePath); + deleteFileItems.push({ + label: `${format.fileName} (${this.getFileSizeInMB(format)})`, + icon: this.getFileIcon(extension), + command: () => this.deleteAdditionalFile(book.id, format.id, format.fileName || 'file') }); - } + }); + } + + // Add separator if both types exist + if (book.alternativeFormats && book.alternativeFormats.length > 0 && + book.supplementaryFiles && book.supplementaryFiles.length > 0) { + deleteFileItems.push({separator: true}); + } + + // Add supplementary files + if (book.supplementaryFiles && book.supplementaryFiles.length > 0) { + book.supplementaryFiles.forEach(file => { + const extension = this.getFileExtension(file.filePath); + deleteFileItems.push({ + label: `${file.fileName} (${this.getFileSizeInMB(file)})`, + icon: this.getFileIcon(extension), + command: () => this.deleteAdditionalFile(book.id, file.id, file.fileName || 'file') + }); + }); + } + + items.push({ + label: 'Delete Additional Files', + icon: 'pi pi-trash', + items: deleteFileItems }); - }, - }, - ]; + } - // Add delete additional files menu if there are any additional files - if ((book.alternativeFormats && book.alternativeFormats.length > 0) || - (book.supplementaryFiles && book.supplementaryFiles.length > 0)) { - const deleteFileItems: MenuItem[] = []; - - // Add alternative formats - if (book.alternativeFormats && book.alternativeFormats.length > 0) { - book.alternativeFormats.forEach(format => { - const extension = this.getFileExtension(format.filePath); - deleteFileItems.push({ - label: `${format.fileName} (${this.getFileSizeInMB(format)})`, - icon: this.getFileIcon(extension), - command: () => this.deleteAdditionalFile(book.id, format.id, format.fileName || 'file') - }); - }); - } - - // Add separator if both types exist - if (book.alternativeFormats && book.alternativeFormats.length > 0 && - book.supplementaryFiles && book.supplementaryFiles.length > 0) { - deleteFileItems.push({separator: true}); - } - - // Add supplementary files - if (book.supplementaryFiles && book.supplementaryFiles.length > 0) { - book.supplementaryFiles.forEach(file => { - const extension = this.getFileExtension(file.filePath); - deleteFileItems.push({ - label: `${file.fileName} (${this.getFileSizeInMB(file)})`, - icon: this.getFileIcon(extension), - command: () => this.deleteAdditionalFile(book.id, file.id, file.fileName || 'file') - }); - }); - } - - items.push({ - label: 'Delete Additional Files', - icon: 'pi pi-trash', - items: deleteFileItems - }); - } - - return items; - }) + return items; + }) + ) + ) ); this.userService.userState$ @@ -846,4 +867,42 @@ export class MetadataViewerComponent implements OnInit, OnChanges { protected readonly ResetProgressTypes = ResetProgressTypes; protected readonly ReadStatus = ReadStatus; + + canNavigatePrevious(): boolean { + return this.bookNavigationService.canNavigatePrevious(); + } + + canNavigateNext(): boolean { + return this.bookNavigationService.canNavigateNext(); + } + + navigatePrevious(): void { + const prevBookId = this.bookNavigationService.getPreviousBookId(); + if (prevBookId) { + this.navigateToBook(prevBookId); + } + } + + navigateNext(): void { + const nextBookId = this.bookNavigationService.getNextBookId(); + if (nextBookId) { + this.navigateToBook(nextBookId); + } + } + + private navigateToBook(bookId: number): void { + this.bookNavigationService.updateCurrentBook(bookId); + if (this.metadataCenterViewMode === 'route') { + this.router.navigate(['/book', bookId], { + queryParams: {tab: 'view'} + }); + } else { + this.metadataHostService.switchBook(bookId); + } + } + + getNavigationPosition(): string { + const position = this.bookNavigationService.getCurrentPosition(); + return position ? `${position.current} of ${position.total}` : ''; + } } diff --git a/booklore-ui/src/app/shared/service/book-metadata-host-service.ts b/booklore-ui/src/app/shared/service/book-metadata-host-service.ts index 90d0079e..3a83a254 100644 --- a/booklore-ui/src/app/shared/service/book-metadata-host-service.ts +++ b/booklore-ui/src/app/shared/service/book-metadata-host-service.ts @@ -9,6 +9,10 @@ export class BookMetadataHostService { this.bookSwitchRequest$.next(bookId); } + switchBook(bookId: number): void { + this.bookSwitchRequest$.next(bookId); + } + get bookSwitches$() { return this.bookSwitchRequest$.asObservable(); }