{ "version": 3, "sources": ["src/app/book-reader/_components/table-of-contents/table-of-contents.component.ts", "src/app/book-reader/_components/table-of-contents/table-of-contents.component.html", "src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts", "src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html", "src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.ts", "src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html", "src/app/book-reader/_components/book-reader/book-reader.component.ts", "src/app/book-reader/_components/book-reader/book-reader.component.html", "src/app/_routes/book-reader.router.module.ts"], "sourcesContent": ["import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';\r\nimport { BookChapterItem } from '../../_models/book-chapter-item';\r\nimport { NgIf, NgFor } from '@angular/common';\r\nimport {TranslocoDirective} from \"@ngneat/transloco\";\r\n\r\n@Component({\r\n selector: 'app-table-of-contents',\r\n templateUrl: './table-of-contents.component.html',\r\n styleUrls: ['./table-of-contents.component.scss'],\r\n changeDetection: ChangeDetectionStrategy.Default,\r\n standalone: true,\r\n imports: [NgIf, NgFor, TranslocoDirective]\r\n})\r\nexport class TableOfContentsComponent {\r\n\r\n @Input({required: true}) chapterId!: number;\r\n @Input({required: true}) pageNum!: number;\r\n @Input({required: true}) currentPageAnchor!: string;\r\n @Input() chapters:Array = [];\r\n\r\n @Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter();\r\n\r\n constructor() {}\r\n\r\n cleanIdSelector(id: string) {\r\n const tokens = id.split('/');\r\n if (tokens.length > 0) {\r\n return tokens[0];\r\n }\r\n return id;\r\n }\r\n\r\n loadChapterPage(pageNum: number, part: string) {\r\n this.loadChapter.emit({pageNum, part});\r\n }\r\n}\r\n", "\r\n
\r\n
\r\n {{t('no-data')}}\r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n
\r\n
\r\n", "import {\r\n ChangeDetectionStrategy, ChangeDetectorRef,\r\n Component,\r\n DestroyRef,\r\n ElementRef, EventEmitter, HostListener,\r\n inject,\r\n Input,\r\n OnInit, Output,\r\n} from '@angular/core';\r\nimport {CommonModule} from '@angular/common';\r\nimport {fromEvent, merge, of} from \"rxjs\";\r\nimport {catchError} from \"rxjs/operators\";\r\nimport {takeUntilDestroyed} from \"@angular/core/rxjs-interop\";\r\nimport {FormControl, FormGroup, ReactiveFormsModule, Validators} from \"@angular/forms\";\r\nimport {ReaderService} from \"../../../_services/reader.service\";\r\nimport {ToastrService} from \"ngx-toastr\";\r\nimport {translate, TranslocoDirective} from \"@ngneat/transloco\";\r\nimport {KEY_CODES} from \"../../../shared/_services/utility.service\";\r\n\r\nenum BookLineOverlayMode {\r\n None = 0,\r\n Bookmark = 1\r\n}\r\n\r\n@Component({\r\n selector: 'app-book-line-overlay',\r\n standalone: true,\r\n imports: [CommonModule, ReactiveFormsModule, TranslocoDirective],\r\n templateUrl: './book-line-overlay.component.html',\r\n styleUrls: ['./book-line-overlay.component.scss'],\r\n changeDetection: ChangeDetectionStrategy.OnPush\r\n})\r\nexport class BookLineOverlayComponent implements OnInit {\r\n @Input({required: true}) libraryId!: number;\r\n @Input({required: true}) seriesId!: number;\r\n @Input({required: true}) volumeId!: number;\r\n @Input({required: true}) chapterId!: number;\r\n @Input({required: true}) pageNumber: number = 0;\r\n @Input({required: true}) parent: ElementRef | undefined;\r\n @Output() refreshToC: EventEmitter = new EventEmitter();\r\n @Output() isOpen: EventEmitter = new EventEmitter(false);\r\n\r\n xPath: string = '';\r\n selectedText: string = '';\r\n mode: BookLineOverlayMode = BookLineOverlayMode.None;\r\n bookmarkForm: FormGroup = new FormGroup({\r\n name: new FormControl('', [Validators.required]),\r\n });\r\n\r\n private readonly destroyRef = inject(DestroyRef);\r\n private readonly cdRef = inject(ChangeDetectorRef);\r\n private readonly readerService = inject(ReaderService);\r\n\r\n get BookLineOverlayMode() { return BookLineOverlayMode; }\r\n constructor(private elementRef: ElementRef, private toastr: ToastrService) {}\r\n\r\n @HostListener('window:keydown', ['$event'])\r\n handleKeyPress(event: KeyboardEvent) {\r\n if (event.key === KEY_CODES.ESC_KEY) {\r\n this.reset();\r\n this.cdRef.markForCheck();\r\n event.stopPropagation();\r\n event.preventDefault();\r\n return;\r\n }\r\n }\r\n\r\n\r\n ngOnInit() {\r\n if (this.parent) {\r\n\r\n const mouseUp$ = fromEvent(this.parent.nativeElement, 'mouseup');\r\n const touchEnd$ = fromEvent(this.parent.nativeElement, 'touchend');\r\n\r\n merge(mouseUp$, touchEnd$)\r\n .pipe(takeUntilDestroyed(this.destroyRef))\r\n .subscribe((event: MouseEvent | TouchEvent) => {\r\n this.handleEvent(event);\r\n });\r\n }\r\n }\r\n\r\n handleEvent(event: MouseEvent | TouchEvent) {\r\n const selection = window.getSelection();\r\n if (!event.target) return;\r\n\r\n\r\n\r\n if ((selection === null || selection === undefined || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) {\r\n if (this.selectedText !== '') {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n }\r\n\r\n const isRightClick = (event instanceof MouseEvent && event.button === 2);\r\n if (!isRightClick) {\r\n this.reset();\r\n }\r\n\r\n return;\r\n }\r\n\r\n this.selectedText = selection ? selection.toString().trim() : '';\r\n\r\n if (this.selectedText.length > 0 && this.mode === BookLineOverlayMode.None) {\r\n this.xPath = this.readerService.getXPathTo(event.target);\r\n if (this.xPath !== '') {\r\n this.xPath = '//' + this.xPath;\r\n }\r\n\r\n this.isOpen.emit(true);\r\n event.preventDefault();\r\n event.stopPropagation();\r\n }\r\n this.cdRef.markForCheck();\r\n }\r\n\r\n switchMode(mode: BookLineOverlayMode) {\r\n this.mode = mode;\r\n this.cdRef.markForCheck();\r\n if (this.mode === BookLineOverlayMode.Bookmark) {\r\n this.bookmarkForm.get('name')?.setValue(this.selectedText);\r\n this.focusOnBookmarkInput();\r\n }\r\n }\r\n\r\n createPTOC() {\r\n this.readerService.createPersonalToC(this.libraryId, this.seriesId, this.volumeId, this.chapterId, this.pageNumber,\r\n this.bookmarkForm.get('name')?.value, this.xPath).pipe(catchError(err => {\r\n this.focusOnBookmarkInput();\r\n return of();\r\n })).subscribe(() => {\r\n this.reset();\r\n this.refreshToC.emit();\r\n this.cdRef.markForCheck();\r\n });\r\n }\r\n\r\n focusOnBookmarkInput() {\r\n if (this.mode !== BookLineOverlayMode.Bookmark) return;\r\n setTimeout(() => this.elementRef.nativeElement.querySelector('#bookmark-name')?.focus(), 10);\r\n }\r\n\r\n reset() {\r\n this.bookmarkForm.reset();\r\n this.mode = BookLineOverlayMode.None;\r\n this.xPath = '';\r\n this.selectedText = '';\r\n const selection = window.getSelection();\r\n if (selection) {\r\n selection.removeAllRanges();\r\n }\r\n this.isOpen.emit(false);\r\n this.cdRef.markForCheck();\r\n }\r\n\r\n async copy() {\r\n const selection = window.getSelection();\r\n if (selection) {\r\n await navigator.clipboard.writeText(selection.toString());\r\n this.toastr.info(translate('toasts.copied-to-clipboard'));\r\n }\r\n this.reset();\r\n }\r\n\r\n\r\n}\r\n", "\r\n
0 || mode !== BookLineOverlayMode.None\">\r\n\r\n
\r\n \r\n \r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n {{t('required-field')}}\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n\r\n\r\n
\r\n
\r\n", "import {\r\n ChangeDetectionStrategy,\r\n ChangeDetectorRef,\r\n Component, DestroyRef, EventEmitter,\r\n Inject,\r\n inject,\r\n Input,\r\n OnInit,\r\n Output\r\n} from '@angular/core';\r\nimport {CommonModule, DOCUMENT} from '@angular/common';\r\nimport {ReaderService} from \"../../../_services/reader.service\";\r\nimport {PersonalToC} from \"../../../_models/readers/personal-toc\";\r\nimport {takeUntilDestroyed} from \"@angular/core/rxjs-interop\";\r\nimport {NgbTooltip} from \"@ng-bootstrap/ng-bootstrap\";\r\nimport {TranslocoDirective} from \"@ngneat/transloco\";\r\n\r\nexport interface PersonalToCEvent {\r\n pageNum: number;\r\n scrollPart: string | undefined;\r\n}\r\n\r\n@Component({\r\n selector: 'app-personal-table-of-contents',\r\n standalone: true,\r\n imports: [CommonModule, NgbTooltip, TranslocoDirective],\r\n templateUrl: './personal-table-of-contents.component.html',\r\n styleUrls: ['./personal-table-of-contents.component.scss'],\r\n changeDetection: ChangeDetectionStrategy.OnPush\r\n})\r\nexport class PersonalTableOfContentsComponent implements OnInit {\r\n\r\n @Input({required: true}) chapterId!: number;\r\n @Input({required: true}) pageNum: number = 0;\r\n @Input({required: true}) tocRefresh!: EventEmitter;\r\n @Output() loadChapter: EventEmitter = new EventEmitter();\r\n\r\n private readonly readerService = inject(ReaderService);\r\n private readonly cdRef = inject(ChangeDetectorRef);\r\n private readonly destroyRef = inject(DestroyRef);\r\n\r\n\r\n bookmarks: {[key: number]: Array} = [];\r\n\r\n get Pages() {\r\n return Object.keys(this.bookmarks).map(p => parseInt(p, 10));\r\n }\r\n\r\n constructor(@Inject(DOCUMENT) private document: Document) {}\r\n\r\n ngOnInit() {\r\n this.tocRefresh.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {\r\n this.load();\r\n });\r\n\r\n this.load();\r\n }\r\n\r\n load() {\r\n this.readerService.getPersonalToC(this.chapterId).subscribe(res => {\r\n res.forEach(t => {\r\n if (!this.bookmarks.hasOwnProperty(t.pageNumber)) {\r\n this.bookmarks[t.pageNumber] = [];\r\n }\r\n this.bookmarks[t.pageNumber].push(t);\r\n })\r\n this.cdRef.markForCheck();\r\n });\r\n }\r\n\r\n loadChapterPage(pageNum: number, scrollPart: string | undefined) {\r\n this.loadChapter.emit({pageNum, scrollPart});\r\n }\r\n\r\n removeBookmark(bookmark: PersonalToC) {\r\n this.readerService.removePersonalToc(bookmark.chapterId, bookmark.pageNumber, bookmark.title).subscribe(() => {\r\n this.bookmarks[bookmark.pageNumber] = this.bookmarks[bookmark.pageNumber].filter(t => t.title != bookmark.title);\r\n\r\n if (this.bookmarks[bookmark.pageNumber].length === 0) {\r\n delete this.bookmarks[bookmark.pageNumber];\r\n }\r\n this.cdRef.markForCheck();\r\n });\r\n }\r\n\r\n}\r\n", "\r\n
\r\n
\r\n {{t('no-data')}}\r\n
\r\n
    \r\n
  • \r\n {{t('page', {value: page})}}\r\n
      \r\n
    • \r\n {{bookmark.title}}\r\n \r\n
    • \r\n
    \r\n
  • \r\n
\r\n
\r\n
\r\n", "import {\r\n AfterViewInit,\r\n ChangeDetectionStrategy,\r\n ChangeDetectorRef,\r\n Component, DestroyRef,\r\n ElementRef, EventEmitter,\r\n HostListener,\r\n inject,\r\n Inject,\r\n OnDestroy,\r\n OnInit,\r\n Renderer2,\r\n RendererStyleFlags2,\r\n ViewChild\r\n} from '@angular/core';\r\nimport { DOCUMENT, NgTemplateOutlet, NgIf, NgStyle, NgClass } from '@angular/common';\r\nimport { ActivatedRoute, Router } from '@angular/router';\r\nimport { ToastrService } from 'ngx-toastr';\r\nimport { forkJoin, fromEvent, of } from 'rxjs';\r\nimport {catchError, debounceTime, distinctUntilChanged, map, take, tap} from 'rxjs/operators';\r\nimport { Chapter } from 'src/app/_models/chapter';\r\nimport { AccountService } from 'src/app/_services/account.service';\r\nimport { NavService } from 'src/app/_services/nav.service';\r\nimport { CHAPTER_ID_DOESNT_EXIST, CHAPTER_ID_NOT_FETCHED, ReaderService } from 'src/app/_services/reader.service';\r\nimport { SeriesService } from 'src/app/_services/series.service';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { BookService } from '../../_services/book.service';\r\nimport { KEY_CODES, UtilityService } from 'src/app/shared/_services/utility.service';\r\nimport { BookChapterItem } from '../../_models/book-chapter-item';\r\nimport { animate, state, style, transition, trigger } from '@angular/animations';\r\nimport { Stack } from 'src/app/shared/data-structures/stack';\r\nimport { MemberService } from 'src/app/_services/member.service';\r\nimport { ReadingDirection } from 'src/app/_models/preferences/reading-direction';\r\nimport {WritingStyle} from \"../../../_models/preferences/writing-style\";\r\nimport { MangaFormat } from 'src/app/_models/manga-format';\r\nimport { LibraryService } from 'src/app/_services/library.service';\r\nimport { LibraryType } from 'src/app/_models/library/library';\r\nimport { BookTheme } from 'src/app/_models/preferences/book-theme';\r\nimport { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode';\r\nimport { PageStyle, ReaderSettingsComponent } from '../reader-settings/reader-settings.component';\r\nimport { User } from 'src/app/_models/user';\r\nimport { ThemeService } from 'src/app/_services/theme.service';\r\nimport { ScrollService } from 'src/app/_services/scroll.service';\r\nimport { PAGING_DIRECTION } from 'src/app/manga-reader/_models/reader-enums';\r\nimport {takeUntilDestroyed} from \"@angular/core/rxjs-interop\";\r\nimport { TableOfContentsComponent } from '../table-of-contents/table-of-contents.component';\r\nimport { NgbProgressbar, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgbNavOutlet, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';\r\nimport { DrawerComponent } from '../../../shared/drawer/drawer.component';\r\nimport {BookLineOverlayComponent} from \"../book-line-overlay/book-line-overlay.component\";\r\nimport {\r\n PersonalTableOfContentsComponent,\r\n PersonalToCEvent\r\n} from \"../personal-table-of-contents/personal-table-of-contents.component\";\r\nimport {translate, TranslocoDirective} from \"@ngneat/transloco\";\r\n\r\n\r\nenum TabID {\r\n Settings = 1,\r\n TableOfContents = 2,\r\n PersonalTableOfContents = 3\r\n}\r\n\r\n\r\ninterface HistoryPoint {\r\n /**\r\n * Page Number\r\n */\r\n page: number;\r\n /**\r\n * XPath to scroll to\r\n */\r\n scrollPart: string;\r\n}\r\n\r\nconst TOP_OFFSET = -50 * 1.5; // px the sticky header takes up // TODO: Do I need this or can I change it with new fixed top height\r\n\r\nconst COLUMN_GAP = 20; // px\r\n/**\r\n * Styles that should be applied on the top level book-content tag\r\n */\r\nconst pageLevelStyles = ['margin-left', 'margin-right', 'font-size'];\r\n/**\r\n * Styles that should be applied on every element within book-content tag\r\n */\r\nconst elementLevelStyles = ['line-height', 'font-family'];\r\n\r\n@Component({\r\n selector: 'app-book-reader',\r\n templateUrl: './book-reader.component.html',\r\n styleUrls: ['./book-reader.component.scss'],\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n animations: [\r\n trigger('isLoading', [\r\n state('false', style({ opacity: 1 })),\r\n state('true', style({ opacity: 0 })),\r\n transition('false <=> true', animate('200ms'))\r\n ]),\r\n trigger('fade', [\r\n state('true', style({ opacity: 0 })),\r\n state('false', style({ opacity: 0.5 })),\r\n transition('false <=> true', animate('4000ms'))\r\n ])\r\n ],\r\n standalone: true,\r\n imports: [NgTemplateOutlet, DrawerComponent, NgIf, NgbProgressbar, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink,\r\n NgbNavContent, ReaderSettingsComponent, TableOfContentsComponent, NgbNavOutlet, NgStyle, NgClass, NgbTooltip,\r\n BookLineOverlayComponent, PersonalTableOfContentsComponent, TranslocoDirective]\r\n})\r\nexport class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {\r\n\r\n private readonly route = inject(ActivatedRoute);\r\n private readonly router = inject(Router);\r\n private readonly accountService = inject(AccountService);\r\n private readonly seriesService = inject(SeriesService);\r\n private readonly readerService = inject(ReaderService);\r\n private readonly renderer = inject(Renderer2);\r\n private readonly navService = inject(NavService);\r\n private readonly toastr = inject(ToastrService);\r\n private readonly domSanitizer = inject(DomSanitizer);\r\n private readonly bookService = inject(BookService);\r\n private readonly memberService = inject(MemberService);\r\n private readonly scrollService = inject(ScrollService);\r\n private readonly utilityService = inject(UtilityService);\r\n private readonly libraryService = inject(LibraryService);\r\n private readonly themeService = inject(ThemeService);\r\n private readonly cdRef = inject(ChangeDetectorRef);\r\n\r\n protected readonly BookPageLayoutMode = BookPageLayoutMode;\r\n protected readonly WritingStyle = WritingStyle;\r\n protected readonly TabID = TabID;\r\n protected readonly ReadingDirection = ReadingDirection;\r\n protected readonly PAGING_DIRECTION = PAGING_DIRECTION;\r\n\r\n libraryId!: number;\r\n seriesId!: number;\r\n volumeId!: number;\r\n chapterId!: number;\r\n chapter!: Chapter;\r\n user!: User;\r\n\r\n /**\r\n * Reading List id. Defaults to -1.\r\n */\r\n readingListId: number = CHAPTER_ID_DOESNT_EXIST;\r\n\r\n /**\r\n * If this is true, no progress will be saved.\r\n */\r\n incognitoMode: boolean = false;\r\n\r\n /**\r\n * If this is true, chapters will be fetched in the order of a reading list, rather than natural series order.\r\n */\r\n readingListMode: boolean = false;\r\n\r\n /**\r\n * The actual pages from the epub, used for showing on table of contents. This must be here as we need access to it for scroll anchors\r\n */\r\n chapters: Array = [];\r\n /**\r\n * Current Page\r\n */\r\n pageNum = 0;\r\n /**\r\n * Max Pages\r\n */\r\n maxPages = 1;\r\n /**\r\n * This allows for exploration into different chapters\r\n */\r\n adhocPageHistory: Stack = new Stack();\r\n /**\r\n * A stack of the chapter ids we come across during continuous reading mode. When we traverse a boundary, we use this to avoid extra API calls.\r\n * @see Stack\r\n * TODO: See if continuousChaptersStack can be moved into reader service so we can reduce code duplication between readers (and also use ChapterInfo with it instead)\r\n */\r\n continuousChaptersStack: Stack = new Stack();\r\n /*\r\n * The current page only contains an image. This is used to determine if we should show the image in the center of the screen.\r\n */\r\n isSingleImagePage = false;\r\n /**\r\n * Belongs to the drawer component\r\n */\r\n activeTabId: TabID = TabID.Settings;\r\n /**\r\n * Sub Nav tab id\r\n */\r\n tocId: TabID = TabID.TableOfContents;\r\n /**\r\n * Belongs to drawer component\r\n */\r\n drawerOpen = false;\r\n /**\r\n * If the word/line overlay is open\r\n */\r\n isLineOverlayOpen = false;\r\n /**\r\n * If the action bar is visible\r\n */\r\n actionBarVisible = true;\r\n /**\r\n * Book reader setting that hides the menuing system\r\n */\r\n immersiveMode: boolean = false;\r\n /**\r\n * If we are loading from backend\r\n */\r\n isLoading = true;\r\n /**\r\n * Title of the book. Rendered in action bars\r\n */\r\n bookTitle: string = '';\r\n /**\r\n * The boolean that decides if the clickToPaginate overlay is visible or not.\r\n */\r\n clickToPaginateVisualOverlay = false;\r\n clickToPaginateVisualOverlayTimeout: any = undefined; // For animation\r\n clickToPaginateVisualOverlayTimeout2: any = undefined; // For kicking off animation, giving enough time to render html\r\n updateImageSizeTimeout: any = undefined;\r\n /**\r\n * This is the html we get from the server\r\n */\r\n page: SafeHtml | undefined = undefined;\r\n /**\r\n * Next Chapter Id. This is not guaranteed to be a valid ChapterId. Prefetched on page load (non-blocking).\r\n */\r\n nextChapterId: number = CHAPTER_ID_NOT_FETCHED;\r\n /**\r\n * Previous Chapter Id. This is not guaranteed to be a valid ChapterId. Prefetched on page load (non-blocking).\r\n */\r\n prevChapterId: number = CHAPTER_ID_NOT_FETCHED;\r\n /**\r\n * Is there a next chapter. If not, this will disable UI controls.\r\n */\r\n nextChapterDisabled: boolean = false;\r\n /**\r\n * Is there a previous chapter. If not, this will disable UI controls.\r\n */\r\n prevChapterDisabled: boolean = false;\r\n /**\r\n * Has the next chapter been prefetched. Prefetched means the backend will cache the files.\r\n */\r\n nextChapterPrefetched: boolean = false;\r\n /**\r\n * Has the previous chapter been prefetched. Prefetched means the backend will cache the files.\r\n */\r\n prevChapterPrefetched: boolean = false;\r\n /**\r\n * If the prev page allows a page change to occur.\r\n */\r\n prevPageDisabled = false;\r\n /**\r\n * If the next page allows a page change to occur.\r\n */\r\n nextPageDisabled = false;\r\n\r\n /**\r\n * Internal property used to capture all the different css properties to render on all elements. This is a cached version that is updated from reader-settings component\r\n */\r\n pageStyles!: PageStyle;\r\n\r\n /**\r\n * Offset for drawer and rendering canvas. Fixed to 62px.\r\n */\r\n topOffset: number = 38;\r\n /**\r\n * Used for showing/hiding bottom action bar. Calculates if there is enough scroll to show it.\r\n * Will hide if all content in book is absolute positioned\r\n */\r\n horizontalScrollbarNeeded = false;\r\n scrollbarNeeded = false;\r\n readingDirection: ReadingDirection = ReadingDirection.LeftToRight;\r\n clickToPaginate = false;\r\n /**\r\n * Used solely for fullscreen to apply a hack\r\n */\r\n darkMode = true;\r\n /**\r\n * A anchors that map to the page number. When you click on one of these, we will load a given page up for the user.\r\n */\r\n pageAnchors: {[n: string]: number } = {};\r\n currentPageAnchor: string = '';\r\n /**\r\n * Last seen progress part path\r\n */\r\n lastSeenScrollPartPath: string = '';\r\n /**\r\n * Library Type used for rendering chapter or issue\r\n */\r\n libraryType: LibraryType = LibraryType.Book;\r\n /**\r\n * If the web browser is in fullscreen mode\r\n */\r\n isFullscreen: boolean = false;\r\n\r\n /**\r\n * How to render the page content\r\n */\r\n layoutMode: BookPageLayoutMode = BookPageLayoutMode.Default;\r\n\r\n /**\r\n * Width of the document (in non-column layout), used for column layout virtual paging\r\n */\r\n windowWidth: number = 0;\r\n windowHeight: number = 0;\r\n\r\n /**\r\n * used to track if a click is a drag or not, for opening menu\r\n */\r\n mousePosition = {\r\n x: 0,\r\n y: 0\r\n };\r\n\r\n /**\r\n * Used to keep track of direction user is paging, to help with virtual paging on column layout\r\n */\r\n pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;\r\n\r\n writingStyle: WritingStyle = WritingStyle.Horizontal;\r\n\r\n\r\n /**\r\n * When the user is highlighting something, then we remove pagination\r\n */\r\n hidePagination = false;\r\n\r\n /**\r\n * Used to refresh the Personal PoC\r\n */\r\n refreshPToC: EventEmitter = new EventEmitter();\r\n\r\n private readonly destroyRef = inject(DestroyRef);\r\n\r\n @ViewChild('bookContainer', {static: false}) bookContainerElemRef!: ElementRef;\r\n /**\r\n * book-content class\r\n */\r\n @ViewChild('readingHtml', {static: false}) bookContentElemRef!: ElementRef;\r\n @ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef;\r\n @ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef;\r\n @ViewChild('reader', {static: false}) reader!: ElementRef;\r\n\r\n /**\r\n * Disables the Left most button\r\n */\r\n get IsPrevDisabled(): boolean {\r\n if (this.readingDirection === ReadingDirection.LeftToRight) {\r\n // Acting as Previous button\r\n return this.isPrevPageDisabled();\r\n }\r\n\r\n // Acting as a Next button\r\n return this.isNextPageDisabled();\r\n }\r\n\r\n get IsNextDisabled(): boolean {\r\n if (this.readingDirection === ReadingDirection.LeftToRight) {\r\n // Acting as Next button\r\n return this.isNextPageDisabled();\r\n }\r\n // Acting as Previous button\r\n return this.isPrevPageDisabled();\r\n }\r\n\r\n isNextPageDisabled() {\r\n const [currentVirtualPage, totalVirtualPages, _] = this.getVirtualPage();\r\n const condition = (this.nextPageDisabled || this.nextChapterId === CHAPTER_ID_DOESNT_EXIST) && this.pageNum + 1 > this.maxPages - 1;\r\n if (this.layoutMode !== BookPageLayoutMode.Default) {\r\n return condition && currentVirtualPage === totalVirtualPages;\r\n }\r\n return condition;\r\n }\r\n\r\n isPrevPageDisabled() {\r\n const [currentVirtualPage,,] = this.getVirtualPage();\r\n const condition = (this.prevPageDisabled || this.prevChapterId === CHAPTER_ID_DOESNT_EXIST) && this.pageNum === 0;\r\n if (this.layoutMode !== BookPageLayoutMode.Default) {\r\n return condition && currentVirtualPage === 0;\r\n }\r\n return condition;\r\n }\r\n\r\n /**\r\n * Determines if we show >> or >\r\n */\r\n get IsNextChapter(): boolean {\r\n if (this.layoutMode === BookPageLayoutMode.Default) {\r\n return this.pageNum + 1 >= this.maxPages;\r\n }\r\n\r\n const [currentVirtualPage, totalVirtualPages, _] = this.getVirtualPage();\r\n if (this.bookContentElemRef == null) return this.pageNum + 1 >= this.maxPages;\r\n\r\n return this.pageNum + 1 >= this.maxPages && (currentVirtualPage === totalVirtualPages);\r\n }\r\n /**\r\n * Determines if we show << or <\r\n */\r\n get IsPrevChapter(): boolean {\r\n if (this.layoutMode === BookPageLayoutMode.Default) {\r\n return this.pageNum === 0;\r\n }\r\n\r\n const [currentVirtualPage,,] = this.getVirtualPage();\r\n if (this.bookContentElemRef == null) return this.pageNum + 1 >= this.maxPages;\r\n\r\n return this.pageNum === 0 && (currentVirtualPage === 0);\r\n }\r\n\r\n get ColumnWidth() {\r\n const base = this.writingStyle === WritingStyle.Vertical ? this.windowHeight : this.windowWidth;\r\n switch (this.layoutMode) {\r\n case BookPageLayoutMode.Default:\r\n return 'unset';\r\n case BookPageLayoutMode.Column1:\r\n return ((base / 2) - 4) + 'px';\r\n case BookPageLayoutMode.Column2:\r\n return (base / 4) + 'px';\r\n default:\r\n return 'unset';\r\n }\r\n }\r\n\r\n get ColumnHeight() {\r\n if (this.layoutMode !== BookPageLayoutMode.Default || this.writingStyle === WritingStyle.Vertical) {\r\n // Take the height after page loads, subtract the top/bottom bar\r\n const height = this.windowHeight - (this.topOffset * 2);\r\n return height + 'px';\r\n }\r\n return 'unset';\r\n }\r\n\r\n get VerticalBookContentWidth() {\r\n if (this.layoutMode !== BookPageLayoutMode.Default && this.writingStyle !== WritingStyle.Horizontal ) {\r\n const width = this.getVerticalPageWidth()\r\n return width + 'px';\r\n }\r\n return '';\r\n }\r\n\r\n get ColumnLayout() {\r\n switch (this.layoutMode) {\r\n case BookPageLayoutMode.Default:\r\n return '';\r\n case BookPageLayoutMode.Column1:\r\n return 'column-layout-1';\r\n case BookPageLayoutMode.Column2:\r\n return 'column-layout-2';\r\n }\r\n }\r\n\r\n get WritingStyleClass() {\r\n switch (this.writingStyle) {\r\n case WritingStyle.Horizontal:\r\n return '';\r\n case WritingStyle.Vertical:\r\n return 'writing-style-vertical';\r\n }\r\n }\r\n\r\n get PageWidthForPagination() {\r\n if (this.layoutMode === BookPageLayoutMode.Default && this.writingStyle === WritingStyle.Vertical && this.horizontalScrollbarNeeded) {\r\n return 'unset';\r\n }\r\n return '100%'\r\n }\r\n\r\n get PageHeightForPagination() {\r\n if (this.layoutMode === BookPageLayoutMode.Default) {\r\n // if the book content is less than the height of the container, override and return height of container for pagination area\r\n if (this.bookContainerElemRef?.nativeElement?.clientHeight > this.bookContentElemRef?.nativeElement?.clientHeight) {\r\n return (this.bookContainerElemRef?.nativeElement?.clientHeight || 0) + 'px';\r\n }\r\n\r\n return (this.bookContentElemRef?.nativeElement?.scrollHeight || 0) - ((this.topOffset * (this.immersiveMode ? 0 : 1)) * 2) + 'px';\r\n }\r\n\r\n if (this.immersiveMode) return this.windowHeight + 'px';\r\n return (this.windowHeight) - (this.topOffset * 2) + 'px';\r\n }\r\n\r\n constructor(@Inject(DOCUMENT) private document: Document) {\r\n this.navService.hideNavBar();\r\n this.themeService.clearThemes();\r\n this.navService.hideSideNav();\r\n this.cdRef.markForCheck();\r\n }\r\n\r\n /**\r\n * After the page has loaded, setup the scroll handler. The scroll handler has 2 parts. One is if there are page anchors setup (aka page anchor elements linked with the\r\n * table of content) then we calculate what has already been reached and grab the last reached one to save progress. If page anchors aren't setup (toc missing), then try to save progress\r\n * based on the last seen scroll part (xpath).\r\n */\r\n ngAfterViewInit() {\r\n // check scroll offset and if offset is after any of the \"id\" markers, save progress\r\n fromEvent(this.reader.nativeElement, 'scroll')\r\n .pipe(\r\n debounceTime(200),\r\n takeUntilDestroyed(this.destroyRef))\r\n .subscribe((event) => {\r\n if (this.isLoading) return;\r\n\r\n this.handleScrollEvent();\r\n });\r\n\r\n fromEvent(this.bookContainerElemRef.nativeElement, 'mousemove')\r\n .pipe(\r\n takeUntilDestroyed(this.destroyRef),\r\n distinctUntilChanged(),\r\n tap((e) => {\r\n const selection = window.getSelection();\r\n this.hidePagination = selection !== null && selection.toString().trim() !== '';\r\n this.cdRef.markForCheck();\r\n })\r\n )\r\n .subscribe();\r\n\r\n fromEvent(this.bookContainerElemRef.nativeElement, 'mouseup')\r\n .pipe(\r\n takeUntilDestroyed(this.destroyRef),\r\n distinctUntilChanged(),\r\n tap((e) => {\r\n this.hidePagination = false;\r\n this.cdRef.markForCheck();\r\n })\r\n )\r\n .subscribe();\r\n\r\n }\r\n\r\n handleScrollEvent() {\r\n // Highlight the current chapter we are on\r\n if (Object.keys(this.pageAnchors).length !== 0) {\r\n // get the height of the document, so we can capture markers that are halfway on the document viewport\r\n const verticalOffset = this.reader.nativeElement?.scrollTop || (this.scrollService.scrollPosition + (this.document.body.offsetHeight / 2));\r\n\r\n const alreadyReached = Object.values(this.pageAnchors).filter((i: number) => i <= verticalOffset);\r\n if (alreadyReached.length > 0) {\r\n this.currentPageAnchor = Object.keys(this.pageAnchors)[alreadyReached.length - 1];\r\n } else {\r\n this.currentPageAnchor = '';\r\n }\r\n\r\n this.cdRef.markForCheck();\r\n }\r\n\r\n // Find the element that is on screen to bookmark against\r\n const xpath: string | null | undefined = this.getFirstVisibleElementXPath();\r\n if (xpath !== null && xpath !== undefined) this.lastSeenScrollPartPath = xpath;\r\n\r\n if (this.lastSeenScrollPartPath !== '') {\r\n this.saveProgress();\r\n }\r\n }\r\n\r\n saveProgress() {\r\n let tempPageNum = this.pageNum;\r\n if (this.pageNum == this.maxPages - 1) {\r\n tempPageNum = this.pageNum + 1;\r\n }\r\n\r\n if (!this.incognitoMode) {\r\n this.readerService.saveProgress(this.libraryId, this.seriesId, this.volumeId, this.chapterId, tempPageNum, this.lastSeenScrollPartPath).pipe(take(1)).subscribe(() => {/* No operation */});\r\n }\r\n }\r\n\r\n ngOnDestroy(): void {\r\n this.clearTimeout(this.clickToPaginateVisualOverlayTimeout);\r\n this.clearTimeout(this.clickToPaginateVisualOverlayTimeout2);\r\n\r\n this.readerService.disableWakeLock();\r\n\r\n this.themeService.clearBookTheme();\r\n\r\n this.themeService.currentTheme$.pipe(take(1)).subscribe(theme => {\r\n this.themeService.setTheme(theme.name);\r\n });\r\n\r\n this.navService.showNavBar();\r\n this.navService.showSideNav();\r\n }\r\n\r\n ngOnInit(): void {\r\n const libraryId = this.route.snapshot.paramMap.get('libraryId');\r\n const seriesId = this.route.snapshot.paramMap.get('seriesId');\r\n const chapterId = this.route.snapshot.paramMap.get('chapterId');\r\n\r\n if (libraryId === null || seriesId === null || chapterId === null) {\r\n this.router.navigateByUrl('/home');\r\n return;\r\n }\r\n\r\n\r\n this.libraryId = parseInt(libraryId, 10);\r\n this.seriesId = parseInt(seriesId, 10);\r\n this.chapterId = parseInt(chapterId, 10);\r\n this.incognitoMode = this.route.snapshot.queryParamMap.get('incognitoMode') === 'true';\r\n\r\n const readingListId = this.route.snapshot.queryParamMap.get('readingListId');\r\n if (readingListId != null) {\r\n this.readingListMode = true;\r\n this.readingListId = parseInt(readingListId, 10);\r\n }\r\n this.cdRef.markForCheck();\r\n\r\n\r\n this.memberService.hasReadingProgress(this.libraryId).pipe(take(1)).subscribe(hasProgress => {\r\n if (!hasProgress) {\r\n this.toggleDrawer();\r\n this.toastr.info(translate('toasts.book-settings-info'));\r\n }\r\n });\r\n\r\n this.accountService.currentUser$.pipe(take(1)).subscribe(user => {\r\n if (user) {\r\n this.user = user;\r\n this.init();\r\n }\r\n });\r\n }\r\n\r\n init() {\r\n this.nextChapterId = CHAPTER_ID_NOT_FETCHED;\r\n this.prevChapterId = CHAPTER_ID_NOT_FETCHED;\r\n this.nextChapterDisabled = false;\r\n this.prevChapterDisabled = false;\r\n this.nextChapterPrefetched = false;\r\n this.cdRef.markForCheck();\r\n\r\n\r\n this.bookService.getBookInfo(this.chapterId).subscribe(info => {\r\n if (this.readingListMode && info.seriesFormat !== MangaFormat.EPUB) {\r\n // Redirect to the manga reader.\r\n const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId);\r\n this.router.navigate(this.readerService.getNavigationArray(info.libraryId, info.seriesId, this.chapterId, info.seriesFormat), {queryParams: params});\r\n return;\r\n }\r\n\r\n this.bookTitle = info.bookTitle;\r\n this.cdRef.markForCheck();\r\n\r\n forkJoin({\r\n chapter: this.seriesService.getChapter(this.chapterId),\r\n progress: this.readerService.getProgress(this.chapterId),\r\n chapters: this.bookService.getBookChapters(this.chapterId),\r\n }).subscribe(results => {\r\n this.chapter = results.chapter;\r\n this.volumeId = results.chapter.volumeId;\r\n this.maxPages = results.chapter.pages;\r\n this.chapters = results.chapters;\r\n this.pageNum = results.progress.pageNum;\r\n this.cdRef.markForCheck();\r\n if (results.progress.bookScrollId) this.lastSeenScrollPartPath = results.progress.bookScrollId;\r\n\r\n this.continuousChaptersStack.push(this.chapterId);\r\n\r\n this.libraryService.getLibraryType(this.libraryId).pipe(take(1)).subscribe(type => {\r\n this.libraryType = type;\r\n });\r\n\r\n this.updateImageSizes();\r\n\r\n if (this.pageNum >= this.maxPages) {\r\n this.pageNum = this.maxPages - 1;\r\n this.cdRef.markForCheck();\r\n this.saveProgress();\r\n }\r\n\r\n this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {\r\n this.nextChapterId = chapterId;\r\n if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {\r\n this.nextChapterDisabled = true;\r\n this.nextChapterPrefetched = true;\r\n this.cdRef.markForCheck();\r\n return;\r\n }\r\n this.setPageNum(this.pageNum);\r\n });\r\n this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {\r\n this.prevChapterId = chapterId;\r\n if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {\r\n this.prevChapterDisabled = true;\r\n this.prevChapterPrefetched = true; // If there is no prev chapter, then mark it as prefetched\r\n this.cdRef.markForCheck();\r\n return;\r\n }\r\n this.setPageNum(this.pageNum);\r\n });\r\n\r\n // Check if user progress has part, if so load it so we scroll to it\r\n this.loadPage(results.progress.bookScrollId || undefined);\r\n this.readerService.enableWakeLock(this.reader.nativeElement);\r\n }, () => {\r\n setTimeout(() => {\r\n this.closeReader();\r\n }, 200);\r\n });\r\n });\r\n }\r\n\r\n @HostListener('window:resize', ['$event'])\r\n @HostListener('window:orientationchange', ['$event'])\r\n onResize(){\r\n // Update the window Height\r\n this.updateWidthAndHeightCalcs();\r\n this.updateImageSizes();\r\n const resumeElement = this.getFirstVisibleElementXPath();\r\n if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) {\r\n this.scrollTo(resumeElement); // This works pretty well, but not perfect\r\n }\r\n }\r\n\r\n @HostListener('window:keydown', ['$event'])\r\n handleKeyPress(event: KeyboardEvent) {\r\n const activeElement = document.activeElement as HTMLElement;\r\n const isInputFocused = activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA';\r\n if (isInputFocused) return;\r\n\r\n if (event.key === KEY_CODES.RIGHT_ARROW) {\r\n this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS);\r\n } else if (event.key === KEY_CODES.LEFT_ARROW) {\r\n this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD);\r\n } else if (event.key === KEY_CODES.ESC_KEY) {\r\n const isHighlighting = window.getSelection()?.toString() != '';\r\n if (isHighlighting) return;\r\n this.closeReader();\r\n } else if (event.key === KEY_CODES.SPACE) {\r\n this.toggleDrawer();\r\n event.stopPropagation();\r\n event.preventDefault();\r\n } else if (event.key === KEY_CODES.G) {\r\n this.goToPage();\r\n } else if (event.key === KEY_CODES.F) {\r\n this.toggleFullscreen()\r\n }\r\n }\r\n\r\n onWheel(event: WheelEvent) {\r\n // This allows the user to scroll the page horizontally without holding shift\r\n if (this.layoutMode !== BookPageLayoutMode.Default || this.writingStyle !== WritingStyle.Vertical) {\r\n return;\r\n }\r\n if (event.deltaY !== 0) {\r\n event.preventDefault()\r\n this.scrollService.scrollToX( event.deltaY + this.reader.nativeElement.scrollLeft, this.reader.nativeElement);\r\n }\r\n}\r\n\r\n closeReader() {\r\n this.readerService.closeReader(this.readingListMode, this.readingListId);\r\n }\r\n\r\n sortElements(a: Element, b: Element) {\r\n const aTop = a.getBoundingClientRect().top;\r\n const bTop = b.getBoundingClientRect().top;\r\n if (aTop < bTop) {\r\n return -1;\r\n }\r\n if (aTop > bTop) {\r\n return 1;\r\n }\r\n\r\n return 0;\r\n }\r\n\r\n loadNextChapter() {\r\n if (this.nextPageDisabled) { return; }\r\n this.isLoading = true;\r\n if (this.nextChapterId === CHAPTER_ID_NOT_FETCHED || this.nextChapterId === this.chapterId) {\r\n this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {\r\n this.nextChapterId = chapterId;\r\n this.loadChapter(chapterId, 'Next');\r\n });\r\n } else {\r\n this.loadChapter(this.nextChapterId, 'Next');\r\n }\r\n }\r\n\r\n loadPrevChapter() {\r\n if (this.prevPageDisabled) { return; }\r\n\r\n this.isLoading = true;\r\n this.cdRef.markForCheck();\r\n this.continuousChaptersStack.pop();\r\n const prevChapter = this.continuousChaptersStack.peek();\r\n if (prevChapter != this.chapterId) {\r\n if (prevChapter !== undefined) {\r\n this.chapterId = prevChapter;\r\n this.init();\r\n return;\r\n }\r\n }\r\n\r\n if (this.prevChapterPrefetched && this.prevChapterId === CHAPTER_ID_DOESNT_EXIST) {\r\n this.isLoading = false;\r\n this.cdRef.markForCheck();\r\n return;\r\n }\r\n\r\n if (this.prevChapterId === CHAPTER_ID_NOT_FETCHED || this.prevChapterId === this.chapterId && !this.prevChapterPrefetched) {\r\n this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {\r\n this.prevChapterId = chapterId;\r\n this.loadChapter(chapterId, 'Prev');\r\n });\r\n } else {\r\n this.loadChapter(this.prevChapterId, 'Prev');\r\n }\r\n }\r\n\r\n loadChapter(chapterId: number, direction: 'Next' | 'Prev') {\r\n if (chapterId >= 0) {\r\n this.chapterId = chapterId;\r\n this.continuousChaptersStack.push(chapterId);\r\n // Load chapter Id onto route but don't reload\r\n const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId);\r\n window.history.replaceState({}, '', newRoute);\r\n const msg = translate(direction === 'Next' ? 'toasts.load-next-chapter' : 'toasts.load-prev-chapter', {entity: this.utilityService.formatChapterName(this.libraryType).toLowerCase()});\r\n this.toastr.info(msg, '', {timeOut: 3000});\r\n this.cdRef.markForCheck();\r\n this.init();\r\n } else {\r\n // This will only happen if no actual chapter can be found\r\n const msg = translate(direction === 'Next' ? 'toasts.no-next-chapter' : 'toasts.no-prev-chapter', {entity: this.utilityService.formatChapterName(this.libraryType).toLowerCase()});\r\n this.toastr.warning(msg);\r\n this.isLoading = false;\r\n if (direction === 'Prev') {\r\n this.prevPageDisabled = true;\r\n } else {\r\n this.nextPageDisabled = true;\r\n }\r\n this.cdRef.markForCheck();\r\n }\r\n }\r\n\r\n loadChapterPage(event: {pageNum: number, part: string}) {\r\n this.setPageNum(event.pageNum);\r\n this.loadPage('id(\"' + event.part + '\")');\r\n }\r\n\r\n /**\r\n * From personal table of contents/bookmark\r\n * @param event\r\n */\r\n loadChapterPart(event: PersonalToCEvent) {\r\n this.setPageNum(event.pageNum);\r\n this.loadPage(event.scrollPart);\r\n }\r\n\r\n /**\r\n * Adds a click handler for any anchors that have 'kavita-page'. If 'kavita-page' present, changes page to kavita-page and optionally passes a part value\r\n * from 'kavita-part', which will cause the reader to scroll to the marker.\r\n */\r\n addLinkClickHandlers() {\r\n const links = this.readingSectionElemRef.nativeElement.querySelectorAll('a');\r\n links.forEach((link: any) => {\r\n link.addEventListener('click', (e: any) => {\r\n e.stopPropagation();\r\n let targetElem = e.target;\r\n if (e.target.nodeName !== 'A' && e.target.parentNode.nodeName === 'A') {\r\n // Certain combos like text can cause the target to be the sup tag and not the anchor\r\n targetElem = e.target.parentNode;\r\n }\r\n if (!targetElem.attributes.hasOwnProperty('kavita-page')) { return; }\r\n const page = parseInt(targetElem.attributes['kavita-page'].value, 10);\r\n if (this.adhocPageHistory.peek()?.page !== this.pageNum) {\r\n this.adhocPageHistory.push({page: this.pageNum, scrollPart: this.lastSeenScrollPartPath});\r\n }\r\n\r\n const partValue = targetElem.attributes.hasOwnProperty('kavita-part') ? targetElem.attributes['kavita-part'].value : undefined;\r\n if (partValue && page === this.pageNum) {\r\n this.scrollTo(targetElem.attributes['kavita-part'].value);\r\n return;\r\n }\r\n\r\n this.setPageNum(page);\r\n this.loadPage(partValue);\r\n });\r\n });\r\n }\r\n\r\n moveFocus() {\r\n const elems = this.document.getElementsByClassName('reading-section');\r\n if (elems.length > 0) {\r\n (elems[0] as HTMLDivElement).focus();\r\n }\r\n }\r\n\r\n\r\n promptForPage() {\r\n const question = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages - 1});\r\n const goToPageNum = window.prompt(question, '');\r\n if (goToPageNum === null || goToPageNum.trim().length === 0) { return null; }\r\n return goToPageNum;\r\n }\r\n\r\n goToPage(pageNum?: number) {\r\n let page = pageNum;\r\n if (pageNum === null || pageNum === undefined) {\r\n const goToPageNum = this.promptForPage();\r\n if (goToPageNum === null) { return; }\r\n page = parseInt(goToPageNum.trim(), 10);\r\n }\r\n\r\n if (page === undefined || this.pageNum === page) { return; }\r\n\r\n if (page > this.maxPages) {\r\n page = this.maxPages;\r\n } else if (page < 0) {\r\n page = 0;\r\n }\r\n\r\n if (!(page === 0 || page === this.maxPages - 1)) {\r\n page -= 1;\r\n }\r\n\r\n this.pageNum = page;\r\n this.loadPage();\r\n }\r\n\r\n\r\n\r\n\r\n loadPage(part?: string | undefined, scrollTop?: number | undefined) {\r\n this.isLoading = true;\r\n this.cdRef.markForCheck();\r\n\r\n this.bookService.getBookPage(this.chapterId, this.pageNum).pipe(take(1)).subscribe(content => {\r\n this.isSingleImagePage = this.checkSingleImagePage(content) // This needs be performed before we set this.page to avoid image jumping\r\n this.updateSingleImagePageStyles()\r\n this.page = this.domSanitizer.bypassSecurityTrustHtml(content); // PERF: Potential optimization to prefetch next/prev page and store in localStorage\r\n\r\n\r\n this.cdRef.markForCheck();\r\n\r\n setTimeout(() => {\r\n this.addLinkClickHandlers();\r\n this.updateReaderStyles(this.pageStyles);\r\n\r\n const imgs = this.readingSectionElemRef.nativeElement.querySelectorAll('img');\r\n if (imgs === null || imgs.length === 0) {\r\n this.setupPage(part, scrollTop);\r\n return;\r\n }\r\n\r\n Promise.all(Array.from(imgs)\r\n .filter(img => !img.complete)\r\n .map(img => new Promise(resolve => { img.onload = img.onerror = resolve; })))\r\n .then(() => {\r\n this.setupPage(part, scrollTop);\r\n this.updateImageSizes();\r\n });\r\n }, 10);\r\n });\r\n }\r\n\r\n /**\r\n * Updates the image properties to fit the current layout mode and screen size\r\n */\r\n updateImageSizes() {\r\n const isVerticalWritingStyle = this.writingStyle === WritingStyle.Vertical;\r\n const height = this.windowHeight - (this.topOffset * 2);\r\n let maxHeight = 'unset';\r\n let maxWidth = '';\r\n switch (this.layoutMode) {\r\n case BookPageLayoutMode.Default:\r\n if (isVerticalWritingStyle) {\r\n maxHeight = `${height}px`;\r\n } else {\r\n maxWidth = `${this.getVerticalPageWidth()}px`;\r\n }\r\n break\r\n\r\n case BookPageLayoutMode.Column1:\r\n maxHeight = `${height}px`;\r\n maxWidth = `${this.getVerticalPageWidth()}px`;\r\n break\r\n\r\n case BookPageLayoutMode.Column2:\r\n maxWidth = `${this.getVerticalPageWidth()}px`;\r\n if (isVerticalWritingStyle && !this.isSingleImagePage) {\r\n maxHeight = `${height / 2}px`;\r\n } else {\r\n maxHeight = `${height}px`;\r\n }\r\n break\r\n }\r\n this.document.documentElement.style.setProperty('--book-reader-content-max-height', maxHeight);\r\n this.document.documentElement.style.setProperty('--book-reader-content-max-width', maxWidth);\r\n\r\n }\r\n\r\n updateSingleImagePageStyles() {\r\n if (this.isSingleImagePage && this.layoutMode !== BookPageLayoutMode.Default) {\r\n this.document.documentElement.style.setProperty('--book-reader-content-position', 'absolute');\r\n this.document.documentElement.style.setProperty('--book-reader-content-top', '50%');\r\n this.document.documentElement.style.setProperty('--book-reader-content-left', '50%');\r\n this.document.documentElement.style.setProperty('--book-reader-content-transform', 'translate(-50%, -50%)');\r\n } else {\r\n this.document.documentElement.style.setProperty('--book-reader-content-position', '');\r\n this.document.documentElement.style.setProperty('--book-reader-content-top', '');\r\n this.document.documentElement.style.setProperty('--book-reader-content-left', '');\r\n this.document.documentElement.style.setProperty('--book-reader-content-transform', '');\r\n }\r\n }\r\n\r\n checkSingleImagePage(content: string) {\r\n // Exclude the style element from the HTML content as it messes up innerText\r\n const htmlContent = content.replace(/