Webtoon fixes + Random release stuff (#1048)

* Refactored the way cover images are updated from SignalR to use an explicit event that is sent at a granular level for a given type of entity.

Fixed a bad event listener for RefreshMetadata (now removed) to update metadata on Series Detail. Now uses ScanService, which indicates a series has completed a scan.

* Lots of attempts at making webtoon stable. Kinda working kinda not.

* Added a new boolean to hide images until the first prefetch loads the images, to prevent jankiness

* On Search, remove : from query

* Added HasBookmark and NumberOfLibraries to stat service

* Cleaned up some dead code

* Fixed a bug where page number wasn't reset between chapter loads with infinite scroller

* Added recently added series back into the dashboard.

* Cleaned up some code in search bar
This commit is contained in:
Joseph Milazzo 2022-02-08 07:30:54 -08:00 committed by GitHub
parent be1a9187e5
commit b571633eab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 93 additions and 61 deletions

View File

@ -227,7 +227,7 @@ namespace API.Controllers
[HttpGet("search")] [HttpGet("search")]
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString) public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
{ {
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty); queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
// Get libraries user has access to // Get libraries user has access to

View File

@ -8,5 +8,7 @@
public string DotnetVersion { get; set; } public string DotnetVersion { get; set; }
public string KavitaVersion { get; set; } public string KavitaVersion { get; set; }
public int NumOfCores { get; set; } public int NumOfCores { get; set; }
public int NumberOfLibraries { get; set; }
public bool HasBookmarks { get; set; }
} }
} }

View File

@ -241,11 +241,7 @@ public class MetadataService : IMetadataService
} }
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();
// foreach (var series in nonLibrarySeries)
// {
// // TODO: This can be removed, we use CoverUpdate elsewhere
// await _messageHub.Clients.All.SendAsync(SignalREvents.RefreshMetadata, MessageFactory.RefreshMetadataEvent(library.Id, series.Id));
// }
_logger.LogInformation( _logger.LogInformation(
"[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}", "[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}",
chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name); chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name);

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -105,7 +106,9 @@ public class StatsService : IStatsService
KavitaVersion = BuildInfo.Version.ToString(), KavitaVersion = BuildInfo.Version.ToString(),
DotnetVersion = Environment.Version.ToString(), DotnetVersion = Environment.Version.ToString(),
IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker, IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker,
NumOfCores = Math.Max(Environment.ProcessorCount, 1) NumOfCores = Math.Max(Environment.ProcessorCount, 1),
HasBookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync()).Any(),
NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count()
}; };
return serverInfo; return serverInfo;

View File

@ -1,7 +1,7 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewChild } from '@angular/core'; import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewChild } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { Subject } from 'rxjs'; import { BehaviorSubject, Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators'; import { debounceTime, takeUntil } from 'rxjs/operators';
import { KEY_CODES } from '../shared/_services/utility.service'; import { KEY_CODES } from '../shared/_services/utility.service';
import { SearchResultGroup } from '../_models/search/search-result-group'; import { SearchResultGroup } from '../_models/search/search-result-group';
@ -77,7 +77,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
} }
constructor(private renderer2: Renderer2, @Inject(DOCUMENT) private document: Document) { } constructor() { }
@HostListener('window:click', ['$event']) @HostListener('window:click', ['$event'])
handleDocumentClick(event: any) { handleDocumentClick(event: any) {
@ -122,13 +122,6 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
} }
if (this.inputElem) {
// hack: To prevent multiple typeaheads from being open at once, click document then trigger the focus
this.document.querySelector('body')?.click();
this.inputElem.nativeElement.focus();
this.open();
}
this.openDropdown(); this.openDropdown();
return this.hasFocus; return this.hasFocus;

View File

@ -18,6 +18,12 @@
</ng-template> </ng-template>
</app-carousel-reel> </app-carousel-reel>
<app-carousel-reel [items]="recentlyAddedSeries" title="Recently Added Series" (sectionClick)="handleSectionClick($event)">
<ng-template #carouselItem let-item let-position="idx">
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
</ng-template>
</app-carousel-reel>
<app-carousel-reel [items]="recentlyAddedChapters" title="Recently Added"> <app-carousel-reel [items]="recentlyAddedChapters" title="Recently Added">
<ng-template #carouselItem let-item let-position="idx"> <ng-template #carouselItem let-item let-position="idx">
<app-card-item [entity]="item" [title]="item.title" [subtitle]="item.seriesName" [imageUrl]="imageService.getRecentlyAddedItem(item)" <app-card-item [entity]="item" [title]="item.title" [subtitle]="item.seriesName" [imageUrl]="imageService.getRecentlyAddedItem(item)"

View File

@ -32,6 +32,7 @@ export class LibraryComponent implements OnInit, OnDestroy {
recentlyUpdatedSeries: SeriesGroup[] = []; recentlyUpdatedSeries: SeriesGroup[] = [];
recentlyAddedChapters: RecentlyAddedItem[] = []; recentlyAddedChapters: RecentlyAddedItem[] = [];
inProgress: Series[] = []; inProgress: Series[] = [];
recentlyAddedSeries: Series[] = [];
private readonly onDestroy = new Subject<void>(); private readonly onDestroy = new Subject<void>();
@ -44,11 +45,16 @@ export class LibraryComponent implements OnInit, OnDestroy {
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => { this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
if (res.event === EVENTS.SeriesAdded) { if (res.event === EVENTS.SeriesAdded) {
const seriesAddedEvent = res.payload as SeriesAddedEvent; const seriesAddedEvent = res.payload as SeriesAddedEvent;
this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
this.recentlyAddedSeries.unshift(series);
});
this.loadRecentlyAdded(); this.loadRecentlyAdded();
} else if (res.event === EVENTS.SeriesRemoved) { } else if (res.event === EVENTS.SeriesRemoved) {
const seriesRemovedEvent = res.payload as SeriesRemovedEvent; const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId); this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
this.recentlyAddedChapters = this.recentlyAddedChapters.filter(item => item.seriesId != seriesRemovedEvent.seriesId); this.recentlyAddedChapters = this.recentlyAddedChapters.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
} else if (res.event === EVENTS.ScanSeries) { } else if (res.event === EVENTS.ScanSeries) {
@ -81,6 +87,7 @@ export class LibraryComponent implements OnInit, OnDestroy {
reloadSeries() { reloadSeries() {
this.loadOnDeck(); this.loadOnDeck();
this.loadRecentlyAdded(); this.loadRecentlyAdded();
this.loadRecentlyAddedSeries();
} }
reloadInProgress(series: Series | boolean) { reloadInProgress(series: Series | boolean) {
@ -102,6 +109,12 @@ export class LibraryComponent implements OnInit, OnDestroy {
}); });
} }
loadRecentlyAddedSeries() {
this.seriesService.getRecentlyAdded().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
this.recentlyAddedSeries = updatedSeries.result;
});
}
loadRecentlyAdded() { loadRecentlyAdded() {
this.seriesService.getRecentlyUpdatedSeries().pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => { this.seriesService.getRecentlyUpdatedSeries().pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {

View File

@ -4,7 +4,7 @@
<strong>Is Scrolling:</strong> {{isScrollingForwards() ? 'Forwards' : 'Backwards'}} {{this.isScrolling}} <strong>Is Scrolling:</strong> {{isScrollingForwards() ? 'Forwards' : 'Backwards'}} {{this.isScrolling}}
<strong>All Images Loaded:</strong> {{this.allImagesLoaded}} <strong>All Images Loaded:</strong> {{this.allImagesLoaded}}
<strong>Prefetched</strong> {{minPageLoaded}}-{{maxPageLoaded}} <strong>Prefetched</strong> {{minPageLoaded}}-{{maxPageLoaded}}
<strong>Pages:</strong> {{pageNum}} / {{totalPages}} <strong>Pages:</strong> {{pageNum}} / {{totalPages - 1}}
<strong>At Top:</strong> {{atTop}} <strong>At Top:</strong> {{atTop}}
<strong>At Bottom:</strong> {{atBottom}} <strong>At Bottom:</strong> {{atBottom}}
<strong>Total Height:</strong> {{getTotalHeight()}} <strong>Total Height:</strong> {{getTotalHeight()}}
@ -27,7 +27,7 @@
</div> </div>
<ng-container *ngFor="let item of webtoonImages | async; let index = index;"> <ng-container *ngFor="let item of webtoonImages | async; let index = index;">
<img src="{{item.src}}" style="display: block" <img src="{{item.src}}" style="display: block"
class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}}" class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}} {{initFinished ? '' : 'full-opacity'}}"
*ngIf="pageNum >= pageNum - bufferPages && pageNum <= pageNum + bufferPages" rel="nofollow" alt="image" *ngIf="pageNum >= pageNum - bufferPages && pageNum <= pageNum + bufferPages" rel="nofollow" alt="image"
(load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;"> (load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">
</ng-container> </ng-container>

View File

@ -6,6 +6,10 @@
border: 2px solid red; border: 2px solid red;
} }
.full-opacity {
opacity: 0;
}
.spacer { .spacer {
width: 100%; width: 100%;
height: 300px; height: 300px;
@ -25,10 +29,6 @@
width: 100% !important; width: 100% !important;
} }
// .img-container {
// overflow: auto;
// }
@keyframes move-up-down { @keyframes move-up-down {
0%, 100% { 0%, 100% {

View File

@ -61,7 +61,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
@Output() loadNextChapter: EventEmitter<void> = new EventEmitter<void>(); @Output() loadNextChapter: EventEmitter<void> = new EventEmitter<void>();
@Output() loadPrevChapter: EventEmitter<void> = new EventEmitter<void>(); @Output() loadPrevChapter: EventEmitter<void> = new EventEmitter<void>();
@Input() goToPage: ReplaySubject<number> = new ReplaySubject<number>(); @Input() goToPage: BehaviorSubject<number> | undefined;
@Input() bookmarkPage: ReplaySubject<number> = new ReplaySubject<number>(); @Input() bookmarkPage: ReplaySubject<number> = new ReplaySubject<number>();
@Input() fullscreenToggled: ReplaySubject<boolean> = new ReplaySubject<boolean>(); @Input() fullscreenToggled: ReplaySubject<boolean> = new ReplaySubject<boolean>();
@ -121,10 +121,18 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
* Keeps track of the previous scrolling height for restoring scroll position after we inject spacer block * Keeps track of the previous scrolling height for restoring scroll position after we inject spacer block
*/ */
previousScrollHeightMinusTop: number = 0; previousScrollHeightMinusTop: number = 0;
/**
* Tracks the first load, until all the initial prefetched images are loaded. We use this to reduce opacity so images can load without jerk.
*/
initFinished: boolean = false;
/** /**
* Debug mode. Will show extra information. Use bitwise (|) operators between different modes to enable different output * Debug mode. Will show extra information. Use bitwise (|) operators between different modes to enable different output
*/ */
debugMode: DEBUG_MODES = DEBUG_MODES.None; debugMode: DEBUG_MODES = DEBUG_MODES.None;
/**
* Debug mode. Will filter out any messages in here so they don't hit the log
*/
debugLogFilter: Array<string> = ['[PREFETCH]', '[Intersection]', '[Visibility]', '[Image Load]'];
get minPageLoaded() { get minPageLoaded() {
return Math.min(...Object.values(this.imagesLoaded)); return Math.min(...Object.values(this.imagesLoaded));
@ -173,8 +181,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : window, 'scroll') fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : window, 'scroll')
.pipe(debounceTime(20), takeUntil(this.onDestroy)) .pipe(debounceTime(20), takeUntil(this.onDestroy))
.subscribe((event) => this.handleScrollEvent(event)); .subscribe((event) => this.handleScrollEvent(event));
} }
ngOnInit(): void { ngOnInit(): void {
@ -182,9 +188,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
if (this.goToPage) { if (this.goToPage) {
this.goToPage.pipe(takeUntil(this.onDestroy)).subscribe(page => { this.goToPage.pipe(takeUntil(this.onDestroy)).subscribe(page => {
this.debugLog('[GoToPage] jump has occured from ' + this.pageNum + ' to ' + page);
const isSamePage = this.pageNum === page; const isSamePage = this.pageNum === page;
if (isSamePage) { return; } if (isSamePage) { return; }
this.debugLog('[GoToPage] jump has occured from ' + this.pageNum + ' to ' + page);
if (this.pageNum < page) { if (this.pageNum < page) {
this.scrollingDirection = PAGING_DIRECTION.FORWARD; this.scrollingDirection = PAGING_DIRECTION.FORWARD;
@ -252,10 +258,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
} }
this.prevScrollPosition = verticalOffset; this.prevScrollPosition = verticalOffset;
console.log('CurrentPageElem: ', this.currentPageElem);
if (this.currentPageElem != null) {
console.log('Element Visible: ', this.isElementVisible(this.currentPageElem));
}
if (this.isScrolling && this.currentPageElem != null && this.isElementVisible(this.currentPageElem)) { if (this.isScrolling && this.currentPageElem != null && this.isElementVisible(this.currentPageElem)) {
this.debugLog('[Scroll] Image is visible from scroll, isScrolling is now false'); this.debugLog('[Scroll] Image is visible from scroll, isScrolling is now false');
this.isScrolling = false; this.isScrolling = false;
@ -356,15 +358,12 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
isElementVisible(elem: Element) { isElementVisible(elem: Element) {
if (elem === null || elem === undefined) { return false; } if (elem === null || elem === undefined) { return false; }
this.debugLog('[Visibility] Checking if Page ' + elem.getAttribute('id') + ' is visible');
// NOTE: This will say an element is visible if it is 1 px offscreen on top // NOTE: This will say an element is visible if it is 1 px offscreen on top
var rect = elem.getBoundingClientRect(); var rect = elem.getBoundingClientRect();
let [innerHeight, innerWidth] = this.getInnerDimensions(); let [innerHeight, innerWidth] = this.getInnerDimensions();
console.log('innerHeight: ', innerHeight);
console.log('innerWidth: ', innerWidth);
return (rect.bottom >= 0 && return (rect.bottom >= 0 &&
rect.right >= 0 && rect.right >= 0 &&
rect.top <= (innerHeight || document.documentElement.clientHeight) && rect.top <= (innerHeight || document.documentElement.clientHeight) &&
@ -399,6 +398,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
initWebtoonReader() { initWebtoonReader() {
this.initFinished = false;
const [innerWidth, _] = this.getInnerDimensions(); const [innerWidth, _] = this.getInnerDimensions();
this.webtoonImageWidth = innerWidth || document.documentElement.clientWidth || document.body.clientWidth; this.webtoonImageWidth = innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
this.imagesLoaded = {}; this.imagesLoaded = {};
@ -437,11 +437,14 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
.filter((img: any) => !img.complete) .filter((img: any) => !img.complete)
.map((img: any) => new Promise(resolve => { img.onload = img.onerror = resolve; }))) .map((img: any) => new Promise(resolve => { img.onload = img.onerror = resolve; })))
.then(() => { .then(() => {
this.debugLog('[Initialization] All images have loaded from initial prefetch, initFinished = true');
this.debugLog('[Image Load] ! Loaded current page !', this.pageNum); this.debugLog('[Image Load] ! Loaded current page !', this.pageNum);
this.currentPageElem = document.querySelector('img#page-' + this.pageNum); this.currentPageElem = document.querySelector('img#page-' + this.pageNum);
// There needs to be a bit of time before we scroll
if (this.currentPageElem && !this.isElementVisible(this.currentPageElem)) { if (this.currentPageElem && !this.isElementVisible(this.currentPageElem)) {
this.scrollToCurrentPage(); this.scrollToCurrentPage();
} else {
this.initFinished = true;
} }
this.allImagesLoaded = true; this.allImagesLoaded = true;
@ -471,8 +474,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
* @param scrollToPage Optional (default false) parameter to trigger scrolling to the newly set page * @param scrollToPage Optional (default false) parameter to trigger scrolling to the newly set page
*/ */
setPageNum(pageNum: number, scrollToPage: boolean = false) { setPageNum(pageNum: number, scrollToPage: boolean = false) {
if (pageNum > this.totalPages) { if (pageNum >= this.totalPages) {
pageNum = this.totalPages; pageNum = this.totalPages - 1;
} else if (pageNum < 0) { } else if (pageNum < 0) {
pageNum = 0; pageNum = 0;
} }
@ -482,9 +485,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.prefetchWebtoonImages(); this.prefetchWebtoonImages();
if (scrollToPage) { if (scrollToPage) {
const currentImage = document.querySelector('img#page-' + this.pageNum);
if (currentImage === null) return;
this.debugLog('[GoToPage] Scrolling to page', this.pageNum);
this.scrollToCurrentPage(); this.scrollToCurrentPage();
} }
} }
@ -499,6 +499,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
scrollToCurrentPage() { scrollToCurrentPage() {
this.currentPageElem = document.querySelector('img#page-' + this.pageNum); this.currentPageElem = document.querySelector('img#page-' + this.pageNum);
if (!this.currentPageElem) { return; } if (!this.currentPageElem) { return; }
this.debugLog('[GoToPage] Scrolling to page', this.pageNum);
// Update prevScrollPosition, so the next scroll event properly calculates direction // Update prevScrollPosition, so the next scroll event properly calculates direction
this.prevScrollPosition = this.currentPageElem.getBoundingClientRect().top; this.prevScrollPosition = this.currentPageElem.getBoundingClientRect().top;
@ -508,6 +509,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
if (this.currentPageElem) { if (this.currentPageElem) {
this.debugLog('[Scroll] Scrolling to page ', this.pageNum); this.debugLog('[Scroll] Scrolling to page ', this.pageNum);
this.currentPageElem.scrollIntoView({behavior: 'smooth'}); this.currentPageElem.scrollIntoView({behavior: 'smooth'});
this.initFinished = true;
} }
}, 600); }, 600);
} }
@ -540,7 +542,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
attachIntersectionObserverElem(elem: HTMLImageElement) { attachIntersectionObserverElem(elem: HTMLImageElement) {
if (elem !== null) { if (elem !== null) {
this.intersectionObserver.observe(elem); this.intersectionObserver.observe(elem);
this.debugLog('Attached Intersection Observer to page', this.readerService.imageUrlToPageNum(elem.src)); this.debugLog('[Intersection] Attached Intersection Observer to page', this.readerService.imageUrlToPageNum(elem.src));
} else { } else {
console.error('Could not attach observer on elem'); // This never happens console.error('Could not attach observer on elem'); // This never happens
} }
@ -610,6 +612,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
debugLog(message: string, extraData?: any) { debugLog(message: string, extraData?: any) {
if (!(this.debugMode & DEBUG_MODES.Logs)) return; if (!(this.debugMode & DEBUG_MODES.Logs)) return;
if (this.debugLogFilter.filter(str => message.replace('\t', '').startsWith(str)).length > 0) return;
if (extraData !== undefined) { if (extraData !== undefined) {
console.log(message, extraData); console.log(message, extraData);
} else { } else {

View File

@ -27,7 +27,7 @@
<canvas #content class="{{getFittingOptionClass()}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}" <canvas #content class="{{getFittingOptionClass()}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}"
ondragstart="return false;" onselectstart="return false;"> ondragstart="return false;" onselectstart="return false;">
</canvas> </canvas>
<div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading"> <div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading && !inSetup">
<app-infinite-scroller [pageNum]="pageNum" <app-infinite-scroller [pageNum]="pageNum"
[bufferPages]="5" [bufferPages]="5"
[goToPage]="goToPageEvent" [goToPage]="goToPageEvent"

View File

@ -10,7 +10,7 @@ import { NavService } from '../_services/nav.service';
import { ReadingDirection } from '../_models/preferences/reading-direction'; import { ReadingDirection } from '../_models/preferences/reading-direction';
import { ScalingOption } from '../_models/preferences/scaling-option'; import { ScalingOption } from '../_models/preferences/scaling-option';
import { PageSplitOption } from '../_models/preferences/page-split-option'; import { PageSplitOption } from '../_models/preferences/page-split-option';
import { forkJoin, ReplaySubject, Subject } from 'rxjs'; import { BehaviorSubject, forkJoin, ReplaySubject, Subject } from 'rxjs';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { KEY_CODES, UtilityService, Breakpoint } from '../shared/_services/utility.service'; import { KEY_CODES, UtilityService, Breakpoint } from '../shared/_services/utility.service';
import { CircularArray } from '../shared/data-structures/circular-array'; import { CircularArray } from '../shared/data-structures/circular-array';
@ -126,7 +126,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
/** /**
* An event emiter when a page change occurs. Used soley by the webtoon reader. * An event emiter when a page change occurs. Used soley by the webtoon reader.
*/ */
goToPageEvent: ReplaySubject<number> = new ReplaySubject<number>(); goToPageEvent!: BehaviorSubject<number>;
/** /**
* An event emiter when a bookmark on a page change occurs. Used soley by the webtoon reader. * An event emiter when a bookmark on a page change occurs. Used soley by the webtoon reader.
*/ */
@ -221,6 +222,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* Library Type used for rendering chapter or issue * Library Type used for rendering chapter or issue
*/ */
libraryType: LibraryType = LibraryType.Manga; libraryType: LibraryType = LibraryType.Manga;
/**
* Used for webtoon reader. When loading pages or data, this will disable the reader
*/
inSetup: boolean = true;
private readonly onDestroy = new Subject<void>(); private readonly onDestroy = new Subject<void>();
@ -424,6 +429,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.nextChapterPrefetched = false; this.nextChapterPrefetched = false;
this.pageNum = 0; this.pageNum = 0;
this.pagingDirection = PAGING_DIRECTION.FORWARD; this.pagingDirection = PAGING_DIRECTION.FORWARD;
this.inSetup = true;
if (this.goToPageEvent) {
// There was a bug where goToPage was emitting old values into infinite scroller between chapter loads. We explicity clear it out between loads
// and we use a BehaviourSubject to ensure only latest value is sent
this.goToPageEvent.complete();
}
forkJoin({ forkJoin({
progress: this.readerService.getProgress(this.chapterId), progress: this.readerService.getProgress(this.chapterId),
@ -445,6 +457,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
page = this.maxPages - 1; page = this.maxPages - 1;
} }
this.setPageNum(page); this.setPageNum(page);
this.goToPageEvent = new BehaviorSubject<number>(this.pageNum);
@ -453,11 +467,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
newOptions.ceil = this.maxPages - 1; // We -1 so that the slider UI shows us hitting the end, since visually we +1 everything. newOptions.ceil = this.maxPages - 1; // We -1 so that the slider UI shows us hitting the end, since visually we +1 everything.
this.pageOptions = newOptions; this.pageOptions = newOptions;
// TODO: Move this into ChapterInfo
this.libraryService.getLibraryType(results.chapterInfo.libraryId).pipe(take(1)).subscribe(type => { this.libraryService.getLibraryType(results.chapterInfo.libraryId).pipe(take(1)).subscribe(type => {
this.libraryType = type; this.libraryType = type;
this.updateTitle(results.chapterInfo, type); this.updateTitle(results.chapterInfo, type);
}); });
this.inSetup = false;
// From bookmarks, create map of pages to make lookup time O(1) // From bookmarks, create map of pages to make lookup time O(1)
@ -1019,7 +1036,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.setPageNum(page); this.setPageNum(page);
this.refreshSlider.emit(); this.refreshSlider.emit();
this.goToPageEvent.next(page); this.goToPageEvent.next(page);
this.render(); this.render();
} }

View File

@ -128,7 +128,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.actionInProgress = false; this.actionInProgress = false;
this.bulkSelectionService.deselectAll(); this.bulkSelectionService.deselectAll();
}); });
break; break;
case Action.MarkAsUnread: case Action.MarkAsUnread:
this.actionService.markMultipleAsUnread(seriesId, selectedVolumeIds, chapters, () => { this.actionService.markMultipleAsUnread(seriesId, selectedVolumeIds, chapters, () => {
@ -167,7 +167,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService, private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
private confirmService: ConfirmService, private titleService: Title, private confirmService: ConfirmService, private titleService: Title,
private downloadService: DownloadService, private actionService: ActionService, private downloadService: DownloadService, private actionService: ActionService,
public imageSerivce: ImageService, private messageHub: MessageHubService, public imageSerivce: ImageService, private messageHub: MessageHubService,
) { ) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.accountService.currentUser$.pipe(take(1)).subscribe(user => { this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
@ -203,7 +203,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
} else if (event.event === EVENTS.ScanSeries) { } else if (event.event === EVENTS.ScanSeries) {
const seriesCoverUpdatedEvent = event.payload as ScanSeriesEvent; const seriesCoverUpdatedEvent = event.payload as ScanSeriesEvent;
if (seriesCoverUpdatedEvent.seriesId === this.series.id) { if (seriesCoverUpdatedEvent.seriesId === this.series.id) {
console.log('ScanSeries called')
this.seriesService.getMetadata(this.series.id).pipe(take(1)).subscribe(metadata => { this.seriesService.getMetadata(this.series.id).pipe(take(1)).subscribe(metadata => {
this.seriesMetadata = metadata; this.seriesMetadata = metadata;
this.createHTML(); this.createHTML();
@ -361,13 +360,13 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.seriesService.getVolumes(this.series.id).subscribe(volumes => { this.seriesService.getVolumes(this.series.id).subscribe(volumes => {
this.volumes = volumes; // volumes are already be sorted in the backend this.volumes = volumes; // volumes are already be sorted in the backend
const vol0 = this.volumes.filter(v => v.number === 0); const vol0 = this.volumes.filter(v => v.number === 0);
this.storyChapters = vol0.map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters); this.storyChapters = vol0.map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters);
this.chapters = volumes.map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters).filter(c => !c.isSpecial || isNaN(parseInt(c.range, 10))); this.chapters = volumes.map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters).filter(c => !c.isSpecial || isNaN(parseInt(c.range, 10)));
this.setContinuePoint(); this.setContinuePoint();
const specials = this.storyChapters.filter(c => c.isSpecial || isNaN(parseInt(c.range, 10))); const specials = this.storyChapters.filter(c => c.isSpecial || isNaN(parseInt(c.range, 10)));
this.hasSpecials = specials.length > 0 this.hasSpecials = specials.length > 0
if (this.hasSpecials) { if (this.hasSpecials) {
@ -390,7 +389,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
/** /**
* This will update the selected tab * This will update the selected tab
* *
* This assumes loadPage() has already primed all the calculations and state variables. Do not call directly. * This assumes loadPage() has already primed all the calculations and state variables. Do not call directly.
*/ */
updateSelectedTab() { updateSelectedTab() {
@ -402,11 +401,11 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
} }
// This shows Volumes tab // This shows Volumes tab
if (this.volumes.filter(v => v.number !== 0).length !== 0) { if (this.volumes.filter(v => v.number !== 0).length !== 0) {
this.hasNonSpecialVolumeChapters = true; this.hasNonSpecialVolumeChapters = true;
} }
// If an update occured and we were on specials, re-activate Volumes/Chapters // If an update occured and we were on specials, re-activate Volumes/Chapters
if (!this.hasSpecials && !this.hasNonSpecialVolumeChapters && this.activeTabId != TabID.Storyline) { if (!this.hasSpecials && !this.hasNonSpecialVolumeChapters && this.activeTabId != TabID.Storyline) {
this.activeTabId = TabID.Storyline; this.activeTabId = TabID.Storyline;
} }
@ -455,7 +454,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
if (this.series === undefined) { if (this.series === undefined) {
return; return;
} }
this.actionService.markChapterAsRead(this.series.id, chapter, () => { this.actionService.markChapterAsRead(this.series.id, chapter, () => {
this.setContinuePoint(); this.setContinuePoint();
this.actionInProgress = false; this.actionInProgress = false;
@ -505,9 +504,9 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.toastr.error('There are no chapters to this volume. Cannot read.'); this.toastr.error('There are no chapters to this volume. Cannot read.');
return; return;
} }
// NOTE: When selecting a volume, we might want to ask the user which chapter they want or an "Automatic" option. For Volumes // NOTE: When selecting a volume, we might want to ask the user which chapter they want or an "Automatic" option. For Volumes
// made up of lots of chapter files, it makes it more versitile. The modal can have pages read / pages with colored background // made up of lots of chapter files, it makes it more versitile. The modal can have pages read / pages with colored background
// to help the user make a good choice. // to help the user make a good choice.
// If user has progress on the volume, load them where they left off // If user has progress on the volume, load them where they left off
if (volume.pagesRead < volume.pages && volume.pagesRead > 0) { if (volume.pagesRead < volume.pages && volume.pagesRead > 0) {