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:
parent
0a70ac35dc
commit
c1490d6e86
48 changed files with 2354 additions and 408 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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()"> <i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}}" aria-controls="extended-series-metadata"></i> 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>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue