misc stuff to avoid scan loop (#1389)

* Implemented a workaround for nginx users with BlockCommonExploits enabled, which would interfere with book image escaping done by Kavita when images had ../ in their path.

* Added back to top support on all pages but those that untilize virtual scrolling without a parent scroll.

* Hide jumpbar on pages where there is no scroll

* Refactored jumbar code into a dedicated service

* Stash some jumpkey resume code as I can't get it working with the virtual scroller.

* Don't allow non-admins to see File locations on card detail drawer.

* Some cleanup on GetServerInfo

* When an error occurs in register, delete the user on exception.

* Fixed a NPE in Stat collection for brand new users

* When we catch an exception on registering a new user, delete the user as rolling back doesn't do anything.

* Don't close typeahead when we are selecting options from it

* Added shortcut key H to open shortcut modal on manga reader

* When processing progress updates on cards, for volumes, properly find the chapter to update pages read.

* Hide cover image on reading list if it's not set and fixed a missing closing div tag

* Hide collection poster when nothing is set on collection detail

* Small fix around updating state

* Sped up the bookmark image call by removing one DB call

* Fixed broken test from change in bookmark code

* Fixed an oversight where if there is no tag in ComicInfo after a chapter was updated with People or Genres, then the People/Genres would never be removed.

* Added test with TagHelper

* Fixed a bug where 2 clear buttons would show on search bar due to browser injecting their own. Search bar wont show clear button until text is typed.

* Fixed a bug where InstallID wasn't being selected correctly in converter
This commit is contained in:
Joseph Milazzo 2022-07-27 10:16:45 -05:00 committed by GitHub
parent b90c6aa76c
commit 5812588fe5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 474 additions and 249 deletions

View file

@ -15,10 +15,7 @@ export class CollectionTagService {
constructor(private httpClient: HttpClient, private imageService: ImageService) { }
allTags() {
return this.httpClient.get<CollectionTag[]>(this.baseUrl + 'collection/').pipe(map(tags => {
tags.forEach(s => s.coverImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(s.id)));
return tags;
}));
return this.httpClient.get<CollectionTag[]>(this.baseUrl + 'collection/');
}
search(query: string) {

View file

@ -0,0 +1,83 @@
import { Injectable } from '@angular/core';
import { JumpKey } from '../_models/jumpbar/jump-key';
const keySize = 25; // Height of the JumpBar button
@Injectable({
providedIn: 'root'
})
export class JumpbarService {
resumeKeys: {[key: string]: string} = {};
constructor() { }
getResumeKey(key: string) {
if (this.resumeKeys.hasOwnProperty(key)) return this.resumeKeys[key];
return '';
}
saveResumeKey(key: string, value: string) {
this.resumeKeys[key] = value;
}
generateJumpBar(jumpBarKeys: Array<JumpKey>, currentSize: number) {
const fullSize = (jumpBarKeys.length * keySize);
if (currentSize >= fullSize) {
return [...jumpBarKeys];
}
const jumpBarKeysToRender: Array<JumpKey> = [];
const targetNumberOfKeys = parseInt(Math.floor(currentSize / keySize) + '', 10);
const removeCount = jumpBarKeys.length - targetNumberOfKeys - 3;
if (removeCount <= 0) return jumpBarKeysToRender;
const removalTimes = Math.ceil(removeCount / 2);
const midPoint = Math.floor(jumpBarKeys.length / 2);
jumpBarKeysToRender.push(jumpBarKeys[0]);
this.removeFirstPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender);
jumpBarKeysToRender.push(jumpBarKeys[midPoint]);
this.removeSecondPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender);
jumpBarKeysToRender.push(jumpBarKeys[jumpBarKeys.length - 1]);
return jumpBarKeysToRender;
}
removeSecondPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array<JumpKey>, jumpBarKeysToRender: Array<JumpKey>) {
const removedIndexes: Array<number> = [];
for(let removal = 0; removal < numberOfRemovals; removal++) {
let min = 100000000;
let minIndex = -1;
for(let i = midPoint + 1; i < jumpBarKeys.length - 2; i++) {
if (jumpBarKeys[i].size < min && !removedIndexes.includes(i)) {
min = jumpBarKeys[i].size;
minIndex = i;
}
}
removedIndexes.push(minIndex);
}
for(let i = midPoint + 1; i < jumpBarKeys.length - 2; i++) {
if (!removedIndexes.includes(i)) jumpBarKeysToRender.push(jumpBarKeys[i]);
}
}
removeFirstPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array<JumpKey>, jumpBarKeysToRender: Array<JumpKey>) {
const removedIndexes: Array<number> = [];
for(let removal = 0; removal < numberOfRemovals; removal++) {
let min = 100000000;
let minIndex = -1;
for(let i = 1; i < midPoint; i++) {
if (jumpBarKeys[i].size < min && !removedIndexes.includes(i)) {
min = jumpBarKeys[i].size;
minIndex = i;
}
}
removedIndexes.push(minIndex);
}
for(let i = 1; i < midPoint; i++) {
if (!removedIndexes.includes(i)) jumpBarKeysToRender.push(jumpBarKeys[i]);
}
}
}

