mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Add sequential next/previous book navigation from library, filtered, and search views (#1931)
Co-authored-by: acx10 <acx10@users.noreply.github.com>
This commit is contained in:
@@ -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<BookState> | undefined;
|
||||
entity$: Observable<Library | Shelf | MagicShelf | null> | undefined;
|
||||
@@ -584,6 +585,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
|
||||
)
|
||||
.subscribe(books => {
|
||||
this.currentBooks = books;
|
||||
this.bookNavigationService.setAvailableBookIds(books.map(book => book.id));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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<BookNavigationState | null>(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<BookNavigationState | null> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,16 @@
|
||||
</p-tablist>
|
||||
<p-tabpanels class="tabpanels-responsive overflow-auto">
|
||||
<p-tabpanel value="view">
|
||||
<app-metadata-viewer [book$]="book$" [recommendedBooks]="recommendedBooks"></app-metadata-viewer>
|
||||
<app-metadata-viewer
|
||||
[book$]="book$"
|
||||
[recommendedBooks]="recommendedBooks">
|
||||
</app-metadata-viewer>
|
||||
</p-tabpanel>
|
||||
@if (admin || canEditMetadata) {
|
||||
<p-tabpanel value="edit">
|
||||
<app-metadata-editor [book$]="book$"></app-metadata-editor>
|
||||
<app-metadata-editor
|
||||
[book$]="book$">
|
||||
</app-metadata-editor>
|
||||
</p-tabpanel>
|
||||
}
|
||||
@if (admin || canEditMetadata) {
|
||||
|
||||
@@ -545,6 +545,35 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (navigationState$ | async) {
|
||||
<div class="flex gap-2 items-center">
|
||||
<p-button
|
||||
icon="pi pi-chevron-left"
|
||||
[disabled]="!canNavigatePrevious()"
|
||||
(onClick)="navigatePrevious()"
|
||||
rounded
|
||||
outlined
|
||||
severity="info"
|
||||
pTooltip="Go to previous book"
|
||||
tooltipPosition="bottom">
|
||||
</p-button>
|
||||
<span class="text-sm text-surface-600 dark:text-surface-400">
|
||||
{{ getNavigationPosition() }}
|
||||
</span>
|
||||
<p-button
|
||||
icon="pi pi-chevron-right"
|
||||
iconPos="right"
|
||||
[disabled]="!canNavigateNext()"
|
||||
(onClick)="navigateNext()"
|
||||
severity="info"
|
||||
rounded
|
||||
outlined
|
||||
pTooltip="Go to next book"
|
||||
tooltipPosition="bottom">
|
||||
</p-button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex gap-x-4 items-center ml-auto">
|
||||
<p-button
|
||||
[label]="isAutoFetching ? 'Fetching…' : 'Auto Fetch'"
|
||||
@@ -575,6 +604,34 @@
|
||||
}
|
||||
|
||||
<div class="flex justify-between items-center w-full gap-4">
|
||||
@if (navigationState$ | async) {
|
||||
<div class="flex gap-2 items-center">
|
||||
<p-button
|
||||
icon="pi pi-chevron-left"
|
||||
[disabled]="!canNavigatePrevious()"
|
||||
(onClick)="navigatePrevious()"
|
||||
rounded
|
||||
outlined
|
||||
severity="info"
|
||||
pTooltip="Go to previous book"
|
||||
tooltipPosition="bottom">
|
||||
</p-button>
|
||||
<span class="text-sm text-surface-600 dark:text-surface-400">
|
||||
{{ getNavigationPosition() }}
|
||||
</span>
|
||||
<p-button
|
||||
icon="pi pi-chevron-right"
|
||||
iconPos="right"
|
||||
[disabled]="!canNavigateNext()"
|
||||
(onClick)="navigateNext()"
|
||||
severity="info"
|
||||
rounded
|
||||
outlined
|
||||
pTooltip="Go to next book"
|
||||
tooltipPosition="bottom">
|
||||
</p-button>
|
||||
</div>
|
||||
}
|
||||
<div class="flex gap-4">
|
||||
<p-button
|
||||
icon="pi pi-bolt"
|
||||
@@ -617,6 +674,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,35 @@
|
||||
</p-progressBar>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (navigationState$ | async) {
|
||||
<div class="navigation-buttons-mobile">
|
||||
<p-button
|
||||
icon="pi pi-chevron-left"
|
||||
[disabled]="!canNavigatePrevious()"
|
||||
(onClick)="navigatePrevious()"
|
||||
rounded
|
||||
outlined
|
||||
severity="info"
|
||||
pTooltip="Go to previous book"
|
||||
tooltipPosition="bottom">
|
||||
</p-button>
|
||||
<span class="text-sm text-surface-600 dark:text-surface-400">
|
||||
{{ getNavigationPosition() }}
|
||||
</span>
|
||||
<p-button
|
||||
icon="pi pi-chevron-right"
|
||||
iconPos="right"
|
||||
[disabled]="!canNavigateNext()"
|
||||
(onClick)="navigateNext()"
|
||||
severity="info"
|
||||
rounded
|
||||
outlined
|
||||
pTooltip="Go to next book"
|
||||
tooltipPosition="bottom">
|
||||
</p-button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
@@ -454,6 +483,35 @@
|
||||
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
<div class="action-buttons">
|
||||
@if (navigationState$ | async) {
|
||||
<div class="navigation-buttons-desktop">
|
||||
<p-button
|
||||
icon="pi pi-chevron-left"
|
||||
[disabled]="!canNavigatePrevious()"
|
||||
(onClick)="navigatePrevious()"
|
||||
rounded
|
||||
outlined
|
||||
severity="info"
|
||||
pTooltip="Go to previous book"
|
||||
tooltipPosition="bottom">
|
||||
</p-button>
|
||||
<span class="text-sm text-surface-600 dark:text-surface-400">
|
||||
{{ getNavigationPosition() }}
|
||||
</span>
|
||||
<p-button
|
||||
icon="pi pi-chevron-right"
|
||||
iconPos="right"
|
||||
[disabled]="!canNavigateNext()"
|
||||
(onClick)="navigateNext()"
|
||||
severity="info"
|
||||
rounded
|
||||
outlined
|
||||
pTooltip="Go to next book"
|
||||
tooltipPosition="bottom">
|
||||
</p-button>
|
||||
</div>
|
||||
<p-divider layout="vertical" class="action-divider"></p-divider>
|
||||
}
|
||||
@if (book!.bookType === 'PDF') {
|
||||
@if (readMenuItems$ | async; as readItems) {
|
||||
<p-splitbutton label="Read" icon="pi pi-book" [model]="readItems" (onClick)="read(book.id, 'ngx')" severity="primary"/>
|
||||
@@ -462,19 +520,14 @@
|
||||
@if (book!.bookType !== 'PDF' && book!.bookType !== 'FB2') {
|
||||
<p-button label="Read" icon="pi pi-book" (onClick)="read(book?.metadata!.bookId, undefined)" severity="primary"/>
|
||||
}
|
||||
<p-button label="Shelf" icon="pi pi-folder" severity="secondary" outlined (onClick)="assignShelf(book.id)"></p-button>
|
||||
<p-button label="Shelf" icon="pi pi-folder" severity="info" outlined (onClick)="assignShelf(book.id)" class="mobile-icon-only"></p-button>
|
||||
@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) {
|
||||
<p-splitbutton label="Download" icon="pi pi-download" [model]="downloadItems" (onClick)="download(book)" severity="success" outlined/>
|
||||
<p-splitbutton label="Download" icon="pi pi-download" [model]="downloadItems" (onClick)="download(book)" severity="success" outlined class="mobile-icon-only"/>
|
||||
}
|
||||
} @else {
|
||||
<p-button label="Download" icon="pi pi-download" severity="success" outlined (onClick)="download(book)"></p-button>
|
||||
}
|
||||
}
|
||||
@if (userState.user!.permissions.canEmailBook || userState.user!.permissions.admin) {
|
||||
@if (emailMenuItems$ | async; as emailItems) {
|
||||
<p-splitbutton label="Quick Send" icon="pi pi-send" [model]="emailItems" (onClick)="quickSend(book!.id)" outlined severity="info"></p-splitbutton>
|
||||
<p-button label="Download" icon="pi pi-download" severity="success" outlined (onClick)="download(book)" class="mobile-icon-only"></p-button>
|
||||
}
|
||||
}
|
||||
@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">
|
||||
</p-splitbutton>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Book | null>;
|
||||
@@ -65,7 +68,6 @@ export class MetadataViewerComponent implements OnInit, OnChanges {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private dialogRef?: DynamicDialogRef;
|
||||
|
||||
emailMenuItems$!: Observable<MenuItem[]>;
|
||||
readMenuItems$!: Observable<MenuItem[]>;
|
||||
refreshMenuItems$!: Observable<MenuItem[]>;
|
||||
otherItems$!: Observable<MenuItem[]>;
|
||||
@@ -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,7 +164,10 @@ export class MetadataViewerComponent implements OnInit, OnChanges {
|
||||
|
||||
this.otherItems$ = this.book$.pipe(
|
||||
filter((book): book is Book => book !== null),
|
||||
map((book): MenuItem[] => {
|
||||
switchMap(book =>
|
||||
this.userService.userState$.pipe(
|
||||
take(1),
|
||||
map(userState => {
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
label: 'Upload File',
|
||||
@@ -187,7 +183,31 @@ export class MetadataViewerComponent implements OnInit, OnChanges {
|
||||
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: () => {
|
||||
@@ -213,8 +233,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// Add delete additional files menu if there are any additional files
|
||||
if ((book.alternativeFormats && book.alternativeFormats.length > 0) ||
|
||||
@@ -260,6 +279,8 @@ export class MetadataViewerComponent implements OnInit, OnChanges {
|
||||
|
||||
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}` : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user