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

@ -197,4 +197,35 @@ export class UtilityService {
return [windowWidth, windowHeight];
}
/**
*
* @param data An array of objects
* @param keySelector A method to fetch a string from the object, which is used to classify the JumpKey
* @returns
*/
getJumpKeys(data :Array<any>, keySelector: (data: any) => string) {
const keys: {[key: string]: number} = {};
data.forEach(obj => {
let ch = keySelector(obj).charAt(0);
if (/\d|\#|!|%|@|\(|\)|\^|\*/g.test(ch)) {
ch = '#';
}
if (!keys.hasOwnProperty(ch)) {
keys[ch] = 0;
}
keys[ch] += 1;
});
return Object.keys(keys).map(k => {
return {
key: k,
size: keys[k],
title: k.toUpperCase()
}
}).sort((a, b) => {
if (a.key < b.key) return -1;
if (a.key > b.key) return 1;
return 0;
});
}
}

View file

@ -1,9 +1,10 @@
import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/core';
@Component({
selector: 'app-badge-expander',
templateUrl: './badge-expander.component.html',
styleUrls: ['./badge-expander.component.scss']
styleUrls: ['./badge-expander.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BadgeExpanderComponent implements OnInit {
@ -18,16 +19,18 @@ export class BadgeExpanderComponent implements OnInit {
get itemsLeft() {
return Math.max(this.items.length - this.itemsTillExpander, 0);
}
constructor() { }
constructor(private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void {
this.visibleItems = this.items.slice(0, this.itemsTillExpander);
this.cdRef.markForCheck();
}
toggleVisible() {
this.isCollapsed = !this.isCollapsed;
this.visibleItems = this.items;
this.cdRef.markForCheck();
}
}

View file

@ -1,29 +1,27 @@
<ng-container *ngIf="currentValue > 0">
<div class="number">
<i class="fa fa-angle-double-down" style="font-size: 36px;" aria-hidden="true"></i>
</div>
<div style="width: 100px; height: 100px;">
<circle-progress
[percent]="currentValue"
[radius]="100"
[outerStrokeWidth]="15"
[innerStrokeWidth]="0"
[space] = "0"
[backgroundPadding]="0"
outerStrokeLinecap="butt"
[outerStrokeColor]="'#4ac694'"
[innerStrokeColor]="innerStrokeColor"
titleFontSize= "24"
unitsFontSize= "24"
[showSubtitle] = "false"
[animation]="animation"
[animationDuration]="300"
[startFromZero]="false"
[responsive]="true"
[backgroundOpacity]="0.5"
[backgroundColor]="'#000'"
></circle-progress>
</div>
<div class="number">
<i class="fa fa-angle-double-down" style="font-size: 36px;" aria-hidden="true"></i>
</div>
<div style="width: 100px; height: 100px;">
<circle-progress
[percent]="currentValue"
[radius]="100"
[outerStrokeWidth]="15"
[innerStrokeWidth]="0"
[space] = "0"
[backgroundPadding]="0"
outerStrokeLinecap="butt"
[outerStrokeColor]="'#4ac694'"
[innerStrokeColor]="innerStrokeColor"
titleFontSize= "24"
unitsFontSize= "24"
[showSubtitle] = "false"
[animation]="animation"
[animationDuration]="300"
[startFromZero]="false"
[responsive]="true"
[backgroundOpacity]="0.5"
[backgroundColor]="'#000'"
></circle-progress>
</div>
</ng-container>

View file

@ -1,20 +1,15 @@
import { Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'app-circular-loader',
templateUrl: './circular-loader.component.html',
styleUrls: ['./circular-loader.component.scss']
styleUrls: ['./circular-loader.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CircularLoaderComponent implements OnInit {
export class CircularLoaderComponent {
@Input() currentValue: number = 0;
@Input() maxValue: number = 0;
@Input() animation: boolean = true;
@Input() innerStrokeColor: string = 'transparent';
constructor() { }
ngOnInit(): void {
}
}

View file

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
export class DrawerOptions {
/**
@ -11,7 +11,8 @@ export class DrawerOptions {
selector: 'app-drawer',
templateUrl: './drawer.component.html',
styleUrls: ['./drawer.component.scss'],
exportAs: "drawer"
exportAs: "drawer",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DrawerComponent {
@Input() isOpen = false;
@ -24,10 +25,12 @@ export class DrawerComponent {
@Output() drawerClosed = new EventEmitter();
@Output() isOpenChange: EventEmitter<boolean> = new EventEmitter();
constructor(private readonly cdRef: ChangeDetectorRef) {}
close() {
this.isOpen = false;
this.isOpenChange.emit(false);
this.drawerClosed.emit(false);
this.cdRef.markForCheck();
}
}

View file

@ -1,11 +1,12 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-icon-and-title',
templateUrl: './icon-and-title.component.html',
styleUrls: ['./icon-and-title.component.scss']
styleUrls: ['./icon-and-title.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class IconAndTitleComponent implements OnInit {
export class IconAndTitleComponent {
/**
* If the component is clickable and should emit click events
*/
@ -19,15 +20,9 @@ export class IconAndTitleComponent implements OnInit {
@Output() click: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>();
constructor() { }
ngOnInit(): void {
}
handleClick(event: MouseEvent) {
if (this.clickable) this.click.emit(event);
}
}

View file

@ -1,15 +1,15 @@
import { Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Person } from '../../_models/person';
@Component({
selector: 'app-person-badge',
templateUrl: './person-badge.component.html',
styleUrls: ['./person-badge.component.scss']
styleUrls: ['./person-badge.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PersonBadgeComponent implements OnInit {
@Input() person!: Person;
constructor() { }

View file

@ -1,6 +1,6 @@
<div>
<span [innerHTML]="currentText | safeHtml" [ngClass]="{'blur-text': blur && isCollapsed}"></span>
<a [class.hidden]="hideToggle" *ngIf="text && text.length > maxLength" class="read-more-link" (click)="toggleView()">
&nbsp;<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}}"></i>&nbsp;Read {{isCollapsed ? 'More' : 'Less'}}
&nbsp;<i aria-hidden="true" class="fa" [ngClass]="{'fa-caret-down': isCollapsed, 'fa-caret-up': !isCollapsed}"></i>&nbsp;Read {{isCollapsed ? 'More' : 'Less'}}
</a>
</div>

View file

@ -1,43 +1,52 @@
import { Component, Input, OnChanges } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core';
@Component({
selector: 'app-read-more',
templateUrl: './read-more.component.html',
styleUrls: ['./read-more.component.scss']
styleUrls: ['./read-more.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReadMoreComponent implements OnChanges {
/**
* String to apply readmore on
*/
@Input() text!: string;
/**
* Max length before apply read more. Defaults to 250 characters.
*/
@Input() maxLength: number = 250;
/**
* If the field is collapsed and blur true, text will not be readable
*/
@Input() blur: boolean = false;
currentText!: string;
hideToggle: boolean = true;
isCollapsed: boolean = true;
public isCollapsed: boolean = true;
constructor(private readonly cdRef: ChangeDetectorRef) {}
toggleView() {
this.isCollapsed = !this.isCollapsed;
this.determineView();
this.isCollapsed = !this.isCollapsed;
this.determineView();
}
determineView() {
if (!this.text || this.text.length <= this.maxLength) {
this.currentText = this.text;
this.isCollapsed = false;
this.hideToggle = true;
return;
}
this.hideToggle = false;
if (this.isCollapsed === true) {
this.currentText = this.text.substring(0, this.maxLength);
this.currentText = this.currentText.substr(0, Math.min(this.currentText.length, this.currentText.lastIndexOf(' ')));
this.currentText = this.currentText + '…';
} else if (this.isCollapsed === false) {
this.currentText = this.text;
}
if (!this.text || this.text.length <= this.maxLength) {
this.currentText = this.text;
this.isCollapsed = false;
this.hideToggle = true;
return;
}
this.hideToggle = false;
if (this.isCollapsed === true) {
this.currentText = this.text.substring(0, this.maxLength);
this.currentText = this.currentText.substr(0, Math.min(this.currentText.length, this.currentText.lastIndexOf(' ')));
this.currentText = this.currentText + '…';
} else if (this.isCollapsed === false) {
this.currentText = this.text;
}
this.cdRef.markForCheck();
}
ngOnChanges() {
this.determineView();

View file

@ -1,23 +1,17 @@
import { Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { MangaFormat } from 'src/app/_models/manga-format';
import { UtilityService } from '../_services/utility.service';
@Component({
selector: 'app-series-format',
templateUrl: './series-format.component.html',
styleUrls: ['./series-format.component.scss']
styleUrls: ['./series-format.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SeriesFormatComponent implements OnInit {
export class SeriesFormatComponent {
@Input() format: MangaFormat = MangaFormat.UNKNOWN;
get MangaFormat(): typeof MangaFormat {
return MangaFormat;
}
constructor(public utilityService: UtilityService) { }
ngOnInit(): void {
}
}

View file

@ -1,5 +1,6 @@
import { AfterViewInit, Directive, ElementRef, TemplateRef, ViewContainerRef } from '@angular/core';
// TODO: Fix this code or remove it
@Directive({
selector: '[appShowIfScrollbar]'
})

View file

@ -1,3 +1,4 @@
<div class="tagbadge {{cursor}} {{fillStyle}}">
<div class="tagbadge {{fillStyle}}" [ngClass]="{'selectable-cursor': selectionMode === TagBadgeCursor.Selectable,
'not-allowed-cursor': selectionMode === TagBadgeCursor.NotAllowed, 'clickable-cursor': selectionMode === TagBadgeCursor.Clickable}">
<ng-content></ng-content>
</div>

View file

@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
/**
* What type of cursor to apply to the tag badge
@ -24,29 +24,15 @@ export enum TagBadgeCursor {
@Component({
selector: 'app-tag-badge',
templateUrl: './tag-badge.component.html',
styleUrls: ['./tag-badge.component.scss']
styleUrls: ['./tag-badge.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagBadgeComponent implements OnInit {
export class TagBadgeComponent {
@Input() selectionMode: TagBadgeCursor = TagBadgeCursor.Selectable;
@Input() fillStyle: 'filled' | 'outline' = 'outline';
cursor: string = 'default';
constructor() { }
ngOnInit(): void {
switch (this.selectionMode) {
case TagBadgeCursor.Selectable:
this.cursor = 'selectable-cursor';
break;
case TagBadgeCursor.NotAllowed:
this.cursor = 'not-allowed-cursor';
break;
case TagBadgeCursor.Clickable:
this.cursor = 'clickable-cursor';
break;
}
get TagBadgeCursor() {
return TagBadgeCursor;
}
}

View file

@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event';
@ -7,20 +7,16 @@ import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event'
@Component({
selector: 'app-update-notification-modal',
templateUrl: './update-notification-modal.component.html',
styleUrls: ['./update-notification-modal.component.scss']
styleUrls: ['./update-notification-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UpdateNotificationModalComponent implements OnInit {
export class UpdateNotificationModalComponent {
@Input() updateData!: UpdateVersionEvent;
constructor(public modal: NgbActiveModal) { }
ngOnInit(): void {
}
close() {
this.modal.close({success: false, series: undefined});
}
}