CBL Import (#1834)
* Wrote my own step tracker and added a prev button. Works up to first conflict flow. * Everything but final import is hooked up in the UI. Polish still needed, but getting there. * Making more progress in the CBL import flow. * Ready for the last step * Cleaned up some logic to prepare for the last step and reset * Users like order to be starting at 1 * Fixed a few bugs around cbl import * CBL import is ready for some basic testing * Added a reading list hook on side nav * Fixed up unit tests * Added icons and color to the import flow * Tweaked some phrasing * Hooked up a loading variable but disabled the component as it didn't look good. * Styling it up * changed an icon to better fit --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
57de661d71
commit
d88a4d5d0c
26 changed files with 1125 additions and 466 deletions
|
@ -90,4 +90,4 @@ img {
|
|||
0px 0px calc(0.5px*3.14) 0.3px rgb(0 0 0 / 43%),
|
||||
0px 0px 1px 0.5px rgb(0 0 0 / 43%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { CblImportReason } from "./cbl-import-reason.enum";
|
||||
|
||||
export interface CblBookResult {
|
||||
order: number;
|
||||
series: string;
|
||||
volume: string;
|
||||
number: string;
|
||||
|
|
|
@ -7,4 +7,5 @@ export enum CblImportReason {
|
|||
EmptyFile = 5,
|
||||
SeriesCollision = 6,
|
||||
AllChapterMissing = 7,
|
||||
Success = 8
|
||||
}
|
|
@ -12,7 +12,4 @@ export interface CblImportSummary {
|
|||
results: Array<CblBookResult>;
|
||||
success: CblImportResult;
|
||||
successfulInserts: Array<CblBookResult>;
|
||||
conflicts: Array<Series>;
|
||||
conflicts2: Array<CblConflictQuestion>;
|
||||
|
||||
}
|
|
@ -95,7 +95,11 @@ export class ReadingListService {
|
|||
return this.httpClient.get<boolean>(this.baseUrl + 'readinglist/name-exists?name=' + name);
|
||||
}
|
||||
|
||||
validateCbl(form: FormData) {
|
||||
return this.httpClient.post<CblImportSummary>(this.baseUrl + 'cbl/validate', form);
|
||||
}
|
||||
|
||||
importCbl(form: FormData) {
|
||||
return this.httpClient.post<CblImportSummary>(this.baseUrl + 'readinglist/import-cbl', form);
|
||||
return this.httpClient.post<CblImportSummary>(this.baseUrl + 'cbl/import', form);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,10 +198,10 @@ $action-bar-height: 38px;
|
|||
height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2
|
||||
}
|
||||
|
||||
&.immersive {
|
||||
// Note: I removed this for bug: https://github.com/Kareadita/Kavita/issues/1726
|
||||
//height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
|
||||
}
|
||||
// &.immersive {
|
||||
// // Note: I removed this for bug: https://github.com/Kareadita/Kavita/issues/1726
|
||||
// //height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
|
||||
// }
|
||||
|
||||
a, :link {
|
||||
color: var(--brtheme-link-text-color);
|
||||
|
|
|
@ -28,7 +28,7 @@ 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)}]
|
||||
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,
|
||||
|
@ -63,6 +63,7 @@ export class ReadingListsComponent implements OnInit {
|
|||
importCbl() {
|
||||
const ref = this.ngbModal.open(ImportCblModalComponent, {size: 'xl'});
|
||||
ref.closed.subscribe(result => this.loadPage());
|
||||
ref.dismissed.subscribe(_ => this.loadPage());
|
||||
}
|
||||
|
||||
handleReadingListActionCallback(action: ActionItem<ReadingList>, readingList: ReadingList) {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
<div class="card card-timeline px-2 mb-3 border-none">
|
||||
<ul class="bs4-order-tracking">
|
||||
<ng-container *ngFor="let step of steps">
|
||||
<li class="step" [ngClass]="{'active': step.index === currentStep}">
|
||||
<div><i class="{{step.icon}}"></i></div>
|
||||
{{step.title}}
|
||||
</li>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,80 @@
|
|||
|
||||
.bs4-order-tracking {
|
||||
overflow: hidden;
|
||||
color: #878788;
|
||||
padding-left: 0px;
|
||||
margin-top: 30px
|
||||
}
|
||||
|
||||
.bs4-order-tracking li {
|
||||
list-style-type: none;
|
||||
font-size: 17px;
|
||||
width: 25%;
|
||||
float: left;
|
||||
position: relative;
|
||||
font-weight: 400;
|
||||
color: #878788;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.bs4-order-tracking li:first-child:before {
|
||||
margin-left: 15px !important;
|
||||
padding-left: 11px !important;
|
||||
text-align: left !important
|
||||
}
|
||||
|
||||
.bs4-order-tracking li:last-child:before {
|
||||
margin-right: 5px !important;
|
||||
padding-right: 11px !important;
|
||||
text-align: right !important
|
||||
}
|
||||
|
||||
.bs4-order-tracking li>div {
|
||||
color: #fff;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
background: #878788;
|
||||
border-radius: 50%;
|
||||
margin: auto
|
||||
}
|
||||
|
||||
.bs4-order-tracking li:after {
|
||||
content: '';
|
||||
width: 150%;
|
||||
height: 2px;
|
||||
background: #878788;
|
||||
position: absolute;
|
||||
left: 0%;
|
||||
right: 0%;
|
||||
top: 25px;
|
||||
z-index: -1
|
||||
}
|
||||
|
||||
.bs4-order-tracking li:first-child:after {
|
||||
left: 50%
|
||||
}
|
||||
|
||||
.bs4-order-tracking li:last-child:after {
|
||||
left: 0% !important;
|
||||
width: 0% !important
|
||||
}
|
||||
|
||||
.bs4-order-tracking li.active {
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.bs4-order-tracking li.active>div {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.bs4-order-tracking li.active:after {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.card-timeline {
|
||||
z-index: 0
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { Component, Input, ChangeDetectionStrategy, OnInit, ChangeDetectorRef } from '@angular/core';
|
||||
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
||||
|
||||
|
||||
export interface TimelineStep {
|
||||
title: string;
|
||||
active: boolean;
|
||||
icon: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-step-tracker',
|
||||
templateUrl: './step-tracker.component.html',
|
||||
styleUrls: ['./step-tracker.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class StepTrackerComponent {
|
||||
@Input() steps: Array<TimelineStep> = [];
|
||||
@Input() currentStep: number = 0;
|
||||
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
}
|
|
@ -1,47 +1,62 @@
|
|||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">CBL Import: {{currentStep.title}}</h4>
|
||||
<h4 class="modal-title" id="modal-basic-title">CBL Import</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 class="modal-body scrollable-modal">
|
||||
<div class="row g-0" style="min-width: 135px;">
|
||||
<app-step-tracker [steps]="steps" [currentStep]="currentStepIndex"></app-step-tracker>
|
||||
</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>
|
||||
<!-- This is going to need to have a fixed height with a scrollbar-->
|
||||
<div>
|
||||
<div class="row g-0" *ngIf="currentStepIndex === Step.Import">
|
||||
<p>To get started, import a .cbl file. Kavita will perform multiple checks before importing. Some steps will block moving forward due to issues with the file.</p>
|
||||
<form [formGroup]="uploadForm" enctype="multipart/form-data">
|
||||
<file-upload formControlName="files"></file-upload>
|
||||
</form>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentStepIndex === Step.Validate">
|
||||
<div class="row g-0">
|
||||
<ng-container *ngIf="validateSummary">
|
||||
<ng-container *ngIf="validateSummary.results.length > 0; 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-numbered list-group-flush" >
|
||||
<li class="list-group-item no-hover" *ngFor="let result of validateSummary.results"
|
||||
[innerHTML]="result | cblConflictReason | safeHtml">
|
||||
</li>
|
||||
</ol>
|
||||
</ng-container>
|
||||
<ng-template #noValidateIssues>
|
||||
No issues found with CBL, press next.
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentStepIndex === Step.DryRun && 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 | cblImportResult}}</h6>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item no-hover" *ngFor="let result of dryRunResults"
|
||||
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentStepIndex === Step.Finalize && finalizeSummary && dryRunSummary">
|
||||
<div class="row g-0">
|
||||
<h5>{{finalizeSummary.success | cblImportResult }} on {{dryRunSummary.cblName}} Import</h5>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item no-hover" *ngFor="let result of finalizeResults"
|
||||
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
@ -49,7 +64,8 @@
|
|||
<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>
|
||||
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">Prev</button>
|
||||
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">{{NextButtonLabel}}</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,41 @@
|
|||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
display: none;
|
||||
}
|
||||
|
||||
::ng-deep .file-info {
|
||||
width: 83%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
::ng-deep .file-buttons {
|
||||
float: right;
|
||||
}
|
||||
|
||||
file-upload {
|
||||
background: none;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
::ng-deep .upload-input {
|
||||
color: var(--input-text-color) !important;
|
||||
}
|
||||
|
||||
::ng-deep file-upload-list-item {
|
||||
color: var(--input-text-color) !important;
|
||||
}
|
||||
|
||||
::ng-deep .remove-btn {
|
||||
background: #C0392B;
|
||||
border-radius: 3px;
|
||||
color: var(--input-text-color) !important;
|
||||
font-weight: bold;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
::ng-deep .reading-list-success--item {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
::ng-deep .reading-list-fail--item {
|
||||
color: var(--error-color);
|
||||
}
|
|
@ -2,9 +2,13 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, View
|
|||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { FileUploadValidators } from '@iplab/ngx-file-upload';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { CblBookResult } from 'src/app/_models/reading-list/cbl/cbl-book-result';
|
||||
import { CblImportResult } from 'src/app/_models/reading-list/cbl/cbl-import-result.enum';
|
||||
import { CblImportSummary } from 'src/app/_models/reading-list/cbl/cbl-import-summary';
|
||||
import { ReadingListService } from 'src/app/_services/reading-list.service';
|
||||
import { TimelineStep } from '../../_components/step-tracker/step-tracker.component';
|
||||
|
||||
enum Step {
|
||||
Import = 0,
|
||||
|
@ -35,59 +39,103 @@ export class ImportCblModalComponent {
|
|||
importSummaries: Array<CblImportSummary> = [];
|
||||
validateSummary: CblImportSummary | undefined;
|
||||
dryRunSummary: CblImportSummary | undefined;
|
||||
dryRunResults: Array<CblBookResult> = [];
|
||||
finalizeSummary: CblImportSummary | undefined;
|
||||
finalizeResults: Array<CblBookResult> = [];
|
||||
|
||||
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},
|
||||
isLoading: boolean = false;
|
||||
|
||||
steps: Array<TimelineStep> = [
|
||||
{title: 'Import CBL', index: Step.Import, active: true, icon: 'fa-solid fa-file-arrow-up'},
|
||||
{title: 'Validate File', index: Step.Validate, active: false, icon: 'fa-solid fa-spell-check'},
|
||||
{title: 'Dry Run', index: Step.DryRun, active: false, icon: 'fa-solid fa-gears'},
|
||||
{title: 'Final Import', index: Step.Finalize, active: false, icon: 'fa-solid fa-floppy-disk'},
|
||||
];
|
||||
currentStep = this.steps[0];
|
||||
currentStepIndex = this.steps[0].index;
|
||||
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
get Step() { return Step; }
|
||||
|
||||
get NextButtonLabel() {
|
||||
switch(this.currentStepIndex) {
|
||||
case Step.DryRun:
|
||||
return 'Import';
|
||||
case Step.Finalize:
|
||||
return 'Restart'
|
||||
default:
|
||||
return 'Next';
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService,
|
||||
public utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {}
|
||||
public utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef,
|
||||
private toastr: ToastrService) {}
|
||||
|
||||
close() {
|
||||
this.ngModal.close();
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.currentStepIndex === Step.Import && !this.isFileSelected()) return;
|
||||
if (this.currentStepIndex === Step.Validate && this.validateSummary && this.validateSummary.results.length > 0) return;
|
||||
|
||||
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) {
|
||||
this.isLoading = true;
|
||||
switch (this.currentStepIndex) {
|
||||
case Step.Import:
|
||||
this.importFile();
|
||||
break;
|
||||
case Step.Validate:
|
||||
this.import(true);
|
||||
break;
|
||||
case Step.DryRun:
|
||||
this.import(false);
|
||||
break;
|
||||
case Step.Finalize:
|
||||
// Clear the models and allow user to do another import
|
||||
this.uploadForm.get('files')?.setValue(undefined);
|
||||
this.currentStepIndex = Step.Import;
|
||||
this.validateSummary = undefined;
|
||||
this.dryRunSummary = undefined;
|
||||
this.dryRunResults = [];
|
||||
this.finalizeSummary = undefined;
|
||||
this.finalizeResults = [];
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
prevStep() {
|
||||
if (this.currentStepIndex === Step.Import) return;
|
||||
this.currentStepIndex--;
|
||||
}
|
||||
|
||||
canMoveToNextStep() {
|
||||
switch (this.currentStep.index) {
|
||||
switch (this.currentStepIndex) {
|
||||
case Step.Import:
|
||||
return this.isFileSelected();
|
||||
case Step.Validate:
|
||||
return this.validateSummary && this.validateSummary.results.length > 0;
|
||||
return this.validateSummary && this.validateSummary.results.length === 0;
|
||||
case Step.DryRun:
|
||||
return true;
|
||||
return this.dryRunSummary?.success != CblImportResult.Fail;
|
||||
case Step.Finalize:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
canMoveToPrevStep() {
|
||||
switch (this.currentStepIndex) {
|
||||
case Step.Import:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
isFileSelected() {
|
||||
const files = this.uploadForm.get('files')?.value;
|
||||
if (files) return files.length > 0;
|
||||
|
@ -98,42 +146,42 @@ export class ImportCblModalComponent {
|
|||
const files = this.uploadForm.get('files')?.value;
|
||||
if (!files) return;
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
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.readingListService.validateCbl(formData).subscribe(res => {
|
||||
if (this.currentStepIndex === Step.Import) {
|
||||
this.validateSummary = res;
|
||||
}
|
||||
if (this.currentStep.index === Step.DryRun) {
|
||||
this.dryRunSummary = res;
|
||||
}
|
||||
this.importSummaries.push(res);
|
||||
this.currentStep.index++;
|
||||
this.currentStepIndex++;
|
||||
this.isLoading = false;
|
||||
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;
|
||||
import(dryRun: boolean = false) {
|
||||
const files = this.uploadForm.get('files')?.value;
|
||||
if (!files) return;
|
||||
|
||||
// const file = (event.target as HTMLInputElement).files![0];
|
||||
const formData = new FormData();
|
||||
formData.append('cbl', files[0]);
|
||||
formData.append('dryRun', dryRun + '');
|
||||
this.readingListService.importCbl(formData).subscribe(res => {
|
||||
// Our step when calling is always one behind
|
||||
if (dryRun) {
|
||||
this.dryRunSummary = res;
|
||||
this.dryRunResults = [...res.successfulInserts, ...res.results].sort((a, b) => a.order - b.order);
|
||||
} else {
|
||||
this.finalizeSummary = res;
|
||||
this.finalizeResults = [...res.successfulInserts, ...res.results].sort((a, b) => a.order - b.order);
|
||||
this.toastr.success('Reading List imported');
|
||||
}
|
||||
|
||||
// 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 = '';
|
||||
// }
|
||||
// }
|
||||
this.isLoading = false;
|
||||
this.currentStepIndex++;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,31 +2,34 @@ 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';
|
||||
|
||||
const failIcon = '<i aria-hidden="true" class="reading-list-fail--item fa-solid fa-circle-xmark me-1"></i>';
|
||||
const successIcon = '<i aria-hidden="true" class="reading-list-success--item fa-solid fa-circle-check me-1"></i>';
|
||||
|
||||
@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.';
|
||||
return failIcon + '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.';
|
||||
return failIcon + result.series + ': ' + '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.';
|
||||
return failIcon + '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.';
|
||||
return failIcon + '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.';
|
||||
return failIcon + '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.';
|
||||
return failIcon + '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.';
|
||||
return failIcon + result.series + ': ' + '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.';
|
||||
return failIcon + 'All chapters cannot be matched to Chapters in Kavita.';
|
||||
case CblImportReason.Success:
|
||||
return successIcon + result.series + ' volume ' + result.volume + ' number ' + result.number + ' mapped successfully';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
19
UI/Web/src/app/reading-list/_pipes/cbl-import-result.pipe.ts
Normal file
19
UI/Web/src/app/reading-list/_pipes/cbl-import-result.pipe.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { CblImportResult } from 'src/app/_models/reading-list/cbl/cbl-import-result.enum';
|
||||
|
||||
@Pipe({
|
||||
name: 'cblImportResult'
|
||||
})
|
||||
export class CblImportResultPipe implements PipeTransform {
|
||||
|
||||
transform(result: CblImportResult): string {
|
||||
switch (result) {
|
||||
case CblImportResult.Success:
|
||||
return 'Success';
|
||||
case CblImportResult.Partial:
|
||||
return 'Partial Success';
|
||||
case CblImportResult.Fail:
|
||||
return 'Failure';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,8 @@ import { ReadingListsComponent } from './_components/reading-lists/reading-lists
|
|||
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';
|
||||
|
||||
import { StepTrackerComponent } from './_components/step-tracker/step-tracker.component';
|
||||
import { CblImportResultPipe } from './_pipes/cbl-import-result.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -28,6 +29,8 @@ import { CblConflictReasonPipe } from './_pipes/cbl-conflict-reason.pipe';
|
|||
ReadingListItemComponent,
|
||||
ImportCblModalComponent,
|
||||
CblConflictReasonPipe,
|
||||
StepTrackerComponent,
|
||||
CblImportResultPipe,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
|
|
@ -10,7 +10,11 @@
|
|||
<app-side-nav-item icon="fa-home" title="Home" link="/libraries/"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-star" title="Want To Read" link="/want-to-read/"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-list" title="Collections" link="/collections/"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-list-ol" title="Reading Lists" link="/lists/"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-list-ol" title="Reading Lists" link="/lists/">
|
||||
<ng-container actions>
|
||||
<app-card-actionables [actions]="readingListActions" labelBy="Reading Lists" iconClass="fa-ellipsis-v" (actionHandler)="importCbl()"></app-card-actionables>
|
||||
</ng-container>
|
||||
</app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-bookmark" title="Bookmarks" link="/bookmarks/"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-regular fa-rectangle-list" title="All Series" link="/all-series/" *ngIf="libraries.length > 0"></app-side-nav-item>
|
||||
<div class="mb-2 mt-3 ms-2 me-2" *ngIf="libraries.length > 10 && (navService?.sideNavCollapsed$ | async) === false">
|
||||
|
|
|
@ -3,6 +3,8 @@ import { NavigationEnd, Router } from '@angular/router';
|
|||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, map, shareReplay, take, takeUntil } from 'rxjs/operators';
|
||||
import { ImportCblModalComponent } from 'src/app/reading-list/_modals/import-cbl-modal/import-cbl-modal.component';
|
||||
import { ReadingList } from 'src/app/_models/reading-list';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
import { Breakpoint, UtilityService } from '../../../shared/_services/utility.service';
|
||||
|
@ -23,6 +25,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||
|
||||
libraries: Library[] = [];
|
||||
actions: ActionItem<Library>[] = [];
|
||||
readingListActions = [{action: Action.Import, title: 'Import CBL', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
|
||||
|
||||
filterQuery: string = '';
|
||||
filterLibrary = (library: Library) => {
|
||||
|
@ -36,7 +39,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||
public utilityService: UtilityService, private messageHub: MessageHubService,
|
||||
private actionFactoryService: ActionFactoryService, private actionService: ActionService,
|
||||
public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef,
|
||||
private modalService: NgbModal, private imageService: ImageService) {
|
||||
private ngbModal: NgbModal, private imageService: ImageService) {
|
||||
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
|
@ -97,6 +100,9 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
importCbl() {
|
||||
const ref = this.ngbModal.open(ImportCblModalComponent, {size: 'xl'});
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Library>, library: Library) {
|
||||
if (typeof action.callback === 'function') {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue