Manga Reader Work (#1729)
* Instead of augmenting prefetcher to move across chapter bounds, let's try to instead just load 5 images (which the browser will cache) from next/prev so when it loads, it's much faster. * Trialing loading next/prev chapters 5 pages to have better next page loading experience. * Tweaked GetChapterInfo API to actually apply conditional includeDimensions parameter. * added a basic language file for upcoming work * Moved the bottom menu up a bit for iOS devices with handlebars. * Fixed fit to width on phones still having a horizontal scrollbar * Fixed a bug where there is extra space under the image when fit to width and on a phone due to pagination going to far. * Changed which variable we use for right pagination calculation * Fixing fit to height - Fixing height calc to account for horizontal scroll bar height. * Added a comment for the height scrollbar fix * Adding screenfull package # Added: - Added screenfull package to handle cross-platform browser fullscreen code # Removed: - Removed custom fullscreen code * Fixed a bug where switching from webtoon reader to other layout modes wouldn't render anything. Webtoon continuous scroll down is now broken. * Fixed it back to how it was and all is good. Need to call detectChanges explicitly. * Removed an additional undeeded save progress call on loadPage * Laid out the test case to move the page snapping to the backend with full unit tests. Current code is broken just like UI layer. * Refactored the snap points into the backend and ensure that it works correctly. * Fixed a broken unit test * Filter out spammy hubs/messages calls in the logs * Swallow all noisy messages that are from RequestLoggingMiddleware when the log level is on Information or above. * Added a common loading component to the app. Have yet to refactor all screens to use this. * Bump json5 from 2.2.0 to 2.2.3 in /UI/Web Bumps [json5](https://github.com/json5/json5) from 2.2.0 to 2.2.3. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v2.2.0...v2.2.3) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Alrigned all the loading messages and styles throughout the app * Webtoon reader will use max width of all images to ensure images align well. * On Original scaling mode, users can use the keyboard to scroll around the images without pagination kicking off. * Removed console logs * Fixed a public vs private issue * Fixed an issue around some cached files getting locked due to NetVips holding them during file size calculations. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
parent
22442d745c
commit
2464a30bc2
37 changed files with 367 additions and 390 deletions
|
@ -7,13 +7,12 @@ img {
|
|||
align-items: center;
|
||||
|
||||
&.full-width {
|
||||
width: 100vw;
|
||||
height: calc(var(--vh)*100);
|
||||
display: grid;
|
||||
}
|
||||
|
||||
&.full-height {
|
||||
height: 100vh;
|
||||
height: calc(100vh - 34px); // 34px is the height of the horizontal scrollbar that will appear
|
||||
display: flex; // changed from inline-block to fix the centering on tablets not working
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { SeriesFilter } from '../_models/metadata/series-filter';
|
|||
import { UtilityService } from '../shared/_services/utility.service';
|
||||
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||
import { FileDimension } from '../manga-reader/_models/file-dimension';
|
||||
import screenfull from 'screenfull';
|
||||
|
||||
export const CHAPTER_ID_DOESNT_EXIST = -1;
|
||||
export const CHAPTER_ID_NOT_FETCHED = -2;
|
||||
|
@ -235,25 +236,10 @@ export class ReaderService {
|
|||
return params;
|
||||
}
|
||||
|
||||
enterFullscreen(el: Element, callback?: VoidFunction) {
|
||||
if (!document.fullscreenElement) {
|
||||
if (el.requestFullscreen) {
|
||||
el.requestFullscreen().then(() => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
toggleFullscreen(el: Element, callback?: VoidFunction) {
|
||||
|
||||
exitFullscreen(callback?: VoidFunction) {
|
||||
if (document.exitFullscreen && this.checkFullscreenMode()) {
|
||||
document.exitFullscreen().then(() => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="list-group" *ngIf="!isLoading">
|
||||
<div class="list-group">
|
||||
<div class="form-check">
|
||||
<input id="selectall" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Member } from 'src/app/_models/auth/member';
|
|||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { SelectionModel } from 'src/app/typeahead/_components/typeahead.component';
|
||||
|
||||
// TODO: Change to OnPush
|
||||
@Component({
|
||||
selector: 'app-library-access-modal',
|
||||
templateUrl: './library-access-modal.component.html',
|
||||
|
@ -17,7 +18,6 @@ export class LibraryAccessModalComponent implements OnInit {
|
|||
selectedLibraries: Array<{selected: boolean, data: Library}> = [];
|
||||
selections!: SelectionModel<Library>;
|
||||
selectAll: boolean = false;
|
||||
isLoading: boolean = false;
|
||||
|
||||
get hasSomeSelected() {
|
||||
return this.selections != null && this.selections.hasSomeSelected();
|
||||
|
@ -49,7 +49,6 @@ export class LibraryAccessModalComponent implements OnInit {
|
|||
|
||||
setupSelections() {
|
||||
this.selections = new SelectionModel<Library>(false, this.allLibraries);
|
||||
this.isLoading = false;
|
||||
|
||||
// If a member is passed in, then auto-select their libraries
|
||||
if (this.member !== undefined) {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
</li>
|
||||
<li *ngIf="loading" class="list-group-item">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -21,6 +21,4 @@
|
|||
</div>
|
||||
|
||||
|
||||
<div class="spinner-border text-secondary" *ngIf="isLoading" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
|
|
|
@ -473,7 +473,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
this.navService.showNavBar();
|
||||
this.navService.showSideNav();
|
||||
this.readerService.exitFullscreen();
|
||||
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
|
@ -1203,13 +1202,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
toggleFullscreen() {
|
||||
this.isFullscreen = this.readerService.checkFullscreenMode();
|
||||
if (this.isFullscreen) {
|
||||
this.readerService.exitFullscreen(() => {
|
||||
this.readerService.toggleFullscreen(this.reader.nativeElement, () => {
|
||||
this.isFullscreen = false;
|
||||
this.cdRef.markForCheck();
|
||||
this.renderer.removeStyle(this.reader.nativeElement, 'background');
|
||||
});
|
||||
} else {
|
||||
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
|
||||
this.readerService.toggleFullscreen(this.reader.nativeElement, () => {
|
||||
this.isFullscreen = true;
|
||||
this.cdRef.markForCheck();
|
||||
// HACK: This is a bug with how browsers change the background color for fullscreen mode
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</li>
|
||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No collections created yet</li>
|
||||
<li class="list-group-item" *ngIf="loading">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -47,11 +47,7 @@
|
|||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="mx-auto" *ngIf="isLoading" style="width: 200px;">
|
||||
<div class="spinner-border text-secondary loading" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<app-loading [loading]="true"></app-loading>
|
||||
|
||||
<ng-template #jumpBar>
|
||||
<div class="jump-bar">
|
||||
|
|
|
@ -6,6 +6,7 @@ import { ScrollService } from 'src/app/_services/scroll.service';
|
|||
import { ReaderService } from '../../../_services/reader.service';
|
||||
import { PAGING_DIRECTION } from '../../_models/reader-enums';
|
||||
import { WebtoonImage } from '../../_models/webtoon-image';
|
||||
import { ManagaReaderService } from '../../_series/managa-reader.service';
|
||||
|
||||
/**
|
||||
* How much additional space should pass, past the original bottom of the document height before we trigger the next chapter load
|
||||
|
@ -129,7 +130,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
/**
|
||||
* 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.Logs;
|
||||
/**
|
||||
* Debug mode. Will filter out any messages in here so they don't hit the log
|
||||
*/
|
||||
|
@ -153,7 +154,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
|
||||
constructor(private readerService: ReaderService, private renderer: Renderer2,
|
||||
@Inject(DOCUMENT) private document: Document, private scrollService: ScrollService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService) {
|
||||
// This will always exist at this point in time since this is used within manga reader
|
||||
const reader = document.querySelector('.reading-area');
|
||||
if (reader !== null) {
|
||||
|
@ -453,7 +454,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.webtoonImageWidth = event.target.width;
|
||||
}
|
||||
|
||||
this.renderer.setAttribute(event.target, 'width', this.webtoonImageWidth + '');
|
||||
this.renderer.setAttribute(event.target, 'width', this.mangaReaderService.maxWidth() + '');
|
||||
this.renderer.setAttribute(event.target, 'height', event.target.height + '');
|
||||
|
||||
this.attachIntersectionObserverElem(event.target);
|
||||
|
|
|
@ -26,11 +26,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <ng-container *ngIf="isLoading">
|
||||
<div class="spinner-border text-secondary loading" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</ng-container> -->
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
<div class="reading-area"
|
||||
appSwipe (swipeEvent)="onSwipeEvent($event)"
|
||||
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
|
||||
|
@ -57,7 +53,7 @@
|
|||
<div class="{{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')"
|
||||
[ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? ImageHeight: '25%'),
|
||||
'left': 'inherit',
|
||||
'right': rightPaginationOffset + 'px'}">
|
||||
'right': RightPaginationOffset + 'px'}">
|
||||
<div *ngIf="showClickOverlay">
|
||||
<i class="fa fa-angle-{{readingDirection === ReadingDirection.LeftToRight ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'right' : 'down'}}"
|
||||
title="Next Page" aria-hidden="true"></i>
|
||||
|
@ -93,7 +89,7 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-template #webtoon>
|
||||
<div class="webtoon-images" *ngIf="readerMode === ReaderMode.Webtoon && !isLoading && !inSetup">
|
||||
<div class="webtoon-images" *ngIf="!isLoading && !inSetup">
|
||||
<app-infinite-scroller [pageNum]="pageNum"
|
||||
[bufferPages]="5"
|
||||
[goToPage]="goToPageEvent"
|
||||
|
@ -130,7 +126,7 @@
|
|||
<button class="btn btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter();resetMenuCloseTimer();" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-4 ms-2 me-2">
|
||||
<div class="row pt-4 ms-2 me-2 mb-2">
|
||||
<div class="col">
|
||||
<button class="btn btn-icon" (click)="setReadingDirection();resetMenuCloseTimer();" [disabled]="readerMode === ReaderMode.Webtoon || readerMode === ReaderMode.UpDown" aria-describedby="reading-direction" title="Reading Direction: {{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}">
|
||||
<i class="fa fa-angle-double-{{readingDirection === ReadingDirection.LeftToRight ? 'right' : 'left'}}" aria-hidden="true"></i>
|
||||
|
|
|
@ -176,6 +176,7 @@ $pointer-offset: 5px;
|
|||
top: 0px;
|
||||
width: $side-width;
|
||||
background: $pagination-bg;
|
||||
max-height: calc(var(--vh)*100);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
@ -194,6 +195,7 @@ $pointer-offset: 5px;
|
|||
top: 0px;
|
||||
width: $side-width;
|
||||
background: $pagination-bg;
|
||||
max-height: calc(var(--vh)*100);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BehaviorSubject, debounceTime, distinctUntilChanged, forkJoin, fromEvent, map, merge, Observable, ReplaySubject, Subject, take, takeUntil, tap } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, debounceTime, distinctUntilChanged, filter, forkJoin, fromEvent, map, merge, Observable, of, ReplaySubject, Subject, take, takeUntil, tap } from 'rxjs';
|
||||
import { LabelType, ChangeContext, Options } from '@angular-slider/ngx-slider';
|
||||
import { trigger, state, style, transition, animate } from '@angular/animations';
|
||||
import { FormGroup, FormBuilder, FormControl } from '@angular/forms';
|
||||
|
@ -30,6 +30,7 @@ import { CanvasRendererComponent } from '../canvas-renderer/canvas-renderer.comp
|
|||
import { DoubleRendererComponent } from '../double-renderer/double-renderer.component';
|
||||
import { DoubleReverseRendererComponent } from '../double-reverse-renderer/double-reverse-renderer.component';
|
||||
import { SingleRendererComponent } from '../single-renderer/single-renderer.component';
|
||||
import { ChapterInfo } from '../../_models/chapter-info';
|
||||
|
||||
|
||||
const PREFETCH_PAGES = 10;
|
||||
|
@ -41,6 +42,19 @@ const ANIMATION_SPEED = 200;
|
|||
const OVERLAY_AUTO_CLOSE_TIME = 3000;
|
||||
const CLICK_OVERLAY_TIMEOUT = 3000;
|
||||
|
||||
enum ChapterInfoPosition {
|
||||
Previous = 0,
|
||||
Current = 1,
|
||||
Next = 2
|
||||
}
|
||||
|
||||
enum KeyDirection {
|
||||
Right = 0,
|
||||
Left = 1,
|
||||
Up = 2,
|
||||
Down = 3
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-manga-reader',
|
||||
templateUrl: './manga-reader.component.html',
|
||||
|
@ -138,6 +152,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
isLoading = true;
|
||||
hasBookmarkRights: boolean = false; // TODO: This can be an observable
|
||||
|
||||
|
||||
getPageFn!: (pageNum: number) => HTMLImageElement;
|
||||
|
||||
|
@ -165,6 +180,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
*/
|
||||
continuousChaptersStack: Stack<number> = new Stack();
|
||||
|
||||
continuousChapterInfos: Array<ChapterInfo | undefined> = [undefined, undefined, undefined];
|
||||
|
||||
/**
|
||||
* An event emitter when a page change occurs. Used solely by the webtoon reader.
|
||||
*/
|
||||
|
@ -511,7 +528,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
this.showBookmarkEffectEvent.complete();
|
||||
this.readerService.exitFullscreen();
|
||||
if (this.goToPageEvent !== undefined) this.goToPageEvent.complete();
|
||||
}
|
||||
|
||||
|
@ -526,14 +542,22 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
switch (this.readerMode) {
|
||||
case ReaderMode.LeftRight:
|
||||
if (event.key === KEY_CODES.RIGHT_ARROW) {
|
||||
//if (!this.checkIfPaginationAllowed()) return;
|
||||
if (!this.checkIfPaginationAllowed(KeyDirection.Right)) return;
|
||||
this.readingDirection === ReadingDirection.LeftToRight ? this.nextPage() : this.prevPage();
|
||||
} else if (event.key === KEY_CODES.LEFT_ARROW) {
|
||||
//if (!this.checkIfPaginationAllowed()) return;
|
||||
if (!this.checkIfPaginationAllowed(KeyDirection.Left)) return;
|
||||
this.readingDirection === ReadingDirection.LeftToRight ? this.prevPage() : this.nextPage();
|
||||
}
|
||||
break;
|
||||
case ReaderMode.UpDown:
|
||||
if (event.key === KEY_CODES.UP_ARROW) {
|
||||
if (!this.checkIfPaginationAllowed(KeyDirection.Up)) return;
|
||||
this.prevPage();
|
||||
} else if (event.key === KEY_CODES.DOWN_ARROW) {
|
||||
if (!this.checkIfPaginationAllowed(KeyDirection.Down)) return;
|
||||
this.nextPage();
|
||||
}
|
||||
break;
|
||||
case ReaderMode.Webtoon:
|
||||
if (event.key === KEY_CODES.DOWN_ARROW) {
|
||||
this.nextPage()
|
||||
|
@ -624,17 +648,36 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
// if there is scroll room and on original, then don't paginate
|
||||
checkIfPaginationAllowed() {
|
||||
checkIfPaginationAllowed(direction: KeyDirection) {
|
||||
// This is not used atm due to the complexity it adds with keyboard.
|
||||
if (this.readingArea === undefined || this.readingArea.nativeElement === undefined) return true;
|
||||
|
||||
const scrollLeft = this.readingArea?.nativeElement?.scrollLeft || 0;
|
||||
const totalScrollWidth = this.readingArea?.nativeElement?.scrollWidth;
|
||||
// need to also check if there is scroll needed
|
||||
const scrollTop = this.readingArea?.nativeElement?.scrollTop || 0;
|
||||
|
||||
if (this.FittingOption === FITTING_OPTION.ORIGINAL && scrollLeft < totalScrollWidth) {
|
||||
return false;
|
||||
switch (direction) {
|
||||
case KeyDirection.Right:
|
||||
if (this.FittingOption === FITTING_OPTION.ORIGINAL && scrollLeft < this.readingArea?.nativeElement.scrollWidth - this.readingArea?.nativeElement.clientWidth) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case KeyDirection.Left:
|
||||
if (this.FittingOption === FITTING_OPTION.ORIGINAL && scrollLeft > 0) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case KeyDirection.Up:
|
||||
if (this.FittingOption === FITTING_OPTION.ORIGINAL && scrollTop > 0) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case KeyDirection.Down:
|
||||
if (this.FittingOption === FITTING_OPTION.ORIGINAL && scrollTop < this.readingArea?.nativeElement.scrollHeight - this.readingArea?.nativeElement.clientHeight) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -712,8 +755,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
this.mangaReaderService.loadPageDimensions(results.chapterInfo.pageDimensions);
|
||||
this.mangaReaderService.load(results.chapterInfo);
|
||||
|
||||
this.continuousChapterInfos[ChapterInfoPosition.Current] = results.chapterInfo;
|
||||
this.volumeId = results.chapterInfo.volumeId;
|
||||
this.maxPages = results.chapterInfo.pages;
|
||||
let page = results.progress.pageNum;
|
||||
|
@ -755,6 +799,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
} else {
|
||||
// Fetch the first page of next chapter
|
||||
this.getPage(0, this.nextChapterId);
|
||||
|
||||
}
|
||||
});
|
||||
this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
|
||||
|
@ -984,6 +1029,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
renderPage() {
|
||||
const page = [this.canvasImage];
|
||||
|
||||
// After switching from webtoon mode, these are all undefined
|
||||
|
||||
this.canvasRenderer?.renderPage(page);
|
||||
this.singleRenderer?.renderPage(page);
|
||||
this.doubleRenderer?.renderPage(page);
|
||||
|
@ -1027,28 +1075,24 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
prefetch() {
|
||||
// NOTE: This doesn't allow for any directionality
|
||||
// NOTE: This doesn't maintain 1 image behind at all times
|
||||
// NOTE: I may want to provide a different prefetcher for double renderer
|
||||
for(let i = 0; i <= PREFETCH_PAGES - 3; i++) {
|
||||
const numOffset = this.pageNum + i;
|
||||
//console.log('numOffset: ', numOffset);
|
||||
if (numOffset > this.maxPages - 1) continue;
|
||||
let numOffset = this.pageNum + i;
|
||||
|
||||
if (numOffset > this.maxPages - 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = (numOffset % this.cachedImages.length + this.cachedImages.length) % this.cachedImages.length;
|
||||
const cachedImagePageNum = this.readerService.imageUrlToPageNum(this.cachedImages[index].src);
|
||||
const cachedImageChapterId = this.readerService.imageUrlToChapterId(this.cachedImages[index].src);
|
||||
//console.log('chapter id for ', cachedImagePageNum, ' = ', cachedImageChapterId)
|
||||
if (cachedImagePageNum !== numOffset) { // && cachedImageChapterId === this.chapterId
|
||||
if (cachedImagePageNum !== numOffset) {
|
||||
this.cachedImages[index] = new Image();
|
||||
this.cachedImages[index].src = this.getPageUrl(numOffset);
|
||||
}
|
||||
}
|
||||
|
||||
const pages = this.cachedImages.map(img => this.readerService.imageUrlToPageNum(img.src));
|
||||
const pagesBefore = pages.filter(p => p >= 0 && p < this.pageNum).length;
|
||||
const pagesAfter = pages.filter(p => p >= 0 && p > this.pageNum).length;
|
||||
//console.log('Buffer Health: Before: ', pagesBefore, ' After: ', pagesAfter);
|
||||
// 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) return '[' + p + ']';
|
||||
// if (this.pageNum === p[1]) return '[' + p + ']';
|
||||
// return '' + p
|
||||
// }));
|
||||
}
|
||||
|
@ -1061,7 +1105,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
if (this.readerMode === ReaderMode.Webtoon) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.setPageNum(this.pageNum);
|
||||
this.setCanvasImage();
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
@ -1094,6 +1137,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
// This will update the value for value except when in webtoon due to how the webtoon reader
|
||||
// responds to page changes
|
||||
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||
console.log('Setting Page Number as slider drag occured');
|
||||
this.setPageNum(context.value);
|
||||
}
|
||||
}
|
||||
|
@ -1107,6 +1151,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS);
|
||||
}
|
||||
|
||||
console.log('Setting Page Number as slider page update occurred');
|
||||
this.setPageNum(this.adjustPagesForDoubleRenderer(page));
|
||||
this.refreshSlider.emit();
|
||||
this.goToPageEvent.next(this.pageNum);
|
||||
|
@ -1122,13 +1167,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
// Tell server to cache the next chapter
|
||||
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) {
|
||||
this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => {
|
||||
this.continuousChapterInfos[ChapterInfoPosition.Next] = res;
|
||||
this.nextChapterPrefetched = true;
|
||||
this.prefetchStartOfChapter(this.nextChapterId, PAGING_DIRECTION.FORWARD);
|
||||
});
|
||||
}
|
||||
} else if (this.pageNum <= 10) {
|
||||
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) {
|
||||
this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => {
|
||||
this.continuousChapterInfos[ChapterInfoPosition.Previous] = res;
|
||||
this.prevChapterPrefetched = true;
|
||||
this.prefetchStartOfChapter(this.nextChapterId, PAGING_DIRECTION.BACKWARDS);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1144,6 +1193,30 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the first 5 images (throwaway cache) from the given chapterId
|
||||
* @param chapterId
|
||||
* @param direction Used to indicate if the chapter is behind or ahead of curent chapter
|
||||
*/
|
||||
prefetchStartOfChapter(chapterId: number, direction: PAGING_DIRECTION) {
|
||||
let pages = [];
|
||||
|
||||
if (direction === PAGING_DIRECTION.BACKWARDS) {
|
||||
if (this.continuousChapterInfos[ChapterInfoPosition.Previous] === undefined) return;
|
||||
const n = this.continuousChapterInfos[ChapterInfoPosition.Previous]!.pages;
|
||||
pages = Array.from({length: n + 1}, (v, k) => n - k);
|
||||
} else {
|
||||
pages = [0, 1, 2, 3, 4];
|
||||
}
|
||||
|
||||
let images = [];
|
||||
pages.forEach((_, i: number) => {
|
||||
let img = new Image();
|
||||
img.src = this.getPageUrl(i, chapterId);
|
||||
images.push(img)
|
||||
});
|
||||
}
|
||||
|
||||
goToPage(pageNum: number) {
|
||||
let page = pageNum;
|
||||
|
||||
|
@ -1165,6 +1238,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS);
|
||||
}
|
||||
|
||||
console.log('Setting Page Number as goto page');
|
||||
this.setPageNum(this.adjustPagesForDoubleRenderer(page));
|
||||
this.goToPageEvent.next(page);
|
||||
this.render();
|
||||
|
@ -1179,20 +1253,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
// This is menu only code
|
||||
toggleFullscreen() {
|
||||
this.isFullscreen = this.readerService.checkFullscreenMode();
|
||||
if (this.isFullscreen) {
|
||||
this.readerService.exitFullscreen(() => {
|
||||
this.isFullscreen = false;
|
||||
this.fullscreenEvent.next(false);
|
||||
this.render();
|
||||
});
|
||||
} else {
|
||||
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
|
||||
this.readerService.toggleFullscreen(this.reader.nativeElement, () => {
|
||||
this.isFullscreen = true;
|
||||
this.fullscreenEvent.next(true);
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This is menu only code
|
||||
|
@ -1212,10 +1277,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
// We must set this here because loadPage from render doesn't call if we aren't page splitting
|
||||
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||
this.canvasImage = this.cachedImages[this.pageNum & this.cachedImages.length];
|
||||
this.canvasImage = this.getPage(this.pageNum);
|
||||
this.currentImage.next(this.canvasImage);
|
||||
this.pageNumSubject.next({pageNum: this.pageNum, maxPages: this.maxPages});
|
||||
this.isLoading = true;
|
||||
//this.isLoading = true;
|
||||
this.cdRef.detectChanges(); // Must use detectChanges to ensure ViewChildren get updated again
|
||||
}
|
||||
|
||||
this.updateForm();
|
||||
|
@ -1243,6 +1309,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
handleWebtoonPageChange(updatedPageNum: number) {
|
||||
console.log('Setting Page Number as webtoon page changed');
|
||||
this.setPageNum(updatedPageNum);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,5 +20,9 @@ export interface ChapterInfo {
|
|||
/**
|
||||
* This will not always be present. Depends on if asked from backend.
|
||||
*/
|
||||
pageDimensions: Array<FileDimension>;
|
||||
pageDimensions?: Array<FileDimension>;
|
||||
/**
|
||||
* This will not always be present. Depends on if asked from backend.
|
||||
*/
|
||||
doublePairs?: {[key: number]: number};
|
||||
}
|
|
@ -2,6 +2,7 @@ export interface FileDimension {
|
|||
pageNumber: number;
|
||||
width: number;
|
||||
height: number;
|
||||
isWide: boolean;
|
||||
}
|
||||
|
||||
export type DimensionMap = {[key: number]: {width: number, height: number, isWide: boolean}};
|
|
@ -2,6 +2,7 @@ import { ElementRef, Injectable, Renderer2, RendererFactory2 } from '@angular/co
|
|||
import { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
|
||||
import { ScalingOption } from 'src/app/_models/preferences/scaling-option';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
import { ChapterInfo } from '../_models/chapter-info';
|
||||
import { DimensionMap, FileDimension } from '../_models/file-dimension';
|
||||
import { FITTING_OPTION } from '../_models/reader-enums';
|
||||
|
||||
|
@ -13,41 +14,22 @@ export class ManagaReaderService {
|
|||
private pageDimensions: DimensionMap = {};
|
||||
private pairs: {[key: number]: number} = {};
|
||||
private renderer: Renderer2;
|
||||
|
||||
constructor(rendererFactory: RendererFactory2, private readerService: ReaderService) {
|
||||
this.renderer = rendererFactory.createRenderer(null, null);
|
||||
}
|
||||
|
||||
loadPageDimensions(dims: Array<FileDimension>) {
|
||||
this.pageDimensions = {};
|
||||
let counter = 0;
|
||||
let i = 0;
|
||||
|
||||
dims.forEach(d => {
|
||||
const isWide = (d.width > d.height);
|
||||
load(chapterInfo: ChapterInfo) {
|
||||
chapterInfo.pageDimensions!.forEach(d => {
|
||||
this.pageDimensions[d.pageNumber] = {
|
||||
height: d.height,
|
||||
width: d.width,
|
||||
isWide: isWide
|
||||
isWide: d.isWide
|
||||
};
|
||||
|
||||
//console.log('Page Number: ', d.pageNumber);
|
||||
|
||||
if (isWide) {
|
||||
console.log('\tPage is wide, counter: ', counter, 'i: ', i);
|
||||
this.pairs[d.pageNumber] = d.pageNumber;
|
||||
//this.pairs[d.pageNumber] = this.pairs[d.pageNumber - 1] + 1;
|
||||
} else {
|
||||
//console.log('\tPage is single, counter: ', counter, 'i: ', i);
|
||||
this.pairs[d.pageNumber] = counter % 2 === 0 ? Math.max(i - 1, 0) : counter;
|
||||
counter++;
|
||||
}
|
||||
//console.log('\t\tMapped to ', this.pairs[d.pageNumber]);
|
||||
|
||||
i++;
|
||||
});
|
||||
//console.log('pairs: ', this.pairs);
|
||||
this.pairs = chapterInfo.doublePairs!;
|
||||
}
|
||||
|
||||
|
||||
adjustForDoubleReader(page: number) {
|
||||
if (!this.pairs.hasOwnProperty(page)) return page;
|
||||
return this.pairs[page];
|
||||
|
@ -67,6 +49,14 @@ export class ManagaReaderService {
|
|||
return this.pageDimensions[pageNum].isWide;
|
||||
}
|
||||
|
||||
maxHeight() {
|
||||
return Object.values(this.pageDimensions).reduce((max, obj) => Math.max(max, obj.height), 0);
|
||||
}
|
||||
|
||||
maxWidth() {
|
||||
return Object.values(this.pageDimensions).reduce((max, obj) => Math.max(max, obj.width), 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -101,7 +101,6 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.navService.showNavBar();
|
||||
this.navService.showSideNav();
|
||||
this.readerService.exitFullscreen();
|
||||
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
|
|
|
@ -59,11 +59,7 @@
|
|||
<ng-container *ngIf="items.length === 0 && !isLoading">
|
||||
Nothing added
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isLoading">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
</div>
|
||||
|
||||
<!-- TODO: This needs virtualization -->
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</li>
|
||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No lists created yet</li>
|
||||
<li class="list-group-item" *ngIf="loading">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -308,9 +308,5 @@
|
|||
<div [ngbNavOutlet]="nav"></div>
|
||||
</ng-container>
|
||||
|
||||
<div class="mx-auto" *ngIf="isLoading" style="width: 200px;">
|
||||
<div class="spinner-border text-secondary loading" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
</div>
|
||||
|
|
7
UI/Web/src/app/shared/loading/loading.component.html
Normal file
7
UI/Web/src/app/shared/loading/loading.component.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<ng-container *ngIf="loading">
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
0
UI/Web/src/app/shared/loading/loading.component.scss
Normal file
0
UI/Web/src/app/shared/loading/loading.component.scss
Normal file
19
UI/Web/src/app/shared/loading/loading.component.ts
Normal file
19
UI/Web/src/app/shared/loading/loading.component.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-loading',
|
||||
templateUrl: './loading.component.html',
|
||||
styleUrls: ['./loading.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LoadingComponent implements OnInit {
|
||||
|
||||
@Input() loading: boolean = false;
|
||||
@Input() message: string = '';
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
|
@ -17,6 +17,7 @@ import { BadgeExpanderComponent } from './badge-expander/badge-expander.componen
|
|||
import { ImageComponent } from './image/image.component';
|
||||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { IconAndTitleComponent } from './icon-and-title/icon-and-title.component';
|
||||
import { LoadingComponent } from './loading/loading.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -32,6 +33,7 @@ import { IconAndTitleComponent } from './icon-and-title/icon-and-title.component
|
|||
BadgeExpanderComponent,
|
||||
ImageComponent,
|
||||
IconAndTitleComponent,
|
||||
LoadingComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -54,6 +56,8 @@ import { IconAndTitleComponent } from './icon-and-title/icon-and-title.component
|
|||
PersonBadgeComponent, // Used Series Detail
|
||||
BadgeExpanderComponent, // Used Series Detail/Metadata
|
||||
IconAndTitleComponent, // Used in Series Detail/Metadata
|
||||
|
||||
LoadingComponent
|
||||
],
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
|
3
UI/Web/src/assets/langs/en.json
Normal file
3
UI/Web/src/assets/langs/en.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"login": "Test"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue