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

@ -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();
});
}