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

@ -21,4 +21,8 @@ export interface ReadingList {
promoted: boolean;
coverImageLocked: boolean;
items: Array<ReadingListItem>;
/**
* If this is empty or null, the cover image isn't set. Do not use this externally.
*/
coverImage: string;
}

View file

@ -41,7 +41,8 @@ export enum SortField {
SortName = 1,
Created = 2,
LastModified = 3,
LastChapterAdded = 4
LastChapterAdded = 4,
TimeToRead = 5
}
export interface ReadStatus {

View file

@ -1,4 +1,8 @@
export interface DirectoryDto {
name: string;
fullPath: string;
/**
* This is only on the UI to disable paths
*/
disabled: boolean;
}

View file

@ -35,7 +35,8 @@ export class AccountService implements OnDestroy {
constructor(private httpClient: HttpClient, private router: Router,
private messageHub: MessageHubService, private themeService: ThemeService) {
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
map(evt => evt.payload as UserUpdateEvent),
map(evt => evt.payload as UserUpdateEvent),
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
switchMap(() => this.refreshToken()))
.subscribe(() => {});
}

View file

@ -1,4 +1,4 @@
import { ElementRef, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'

View file

@ -3,14 +3,6 @@
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
</div>
<div class="modal-body">
<!-- <div class="mb-3">
<label for="filter" class="form-label">Filter</label>
<div class="input-group">
<input id="filter" autocomplete="off" class="form-control" [(ngModel)]="filterQuery" type="text" aria-describedby="reset-input">
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="filterQuery = '';">Clear</button>
</div>
</div> -->
<div class="mb-3">
<label for="filter" class="form-label">Path</label>
@ -46,7 +38,7 @@
<table class="table table-striped scrollable">
<thead>
<tr>
<th scope="col">Type</th>
<th scope="col" style="width: 40px;">Type</th>
<th scope="col">Name</th>
</tr>
</thead>
@ -55,7 +47,7 @@
<td><i class="fa-solid fa-arrow-turn-up" aria-hidden="true"></i></td>
<td>...</td>
</tr>
<tr *ngFor="let folder of folders; let idx = index;" (click)="selectNode(folder)">
<tr *ngFor="let folder of folders; let idx = index;" (click)="selectNode(folder)" style="cursor: pointer;" [ngClass]="{'disabled': folder.disabled}">
<td><i class="fa-regular fa-folder" aria-hidden="true"></i></td>
<td id="folder--{{idx}}">
{{folder.name}}

View file

@ -16,4 +16,10 @@ $breadcrumb-divider: quote(">");
.table {
background-color: lightgrey;
}
.disabled {
color: lightgrey !important;
cursor: not-allowed !important;
background-color: var(--error-color);
}

View file

@ -91,6 +91,7 @@ export class DirectoryPickerComponent implements OnInit {
selectNode(folder: DirectoryDto) {
if (folder.disabled) return;
this.currentRoot = folder.name;
this.routeStack.push(folder.name);
this.path = folder.fullPath;
@ -116,6 +117,10 @@ export class DirectoryPickerComponent implements OnInit {
}, err => {
// If there was an error, pop off last directory added to stack
this.routeStack.pop();
const item = this.folders.find(f => f.fullPath === path);
if (item) {
item.disabled = true;
}
});
}

View file

@ -11,4 +11,5 @@ export interface ServerSettings {
emailServiceUrl: string;
convertBookmarkToWebP: boolean;
enableSwaggerUi: boolean;
totalBackups: number;
}

View file

@ -21,14 +21,32 @@
</div>
<div class="row g-0 mb-2">
<div class="col-md-6 col-sm-12 pe-2">
<div class="col-md-4 col-sm-12 pe-2">
<label for="settings-port" class="form-label">Port</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>
<span class="visually-hidden" id="settings-port-help">Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</span>
<input id="settings-port" aria-describedby="settings-port-help" class="form-control" formControlName="port" type="number" step="1" min="1" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
</div>
<div class="col-md-4 col-sm-12 pe-2">
<label for="backup-tasks" class="form-label">Backup Tasks</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="backupTasksTooltip" role="button" tabindex="0"></i>
<ng-template #backupTasksTooltip>The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</ng-template>
<span class="visually-hidden" id="backup-tasks-help">The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</span>
<input id="backup-tasks" aria-describedby="backup-tasks-help" class="form-control" formControlName="totalBackups" type="number" step="1" min="1" max="30" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
<ng-container *ngIf="settingsForm.get('totalBackups')?.errors as errors">
<p class="invalid-feedback" *ngIf="errors.min">
You must have at least 1 backup
</p>
<p class="invalid-feedback" *ngIf="errors.max">
You cannot have more than {{errors.max.max}} backups
</p>
<p class="invalid-feedback" *ngIf="errors.required">
This field is required
</p>
</ng-container>
</div>
<div class="col-md-6 col-sm-12">
<div class="col-md-4 col-sm-12">
<label for="logging-level-port" class="form-label">Logging Level</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="loggingLevelTooltip" role="button" tabindex="0"></i>
<ng-template #loggingLevelTooltip>Use debug to help identify issues. Debug can eat up a lot of disk space. Requires restart to take effect.</ng-template>
<span class="visually-hidden" id="logging-level-port-help">Port the server listens on. Requires restart to take effect.</span>

View file

@ -5,4 +5,8 @@
padding: 10px;
color: black;
border-radius: 6px;
}
.invalid-feedback {
display: inherit;
}

View file

@ -43,6 +43,7 @@ export class ManageSettingsComponent implements OnInit {
this.settingsForm.addControl('baseUrl', new FormControl(this.serverSettings.baseUrl, [Validators.required]));
this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
this.settingsForm.addControl('enableSwaggerUi', new FormControl(this.serverSettings.enableSwaggerUi, [Validators.required]));
this.settingsForm.addControl('totalBackups', new FormControl(this.serverSettings.totalBackups, [Validators.required, Validators.min(1), Validators.max(30)]));
});
}
@ -58,6 +59,7 @@ export class ManageSettingsComponent implements OnInit {
this.settingsForm.get('baseUrl')?.setValue(this.serverSettings.baseUrl);
this.settingsForm.get('emailServiceUrl')?.setValue(this.serverSettings.emailServiceUrl);
this.settingsForm.get('enableSwaggerUi')?.setValue(this.serverSettings.enableSwaggerUi);
this.settingsForm.get('totalBackups')?.setValue(this.serverSettings.totalBackups);
}
async saveSettings() {

View file

@ -12,8 +12,8 @@
[pagination]="pagination"
[filterSettings]="filterSettings"
[filterOpen]="filterOpen"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)"
(pageChange)="onPageChange($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()"

View file

@ -7,11 +7,13 @@ import { BulkSelectionService } from '../cards/bulk-selection.service';
import { FilterSettings } from '../metadata-filter/filter-settings';
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
import { JumpKey } from '../_models/jumpbar/jump-key';
import { Pagination } from '../_models/pagination';
import { Series } from '../_models/series';
import { FilterEvent, SeriesFilter } from '../_models/series-filter';
import { Action } from '../_services/action-factory.service';
import { ActionService } from '../_services/action.service';
import { LibraryService } from '../_services/library.service';
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
import { SeriesService } from '../_services/series.service';
@ -31,6 +33,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActiveCheck!: SeriesFilter;
filterActive: boolean = false;
jumpbarKeys: Array<JumpKey> = [];
bulkActionCallback = (action: Action, data: any) => {
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
@ -73,7 +76,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
private titleService: Title, private actionService: ActionService,
public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
private utilityService: UtilityService, private route: ActivatedRoute,
private filterUtilityService: FilterUtilitiesService) {
private filterUtilityService: FilterUtilitiesService, private libraryService: LibraryService) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.titleService.setTitle('Kavita - All Series');
@ -108,6 +111,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
this.bulkSelectionService.isShiftDown = false;
}
}
updateFilter(data: FilterEvent) {
this.filter = data.filter;
@ -118,18 +122,35 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
loadPage() {
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.seriesService.getAllSeries(this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
this.seriesService.getAllSeries(undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result;
const keys: {[key: string]: number} = {};
series.result.forEach(s => {
let ch = s.name.charAt(0);
if (/\d|\#|!|%|@|\(|\)|\^|\*/g.test(ch)) {
ch = '#';
}
if (!keys.hasOwnProperty(ch)) {
keys[ch] = 0;
}
keys[ch] += 1;
});
this.jumpbarKeys = 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;
});
this.pagination = series.pagination;
this.loadingSeries = false;
window.scrollTo(0, 0);
});
}
onPageChange(pagination: Pagination) {
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);
this.loadPage();
}
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
}

View file

@ -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.

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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)
// }
}
}

View file

@ -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>
&nbsp;{{title}}
</span>
<span class="card-actions float-end">

View file

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

View file

@ -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

View file

@ -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>

View file

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

View file

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

View file

@ -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>

View file

@ -8,6 +8,7 @@
[isLoading]="isLoading"
[items]="collections"
[filterOpen]="filterOpen"
[jumpBarKeys]="jumpbarKeys"
>
<ng-template #cardItem let-item let-position="idx">
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions" [imageUrl]="item.coverImage" (clicked)="loadCollection(item)"></app-card-item>

View file

@ -4,6 +4,7 @@ import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
import { CollectionTag } from 'src/app/_models/collection-tag';
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service';
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
import { ImageService } from 'src/app/_services/image.service';
@ -19,6 +20,7 @@ export class AllCollectionsComponent implements OnInit {
isLoading: boolean = true;
collections: CollectionTag[] = [];
collectionTagActions: ActionItem<CollectionTag>[] = [];
jumpbarKeys: Array<JumpKey> = [];
filterOpen: EventEmitter<boolean> = new EventEmitter();
@ -44,7 +46,31 @@ export class AllCollectionsComponent implements OnInit {
this.collectionService.allTags().subscribe(tags => {
this.collections = tags;
this.isLoading = false;
const keys: {[key: string]: number} = {};
tags.forEach(s => {
let ch = s.title.charAt(0);
if (/\d|\#|!|%|@|\(|\)|\^|\*/g.test(ch)) {
ch = '#';
}
if (!keys.hasOwnProperty(ch)) {
keys[ch] = 0;
}
keys[ch] += 1;
});
this.jumpbarKeys = 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;
});
});
}
handleCollectionActionCallback(action: Action, collectionTag: CollectionTag) {

View file

@ -1,12 +1,15 @@
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<ng-container title>
<h2 style="margin-bottom: 0px" *ngIf="collectionTag !== undefined">
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
{{collectionTag.title}}<span *ngIf="collectionTag.promoted">&nbsp;(<i aria-hidden="true" class="fa fa-angle-double-up"></i>)</span>
</h2>
</ng-container>
</app-side-nav-companion-bar>
<div class="container-fluid pt-2" *ngIf="collectionTag !== undefined">
<div #companionBar>
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<ng-container title>
<h2 style="margin-bottom: 0px" *ngIf="collectionTag !== undefined">
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
{{collectionTag.title}}<span class="ms-1" *ngIf="collectionTag.promoted">(<i aria-hidden="true" class="fa fa-angle-double-up"></i>)</span>
</h2>
</ng-container>
</app-side-nav-companion-bar>
</div>
<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">
<app-image maxWidth="481px" [imageUrl]="tagImage"></app-image>
@ -19,15 +22,15 @@
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout
header="Series"
[isLoading]="isLoading"
[items]="series"
[pagination]="seriesPagination"
[filterSettings]="filterSettings"
[filterOpen]="filterOpen"
(pageChange)="onPageChange($event)"
(applyFilter)="updateFilter($event)"
>
header="Series"
[isLoading]="isLoading"
[items]="series"
[pagination]="seriesPagination"
[filterSettings]="filterSettings"
[filterOpen]="filterOpen"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)">
<ng-template #cardItem let-item let-position="idx">
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()"
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"

View file

@ -13,4 +13,26 @@
.read-btn--text {
display: none;
}
}
.card-container {
display: grid;
grid-template-columns: repeat(auto-fill, 158px);
grid-gap: 0.5rem;
justify-content: space-around;
}
.virtual-scroller, virtual-scroller {
width: 100%;
height: calc(100vh - 85px);
max-height: calc(var(--vh)*100 - 170px);
}
// This is responsible for ensuring we scroll down and only tabs and companion bar is visible
.main-container {
// Height set dynamically by get ScrollingBlockHeight()
overflow-y: auto;
position: relative;
overscroll-behavior-y: none;
}

