Drawers, Estimated Reading Time, Korean Parsing Support (#1297)

* Started building out idea around detail drawer. Need code from word count to continue

* Fixed the logic for caluclating time to read on comics

* Adding styles

* more styling fixes

* Cleaned up the styles a bit more so it's at least functional. Not sure on the feature, might abandon.

* Pulled Robbie's changes in and partially migrated them to the drawer.

* Add offset overrides for offcanvas so it takes our header into account

* Implemented a basic time left to finish the series (or at least what's in Kavita). Rough around the edges.

* Cleaned up the drawer code.

* Added Quick Catch ups to recommended page. Updated the timeout for scan tasks to ensure we don't run 2 at the same time.

* Quick catchups implemented

* Added preliminary support for Korean filename parsing. Reduced an array alloc that is called many thousands of times per scan.

* Fixing drawer overflow

* Fixed a calculation bug with average reading time.

* Small spacing changes to drawer

* Don't show estimated reading time if the user hasn't read anything

* Bump eventsource from 1.1.1 to 2.0.2 in /UI/Web

Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.1 to 2.0.2.
- [Release notes](https://github.com/EventSource/eventsource/releases)
- [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md)
- [Commits](https://github.com/EventSource/eventsource/compare/v1.1.1...v2.0.2)

---
updated-dependencies:
- dependency-name: eventsource
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Added image to series detail drawer

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Joseph Milazzo 2022-05-27 09:08:54 -05:00 committed by GitHub
parent d796bcdc0a
commit 63475722ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 883 additions and 144 deletions

View file

@ -61,28 +61,6 @@
<div *ngIf="seriesMetadata" class="mt-2">
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [readingLists]="readingLists" [series]="series"></app-series-metadata-detail>
</div>
<!-- <ng-container>
<div class="row g-0">
<div class="col-2">
<i class="fa-regular fa-file-lines" aria-hidden="true"></i>
{{series.pages}} Pages
</div>
|
<div class="col-2">
<i class="fa-regular fa-clock" aria-hidden="true"></i>
1-2 Hours to Read
</div>
<ng-container *ngIf="utilityService.mangaFormat(series.format) === 'EPUB'">
|
<div class="col-2">
<i class="fa-regular fa-book-open" aria-hidden="true"></i>
10K Total Words
</div>
</ng-container>
</div>
</ng-container> -->
</div>
</div>

View file

@ -1,7 +1,7 @@
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap';
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { forkJoin, Subject } from 'rxjs';
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
@ -35,6 +35,7 @@ import { SeriesService } from '../_services/series.service';
import { NavService } from '../_services/nav.service';
import { RelatedSeries } from '../_models/series-detail/related-series';
import { RelationKind } from '../_models/series-detail/relation-kind';
import { CardDetailDrawerComponent } from '../cards/card-detail-drawer/card-detail-drawer.component';
interface RelatedSeris {
series: Series;
@ -196,7 +197,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
private confirmService: ConfirmService, private titleService: Title,
private downloadService: DownloadService, private actionService: ActionService,
public imageSerivce: ImageService, private messageHub: MessageHubService,
private readingListService: ReadingListService, public navService: NavService
private readingListService: ReadingListService, public navService: NavService,
private offcanvasService: NgbOffcanvas
) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
@ -560,12 +562,12 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
}
openViewInfo(data: Volume | Chapter) {
const modalRef = this.modalService.open(CardDetailsModalComponent, { size: 'lg' });
modalRef.componentInstance.data = data;
modalRef.componentInstance.parentName = this.series?.name;
modalRef.componentInstance.seriesId = this.series?.id;
modalRef.componentInstance.libraryId = this.series?.libraryId;
modalRef.closed.subscribe((result: {coverImageUpdate: boolean}) => {
const drawerRef = this.offcanvasService.open(CardDetailDrawerComponent, {position: 'bottom'});
drawerRef.componentInstance.data = data;
drawerRef.componentInstance.parentName = this.series?.name;
drawerRef.componentInstance.seriesId = this.series?.id;
drawerRef.componentInstance.libraryId = this.series?.libraryId;
drawerRef.closed.subscribe((result: {coverImageUpdate: boolean}) => {
if (result.coverImageUpdate) {
this.coverImageOffset += 1;
}

View file

@ -5,83 +5,100 @@
<!-- This first row will have random information about the series-->
<div class="row g-0 mb-4 mt-3">
<ng-container *ngIf="seriesMetadata.ageRating">
<div class="col-auto">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-3">
<app-icon-and-title [clickable]="true" fontClasses="fas fa-eye" (click)="goTo(FilterQueryParam.AgeRating, seriesMetadata.ageRating)" title="Age Rating">
{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}
</app-icon-and-title>
</div>
<div class="vr m-2"></div>
<div class="vr d-none d-lg-block m-2"></div>
</ng-container>
<ng-container *ngIf="series">
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
<div class="col-auto mb-2">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-3">
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release Year">
{{seriesMetadata.releaseYear}}
</app-icon-and-title>
</div>
<div class="vr m-2"></div>
<div class="vr d-none d-lg-block m-2"></div>
</ng-container>
<ng-container *ngIf="seriesMetadata.language !== null">
<div class="col-auto mb-2">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-3">
<app-icon-and-title [clickable]="true" fontClasses="fas fa-language" (click)="goTo(FilterQueryParam.Languages, seriesMetadata.language)" title="Language">
{{seriesMetadata.language | defaultValue:'en' | languageName | async}}
</app-icon-and-title>
</div>
<div class="vr m-2"></div>
<div class="vr d-none d-lg-block m-2"></div>
</ng-container>
<ng-container>
<div class="col-auto mb-2">
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [clickable]="true" fontClasses="fa-solid fa-hourglass-empty" (click)="goTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)" title="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})">
{{seriesMetadata.publicationStatus | publicationStatus}}
</app-icon-and-title>
</div>
<div class="vr m-2 mb-2"></div>
<div class="vr m-2 d-none d-lg-block"></div>
</ng-container>
<ng-container>
<div class="col-auto mb-2">
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [clickable]="true" [fontClasses]="'fa ' + utilityService.mangaFormatIcon(series.format)" (click)="goTo(FilterQueryParam.Format, series.format)" title="Format">
{{utilityService.mangaFormat(series.format)}}
</app-icon-and-title>
</div>
<div class="vr m-2"></div>
<div class="vr d-none d-lg-block m-2"></div>
</ng-container>
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
<div class="col-auto mb-2">
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-clock" title="Last Read">
{{series.latestReadDate | date:'shortDate'}}
</app-icon-and-title>
</div>
<div class="vr m-2"></div>
<div class="vr d-none d-lg-block m-2"></div>
</ng-container>
<div class="col-auto mb-2">
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-file-lines">
{{series.pages}} Pages
</app-icon-and-title>
</div>
<div class="vr m-2"></div>
<ng-container *ngIf="series.format === MangaFormat.EPUB; else showPages">
<ng-container *ngIf="series.wordCount > 0">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [clickable]="false" fontClasses="fa-solid fa-book-open">
{{series.wordCount | compactNumber}} Words
</app-icon-and-title>
</div>
</ng-container>
</ng-container>
<ng-template #showPages>
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-file-lines">
{{series.pages}} Pages
</app-icon-and-title>
</div>
</ng-template>
<div class="vr d-none d-lg-block m-2"></div>
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
<div class="col-auto mb-2">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-clock">
{{minHoursToRead}}{{maxHoursToRead !== minHoursToRead ? ('-' + maxHoursToRead) : ''}} Hour{{minHoursToRead > 1 ? 's' : ''}}
</app-icon-and-title>
</div>
</ng-container>
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0">
<div class="vr m-2"></div>
<div class="col-auto mb-2">
<app-icon-and-title [clickable]="false" fontClasses="fa-solid fa-book-open">
{{series.wordCount | compactNumber}} Words
<ng-container *ngIf="readingTimeLeft.hasProgress && readingTimeLeft.minHours !== 1 && readingTimeLeft.maxHours !== 1 && readingTimeLeft.avgHours !== 0">
<div class="vr d-none d-lg-block m-2"></div>
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [clickable]="false" fontClasses="fa-solid fa-clock">
~{{readingTimeLeft.avgHours}} Hour{{readingTimeLeft.avgHours > 1 ? 's' : ''}} Left
</app-icon-and-title>
</div>
</div>
</ng-container>
</ng-container>
</div>

View file

@ -1,5 +1,7 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router';
import { HourEstimateRange } from 'src/app/_models/hour-estimate-range';
import { MAX_WORDS_PER_HOUR, MIN_WORDS_PER_HOUR, MIN_PAGES_PER_MINUTE, MAX_PAGES_PER_MINUTE, ReaderService } from 'src/app/_services/reader.service';
import { TagBadgeCursor } from '../../shared/tag-badge/tag-badge.component';
import { FilterQueryParam } from '../../shared/_services/filter-utilities.service';
import { UtilityService } from '../../shared/_services/utility.service';
@ -9,11 +11,6 @@ import { Series } from '../../_models/series';
import { SeriesMetadata } from '../../_models/series-metadata';
import { MetadataService } from '../../_services/metadata.service';
const MAX_WORDS_PER_HOUR = 30_000;
const MIN_WORDS_PER_HOUR = 10_260;
const MAX_PAGES_PER_MINUTE = 2.75;
const MIN_PAGES_PER_MINUTE = 3.33;
@Component({
selector: 'app-series-metadata-detail',
@ -34,6 +31,7 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
minHoursToRead: number = 1;
maxHoursToRead: number = 1;
readingTimeLeft: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1, hasProgress: false};
/**
* Html representation of Series Summary
@ -52,7 +50,9 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
return FilterQueryParam;
}
constructor(public utilityService: UtilityService, public metadataService: MetadataService, private router: Router) { }
constructor(public utilityService: UtilityService, public metadataService: MetadataService, private router: Router, public readerService: ReaderService) {
}
ngOnChanges(changes: SimpleChanges): void {
this.hasExtendedProperites = this.seriesMetadata.colorists.length > 0 ||
@ -67,17 +67,17 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
if (this.seriesMetadata !== null) {
this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
}
if (this.series !== null) {
this.readerService.getTimeLeft(this.series.id).subscribe((timeLeft) => this.readingTimeLeft = timeLeft);
if (this.series.format === MangaFormat.EPUB && this.series.wordCount > 0) {
this.minHoursToRead = parseInt(Math.round(this.series.wordCount / MAX_WORDS_PER_HOUR) + '', 10);
this.maxHoursToRead = parseInt(Math.round(this.series.wordCount / MIN_WORDS_PER_HOUR) + '', 10);
this.minHoursToRead = parseInt(Math.round(this.series.wordCount / MAX_WORDS_PER_HOUR) + '', 10) || 1;
this.maxHoursToRead = parseInt(Math.round(this.series.wordCount / MIN_WORDS_PER_HOUR) + '', 10) || 1;
} else if (this.series.format !== MangaFormat.EPUB) {
this.minHoursToRead = parseInt(Math.round((this.series.pages / MIN_PAGES_PER_MINUTE) / 60) + '', 10);
this.maxHoursToRead = parseInt(Math.round((this.series.pages / MAX_PAGES_PER_MINUTE) / 60) + '', 10);
this.minHoursToRead = parseInt(Math.round((this.series.pages / MIN_PAGES_PER_MINUTE) / 60) + '', 10) || 1;
this.maxHoursToRead = parseInt(Math.round((this.series.pages / MAX_PAGES_PER_MINUTE) / 60) + '', 10) || 1;
}
}
}