Reader Polish (#2465)

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>
This commit is contained in:
Joe Milazzo 2023-11-30 08:40:02 -06:00 committed by GitHub
parent 9fdaf5f99f
commit e489d2404a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 156 additions and 120 deletions

View File

@ -308,7 +308,7 @@ public class OpdsController : BaseApiController
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var (_, prefix) = await GetPrefix();
var filters = _unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(userId);
var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters"), $"{prefix}{apiKey}/smart-filters", apiKey, prefix);
@ -337,7 +337,7 @@ public class OpdsController : BaseApiController
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var (_, prefix) = await GetPrefix();
var externalSources = await _unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId);
var feed = CreateFeed(await _localizationService.Translate(userId, "external-sources"), $"{prefix}{apiKey}/external-sources", apiKey, prefix);
@ -370,15 +370,13 @@ public class OpdsController : BaseApiController
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId);
var feed = CreateFeed(await _localizationService.Translate(userId, "libraries"), $"{prefix}{apiKey}/libraries", apiKey, prefix);
SetFeedId(feed, "libraries");
// Ensure libraries follow SideNav order
var userSideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreams(userId, false);
foreach (var sideNavStream in userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library))
foreach (var library in userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library).Select(sideNavStream => sideNavStream.Library))
{
var library = sideNavStream.Library;
feed.Entries.Add(new FeedEntry()
{
Id = library!.Id.ToString(),
@ -779,13 +777,13 @@ public class OpdsController : BaseApiController
var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id)).OrderBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture),
_chapterSortComparer);
foreach (var chapter in chapters)
foreach (var chapterId in chapters.Select(c => c.Id))
{
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id);
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id);
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId);
foreach (var mangaFile in files)
{
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volume.Id, chapterId, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
}
}

View File

@ -105,18 +105,23 @@ public class ReaderController : BaseApiController
/// <param name="extractPdf">Should Kavita extract pdf into images. Defaults to false.</param>
/// <returns></returns>
[HttpGet("image")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId","page", "extractPdf", "apiKey"})]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "page", "extractPdf", "apiKey"})]
[AllowAnonymous]
public async Task<ActionResult> GetImage(int chapterId, int page, string apiKey, bool extractPdf = false)
{
if (page < 0) page = 0;
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
if (chapter == null) return NoContent();
try
{
if (new Random().Next(1, 10) > 5)
{
await Task.Delay(1000);
}
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
if (chapter == null) return NoContent();
_logger.LogInformation("Fetching Page {PageNum} on Chapter {ChapterId}", page, chapterId);
var path = _cacheService.GetCachedPagePath(chapter.Id, page);
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path))
return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page));
@ -245,8 +250,8 @@ public class ReaderController : BaseApiController
LibraryId = dto.LibraryId,
IsSpecial = dto.IsSpecial,
Pages = dto.Pages,
SeriesTotalPages = series?.Pages ?? 0,
SeriesTotalPagesRead = series?.PagesRead ?? 0,
SeriesTotalPages = series.Pages,
SeriesTotalPagesRead = series.PagesRead,
ChapterTitle = dto.ChapterTitle ?? string.Empty,
Subtitle = string.Empty,
Title = dto.SeriesName,

View File

@ -280,7 +280,7 @@ public class LibraryRepository : ILibraryRepository
{
Title = s,
IsoCode = s
};;
};
}
public IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)

View File

@ -644,6 +644,7 @@ public class DirectoryService : IDirectoryService
/// Scans a directory by utilizing a recursive folder search. If a .kavitaignore file is found, will ignore matching patterns
/// </summary>
/// <param name="folderPath"></param>
/// <param name="supportedExtensions"></param>
/// <param name="matcher"></param>
/// <returns></returns>
public IList<string> ScanFiles(string folderPath, string supportedExtensions, GlobMatcher? matcher = null)

View File

@ -398,7 +398,7 @@ public class ReaderService : IReaderService
// Handle Chapters within next Volume
// ! When selecting the chapter for the next volume, we need to make sure a c0 comes before a c1+
var chapters = volume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparer).ToList();
if (currentChapter.Number.Equals(Parser.DefaultChapter) && chapters.Last().Number.Equals(Parser.DefaultChapter))
if (currentChapter.Number.Equals(Parser.DefaultChapter) && chapters[^1].Number.Equals(Parser.DefaultChapter))
{
// We need to handle an extra check if the current chapter is the last special, as we should return -1
if (currentChapter.IsSpecial) return -1;

View File

@ -813,7 +813,7 @@ public class SeriesService : ISeriesService
private static double ExponentialSmoothing(IList<double> data, double alpha)
{
var forecast = data.First();
var forecast = data[0];
foreach (var value in data)
{

View File

@ -71,7 +71,7 @@ public class StreamService : IStreamService
var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId);
if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist"));
var stream = user?.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId);
var stream = user.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId);
if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use"));
var maxOrder = user!.DashboardStreams.Max(d => d.Order);
@ -159,7 +159,7 @@ public class StreamService : IStreamService
var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId);
if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist"));
var stream = user?.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId);
var stream = user.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId);
if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use"));
var maxOrder = user!.SideNavStreams.Max(d => d.Order);

View File

@ -72,7 +72,7 @@ public class TachiyomiService : ITachiyomiService
if (looseLeafChapterVolume == null)
{
var volumeChapter = _mapper.Map<ChapterDto>(volumes
.Last().Chapters
[^1].Chapters
.OrderBy(c => c.Number.AsFloat(), ChapterSortComparerZeroFirst.Default)
.Last());
if (volumeChapter.Number == "0")

View File

@ -179,7 +179,7 @@ public class LibraryWatcher : ILibraryWatcher
/// <param name="e"></param>
private void OnError(object sender, ErrorEventArgs e)
{
_logger.LogError(e.GetException(), "[LibraryWatcher] An error occured, likely too many changes occured at once or the folder being watched was deleted. Restarting Watchers");
_logger.LogError(e.GetException(), "[LibraryWatcher] An error occured, likely too many changes occured at once or the folder being watched was deleted. Restarting Watchers {Current}/{Total}", _bufferFullCounter, 3);
bool condition;
lock (Lock)
{

View File

@ -312,7 +312,6 @@ public class ParseScannedFiles
}
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", library.Name, ProgressEventType.Ended));
return;
async Task ProcessFolder(IList<string> files, string folder)
{

View File

@ -295,7 +295,7 @@ public class ProcessSeries : IProcessSeries
if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1)
{
series.Metadata.MaxCount = 1;
} else if (series.Metadata.TotalCount == 1 && chapters.Count == 1 && chapters.First().IsSpecial)
} else if (series.Metadata.TotalCount == 1 && chapters.Count == 1 && chapters[0].IsSpecial)
{
// If a series has a TotalCount of 1 and there is only a Special, mark it as Complete
series.Metadata.MaxCount = series.Metadata.TotalCount;

View File

@ -347,6 +347,7 @@ public class Startup
=>
{
opts.EnrichDiagnosticContext = LogEnricher.EnrichFromRequest;
opts.IncludeQueryInRequestPath = true;
});
app.Use(async (context, next) =>

View File

@ -17,7 +17,7 @@ export enum RelationKind {
Edition = 13
}
export const RelationKinds = [
const RelationKindsUnsorted = [
{text: 'Prequel', value: RelationKind.Prequel},
{text: 'Sequel', value: RelationKind.Sequel},
{text: 'Spin Off', value: RelationKind.SpinOff},
@ -31,3 +31,5 @@ export const RelationKinds = [
{text: 'Doujinshi', value: RelationKind.Doujinshi},
{text: 'Other', value: RelationKind.Other},
];
export const RelationKinds = RelationKindsUnsorted.slice().sort((a, b) => a.text.localeCompare(b.text));

View File

@ -38,6 +38,7 @@ export class BookLineOverlayComponent implements OnInit {
@Input({required: true}) pageNumber: number = 0;
@Input({required: true}) parent: ElementRef | undefined;
@Output() refreshToC: EventEmitter<void> = new EventEmitter();
@Output() isOpen: EventEmitter<boolean> = new EventEmitter(false);
xPath: string = '';
selectedText: string = '';
@ -84,6 +85,8 @@ export class BookLineOverlayComponent implements OnInit {
if (!event.target) return;
if ((!selection || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) {
event.preventDefault();
event.stopPropagation();
this.reset();
return;
}
@ -96,6 +99,7 @@ export class BookLineOverlayComponent implements OnInit {
this.xPath = '//' + this.xPath;
}
this.isOpen.emit(true);
event.preventDefault();
event.stopPropagation();
}
@ -137,6 +141,7 @@ export class BookLineOverlayComponent implements OnInit {
if (selection) {
selection.removeAllRanges();
}
this.isOpen.emit(false);
this.cdRef.markForCheck();
}

View File

@ -3,13 +3,14 @@
<ng-container *transloco="let t; read: 'book-reader'">
<div class="fixed-top" #stickyTop>
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">{{t('skip-header')}}</a>
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
<ng-container [ngTemplateOutlet]="actionBar" [ngTemplateOutletContext]="{isTop: true}"></ng-container>
<app-book-line-overlay [parent]="bookContainerElemRef" *ngIf="page !== undefined"
[libraryId]="libraryId"
[volumeId]="volumeId"
[chapterId]="chapterId"
[seriesId]="seriesId"
[pageNumber]="pageNum"
(isOpen)="updateLineOverlayOpen($event)"
(refreshToC)="refreshPersonalToC()">
</app-book-line-overlay>
<app-drawer #commentDrawer="drawer" [(isOpen)]="drawerOpen" [options]="{topOffset: topOffset}">
@ -18,11 +19,6 @@
<span style="font-size: 14px; color: var(--primary-color)" tabindex="0" role="button" (click)="closeReader()">{{t('close-reader')}}</span>
</div>
<div subheader>
<!-- <div class="g-0 text-center" *ngIf="!isLoading">-->
<!-- <span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" [attr.aria-label]="t('incognito-mode-alt')">-->
<!-- (<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-mode-label')}}</span>)</span>-->
<!-- <span class="book-title-text ms-1" [ngbTooltip]="bookTitle">{{bookTitle}}</span>-->
<!-- </div>-->
<div class="pagination-cont">
<ng-container *ngIf="layoutMode !== BookPageLayoutMode.Default">
<div class="virt-pagination-cont">
@ -129,13 +125,14 @@
<div *ngIf="page !== undefined && (scrollbarNeeded || layoutMode !== BookPageLayoutMode.Default) && !(writingStyle === WritingStyle.Vertical && layoutMode === BookPageLayoutMode.Default)"
(click)="$event.stopPropagation();"
[ngClass]="{'bottom-bar': layoutMode !== BookPageLayoutMode.Default}">
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
<ng-container [ngTemplateOutlet]="actionBar" [ngTemplateOutletContext]="{isTop: false}"></ng-container>
</div>
</div>
</div>
<ng-template #actionBar>
<ng-template #actionBar let-isTop>
<div class="action-bar row g-0 justify-content-between" *ngIf="!immersiveMode || drawerOpen || actionBarVisible">
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)"
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1"
(click)="!isTop && movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)"
[disabled]="readingDirection === ReadingDirection.LeftToRight ? IsPrevDisabled : IsNextDisabled"
title="{{readingDirection === ReadingDirection.LeftToRight ? t('previous') : t('next')}} Page">
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsPrevChapter : IsNextChapter) ? 'fa-angle-double-left' : 'fa-angle-left'}} {{readingDirection === ReadingDirection.RightToLeft ? 'next-page-highlight' : ''}}" aria-hidden="true"></i>
@ -146,20 +143,20 @@
<button class="btn btn-secondary col-2 col-xs-1" (click)="toggleDrawer()">
<i class="fa fa-bars" aria-hidden="true"></i></button>
<div class="book-title col-2 d-none d-sm-block">
<ng-container *ngIf="isLoading; else showTitle">
@if(isLoading) {
<div class="spinner-border spinner-border-sm text-primary" style="border-radius: 50%;" role="status">
<span class="visually-hidden">{{t('loading-book')}}</span>
</div>
</ng-container>
<ng-template #showTitle>
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" [attr.aria-label]="t('incognito-mode-alt')">
} @else {
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" [attr.aria-label]="t('incognito-mode-alt')">
(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-mode-label')}}</span>)</span>
<span class="book-title-text ms-1" [ngbTooltip]="bookTitle">{{bookTitle}}</span>
</ng-template>
<span class="book-title-text ms-1" [ngbTooltip]="bookTitle">{{bookTitle}}</span>
}
</div>
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i></button>
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1"
[disabled]="readingDirection === ReadingDirection.LeftToRight ? IsNextDisabled : IsPrevDisabled"
(click)="!isTop && movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)"
title="{{readingDirection === ReadingDirection.LeftToRight ? t('next') : t('previous')}} Page">
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsNextChapter : IsPrevChapter) ? 'fa-angle-double-right' : 'fa-angle-right'}} {{readingDirection === ReadingDirection.LeftToRight ? 'next-page-highlight' : ''}}" aria-hidden="true"></i>
</button>

View File

@ -185,6 +185,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* Belongs to drawer component
*/
drawerOpen = false;
/**
* If the word/line overlay is open
*/
isLineOverlayOpen = false;
/**
* If the action bar is visible
*/
@ -1630,20 +1634,21 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.cdRef.markForCheck();
}
// Responsible for handling pagination only
handleContainerClick(event: MouseEvent) {
if (this.drawerOpen || ['action-bar', 'offcanvas-backdrop'].some(className => (event.target as Element).classList.contains(className))) {
if (this.drawerOpen || this.isLineOverlayOpen || ['action-bar', 'offcanvas-backdrop'].some(className => (event.target as Element).classList.contains(className))) {
return;
}
if (this.isCursorOverLeftPaginationArea(event)) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD);
} else if (this.isCursorOverRightPaginationArea(event)) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)
} else {
this.toggleMenu(event);
if (this.clickToPaginate) {
if (this.isCursorOverLeftPaginationArea(event)) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD);
} else if (this.isCursorOverRightPaginationArea(event)) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)
}
}
this.toggleMenu(event);
}
handleReaderClick(event: MouseEvent) {
@ -1706,4 +1711,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
refreshPersonalToC() {
this.refreshPToC.emit();
}
updateLineOverlayOpen(isOpen: boolean) {
// HACK: This hack allows the boolean to be changed to false so that the pagination doesn't trigger and move us to the next page when
// the book overlay is just closing
setTimeout(() => {
this.isLineOverlayOpen = isOpen;
this.cdRef.markForCheck();
}, 10);
}
}

View File

@ -106,9 +106,9 @@
<div class="col-md-12">
<div class="mb-3">
<label for="genres" class="form-label">{{t('genres-label')}}</label>
<app-typeahead (selectedData)="updateGenres($event)" [settings]="genreSettings"
<app-typeahead (selectedData)="updateGenres($event);metadata.genresLocked = true" [settings]="genreSettings"
[(locked)]="metadata.genresLocked" (onUnlock)="metadata.genresLocked = false"
(newItemAdded)="metadata.genresLocked = true" (selectedData)="metadata.genresLocked = true">
(newItemAdded)="metadata.genresLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -142,9 +142,9 @@
<div class="col-lg-4 col-md-12 pe-2">
<div class="mb-3">
<label for="language" class="form-label">{{t('language-label')}}</label>
<app-typeahead (selectedData)="updateLanguage($event)" [settings]="languageSettings"
<app-typeahead (selectedData)="updateLanguage($event);metadata.languageLocked = true;" [settings]="languageSettings"
[(locked)]="metadata.languageLocked" (onUnlock)="metadata.languageLocked = false"
(newItemAdded)="metadata.languageLocked = true" (selectedData)="metadata.languageLocked = true">
(newItemAdded)="metadata.languageLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -159,7 +159,7 @@
<label for="age-rating" class="form-label">{{t('age-rating-label')}}</label>
<div class="input-group {{metadata.ageRatingLocked ? 'lock-active' : ''}}">
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'ageRatingLocked' }"></ng-container>
<select class="form-select"id="age-rating" formControlName="ageRating">
<select class="form-select" id="age-rating" formControlName="ageRating">
<option *ngFor="let opt of ageRatings" [value]="opt.value">{{opt.title | titlecase}}</option>
</select>
</div>
@ -170,7 +170,7 @@
<label for="publication-status" class="form-label">{{t('publication-status-label')}}</label>
<div class="input-group {{metadata.publicationStatusLocked ? 'lock-active' : ''}}">
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'publicationStatusLocked' }"></ng-container>
<select class="form-select"id="publication-status" formControlName="publicationStatus">
<select class="form-select" id="publication-status" formControlName="publicationStatus">
<option *ngFor="let opt of publicationStatuses" [value]="opt.value">{{opt.title | titlecase}}</option>
</select>
</div>
@ -186,9 +186,9 @@
<div class="row g-0">
<div class="mb-3">
<label for="writer" class="form-label">{{t('writer-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)"
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Writer);metadata.writerLocked = true" [settings]="getPersonsSettings(PersonRole.Writer)"
[(locked)]="metadata.writerLocked" (onUnlock)="metadata.writerLocked = false"
(newItemAdded)="metadata.writerLocked = true" (selectedData)="metadata.writerLocked = true">
(newItemAdded)="metadata.writerLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -201,9 +201,9 @@
<div class="row g-0">
<div class="mb-3">
<label for="cover-artist" class="form-label">{{t('cover-artist-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)"
<app-typeahead (selectedData)="updatePerson($event, PersonRole.CoverArtist);metadata.coverArtistLocked = true" [settings]="getPersonsSettings(PersonRole.CoverArtist)"
[(locked)]="metadata.coverArtistLocked" (onUnlock)="metadata.coverArtistLocked = false"
(newItemAdded)="metadata.coverArtistLocked = true" (selectedData)="metadata.coverArtistLocked = true">
(newItemAdded)="metadata.coverArtistLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -218,9 +218,9 @@
<div class="row g-0">
<div class="mb-3">
<label for="publisher" class="form-label">{{t('publisher-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)"
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Publisher);metadata.publisherLocked = true" [settings]="getPersonsSettings(PersonRole.Publisher)"
[(locked)]="metadata.publisherLocked" (onUnlock)="metadata.publisherLocked = false"
(newItemAdded)="metadata.publisherLocked = true" (selectedData)="metadata.publisherLocked = true">
(newItemAdded)="metadata.publisherLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -233,9 +233,9 @@
<div class="row g-0">
<div class="mb-3">
<label for="penciller" class="form-label">{{t('penciller-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)"
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Penciller);metadata.pencillerLocked = true" [settings]="getPersonsSettings(PersonRole.Penciller)"
[(locked)]="metadata.pencillerLocked" (onUnlock)="metadata.pencillerLocked = false"
(newItemAdded)="metadata.pencillerLocked = true" (selectedData)="metadata.pencillerLocked = true">
(newItemAdded)="metadata.pencillerLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -250,9 +250,9 @@
<div class="row g-0">
<div class="mb-3">
<label for="letterer" class="form-label">{{t('letterer-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)"
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Letterer);metadata.lettererLocked = true" [settings]="getPersonsSettings(PersonRole.Letterer)"
[(locked)]="metadata.lettererLocked" (onUnlock)="metadata.lettererLocked = false"
(newItemAdded)="metadata.lettererLocked = true" (selectedData)="metadata.lettererLocked = true">
(newItemAdded)="metadata.lettererLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -265,9 +265,9 @@
<div class="row g-0">
<div class="mb-3">
<label for="inker" class="form-label">{{t('inker-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)"
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Inker);metadata.inkerLocked = true" [settings]="getPersonsSettings(PersonRole.Inker)"
[(locked)]="metadata.inkerLocked" (onUnlock)="metadata.inkerLocked = false"
(newItemAdded)="metadata.inkerLocked = true" (selectedData)="metadata.inkerLocked = true">
(newItemAdded)="metadata.inkerLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -283,9 +283,9 @@
<div class="row g-0">
<div class="mb-3">
<label for="editor" class="form-label">{{t('editor-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)"
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Editor);metadata.editorLocked = true" [settings]="getPersonsSettings(PersonRole.Editor)"
[(locked)]="metadata.editorLocked" (onUnlock)="metadata.editorLocked = false"
(newItemAdded)="metadata.editorLocked = true" (selectedData)="metadata.editorLocked = true">
(newItemAdded)="metadata.editorLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -298,9 +298,9 @@
<div class="row g-0">
<div class="mb-3">
<label for="colorist" class="form-label">{{t('colorist-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)"
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Colorist);metadata.coloristLocked = true" [settings]="getPersonsSettings(PersonRole.Colorist)"
[(locked)]="metadata.coloristLocked" (onUnlock)="metadata.coloristLocked = false"
(newItemAdded)="metadata.coloristLocked = true" (selectedData)="metadata.coloristLocked = true">
(newItemAdded)="metadata.coloristLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -316,9 +316,9 @@
<div class="row g-0">
<div class="mb-3">
<label for="character" class="form-label">{{t('character-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)"
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Character);metadata.characterLocked = true" [settings]="getPersonsSettings(PersonRole.Character)"
[(locked)]="metadata.characterLocked" (onUnlock)="metadata.characterLocked = false"
(newItemAdded)="metadata.characterLocked = true" (selectedData)="metadata.characterLocked = true">
(newItemAdded)="metadata.characterLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -331,9 +331,9 @@
<div class="row g-0">
<div class="mb-3">
<label for="translator" class="form-label">{{t('translator-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)"
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Translator);metadata.translatorLocked = true;" [settings]="getPersonsSettings(PersonRole.Translator)"
[(locked)]="metadata.translatorLocked" (onUnlock)="metadata.translatorLocked = false"
(newItemAdded)="metadata.translatorLocked = true" (selectedData)="metadata.translatorLocked = true">
(newItemAdded)="metadata.translatorLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>

View File

@ -47,6 +47,15 @@ interface RelationControl {
})
export class EditSeriesRelationComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly seriesService = inject(SeriesService);
private readonly utilityService = inject(UtilityService);
private readonly libraryService = inject(LibraryService);
private readonly searchService = inject(SearchService);
public readonly imageService = inject(ImageService);
protected readonly RelationKind = RelationKind;
@Input({required: true}) series!: Series;
/**
* This will tell the component to save based on its internal state
@ -60,16 +69,6 @@ export class EditSeriesRelationComponent implements OnInit {
libraryNames: {[key:number]: string} = {};
focusTypeahead = new EventEmitter();
private readonly destroyRef = inject(DestroyRef);
get RelationKind() {
return RelationKind;
}
constructor(private seriesService: SeriesService, private utilityService: UtilityService,
public imageService: ImageService, private libraryService: LibraryService, private searchService: SearchService,
private readonly cdRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.seriesService.getRelatedForSeries(this.series.id).subscribe(async relations => {

View File

@ -76,9 +76,8 @@ export class DoubleRendererComponent implements OnInit, ImageRenderer {
shouldRenderDouble$!: Observable<boolean>;
get ReaderMode() {return ReaderMode;}
get LayoutMode() {return LayoutMode;}
protected readonly ReaderMode = ReaderMode;
protected readonly LayoutMode = LayoutMode;
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,

View File

@ -19,7 +19,7 @@
<div>
<div style="font-weight: bold;">{{title}} <span class="clickable" *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" [attr.aria-label]="t('incognito-alt')">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-title')}}</span>)</span></div>
<div class="subtitle">
{{subtitle}} <span *ngIf="totalSeriesPages > 0">{{t('series-progress', {percentage: (((totalSeriesPagesRead + pageNum) / totalSeriesPages) | percent)}) }}</span>
{{subtitle}} <span *ngIf="totalSeriesPages > 0">{{t('series-progress', {percentage: (Math.min(1, ((totalSeriesPagesRead + pageNum) / totalSeriesPages)) | percent)}) }}</span>
</div>
</div>
@ -36,7 +36,7 @@
</div>
</div>
</div>
<app-loading [loading]="isLoading" [absolute]="true"></app-loading>
<app-loading [loading]="isLoading || !(currentImage$ | async)?.complete" [absolute]="true"></app-loading>
<div class="reading-area"
ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)"
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>

View File

@ -13,7 +13,7 @@ import {
OnInit,
ViewChild
} from '@angular/core';
import {DOCUMENT, NgStyle, NgIf, NgFor, NgSwitch, NgSwitchCase, PercentPipe, NgClass} from '@angular/common';
import {DOCUMENT, NgStyle, NgIf, NgFor, NgSwitch, NgSwitchCase, PercentPipe, NgClass, AsyncPipe} from '@angular/common';
import {ActivatedRoute, Router} from '@angular/router';
import {
BehaviorSubject,
@ -68,6 +68,7 @@ import { InfiniteScrollerComponent } from '../infinite-scroller/infinite-scrolle
import { SwipeDirective } from '../../../ng-swipe/ng-swipe.directive';
import { LoadingComponent } from '../../../shared/loading/loading.component';
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {shareReplay} from "rxjs/operators";
const PREFETCH_PAGES = 10;
@ -124,7 +125,7 @@ enum KeyDirection {
imports: [NgStyle, NgIf, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent,
DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent,
NgxSliderModule, ReactiveFormsModule, NgFor, NgSwitch, NgSwitchCase, FittingIconPipe, ReaderModeIconPipe,
FullscreenIconPipe, TranslocoDirective, NgbProgressbar, PercentPipe, NgClass]
FullscreenIconPipe, TranslocoDirective, NgbProgressbar, PercentPipe, NgClass, AsyncPipe]
})
export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ -158,6 +159,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
protected readonly LayoutMode = LayoutMode;
protected readonly ReadingDirection = ReadingDirection;
protected readonly Breakpoint = Breakpoint;
protected readonly Math = Math;
libraryId!: number;
seriesId!: number;
@ -400,7 +402,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// Renderer interaction
readerSettings$!: Observable<ReaderSetting>;
private currentImage: Subject<HTMLImageElement | null> = new ReplaySubject(1);
currentImage$: Observable<HTMLImageElement | null> = this.currentImage.asObservable();
currentImage$: Observable<HTMLImageElement | null> = this.currentImage.asObservable().pipe(
shareReplay({refCount: true, bufferSize: 2})
);
private pageNumSubject: Subject<{pageNum: number, maxPages: number}> = new ReplaySubject();
pageNum$: Observable<{pageNum: number, maxPages: number}> = this.pageNumSubject.asObservable();
@ -743,18 +747,34 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* @param pageNum Page Number to load
* @param forceNew Forces to fetch a new image
* @param chapterId ChapterId to fetch page from. Defaults to current chapterId. Not used when in bookmark mode
* @returns
* @returns HTMLImageElement | undefined
*/
getPage(pageNum: number, chapterId: number = this.chapterId, forceNew: boolean = false) {
let img;
let img: HTMLImageElement | undefined;
if (this.bookmarkMode) img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum);
else img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum
&& (this.readerService.imageUrlToChapterId(img.src) == chapterId || this.readerService.imageUrlToChapterId(img.src) === -1)
);
//console.log('Requesting page ', pageNum, ' found page: ', img, ' and app is requesting new image? ', forceNew);
if (!img || forceNew) {
img = new Image();
img.src = this.getPageUrl(pageNum, chapterId);
img.onload = (evt) => {
this.currentImage.next(img!);
this.cdRef.markForCheck();
}
// img.onerror = (evt) => {
// const event = evt as Event;
// const page = this.readerService.imageUrlToPageNum((event.target as HTMLImageElement).src);
// console.error('Image failed to load: ', page);
// (event.target as HTMLImageElement).onerror = null;
// const newSrc = this.getPageUrl(pageNum, chapterId) + '#' + new Date().getTime();
// console.log('requesting page ', page, ' with url: ', newSrc);
// (event.target as HTMLImageElement).src = newSrc;
// this.cdRef.markForCheck();
// }
}
return img;
@ -1224,6 +1244,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/
setCanvasImage() {
if (this.cachedImages === undefined) return;
this.canvasImage = this.getPage(this.pageNum, this.chapterId, this.layoutMode !== LayoutMode.Single);
if (!this.canvasImage.complete) {
this.canvasImage.addEventListener('load', () => {
@ -1346,19 +1367,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const index = (numOffset % this.cachedImages.length + this.cachedImages.length) % this.cachedImages.length;
const cachedImagePageNum = this.readerService.imageUrlToPageNum(this.cachedImages[index].src);
if (cachedImagePageNum !== numOffset) {
this.cachedImages[index] = new Image();
this.cachedImages[index].src = this.getPageUrl(numOffset);
this.cachedImages[index].onload = (evt) => {
this.cdRef.markForCheck();
}
this.cachedImages[index] = this.getPage(numOffset, this.chapterId);
}
}
//const pages = this.cachedImages.map(img => [this.readerService.imageUrlToChapterId(img.src), this.readerService.imageUrlToPageNum(img.src)]);
// console.log(this.pageNum, ' Prefetched pages: ', pages.map(p => {
// if (this.pageNum === p[1]) return '[' + p + ']';
// return '' + p
// }));
}
@ -1681,4 +1692,5 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
d.text = translate('preferences.' + o.text);
return d;
}
}

View File

@ -1,12 +1,12 @@
<ng-container *ngIf="isValid() && !this.mangaReaderService.shouldSplit(this.currentImage, this.pageSplit)">
<div class="image-container {{imageFitClass$ | async}} {{emulateBookClass$ | async}}"
[style.filter]="(darkness$ | async) ?? '' | safeStyle" [style.height]="(imageContainerHeight$ | async) ?? '' | safeStyle">
<ng-container *ngIf="currentImage">
<img alt=" "
#image [src]="currentImage.src"
id="image-1"
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
>
</ng-container>
</div>
</ng-container>
@if(isValid() && !mangaReaderService.shouldSplit(currentImage, pageSplit)) {
<div class="image-container {{imageFitClass$ | async}} {{emulateBookClass$ | async}}"
[style.filter]="(darkness$ | async) ?? '' | safeStyle" [style.height]="(imageContainerHeight$ | async) ?? '' | safeStyle">
@if(currentImage) {
<img alt=" "
#image
[src]="currentImage.src"
id="image-1"
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}" />
}
</div>
}

View File

@ -27,7 +27,7 @@ import { SafeStylePipe } from '../../../_pipes/safe-style.pipe';
styleUrls: ['./single-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, AsyncPipe, SafeStylePipe]
imports: [AsyncPipe, SafeStylePipe]
})
export class SingleRendererComponent implements OnInit, ImageRenderer {
@ -137,7 +137,7 @@ export class SingleRendererComponent implements OnInit, ImageRenderer {
return fit;
}),
shareReplay(),
shareReplay({refCount: true, bufferSize: 1}),
filter(_ => this.isValid()),
takeUntilDestroyed(this.destroyRef),
);

View File

@ -153,7 +153,8 @@ export class LibrarySettingsModalComponent implements OnInit {
// This needs to only apply after first render
this.libraryForm.get('type')?.valueChanges.pipe(
tap((type: LibraryType) => {
switch (type) {
const libType = parseInt(type + '', 10) as LibraryType;
switch (libType) {
case LibraryType.Manga:
this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(true);
this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(true);
@ -177,6 +178,7 @@ export class LibrarySettingsModalComponent implements OnInit {
this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(true);
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false);
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false);
break;
}
}),
takeUntilDestroyed(this.destroyRef)
@ -203,15 +205,17 @@ export class LibrarySettingsModalComponent implements OnInit {
for(let glob of this.library.excludePatterns) {
this.libraryForm.addControl('excludeGlob-' , new FormControl(glob, []));
}
this.excludePatterns = this.library.excludePatterns;
} else {
for(let fileTypeGroup of allFileTypeGroup) {
this.libraryForm.addControl(fileTypeGroup + '', new FormControl(true, []));
}
}
this.excludePatterns = this.library.excludePatterns;
if (this.excludePatterns.length === 0) {
this.excludePatterns = [''];
}
this.cdRef.markForCheck();
}