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:
Joe Milazzo 2023-02-12 08:20:51 -08:00 committed by GitHub
parent ae1af22af1
commit 3f24dc7392
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 21951 additions and 170 deletions

View file

@ -0,0 +1,8 @@
import { CblImportReason } from "./cbl-import-reason.enum";
export interface CblBookResult {
series: string;
volume: string;
number: string;
reason: CblImportReason;
}

View file

@ -0,0 +1,10 @@
export enum CblImportReason {
ChapterMissing = 0,
VolumeMissing = 1,
SeriesMissing = 2,
NameConflict = 3,
AllSeriesMissing = 4,
EmptyFile = 5,
SeriesCollision = 6,
AllChapterMissing = 7,
}

View file

@ -0,0 +1,5 @@
export enum CblImportResult {
Fail = 0,
Partial = 1,
Success = 2
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
.file-input {
display: none;
}

View file

@ -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 = '';
// }
// }
}

View file

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

View file

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

View file

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

View file

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

View file

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