Word Count (#1286)

* Adding some code for Robbie

* See more on series detail metadata area is now at the bottom on the section

* Cleaned up subtitle headings to use a single class for offset with actionables

* Added some markup for the new design, waiting for Robbie to finish it off

* styling age-rating badge

* Started hooking up basic analyze file service and hooks in the UI. Basic code to implement the count is implemented and in benchmarks.

* Hooked up analyze ui to backend

* Refactored Series Detail metadata area to use a new icon/title design

* Cleaned up the new design

* Pushing for robbie to do css

* Massive performance improvement to scan series where we only need to scan folders reported that have series in them, rather than the whole library.

* Removed theme page as we no longer need it. Added WordCount to DTOs so the UI can show them. Added new pipe to format numbers in compact mode.

* Hooked up actual reading time based on user's words per hour

* Refactor some magic numbers to consts

* Hooked in progress reporting for series word count

* Hooked up analyze files

* Re-implemented time to read on comics

* Removed the word Last Read

* Show proper language name instead of iso tag on series detail page. Added some error handling on word count code.

* Reworked error handling

* Fixed some security vulnerabilities in npm.

* Handle a case where there are no text nodes and instead of returning an empty list, htmlagilitypack returns null.

* Tweaked the styles a bit on the icon-and-title

* Code cleanup

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joseph Milazzo 2022-05-25 16:53:39 -05:00 committed by GitHub
parent 0a70ac35dc
commit c1490d6e86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 2354 additions and 408 deletions

View file

@ -6,7 +6,7 @@
</h2>
</ng-container>
<ng-container subtitle *ngIf="series?.localizedName !== series?.name">
<h6 style="margin-left:40px;" title="Localized Name">{{series?.localizedName}}</h6>
<h6 class="subtitle-with-actionables" title="Localized Name">{{series?.localizedName}}</h6>
</ng-container>
</app-side-nav-companion-bar>
<div class="container-fluid pt-2" *ngIf="series !== undefined">
@ -62,6 +62,26 @@
<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

@ -290,6 +290,9 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
case(Action.AddToCollection):
this.actionService.addMultipleSeriesToCollectionTag([series], () => this.actionInProgress = false);
break;
case (Action.AnalyzeFiles):
this.actionService.analyzeFilesForSeries(series, () => this.actionInProgress = false);
break;
default:
break;
}
@ -372,12 +375,10 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))
.filter(action => action.action !== Action.Edit)
.filter(action => this.actionFactoryService.filterBookmarksForFormat(action, this.series));
.filter(action => action.action !== Action.Edit);
this.volumeActions = this.actionFactoryService.getVolumeActions(this.handleVolumeActionCallback.bind(this));
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
// TODO: Move this to a forkJoin?
this.seriesService.getRelatedForSeries(this.seriesId).subscribe((relations: RelatedSeries) => {
this.relations = [
...relations.prequels.map(item => this.createRelatedSeries(item, RelationKind.Prequel)),

View file

@ -3,22 +3,85 @@
</div>
<!-- This first row will have random information about the series-->
<div class="row g-0 mb-2">
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating" a11y-click="13,32" class="clickable col-auto" (click)="goTo(FilterQueryParam.AgeRating, seriesMetadata.ageRating)" [selectionMode]="TagBadgeCursor.Clickable">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
<div class="row g-0 mb-4 mt-3">
<ng-container *ngIf="seriesMetadata.ageRating">
<div class="col-auto">
<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>
</ng-container>
<ng-container *ngIf="series">
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date" class="col-auto">{{seriesMetadata.releaseYear}}</app-tag-badge>
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Languages, seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
<app-tag-badge title="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})" [fillStyle]="seriesMetadata.maxCount != 0 && seriesMetadata.totalCount != 0 && seriesMetadata.maxCount >= seriesMetadata.totalCount ? 'filled' : 'outline'" a11y-click="13,32" class="col-auto"
(click)="goTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)"
[selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
<div class="col-auto mb-2">
<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>
</ng-container>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Format, series.format)" [selectionMode]="TagBadgeCursor.Clickable">
<app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format>
</app-tag-badge>
<app-tag-badge title="Last Read" class="col-auto" *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'" [selectionMode]="TagBadgeCursor.Selectable">
Last Read: {{series.latestReadDate | date:'shortDate'}}
</app-tag-badge>
<ng-container *ngIf="seriesMetadata.language !== null">
<div class="col-auto mb-2">
<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>
</ng-container>
<ng-container>
<div class="col-auto 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>
</ng-container>
<ng-container>
<div class="col-auto 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>
</ng-container>
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
<div class="col-auto 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>
</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 && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
<div class="col-auto mb-2">
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-clock">
{{minHoursToRead}}{{maxHoursToRead !== minHoursToRead ? ('-' + maxHoursToRead) : ''}} Hours
</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
</app-icon-and-title>
</div>
</ng-container>
</ng-container>
</div>
@ -92,11 +155,6 @@
</div>
</div>
<div class="row g-0">
<hr class="col mt-3" *ngIf="hasExtendedProperites" >
<a [class.hidden]="hasExtendedProperites" *ngIf="hasExtendedProperites" class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">&nbsp;<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}}" aria-controls="extended-series-metadata"></i>&nbsp;See {{isCollapsed ? 'More' : 'Less'}}</a>
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
<div class="row g-0 mt-1" *ngIf="seriesMetadata.coverArtists && seriesMetadata.coverArtists.length > 0">
<div class="col-md-4">
@ -213,4 +271,13 @@
</app-badge-expander>
</div>
</div>
</div>
<div class="row g-0">
<hr class="col mt-3" *ngIf="hasExtendedProperites" >
<a [class.hidden]="hasExtendedProperites" *ngIf="hasExtendedProperites"
class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
See {{isCollapsed ? 'More' : 'Less'}}
</a>
</div>

View file

@ -9,6 +9,12 @@ 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',
templateUrl: './series-metadata-detail.component.html',
@ -26,6 +32,9 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
isCollapsed: boolean = true;
hasExtendedProperites: boolean = false;
minHoursToRead: number = 1;
maxHoursToRead: number = 1;
/**
* Html representation of Series Summary
*/
@ -58,8 +67,19 @@ 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.series.wordCount > 0) {
if (this.series.format === MangaFormat.EPUB) {
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);
} else if (this.series.format === MangaFormat.IMAGE || this.series.format === MangaFormat.ARCHIVE) {
this.minHoursToRead = parseInt(Math.round((this.series.wordCount * MAX_PAGES_PER_MINUTE) / 60) + '', 10);
this.maxHoursToRead = parseInt(Math.round((this.series.wordCount * MIN_PAGES_PER_MINUTE) / 60) + '', 10);
}
}
}
ngOnInit(): void {