All Around Polish (#1328)

* Added --card-list-item-bg-color for the card list items

* Updated the card list item progress to match how cards render

* Implemented the ability to configure how many backups are retained.

* Fixed a bug where odd jump keys could cause a bad index error for jump bar

* Commented out more code for the pagination route if we go with that.

* Reverted a move of DisableConcurrentExecution to interface, as it seems to not work there.

* Updated manga format utility code to pipes

* Fixed bulk selection on series detail page

* Fixed bulk selection on all other pages

* Changed card item to OnPush

* Updated image component to OnPush

* Updated Series Card to OnPush

* Updated Series Detail to OnPush

* Lots of changes here. Integrated parentscroll support on card detail layout. Added jump bar (custom js implementation) on collection, reading list and all series pages. Updated UserParams to default to no pagination. Lots of cleanup all around

* Updated some notes on a module use

* Some code cleanup

* Fixed up a broken test due to the mapper not being configured in the test.

* Applied TabID pattern to edit collection tags

* Applied css from series detail to collection detail page to remove double scrollbar

* Implemented the ability to sort by Time To Read.

* Throw an error to the UI when we extract an archive and it contains invalid characters in the filename for the Server OS.

* Tweaked how the page scrolls for jumpbar on collection detail. We will have to polish another release

* Cleaned up the styling on directory picker

* Put some code in but it doesn't work for scroll to top on virtual scrolling. I'll do it later.

* Fixed a container bug
This commit is contained in:
Joseph Milazzo 2022-06-22 12:25:52 -05:00 committed by GitHub
parent 2ed0aca866
commit f54eb5865b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 626 additions and 436 deletions

View file

@ -1,57 +1,57 @@
<div #companionBar>
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasExtras]="true" [extraDrawer]="extrasDrawer">
<ng-container title>
<h2 style="margin-bottom: 0px">
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-v"></app-card-actionables>
<span>{{series?.name}}</span>
</h2>
</ng-container>
<ng-container subtitle *ngIf="series?.localizedName !== series?.name">
<h6 class="subtitle-with-actionables" title="Localized Name">{{series?.localizedName}}</h6>
</ng-container>
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasExtras]="true" [extraDrawer]="extrasDrawer">
<ng-container title>
<h2 style="margin-bottom: 0px">
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-v"></app-card-actionables>
<span>{{series?.name}}</span>
</h2>
</ng-container>
<ng-container subtitle *ngIf="series?.localizedName !== series?.name">
<h6 class="subtitle-with-actionables" title="Localized Name">{{series?.localizedName}}</h6>
</ng-container>
<ng-template #extrasDrawer let-offcanvas>
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">Page Settings</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
</div>
<div class="offcanvas-body">
<form [formGroup]="pageExtrasGroup">
<!-- <div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<label for="settings-book-reading-direction" class="form-label">Sort Order</label>
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
<i class="fa fa-arrow-up" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
<ng-template #descSort>
<i class="fa fa-arrow-down" title="Descending"></i>
</ng-template>
</button>
<ng-template #extrasDrawer let-offcanvas>
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">Page Settings</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
</div>
<div class="offcanvas-body">
<form [formGroup]="pageExtrasGroup">
<!-- <div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<label for="settings-book-reading-direction" class="form-label">Sort Order</label>
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
<i class="fa fa-arrow-up" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
<ng-template #descSort>
<i class="fa fa-arrow-down" title="Descending"></i>
</ng-template>
</button>
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="sortingOption">
<option *ngFor="let opt of sortingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div> -->
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<label id="list-layout-mode-label" class="form-label">Layout Mode</label>
<br/>
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.Cards" class="btn-check" id="layout-mode-default" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-default">Card</label>
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.List" class="btn-check" id="layout-mode-col1" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col1">List</label>
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="sortingOption">
<option *ngFor="let opt of sortingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div> -->
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<label id="list-layout-mode-label" class="form-label">Layout Mode</label>
<br/>
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.Cards" class="btn-check" id="layout-mode-default" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-default">Card</label>
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.List" class="btn-check" id="layout-mode-col1" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col1">List</label>
</div>
</div>
</div>
</div>
</form>
</div>
</ng-template>
</form>
</div>
</ng-template>
</app-side-nav-companion-bar>
</app-side-nav-companion-bar>
</div>
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="series !== undefined" #scrollingBlock>
@ -122,12 +122,16 @@
<ng-container *ngIf="!item.isChapter; else chapterCardItem">
<app-card-item class="col-auto mt-2 mb-2" *ngIf="item.volume.number != 0" [entity]="item.volume" [title]="item.volume.name" (click)="openVolume(item.volume)"
[imageUrl]="imageService.getVolumeCoverImage(item.volume.id)"
[read]="item.volume.pagesRead" [total]="item.volume.pages" [actions]="volumeActions" (selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)" [selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true"></app-card-item>
[read]="item.volume.pagesRead" [total]="item.volume.pages" [actions]="volumeActions"
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, volumes.length, $event)"
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
</ng-container>
<ng-template #chapterCardItem>
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.chapter.isSpecial" [entity]="item.chapter" [title]="item.chapter.title" (click)="openChapter(item.chapter)"
[imageUrl]="imageService.getChapterCoverImage(item.chapter.id)"
[read]="item.chapter.pagesRead" [total]="item.chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('chapter', idx, storyChapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item>
[read]="item.chapter.pagesRead" [total]="item.chapter.pages" [actions]="chapterActions"
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, storyChapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
</ng-template>
</ng-container>
</div>
@ -170,8 +174,8 @@
<app-card-item class="col-auto mt-2 mb-2" [entity]="item" [title]="item.name" (click)="openVolume(item)"
[imageUrl]="imageService.getVolumeCoverImage(item.id)"
[read]="item.pagesRead" [total]="item.pages" [actions]="volumeActions"
(selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)"
[selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true">
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, volumes.length, $event)"
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
</app-card-item>
</ng-container>
</div>
@ -203,8 +207,8 @@
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.isSpecial" [entity]="item" [title]="item.title" (click)="openChapter(item)"
[imageUrl]="imageService.getChapterCoverImage(item.id)"
[read]="item.pagesRead" [total]="item.pages" [actions]="chapterActions"
(selection)="bulkSelectionService.handleCardSelection('chapter', idx, chapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true">
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, chapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="item" [seriesName]="series.name" [includeVolume]="true"></app-entity-title>
</ng-container>
@ -239,8 +243,8 @@
<app-card-item class="col-auto mt-2 mb-2" [entity]="item" [title]="item.title || item.range" (click)="openChapter(item)"
[imageUrl]="imageService.getChapterCoverImage(item.id)"
[read]="item.pagesRead" [total]="item.pages" [actions]="chapterActions"
(selection)="bulkSelectionService.handleCardSelection('special', idx, chapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('special', idx)" [allowSelection]="true">
(selection)="bulkSelectionService.handleCardSelection('special', scroll.viewPortInfo.startIndexWithBuffer + idx, chapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('special', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
</app-card-item>
</ng-container>
</div>

View file

@ -22,7 +22,7 @@
// This is responsible for ensuring we scroll down and only tabs and companion bar is visible
.main-container {
// Height set dynamically by getHeight()
// Height set dynamically by get ScrollingBlockHeight()
overflow-y: auto;
position: relative;
overscroll-behavior-y: none;

View file

@ -1,9 +1,9 @@
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, Renderer2, AfterViewInit, Inject } from '@angular/core';
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, AfterViewInit, Inject, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { forkJoin, fromEvent, Subject, debounceTime } from 'rxjs';
import { forkJoin, Subject } from 'rxjs';
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
import { BulkSelectionService } from '../cards/bulk-selection.service';
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
@ -61,7 +61,8 @@ interface StoryLineItem {
@Component({
selector: 'app-series-detail',
templateUrl: './series-detail.component.html',
styleUrls: ['./series-detail.component.scss']
styleUrls: ['./series-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
@ -177,6 +178,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
case Action.AddToReadingList:
this.actionService.addMultipleToReadingList(seriesId, selectedVolumeIds, chapters, () => {
this.actionInProgress = false;
this.changeDetectionRef.markForCheck();
this.bulkSelectionService.deselectAll();
});
break;
@ -184,6 +186,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
this.actionService.markMultipleAsRead(seriesId, selectedVolumeIds, chapters, () => {
this.setContinuePoint();
this.actionInProgress = false;
this.changeDetectionRef.markForCheck();
this.bulkSelectionService.deselectAll();
});
@ -192,6 +195,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
this.actionService.markMultipleAsUnread(seriesId, selectedVolumeIds, chapters, () => {
this.setContinuePoint();
this.actionInProgress = false;
this.changeDetectionRef.markForCheck();
this.bulkSelectionService.deselectAll();
});
break;
@ -242,8 +246,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
private downloadService: DownloadService, private actionService: ActionService,
public imageSerivce: ImageService, private messageHub: MessageHubService,
private readingListService: ReadingListService, public navService: NavService,
private offcanvasService: NgbOffcanvas, private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document
private offcanvasService: NgbOffcanvas, @Inject(DOCUMENT) private document: Document,
private changeDetectionRef: ChangeDetectorRef
) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
@ -252,6 +256,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
this.hasDownloadingRole = this.accountService.hasDownloadRole(user);
this.renderMode = user.preferences.globalPageLayoutMode;
this.pageExtrasGroup.get('renderMode')?.setValue(this.renderMode);
this.changeDetectionRef.markForCheck();
}
});
}
@ -283,10 +288,12 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
this.seriesId = parseInt(routeId, 10);
this.libraryId = parseInt(libraryId, 10);
this.seriesImage = this.imageService.getSeriesCoverImage(this.seriesId);
this.changeDetectionRef.markForCheck();
this.loadSeries(this.seriesId);
this.pageExtrasGroup.get('renderMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((val: PageLayoutMode) => {
this.renderMode = val;
this.changeDetectionRef.markForCheck();
});
}
@ -296,15 +303,16 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
}
ngAfterViewInit(): void {
this.initScroll();
// this.initScroll();
}
initScroll() {
if (this.scrollingBlock === undefined || this.scrollingBlock.nativeElement === undefined) {
setTimeout(() => {this.initScroll()}, 10);
return;
}
}
// initScroll() {
// // TODO: Remove this code?
// if (this.scrollingBlock === undefined || this.scrollingBlock.nativeElement === undefined) {
// setTimeout(() => {this.initScroll()}, 10);
// return;
// }
// }
@HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) {
@ -322,10 +330,12 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
onNavChange(event: NgbNavChangeEvent) {
this.bulkSelectionService.deselectAll();
this.changeDetectionRef.markForCheck();
}
handleSeriesActionCallback(action: Action, series: Series) {
this.actionInProgress = true;
this.changeDetectionRef.markForCheck();
switch(action) {
case(Action.MarkAsRead):
this.actionService.markSeriesAsRead(series, (series: Series) => {
@ -416,6 +426,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
async deleteSeries(series: Series) {
this.actionService.deleteSeries(series, (result: boolean) => {
this.actionInProgress = false;
this.changeDetectionRef.markForCheck();
if (result) {
this.router.navigate(['library', this.libraryId]);
}
@ -426,6 +437,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
this.seriesService.getMetadata(seriesId).subscribe(metadata => this.seriesMetadata = metadata);
this.readingListService.getReadingListsForSeries(seriesId).subscribe(lists => {
this.readingLists = lists;
this.changeDetectionRef.markForCheck();
});
this.setContinuePoint();
@ -464,6 +476,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
];
if (this.relations.length > 0) {
this.hasRelations = true;
this.changeDetectionRef.markForCheck();
}
});
@ -487,6 +500,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
this.updateSelectedTab();
this.isLoading = false;
this.changeDetectionRef.markForCheck();
});
}, err => {
this.router.navigateByUrl('/libraries');
@ -523,11 +537,18 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
createHTML() {
this.userReview = (this.series.userReview === null ? '' : this.series.userReview).replace(/\n/g, '<br>');
this.changeDetectionRef.markForCheck();
}
setContinuePoint() {
this.readerService.hasSeriesProgress(this.seriesId).subscribe(hasProgress => this.hasReadingProgress = hasProgress);
this.readerService.getCurrentChapter(this.seriesId).subscribe(chapter => this.currentlyReadingChapter = chapter);
this.readerService.hasSeriesProgress(this.seriesId).subscribe(hasProgress => {
this.hasReadingProgress = hasProgress;
this.changeDetectionRef.markForCheck();
});
this.readerService.getCurrentChapter(this.seriesId).subscribe(chapter => {
this.currentlyReadingChapter = chapter;
this.changeDetectionRef.markForCheck();
});
}
markVolumeAsRead(vol: Volume) {
@ -538,6 +559,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
this.actionService.markVolumeAsRead(this.seriesId, vol, () => {
this.setContinuePoint();
this.actionInProgress = false;
this.changeDetectionRef.markForCheck();
});
}
@ -549,6 +571,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
this.actionService.markVolumeAsUnread(this.seriesId, vol, () => {
this.setContinuePoint();
this.actionInProgress = false;
this.changeDetectionRef.markForCheck();
});
}
@ -560,6 +583,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
this.actionService.markChapterAsRead(this.seriesId, chapter, () => {
this.setContinuePoint();
this.actionInProgress = false;
this.changeDetectionRef.markForCheck();
});
}
@ -571,6 +595,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => {
this.setContinuePoint();
this.actionInProgress = false;
this.changeDetectionRef.markForCheck();
});
}
@ -626,7 +651,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
}
isNullOrEmpty(val: string) {
return val === null || val === undefined || val === '';
return val === null || val === undefined || val === ''; // TODO: Validate if this code is used
}
openViewInfo(data: Volume | Chapter) {
@ -645,6 +670,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
if (closeResult.success) {
this.seriesService.getSeries(this.seriesId).subscribe(s => {
this.series = s;
this.changeDetectionRef.markForCheck();
});
this.loadSeries(this.seriesId);
@ -693,12 +719,14 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
const wantToDownload = await this.downloadService.confirmSize(size, 'series');
if (!wantToDownload) { return; }
this.downloadInProgress = true;
this.changeDetectionRef.markForCheck();
this.downloadService.downloadSeries(this.series).pipe(
takeWhile(val => {
return val.state != 'DONE';
}),
finalize(() => {
this.downloadInProgress = false;
this.changeDetectionRef.markForCheck();
})).subscribe(() => {/* No Operation */});;
});
}