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:
parent
2ed0aca866
commit
f54eb5865b
74 changed files with 626 additions and 436 deletions
|
|
@ -7,8 +7,8 @@
|
|||
</div>
|
||||
<div class="modal-body {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[0]">
|
||||
<a ngbNavLink>{{tabs[0]}}</a>
|
||||
<li [ngbNavItem]="tabs[TabID.General].id">
|
||||
<a ngbNavLink>{{tabs[TabID.General].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p>
|
||||
This tag is currently {{tag?.promoted ? 'promoted' : 'not promoted'}} (<i class="fa fa-angle-double-up" aria-hidden="true"></i>).
|
||||
|
|
@ -49,8 +49,8 @@
|
|||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="tabs[1]">
|
||||
<a ngbNavLink>{{tabs[1]}}</a>
|
||||
<li [ngbNavItem]="tabs[TabID.CoverImage].id">
|
||||
<a ngbNavLink>{{tabs[TabID.CoverImage].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p class="alert alert-primary" role="alert">
|
||||
Upload and choose a new cover image. Press Save to upload and override the cover.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ import { SeriesService } from 'src/app/_services/series.service';
|
|||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
|
||||
|
||||
enum TabID {
|
||||
General = 0,
|
||||
CoverImage = 1,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-collection-tags',
|
||||
templateUrl: './edit-collection-tags.component.html',
|
||||
|
|
@ -32,8 +37,8 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
selectAll: boolean = true;
|
||||
libraryNames!: any;
|
||||
collectionTagForm!: FormGroup;
|
||||
tabs = ['General', 'Cover Image'];
|
||||
active = this.tabs[0];
|
||||
tabs = [{title: 'General', id: TabID.General}, {title: 'Cover Image', id: TabID.CoverImage}];
|
||||
active = TabID.General;
|
||||
imageUrls: Array<string> = [];
|
||||
selectedCover: string = '';
|
||||
|
||||
|
|
@ -45,6 +50,10 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
return Breakpoint;
|
||||
}
|
||||
|
||||
get TabID() {
|
||||
return TabID;
|
||||
}
|
||||
|
||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
|
||||
private collectionService: CollectionTagService, private toastr: ToastrService,
|
||||
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@
|
|||
<h4>Information</h4>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6" *ngIf="libraryName">Library: {{libraryName | sentenceCase}}</div>
|
||||
<div class="col-md-6">Format: <app-tag-badge>{{utilityService.mangaFormat(series.format)}}</app-tag-badge></div>
|
||||
<div class="col-md-6">Format: <app-tag-badge>{{series.format | mangaFormat}}</app-tag-badge></div>
|
||||
</div>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6" >Created: {{series.created | date:'shortDate'}}</div>
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
|
||||
<div class="viewport-container" #scrollingBlock>
|
||||
<div class="viewport-container">
|
||||
<div class="content-container">
|
||||
|
||||
<div class="card-container mt-2 mb-2">
|
||||
<virtual-scroller #scroll [items]="items" (vsEnd)="fetchMore($event)" [bufferAmount]="1">
|
||||
<virtual-scroller #scroll [items]="items" [bufferAmount]="1" [parentScroll]="parentScroll">
|
||||
<div class="grid row g-0" #container>
|
||||
<div class="card col-auto mt-2 mb-2" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: scroll.viewPortInfo.startIndexWithBuffer + i }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</virtual-scroller>
|
||||
|
|
@ -48,53 +48,6 @@
|
|||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #paginationTemplate let-id="id">
|
||||
|
||||
<div class="d-flex justify-content-center mb-0" *ngIf="pagination && items.length > 0">
|
||||
<ngb-pagination
|
||||
*ngIf="pagination.totalPages > 1"
|
||||
[maxSize]="8"
|
||||
[rotate]="true"
|
||||
[ellipses]="false"
|
||||
[(page)]="pagination.currentPage"
|
||||
[pageSize]="pagination.itemsPerPage"
|
||||
(pageChange)="onPageChange($event)"
|
||||
[collectionSize]="pagination.totalItems">
|
||||
|
||||
<ng-template ngbPaginationPages let-page let-pages="pages" *ngIf="pagination.totalItems / pagination.itemsPerPage > 20">
|
||||
<li class="ngb-custom-pages-item" *ngIf="pagination.totalPages > 1">
|
||||
<div class="d-flex flex-nowrap px-2">
|
||||
<label
|
||||
id="paginationInputLabel-{{id}}"
|
||||
for="paginationInput-{{id}}"
|
||||
class="col-form-label me-2 ms-1 form-label"
|
||||
>Page</label>
|
||||
<input #i
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
class="form-control custom-pages-input"
|
||||
id="paginationInput-{{id}}"
|
||||
[value]="page"
|
||||
(keyup.enter)="selectPageStr(i.value)"
|
||||
(blur)="selectPageStr(i.value)"
|
||||
(input)="formatInput($any($event).target)"
|
||||
attr.aria-labelledby="paginationInputLabel-{{id}} paginationDescription-{{id}}"
|
||||
[ngStyle]="{width: (0.5 + pagination.currentPage + '').length + 'rem'} "
|
||||
/>
|
||||
<span id="paginationDescription-{{id}}" class="col-form-label text-nowrap px-2">
|
||||
of {{pagination.totalPages}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ng-template>
|
||||
|
||||
</ngb-pagination>
|
||||
</div>
|
||||
|
||||
<!-- <ng-container *ngIf="pagination && items.length > 0 && id == 'bottom' && pagination.totalPages > 1 " [ngTemplateOutlet]="jumpBar"></ng-container> -->
|
||||
</ng-template>
|
||||
|
||||
|
||||
<div class="mx-auto" *ngIf="isLoading" style="width: 200px;">
|
||||
<div class="spinner-border text-secondary loading" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,35 @@
|
|||
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { AfterViewInit, Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, NgZone, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewChild } from '@angular/core';
|
||||
import { IPageInfo, VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
|
||||
import { filter, from, map, pairwise, Subject, tap, throttleTime } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewChild } from '@angular/core';
|
||||
import { VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
|
||||
import { Subject } 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';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { PaginatedResult, Pagination } from 'src/app/_models/pagination';
|
||||
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 { SeriesService } from 'src/app/_services/series.service';
|
||||
|
||||
const FILTER_PAG_REGEX = /[^0-9]/g;
|
||||
const SCROLL_BREAKPOINT = 300;
|
||||
const keySize = 24;
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-detail-layout',
|
||||
templateUrl: './card-detail-layout.component.html',
|
||||
styleUrls: ['./card-detail-layout.component.scss']
|
||||
styleUrls: ['./card-detail-layout.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewInit, OnChanges {
|
||||
export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
@Input() header: string = '';
|
||||
@Input() isLoading: boolean = false;
|
||||
@Input() items: any[] = [];
|
||||
// ?! we need to have chunks to render in, because if we scroll down, then up, then down, we don't want to trigger a duplicate call
|
||||
@Input() paginatedItems: PaginatedResult<any> | undefined;
|
||||
@Input() pagination!: Pagination;
|
||||
/**
|
||||
* Parent scroll for virtualize pagination
|
||||
*/
|
||||
@Input() parentScroll!: Element | Window;
|
||||
|
||||
// Filter Code
|
||||
@Input() filterOpen!: EventEmitter<boolean>;
|
||||
|
|
@ -48,8 +49,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
jumpBarKeysToRender: Array<JumpKey> = []; // Original
|
||||
|
||||
@Output() itemClicked: EventEmitter<any> = new EventEmitter();
|
||||
@Output() pageChange: EventEmitter<Pagination> = new EventEmitter();
|
||||
@Output() pageChangeWithDirection: EventEmitter<0 | 1> = new EventEmitter();
|
||||
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
|
||||
|
||||
@ContentChild('cardItem') itemTemplate!: TemplateRef<any>;
|
||||
|
|
@ -59,8 +58,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
|
||||
@ViewChild(VirtualScrollerComponent) private virtualScroller!: VirtualScrollerComponent;
|
||||
|
||||
itemSize: number = 100; // Idk what this actually does. Less results in more items rendering, 5 works well with pagination. 230 is technically what a card is height wise
|
||||
|
||||
filter!: SeriesFilter;
|
||||
libraries: Array<FilterItem<Library>> = [];
|
||||
|
||||
|
|
@ -73,8 +70,9 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
}
|
||||
|
||||
constructor(private seriesService: SeriesService, public utilityService: UtilityService,
|
||||
@Inject(DOCUMENT) private document: Document, private ngZone: NgZone) {
|
||||
@Inject(DOCUMENT) private document: Document, private changeDetectionRef: ChangeDetectorRef) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
|
|
@ -83,6 +81,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -94,12 +94,13 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
this.jumpBarKeysToRender = [];
|
||||
|
||||
const removalTimes = Math.ceil(removeCount / 2);
|
||||
const midPoint = this.jumpBarKeys.length / 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.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
removeSecondPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1) {
|
||||
|
|
@ -141,16 +142,18 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
|
||||
ngOnInit(): void {
|
||||
if (this.trackByIdentity === undefined) {
|
||||
this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`; // ${this.pagination?.currentPage}_
|
||||
this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`;
|
||||
}
|
||||
|
||||
|
||||
if (this.filterSettings === undefined) {
|
||||
this.filterSettings = new FilterSettings();
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
if (this.pagination === undefined) {
|
||||
this.pagination = {currentPage: 1, itemsPerPage: this.items.length, totalItems: this.items.length, totalPages: 1}
|
||||
this.pagination = {currentPage: 1, itemsPerPage: this.items.length, totalItems: this.items.length, totalPages: 1};
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,58 +162,12 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
this.resizeJumpBar();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// this.scroller.elementScrolled().pipe(
|
||||
// map(() => this.scroller.measureScrollOffset('bottom')),
|
||||
// pairwise(),
|
||||
// filter(([y1, y2]) => ((y2 < y1 && y2 < SCROLL_BREAKPOINT))), // 140
|
||||
// throttleTime(200)
|
||||
// ).subscribe(([y1, y2]) => {
|
||||
// const movingForward = y2 < y1;
|
||||
// if (this.pagination.currentPage === this.pagination.totalPages || this.pagination.currentPage === 1 && !movingForward) return;
|
||||
// this.ngZone.run(() => {
|
||||
// console.log('Load next pages');
|
||||
|
||||
// this.pagination.currentPage = this.pagination.currentPage + 1;
|
||||
// this.pageChangeWithDirection.emit(1);
|
||||
// });
|
||||
// });
|
||||
|
||||
// this.scroller.elementScrolled().pipe(
|
||||
// map(() => this.scroller.measureScrollOffset('top')),
|
||||
// pairwise(),
|
||||
// filter(([y1, y2]) => y2 >= y1 && y2 < SCROLL_BREAKPOINT),
|
||||
// throttleTime(200)
|
||||
// ).subscribe(([y1, y2]) => {
|
||||
// if (this.pagination.currentPage === 1) return;
|
||||
// this.ngZone.run(() => {
|
||||
// console.log('Load prev pages');
|
||||
|
||||
// this.pagination.currentPage = this.pagination.currentPage - 1;
|
||||
// this.pageChangeWithDirection.emit(0);
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestory.next();
|
||||
this.onDestory.complete();
|
||||
}
|
||||
|
||||
|
||||
onPageChange(page: number) {
|
||||
this.pageChange.emit(this.pagination);
|
||||
}
|
||||
|
||||
selectPageStr(page: string) {
|
||||
this.pagination.currentPage = parseInt(page, 10) || 1;
|
||||
this.onPageChange(this.pagination.currentPage);
|
||||
}
|
||||
|
||||
formatInput(input: HTMLInputElement) {
|
||||
input.value = input.value.replace(FILTER_PAG_REGEX, '');
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, undefined);
|
||||
|
|
@ -220,63 +177,19 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
applyMetadataFilter(event: FilterEvent) {
|
||||
this.applyFilter.emit(event);
|
||||
this.updateApplied++;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
loading: boolean = false;
|
||||
fetchMore(event: IPageInfo) {
|
||||
if (event.endIndex !== this.items.length - 1) return;
|
||||
if (event.startIndex < 0) return;
|
||||
console.log('Requesting next page ', (this.pagination.currentPage + 1), 'of data', event);
|
||||
this.loading = true;
|
||||
|
||||
// this.pagination.currentPage = this.pagination.currentPage + 1;
|
||||
// this.pageChangeWithDirection.emit(1);
|
||||
|
||||
// this.fetchNextChunk(this.items.length, 10).then(chunk => {
|
||||
// this.items = this.items.concat(chunk);
|
||||
// this.loading = false;
|
||||
// }, () => this.loading = false);
|
||||
}
|
||||
|
||||
scrollTo(jumpKey: JumpKey) {
|
||||
// TODO: Figure out how to do this
|
||||
|
||||
let targetIndex = 0;
|
||||
for(var i = 0; i < this.jumpBarKeys.length; i++) {
|
||||
if (this.jumpBarKeys[i].key === jumpKey.key) break;
|
||||
targetIndex += this.jumpBarKeys[i].size;
|
||||
}
|
||||
//console.log('scrolling to card that starts with ', jumpKey.key, + ' with index of ', targetIndex);
|
||||
|
||||
// Infinite scroll
|
||||
this.virtualScroller.scrollToIndex(targetIndex, true, undefined, 1000);
|
||||
this.changeDetectionRef.markForCheck();
|
||||
return;
|
||||
|
||||
// Basic implementation based on itemsPerPage being the same.
|
||||
//var minIndex = this.pagination.currentPage * this.pagination.itemsPerPage;
|
||||
var targetPage = Math.max(Math.ceil(targetIndex / this.pagination.itemsPerPage), 1);
|
||||
//console.log('We are on page ', this.pagination.currentPage, ' and our target page is ', targetPage);
|
||||
if (targetPage === this.pagination.currentPage) {
|
||||
// Scroll to the element
|
||||
const elem = this.document.querySelector(`div[id="jumpbar-index--${targetIndex}"`);
|
||||
if (elem !== null) {
|
||||
|
||||
this.virtualScroller.scrollToIndex(targetIndex);
|
||||
// elem.scrollIntoView({
|
||||
// behavior: 'smooth'
|
||||
// });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// With infinite scroll, we can't just jump to a random place, because then our list of items would be out of sync.
|
||||
this.selectPageStr(targetPage + '');
|
||||
//this.pageChangeWithDirection.emit(1);
|
||||
|
||||
// if (minIndex > targetIndex) {
|
||||
// // We need to scroll forward (potentially to another page)
|
||||
// } else if (minIndex < targetIndex) {
|
||||
// // We need to scroll back (potentially to another page)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@
|
|||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">(promoted)</span>
|
||||
</span>
|
||||
<i class="fa {{utilityService.mangaFormatIcon(format)}}" aria-hidden="true" *ngIf="format != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(format)}}"></i><span class="visually-hidden">{{utilityService.mangaFormat(format)}}</span>
|
||||
<ng-container *ngIf="format | mangaFormat as formatString">
|
||||
<i class="fa {{format | mangaFormatIcon}}" aria-hidden="true" *ngIf="format != MangaFormat.UNKNOWN" title="{{formatString}}"></i>
|
||||
<span class="visually-hidden">{{formatString}}</span>
|
||||
</ng-container>
|
||||
{{title}}
|
||||
</span>
|
||||
<span class="card-actions float-end">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { filter, finalize, map, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
|
|
@ -25,7 +25,8 @@ import { BulkSelectionService } from '../bulk-selection.service';
|
|||
@Component({
|
||||
selector: 'app-card-item',
|
||||
templateUrl: './card-item.component.html',
|
||||
styleUrls: ['./card-item.component.scss']
|
||||
styleUrls: ['./card-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CardItemComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
|
@ -112,6 +113,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
* Handles touch events for selection on mobile devices to ensure you aren't touch scrolling
|
||||
*/
|
||||
prevOffset: number = 0;
|
||||
selectionInProgress: boolean = false;
|
||||
|
||||
private user: User | undefined;
|
||||
|
||||
|
|
@ -130,11 +132,12 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
constructor(public imageService: ImageService, private libraryService: LibraryService,
|
||||
public utilityService: UtilityService, private downloadService: DownloadService,
|
||||
private toastr: ToastrService, public bulkSelectionService: BulkSelectionService,
|
||||
private messageHub: MessageHubService, private accountService: AccountService, private scrollService: ScrollService) {}
|
||||
private messageHub: MessageHubService, private accountService: AccountService, private scrollService: ScrollService, private changeDetectionRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
||||
this.supressArchiveWarning = true;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
if (this.suppressLibraryLink === false) {
|
||||
|
|
@ -145,6 +148,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
if (this.libraryId !== undefined && this.libraryId > 0) {
|
||||
this.libraryService.getLibraryName(this.libraryId).pipe(takeUntil(this.onDestroy)).subscribe(name => {
|
||||
this.libraryName = name;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -171,6 +175,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
||||
|
||||
this.read = updateEvent.pagesRead;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -179,6 +184,12 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
@HostListener('touchmove', ['$event'])
|
||||
onTouchMove(event: TouchEvent) {
|
||||
if (!this.allowSelection) return;
|
||||
|
||||
this.selectionInProgress = false;
|
||||
}
|
||||
|
||||
@HostListener('touchstart', ['$event'])
|
||||
onTouchStart(event: TouchEvent) {
|
||||
|
|
@ -186,6 +197,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.prevTouchTime = event.timeStamp;
|
||||
this.prevOffset = this.scrollService.scrollPosition;
|
||||
this.selectionInProgress = true;
|
||||
}
|
||||
|
||||
@HostListener('touchend', ['$event'])
|
||||
|
|
@ -194,12 +206,13 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
const delta = event.timeStamp - this.prevTouchTime;
|
||||
const verticalOffset = this.scrollService.scrollPosition;
|
||||
|
||||
if (delta >= 300 && delta <= 1000 && (verticalOffset === this.prevOffset)) {
|
||||
if (delta >= 300 && delta <= 1000 && (verticalOffset === this.prevOffset) && this.selectionInProgress) {
|
||||
this.handleSelection();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
this.prevTouchTime = 0;
|
||||
this.selectionInProgress = false;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -207,10 +220,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
this.clicked.emit(this.title);
|
||||
}
|
||||
|
||||
isNullOrEmpty(val: string) {
|
||||
return val === null || val === undefined || val === '';
|
||||
}
|
||||
|
||||
preventClick(event: any) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
|
@ -229,6 +238,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
const wantToDownload = await this.downloadService.confirmSize(size, 'volume');
|
||||
if (!wantToDownload) { return; }
|
||||
this.downloadInProgress = true;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.download$ = this.downloadService.downloadVolume(volume).pipe(
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
|
|
@ -236,6 +246,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}));
|
||||
});
|
||||
} else if (this.utilityService.isChapter(this.entity)) {
|
||||
|
|
@ -244,6 +255,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
const wantToDownload = await this.downloadService.confirmSize(size, 'chapter');
|
||||
if (!wantToDownload) { return; }
|
||||
this.downloadInProgress = true;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.download$ = this.downloadService.downloadChapter(chapter).pipe(
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
|
|
@ -251,6 +263,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}));
|
||||
});
|
||||
} else if (this.utilityService.isSeries(this.entity)) {
|
||||
|
|
@ -259,6 +272,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
const wantToDownload = await this.downloadService.confirmSize(size, 'series');
|
||||
if (!wantToDownload) { return; }
|
||||
this.downloadInProgress = true;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.download$ = this.downloadService.downloadSeries(series).pipe(
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
|
|
@ -266,6 +280,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ import { SeriesInfoCardsComponent } from './series-info-cards/series-info-cards.
|
|||
|
||||
NgbOffcanvasModule, // Series Detail, action of cards
|
||||
NgbNavModule, //Series Detail
|
||||
NgbPaginationModule, // CardDetailLayoutComponent
|
||||
NgbPaginationModule, // EditCollectionTagsComponent
|
||||
NgbDropdownModule,
|
||||
NgbProgressbarModule,
|
||||
NgxFileDropModule, // Cover Chooser
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
||||
<div class="pe-2">
|
||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" [width]="imageWidth"></app-image>
|
||||
|
||||
<div class="not-read-badge" *ngIf="pagesRead === 0 && totalPages > 0"></div>
|
||||
<span class="download" *ngIf="download$ | async as download">
|
||||
<app-circular-loader [currentValue]="download.progress"></app-circular-loader>
|
||||
<span class="visually-hidden" role="status">
|
||||
{{download.progress}}% downloaded
|
||||
</span>
|
||||
</span>
|
||||
<div class="progress-banner" *ngIf="totalPages > 0">
|
||||
<div class="progress-banner" *ngIf="pagesRead < totalPages && totalPages > 0 && pagesRead !== totalPages">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="pagesRead" [max]="totalPages"></ngb-progressbar></p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
$image-height: 230px;
|
||||
$image-width: 160px;
|
||||
$triangle-size: 30px;
|
||||
|
||||
.download {
|
||||
width: 80px;
|
||||
|
|
@ -19,8 +20,18 @@ $image-width: 160px;
|
|||
}
|
||||
|
||||
.list-item-container {
|
||||
background: rgb(0,0,0);
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
|
||||
background: var(--card-list-item-bg-color);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.not-read-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 108px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 $triangle-size $triangle-size 0;
|
||||
border-color: transparent var(--primary-color) transparent transparent;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
|
@ -8,17 +8,16 @@ import { AccountService } from 'src/app/_services/account.service';
|
|||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { ActionService } from 'src/app/_services/action.service';
|
||||
import { EditSeriesModalComponent } from '../_modals/edit-series-modal/edit-series-modal.component';
|
||||
import { MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
import { Subject } from 'rxjs';
|
||||
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-card',
|
||||
templateUrl: './series-card.component.html',
|
||||
styleUrls: ['./series-card.component.scss']
|
||||
styleUrls: ['./series-card.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() data!: Series;
|
||||
|
|
@ -52,9 +51,9 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
|
||||
constructor(private accountService: AccountService, private router: Router,
|
||||
private seriesService: SeriesService, private toastr: ToastrService,
|
||||
private modalService: NgbModal, private confirmService: ConfirmService,
|
||||
public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, private hubService: MessageHubService) {
|
||||
private modalService: NgbModal, private imageService: ImageService,
|
||||
private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, private changeDetectionRef: ChangeDetectorRef) {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
|
|
@ -72,8 +71,6 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
ngOnChanges(changes: any) {
|
||||
if (this.data) {
|
||||
this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series));
|
||||
//this.imageUrl = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.data.id));
|
||||
this.imageUrl = this.imageService.getSeriesCoverImage(this.data.id); // TODO: Do I need to do this since image now handles updates?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,13 +117,10 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
const modalRef = this.modalService.open(EditSeriesModalComponent, { size: 'lg' });
|
||||
modalRef.componentInstance.series = data;
|
||||
modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => {
|
||||
window.scrollTo(0, 0);
|
||||
if (closeResult.success) {
|
||||
if (closeResult.coverImageUpdate) {
|
||||
this.imageUrl = this.imageService.randomize(this.imageService.getSeriesCoverImage(closeResult.series.id));
|
||||
}
|
||||
this.seriesService.getSeries(data.id).subscribe(series => {
|
||||
this.data = series;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.reload.emit(true);
|
||||
this.dataChanged.emit(series);
|
||||
});
|
||||
|
|
@ -156,6 +150,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.actionService.markSeriesAsUnread(series, () => {
|
||||
if (this.data) {
|
||||
this.data.pagesRead = 0;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
this.dataChanged.emit(series);
|
||||
|
|
@ -166,6 +161,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.actionService.markSeriesAsRead(series, () => {
|
||||
if (this.data) {
|
||||
this.data.pagesRead = series.pages;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
this.dataChanged.emit(series);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@
|
|||
<ng-container *ngIf="series">
|
||||
<ng-container>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Format" [clickable]="true" [fontClasses]="'fa ' + utilityService.mangaFormatIcon(series.format)" (click)="handleGoTo(FilterQueryParam.Format, series.format)" title="Format">
|
||||
{{utilityService.mangaFormat(series.format)}}
|
||||
<app-icon-and-title label="Format" [clickable]="true" [fontClasses]="'fa ' + (series.format | mangaFormatIcon)" (click)="handleGoTo(FilterQueryParam.Format, series.format)" title="Format">
|
||||
{{series.format | mangaFormat}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue