Change Detection: On Push aka UI Smoothness (#1369)

* Updated Series Info Cards to use OnPush and hooked in progress events when we do a mark as read/unread on entities. These events update progress bars but also will now trigger a re-calculation on Read Time Left.

* Removed Library Card Component

* Refactored manga reader title and subtitle calculation to the backend.

* Coverted card actionables to onPush

* Series Card on push cleanup

* Updated edit collection tags for on push

* Update cover image chooser for on push

* Cleaned up carsouel reel

* Updated cover image to allow for uploading gif and webp files

* Bulk add to collection on push

* Updated bulk operation to use on push. Updated bulk operation to have mark as unread and read buttons explicitly. Updated so add to collection is visible and delete.

Fixed a bug where manage library component wasn't invoking the trackBy function

* Updating entity title for on push

* Removed file info component

* Updated Mange Library for on push

* Entity info cards on push

* List item on push

* Updated icon and title for on push and fixed some missing change detection on series detail

* Restricted the typeahead interface to simplify the design

* Edit Series Relation now shows a value in the dropdown for Parent relationships and disables the field.

* Updated edit series relation to focus on new typeahead when adding a new relationship

* Added some documentation and when Scanning a library, don't allow the user to enqueue the same job multiple times.

* Applied the No-enqueue if already enqueued logic to other tasks

* Library detail on push

* Updated events widget to onpush

* Card detail drawer on push. Card detail cover chooser now will show all chapter's covers for selection in cover chooser.

* Chapter metadata detail on push

* Removed Card Detail modal

* All collections on push

* Removed some comments

* Updated bulk selection to use an observable rather than function calls so new on push strategy works

* collection detail now uses on push and scroller is placed on correct element

* Updated library recommended to on push. Ensure that when mark as read occurs, the appropriate streams are refreshed.

* Updated library detail to on push

* Update metadata fiter to onpush. Bugs found and reported to Project

* person badge on push

* Read more on push

* Updated tag badge to on push

* User login on push

* When initing side nav, don't call an authenticated api until we are sure a user is logged in

* Updated splash container to on push

* Dashboard on push

* Side nav slight refactor around some api calls

* Cleaned up series card on push to use same cdRef naming convention

* Updated Static Files to use caching

* Added width and height to logo image

* shortcuts modal on push

* reading lists on push

* Reading list detail on push

* draggable ordered list on push

* Refactored reading-list-detail to use a new item which drastically reduces renders on operations

* series format on push

* circular loader on push

* Badge Expander on push

* update notification modal on push

* drawer on push

* Edit Series Modal on push

* reset password on push

* review series modal on push

* series metadata detail on push

* theme manager on push

* confirm reset password on push

* register on push

* confirm migration email on push

* confirm email on push

* add email to account migration on push

* user preferences on push. Made global settings default open

* edit series relation on push

* Fixed an edge case bug for next chapter where if the current volume had a single chapter of 1 and the next volume had a chapter number of 0, it would say there are no more chapters.

* Updated infinite scroller with on push support

* Moved some animations over to typeahead, not integrated yet.

* Manga reader is now on push

* Reader settings on push

* refactored how we close the book

* Updated table of contents for on push

* Updated book reader for on push. Fixed a bug where table of contents wasn't showing current page anchor due to a scroll calulation bug

* Small code tweak

* Icon and title on push

* nav header on push

* grouped typeahead on push

* typeahead on push and added a new trackby identity function to allow even faster rendering of big lists

* pdf reader on push

* code cleanup
This commit is contained in:
Joseph Milazzo 2022-07-11 11:57:07 -04:00 committed by GitHub
parent f5be0fac58
commit 4e49aa47ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
126 changed files with 1658 additions and 1674 deletions

View file

@ -14,4 +14,6 @@ export interface ChapterInfo {
isSpecial: boolean;
volumeId: number;
pages: number;
subtitle: string;
title: string;
}

View file

@ -1,5 +1,5 @@
import { DOCUMENT } from '@angular/common';
import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
import { BehaviorSubject, fromEvent, ReplaySubject, Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { ScrollService } from 'src/app/_services/scroll.service';
@ -39,7 +39,8 @@ const enum DEBUG_MODES {
@Component({
selector: 'app-infinite-scroller',
templateUrl: './infinite-scroller.component.html',
styleUrls: ['./infinite-scroller.component.scss']
styleUrls: ['./infinite-scroller.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
@ -150,11 +151,11 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
}
private readonly onDestroy = new Subject<void>();
constructor(private readerService: ReaderService, private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, private scrollService: ScrollService) {
constructor(private readerService: ReaderService, private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document, private scrollService: ScrollService,
private readonly cdRef: ChangeDetectorRef) {
// This will always exist at this point in time since this is used within manga reader
const reader = document.querySelector('.reader');
if (reader !== null) {
@ -165,6 +166,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void {
if (changes.hasOwnProperty('totalPages') && changes['totalPages'].previousValue != changes['totalPages'].currentValue) {
this.totalPages = changes['totalPages'].currentValue;
this.cdRef.markForCheck();
this.initWebtoonReader();
}
}
@ -211,6 +213,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
const image = document.querySelector('img[id^="page-' + page + '"]');
if (image) {
this.renderer.addClass(image, 'bookmark-effect');
setTimeout(() => {
this.renderer.removeClass(image, 'bookmark-effect');
}, 1000);
@ -222,6 +225,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.fullscreenToggled.pipe(takeUntil(this.onDestroy)).subscribe(isFullscreen => {
this.debugLog('[FullScreen] Fullscreen mode: ', isFullscreen);
this.isFullscreenMode = isFullscreen;
this.cdRef.markForCheck();
this.recalculateImageWidth();
this.initScrollHandler();
this.setPageNum(this.pageNum, true);
@ -232,6 +237,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
recalculateImageWidth() {
const [_, innerWidth] = this.getInnerDimensions();
this.webtoonImageWidth = innerWidth || document.body.clientWidth || document.documentElement.clientWidth;
this.cdRef.markForCheck();
}
getVerticalOffset() {
@ -256,9 +262,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
* @param event Scroll Event
*/
handleScrollEvent(event?: any) {
// Need a fullscreen handler here too
let verticalOffset = this.getVerticalOffset();
const verticalOffset = this.getVerticalOffset();
if (verticalOffset > this.prevScrollPosition) {
this.scrollingDirection = PAGING_DIRECTION.FORWARD;
@ -270,6 +274,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
if (this.isScrolling && this.currentPageElem != null && this.isElementVisible(this.currentPageElem)) {
this.debugLog('[Scroll] Image is visible from scroll, isScrolling is now false');
this.isScrolling = false;
this.cdRef.markForCheck();
}
if (!this.isScrolling) {
@ -282,11 +287,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.setPageNum(parseInt(midlineImages[0].getAttribute('page') || this.pageNum + '', 10));
}
}
// Check if we hit the last page
this.checkIfShouldTriggerContinuousReader();
}
getTotalHeight() {
@ -294,17 +297,18 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
document.querySelectorAll('img[id^="page-"]').forEach(img => totalHeight += img.getBoundingClientRect().height);
return Math.round(totalHeight);
}
getTotalScroll() {
if (this.isFullscreenMode) {
return this.readerElemRef.nativeElement.offsetHeight + this.readerElemRef.nativeElement.scrollTop;
}
return document.body.offsetHeight + document.body.scrollTop;
}
getScrollTop() {
if (this.isFullscreenMode) {
return this.readerElemRef.nativeElement.scrollTop;
}
return document.body.scrollTop;
}
@ -318,25 +322,32 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
// If we were at top but have started scrolling down past page 0, remove top spacer
if (this.atTop && this.pageNum > 0) {
this.atTop = false;
this.cdRef.markForCheck();
}
if (totalScroll === totalHeight && !this.atBottom) {
this.atBottom = true;
this.cdRef.markForCheck();
this.setPageNum(this.totalPages);
// Scroll user back to original location
this.previousScrollHeightMinusTop = this.getScrollTop();
requestAnimationFrame(() => document.body.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2));
requestAnimationFrame(() => {
document.body.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2);
this.cdRef.markForCheck();
});
} else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) {
// This if statement will fire once we scroll into the spacer at all
this.loadNextChapter.emit();
this.cdRef.markForCheck();
}
} else {
// < 5 because debug mode and FF (mobile) can report non 0, despite being at 0
if (this.getScrollTop() < 5 && this.pageNum === 0 && !this.atTop) {
this.atBottom = false;
this.atTop = true;
this.cdRef.markForCheck();
// Scroll user back to original location
this.previousScrollHeightMinusTop = document.body.scrollHeight - document.body.scrollTop;
@ -345,9 +356,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
} else if (this.getScrollTop() < 5 && this.pageNum === 0 && this.atTop) {
// If already at top, then we moving on
this.loadPrevChapter.emit();
this.cdRef.markForCheck();
}
}
}
/**
@ -376,9 +387,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.debugLog('[Visibility] Checking if Page ' + elem.getAttribute('id') + ' is visible');
// NOTE: This will say an element is visible if it is 1 px offscreen on top
var rect = elem.getBoundingClientRect();
const rect = elem.getBoundingClientRect();
let [innerHeight, innerWidth] = this.getInnerDimensions();
const [innerHeight, innerWidth] = this.getInnerDimensions();
return (rect.bottom >= 0 &&
rect.right >= 0 &&
@ -396,9 +407,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
shouldElementCountAsCurrentPage(elem: Element) {
if (elem === null || elem === undefined) { return false; }
var rect = elem.getBoundingClientRect();
const rect = elem.getBoundingClientRect();
let [innerHeight, innerWidth] = this.getInnerDimensions();
const [innerHeight, innerWidth] = this.getInnerDimensions();
if (rect.bottom >= 0 &&
@ -420,13 +431,14 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.webtoonImages.next([]);
this.atBottom = false;
this.checkIfShouldTriggerContinuousReader();
this.cdRef.markForCheck();
const [startingIndex, endingIndex] = this.calculatePrefetchIndecies();
this.debugLog('[INIT] Prefetching pages ' + startingIndex + ' to ' + endingIndex + '. Current page: ', this.pageNum);
for(let i = startingIndex; i <= endingIndex; i++) {
this.loadWebtoonImage(i);
}
this.cdRef.markForCheck();
}
/**
@ -460,9 +472,11 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.scrollToCurrentPage();
} else {
this.initFinished = true;
this.cdRef.markForCheck();
}
this.allImagesLoaded = true;
this.cdRef.markForCheck();
});
}
}
@ -496,6 +510,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
}
this.pageNum = pageNum;
this.pageNumberChange.emit(this.pageNum);
this.cdRef.markForCheck();
this.prefetchWebtoonImages();
@ -519,26 +534,27 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
// Update prevScrollPosition, so the next scroll event properly calculates direction
this.prevScrollPosition = this.currentPageElem.getBoundingClientRect().top;
this.isScrolling = true;
this.cdRef.markForCheck();
setTimeout(() => {
if (this.currentPageElem) {
this.debugLog('[Scroll] Scrolling to page ', this.pageNum);
this.currentPageElem.scrollIntoView({behavior: 'smooth'});
this.initFinished = true;
this.cdRef.markForCheck();
}
}, 600);
}
loadWebtoonImage(page: number) {
let data = this.webtoonImages.value;
if (this.imagesLoaded.hasOwnProperty(page)) {
this.debugLog('\t[PREFETCH] Skipping prefetch of ', page);
return;
}
this.debugLog('\t[PREFETCH] Prefetching ', page);
data = data.concat({src: this.urlProvider(page), page});
this.debugLog('\t[PREFETCH] Prefetching ', page);
const data = this.webtoonImages.value.concat({src: this.urlProvider(page), page});
data.sort((a: WebtoonImage, b: WebtoonImage) => {
if (a.page < b.page) { return -1; }
@ -547,6 +563,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
});
this.allImagesLoaded = false;
this.cdRef.markForCheck();
this.webtoonImages.next(data);
if (!this.imagesLoaded.hasOwnProperty(page)) {
@ -603,7 +620,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
}
prefetchWebtoonImages(pageNum: number = -1) {
if (pageNum === -1) {
pageNum = this.pageNum;
}
@ -621,6 +637,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
.map((img: any) => new Promise(resolve => { img.onload = img.onerror = resolve; })))
.then(() => {
this.allImagesLoaded = true;
this.cdRef.markForCheck();
});
}

View file

@ -1,5 +1,5 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { DOCUMENT, Location } from '@angular/common';
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { debounceTime, take, takeUntil } from 'rxjs/operators';
import { User } from '../_models/user';
@ -18,7 +18,6 @@ import { MemberService } from '../_services/member.service';
import { Stack } from '../shared/data-structures/stack';
import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { ChapterInfo } from './_models/chapter-info';
import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
import { layoutModes, pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
import { ReaderMode } from '../_models/preferences/reader-mode';
@ -43,6 +42,7 @@ const CLICK_OVERLAY_TIMEOUT = 3000;
selector: 'app-manga-reader',
templateUrl: './manga-reader.component.html',
styleUrls: ['./manga-reader.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('slideFromTop', [
state('in', style({ transform: 'translateY(0)'})),
@ -342,20 +342,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
);
}
@HostListener('window:resize', ['$event'])
@HostListener('window:orientationchange', ['$event'])
onResize() {
if (window.innerWidth > window.innerHeight) {
this.generalSettingsForm.get('layoutMode')?.enable();
return;
};
if (this.layoutMode === LayoutMode.Single || this.readerMode === ReaderMode.Webtoon) return;
this.generalSettingsForm.get('layoutMode')?.setValue(LayoutMode.Single);
this.generalSettingsForm.get('layoutMode')?.disable();
this.toastr.info('Layout mode switched to Single due to insufficient space to render double layout');
}
get CurrentPageBookmarked() {
return this.bookmarks.hasOwnProperty(this.pageNum);
}
@ -448,13 +434,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
public readerService: ReaderService, private location: Location,
private formBuilder: FormBuilder, private navService: NavService,
public readerService: ReaderService, private formBuilder: FormBuilder, private navService: NavService,
private toastr: ToastrService, private memberService: MemberService,
public utilityService: UtilityService, private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document, private modalService: NgbModal) {
@Inject(DOCUMENT) private document: Document, private modalService: NgbModal,
private readonly cdRef: ChangeDetectorRef) {
this.navService.hideNavBar();
this.navService.hideSideNav();
this.cdRef.markForCheck();
}
ngOnInit(): void {
@ -519,6 +506,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm.get('fittingOption')?.setValue(this.translateScalingOption(ScalingOption.FitToHeight));
this.generalSettingsForm.get('fittingOption')?.disable();
}
this.cdRef.markForCheck();
// Re-render the current page when we switch layouts
if (changeOccurred) {
@ -527,7 +515,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
});
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => {
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value; // TODO: Do I need cd check here?
const needsSplitting = this.isWideImage();
// If we need to split on a menu change, then we need to re-render.
if (needsSplitting) {
@ -551,9 +539,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.readerMode === ReaderMode.Webtoon) return;
if (this.readerMode === ReaderMode.LeftRight && this.FittingOption === FITTING_OPTION.HEIGHT) {
this.rightPaginationOffset = (this.readingArea.nativeElement.scrollLeft) * -1;
this.cdRef.markForCheck();
return;
}
this.rightPaginationOffset = 0;
this.cdRef.markForCheck();
});
if (this.canvas) {
@ -573,6 +563,22 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.goToPageEvent !== undefined) this.goToPageEvent.complete();
}
@HostListener('window:resize', ['$event'])
@HostListener('window:orientationchange', ['$event'])
onResize() {
if (window.innerWidth > window.innerHeight) {
this.generalSettingsForm.get('layoutMode')?.enable();
this.cdRef.markForCheck();
return;
};
if (this.layoutMode === LayoutMode.Single || this.readerMode === ReaderMode.Webtoon) return;
this.generalSettingsForm.get('layoutMode')?.setValue(LayoutMode.Single);
this.generalSettingsForm.get('layoutMode')?.disable();
this.toastr.info('Layout mode switched to Single due to insufficient space to render double layout');
this.cdRef.markForCheck();
}
@HostListener('window:keyup', ['$event'])
handleKeyPress(event: KeyboardEvent) {
switch (this.readerMode) {
@ -651,6 +657,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.pageNum = 0;
this.pagingDirection = PAGING_DIRECTION.FORWARD;
this.inSetup = true;
this.cdRef.markForCheck();
if (this.goToPageEvent) {
// There was a bug where goToPage was emitting old values into infinite scroller between chapter loads. We explicity clear it out between loads
@ -670,8 +677,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
newOptions.ceil = this.maxPages - 1; // We -1 so that the slider UI shows us hitting the end, since visually we +1 everything.
this.pageOptions = newOptions;
this.inSetup = false;
this.prevChapterDisabled = true;
this.nextChapterDisabled = true;
const images = [];
for (let i = 0; i < PREFETCH_PAGES + 2; i++) {
@ -691,8 +696,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
chapterInfo: this.readerService.getChapterInfo(this.chapterId),
bookmarks: this.readerService.getBookmarks(this.chapterId),
}).pipe(take(1)).subscribe(results => {
if (this.readingListMode && (results.chapterInfo.seriesFormat === MangaFormat.EPUB || results.chapterInfo.seriesFormat === MangaFormat.PDF)) {
// Redirect to the book reader.
const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId);
@ -718,7 +721,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.pageOptions = newOptions;
this.libraryType = results.chapterInfo.libraryType;
this.updateTitle(results.chapterInfo, this.libraryType);
this.title = results.chapterInfo.title;
this.subtitle = results.chapterInfo.subtitle;
this.inSetup = false;
@ -729,17 +733,20 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
results.bookmarks.forEach(bookmark => {
this.bookmarks[bookmark.page] = 1;
});
this.cdRef.markForCheck();
this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
this.nextChapterId = chapterId;
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
this.nextChapterDisabled = true;
this.cdRef.markForCheck();
}
});
this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
this.prevChapterId = chapterId;
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
this.prevChapterDisabled = true;
this.cdRef.markForCheck();
}
});
@ -760,43 +767,19 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
closeReader() {
this.readerService.closeReader(this.readingListMode, this.readingListId);
}
render() {
if (this.readerMode === ReaderMode.Webtoon) {
this.isLoading = false;
this.cdRef.markForCheck();
} else {
this.loadPage();
}
}
closeReader() {
if (this.readingListMode) {
this.router.navigateByUrl('lists/' + this.readingListId);
} else {
this.location.back();
}
}
updateTitle(chapterInfo: ChapterInfo, type: LibraryType) {
this.title = chapterInfo.seriesName;
if (chapterInfo.chapterTitle != null && chapterInfo.chapterTitle.length > 0) {
this.title += ' - ' + chapterInfo.chapterTitle;
}
// TODO: Move this to the backend
this.subtitle = '';
if (chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') {
this.subtitle = chapterInfo.fileName;
} else if (!chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') {
this.subtitle = this.utilityService.formatChapterName(type, true, true) + chapterInfo.chapterNumber;
} else {
this.subtitle = 'Volume ' + chapterInfo.volumeNumber;
if (chapterInfo.chapterNumber !== '0') {
this.subtitle += ' ' + this.utilityService.formatChapterName(type, true, true) + chapterInfo.chapterNumber;
}
}
}
translateScalingOption(option: ScalingOption) {
switch (option) {
case (ScalingOption.Automatic):
@ -907,6 +890,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
toggleMenu() {
this.menuOpen = !this.menuOpen;
this.cdRef.markForCheck();
if (this.menuTimeout) {
clearTimeout(this.menuTimeout);
@ -917,6 +901,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} else {
this.showClickOverlay = false;
this.settingsOpen = false;
this.cdRef.markForCheck();
}
}
@ -1084,8 +1069,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
loadNextChapter() {
if (this.nextPageDisabled) { return; }
if (this.nextChapterDisabled) { return; }
if (this.nextChapterDisabled) {
this.toastr.info('No Next Chapter');
return;
}
this.isLoading = true;
this.cdRef.markForCheck();
if (this.nextChapterId === CHAPTER_ID_NOT_FETCHED || this.nextChapterId === this.chapterId) {
this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
this.nextChapterId = chapterId;
@ -1098,8 +1088,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
loadPrevChapter() {
if (this.prevPageDisabled) { return; }
if (this.prevChapterDisabled) { return; }
if (this.prevChapterDisabled) {
this.toastr.info('No Previous Chapter');
return;
}
this.isLoading = true;
this.cdRef.markForCheck();
this.continuousChaptersStack.pop();
const prevChapter = this.continuousChaptersStack.peek();
if (prevChapter != this.chapterId) {
@ -1138,7 +1132,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} else {
this.nextPageDisabled = true;
}
this.cdRef.markForCheck();
}
}
@ -1170,44 +1164,55 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.canvas.nativeElement.width = this.canvasImage.width;
this.canvas.nativeElement.height = this.canvasImage.height;
}
this.cdRef.markForCheck();
}
}
renderPage() {
const pageSplit = this.generalSettingsForm.get('pageSplitOption')?.value;
if (!this.ctx || !this.canvas || pageSplit !== PageSplitOption.FitSplit || pageSplit !== PageSplitOption.NoSplit) {
const pageSplit = parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10);
if (!this.ctx || !this.canvas || (pageSplit === PageSplitOption.FitSplit || pageSplit === PageSplitOption.NoSplit)) {
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
this.readingArea.nativeElement.scroll(0,0);
}
this.isLoading = false;
this.cdRef.markForCheck();
return;
}
const needsSplitting = this.isWideImage();
if (!needsSplitting) {
this.renderWithCanvas = false;
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
this.readingArea.nativeElement.scroll(0,0);
}
this.isLoading = false;
this.cdRef.markForCheck();
return;
}
this.canvasImage.onload = null;
this.setCanvasSize();
const needsSplitting = this.isWideImage();
this.updateSplitPage();
if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) {
this.canvas.nativeElement.width = this.canvasImage.width / 2;
this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, 0, 0, this.canvasImage.width, this.canvasImage.height);
this.renderWithCanvas = true;
this.cdRef.markForCheck();
} else if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.RIGHT_PART) {
this.canvas.nativeElement.width = this.canvasImage.width / 2;
this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height);
this.renderWithCanvas = true;
} else {
this.renderWithCanvas = false;
this.cdRef.markForCheck();
}
// Reset scroll on non HEIGHT Fits
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
this.readingArea.nativeElement.scroll(0,0);
}
this.isLoading = false;
this.cdRef.markForCheck();
}
updateScalingForFirstPageRender() {
@ -1233,6 +1238,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.firstPageRendered = true;
this.generalSettingsForm.get('fittingOption')?.setValue(newScale, {emitEvent: false});
//this.cdRef.markForCheck();
}
/**
@ -1330,9 +1336,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
}
this.cdRef.markForCheck();
this.renderPage();
this.prefetch();
this.isLoading = false;
this.cdRef.markForCheck();
}
setReadingDirection() {
@ -1376,18 +1385,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
setPageNum(pageNum: number) {
this.pageNum = Math.max(Math.min(pageNum, this.maxPages - 1), 0);
this.cdRef.markForCheck();
if (this.pageNum >= this.maxPages - 10) {
// Tell server to cache the next chapter
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) {
this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => {
this.nextChapterPrefetched = true;
this.cdRef.markForCheck();
});
}
} else if (this.pageNum <= 10) {
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) {
this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => {
this.prevChapterPrefetched = true;
this.cdRef.markForCheck();
});
}
}
@ -1481,7 +1493,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
updateForm() {
if ( this.readerMode === ReaderMode.Webtoon) {
this.generalSettingsForm.get('pageSplitOption')?.disable()
this.generalSettingsForm.get('fittingOption')?.disable()
@ -1496,6 +1507,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm.get('fittingOption')?.disable();
}
}
this.cdRef.markForCheck();
}
handleWebtoonPageChange(updatedPageNum: number) {