View file

@ -1,4 +1,5 @@
import { Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { 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';
@ -12,6 +13,7 @@ import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilitie
import { KEY_CODES, UtilityService } from 'src/app/shared/_services/utility.service';
import { CollectionTag } from 'src/app/_models/collection-tag';
import { SeriesAddedToCollectionEvent } from 'src/app/_models/events/series-added-to-collection-event';
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
import { Pagination } from 'src/app/_models/pagination';
import { Series } from 'src/app/_models/series';
import { FilterEvent, SeriesFilter } from 'src/app/_models/series-filter';
@ -29,6 +31,9 @@ import { SeriesService } from 'src/app/_services/series.service';
})
export class CollectionDetailComponent implements OnInit, OnDestroy {
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
collectionTag!: CollectionTag;
tagImage: string = '';
isLoading: boolean = true;
@ -43,8 +48,10 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
filterActiveCheck!: SeriesFilter;
filterActive: boolean = false;
jumpbarKeys: Array<JumpKey> = [];
filterOpen: EventEmitter<boolean> = new EventEmitter();
private onDestory: Subject<void> = new Subject<void>();
@ -84,11 +91,22 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
}
}
get ScrollingBlockHeight() {
if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)';
const navbar = this.document.querySelector('.navbar') as HTMLElement;
if (navbar === null) return 'calc(var(--vh)*100)';
const companionHeight = this.companionBar!.nativeElement.offsetHeight;
const navbarHeight = navbar.offsetHeight;
const totalHeight = companionHeight + navbarHeight + 21; //21px to account for padding
return 'calc(var(--vh)*100 - ' + totalHeight + 'px)';
}
constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute,
private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService,
private modalService: NgbModal, private titleService: Title,
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService) {
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
const routeId = this.route.snapshot.paramMap.get('id');
@ -157,16 +175,40 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
});
}
onPageChange(pagination: Pagination) {
this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, undefined);
this.loadPage();
}
// onPageChange(pagination: Pagination) {
// this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, undefined);
// this.loadPage();
// }
loadPage() {
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.seriesService.getAllSeries(this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
this.seriesService.getAllSeries(undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result;
this.seriesPagination = series.pagination;
const keys: {[key: string]: number} = {};
series.result.forEach(s => {
let ch = s.name.charAt(0);
if (/\d|\#|!|%|@|\(|\)|\^|\*/g.test(ch)) {
ch = '#';
}
if (!keys.hasOwnProperty(ch)) {
keys[ch] = 0;
}
keys[ch] += 1;
});
this.jumpbarKeys = 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;
});
this.isLoading = false;
window.scrollTo(0, 0);
});

View file

@ -27,8 +27,6 @@
[filterOpen]="filterOpen"
[jumpBarKeys]="jumpKeys"
(applyFilter)="updateFilter($event)"
(pageChange)="onPageChange($event)"
(pageChangeWithDirection)="handlePaginationChange($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()"

View file

@ -106,13 +106,11 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
});
this.libraryService.getJumpBar(this.libraryId).subscribe(barDetails => {
//console.log('JumpBar: ', barDetails);
this.jumpKeys = barDetails;
});
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
this.pagination.itemsPerPage = 0; // TODO: Validate what pagination setting is ideal
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId];
// Setup filterActiveCheck to check filter against
@ -185,12 +183,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
this.loadPage();
}
handlePaginationChange(direction: 0 | 1) {
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);
this.loadPage(direction);
}
loadPage(direction: 0 | 1 = 1) {
loadPage() {
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
if (this.filter == undefined) {
this.filter = this.seriesService.createSeriesFilter();
@ -199,32 +192,14 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
this.loadingSeries = true;
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.seriesService.getSeriesForLibrary(0, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result;
// For Pagination
// if (this.series.length === 0) {
// this.series = series.result;
// } else {
// if (direction === 1) {
// //this.series = [...this.series, ...series.result];
// this.series.concat(series.result);
// } else {
// this.series = [...series.result, ...this.series];
// }
// }
this.seriesService.getSeriesForLibrary(0, undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result;
this.pagination = series.pagination;
this.loadingSeries = false;
window.scrollTo(0, 0);
});
}
onPageChange(pagination: Pagination) {
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);
this.loadPage();
}
seriesClicked(series: Series) {
this.router.navigate(['library', this.libraryId, 'series', series.id]);
}

View file

@ -340,6 +340,7 @@
<option [value]="SortField.Created">Created</option>
<option [value]="SortField.LastModified">Last Modified</option>
<option [value]="SortField.LastChapterAdded">Item Added</option>
<option [value]="SortField.TimeToRead">Time to Read</option>
</select>
</div>
</form>

View file

@ -1,8 +1,8 @@
import { DOCUMENT } from '@angular/common';
import { Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Component, ContentChildren, ElementRef, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { fromEvent, Subject } from 'rxjs';
import { debounceTime, filter, takeUntil } 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';
@ -51,12 +51,34 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
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 scrollService: ScrollService, private seriesService: SeriesService,) { }
ngOnInit(): void {}
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);
// });
}
// 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;
if (offset > 100) {
this.backToTopNeeded = true;

View file

@ -0,0 +1,27 @@
import { Pipe, PipeTransform } from '@angular/core';
import { MangaFormat } from '../_models/manga-format';
/**
* Returns the icon class representing the format
*/
@Pipe({
name: 'mangaFormatIcon'
})
export class MangaFormatIconPipe implements PipeTransform {
transform(format: MangaFormat): string {
switch (format) {
case MangaFormat.EPUB:
return 'fa-book';
case MangaFormat.ARCHIVE:
return 'fa-file-archive';
case MangaFormat.IMAGE:
return 'fa-image';
case MangaFormat.PDF:
return 'fa-file-pdf';
case MangaFormat.UNKNOWN:
return 'fa-question';
}
}
}

View file

@ -0,0 +1,27 @@
import { Pipe, PipeTransform } from '@angular/core';
import { MangaFormat } from '../_models/manga-format';
/**
* Returns the string name for the format
*/
@Pipe({
name: 'mangaFormat'
})
export class MangaFormatPipe implements PipeTransform {
transform(format: MangaFormat): string {
switch (format) {
case MangaFormat.EPUB:
return 'EPUB';
case MangaFormat.ARCHIVE:
return 'Archive';
case MangaFormat.IMAGE:
return 'Image';
case MangaFormat.PDF:
return 'PDF';
case MangaFormat.UNKNOWN:
return 'Unknown';
}
}
}

View file

@ -10,6 +10,8 @@ import { DefaultValuePipe } from './default-value.pipe';
import { CompactNumberPipe } from './compact-number.pipe';
import { LanguageNamePipe } from './language-name.pipe';
import { AgeRatingPipe } from './age-rating.pipe';
import { MangaFormatPipe } from './manga-format.pipe';
import { MangaFormatIconPipe } from './manga-format-icon.pipe';
@ -24,7 +26,9 @@ import { AgeRatingPipe } from './age-rating.pipe';
DefaultValuePipe,
CompactNumberPipe,
LanguageNamePipe,
AgeRatingPipe
AgeRatingPipe,
MangaFormatPipe,
MangaFormatIconPipe
],
imports: [
CommonModule,
@ -39,7 +43,9 @@ import { AgeRatingPipe } from './age-rating.pipe';
DefaultValuePipe,
CompactNumberPipe,
LanguageNamePipe,
AgeRatingPipe
AgeRatingPipe,
MangaFormatPipe,
MangaFormatIconPipe
]
})
export class PipeModule { }

View file

@ -3,14 +3,15 @@
<span *ngIf="actions.length > 0">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>
</span>
{{readingList?.title}}&nbsp;<span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
{{readingList?.title}}
<span *ngIf="readingList?.promoted" class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
</h2>
<h6 subtitle class="subtitle-with-actionables">{{items.length}} Items</h6>
</app-side-nav-companion-bar>
<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">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="readingList.coverImage !== '' || readingList.coverImage !== undefined">
<app-image maxWidth="300px" [imageUrl]="readingListImage"></app-image>
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
@ -49,21 +50,25 @@
Nothing added
</div>
<!-- TODO: This needs virtualization -->
<app-dragable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" (itemRemove)="itemRemoved($event)" [accessibilityMode]="accessibilityMode">
<ng-template #draggableItem let-item let-position="idx">
<div class="d-flex" style="width: 100%;">
<app-image width="74px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
<div class="flex-grow-1">
<h5 class="mt-0 mb-1" id="item.id--{{position}}">{{formatTitle(item)}}&nbsp;
<h5 class="mt-0 mb-1" id="item.id--{{position}}">
{{formatTitle(item)}}&nbsp;
<!-- TODO: Create a read/unread badge -->
<span class="badge bg-primary rounded-pill">
<span *ngIf="item.pagesRead > 0 && item.pagesRead < item.pagesTotal">{{item.pagesRead}} / {{item.pagesTotal}}</span>
<span *ngIf="item.pagesRead === 0">UNREAD</span>
<span *ngIf="item.pagesRead === item.pagesTotal">READ</span>
<span *ngIf="item.pagesRead > 0 && item.pagesRead < item.pagesTotal">{{item.pagesRead}} / {{item.pagesTotal}}</span>
<span *ngIf="item.pagesRead === 0">UNREAD</span>
<span *ngIf="item.pagesRead === item.pagesTotal">READ</span>
</span>
</h5>
<i class="fa {{utilityService.mangaFormatIcon(item.seriesFormat)}}" aria-hidden="true" *ngIf="item.seriesFormat != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(item.seriesFormat)}}"></i>
<span class="visually-hidden">{{utilityService.mangaFormat(item.seriesFormat)}}</span>&nbsp;
<ng-container *ngIf="item.seriesFormat | mangaFormat as formatString">
<i class="fa {{item.seriesFormat | mangaFormatIcon}}" aria-hidden="true" *ngIf="item.seriesFormat != MangaFormat.UNKNOWN" title="{{formatString}}"></i>
<span class="visually-hidden">{{formatString}}</span>&nbsp;
</ng-container>
<a href="/library/{{item.libraryId}}/series/{{item.seriesId}}">{{item.seriesName}}</a>
<span *ngIf="item.promoted">

View file

@ -1,4 +1,10 @@
.container-sm {
padding-left: 0px;
padding-right: 0px;
}
}
.virtual-scroller, virtual-scroller {
width: 100%;
height: calc(100vh - 85px);
max-height: calc(var(--vh)*100 - 170px);
}

View file

@ -60,7 +60,7 @@ export class ReadingListDetailComponent implements OnInit {
this.listId = parseInt(listId, 10);
this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
//this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
this.libraryService.getLibraries().subscribe(libs => {
@ -146,6 +146,7 @@ export class ReadingListDetailComponent implements OnInit {
}
formatTitle(item: ReadingListItem) {
// TODO: Use new app-entity-title component instead
if (item.chapterNumber === '0') {
return 'Volume ' + item.volumeNumber;
}

View file

@ -11,7 +11,6 @@
[actions]="actions"
[pagination]="pagination"
[filteringDisabled]="true"
(pageChange)="onPageChange($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)" (clicked)="handleClick(item)"></app-card-item>

View file

@ -77,7 +77,7 @@ export class ReadingListsComponent implements OnInit {
}
this.loadingLists = true;
this.readingListService.getReadingLists(true, this.pagination?.currentPage, this.pagination?.itemsPerPage).pipe(take(1)).subscribe((readingLists: PaginatedResult<ReadingList[]>) => {
this.readingListService.getReadingLists(true).pipe(take(1)).subscribe((readingLists: PaginatedResult<ReadingList[]>) => {
this.lists = readingLists.result;
this.pagination = readingLists.pagination;
this.loadingLists = false;

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 */});;
});
}

View file

@ -96,47 +96,6 @@ export class UtilityService {
return input.toUpperCase().replace(reg, '').includes(filter.toUpperCase().replace(reg, ''));
}
mangaFormat(format: MangaFormat): string {
switch (format) {
case MangaFormat.EPUB:
return 'EPUB';
case MangaFormat.ARCHIVE:
return 'Archive';
case MangaFormat.IMAGE:
return 'Image';
case MangaFormat.PDF:
return 'PDF';
case MangaFormat.UNKNOWN:
return 'Unknown';
}
}
mangaFormatIcon(format: MangaFormat): string {
switch (format) {
case MangaFormat.EPUB:
return 'fa-book';
case MangaFormat.ARCHIVE:
return 'fa-file-archive';
case MangaFormat.IMAGE:
return 'fa-image';
case MangaFormat.PDF:
return 'fa-file-pdf';
case MangaFormat.UNKNOWN:
return 'fa-question';
}
}
getLibraryTypeIcon(format: LibraryType) {
switch (format) {
case LibraryType.Book:
return 'fa-book';
case LibraryType.Comic:
case LibraryType.Manga:
return 'fa-book-open';
}
}
isVolume(d: any) {
return d != null && d.hasOwnProperty('chapters');
}
@ -237,4 +196,5 @@ export class UtilityService {
|| document.body.clientHeight;
return [windowWidth, windowHeight];
}
}

View file

@ -1,4 +1,4 @@
import { Component, ElementRef, Input, OnChanges, OnDestroy, Renderer2, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, OnDestroy, Renderer2, ViewChild } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { CoverUpdateEvent } from 'src/app/_models/events/cover-update-event';
@ -11,7 +11,8 @@ import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service
@Component({
selector: 'app-image',
templateUrl: './image.component.html',
styleUrls: ['./image.component.scss']
styleUrls: ['./image.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImageComponent implements OnChanges, OnDestroy {
@ -48,7 +49,7 @@ export class ImageComponent implements OnChanges, OnDestroy {
private readonly onDestroy = new Subject<void>();
constructor(public imageService: ImageService, private renderer: Renderer2, private hubService: MessageHubService) {
constructor(public imageService: ImageService, private renderer: Renderer2, private hubService: MessageHubService, private changeDetectionRef: ChangeDetectorRef) {
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
if (!this.processEvents) return;
if (res.event === EVENTS.CoverUpdate) {
@ -65,6 +66,7 @@ export class ImageComponent implements OnChanges, OnDestroy {
}
if (id === (updateEvent.id + '')) {
this.imageUrl = this.imageService.randomize(this.imageUrl);
this.changeDetectionRef.markForCheck();
}
}
}

View file

@ -1,4 +1,4 @@
<ng-container *ngIf="format != MangaFormat.UNKNOWN">
<i class="fa {{utilityService.mangaFormatIcon(format)}}" aria-hidden="true" title="{{utilityService.mangaFormat(format)}}"></i>&nbsp;
<i class="fa {{format | mangaFormatIcon}}" aria-hidden="true" title="{{format | mangaFormat}}"></i>&nbsp;
<ng-content></ng-content>
</ng-container>

View file

@ -20,7 +20,7 @@
</div>
</div>
<app-side-nav-item *ngFor="let library of libraries | filter: filterLibrary" [link]="'/library/' + library.id + '/'"
[icon]="utilityService.getLibraryTypeIcon(library.type)" [title]="library.name" [comparisonMethod]="'startsWith'">
[icon]="getLibraryTypeIcon(library.type)" [title]="library.name" [comparisonMethod]="'startsWith'">
<ng-container actions>
<app-card-actionables [actions]="actions" [labelBy]="library.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event, library)"></app-card-actionables>
</ng-container>

View file

@ -1,10 +1,10 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { Observable, Subject } from 'rxjs';
import { filter, map, take, takeUntil, takeWhile } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { filter, map, take, takeUntil } from 'rxjs/operators';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { Breakpoint, UtilityService } from '../../shared/_services/utility.service';
import { Library } from '../../_models/library';
import { Library, LibraryType } from '../../_models/library';
import { User } from '../../_models/user';
import { AccountService } from '../../_services/account.service';
import { Action, ActionFactoryService, ActionItem } from '../../_services/action-factory.service';
@ -99,4 +99,14 @@ export class SideNavComponent implements OnInit, OnDestroy {
}
}
getLibraryTypeIcon(format: LibraryType) {
switch (format) {
case LibraryType.Book:
return 'fa-book';
case LibraryType.Comic:
case LibraryType.Manga:
return 'fa-book-open';
}
}
}

View file

@ -12,4 +12,4 @@
background-color: var(--card-overlay-hover-bg-color);
}
}
}
}

View file

@ -234,4 +234,7 @@
--bulk-selection-text-color: var(--navbar-text-color);
--bulk-selection-highlight-text-color: var(--primary-color);
/* List Card Item */
--card-list-item-bg-color: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
}