View file

@ -1,11 +1,27 @@
import { Injectable } from '@angular/core';
import { ElementRef, Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter, ReplaySubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ScrollService {
constructor() { }
private scrollContainerSource = new ReplaySubject<string | ElementRef<HTMLElement>>(1);
/**
* Exposes the current container on the active screen that is our primary overlay area. Defaults to 'body' and changes to 'body' on page loads
*/
public scrollContainer$ = this.scrollContainerSource.asObservable();
constructor(router: Router) {
router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => {
this.scrollContainerSource.next('body');
});
this.scrollContainerSource.next('body');
}
get scrollPosition() {
return (window.pageYOffset
@ -26,4 +42,10 @@ export class ScrollService {
behavior: 'auto'
});
}
setScrollContainer(elem: ElementRef<HTMLElement> | undefined) {
if (elem !== undefined) {
this.scrollContainerSource.next(elem);
}
}
}

View file

@ -108,7 +108,7 @@
</ng-template>
</li>
<li [ngbNavItem]="tabs[TabID.Files]">
<li [ngbNavItem]="tabs[TabID.Files]" [disabled]="!(isAdmin$ | async)">
<a ngbNavLink>{{tabs[TabID.Files].title}}</a>
<ng-template ngbNavContent>
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>

View file

@ -30,7 +30,7 @@
</div>
</div>
<ng-container *ngIf="jumpBarKeysToRender.length >= 4" [ngTemplateOutlet]="jumpBar" [ngTemplateOutletContext]="{ id: 'jumpbar' }"></ng-container>
<ng-container *ngIf="jumpBarKeysToRender.length >= 4 && scroll.viewPortInfo.maxScrollPosition > 0" [ngTemplateOutlet]="jumpBar" [ngTemplateOutletContext]="{ id: 'jumpbar' }"></ng-container>
</div>
<ng-template #cardTemplate>
<virtual-scroller #scroll [items]="items" [bufferAmount]="1">

View file

@ -1,8 +1,8 @@
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewChild } from '@angular/core';
import { AfterContentInit, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnChanges, OnDestroy, OnInit, Output, TemplateRef, TrackByFunction, ViewChild } from '@angular/core';
import { VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
import { Subject } from 'rxjs';
import { first, Subject, takeUntil, takeWhile } from 'rxjs';
import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
@ -10,9 +10,10 @@ import { Library } from 'src/app/_models/library';
import { Pagination } from 'src/app/_models/pagination';
import { FilterEvent, FilterItem, SeriesFilter } from 'src/app/_models/series-filter';
import { ActionItem } from 'src/app/_services/action-factory.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service';
import { SeriesService } from 'src/app/_services/series.service';
const keySize = 24;
const keySize = 25; // Height of the JumpBar button
@Component({
selector: 'app-card-detail-layout',
@ -20,7 +21,7 @@ const keySize = 24;
styleUrls: ['./card-detail-layout.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
@Input() header: string = '';
@Input() isLoading: boolean = false;
@ -62,6 +63,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
libraries: Array<FilterItem<Library>> = [];
updateApplied: number = 0;
hasResumedJumpKey: boolean = false;
private onDestory: Subject<void> = new Subject();
@ -70,7 +72,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
}
constructor(private seriesService: SeriesService, public utilityService: UtilityService,
@Inject(DOCUMENT) private document: Document, private changeDetectionRef: ChangeDetectorRef) {
@Inject(DOCUMENT) private document: Document, private changeDetectionRef: ChangeDetectorRef,
private jumpbarService: JumpbarService) {
this.filter = this.seriesService.createSeriesFilter();
this.changeDetectionRef.markForCheck();
}
@ -78,74 +81,16 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
@HostListener('window:resize', ['$event'])
@HostListener('window:orientationchange', ['$event'])
resizeJumpBar() {
const fullSize = (this.jumpBarKeys.length * keySize);
const currentSize = (this.document.querySelector('.viewport-container')?.getBoundingClientRect().height || 10) - 30;
if (currentSize >= fullSize) {
this.jumpBarKeysToRender = [...this.jumpBarKeys];
this.changeDetectionRef.markForCheck();
return;
}
const targetNumberOfKeys = parseInt(Math.floor(currentSize / keySize) + '', 10);
const removeCount = this.jumpBarKeys.length - targetNumberOfKeys - 3;
if (removeCount <= 0) return;
this.jumpBarKeysToRender = [];
const removalTimes = Math.ceil(removeCount / 2);
const midPoint = Math.floor(this.jumpBarKeys.length / 2);
this.jumpBarKeysToRender.push(this.jumpBarKeys[0]);
this.removeFirstPartOfJumpBar(midPoint, removalTimes);
this.jumpBarKeysToRender.push(this.jumpBarKeys[midPoint]);
this.removeSecondPartOfJumpBar(midPoint, removalTimes);
this.jumpBarKeysToRender.push(this.jumpBarKeys[this.jumpBarKeys.length - 1]);
this.jumpBarKeysToRender = this.jumpbarService.generateJumpBar(this.jumpBarKeys, currentSize);
this.changeDetectionRef.markForCheck();
}
removeSecondPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1) {
const removedIndexes: Array<number> = [];
for(let removal = 0; removal < numberOfRemovals; removal++) {
let min = 100000000;
let minIndex = -1;
for(let i = midPoint + 1; i < this.jumpBarKeys.length - 2; i++) {
if (this.jumpBarKeys[i].size < min && !removedIndexes.includes(i)) {
min = this.jumpBarKeys[i].size;
minIndex = i;
}
}
removedIndexes.push(minIndex);
}
for(let i = midPoint + 1; i < this.jumpBarKeys.length - 2; i++) {
if (!removedIndexes.includes(i)) this.jumpBarKeysToRender.push(this.jumpBarKeys[i]);
}
}
removeFirstPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1) {
const removedIndexes: Array<number> = [];
for(let removal = 0; removal < numberOfRemovals; removal++) {
let min = 100000000;
let minIndex = -1;
for(let i = 1; i < midPoint; i++) {
if (this.jumpBarKeys[i].size < min && !removedIndexes.includes(i)) {
min = this.jumpBarKeys[i].size;
minIndex = i;
}
}
removedIndexes.push(minIndex);
}
for(let i = 1; i < midPoint; i++) {
if (!removedIndexes.includes(i)) this.jumpBarKeysToRender.push(this.jumpBarKeys[i]);
}
}
ngOnInit(): void {
if (this.trackByIdentity === undefined) {
this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`;
}
if (this.filterSettings === undefined) {
this.filterSettings = new FilterSettings();
this.changeDetectionRef.markForCheck();
@ -157,9 +102,27 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
}
}
ngOnChanges(changes: SimpleChanges): void {
ngAfterViewInit(): void {
// NOTE: I can't seem to figure out a way to resume the JumpKey with the scroller.
// this.virtualScroller.vsUpdate.pipe(takeWhile(() => this.hasResumedJumpKey), takeUntil(this.onDestory)).subscribe(() => {
// const resumeKey = this.jumpbarService.getResumeKey(this.header);
// console.log('Resume key:', resumeKey);
// if (resumeKey !== '') {
// const keys = this.jumpBarKeys.filter(k => k.key === resumeKey);
// if (keys.length >= 1) {
// console.log('Scrolling to ', keys[0].key);
// this.scrollTo(keys[0]);
// this.hasResumedJumpKey = true;
// }
// }
// this.hasResumedJumpKey = true;
// });
}
ngOnChanges(): void {
this.jumpBarKeysToRender = [...this.jumpBarKeys];
this.resizeJumpBar();
}
@ -188,7 +151,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
targetIndex += this.jumpBarKeys[i].size;
}
this.virtualScroller.scrollToIndex(targetIndex, true, undefined, 1000);
this.virtualScroller.scrollToIndex(targetIndex, true, 800, 1000);
this.jumpbarService.saveResumeKey(this.header, jumpKey.key);
this.changeDetectionRef.markForCheck();
return;
}

View file

@ -177,6 +177,33 @@ export class CardItemComponent implements OnInit, OnDestroy {
if (this.utilityService.isVolume(this.entity) && updateEvent.volumeId !== this.entity.id) return;
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
// For volume or Series, we can't just take the event
if (this.utilityService.isVolume(this.entity) || this.utilityService.isSeries(this.entity)) {
if (this.utilityService.isVolume(this.entity)) {
const v = this.utilityService.asVolume(this.entity);
const chapter = v.chapters.find(c => c.id === updateEvent.chapterId);
if (chapter) {
chapter.pagesRead = updateEvent.pagesRead;
}
} else {
// re-request progress for the series
const s = this.utilityService.asSeries(this.entity);
let pagesRead = 0;
if (s.hasOwnProperty('volumes')) {
s.volumes.forEach(v => {
v.chapters.forEach(c => {
if (c.id === updateEvent.chapterId) {
c.pagesRead = updateEvent.pagesRead;
}
pagesRead += c.pagesRead;
});
});
s.pagesRead = pagesRead;
}
}
}
this.read = updateEvent.pagesRead;
this.cdRef.detectChanges();
});

View file

@ -11,7 +11,7 @@
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="collectionTag !== undefined" #scrollingBlock>
<div class="row mb-3">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="collectionTag.coverImage !== '' && collectionTag.coverImage !== undefined && collectionTag.coverImage !== null">
<app-image maxWidth="481px" [imageUrl]="tagImage"></app-image>
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">

View file

@ -1,5 +1,5 @@
import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { AfterContentChecked, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Router, ActivatedRoute } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@ -22,6 +22,7 @@ import { ActionService } from 'src/app/_services/action.service';
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
import { ImageService } from 'src/app/_services/image.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { ScrollService } from 'src/app/_services/scroll.service';
import { SeriesService } from 'src/app/_services/series.service';
@Component({
@ -30,7 +31,7 @@ import { SeriesService } from 'src/app/_services/series.service';
styleUrls: ['./collection-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CollectionDetailComponent implements OnInit, OnDestroy {
export class CollectionDetailComponent implements OnInit, OnDestroy, AfterContentChecked {
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
@ -113,7 +114,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
private modalService: NgbModal, private titleService: Title,
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
private readonly cdRef: ChangeDetectorRef) {
private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
const routeId = this.route.snapshot.paramMap.get('id');
@ -148,6 +149,10 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
});
}
ngAfterContentChecked(): void {
this.scrollService.setScrollContainer(this.scrollingBlock);
}
ngOnDestroy() {
this.onDestory.next();
this.onDestory.complete();
@ -230,6 +235,8 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
this.loadPage();
if (results.coverImageUpdated) {
this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(collectionTag.id));
this.collectionTag.coverImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(collectionTag.id));
this.cdRef.markForCheck();
}
});
}

View file

@ -611,7 +611,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} else if (event.key === KEY_CODES.B) {
this.bookmarkPage();
} else if (event.key === KEY_CODES.F) {
this.toggleFullscreen()
this.toggleFullscreen();
} else if (event.key === KEY_CODES.H) {
this.openShortcutModal();
}
}

View file

@ -1,16 +1,14 @@
<form [formGroup]="typeaheadForm" class="grouped-typeahead">
<div class="typeahead-input" [ngClass]="{'focused': hasFocus == true}" (click)="onInputFocus($event)">
<div class="search">
<input #input [id]="id" type="search" autocomplete="off" formControlName="typeahead" [placeholder]="placeholder"
<input #input [id]="id" type="text" autocomplete="off" formControlName="typeahead" [placeholder]="placeholder"
aria-haspopup="listbox" aria-owns="dropdown" aria-expanded="hasFocus && (grouppedData.persons.length || grouppedData.collections.length || grouppedData.series.length || grouppedData.persons.length || grouppedData.tags.length || grouppedData.genres.length)"
aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)" role="search"
>
<div class="spinner-border spinner-border-sm" role="status" *ngIf="isLoading">
<span class="visually-hidden">Loading...</span>
</div>
<button type="button" class="btn-close" aria-label="Close" (click)="resetField()">
</button>
<button type="button" class="btn-close" aria-label="Close" (click)="resetField()" *ngIf="typeaheadForm.get('typeahead')?.value.length > 0"></button>
</div>
</div>
<div class="dropdown" *ngIf="hasFocus">

View file

@ -129,8 +129,8 @@
</ul>
<ng-container *ngIf="!searchFocused">
<div class="back-to-top">
<button class="btn btn-icon scroll-to-top" (click)="scrollToTop()" *ngIf="backToTopNeeded">
<div class="back-to-top" *ngIf="backToTopNeeded">
<button class="btn btn-icon scroll-to-top" (click)="scrollToTop()">
<i class="fa fa-angle-double-up nav" aria-hidden="true"></i>
<span class="visually-hidden">Scroll to Top</span>
</button>

View file

@ -1,8 +1,8 @@
import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { fromEvent, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, takeUntil, takeWhile, tap } from 'rxjs/operators';
import { Chapter } from 'src/app/_models/chapter';
import { MangaFile } from 'src/app/_models/manga-file';
import { ScrollService } from 'src/app/_services/scroll.service';
@ -48,44 +48,36 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
backToTopNeeded = false;
searchFocused: boolean = false;
scrollElem: HTMLElement;
private readonly onDestroy = new Subject<void>();
constructor(public accountService: AccountService, private router: Router, public navService: NavService,
private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document,
private scrollService: ScrollService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) { }
private scrollService: ScrollService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {
this.scrollElem = this.document.body;
}
ngOnInit(): void {
// setTimeout(() => this.setupScrollChecker(), 1000);
// // TODO: on router change, reset the scroll check
// this.router.events
// .pipe(filter(event => event instanceof NavigationStart))
// .subscribe((event) => {
// setTimeout(() => this.setupScrollChecker(), 1000);
// });
this.scrollService.scrollContainer$.pipe(distinctUntilChanged(), takeUntil(this.onDestroy), tap((scrollContainer) => {
if (scrollContainer === 'body' || scrollContainer === undefined) {
this.scrollElem = this.document.body;
fromEvent(this.document.body, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded(this.document.body));
} else {
const elem = scrollContainer as ElementRef<HTMLDivElement>;
this.scrollElem = elem.nativeElement;
fromEvent(elem.nativeElement, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded(elem.nativeElement));
}
})).subscribe();
}
// setupScrollChecker() {
// const viewportScroller = this.document.querySelector('.viewport-container');
// console.log('viewport container', viewportScroller);
// if (viewportScroller) {
// fromEvent(viewportScroller, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded());
// } else {
// fromEvent(this.document.body, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded());
// }
// }
@HostListener('body:scroll', [])
checkBackToTopNeeded() {
// TODO: This somehow needs to hook into the scrolling for virtual scroll
const offset = this.scrollService.scrollPosition;
checkBackToTopNeeded(elem: HTMLElement) {
const offset = elem.scrollTop || 0;
if (offset > 100) {
this.backToTopNeeded = true;
} else if (offset < 40) {
this.backToTopNeeded = false;
}
this.cdRef.markForCheck();
}
ngOnDestroy() {
@ -219,7 +211,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
scrollToTop() {
this.scrollService.scrollTo(0, this.document.body);
this.scrollService.scrollTo(0, this.scrollElem);
}
focusUpdate(searchFocused: boolean) {

View file

@ -11,7 +11,7 @@
<div class="container-fluid mt-2" *ngIf="readingList">
<div class="row mb-3">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="readingList.coverImage !== '' || readingList.coverImage !== undefined">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="readingList.coverImage !== '' && readingList.coverImage !== undefined && readingList.coverImage !== null">
<app-image maxWidth="300px" maxHeight="400px" [imageUrl]="readingListImage"></app-image>
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
@ -45,7 +45,9 @@
<app-read-more [text]="readingListSummary" [maxLength]="250"></app-read-more>
</div>
</div>
</div>
<div class="row mb-3">
<div class="mx-auto" style="width: 200px;">
<ng-container *ngIf="items.length === 0 && !isLoading">
Nothing added
@ -64,4 +66,5 @@
[promoted]="item.promoted" (read)="readChapter($event)"></app-reading-list-item>
</ng-template>
</app-draggable-ordered-list>
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, Inject, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, Inject, ChangeDetectionStrategy, ChangeDetectorRef, AfterContentChecked, AfterViewInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
@ -40,6 +40,7 @@ import { PageLayoutMode } from '../_models/page-layout-mode';
import { DOCUMENT } from '@angular/common';
import { User } from '../_models/user';
import { Download } from '../shared/_models/download';
import { ScrollService } from '../_services/scroll.service';
interface RelatedSeris {
series: Series;
@ -66,7 +67,7 @@ interface StoryLineItem {
styleUrls: ['./series-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SeriesDetailComponent implements OnInit, OnDestroy {
export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChecked {
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
@ -250,7 +251,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
public imageSerivce: ImageService, private messageHub: MessageHubService,
private readingListService: ReadingListService, public navService: NavService,
private offcanvasService: NgbOffcanvas, @Inject(DOCUMENT) private document: Document,
private changeDetectionRef: ChangeDetectorRef
private changeDetectionRef: ChangeDetectorRef, private scrollService: ScrollService
) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
@ -265,6 +266,10 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
});
}
ngAfterContentChecked(): void {
this.scrollService.setScrollContainer(this.scrollingBlock);
}
ngOnInit(): void {
const routeId = this.route.snapshot.paramMap.get('seriesId');

View file

@ -18,6 +18,7 @@ export enum KEY_CODES {
G = 'g',
B = 'b',
F = 'f',
H = 'h',
BACKSPACE = 'Backspace',
DELETE = 'Delete',
SHIFT = 'Shift'

View file

@ -10,7 +10,6 @@
<ng-container [ngTemplateOutlet]="badgeTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: i }"></ng-container>
<i class="fa fa-times" *ngIf="!disabled" (click)="toggleSelection(option)" tabindex="0" aria-label="close"></i>
</app-tag-badge>
<input #input [id]="settings.id" type="text" autocomplete="off" formControlName="typeahead" *ngIf="!disabled">
<div class="spinner-border spinner-border-sm {{settings.multiple ? 'close-offset' : ''}}" role="status" *ngIf="isLoadingOptions">
<span class="visually-hidden">Loading...</span>

View file

@ -286,8 +286,12 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
}
@HostListener('window:click', ['$event'])
@HostListener('body:click', ['$event'])
handleDocumentClick(event: any) {
// Don't close the typeahead when we select an item from it
if (event.target && (event.target as HTMLElement).classList.contains('list-group-item')) {
return;
}
this.hasFocus = false;
}
@ -331,7 +335,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
case KEY_CODES.DELETE:
{
if (this.typeaheadControl.value !== null && this.typeaheadControl.value !== undefined && this.typeaheadControl.value.trim() !== '') {
return;
break;
}
const selected = this.optionSelection.selected();
if (selected.length > 0) {
@ -364,12 +368,14 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
if (!untoggleAll && this.settings.savedData) {
const isArray = this.settings.savedData.hasOwnProperty('length');
if (isArray) {
this.optionSelection = new SelectionModel<any>(true, this.settings.savedData);
this.optionSelection = new SelectionModel<any>(true, this.settings.savedData); // NOTE: Library-detail will break the 'x' button due to how savedData is being set to avoid state reset
} else {
this.optionSelection = new SelectionModel<any>(true, [this.settings.savedData]);
}
this.cdRef.markForCheck();
} else {
this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false));
this.cdRef.markForCheck();
}
this.selectedData.emit(this.optionSelection.selected());
@ -386,7 +392,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
this.toggleSelection(opt);
this.resetField();
this.onInputFocus(undefined);
this.onInputFocus();
}
addNewItem(title: string) {
@ -398,7 +404,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
this.toggleSelection(newItem);
this.resetField();
this.onInputFocus(undefined);
this.onInputFocus();
}
/**
@ -421,7 +427,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
});
}
onInputFocus(event: any) {
onInputFocus(event?: any) {
if (event) {
event.stopPropagation();
event.preventDefault();