mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-08-30 23:00:06 -04:00
Estimated time is coded up.
This commit is contained in:
parent
ab6669703d
commit
64ee5ee459
@ -10,4 +10,8 @@ namespace API.Controllers;
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class BaseApiController : ControllerBase
|
public class BaseApiController : ControllerBase
|
||||||
{
|
{
|
||||||
|
public BaseApiController()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,13 +41,12 @@ public class BookController : BaseApiController
|
|||||||
/// <param name="chapterId"></param>
|
/// <param name="chapterId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("{chapterId}/book-info")]
|
[HttpGet("{chapterId}/book-info")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "includeWordCounts"])]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId"])]
|
||||||
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId, bool includeWordCounts = false)
|
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
||||||
{
|
{
|
||||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||||
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||||
var bookTitle = string.Empty;
|
var bookTitle = string.Empty;
|
||||||
IDictionary<int, int>? pageWordCounts = null;
|
|
||||||
|
|
||||||
switch (dto.SeriesFormat)
|
switch (dto.SeriesFormat)
|
||||||
{
|
{
|
||||||
@ -57,11 +56,6 @@ public class BookController : BaseApiController
|
|||||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.LenientBookReaderOptions);
|
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.LenientBookReaderOptions);
|
||||||
bookTitle = book.Title;
|
bookTitle = book.Title;
|
||||||
|
|
||||||
if (includeWordCounts)
|
|
||||||
{
|
|
||||||
// TODO: Cache this in temp/chapterId folder to avoid having to process file each time
|
|
||||||
pageWordCounts = await _bookService.GetWordCountsPerPage(mangaFile.FilePath);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MangaFormat.Pdf:
|
case MangaFormat.Pdf:
|
||||||
@ -94,7 +88,6 @@ public class BookController : BaseApiController
|
|||||||
LibraryId = dto.LibraryId,
|
LibraryId = dto.LibraryId,
|
||||||
IsSpecial = dto.IsSpecial,
|
IsSpecial = dto.IsSpecial,
|
||||||
Pages = dto.Pages,
|
Pages = dto.Pages,
|
||||||
PageWordCounts = pageWordCounts
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ using API.Entities.Enums;
|
|||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
|
using API.Services.Tasks.Metadata;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
@ -222,7 +223,6 @@ public class ReaderController : BaseApiController
|
|||||||
/// <param name="chapterId"></param>
|
/// <param name="chapterId"></param>
|
||||||
/// <param name="extractPdf">Should Kavita extract pdf into images. Defaults to false.</param>
|
/// <param name="extractPdf">Should Kavita extract pdf into images. Defaults to false.</param>
|
||||||
/// <param name="includeDimensions">Include file dimensions. Only useful for image-based reading</param>
|
/// <param name="includeDimensions">Include file dimensions. Only useful for image-based reading</param>
|
||||||
/// <param name="includeWordCounts">Include epub word counts per page. Only useful for epub-based reading</param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("chapter-info")]
|
[HttpGet("chapter-info")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions"])]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions"])]
|
||||||
@ -849,10 +849,18 @@ public class ReaderController : BaseApiController
|
|||||||
// Patch in the reading progress
|
// Patch in the reading progress
|
||||||
await _unitOfWork.ChapterRepository.AddChapterModifiers(User.GetUserId(), chapter);
|
await _unitOfWork.ChapterRepository.AddChapterModifiers(User.GetUserId(), chapter);
|
||||||
|
|
||||||
// TODO: We need to actually use word count from the pages
|
|
||||||
if (series.Format == MangaFormat.Epub)
|
if (series.Format == MangaFormat.Epub)
|
||||||
{
|
{
|
||||||
var progressCount = chapter.WordCount;
|
// Get the word counts for all the pages
|
||||||
|
var pageCounts = await _bookService.GetWordCountsPerPage(chapter.Files.First().FilePath); // TODO: Cache
|
||||||
|
if (pageCounts == null) return _readerService.GetTimeEstimate(series.WordCount, 0, true);
|
||||||
|
|
||||||
|
// Sum character counts only for pages that have been read
|
||||||
|
var totalCharactersRead = pageCounts
|
||||||
|
.Where(kvp => kvp.Key <= chapter.PagesRead)
|
||||||
|
.Sum(kvp => kvp.Value);
|
||||||
|
|
||||||
|
var progressCount = WordCountAnalyzerService.GetWordCount(totalCharactersRead);
|
||||||
var wordsLeft = series.WordCount - progressCount;
|
var wordsLeft = series.WordCount - progressCount;
|
||||||
return _readerService.GetTimeEstimate(wordsLeft, 0, true);
|
return _readerService.GetTimeEstimate(wordsLeft, 0, true);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Enums;
|
|
||||||
|
|
||||||
namespace API.DTOs.Reader;
|
namespace API.DTOs.Reader;
|
||||||
|
|
||||||
@ -16,9 +15,4 @@ public sealed record BookInfoDto : IChapterInfoDto
|
|||||||
public int Pages { get; set; }
|
public int Pages { get; set; }
|
||||||
public bool IsSpecial { get; set; }
|
public bool IsSpecial { get; set; }
|
||||||
public string ChapterTitle { get; set; } = default! ;
|
public string ChapterTitle { get; set; } = default! ;
|
||||||
/// <summary>
|
|
||||||
/// For Epub reader, this will contain Page number -> word count. All other times will be null.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>This is optionally returned by includeWordCounts</remarks>
|
|
||||||
public IDictionary<int, int>? PageWordCounts { get; set; }
|
|
||||||
}
|
}
|
||||||
|
@ -247,7 +247,6 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||||||
_unitOfWork.MangaFileRepository.Update(file);
|
_unitOfWork.MangaFileRepository.Update(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<int> GetWordCountFromHtml(EpubLocalTextContentFileRef bookFile, string filePath)
|
private async Task<int> GetWordCountFromHtml(EpubLocalTextContentFileRef bookFile, string filePath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -256,7 +255,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||||||
doc.LoadHtml(await bookFile.ReadContentAsync());
|
doc.LoadHtml(await bookFile.ReadContentAsync());
|
||||||
|
|
||||||
var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]");
|
var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]");
|
||||||
return textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) / AverageCharactersPerWord ?? 0;
|
var characterCount = textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) ?? 0;
|
||||||
|
return GetWordCount(characterCount);
|
||||||
}
|
}
|
||||||
catch (EpubContentException ex)
|
catch (EpubContentException ex)
|
||||||
{
|
{
|
||||||
@ -267,4 +267,10 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int GetWordCount(int characterCount)
|
||||||
|
{
|
||||||
|
if (characterCount == 0) return 0;
|
||||||
|
return characterCount / AverageCharactersPerWord;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core';
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
import {TranslocoService} from "@jsverse/transloco";
|
import {TranslocoService} from "@jsverse/transloco";
|
||||||
import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range";
|
import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range";
|
||||||
import {DecimalPipe} from "@angular/common";
|
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'readTimeLeft',
|
name: 'readTimeLeft',
|
||||||
@ -11,10 +10,10 @@ export class ReadTimeLeftPipe implements PipeTransform {
|
|||||||
|
|
||||||
constructor(private readonly translocoService: TranslocoService) {}
|
constructor(private readonly translocoService: TranslocoService) {}
|
||||||
|
|
||||||
transform(readingTimeLeft: HourEstimateRange): string {
|
transform(readingTimeLeft: HourEstimateRange, includeLeftLabel = false): string {
|
||||||
const hoursLabel = readingTimeLeft.avgHours > 1
|
const hoursLabel = readingTimeLeft.avgHours > 1
|
||||||
? this.translocoService.translate('read-time-pipe.hours')
|
? this.translocoService.translate(`read-time-pipe.hours${includeLeftLabel ? '-left' : ''}`)
|
||||||
: this.translocoService.translate('read-time-pipe.hour');
|
: this.translocoService.translate(`read-time-pipe.hour${includeLeftLabel ? '-left' : ''}`);
|
||||||
|
|
||||||
const formattedHours = this.customRound(readingTimeLeft.avgHours);
|
const formattedHours = this.customRound(readingTimeLeft.avgHours);
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<div class="book-title col-2 d-none d-sm-block">
|
<div class="book-title col-3 d-none d-sm-block">
|
||||||
@if(isLoading) {
|
@if(isLoading) {
|
||||||
<!--Just render a blank div here-->
|
<!--Just render a blank div here-->
|
||||||
} @else {
|
} @else {
|
||||||
@ -139,9 +139,9 @@
|
|||||||
@let timeLeft = readingTimeLeftResource.value();
|
@let timeLeft = readingTimeLeftResource.value();
|
||||||
@if (timeLeft) {
|
@if (timeLeft) {
|
||||||
,
|
,
|
||||||
<span class="time-left" [ngbTooltip]="t('time-left-alt')">
|
<span class="time-left">
|
||||||
<i class="fa-solid fa-clock" aria-hidden="true"></i>
|
<i class="fa-solid fa-clock" aria-hidden="true"></i>
|
||||||
{{timeLeft! | readTimeLeft }}
|
{{timeLeft! | readTimeLeft:true }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,12 +246,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
nextPageDisabled = false;
|
nextPageDisabled = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal property used to capture all the different css properties to render on all elements. This is a cached version that is updated from reader-settings component
|
|
||||||
*/
|
|
||||||
//pageStyles = model<PageStyle>(this.readerSettingsService.getDefaultPageStyles());
|
|
||||||
//pageStyles!: PageStyle;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Offset for drawer and rendering canvas. Fixed to 62px.
|
* Offset for drawer and rendering canvas. Fixed to 62px.
|
||||||
*/
|
*/
|
||||||
@ -270,7 +264,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
request: () => ({
|
request: () => ({
|
||||||
chapterId: this.chapterId,
|
chapterId: this.chapterId,
|
||||||
seriesId: this.seriesId,
|
seriesId: this.seriesId,
|
||||||
pageNumber: this.pageNum()
|
pageNumber: this.pageNum(),
|
||||||
}),
|
}),
|
||||||
loader: async ({ request }) => {
|
loader: async ({ request }) => {
|
||||||
return this.readerService.getTimeLeftForChapter(this.seriesId, this.chapterId).toPromise();
|
return this.readerService.getTimeLeftForChapter(this.seriesId, this.chapterId).toPromise();
|
||||||
@ -422,15 +416,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// get VerticalBookContentWidth() {
|
|
||||||
// if (this.layoutMode() !== BookPageLayoutMode.Default && this.writingStyle() !== WritingStyle.Horizontal ) {
|
|
||||||
// const width = this.getVerticalPageWidth()
|
|
||||||
// return width + 'px';
|
|
||||||
// }
|
|
||||||
// return '';
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
get PageWidthForPagination() {
|
get PageWidthForPagination() {
|
||||||
if (this.layoutMode() === BookPageLayoutMode.Default && this.writingStyle() === WritingStyle.Vertical && this.horizontalScrollbarNeeded) {
|
if (this.layoutMode() === BookPageLayoutMode.Default && this.writingStyle() === WritingStyle.Vertical && this.horizontalScrollbarNeeded) {
|
||||||
return 'unset';
|
return 'unset';
|
||||||
|
@ -6,8 +6,4 @@ export interface BookInfo {
|
|||||||
seriesId: number;
|
seriesId: number;
|
||||||
libraryId: number;
|
libraryId: number;
|
||||||
volumeId: number;
|
volumeId: number;
|
||||||
/**
|
|
||||||
* Maps the page number to character count. Only available on epub reader.
|
|
||||||
*/
|
|
||||||
pageWordCounts: {[key: number]: number};
|
|
||||||
}
|
}
|
||||||
|
@ -2785,7 +2785,9 @@
|
|||||||
"read-time-pipe": {
|
"read-time-pipe": {
|
||||||
"less-than-hour": "<1 Hour",
|
"less-than-hour": "<1 Hour",
|
||||||
"hour": "Hour",
|
"hour": "Hour",
|
||||||
"hours": "Hours"
|
"hours": "Hours",
|
||||||
|
"hour-left": "Hour left",
|
||||||
|
"hours-left": "Hours left"
|
||||||
},
|
},
|
||||||
|
|
||||||
"metadata-setting-field-pipe": {
|
"metadata-setting-field-pipe": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user