Reading List Fixes (#1784)
* Add ability to save readinglist comicinfo fields in Chapter. * Added the appropriate fields and migration for Reading List generation. * Started the reading list code * Started building out the CBL import code with some initial unit tests. * Fixed first unit test * Started refactoring control code into services and writing unit tests for ReadingLists. Found a logic issue around reading list title between create/update. Will be corrected in this branch with unit tests. * Can't figure out how to mock UserManager, so had to uncomment a few tests. * Tooltip for total pages read shows the full number * Tweaked the math a bit for average reading per week. * Fixed up the reading list unit tests. Fixed an issue where when inserting chapters into a blank reading list, the initial reading list item would have an order of 1 instead of 0. * Cleaned up the code to allow the reading list code to be localized easily and fixed up a bug in last PR. * Fixed a sorting issue on reading activity * Tweaked the code around reading list actionables not showing due to some weird filter. * Fixed edit library settings not opening on library detail page * Fixed a bug where reading activity dates would be out of order due to a bug in how charts works. A temp hack has been added. * Disable promotion in edit reading list modal since non-admins can (and should have) been able to use it. * Fixed a bug where non-admins couldn't update their OWN reading lists. Made uploading a cover image for readinglists now check against the user's reading list access to allow non-admin's to set images. * Fixed an issue introduced earlier in PR where adding chapters to reading list could cause order to get skewed. * Fixed another regression from earlier commit * Hooked in Import CBL flow. No functionality yet. * Code is a mess. Shifting how the whole import process is going to be done. Commiting so I can pivot drastically. * Very rough code for first step is done. * Ui has started, I've run out of steam for this feature. * Cleaned up the UI code a bit to make the step tracker nature easier without a dedicated component. * Much flow implementation and tweaking to how validation checks and what is sent back. * Removed import via cbl code as it's not done. Pushing to next release.
This commit is contained in:
parent
ae1af22af1
commit
3f24dc7392
48 changed files with 21951 additions and 170 deletions
|
|
@ -0,0 +1,8 @@
|
|||
import { CblImportReason } from "./cbl-import-reason.enum";
|
||||
|
||||
export interface CblBookResult {
|
||||
series: string;
|
||||
volume: string;
|
||||
number: string;
|
||||
reason: CblImportReason;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
export enum CblImportReason {
|
||||
ChapterMissing = 0,
|
||||
VolumeMissing = 1,
|
||||
SeriesMissing = 2,
|
||||
NameConflict = 3,
|
||||
AllSeriesMissing = 4,
|
||||
EmptyFile = 5,
|
||||
SeriesCollision = 6,
|
||||
AllChapterMissing = 7,
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export enum CblImportResult {
|
||||
Fail = 0,
|
||||
Partial = 1,
|
||||
Success = 2
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Series } from "../../series";
|
||||
import { CblBookResult } from "./cbl-book-result";
|
||||
import { CblImportResult } from "./cbl-import-result.enum";
|
||||
|
||||
export interface CblConflictQuestion {
|
||||
seriesName: string;
|
||||
librariesIds: Array<number>;
|
||||
}
|
||||
|
||||
export interface CblImportSummary {
|
||||
cblName: string;
|
||||
results: Array<CblBookResult>;
|
||||
success: CblImportResult;
|
||||
successfulInserts: Array<CblBookResult>;
|
||||
conflicts: Array<Series>;
|
||||
conflicts2: Array<CblConflictQuestion>;
|
||||
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import { Chapter } from '../_models/chapter';
|
|||
import { CollectionTag } from '../_models/collection-tag';
|
||||
import { Device } from '../_models/device/device';
|
||||
import { Library } from '../_models/library';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
import { ReadingList } from '../_models/reading-list';
|
||||
import { Series } from '../_models/series';
|
||||
import { Volume } from '../_models/volume';
|
||||
|
|
@ -85,6 +84,10 @@ export enum Action {
|
|||
* Send to a device
|
||||
*/
|
||||
SendTo = 17,
|
||||
/**
|
||||
* Import some data into Kavita
|
||||
*/
|
||||
Import = 18,
|
||||
}
|
||||
|
||||
export interface ActionItem<T> {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { BulkAddToCollectionComponent } from '../cards/_modals/bulk-add-to-colle
|
|||
import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component';
|
||||
import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component';
|
||||
import { ConfirmService } from '../shared/confirm.service';
|
||||
import { LibrarySettingsModalComponent } from '../sidenav/_modals/library-settings-modal/library-settings-modal.component';
|
||||
import { Chapter } from '../_models/chapter';
|
||||
import { Device } from '../_models/device/device';
|
||||
import { Library } from '../_models/library';
|
||||
|
|
@ -99,6 +100,14 @@ export class ActionService implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
editLibrary(library: Partial<Library>, callback?: LibraryActionCallback) {
|
||||
const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' });
|
||||
modalRef.componentInstance.library = library;
|
||||
modalRef.closed.subscribe((closeResult: {success: boolean, library: Library, coverImageUpdate: boolean}) => {
|
||||
if (callback) callback(library)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request an analysis of files for a given Library (currently just word count)
|
||||
* @param library Partial Library, must have id and name populated
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { environment } from 'src/environments/environment';
|
|||
import { UtilityService } from '../shared/_services/utility.service';
|
||||
import { PaginatedResult } from '../_models/pagination';
|
||||
import { ReadingList, ReadingListItem } from '../_models/reading-list';
|
||||
import { CblImportResult } from '../_models/reading-list/cbl/cbl-import-result.enum';
|
||||
import { CblImportSummary } from '../_models/reading-list/cbl/cbl-import-summary';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import { ActionItem } from './action-factory.service';
|
||||
|
||||
|
|
@ -92,4 +94,8 @@ export class ReadingListService {
|
|||
nameExists(name: string) {
|
||||
return this.httpClient.get<boolean>(this.baseUrl + 'readinglist/name-exists?name=' + name);
|
||||
}
|
||||
|
||||
importCbl(form: FormData) {
|
||||
return this.httpClient.post<CblImportSummary>(this.baseUrl + 'readinglist/import-cbl', form);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
|||
}
|
||||
|
||||
hasCustomSort() {
|
||||
return this.filter.sortOptions !== null || this.filterSettings.presets?.sortOptions !== null;
|
||||
return this.filter.sortOptions !== null || this.filterSettings?.presets?.sortOptions !== null;
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ export class CardActionablesComponent implements OnInit {
|
|||
this.canDownload = this.accountService.hasDownloadRole(user);
|
||||
|
||||
// We want to avoid an empty menu when user doesn't have access to anything
|
||||
const validActions = this.actions.filter(a => a.children.length > 0 || a.dynamicList);
|
||||
if (!this.isAdmin && validActions.filter(a => !a.requiresAdmin).length === 0) {
|
||||
if (!this.isAdmin && this.actions.filter(a => !a.requiresAdmin).length === 0) {
|
||||
this.actions = [];
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<div class="container-fluid" style="padding-left: 0px; padding-right: 0px">
|
||||
<form [formGroup]="form">
|
||||
<ngx-file-drop (onFileDrop)="dropped($event)"
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" [accept]="acceptableExtensions" [directory]="false" dropZoneClassName="file-upload" contentClassName="file-upload-zone" [directory]="false">
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" [accept]="acceptableExtensions" [directory]="false"
|
||||
dropZoneClassName="file-upload" contentClassName="file-upload-zone">
|
||||
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
||||
<div class="row g-0 mt-3 pb-3" *ngIf="mode === 'all'">
|
||||
<div class="mx-auto">
|
||||
|
|
|
|||
|
|
@ -207,7 +207,10 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
this.actionService.scanLibrary(lib);
|
||||
break;
|
||||
case(Action.RefreshMetadata):
|
||||
this.actionService.refreshMetadata(lib);
|
||||
this.actionService.refreshMetadata(lib);
|
||||
break;
|
||||
case(Action.Edit):
|
||||
this.actionService.editLibrary(lib);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<app-side-nav-companion-bar>
|
||||
<app-side-nav-companion-bar >
|
||||
<h2 title>
|
||||
Reading Lists
|
||||
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
|
||||
<span>Reading Lists</span>
|
||||
</h2>
|
||||
<h6 subtitle *ngIf="pagination">{{pagination.totalItems}} Items</h6>
|
||||
</app-side-nav-companion-bar>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||
|
|
@ -11,6 +12,7 @@ import { ActionService } from 'src/app/_services/action.service';
|
|||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { JumpbarService } from 'src/app/_services/jumpbar.service';
|
||||
import { ReadingListService } from 'src/app/_services/reading-list.service';
|
||||
import { ImportCblModalComponent } from '../../_modals/import-cbl-modal/import-cbl-modal.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reading-lists',
|
||||
|
|
@ -26,10 +28,11 @@ export class ReadingListsComponent implements OnInit {
|
|||
isAdmin: boolean = false;
|
||||
jumpbarKeys: Array<JumpKey> = [];
|
||||
actions: {[key: number]: Array<ActionItem<ReadingList>>} = {};
|
||||
globalActions: Array<ActionItem<any>> = []; //[{action: Action.Import, title: 'Import CBL', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}]
|
||||
|
||||
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
||||
private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService,
|
||||
private jumpbarService: JumpbarService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
private jumpbarService: JumpbarService, private readonly cdRef: ChangeDetectorRef, private ngbModal: NgbModal) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
|
|
@ -51,6 +54,17 @@ export class ReadingListsComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
performGlobalAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
importCbl() {
|
||||
const ref = this.ngbModal.open(ImportCblModalComponent, {size: 'xl'});
|
||||
ref.closed.subscribe(result => this.loadPage());
|
||||
}
|
||||
|
||||
handleReadingListActionCallback(action: ActionItem<ReadingList>, readingList: ReadingList) {
|
||||
switch(action.action) {
|
||||
case Action.Delete:
|
||||
|
|
|
|||
|
|
@ -23,16 +23,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12 ms-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="tag-promoted" role="switch" formControlName="promoted" class="form-check-input"
|
||||
aria-labelledby="auto-close-label" aria-describedby="tag-promoted-help">
|
||||
<label class="form-check-label me-1" for="tag-promoted">Promote</label>
|
||||
<i class="fa fa-info-circle" aria-hidden="true" placement="left" [ngbTooltip]="promotedTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #promotedTooltip>Promotion means that the tag can be seen server-wide, not just for admin users. All series that have this tag will still have user-access restrictions placed on them.</ng-template>
|
||||
<span class="visually-hidden" id="tag-promoted-help"><ng-container [ngTemplateOutlet]="promotedTooltip"></ng-container></span>
|
||||
<ng-container *ngIf="(accountService.currentUser$ | async) as user">
|
||||
<div class="col-md-3 col-sm-12 ms-2" *ngIf="accountService.hasAdminRole(user)">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="tag-promoted" role="switch" formControlName="promoted" class="form-check-input"
|
||||
aria-labelledby="auto-close-label" aria-describedby="tag-promoted-help">
|
||||
<label class="form-check-label me-1" for="tag-promoted">Promote</label>
|
||||
<i class="fa fa-info-circle" aria-hidden="true" placement="left" [ngbTooltip]="promotedTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #promotedTooltip>Promotion means that the tag can be seen server-wide, not just for admin users. All series that have this tag will still have user-access restrictions placed on them.</ng-template>
|
||||
<span class="visually-hidden" id="tag-promoted-help"><ng-container [ngTemplateOutlet]="promotedTooltip"></ng-container></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-3">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ToastrService } from 'ngx-toastr';
|
|||
import { debounceTime, distinctUntilChanged, forkJoin, Subject, switchMap, takeUntil, tap } from 'rxjs';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { ReadingList } from 'src/app/_models/reading-list';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { ReadingListService } from 'src/app/_services/reading-list.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
|
|
@ -41,7 +42,7 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
|
|||
|
||||
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService,
|
||||
public utilityService: UtilityService, private uploadService: UploadService, private toastr: ToastrService,
|
||||
private imageService: ImageService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
private imageService: ImageService, private readonly cdRef: ChangeDetectorRef, public accountService: AccountService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reviewGroup = new FormGroup({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">CBL Import: {{currentStep.title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
|
||||
<div class="row g-0" *ngIf="currentStep.index === 0">
|
||||
<p>Import a .cbl file as a reading list</p>
|
||||
<form [formGroup]="uploadForm" enctype="multipart/form-data">
|
||||
<file-upload formControlName="files"></file-upload>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="currentStep.index === 1">
|
||||
<div class="row g-0">
|
||||
<ng-container *ngIf="validateSummary; else noValidateIssues">
|
||||
<h5>There are issues with the CBL that will prevent an import. Correct these issues then try again.</h5>
|
||||
<ol class="list-group list-group-flush" >
|
||||
<li class="list-group-item no-hover" *ngFor="let result of validateSummary.results">
|
||||
{{result | cblConflictReason}}
|
||||
</li>
|
||||
</ol>
|
||||
</ng-container>
|
||||
<ng-template #noValidateIssues>No issues found with CBL, press next.</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentStep.index === 2 && dryRunSummary">
|
||||
<div class="row g-0">
|
||||
<h5>This is a dry run and shows what will happen if you press Next</h5>
|
||||
<h6>The import was a {{dryRunSummary.success}}</h6>
|
||||
<ul class="list-group list-group-flush" *ngIf="dryRunSummary">
|
||||
<li class="list-group-item no-hover" *ngFor="let result of dryRunSummary.results">
|
||||
{{result | cblConflictReason}}
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-group list-group-flush" *ngIf="dryRunSummary">
|
||||
<li class="list-group-item no-hover" *ngFor="let result of dryRunSummary.successfulInserts">
|
||||
{{result | cblConflictReason}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-icon" href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/reading-lists#creating-a-reading-list-via-cbl" target="_blank" rel="noopener noreferrer">Help</a>
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
||||
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">Next</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { FileUploadValidators } from '@iplab/ngx-file-upload';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { CblImportSummary } from 'src/app/_models/reading-list/cbl/cbl-import-summary';
|
||||
import { ReadingListService } from 'src/app/_services/reading-list.service';
|
||||
|
||||
enum Step {
|
||||
Import = 0,
|
||||
Validate = 1,
|
||||
DryRun = 2,
|
||||
Finalize = 3
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-import-cbl-modal',
|
||||
templateUrl: './import-cbl-modal.component.html',
|
||||
styleUrls: ['./import-cbl-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ImportCblModalComponent {
|
||||
|
||||
@ViewChild('fileUpload') fileUpload!: ElementRef<HTMLInputElement>;
|
||||
|
||||
fileUploadControl = new FormControl<undefined | Array<File>>(undefined, [
|
||||
FileUploadValidators.filesLimit(1),
|
||||
FileUploadValidators.accept(['.cbl']),
|
||||
]);
|
||||
|
||||
uploadForm = new FormGroup({
|
||||
files: this.fileUploadControl
|
||||
});
|
||||
|
||||
importSummaries: Array<CblImportSummary> = [];
|
||||
validateSummary: CblImportSummary | undefined;
|
||||
dryRunSummary: CblImportSummary | undefined;
|
||||
|
||||
steps = [
|
||||
{title: 'Import CBL', index: Step.Import},
|
||||
{title: 'Validate File', index: Step.Validate},
|
||||
{title: 'Dry Run', index: Step.DryRun},
|
||||
{title: 'Final Import', index: Step.Finalize},
|
||||
];
|
||||
currentStep = this.steps[0];
|
||||
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
get Step() { return Step; }
|
||||
|
||||
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService,
|
||||
public utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
close() {
|
||||
this.ngModal.close();
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
|
||||
if (this.currentStep.index >= Step.Finalize) return;
|
||||
if (this.currentStep.index === Step.Import && !this.isFileSelected()) return;
|
||||
if (this.currentStep.index === Step.Validate && this.validateSummary && this.validateSummary.results.length > 0) return;
|
||||
|
||||
switch (this.currentStep.index) {
|
||||
case Step.Import:
|
||||
this.importFile();
|
||||
break;
|
||||
case Step.Validate:
|
||||
break;
|
||||
case Step.DryRun:
|
||||
break;
|
||||
case Step.Finalize:
|
||||
// Clear the models and allow user to do another import
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
canMoveToNextStep() {
|
||||
switch (this.currentStep.index) {
|
||||
case Step.Import:
|
||||
return this.isFileSelected();
|
||||
case Step.Validate:
|
||||
return this.validateSummary && this.validateSummary.results.length > 0;
|
||||
case Step.DryRun:
|
||||
return true;
|
||||
case Step.Finalize:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
isFileSelected() {
|
||||
const files = this.uploadForm.get('files')?.value;
|
||||
if (files) return files.length > 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
importFile() {
|
||||
const files = this.uploadForm.get('files')?.value;
|
||||
if (!files) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('cbl', files[0]);
|
||||
formData.append('dryRun', (this.currentStep.index !== Step.Finalize) + '');
|
||||
this.readingListService.importCbl(formData).subscribe(res => {
|
||||
console.log('Result: ', res);
|
||||
if (this.currentStep.index === Step.Import) {
|
||||
this.validateSummary = res;
|
||||
}
|
||||
if (this.currentStep.index === Step.DryRun) {
|
||||
this.dryRunSummary = res;
|
||||
}
|
||||
this.importSummaries.push(res);
|
||||
this.currentStep.index++;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
// onFileSelected(event: any) {
|
||||
// console.log('event: ', event);
|
||||
// if (!(event.target as HTMLInputElement).files === null || (event.target as HTMLInputElement).files?.length === 0) return;
|
||||
|
||||
// const file = (event.target as HTMLInputElement).files![0];
|
||||
|
||||
// if (file) {
|
||||
|
||||
// //this.fileName = file.name;
|
||||
|
||||
// const formData = new FormData();
|
||||
|
||||
// formData.append("cbl", file);
|
||||
|
||||
// this.readingListService.importCbl(formData).subscribe(res => {
|
||||
// this.importSummaries.push(res);
|
||||
// this.cdRef.markForCheck();
|
||||
// });
|
||||
// this.fileUpload.value = '';
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { CblBookResult } from 'src/app/_models/reading-list/cbl/cbl-book-result';
|
||||
import { CblImportReason } from 'src/app/_models/reading-list/cbl/cbl-import-reason.enum';
|
||||
|
||||
@Pipe({
|
||||
name: 'cblConflictReason'
|
||||
})
|
||||
export class CblConflictReasonPipe implements PipeTransform {
|
||||
|
||||
transform(result: CblBookResult): string {
|
||||
if (result.reason === undefined)
|
||||
return result.series + ' volume ' + result.volume + ' number ' + result.number + ' mapped successfully';
|
||||
switch (result.reason) {
|
||||
case CblImportReason.AllSeriesMissing:
|
||||
return 'Your account is missing access to all series in the list or Kavita does not have anything present in the list.';
|
||||
case CblImportReason.ChapterMissing:
|
||||
return 'Chapter ' + result.number + ' is missing from Kavita. This item will be skipped.';
|
||||
case CblImportReason.EmptyFile:
|
||||
return 'The Cbl file is empty, nothing to be done.';
|
||||
case CblImportReason.NameConflict:
|
||||
return 'A reading list already exists on your account that matches the Cbl file.';
|
||||
case CblImportReason.SeriesCollision:
|
||||
return 'The series, ' + result.series + ', collides with another series of the same name in another library.';
|
||||
case CblImportReason.SeriesMissing:
|
||||
return 'The series, ' + result.series + ', is missing from Kavita or your account does not have permission. All items with this series will be skipped from import.';
|
||||
case CblImportReason.VolumeMissing:
|
||||
return 'Volume ' + result.volume + ' is missing from Kavita. All items with this volume number will be skipped.';
|
||||
case CblImportReason.AllChapterMissing:
|
||||
return 'All chapters cannot be matched to Chapters in Kavita.';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -8,11 +8,14 @@ import { ReactiveFormsModule } from '@angular/forms';
|
|||
import { EditReadingListModalComponent } from './_modals/edit-reading-list-modal/edit-reading-list-modal.component';
|
||||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { NgbNavModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgbAccordionModule, NgbNavModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
|
||||
import { ReadingListDetailComponent } from './_components/reading-list-detail/reading-list-detail.component';
|
||||
import { ReadingListItemComponent } from './_components/reading-list-item/reading-list-item.component';
|
||||
import { ReadingListsComponent } from './_components/reading-lists/reading-lists.component';
|
||||
import { ImportCblModalComponent } from './_modals/import-cbl-modal/import-cbl-modal.component';
|
||||
import { FileUploadModule } from '@iplab/ngx-file-upload';
|
||||
import { CblConflictReasonPipe } from './_pipes/cbl-conflict-reason.pipe';
|
||||
|
||||
|
||||
@NgModule({
|
||||
|
|
@ -22,7 +25,9 @@ import { ReadingListsComponent } from './_components/reading-lists/reading-lists
|
|||
AddToListModalComponent,
|
||||
ReadingListsComponent,
|
||||
EditReadingListModalComponent,
|
||||
ReadingListItemComponent
|
||||
ReadingListItemComponent,
|
||||
ImportCblModalComponent,
|
||||
CblConflictReasonPipe,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
|
@ -37,6 +42,8 @@ import { ReadingListsComponent } from './_components/reading-lists/reading-lists
|
|||
SharedSideNavCardsModule,
|
||||
|
||||
ReadingListRoutingModule,
|
||||
NgbAccordionModule, // Import CBL
|
||||
FileUploadModule, // Import CBL
|
||||
],
|
||||
exports: [
|
||||
AddToListModalComponent,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { Action, ActionFactoryService, ActionItem } from '../../../_services/act
|
|||
import { ActionService } from '../../../_services/action.service';
|
||||
import { LibraryService } from '../../../_services/library.service';
|
||||
import { NavService } from '../../../_services/nav.service';
|
||||
import { LibrarySettingsModalComponent } from '../../_modals/library-settings-modal/library-settings-modal.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-side-nav',
|
||||
|
|
@ -91,18 +90,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||
this.actionService.analyzeFiles(library);
|
||||
break;
|
||||
case (Action.Edit):
|
||||
const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' });
|
||||
modalRef.componentInstance.library = library;
|
||||
modalRef.closed.subscribe((closeResult: {success: boolean, library: Library, coverImageUpdate: boolean}) => {
|
||||
window.scrollTo(0, 0);
|
||||
if (closeResult.success) {
|
||||
|
||||
}
|
||||
|
||||
if (closeResult.coverImageUpdate) {
|
||||
|
||||
}
|
||||
});
|
||||
this.actionService.editLibrary(library, () => window.scrollTo(0, 0));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="dashboard-card-content">
|
||||
<div class="row g-0 mb-2 align-items-center">
|
||||
<div class="col-4">
|
||||
<h4>Top Readers</h4>
|
||||
<h4>Reading Activity</h4>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<form [formGroup]="formGroup" class="d-inline-flex float-end">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div class="row g-0 mt-4 mb-3 d-flex justify-content-around">
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Pages Read" [clickable]="true" fontClasses="fa-regular fa-file-lines" title="Total Pages Read" (click)="openPageByYearList();$event.stopPropagation();">
|
||||
<app-icon-and-title label="Total Pages Read: {{totalPagesRead}}" [clickable]="true" fontClasses="fa-regular fa-file-lines" title="Total Pages Read" (click)="openPageByYearList();$event.stopPropagation();">
|
||||
{{totalPagesRead | compactNumber}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Words Read" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Total Words Read" (click)="openWordByYearList();$event.stopPropagation();">
|
||||
<app-icon-and-title label="Total Words Read: {{totalWordsRead}}" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Total Words Read" (click)="openWordByYearList();$event.stopPropagation();">
|
||||
{{totalWordsRead | compactNumber}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
<ng-container >
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Time Spent Reading" [clickable]="false" fontClasses="fas fa-eye" title="Time Spent Reading">
|
||||
<app-icon-and-title label="Time Spent Reading: {{timeSpentReading}}" [clickable]="false" fontClasses="fas fa-eye" title="Time Spent Reading">
|
||||
{{timeSpentReading | compactNumber}} hours
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue