Fixes, Tweaks, and Series Filtering (#1217)
* From previous fix, added the other locking conditions on the update series metadata. * Fixed a bug where custom series, collection tag, and reading list covers weren't being removed on cleanup. * Ensure reading list detail has a margin to align to the standard * Refactored some event stuff to use dedicated consts. Introduced a new event when users read something, which can update progress bars on cards. * Added recomended and library tags to the library detail page. This will eventually offer more custom analytics * Cleanup some code onc arousel * Adjusted scale to height/width css to better fit * Small css tweaks to better center images in the manga reader in both axis. This takes care of double page rendering as well. * When a special has a Title set in the metadata, on series detail page, show that on the card rather than filename. * Fixed a bug where when paging in manga reader, the scroll to top wasn't working due to changing where scrolling is done * More css goodness for rendering images in manga reader * Fixed a bug where clearing a typeahead externally wouldn't clear the x button * Fixed a bug where filering then using keyboard would select wrong option * Added a new sorting field for Last Chapter Added (new field) to get a similar on deck feel. * Tweaked recently updated to hit the NFR of 500ms (300ms fresh start) and still give a much better experience. * Refactored On deck to now go to all series and also sort by last updated. Recently Added Series now loads all series with sort by created. * Some tweaks on css for cover image chooser * Fixed a bug in pagination control where multiple pagination events could trigger on load and thus multiple requests for data on parent controller. * Updated edit series modal to show when the last chapter was added and when user last read it. * Implemented a highlight on the fitler button when a filter is active. * Refactored metadata filter screens to perserve the filters in the url and thus when navigating back and forth, it will retain. users should click side nav to reset the state. * Hide middle section on companion bar on phones * Cleaned up some prefilters and console.logs * Don't open drawer by default when a filter is active
This commit is contained in:
parent
5e629913b7
commit
553f9b0d98
63 changed files with 864 additions and 537 deletions
|
@ -100,12 +100,12 @@ export class ErrorInterceptor implements HttpInterceptor {
|
|||
const err = error.error;
|
||||
if (err.hasOwnProperty('message') && err.message.trim() !== '') {
|
||||
if (err.message != 'User is not authenticated') {
|
||||
console.log('500 error: ', error);
|
||||
console.error('500 error: ', error);
|
||||
}
|
||||
this.toastr.error(err.message);
|
||||
} else if (error.hasOwnProperty('message') && error.message.trim() !== '') {
|
||||
if (error.message != 'User is not authenticated') {
|
||||
console.log('500 error: ', error);
|
||||
console.error('500 error: ', error);
|
||||
}
|
||||
this.toastr.error(error.message);
|
||||
}
|
||||
|
|
10
UI/Web/src/app/_models/events/user-progress-update-event.ts
Normal file
10
UI/Web/src/app/_models/events/user-progress-update-event.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export interface UserProgressUpdateEvent {
|
||||
userId: number;
|
||||
username: string;
|
||||
//entityId: number;
|
||||
//entityType: 'series' | 'collection' | 'chapter' | 'volume' | 'readingList';
|
||||
seriesId: number;
|
||||
volumeId: number;
|
||||
chapterId: number;
|
||||
pagesRead: number;
|
||||
}
|
|
@ -40,7 +40,8 @@ export interface SortOptions {
|
|||
export enum SortField {
|
||||
SortName = 1,
|
||||
Created = 2,
|
||||
LastModified = 3
|
||||
LastModified = 3,
|
||||
LastChapterAdded = 4
|
||||
}
|
||||
|
||||
export interface ReadStatus {
|
||||
|
|
|
@ -44,4 +44,8 @@ export interface Series {
|
|||
* DateTime that represents last time the logged in user read this series
|
||||
*/
|
||||
latestReadDate: string;
|
||||
/**
|
||||
* DateTime representing last time a chapter was added to the Series
|
||||
*/
|
||||
lastChapterAdded: string;
|
||||
}
|
||||
|
|
|
@ -235,9 +235,7 @@ export class AccountService implements OnDestroy {
|
|||
// set a timeout to refresh the token a minute before it expires
|
||||
const expires = new Date(jwtToken.exp * 1000);
|
||||
const timeout = expires.getTime() - Date.now() - (60 * 1000);
|
||||
this.refreshTokenTimeout = setTimeout(() => this.refreshToken().subscribe(() => {
|
||||
console.log('Token Refreshed');
|
||||
}), timeout);
|
||||
this.refreshTokenTimeout = setTimeout(() => this.refreshToken().subscribe(() => {}), timeout);
|
||||
}
|
||||
|
||||
private stopRefreshTokenTimer() {
|
||||
|
|
|
@ -53,7 +53,11 @@ export enum EVENTS {
|
|||
/**
|
||||
* A library is created or removed from the instance
|
||||
*/
|
||||
LibraryModified = 'LibraryModified'
|
||||
LibraryModified = 'LibraryModified',
|
||||
/**
|
||||
* A user updates an entities read progress
|
||||
*/
|
||||
UserProgressUpdate = 'UserProgressUpdate',
|
||||
}
|
||||
|
||||
export interface Message<T> {
|
||||
|
@ -164,6 +168,13 @@ export class MessageHubService {
|
|||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.UserProgressUpdate, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.UserProgressUpdate,
|
||||
payload: resp.body
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.Error, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.Error,
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { Member } from 'src/app/_models/member';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
|
||||
// TODO: Rename this to EditUserModal
|
||||
@Component({
|
||||
|
@ -27,8 +25,7 @@ export class EditUserComponent implements OnInit {
|
|||
public get username() { return this.userForm.get('username'); }
|
||||
public get password() { return this.userForm.get('password'); }
|
||||
|
||||
constructor(public modal: NgbActiveModal, private accountService: AccountService, private serverService: ServerService,
|
||||
private confirmService: ConfirmService) { }
|
||||
constructor(public modal: NgbActiveModal, private accountService: AccountService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.userForm.addControl('email', new FormControl(this.member.email, [Validators.required, Validators.email]));
|
||||
|
|
|
@ -42,8 +42,6 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
|||
distinctUntilChanged((prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) =>
|
||||
this.hasMessageChanged(prev, curr)))
|
||||
.subscribe((event: Message<ScanSeriesEvent | NotificationProgressEvent>) => {
|
||||
console.log('scan event: ', event);
|
||||
|
||||
let libId = 0;
|
||||
if (event.event === EVENTS.ScanSeries) {
|
||||
libId = (event.payload as ScanSeriesEvent).libraryId;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)">
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
|
||||
<h2 title>
|
||||
<app-card-actionables [actions]="actions"></app-card-actionables>
|
||||
All Series
|
||||
|
@ -16,6 +16,8 @@
|
|||
(pageChange)="onPageChange($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"></app-series-card>
|
||||
<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"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Subject } from 'rxjs';
|
|||
import { take, debounceTime, takeUntil } from 'rxjs/operators';
|
||||
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 { Library } from '../_models/library';
|
||||
import { Pagination } from '../_models/pagination';
|
||||
|
@ -30,6 +31,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||
onDestroy: Subject<void> = new Subject<void>();
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
filterActive: boolean = false;
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||
|
@ -71,14 +73,14 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||
constructor(private router: Router, private seriesService: SeriesService,
|
||||
private titleService: Title, private actionService: ActionService,
|
||||
public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||
private utilityService: UtilityService, private route: ActivatedRoute) {
|
||||
private utilityService: UtilityService, private route: ActivatedRoute,
|
||||
private filterUtilityService: FilterUtilitiesService) {
|
||||
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
|
||||
this.titleService.setTitle('Kavita - All Series');
|
||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.utilityService.filterPresetsFromUrl(this.route.snapshot, this.seriesService.createSeriesFilter());
|
||||
this.pagination = this.filterUtilityService.pagination();
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -109,12 +111,9 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||
|
||||
updateFilter(data: FilterEvent) {
|
||||
this.filter = data.filter;
|
||||
if (this.pagination !== undefined && this.pagination !== null && !data.isFirst) {
|
||||
this.pagination.currentPage = 1;
|
||||
this.onPageChange(this.pagination);
|
||||
} else {
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
|
@ -123,6 +122,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||
this.filter = this.seriesService.createSeriesFilter();
|
||||
}
|
||||
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
|
||||
this.seriesService.getAllSeries(this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.pagination = series.pagination;
|
||||
|
@ -132,15 +132,9 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
onPageChange(pagination: Pagination) {
|
||||
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage);
|
||||
this.filterUtilityService.updateUrlFromPagination(this.pagination);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.originalName}_${item.localizedName}_${item.pagesRead}`;
|
||||
|
||||
getPage() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('page');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -95,7 +95,6 @@ export class CardDetailsModalComponent implements OnInit {
|
|||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.data);
|
||||
console.log('isChapter: ', this.isChapter);
|
||||
|
||||
this.chapter = this.utilityService.isChapter(this.data) ? (this.data as Chapter) : (this.data as Volume).chapters[0];
|
||||
|
||||
|
|
|
@ -336,6 +336,11 @@
|
|||
<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>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6" >Created: {{series.created | date:'shortDate'}}</div>
|
||||
<div class="col-md-6">Last Read: {{series.latestReadDate | date:'shortDate'}}</div>
|
||||
<div class="col-md-6">Last Added To: {{series.lastChapterAdded | date:'shortDate'}}</div>
|
||||
</div>
|
||||
<h4>Volumes</h4>
|
||||
<div class="spinner-border text-secondary" role="status" *ngIf="isLoadingVolumes">
|
||||
|
|
|
@ -219,10 +219,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
return {id: 0, title: title, promoted: false, coverImage: '', summary: '', coverImageLocked: false };
|
||||
});
|
||||
this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||
// console.log('compareFN:')
|
||||
// console.log('options: ', options);
|
||||
// console.log('filter: ', filter);
|
||||
// console.log('results: ', options.filter(m => this.utilityService.filter(m.title, filter)));
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.collectionTagSettings.selectionCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||
|
|
|
@ -61,6 +61,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
|
||||
|
||||
if (this.filterSettings === undefined) {
|
||||
console.log('filter settings was empty, creating our own');
|
||||
this.filterSettings = new FilterSettings();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,6 @@
|
|||
</span>
|
||||
</div>
|
||||
<span class="card-title library" [ngbTooltip]="subtitle" placement="top" *ngIf="subtitle.length > 0">{{subtitle}}</span>
|
||||
<a class="card-title library" [routerLink]="['/library', libraryId]" routerLinkActive="router-link-active" *ngIf="!supressLibraryLink && libraryName">{{libraryName | sentenceCase}}</a>
|
||||
<a class="card-title library" [routerLink]="['/library', libraryId]" routerLinkActive="router-link-active" *ngIf="!suppressLibraryLink && libraryName">{{libraryName | sentenceCase}}</a>
|
||||
</div>
|
||||
</div>
|
|
@ -1,20 +1,24 @@
|
|||
import { Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { filter, finalize, map, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { Download } from 'src/app/shared/_models/download';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { UserProgressUpdateEvent } from 'src/app/_models/events/user-progress-update-event';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||
import { RecentlyAddedItem } from 'src/app/_models/recently-added-item';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
import { BulkSelectionService } from '../bulk-selection.service';
|
||||
|
||||
@Component({
|
||||
|
@ -51,7 +55,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
/**
|
||||
* Supress library link
|
||||
*/
|
||||
@Input() supressLibraryLink = false;
|
||||
@Input() suppressLibraryLink = false;
|
||||
/**
|
||||
* This is the entity we are representing. It will be returned if an action is executed.
|
||||
*/
|
||||
|
@ -97,6 +101,8 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
|
||||
isShiftDown: boolean = false;
|
||||
|
||||
private user: User | undefined;
|
||||
|
||||
get tooltipTitle() {
|
||||
if (this.chapterTitle === '' || this.chapterTitle === null) return this.title;
|
||||
return this.chapterTitle;
|
||||
|
@ -111,14 +117,15 @@ 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 toastr: ToastrService, public bulkSelectionService: BulkSelectionService,
|
||||
private messageHub: MessageHubService, private accountService: AccountService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
||||
this.supressArchiveWarning = true;
|
||||
}
|
||||
|
||||
if (this.supressLibraryLink === false) {
|
||||
if (this.suppressLibraryLink === false) {
|
||||
if (this.entity !== undefined && this.entity.hasOwnProperty('libraryId')) {
|
||||
this.libraryId = (this.entity as Series).libraryId;
|
||||
}
|
||||
|
@ -139,6 +146,20 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
this.chapterTitle = vol.chapters[0].titleName;
|
||||
}
|
||||
}
|
||||
|
||||
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => {
|
||||
this.user = user;
|
||||
});
|
||||
|
||||
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
|
||||
map(evt => evt.payload as UserProgressUpdateEvent), takeUntil(this.onDestroy)).subscribe(updateEvent => {
|
||||
if (this.user !== undefined && this.user.username !== updateEvent.username) return;
|
||||
if (this.utilityService.isChapter(this.entity) && updateEvent.chapterId !== this.entity.id) return;
|
||||
if (this.utilityService.isVolume(this.entity) && updateEvent.volumeId !== this.entity.id) return;
|
||||
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
||||
|
||||
this.read = updateEvent.pagesRead;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
<div class="row g-0 mb-3">
|
||||
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mx-auto" style="width: 350px;">
|
||||
<a class="col" style="padding-right:0px" href="javascript:void(0)" (click)="changeMode('url')"><span class="phone-hidden">Enter a </span>Url</a>
|
||||
<span class="col ps-1 pe-1">•</span>
|
||||
<span class="col" style="padding-right:0px" href="javascript:void(0)">Drag and drop</span>
|
||||
<span class="col ps-1 pe-1" style="padding-right:0px">•</span>
|
||||
<a class="col" style="padding-right:0px" href="javascript:void(0)" (click)="openFileSelector()">Upload<span class="phone-hidden"> an image</span></a>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-evenly">
|
||||
<a style="padding-right:0px" href="javascript:void(0)" (click)="changeMode('url')"><span class="phone-hidden">Enter a </span>Url</a>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<span style="padding-right:0px" href="javascript:void(0)">Drag and drop</span>
|
||||
<span class="ps-1 pe-1" style="padding-right:0px">•</span>
|
||||
<a style="padding-right:0px" href="javascript:void(0)" (click)="openFileSelector()">Upload<span class="phone-hidden"> an image</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<ng-container *ngIf="data !== undefined">
|
||||
<app-card-item [title]="data.name" [actions]="actions" [supressLibraryLink]="suppressLibraryLink" [imageUrl]="imageUrl"
|
||||
<app-card-item [title]="data.name" [actions]="actions" [suppressLibraryLink]="suppressLibraryLink" [imageUrl]="imageUrl"
|
||||
[entity]="data" [total]="data.pages" [read]="data.pagesRead" (clicked)="handleClick()"
|
||||
[allowSelection]="allowSelection" (selection)="selection.emit(selected)" [selected]="selected"
|
||||
></app-card-item>
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<div class="carousel-container" *ngIf="items.length > 0">
|
||||
<div class="carousel-container" *ngIf="items.length > 0 ">
|
||||
<div>
|
||||
<h3 style="display: inline-block;"><a href="javascript:void(0)" (click)="sectionClicked($event)" class="section-title" [ngClass]="{'non-selectable': !clickableTitle}">{{title}}</a></h3>
|
||||
<div class="float-end">
|
||||
<button class="btn btn-icon" [disabled]="isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="visually-hidden">Previous Items</span></button>
|
||||
<button class="btn btn-icon" [disabled]="isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="visually-hidden">Next Items</span></button>
|
||||
<h3 style="display: inline-block;">
|
||||
<a href="javascript:void(0)" (click)="sectionClicked($event)" class="section-title" [ngClass]="{'non-selectable': !clickableTitle}">{{title}}</a>
|
||||
</h3>
|
||||
<div class="float-end" *ngIf="swiper">
|
||||
<button class="btn btn-icon" [disabled]="swiper.isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="visually-hidden">Previous Items</span></button>
|
||||
<button class="btn btn-icon" [disabled]="swiper.isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="visually-hidden">Next Items</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { SwiperComponent } from 'swiper/angular';
|
||||
import { Component, ContentChild, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
|
||||
import { Swiper, SwiperEvents } from 'swiper/types';
|
||||
|
||||
@Component({
|
||||
|
@ -7,7 +6,7 @@ import { Swiper, SwiperEvents } from 'swiper/types';
|
|||
templateUrl: './carousel-reel.component.html',
|
||||
styleUrls: ['./carousel-reel.component.scss']
|
||||
})
|
||||
export class CarouselReelComponent implements OnInit {
|
||||
export class CarouselReelComponent {
|
||||
|
||||
@ContentChild('carouselItem') carouselItemTemplate!: TemplateRef<any>;
|
||||
@Input() items: any[] = [];
|
||||
|
@ -19,30 +18,20 @@ export class CarouselReelComponent implements OnInit {
|
|||
|
||||
trackByIdentity: (index: number, item: any) => string;
|
||||
|
||||
get isEnd() {
|
||||
return this.swiper?.isEnd;
|
||||
}
|
||||
|
||||
get isBeginning() {
|
||||
return this.swiper?.isBeginning;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.trackByIdentity = (index: number, item: any) => `${this.title}_${item.id}_${item?.name}_${item?.pagesRead}_${index}`;
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
nextPage() {
|
||||
if (this.isEnd) return;
|
||||
if (this.swiper) {
|
||||
if (this.swiper.isEnd) return;
|
||||
this.swiper.setProgress(this.swiper.progress + 0.25, 600);
|
||||
}
|
||||
}
|
||||
|
||||
prevPage() {
|
||||
if (this.isBeginning) return;
|
||||
if (this.swiper) {
|
||||
if (this.swiper.isBeginning) return;
|
||||
this.swiper.setProgress(this.swiper.progress - 0.25, 600);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasFilter]="false" (filterOpen)="filterOpen.emit($event)">
|
||||
<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>
|
||||
|
|
|
@ -13,4 +13,4 @@
|
|||
.read-btn--text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,10 +4,11 @@ import { Router, ActivatedRoute } from '@angular/router';
|
|||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from 'src/app/cards/bulk-selection.service';
|
||||
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
||||
import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
|
||||
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
|
||||
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';
|
||||
|
@ -32,17 +33,15 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||
collectionTag!: CollectionTag;
|
||||
tagImage: string = '';
|
||||
isLoading: boolean = true;
|
||||
collections: CollectionTag[] = [];
|
||||
collectionTagName: string = '';
|
||||
series: Array<Series> = [];
|
||||
seriesPagination!: Pagination;
|
||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||
isAdmin: boolean = false;
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
summary: string = '';
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
filterActive: boolean = false;
|
||||
|
||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
|
@ -87,26 +86,20 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||
|
||||
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, private accountService: AccountService,
|
||||
private modalService: NgbModal, private titleService: Title,
|
||||
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
|
||||
private utilityService: UtilityService) {
|
||||
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
}
|
||||
});
|
||||
|
||||
const routeId = this.route.snapshot.paramMap.get('id');
|
||||
if (routeId === null) {
|
||||
this.router.navigate(['collections']);
|
||||
return;
|
||||
}
|
||||
const tagId = parseInt(routeId, 10);
|
||||
this.seriesPagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.utilityService.filterPresetsFromUrl(this.route.snapshot, this.seriesService.createSeriesFilter());
|
||||
this.seriesPagination = this.filterUtilityService.pagination();
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl();
|
||||
this.filterSettings.presets.collectionTags = [tagId];
|
||||
|
||||
this.updateTag(tagId);
|
||||
|
@ -148,13 +141,13 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||
|
||||
updateTag(tagId: number) {
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
this.collections = tags;
|
||||
const matchingTags = this.collections.filter(t => t.id === tagId);
|
||||
const matchingTags = tags.filter(t => t.id === tagId);
|
||||
if (matchingTags.length === 0) {
|
||||
this.toastr.error('You don\'t have access to any libraries this tag belongs to or this tag is invalid');
|
||||
|
||||
this.router.navigateByUrl('/');
|
||||
return;
|
||||
}
|
||||
|
||||
this.collectionTag = matchingTags[0];
|
||||
this.summary = (this.collectionTag.summary === null ? '' : this.collectionTag.summary).replace(/\n/g, '<br>');
|
||||
this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(this.collectionTag.id));
|
||||
|
@ -163,46 +156,25 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
onPageChange(pagination: Pagination) {
|
||||
this.router.navigate(['collections', this.collectionTag.id], {replaceUrl: true, queryParamsHandling: 'merge', queryParams: {page: this.seriesPagination.currentPage} });
|
||||
this.filterUtilityService.updateUrlFromPagination(this.seriesPagination);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
const page = this.getPage();
|
||||
if (page != null) {
|
||||
this.seriesPagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
|
||||
// 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();
|
||||
this.filter.collectionTags.push(this.collectionTag.id);
|
||||
}
|
||||
|
||||
// TODO: Add ability to filter series for a collection
|
||||
// Reload page after a series is updated or first load
|
||||
this.seriesService.getSeriesForTag(this.collectionTag.id, this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage).subscribe(tags => {
|
||||
this.series = tags.result;
|
||||
this.seriesPagination = tags.pagination;
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
|
||||
this.seriesService.getAllSeries(this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.seriesPagination = series.pagination;
|
||||
this.isLoading = false;
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
updateFilter(event: FilterEvent) {
|
||||
this.filter = event.filter;
|
||||
const page = this.getPage();
|
||||
if (page === undefined || page === null || !event.isFirst) {
|
||||
this.seriesPagination.currentPage = 1;
|
||||
this.onPageChange(this.seriesPagination);
|
||||
} else {
|
||||
this.loadPage();
|
||||
}
|
||||
}
|
||||
|
||||
getPage() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('page');
|
||||
updateFilter(data: FilterEvent) {
|
||||
this.filter = data.filter;
|
||||
|
||||
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, this.filter);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
handleCollectionActionCallback(action: Action, collectionTag: CollectionTag) {
|
||||
|
|
|
@ -1,24 +1,38 @@
|
|||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)">
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
|
||||
<h2 title>
|
||||
<app-card-actionables [actions]="actions"></app-card-actionables>
|
||||
{{libraryName}}
|
||||
</h2>
|
||||
<h6 subtitle style="margin-left:40px;">{{pagination?.totalItems}} Series</h6>
|
||||
<h6 subtitle style="margin-left:40px;" *ngIf="active.fragment === ''">{{pagination?.totalItems}} Series</h6>
|
||||
<div main>
|
||||
<!-- TODO: Implement Tabs here for Recommended and Library view -->
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-pills" style="flex-wrap: nowrap;">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||
<a ngbNavLink>{{tab.title | sentenceCase}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngIf="tab.title === 'Recommended'">
|
||||
<app-library [libraryId]="libraryId"></app-library>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.title === 'Library'">
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingSeries"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[filterOpen]="filterOpen"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()" (selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</app-side-nav-companion-bar>
|
||||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingSeries"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[filterOpen]="filterOpen"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()" (selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { LibraryService } from '../_services/library.service';
|
|||
import { EVENTS, MessageHubService } from '../_services/message-hub.service';
|
||||
import { SeriesService } from '../_services/series.service';
|
||||
import { NavService } from '../_services/nav.service';
|
||||
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-detail',
|
||||
|
@ -35,6 +36,13 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
onDestroy: Subject<void> = new Subject<void>();
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
filterActive: boolean = false;
|
||||
|
||||
tabs: Array<{title: string, fragment: string}> = [
|
||||
{title: 'Library', fragment: ''},
|
||||
{title: 'Recommended', fragment: 'recomended'},
|
||||
];
|
||||
active = this.tabs[0];
|
||||
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
|
@ -77,13 +85,14 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
|
||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||
private utilityService: UtilityService, public navService: NavService) {
|
||||
private utilityService: UtilityService, public navService: NavService, private filterUtilityService: FilterUtilitiesService) {
|
||||
const routeId = this.route.snapshot.paramMap.get('id');
|
||||
if (routeId === null) {
|
||||
this.router.navigateByUrl('/libraries');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.libraryId = parseInt(routeId, 10);
|
||||
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(names => {
|
||||
|
@ -91,9 +100,9 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
this.titleService.setTitle('Kavita - ' + this.libraryName);
|
||||
});
|
||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.utilityService.filterPresetsFromUrl(this.route.snapshot, this.seriesService.createSeriesFilter());
|
||||
this.pagination = this.filterUtilityService.pagination();
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl();
|
||||
this.filterSettings.presets.libraries = [this.libraryId];
|
||||
}
|
||||
|
||||
|
@ -142,30 +151,21 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
updateFilter(event: FilterEvent) {
|
||||
this.filter = event.filter;
|
||||
const page = this.getPage();
|
||||
if (page === undefined || page === null || !event.isFirst) {
|
||||
this.pagination.currentPage = 1;
|
||||
this.onPageChange(this.pagination);
|
||||
} else {
|
||||
this.loadPage();
|
||||
}
|
||||
updateFilter(data: FilterEvent) {
|
||||
this.filter = data.filter;
|
||||
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
const page = this.getPage();
|
||||
if (page != null) {
|
||||
this.pagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
this.loadingSeries = true;
|
||||
|
||||
// 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();
|
||||
this.filter.libraries.push(this.libraryId);
|
||||
}
|
||||
|
||||
this.loadingSeries = true;
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
|
||||
this.seriesService.getSeriesForLibrary(0, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.pagination = series.pagination;
|
||||
|
@ -175,7 +175,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
onPageChange(pagination: Pagination) {
|
||||
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?' + 'page=' + this.pagination.currentPage);
|
||||
this.filterUtilityService.updateUrlFromPagination(this.pagination);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
|
@ -184,10 +184,4 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.originalName}_${item.localizedName}_${item.pagesRead}`;
|
||||
|
||||
getPage() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('page');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,19 +11,19 @@
|
|||
|
||||
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
|
||||
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series" (sectionClick)="handleSectionClick($event)">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-card-item [entity]="item" [title]="item.seriesName" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||
<app-card-item [entity]="item" [title]="item.seriesName" [suppressLibraryLink]="libraryId !== 0" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||
[supressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
|
||||
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series" [clickableTitle]="false">
|
||||
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series" (sectionClick)="handleSectionClick($event)">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
|
@ -1,13 +1,14 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { ReplaySubject, Subject } from 'rxjs';
|
||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||
import { debounceTime, filter, take, takeUntil } from 'rxjs/operators';
|
||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
|
||||
import { Library } from '../_models/library';
|
||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||
import { Series } from '../_models/series';
|
||||
import { SortField } from '../_models/series-filter';
|
||||
import { SeriesGroup } from '../_models/series-group';
|
||||
import { User } from '../_models/user';
|
||||
import { AccountService } from '../_services/account.service';
|
||||
|
@ -23,6 +24,11 @@ import { SeriesService } from '../_services/series.service';
|
|||
})
|
||||
export class LibraryComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* By default, 0, but if non-zero, will limit all API calls to library id
|
||||
*/
|
||||
@Input() libraryId: number = 0;
|
||||
|
||||
user: User | undefined;
|
||||
libraries: Library[] = [];
|
||||
isLoading = false;
|
||||
|
@ -107,21 +113,36 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
loadOnDeck() {
|
||||
this.seriesService.getOnDeck().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
let api = this.seriesService.getOnDeck();
|
||||
if (this.libraryId > 0) {
|
||||
api = this.seriesService.getOnDeck(this.libraryId);
|
||||
}
|
||||
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
this.inProgress = updatedSeries.result;
|
||||
});
|
||||
}
|
||||
|
||||
loadRecentlyAddedSeries() {
|
||||
this.seriesService.getRecentlyAdded().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
let api = this.seriesService.getRecentlyAdded();
|
||||
if (this.libraryId > 0) {
|
||||
api = this.seriesService.getRecentlyAdded(this.libraryId);
|
||||
}
|
||||
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
this.recentlyAddedSeries = updatedSeries.result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
loadRecentlyAdded() {
|
||||
this.seriesService.getRecentlyUpdatedSeries().pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
||||
this.recentlyUpdatedSeries = updatedSeries;
|
||||
let api = this.seriesService.getRecentlyUpdatedSeries();
|
||||
if (this.libraryId > 0) {
|
||||
api = this.seriesService.getRecentlyUpdatedSeries();
|
||||
}
|
||||
api.pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
||||
this.recentlyUpdatedSeries = updatedSeries.filter(group => {
|
||||
if (this.libraryId === 0) return true;
|
||||
return group.libraryId === this.libraryId;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -130,17 +151,22 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
handleSectionClick(sectionTitle: string) {
|
||||
if (sectionTitle.toLowerCase() === 'collections') {
|
||||
this.router.navigate(['collections']);
|
||||
} else if (sectionTitle.toLowerCase() === 'recently updated series') {
|
||||
this.router.navigate(['recently-added']);
|
||||
if (sectionTitle.toLowerCase() === 'recently updated series') {
|
||||
const params: any = {};
|
||||
params['sortBy'] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
|
||||
params['page'] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
||||
const params: any = {};
|
||||
params['readStatus'] = 'true,false,false';
|
||||
params['sortBy'] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
|
||||
params['page'] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
}else if (sectionTitle.toLowerCase() === 'newly added series') {
|
||||
const params: any = {};
|
||||
params['sortBy'] = SortField.Created + ',false'; // sort by created, desc
|
||||
params['page'] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
} else if (sectionTitle.toLowerCase() === 'libraries') {
|
||||
this.router.navigate(['all-series']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,13 +21,20 @@ img {
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
text-align: center;
|
||||
display: block;
|
||||
height: 100vh;
|
||||
|
||||
// Original
|
||||
//display: block;
|
||||
|
||||
// New (for centering in both axis)
|
||||
//display: flex; // Leave this off as it can cutoff the image
|
||||
align-items: center;
|
||||
|
||||
#image-1 {
|
||||
&.double {
|
||||
margin: 0 0 0 auto;
|
||||
|
@ -92,7 +99,7 @@ img {
|
|||
.full-height {
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
@ -101,16 +108,16 @@ img {
|
|||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
max-width: 100%;
|
||||
align-self: center;
|
||||
|
||||
&.double {
|
||||
width: 50%;
|
||||
&.double {
|
||||
width: 50%;
|
||||
|
||||
&.cover {
|
||||
width: 100%;
|
||||
}
|
||||
&.cover {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.center-double {
|
||||
|
@ -119,11 +126,11 @@ img {
|
|||
}
|
||||
|
||||
.fit-to-width-double-offset {
|
||||
width: 100%;
|
||||
max-width: 100%; // max-width fixes center alignment issue
|
||||
}
|
||||
|
||||
.original-double-offset {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.fit-to-height-double-offset {
|
||||
|
|
|
@ -23,7 +23,6 @@ import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/rea
|
|||
import { layoutModes, pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
|
||||
import { ReaderMode } from '../_models/preferences/reader-mode';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
import { LibraryService } from '../_services/library.service';
|
||||
import { LibraryType } from '../_models/library';
|
||||
import { ShorcutsModalComponent } from '../reader-shared/_modals/shorcuts-modal/shorcuts-modal.component';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
@ -326,8 +325,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
public readerService: ReaderService, private location: Location,
|
||||
private formBuilder: FormBuilder, private navService: NavService,
|
||||
private toastr: ToastrService, private memberService: MemberService,
|
||||
private libraryService: LibraryService, public utilityService: UtilityService,
|
||||
private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, private modalService: NgbModal) {
|
||||
public utilityService: UtilityService, private renderer: Renderer2,
|
||||
@Inject(DOCUMENT) private document: Document, private modalService: NgbModal) {
|
||||
this.navService.hideNavBar();
|
||||
this.navService.hideSideNav();
|
||||
}
|
||||
|
@ -852,11 +851,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.LEFT_PART);
|
||||
|
||||
// If the prev page before we change current page is a cover image, we actually are skipping a page
|
||||
console.log('Page ', this.PageNumber, ' is cover image: ', this.isCoverImage(this.cachedImages.prev()))
|
||||
console.log('Page ', this.pageNum, ' is cover image: ', this.isCoverImage())
|
||||
//console.log('Page ', this.PageNumber, ' is cover image: ', this.isCoverImage(this.cachedImages.prev()))
|
||||
//console.log('Page ', this.pageNum, ' is cover image: ', this.isCoverImage())
|
||||
const pageAmount = (this.layoutMode !== LayoutMode.Single && !this.isCoverImage(this.cachedImages.prev())) ? 2: 1;
|
||||
// BUG: isCoverImage works on canvasImage, where we need to know if the previous image is a cover image or not.
|
||||
console.log('pageAmt: ', pageAmount);
|
||||
//console.log('pageAmt: ', pageAmount);
|
||||
if ((this.pageNum - 1 < 0 && notInSplit) || this.isLoading) {
|
||||
if (this.isLoading) { return; }
|
||||
|
||||
|
@ -992,7 +991,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
// Reset scroll on non HEIGHT Fits
|
||||
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
|
||||
window.scrollTo(0, 0);
|
||||
this.document.body.scroll(0, 0)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -327,6 +327,7 @@
|
|||
<option [value]="SortField.SortName">Sort Name</option>
|
||||
<option [value]="SortField.Created">Created</option>
|
||||
<option [value]="SortField.LastModified">Last Modified</option>
|
||||
<option [value]="SortField.LastChapterAdded">Item Added</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -84,7 +84,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||
|
||||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
|
||||
private utilityService: UtilityService, private collectionTagService: CollectionTagService) {
|
||||
|
||||
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.readProgressGroup = new FormGroup({
|
||||
read: new FormControl(this.filter.readStatus.read, []),
|
||||
|
@ -134,7 +134,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||
this.seriesNameGroup.get('seriesNameQuery')?.valueChanges.pipe(
|
||||
map(val => (val || '').trim()),
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.onDestory)).subscribe(changes => {
|
||||
takeUntil(this.onDestory))
|
||||
.subscribe(changes => {
|
||||
this.filter.seriesNameQuery = changes;
|
||||
});
|
||||
}
|
||||
|
@ -151,11 +152,29 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
if (this.filterSettings.presets) {
|
||||
this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets?.readStatus.read);
|
||||
this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets?.readStatus.notRead);
|
||||
this.readProgressGroup.get('inProgress')?.patchValue(this.filterSettings.presets?.readStatus.inProgress);
|
||||
this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets.readStatus.read);
|
||||
this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets.readStatus.notRead);
|
||||
this.readProgressGroup.get('inProgress')?.patchValue(this.filterSettings.presets.readStatus.inProgress);
|
||||
|
||||
if (this.filterSettings.presets.sortOptions) {
|
||||
this.sortGroup.get('sortField')?.setValue(this.filterSettings.presets.sortOptions.sortField);
|
||||
this.isAscendingSort = this.filterSettings.presets.sortOptions.isAscending;
|
||||
if (this.filter.sortOptions) {
|
||||
this.filter.sortOptions.isAscending = this.isAscendingSort;
|
||||
this.filter.sortOptions.sortField = this.filterSettings.presets.sortOptions.sortField;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets.rating > 0) {
|
||||
this.updateRating(this.filterSettings.presets.rating);
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets.seriesNameQuery !== '') {
|
||||
this.seriesNameGroup.get('searchNameQuery')?.setValue(this.filterSettings.presets.seriesNameQuery);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.setupTypeaheads();
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</span>
|
||||
{{readingList?.title}} <span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
</h2>
|
||||
<h6 subtitle>{{items.length}} Items</h6>
|
||||
<h6 subtitle style="margin-left: 40px">{{items.length}} Items</h6>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid mt-2" *ngIf="readingList">
|
||||
|
||||
|
|
|
@ -15,6 +15,6 @@
|
|||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [supressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)" (clicked)="handleClick(item)"></app-card-item>
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)" (clicked)="handleClick(item)"></app-card-item>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)">
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
|
||||
<h2 title>
|
||||
Recently Added
|
||||
</h2>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Subject } from 'rxjs';
|
|||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||
import { FilterSettings } from '../metadata-filter/filter-settings';
|
||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||
import { Pagination } from '../_models/pagination';
|
||||
import { Series } from '../_models/series';
|
||||
|
@ -15,6 +15,8 @@ import { ActionService } from '../_services/action.service';
|
|||
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
|
||||
import { SeriesService } from '../_services/series.service';
|
||||
|
||||
// TODO: Do I still need this or can All Series handle it with a custom sort
|
||||
|
||||
/**
|
||||
* This component is used as a standard layout for any card detail. ie) series, on-deck, collections, etc.
|
||||
*/
|
||||
|
@ -23,7 +25,6 @@ import { SeriesService } from '../_services/series.service';
|
|||
templateUrl: './recently-added.component.html',
|
||||
styleUrls: ['./recently-added.component.scss']
|
||||
})
|
||||
|
||||
export class RecentlyAddedComponent implements OnInit, OnDestroy {
|
||||
|
||||
isLoading: boolean = true;
|
||||
|
@ -34,15 +35,17 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy {
|
|||
filter: SeriesFilter | undefined = undefined;
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
filterActive: boolean = false;
|
||||
|
||||
onDestroy: Subject<void> = new Subject();
|
||||
|
||||
constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private titleService: Title,
|
||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService) {
|
||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||
private utilityService: UtilityService) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.titleService.setTitle('Kavita - Recently Added');
|
||||
if (this.pagination === undefined || this.pagination === null) {
|
||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
this.pagination = {currentPage: 1, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
}
|
||||
this.filterSettings.sortDisabled = true;
|
||||
|
||||
|
@ -102,6 +105,7 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy {
|
|||
if (page != null) {
|
||||
this.pagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
|
||||
this.isLoading = true;
|
||||
this.seriesService.getRecentlyAdded(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
|
|
|
@ -42,7 +42,7 @@ export class ConfirmResetPasswordComponent implements OnInit {
|
|||
this.toastr.success("Password reset");
|
||||
this.router.navigateByUrl('login');
|
||||
}, err => {
|
||||
console.log(err);
|
||||
console.error(err, 'There was an error trying to confirm reset password');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
||||
<app-image maxWidth="300px" [imageUrl]="seriesImage"></app-image>
|
||||
<!-- NOTE: We can put continue point here as Vol X Ch Y or just Ch Y or Book Z ?-->
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
<div class="row g-0">
|
||||
|
|
276
UI/Web/src/app/shared/_services/filter-utilities.service.ts
Normal file
276
UI/Web/src/app/shared/_services/filter-utilities.service.ts
Normal file
|
@ -0,0 +1,276 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { Pagination } from 'src/app/_models/pagination';
|
||||
import { SeriesFilter, SortField } from 'src/app/_models/series-filter';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FilterUtilitiesService {
|
||||
|
||||
constructor(private route: ActivatedRoute, private seriesService: SeriesService) { }
|
||||
|
||||
/**
|
||||
* Updates the window location with a custom url based on filter and pagination objects
|
||||
* @param pagination
|
||||
* @param filter
|
||||
*/
|
||||
updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter) {
|
||||
let params = '?page=' + pagination.currentPage;
|
||||
|
||||
const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter);
|
||||
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination));
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the page query param in the window location.
|
||||
* @param pagination
|
||||
*/
|
||||
updateUrlFromPagination(pagination: Pagination) {
|
||||
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(window.location.href, pagination));
|
||||
}
|
||||
|
||||
private replacePaginationOnUrl(url: string, pagination: Pagination) {
|
||||
return url.replace(/page=\d+/i, 'page=' + pagination.currentPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will fetch current page from route if present
|
||||
* @returns A default pagination object
|
||||
*/
|
||||
pagination(): Pagination {
|
||||
return {currentPage: parseInt(this.route.snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the current url with query params for the filter
|
||||
* @param currentUrl Full url, with ?page=1 as a minimum
|
||||
* @param filter Filter to build url off
|
||||
* @returns current url with query params added
|
||||
*/
|
||||
urlFromFilter(currentUrl: string, filter: SeriesFilter | undefined) {
|
||||
if (filter === undefined) return currentUrl;
|
||||
let params = '';
|
||||
|
||||
|
||||
|
||||
params += this.joinFilter(filter.formats, 'format');
|
||||
params += this.joinFilter(filter.genres, 'genres');
|
||||
params += this.joinFilter(filter.ageRating, 'ageRating');
|
||||
params += this.joinFilter(filter.publicationStatus, 'publicationStatus');
|
||||
params += this.joinFilter(filter.tags, 'tags');
|
||||
params += this.joinFilter(filter.languages, 'languages');
|
||||
params += this.joinFilter(filter.collectionTags, 'collectionTags');
|
||||
params += this.joinFilter(filter.libraries, 'libraries');
|
||||
|
||||
params += this.joinFilter(filter.writers, 'writers');
|
||||
params += this.joinFilter(filter.artists, 'artists');
|
||||
params += this.joinFilter(filter.character, 'character');
|
||||
params += this.joinFilter(filter.colorist, 'colorist');
|
||||
params += this.joinFilter(filter.coverArtist, 'coverArtists');
|
||||
params += this.joinFilter(filter.editor, 'editor');
|
||||
params += this.joinFilter(filter.inker, 'inker');
|
||||
params += this.joinFilter(filter.letterer, 'letterer');
|
||||
params += this.joinFilter(filter.penciller, 'penciller');
|
||||
params += this.joinFilter(filter.publisher, 'publisher');
|
||||
params += this.joinFilter(filter.translators, 'translators');
|
||||
|
||||
// readStatus (we need to do an additonal check as there is a default case)
|
||||
if (filter.readStatus && filter.readStatus.inProgress !== true && filter.readStatus.notRead !== true && filter.readStatus.read !== true) {
|
||||
params += '&readStatus=' + `${filter.readStatus.inProgress},${filter.readStatus.notRead},${filter.readStatus.read}`;
|
||||
}
|
||||
|
||||
// sortBy (additional check to not save to url if default case)
|
||||
if (filter.sortOptions && !(filter.sortOptions.sortField === SortField.SortName && filter.sortOptions.isAscending === true)) {
|
||||
params += '&sortBy=' + filter.sortOptions.sortField + ',' + filter.sortOptions.isAscending;
|
||||
}
|
||||
|
||||
if (filter.rating > 0) {
|
||||
params += '&rating=' + filter.rating;
|
||||
}
|
||||
|
||||
if (filter.seriesNameQuery !== '') {
|
||||
params += '&name=' + encodeURIComponent(filter.seriesNameQuery);
|
||||
}
|
||||
|
||||
return currentUrl + params;
|
||||
}
|
||||
|
||||
private joinFilter(filterProp: Array<any>, key: string) {
|
||||
let params = '';
|
||||
if (filterProp.length > 0) {
|
||||
params += `&${key}=` + filterProp.join(',');
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of a filterSettings that is populated with filter presets from URL
|
||||
* @returns The Preset filter and if something was set within
|
||||
*/
|
||||
filterPresetsFromUrl(): [SeriesFilter, boolean] {
|
||||
const snapshot = this.route.snapshot;
|
||||
const filter = this.seriesService.createSeriesFilter();
|
||||
let anyChanged = false;
|
||||
|
||||
const format = snapshot.queryParamMap.get('format');
|
||||
if (format !== undefined && format !== null) {
|
||||
filter.formats = [...filter.formats, ...format.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const genres = snapshot.queryParamMap.get('genres');
|
||||
if (genres !== undefined && genres !== null) {
|
||||
filter.genres = [...filter.genres, ...genres.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const ageRating = snapshot.queryParamMap.get('ageRating');
|
||||
if (ageRating !== undefined && ageRating !== null) {
|
||||
filter.ageRating = [...filter.ageRating, ...ageRating.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const publicationStatus = snapshot.queryParamMap.get('publicationStatus');
|
||||
if (publicationStatus !== undefined && publicationStatus !== null) {
|
||||
filter.publicationStatus = [...filter.publicationStatus, ...publicationStatus.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const tags = snapshot.queryParamMap.get('tags');
|
||||
if (tags !== undefined && tags !== null) {
|
||||
filter.tags = [...filter.tags, ...tags.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const languages = snapshot.queryParamMap.get('languages');
|
||||
if (languages !== undefined && languages !== null) {
|
||||
filter.languages = [...filter.languages, ...languages.split(',')];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const writers = snapshot.queryParamMap.get('writers');
|
||||
if (writers !== undefined && writers !== null) {
|
||||
filter.writers = [...filter.writers, ...writers.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const artists = snapshot.queryParamMap.get('artists');
|
||||
if (artists !== undefined && artists !== null) {
|
||||
filter.artists = [...filter.artists, ...artists.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const character = snapshot.queryParamMap.get('character');
|
||||
if (character !== undefined && character !== null) {
|
||||
filter.character = [...filter.character, ...character.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const colorist = snapshot.queryParamMap.get('colorist');
|
||||
if (colorist !== undefined && colorist !== null) {
|
||||
filter.colorist = [...filter.colorist, ...colorist.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const coverArtists = snapshot.queryParamMap.get('coverArtists');
|
||||
if (coverArtists !== undefined && coverArtists !== null) {
|
||||
filter.coverArtist = [...filter.coverArtist, ...coverArtists.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const editor = snapshot.queryParamMap.get('editor');
|
||||
if (editor !== undefined && editor !== null) {
|
||||
filter.editor = [...filter.editor, ...editor.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const inker = snapshot.queryParamMap.get('inker');
|
||||
if (inker !== undefined && inker !== null) {
|
||||
filter.inker = [...filter.inker, ...inker.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const letterer = snapshot.queryParamMap.get('letterer');
|
||||
if (letterer !== undefined && letterer !== null) {
|
||||
filter.letterer = [...filter.letterer, ...letterer.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const penciller = snapshot.queryParamMap.get('penciller');
|
||||
if (penciller !== undefined && penciller !== null) {
|
||||
filter.penciller = [...filter.penciller, ...penciller.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const publisher = snapshot.queryParamMap.get('publisher');
|
||||
if (publisher !== undefined && publisher !== null) {
|
||||
filter.publisher = [...filter.publisher, ...publisher.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const translators = snapshot.queryParamMap.get('translators');
|
||||
if (translators !== undefined && translators !== null) {
|
||||
filter.translators = [...filter.translators, ...translators.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const libraries = snapshot.queryParamMap.get('libraries');
|
||||
if (libraries !== undefined && libraries !== null) {
|
||||
filter.libraries = [...filter.libraries, ...libraries.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const collectionTags = snapshot.queryParamMap.get('collectionTags');
|
||||
if (collectionTags !== undefined && collectionTags !== null) {
|
||||
filter.collectionTags = [...filter.collectionTags, ...collectionTags.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
// Rating, seriesName,
|
||||
const rating = snapshot.queryParamMap.get('rating');
|
||||
if (rating !== undefined && rating !== null && parseInt(rating, 10) > 0) {
|
||||
filter.rating = parseInt(rating, 10);
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
/// Read status is encoded as true,true,true
|
||||
const readStatus = snapshot.queryParamMap.get('readStatus');
|
||||
if (readStatus !== undefined && readStatus !== null) {
|
||||
const values = readStatus.split(',').map(i => i === 'true');
|
||||
if (values.length === 3) {
|
||||
filter.readStatus.inProgress = values[0];
|
||||
filter.readStatus.notRead = values[1];
|
||||
filter.readStatus.read = values[2];
|
||||
anyChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
const sortBy = snapshot.queryParamMap.get('sortBy');
|
||||
if (sortBy !== undefined && sortBy !== null) {
|
||||
const values = sortBy.split(',');
|
||||
if (values.length === 1) {
|
||||
values.push('true');
|
||||
}
|
||||
if (values.length === 2) {
|
||||
filter.sortOptions = {
|
||||
isAscending: values[1] === 'true',
|
||||
sortField: Number(values[0])
|
||||
}
|
||||
anyChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
const searchNameQuery = snapshot.queryParamMap.get('name');
|
||||
if (searchNameQuery !== undefined && searchNameQuery !== null && searchNameQuery !== '') {
|
||||
filter.seriesNameQuery = decodeURIComponent(searchNameQuery);
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
|
||||
return [filter, false]; // anyChanged. Testing out if having a filter active but keep drawer closed by default works better
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import { Chapter } from 'src/app/_models/chapter';
|
|||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { SeriesFilter } from 'src/app/_models/series-filter';
|
||||
import { SeriesFilter, SortField } from 'src/app/_models/series-filter';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
|
||||
export enum KEY_CODES {
|
||||
|
@ -67,7 +67,7 @@ export class UtilityService {
|
|||
* @param includeSpace Add a space at the end of the string. if includeHash and includeSpace are true, only hash will be at the end.
|
||||
* @returns
|
||||
*/
|
||||
formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) {
|
||||
formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) {
|
||||
switch(libraryType) {
|
||||
case LibraryType.Book:
|
||||
return 'Book' + (includeSpace ? ' ' : '');
|
||||
|
@ -96,133 +96,6 @@ export class UtilityService {
|
|||
return input.toUpperCase().replace(reg, '').includes(filter.toUpperCase().replace(reg, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of a filterSettings that is populated with filter presets from URL
|
||||
* @param snapshot
|
||||
* @param blankFilter Filter to start with
|
||||
* @returns The Preset filter and if something was set within
|
||||
*/
|
||||
filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot, blankFilter: SeriesFilter): [SeriesFilter, boolean] {
|
||||
const filter = Object.assign({}, blankFilter);
|
||||
let anyChanged = false;
|
||||
|
||||
const format = snapshot.queryParamMap.get('format');
|
||||
if (format !== undefined && format !== null) {
|
||||
filter.formats = [...filter.formats, ...format.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const genres = snapshot.queryParamMap.get('genres');
|
||||
if (genres !== undefined && genres !== null) {
|
||||
filter.genres = [...filter.genres, ...genres.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const ageRating = snapshot.queryParamMap.get('ageRating');
|
||||
if (ageRating !== undefined && ageRating !== null) {
|
||||
filter.ageRating = [...filter.ageRating, ...ageRating.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const publicationStatus = snapshot.queryParamMap.get('publicationStatus');
|
||||
if (publicationStatus !== undefined && publicationStatus !== null) {
|
||||
filter.publicationStatus = [...filter.publicationStatus, ...publicationStatus.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const tags = snapshot.queryParamMap.get('tags');
|
||||
if (tags !== undefined && tags !== null) {
|
||||
filter.tags = [...filter.tags, ...tags.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const languages = snapshot.queryParamMap.get('languages');
|
||||
if (languages !== undefined && languages !== null) {
|
||||
filter.languages = [...filter.languages, ...languages.split(',')];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const writers = snapshot.queryParamMap.get('writers');
|
||||
if (writers !== undefined && writers !== null) {
|
||||
filter.writers = [...filter.writers, ...writers.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const artists = snapshot.queryParamMap.get('artists');
|
||||
if (artists !== undefined && artists !== null) {
|
||||
filter.artists = [...filter.artists, ...artists.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const character = snapshot.queryParamMap.get('character');
|
||||
if (character !== undefined && character !== null) {
|
||||
filter.character = [...filter.character, ...character.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const colorist = snapshot.queryParamMap.get('colorist');
|
||||
if (colorist !== undefined && colorist !== null) {
|
||||
filter.colorist = [...filter.colorist, ...colorist.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const coverArtists = snapshot.queryParamMap.get('coverArtists');
|
||||
if (coverArtists !== undefined && coverArtists !== null) {
|
||||
filter.coverArtist = [...filter.coverArtist, ...coverArtists.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const editor = snapshot.queryParamMap.get('editor');
|
||||
if (editor !== undefined && editor !== null) {
|
||||
filter.editor = [...filter.editor, ...editor.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const inker = snapshot.queryParamMap.get('inker');
|
||||
if (inker !== undefined && inker !== null) {
|
||||
filter.inker = [...filter.inker, ...inker.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const letterer = snapshot.queryParamMap.get('letterer');
|
||||
if (letterer !== undefined && letterer !== null) {
|
||||
filter.letterer = [...filter.letterer, ...letterer.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const penciller = snapshot.queryParamMap.get('penciller');
|
||||
if (penciller !== undefined && penciller !== null) {
|
||||
filter.penciller = [...filter.penciller, ...penciller.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const publisher = snapshot.queryParamMap.get('publisher');
|
||||
if (publisher !== undefined && publisher !== null) {
|
||||
filter.publisher = [...filter.publisher, ...publisher.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
const translators = snapshot.queryParamMap.get('translators');
|
||||
if (translators !== undefined && translators !== null) {
|
||||
filter.translators = [...filter.translators, ...translators.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
/// Read status is encoded as true,true,true
|
||||
const readStatus = snapshot.queryParamMap.get('readStatus');
|
||||
if (readStatus !== undefined && readStatus !== null) {
|
||||
const values = readStatus.split(',').map(i => i === "true");
|
||||
if (values.length === 3) {
|
||||
filter.readStatus.inProgress = values[0];
|
||||
filter.readStatus.notRead = values[1];
|
||||
filter.readStatus.read = values[2];
|
||||
anyChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return [filter, anyChanged];
|
||||
}
|
||||
|
||||
mangaFormat(format: MangaFormat): string {
|
||||
switch (format) {
|
||||
|
@ -305,4 +178,27 @@ export class UtilityService {
|
|||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
deepEqual(object1: any, object2: any) {
|
||||
const keys1 = Object.keys(object1);
|
||||
const keys2 = Object.keys(object2);
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false;
|
||||
}
|
||||
for (const key of keys1) {
|
||||
const val1 = object1[key];
|
||||
const val2 = object2[key];
|
||||
const areObjects = this.isObject(val1) && this.isObject(val2);
|
||||
if (
|
||||
areObjects && !this.deepEqual(val1, val2) ||
|
||||
!areObjects && val1 !== val2
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private isObject(object: any) {
|
||||
return object != null && typeof object === 'object';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
<img #img class="lazyload" [src]="imageService.placeholderImage" [attr.data-src]="imageUrl"
|
||||
(error)="imageService.updateErroredImage($event)"
|
||||
aria-hidden="true">
|
||||
|
||||
<!-- <img #img [defaultImage]="imageService.placeholderImage" [lazyload]="imageUrl"
|
||||
(error)="imageService.updateErroredImage($event)"
|
||||
aria-hidden="true"> -->
|
||||
aria-hidden="true">
|
|
@ -4,13 +4,13 @@
|
|||
<ng-content select="[title]"></ng-content>
|
||||
<ng-content select="[subtitle]"></ng-content>
|
||||
</div>
|
||||
<div class="col mr-auto hide-if-empty">
|
||||
<div class="col mr-auto hide-if-empty d-none d-sm-flex">
|
||||
<ng-content select="[main]"></ng-content>
|
||||
</div>
|
||||
<div class="col" *ngIf="hasFilter">
|
||||
<div class="row justify-content-end">
|
||||
<div class="col-auto align-self-end">
|
||||
<button *ngIf="hasFilter" class="btn btn-secondary btn-small" (click)="toggleFilter()" [attr.aria-expanded]="filterOpen" placement="left" ngbTooltip="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting">
|
||||
<button *ngIf="hasFilter" class="btn btn-{{filterActive ? 'primary' : 'secondary'}} btn-small" (click)="toggleFilter()" [attr.aria-expanded]="filterOpen" placement="left" ngbTooltip="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting">
|
||||
<i class="fa fa-filter" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Sort / Filter</span>
|
||||
</button>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
|
||||
/**
|
||||
* This should go on all pages which have the side nav present and is not Settings related.
|
||||
|
@ -20,6 +20,11 @@ export class SideNavCompanionBarComponent implements OnInit {
|
|||
*/
|
||||
@Input() filterOpenByDefault: boolean = false;
|
||||
|
||||
/**
|
||||
* This implies there is a filter in effect on the underlying page. Will show UI styles to imply this to the user.
|
||||
*/
|
||||
@Input() filterActive: boolean = false;
|
||||
|
||||
/**
|
||||
* Should be passed through from Filter component.
|
||||
*/
|
||||
|
|
|
@ -50,7 +50,6 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
|
||||
this.messageHub.messages$.pipe(takeUntil(this.onDestory), filter(event => event.event === EVENTS.LibraryModified)).subscribe(event => {
|
||||
console.log('Received library modfied event');
|
||||
this.libraryService.getLibrariesForMember().pipe(take(1)).subscribe((libraries: Library[]) => {
|
||||
this.libraries = libraries;
|
||||
});
|
||||
|
|
|
@ -46,7 +46,8 @@ export class ThemeTestComponent implements OnInit {
|
|||
volumes: [],
|
||||
localizedNameLocked: false,
|
||||
nameLocked: false,
|
||||
sortNameLocked: false
|
||||
sortNameLocked: false,
|
||||
lastChapterAdded: '',
|
||||
}
|
||||
|
||||
seriesWithProgress: Series = {
|
||||
|
@ -67,7 +68,8 @@ export class ThemeTestComponent implements OnInit {
|
|||
volumes: [],
|
||||
localizedNameLocked: false,
|
||||
nameLocked: false,
|
||||
sortNameLocked: false
|
||||
sortNameLocked: false,
|
||||
lastChapterAdded: '',
|
||||
}
|
||||
|
||||
get TagBadgeCursor(): typeof TagBadgeCursor {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<ng-container *ngIf="settings.multiple && (selectedData | async) as selected">
|
||||
<button class="btn btn-close float-end mt-2" *ngIf="selected.length > 0" style="font-size: 0.8rem;" (click)="clearSelections($event)"></button>
|
||||
<button class="btn btn-close float-end mt-2" *ngIf="selected.length > 0" style="font-size: 0.8rem;" (click)="clearSelections()"></button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -28,7 +28,7 @@
|
|||
Add {{typeaheadControl.value}}...
|
||||
</li>
|
||||
<li *ngFor="let option of filteredOptions | async; let index = index;" (click)="handleOptionClick(option)"
|
||||
class="list-group-item" role="option"
|
||||
class="list-group-item" role="option" [attr.data-index]="index"
|
||||
(mouseenter)="focusedIndex = index + (showAddItem ? 1 : 0); updateHighlight();">
|
||||
<ng-container [ngTemplateOutlet]="optionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
|
||||
</li>
|
||||
|
|
|
@ -179,6 +179,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
ngOnInit() {
|
||||
|
||||
this.reset.pipe(takeUntil(this.onDestroy)).subscribe((reset: boolean) => {
|
||||
this.clearSelections();
|
||||
this.init();
|
||||
});
|
||||
|
||||
|
@ -258,9 +259,6 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
} else {
|
||||
this.optionSelection = new SelectionModel<any>(true, [this.settings.savedData]);
|
||||
}
|
||||
|
||||
|
||||
//this.typeaheadControl.setValue(this.settings.displayFn(this.settings.savedData))
|
||||
}
|
||||
} else {
|
||||
this.optionSelection = new SelectionModel<any>();
|
||||
|
@ -308,14 +306,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
const filteredResults = opts.filter(item => this.filterSelected(item));
|
||||
|
||||
if (filteredResults.length < this.focusedIndex) return;
|
||||
const option = filteredResults[this.focusedIndex];
|
||||
|
||||
this.toggleSelection(option);
|
||||
this.resetField();
|
||||
(item as HTMLElement).click();
|
||||
this.focusedIndex = 0;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -356,10 +347,12 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
this.resetField();
|
||||
}
|
||||
|
||||
clearSelections(event: any) {
|
||||
this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false));
|
||||
this.selectedData.emit(this.optionSelection.selected());
|
||||
this.resetField();
|
||||
clearSelections() {
|
||||
if (this.optionSelection) {
|
||||
this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false));
|
||||
this.selectedData.emit(this.optionSelection.selected());
|
||||
this.resetField();
|
||||
}
|
||||
}
|
||||
|
||||
handleOptionClick(opt: any) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue