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
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue