diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/API/Services/Tasks/Scanner/LibraryWatcher.cs index 183793457..6cfafa575 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/API/Services/Tasks/Scanner/LibraryWatcher.cs @@ -236,17 +236,17 @@ public class LibraryWatcher : ILibraryWatcher private string GetFolder(string filePath, IEnumerable libraryFolders) { var parentDirectory = _directoryService.GetParentDirectoryName(filePath); - _logger.LogDebug("[LibraryWatcher] Parent Directory: {ParentDirectory}", parentDirectory); + _logger.LogTrace("[LibraryWatcher] Parent Directory: {ParentDirectory}", parentDirectory); if (string.IsNullOrEmpty(parentDirectory)) return string.Empty; // We need to find the library this creation belongs to // Multiple libraries can point to the same base folder. In this case, we need use FirstOrDefault var libraryFolder = libraryFolders.FirstOrDefault(f => parentDirectory.Contains(f)); - _logger.LogDebug("[LibraryWatcher] Library Folder: {LibraryFolder}", libraryFolder); + _logger.LogTrace("[LibraryWatcher] Library Folder: {LibraryFolder}", libraryFolder); if (string.IsNullOrEmpty(libraryFolder)) return string.Empty; var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList(); - _logger.LogDebug("[LibraryWatcher] Root Folders: {RootFolders}", rootFolder); + _logger.LogTrace("[LibraryWatcher] Root Folders: {RootFolders}", rootFolder); if (!rootFolder.Any()) return string.Empty; // Select the first folder and join with library folder, this should give us the folder to scan. diff --git a/UI/Web/src/_manga-reader-common.scss b/UI/Web/src/_manga-reader-common.scss new file mode 100644 index 000000000..e6ff3aac8 --- /dev/null +++ b/UI/Web/src/_manga-reader-common.scss @@ -0,0 +1,73 @@ +img { + user-select: none; +} + +.image-container { + text-align: center; + align-items: center; + + &.full-width { + width: 100vw; + height: calc(var(--vh)*100); + display: grid; + } + + &.full-height { + height: 100vh; + display: inline-block; + } + + &.original { + height: 100vh; + display: grid; + } + + .full-height { + width: auto; + margin: 0 auto; + max-height: calc(var(--vh)*100); + vertical-align: top; + &.wide { + height: 100vh; + } + } + + .original { + align-self: center; + width: auto; + margin: 0 auto; + vertical-align: top; + } + + .full-width { + width: 100%; + margin: 0 auto; + vertical-align: top; + max-width: fit-content; + } +} + + +.bookmark-effect { + animation: bookmark 0.7s cubic-bezier(0.165, 0.84, 0.44, 1); +} + +// TODO: Move this into a dedicated component +.loading { + position: absolute; + left: 48%; + top: 20%; + z-index: 1; +} + + +.highlight { + background-color: var(--manga-reader-next-highlight-bg-color) !important; + animation: fadein .5s both; + backdrop-filter: blur(10px); +} +.highlight-2 { + background-color: var(--manga-reader-prev-highlight-bg-color) !important; + animation: fadein .5s both; + backdrop-filter: blur(10px); +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/readers/page-layout-mode.ts b/UI/Web/src/app/_models/page-layout-mode.ts similarity index 100% rename from UI/Web/src/app/_models/readers/page-layout-mode.ts rename to UI/Web/src/app/_models/page-layout-mode.ts diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index 680386e48..674dd88ca 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -1,7 +1,7 @@ import { LayoutMode } from 'src/app/manga-reader/_models/layout-mode'; import { BookPageLayoutMode } from '../readers/book-page-layout-mode'; -import { PageLayoutMode } from '../readers/page-layout-mode'; +import { PageLayoutMode } from '../page-layout-mode'; import { PageSplitOption } from './page-split-option'; import { ReaderMode } from './reader-mode'; import { ReadingDirection } from './reading-direction'; diff --git a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts index f6a83e8bd..cc0167393 100644 --- a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts +++ b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts @@ -1,10 +1,10 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SelectionModel } from 'src/app/typeahead/typeahead.component'; import { Library } from 'src/app/_models/library'; 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'; @Component({ selector: 'app-library-access-modal', diff --git a/UI/Web/src/app/admin/library-selector/library-selector.component.ts b/UI/Web/src/app/admin/library-selector/library-selector.component.ts index 05b4f3bdb..55650f332 100644 --- a/UI/Web/src/app/admin/library-selector/library-selector.component.ts +++ b/UI/Web/src/app/admin/library-selector/library-selector.component.ts @@ -1,9 +1,9 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormBuilder } from '@angular/forms'; -import { SelectionModel } from 'src/app/typeahead/typeahead.component'; import { Library } from 'src/app/_models/library'; 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'; @Component({ selector: 'app-library-selector', diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss index 8b834a2ff..2738346e7 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss @@ -1,36 +1,36 @@ @font-face { font-family: "Fira_Sans"; - src: url(../../../assets/fonts/Fira_Sans/FiraSans-Regular.ttf) format("truetype"); + src: url(../../../../assets/fonts/Fira_Sans/FiraSans-Regular.ttf) format("truetype"); } @font-face { font-family: "Lato"; - src: url(../../../assets/fonts/Lato/Lato-Regular.ttf) format("truetype"); + src: url(../../../../assets/fonts/Lato/Lato-Regular.ttf) format("truetype"); } @font-face { font-family: "Libre_Baskerville"; - src: url(../../../assets/fonts/Libre_Baskerville/LibreBaskerville-Regular.ttf) format("truetype"); + src: url(../../../../assets/fonts/Libre_Baskerville/LibreBaskerville-Regular.ttf) format("truetype"); } @font-face { font-family: "Merriweather"; - src: url(../../../assets/fonts/Merriweather/Merriweather-Regular.ttf) format("truetype"); + src: url(../../../../assets/fonts/Merriweather/Merriweather-Regular.ttf) format("truetype"); } @font-face { font-family: "Nanum_Gothic"; - src: url(../../../assets/fonts/Nanum_Gothic/NanumGothic-Regular.ttf) format("truetype"); + src: url(../../../../assets/fonts/Nanum_Gothic/NanumGothic-Regular.ttf) format("truetype"); } @font-face { font-family: "RocknRoll_One"; - src: url(../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf) format("truetype"); + src: url(../../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf) format("truetype"); } @font-face { font-family: "OpenDyslexic2"; - src: url(../../../assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.otf) format("opentype"); + src: url(../../../../assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.otf) format("opentype"); } :root { diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index 88d69d344..88e445e90 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -1254,9 +1254,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { updateReadingSectionHeight() { setTimeout(() => { if (this.immersiveMode) { - this.renderer.setStyle(this.readingSectionElemRef, 'height', 'calc(var(--vh, 1vh) * 100)', RendererStyleFlags2.Important); + this.renderer?.setStyle(this.readingSectionElemRef, 'height', 'calc(var(--vh, 1vh) * 100)', RendererStyleFlags2.Important); } else { - this.renderer.setStyle(this.readingSectionElemRef, 'height', 'calc(var(--vh, 1vh) * 100 - ' + this.topOffset + 'px)', RendererStyleFlags2.Important); + this.renderer?.setStyle(this.readingSectionElemRef, 'height', 'calc(var(--vh, 1vh) * 100 - ' + this.topOffset + 'px)', RendererStyleFlags2.Important); } }); } diff --git a/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.ts b/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.ts index b94427051..7d0b9ee6a 100644 --- a/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.ts @@ -5,7 +5,7 @@ import { ToastrService } from 'ngx-toastr'; import { debounceTime, distinctUntilChanged, forkJoin, Subject, switchMap, takeUntil, tap } from 'rxjs'; import { ConfirmService } from 'src/app/shared/confirm.service'; import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; -import { SelectionModel } from 'src/app/typeahead/typeahead.component'; +import { SelectionModel } from 'src/app/typeahead/_components/typeahead.component'; import { CollectionTag } from 'src/app/_models/collection-tag'; import { Pagination } from 'src/app/_models/pagination'; import { Series } from 'src/app/_models/series'; diff --git a/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.html b/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.html new file mode 100644 index 000000000..f34bec0a2 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.html @@ -0,0 +1,6 @@ +
+ +
+ diff --git a/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.scss b/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.scss new file mode 100644 index 000000000..1443e0e15 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.scss @@ -0,0 +1,25 @@ +@use '../../../../manga-reader-common'; + +.full-height { + width: auto; + margin: 0 auto; + max-height: calc(var(--vh)*100); + vertical-align: top; + &.wide { + height: 100vh; + } + } + + .original { + align-self: center; + width: auto; + margin: 0 auto; + vertical-align: top; + } + + .full-width { + width: 100%; + margin: 0 auto; + vertical-align: top; + max-width: fit-content; + } diff --git a/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts new file mode 100644 index 000000000..fbe731238 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts @@ -0,0 +1,249 @@ +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; +import { map, Observable, of, Subject, takeUntil, tap } from 'rxjs'; +import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; +import { LayoutMode } from '../../_models/layout-mode'; +import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from '../../_models/reader-enums'; +import { ReaderSetting } from '../../_models/reader-setting'; +import { ImageRenderer } from '../../_models/renderer'; +import { ManagaReaderService } from '../../_series/managa-reader.service'; + +@Component({ + selector: 'app-canvas-renderer', + templateUrl: './canvas-renderer.component.html', + styleUrls: ['./canvas-renderer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy, ImageRenderer { + + @Input() readerSettings$!: Observable; + @Input() image$!: Observable; + @Input() bookmark$!: Observable; + @Input() showClickOverlay$!: Observable; + @Input() imageFit$!: Observable; + @Output() imageHeight: EventEmitter = new EventEmitter(); + + @ViewChild('content') canvas: ElementRef | undefined; + private ctx!: CanvasRenderingContext2D; + private readonly onDestroy = new Subject(); + + currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT; + pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD; + + fit: FITTING_OPTION = FITTING_OPTION.ORIGINAL; + pageSplit: PageSplitOption = PageSplitOption.FitSplit; + layoutMode: LayoutMode = LayoutMode.Single; + + canvasImage: HTMLImageElement | null = null; + showClickOverlayClass$!: Observable; + /** + * Maps darkness value to the filter style + */ + darkenss$: Observable = of('brightness(100%)'); + /** + * Maps image fit value to the classes for image fitting + */ + imageFitClass$!: Observable; + renderWithCanvas: boolean = false; + + + + constructor(private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService) { } + + ngOnInit(): void { + this.readerSettings$.pipe(takeUntil(this.onDestroy), tap(value => { + this.fit = value.fitting; + this.pageSplit = value.pageSplit; + this.layoutMode = value.layoutMode; + const rerenderNeeded = this.pageSplit != value.pageSplit; + this.pagingDirection = value.pagingDirection; + if (rerenderNeeded) { + this.reset(); + } + })).subscribe(() => {}); + + this.darkenss$ = this.readerSettings$.pipe( + map(values => 'brightness(' + values.darkness + '%)'), + takeUntil(this.onDestroy) + ); + + this.imageFitClass$ = this.readerSettings$.pipe( + takeUntil(this.onDestroy), + map(values => values.fitting), + map(fit => { + if (fit === FITTING_OPTION.WIDTH || this.layoutMode === LayoutMode.Single) return fit; + if (this.canvasImage === null) return fit; + + // Would this ever execute given that we perform splitting only in this renderer? + if ( + this.mangaReaderService.isWideImage(this.canvasImage) && + this.mangaReaderService.shouldRenderAsFitSplit(this.pageSplit) + ) { + // Rewriting to fit to width for this cover image + console.log('Fit (override): ', fit); + return FITTING_OPTION.WIDTH; + } + return fit; + }) + ); + + + this.bookmark$.pipe( + takeUntil(this.onDestroy), + tap(_ => { + if (this.currentImageSplitPart === SPLIT_PAGE_PART.NO_SPLIT) return; + if (!this.canvas) return; + + const elements = [this.canvas?.nativeElement]; + console.log('Applying bookmark on ', elements); + this.mangaReaderService.applyBookmarkEffect(elements); + }) + ).subscribe(() => {}); + + this.showClickOverlayClass$ = this.showClickOverlay$.pipe( + map(showOverlay => showOverlay ? 'blur' : ''), + takeUntil(this.onDestroy) + ); + } + + ngAfterViewInit() { + if (this.canvas) { + this.ctx = this.canvas.nativeElement.getContext('2d', { alpha: false }); + } + } + + ngOnDestroy() { + this.onDestroy.next(); + this.onDestroy.complete(); + } + + reset() { + this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT; + } + + updateSplitPage() { + if (this.canvasImage == null) return; + const needsSplitting = this.mangaReaderService.isWideImage(this.canvasImage); + if (!needsSplitting || this.mangaReaderService.isNoSplit(this.pageSplit)) { + this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT; + return needsSplitting; + } + const splitLeftToRight = this.mangaReaderService.isSplitLeftToRight(this.pageSplit); + + if (this.pagingDirection === PAGING_DIRECTION.FORWARD) { + switch (this.currentImageSplitPart) { + case SPLIT_PAGE_PART.NO_SPLIT: + this.currentImageSplitPart = splitLeftToRight ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.RIGHT_PART; + break; + case SPLIT_PAGE_PART.LEFT_PART: + const r2lSplittingPart = (needsSplitting ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.NO_SPLIT); + this.currentImageSplitPart = splitLeftToRight ? SPLIT_PAGE_PART.RIGHT_PART : r2lSplittingPart; + break; + case SPLIT_PAGE_PART.RIGHT_PART: + const l2rSplittingPart = (needsSplitting ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.NO_SPLIT); + this.currentImageSplitPart = splitLeftToRight ? l2rSplittingPart : SPLIT_PAGE_PART.LEFT_PART; + break; + } + } else if (this.pagingDirection === PAGING_DIRECTION.BACKWARDS) { + switch (this.currentImageSplitPart) { + case SPLIT_PAGE_PART.NO_SPLIT: + this.currentImageSplitPart = splitLeftToRight ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.LEFT_PART; + break; + case SPLIT_PAGE_PART.LEFT_PART: + const l2rSplittingPart = (needsSplitting ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.NO_SPLIT); + this.currentImageSplitPart = splitLeftToRight? l2rSplittingPart : SPLIT_PAGE_PART.RIGHT_PART; + break; + case SPLIT_PAGE_PART.RIGHT_PART: + this.currentImageSplitPart = splitLeftToRight ? SPLIT_PAGE_PART.LEFT_PART : (needsSplitting ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.NO_SPLIT); + break; + } + } + return needsSplitting; + } + + /** + * This renderer does not render when splitting is not needed + * @param img + * @returns + */ + renderPage(img: Array) { + this.renderWithCanvas = false; + if (img === null || img.length === 0 || img[0] === null) return; + if (!this.ctx || !this.canvas) return; + this.canvasImage = img[0]; + this.cdRef.markForCheck(); + + const needsSplitting = this.updateSplitPage(); + //console.log('split: ',this.currentImageSplitPart); + if (!needsSplitting) return; + if (this.currentImageSplitPart === SPLIT_PAGE_PART.NO_SPLIT) return; + + this.renderWithCanvas = true; + this.setCanvasSize(); + + if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) { + this.canvas.nativeElement.width = this.canvasImage.width / 2; + this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, 0, 0, this.canvasImage.width, this.canvasImage.height); + this.cdRef.markForCheck(); + } else if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.RIGHT_PART) { + this.canvas.nativeElement.width = this.canvasImage.width / 2; + this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height); + this.cdRef.markForCheck(); + } + + this.cdRef.markForCheck(); + } + + getPageAmount(direction: PAGING_DIRECTION) { + if (this.canvasImage === null) return 1; + if (!this.mangaReaderService.isWideImage(this.canvasImage)) return 1; + switch(direction) { + case PAGING_DIRECTION.FORWARD: + return this.shouldMoveNext() ? 1 : 0; + case PAGING_DIRECTION.BACKWARDS: + return this.shouldMovePrev() ? 1 : 0; + } + } + + shouldMoveNext() { + if (this.mangaReaderService.isNoSplit(this.pageSplit)) return true; + return this.currentImageSplitPart !== (this.mangaReaderService.isSplitLeftToRight(this.pageSplit) ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.RIGHT_PART); + } + + shouldMovePrev() { + if (this.mangaReaderService.isNoSplit(this.pageSplit)) return true; + return this.currentImageSplitPart !== (this.mangaReaderService.isSplitLeftToRight(this.pageSplit) ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.LEFT_PART); + } + + /** + * There are some hard limits on the size of canvas' that we must cap at. https://github.com/jhildenbiddle/canvas-size#test-results + * For Safari, it's 16,777,216, so we cap at 4096x4096 when this happens. The drawImage in render will perform bi-cubic scaling for us. + */ + setCanvasSize() { + if (this.canvasImage == null) return; + if (!this.ctx || !this.canvas) { return; } + // TODO: Move this somewhere else (maybe canvas renderer?) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const isSafari = [ + 'iPad Simulator', + 'iPhone Simulator', + 'iPod Simulator', + 'iPad', + 'iPhone', + 'iPod' + ].includes(navigator.platform) + // iPad on iOS 13 detection + || (navigator.userAgent.includes("Mac") && "ontouchend" in document); + const canvasLimit = isSafari ? 16_777_216 : 124_992_400; + const needsScaling = this.canvasImage.width * this.canvasImage.height > canvasLimit; + if (needsScaling) { + this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384; + this.canvas.nativeElement.height = isSafari ? 4_096 : 16_384; + } else { + this.canvas.nativeElement.width = this.canvasImage.width; + this.canvas.nativeElement.height = this.canvasImage.height; + } + this.imageHeight.emit(this.canvas.nativeElement.height); + this.cdRef.markForCheck(); + } +} diff --git a/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.html b/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.html new file mode 100644 index 000000000..f3611c3f2 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.html @@ -0,0 +1,18 @@ + +
+ +  + + +  + +
+
\ No newline at end of file diff --git a/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.scss b/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.scss new file mode 100644 index 000000000..919a0ab00 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.scss @@ -0,0 +1,45 @@ +@use '../../../../manga-reader-common'; + +.image-container { + #image-1 { + &.double { + margin: 0 0 0 auto; + } + } +} + +.full-width { + width: 100%; + margin: 0 auto; + vertical-align: top; + max-width: fit-content; + + &.double { + width: 50%; + + &.cover { + width: 100%; + } + } +} + +.center-double { + display: flex; + overflow: unset; +} + +.fit-to-width-double-offset { + max-width: 100%; // max-width fixes center alignment issue +} + +.original-double-offset { + max-width: 100%; +} + +.fit-to-height-double-offset { + height: 100vh; + object-fit: scale-down; + top: 50%; + left: 50%; + max-width: 100%; +} diff --git a/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.ts new file mode 100644 index 000000000..0a087e47e --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.ts @@ -0,0 +1,324 @@ +import { DOCUMENT } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Observable, of, Subject, map, takeUntil, tap, zip, shareReplay, filter } from 'rxjs'; +import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; +import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; +import { ReaderService } from 'src/app/_services/reader.service'; +import { LayoutMode } from '../../_models/layout-mode'; +import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums'; +import { ReaderSetting } from '../../_models/reader-setting'; +import { ImageRenderer } from '../../_models/renderer'; +import { ManagaReaderService } from '../../_series/managa-reader.service'; + +@Component({ + selector: 'app-double-renderer', + templateUrl: './double-renderer.component.html', + styleUrls: ['./double-renderer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer { + + @Input() readerSettings$!: Observable; + @Input() image$!: Observable; + /** + * The image fit class + */ + @Input() imageFit$!: Observable; + @Input() bookmark$!: Observable; + @Input() showClickOverlay$!: Observable; + @Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>; + + @Input() getPage!: (pageNum: number) => HTMLImageElement; + + @Output() imageHeight: EventEmitter = new EventEmitter(); + + imageFitClass$!: Observable; + showClickOverlayClass$!: Observable; + readerModeClass$!: Observable; + layoutClass$!: Observable; + shouldRenderSecondPage$!: Observable; + darkenss$: Observable = of('brightness(100%)'); + layoutMode: LayoutMode = LayoutMode.Single; + pageSplit: PageSplitOption = PageSplitOption.FitSplit; + pageNum: number = 0; + maxPages: number = 0; + + /** + * Used to render a page on the canvas or in the image tag. This Image element is prefetched by the cachedImages buffer. + * @remarks Used for rendering to screen. + */ + currentImage = new Image(); + /** + * Used solely for LayoutMode.Double rendering. + * @remarks Used for rendering to screen. + */ + currentImage2 = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the previous image to currentImage + * @see currentImage + */ + currentImagePrev = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the next image to currentImage + * @see currentImage + */ + currentImageNext = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the current - 2 image to currentImage + * @see currentImage + */ + currentImage2Behind = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the current + 2 image to currentImage + * @see currentImage + */ + currentImage2Ahead = new Image(); + + /** + * Determines if we should render a double page. + * The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image + * and the next page is not a wide image (as only non-wides should be shown next to each other). + * @remarks This will always fail if the window's width is greater than the height + */ + shouldRenderDouble$!: Observable; + + private readonly onDestroy = new Subject(); + + get ReaderMode() {return ReaderMode;} + get FITTING_OPTION() {return FITTING_OPTION;} + get LayoutMode() {return LayoutMode;} + + + + constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService, + @Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { } + + ngOnInit(): void { + this.readerModeClass$ = this.readerSettings$.pipe( + filter(_ => this.isValid()), + map(values => values.readerMode), + map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'), + takeUntil(this.onDestroy) + ); + + this.darkenss$ = this.readerSettings$.pipe( + map(values => 'brightness(' + values.darkness + '%)'), + filter(_ => this.isValid()), + takeUntil(this.onDestroy) + ); + + this.showClickOverlayClass$ = this.showClickOverlay$.pipe( + map(showOverlay => showOverlay ? 'blur' : ''), + filter(_ => this.isValid()), + takeUntil(this.onDestroy) + ); + + this.pageNum$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + tap(pageInfo => { + this.pageNum = pageInfo.pageNum; + this.maxPages = pageInfo.maxPages; + + this.currentImage = this.getPage(this.pageNum); + this.currentImage2 = this.getPage(this.pageNum + 1); + + this.currentImageNext = this.getPage(this.pageNum + 1); + this.currentImagePrev = this.getPage(this.pageNum - 1); + + this.currentImage2Behind = this.getPage(this.pageNum - 2); + this.currentImage2Ahead = this.getPage(this.pageNum + 2); + this.cdRef.markForCheck(); + })).subscribe(() => {}); + + this.shouldRenderDouble$ = this.pageNum$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + map((_) => { + return this.shouldRenderDouble(); + }) + ); + + this.layoutClass$ = zip(this.shouldRenderDouble$, this.imageFit$).pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + map((value) => { + if (!value[0]) return 'd-none'; + if (value[0] && value[1] === FITTING_OPTION.WIDTH) return 'fit-to-width-double-offset'; + if (value[0] && value[1] === FITTING_OPTION.HEIGHT) return 'fit-to-height-double-offset'; + if (value[0] && value[1] === FITTING_OPTION.ORIGINAL) return 'original-double-offset'; + return ''; + }) + ); + + this.shouldRenderSecondPage$ = this.pageNum$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + map(_ => { + if (this.currentImage2.src === '') { + console.log('Not rendering second page as 2nd image is empty'); + return false; + } + if (this.mangaReaderService.isCoverImage(this.pageNum)) { + console.log('Not rendering second page as on cover image'); + return false; + } + if (this.readerService.imageUrlToPageNum(this.currentImage2.src) > this.maxPages - 1) { + console.log('Not rendering second page as 2nd image is on last page'); + return false; + } + if (this.mangaReaderService.isWideImage(this.currentImageNext)) { + console.log('Not rendering second page as next page is wide'); + return false; + } + + if (this.mangaReaderService.isWideImage(this.currentImage)) { + console.log('Not rendering second page as next page is wide'); + return false; + } + + if (this.mangaReaderService.isWideImage(this.currentImagePrev)) { + console.log('Not rendering second page as prev page is wide'); + return false; + } + return true; + }) + ); + + this.readerSettings$.pipe( + takeUntil(this.onDestroy), + tap(values => { + this.layoutMode = values.layoutMode; + this.pageSplit = values.pageSplit; + this.cdRef.markForCheck(); + }) + ).subscribe(() => {}); + + this.bookmark$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + tap(_ => { + const elements = []; + const image1 = this.document.querySelector('#image-1'); + if (image1 != null) elements.push(image1); + + const image2 = this.document.querySelector('#image-2'); + if (image2 != null) elements.push(image2); + + this.mangaReaderService.applyBookmarkEffect(elements); + }) + ).subscribe(() => {}); + + + this.imageFitClass$ = this.readerSettings$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + map(values => values.fitting), + shareReplay() + ); + } + + ngOnDestroy(): void { + this.onDestroy.next(); + this.onDestroy.complete(); + } + + shouldRenderDouble() { + if (this.layoutMode !== LayoutMode.Double) return false; + + return !( + this.mangaReaderService.isCoverImage(this.pageNum) + || this.mangaReaderService.isWideImage(this.currentImage) + || this.mangaReaderService.isWideImage(this.currentImageNext) + ); + } + + isValid() { + return this.layoutMode === LayoutMode.Double; + } + + renderPage(img: Array): void { + if (img === null || img.length === 0 || img[0] === null) return; + if (!this.isValid()) return; + + console.log('[DoubleRenderer] renderPage(): ', this.pageNum); + console.log(this.readerService.imageUrlToPageNum(this.currentImage2Behind.src), this.readerService.imageUrlToPageNum(this.currentImagePrev.src), + '[', this.readerService.imageUrlToPageNum(this.currentImage.src), ']', + this.readerService.imageUrlToPageNum(this.currentImageNext.src), this.readerService.imageUrlToPageNum(this.currentImage2Ahead.src)) + + + if (!this.shouldRenderDouble()) { + this.imageHeight.emit(this.currentImage.height); + return; + } + + this.currentImage2 = this.currentImageNext; + + this.cdRef.markForCheck(); + this.imageHeight.emit(Math.max(this.currentImage.height, this.currentImage2.height)); + this.cdRef.markForCheck(); + } + + shouldMovePrev(): boolean { + return true; + } + shouldMoveNext(): boolean { + return true; + } + getPageAmount(direction: PAGING_DIRECTION): number { + if (this.layoutMode !== LayoutMode.Double) return 0; + + // If prev page: + switch (direction) { + case PAGING_DIRECTION.FORWARD: + if (this.mangaReaderService.isCoverImage(this.pageNum)) { + console.log('Moving forward 1 page as on cover image'); + return 1; + } + if (this.mangaReaderService.isWideImage(this.currentImage)) { + console.log('Moving forward 1 page as current page is wide'); + return 1; + } + if (this.mangaReaderService.isWideImage(this.currentImageNext)) { + console.log('Moving forward 1 page as next page is wide'); + return 1; + } + if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) { + console.log('Moving forward 1 page as 2 pages left'); + return 1; + } + if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) { + console.log('Moving forward 1 page as 1 page left'); + return 1; + } + console.log('Moving forward 2 pages'); + return 2; + case PAGING_DIRECTION.BACKWARDS: + if (this.mangaReaderService.isCoverImage(this.pageNum)) { + console.log('Moving back 1 page as on cover image'); + return 1; + } + if (this.mangaReaderService.isWideImage(this.currentImage)) { + console.log('Moving back 1 page as current page is wide'); + return 1; + } + if (this.mangaReaderService.isWideImage(this.currentImagePrev)) { + console.log('Moving back 1 page as prev page is wide'); + return 1; + } + if (this.mangaReaderService.isWideImage(this.currentImage2Behind)) { + console.log('Moving back 1 page as 2 pages back is wide'); + return 1; + } + // Not sure about this condition on moving backwards + if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) { + console.log('Moving back 1 page as 2 pages left'); + return 1; + } + console.log('Moving back 2 pages'); + return 2; + } + } + reset(): void {} + +} diff --git a/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.html b/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.html new file mode 100644 index 000000000..c1874dd9f --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.html @@ -0,0 +1,18 @@ + +
+ +  + + +  + +
+
\ No newline at end of file diff --git a/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.scss b/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.scss new file mode 100644 index 000000000..537d60286 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.scss @@ -0,0 +1,64 @@ +@use '../../../../manga-reader-common'; + +// Overrides for reverse +.image-container { + &.reverse { + overflow: unset; + display: flex; + align-content: center; + justify-content: center; + flex-direction: row-reverse; + + img { + margin: unset; + } + } + + #image-1 { + &.double { + margin: 0 0 0 auto; + } + } + + #image-2 { + &.double { + margin: 0 auto 0 0; + } + } +} + +.full-width { + width: 100%; + margin: 0 auto; + vertical-align: top; + max-width: fit-content; + + &.double { + width: 50%; + + &.cover { + width: 100%; + } + } +} + +.center-double { + display: flex; + overflow: unset; +} + +.fit-to-width-double-offset { + max-width: 100%; // max-width fixes center alignment issue +} + +.original-double-offset { + max-width: 100%; +} + +.fit-to-height-double-offset { + height: 100vh; + object-fit: scale-down; + top: 50%; + left: 50%; + max-width: 100%; +} diff --git a/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.ts new file mode 100644 index 000000000..feda43ada --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.ts @@ -0,0 +1,499 @@ +import { DOCUMENT } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Observable, of, Subject, map, takeUntil, tap, zip, shareReplay, filter } from 'rxjs'; +import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; +import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; +import { ReaderService } from 'src/app/_services/reader.service'; +import { LayoutMode } from '../../_models/layout-mode'; +import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums'; +import { ReaderSetting } from '../../_models/reader-setting'; +import { ImageRenderer } from '../../_models/renderer'; +import { ManagaReaderService } from '../../_series/managa-reader.service'; + +/** + * This is aimed at manga. Double page renderer but where if we have page = 10, you will see + * page 11 page 10. + */ +@Component({ + selector: 'app-double-reverse-renderer', + templateUrl: './double-reverse-renderer.component.html', + styleUrls: ['./double-reverse-renderer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageRenderer { + + + @Input() readerSettings$!: Observable; + @Input() image$!: Observable; + /** + * The image fit class + */ + @Input() imageFit$!: Observable; + @Input() bookmark$!: Observable; + @Input() showClickOverlay$!: Observable; + @Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>; + + @Input() getPage!: (pageNum: number) => HTMLImageElement; + + @Output() imageHeight: EventEmitter = new EventEmitter(); + + imageFitClass$!: Observable; + showClickOverlayClass$!: Observable; + readerModeClass$!: Observable; + layoutClass$!: Observable; + shouldRenderSecondPage$!: Observable; + darkenss$: Observable = of('brightness(100%)'); + layoutMode: LayoutMode = LayoutMode.Single; + pageSplit: PageSplitOption = PageSplitOption.FitSplit; + pageNum: number = 0; + maxPages: number = 0; + + /** + * Used to render a page on the canvas or in the image tag. This Image element is prefetched by the cachedImages buffer. + * @remarks Used for rendering to screen. + */ + leftImage = new Image(); + /** + * Used solely for LayoutMode.Double rendering. + * @remarks Used for rendering to screen. + */ + rightImage = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the previous image to currentImage + * @see currentImage + */ + currentImagePrev = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the next image to currentImage + * @see currentImage + */ + currentImageNext = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the current - 2 image to currentImage + * @see currentImage + */ + currentImage2Behind = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the current + 2 image to currentImage + * @see currentImage + */ + currentImage2Ahead = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the current - 3 image to currentImage + * @see currentImage + */ + currentImage3Behind = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the current + 3 image to currentImage + * @see currentImage + */ + currentImage3Ahead = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the current - 4 image to currentImage + * @see currentImage + */ + currentImage4Behind = new Image(); + /** + * Used solely for LayoutMode.Double rendering. Will always hold the current + 4 image to currentImage + * @see currentImage + */ + currentImage4Ahead = new Image(); + + /** + * Determines if we should render a double page. + * The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image + * and the next page is not a wide image (as only non-wides should be shown next to each other). + * @remarks This will always fail if the window's width is greater than the height + */ + shouldRenderDouble$!: Observable; + + pageSpreadMap: {[key: number]: 'W'|'S'} = {}; + + private readonly onDestroy = new Subject(); + + get ReaderMode() {return ReaderMode;} + get FITTING_OPTION() {return FITTING_OPTION;} + get LayoutMode() {return LayoutMode;} + + + + constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService, + @Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { } + + ngOnInit(): void { + this.readerModeClass$ = this.readerSettings$.pipe( + filter(_ => this.isValid()), + map(values => values.readerMode), + map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'), + takeUntil(this.onDestroy) + ); + + this.darkenss$ = this.readerSettings$.pipe( + filter(_ => this.isValid()), + map(values => 'brightness(' + values.darkness + '%)'), + takeUntil(this.onDestroy) + ); + + this.showClickOverlayClass$ = this.showClickOverlay$.pipe( + filter(_ => this.isValid()), + map(showOverlay => showOverlay ? 'blur' : ''), + takeUntil(this.onDestroy) + ); + + this.pageNum$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + tap(pageInfo => { + this.pageNum = pageInfo.pageNum; + this.maxPages = pageInfo.maxPages; + + this.leftImage = this.getPage(this.pageNum); + this.rightImage = this.getPage(this.pageNum + 1); + + this.currentImageNext = this.getPage(this.pageNum + 1); + this.currentImagePrev = this.getPage(this.pageNum - 1); + + this.currentImage2Behind = this.getPage(this.pageNum - 2); + this.currentImage2Ahead = this.getPage(this.pageNum + 2); + + this.currentImage3Behind = this.getPage(this.pageNum - 3); + this.currentImage3Ahead = this.getPage(this.pageNum + 3); + + this.currentImage4Behind = this.getPage(this.pageNum - 4); + this.currentImage4Ahead = this.getPage(this.pageNum + 4); + + this.leftImage.addEventListener('load', () => { + this.updatePageMap(this.leftImage) + }); + this.rightImage.addEventListener('load', () => { + this.updatePageMap(this.rightImage) + }); + this.currentImageNext.addEventListener('load', () => { + this.updatePageMap(this.currentImageNext) + }); + this.currentImagePrev.addEventListener('load', () => { + this.updatePageMap(this.currentImagePrev) + }); + this.currentImage2Behind.addEventListener('load', () => { + this.updatePageMap(this.currentImage2Behind) + }); + this.currentImage2Ahead.addEventListener('load', () => { + this.updatePageMap(this.currentImage2Ahead) + }); + this.currentImage3Behind.addEventListener('load', () => { + this.updatePageMap(this.currentImage3Behind) + }); + this.currentImage3Ahead.addEventListener('load', () => { + this.updatePageMap(this.currentImage3Ahead) + }); + this.currentImage4Behind.addEventListener('load', () => { + this.updatePageMap(this.currentImage4Behind) + }); + this.currentImage4Ahead.addEventListener('load', () => { + this.updatePageMap(this.currentImage4Ahead) + }); + })).subscribe(() => {}); + + this.shouldRenderDouble$ = this.pageNum$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + map((_) => this.shouldRenderDouble()), + shareReplay() + ); + + this.layoutClass$ = zip(this.shouldRenderDouble$, this.imageFit$).pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + map((value) => { + if (!value[0]) return 'd-none'; + if (value[0] && value[1] === FITTING_OPTION.WIDTH) return 'fit-to-width-double-offset'; + if (value[0] && value[1] === FITTING_OPTION.HEIGHT) return 'fit-to-height-double-offset'; + if (value[0] && value[1] === FITTING_OPTION.ORIGINAL) return 'original-double-offset'; + return ''; + }) + ); + + this.shouldRenderSecondPage$ = this.pageNum$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + map(_ => { + if (this.mangaReaderService.isCoverImage(this.pageNum)) { + console.log('Not rendering second page as on cover image'); + return false; + } + if (this.readerService.imageUrlToPageNum(this.rightImage.src) > this.maxPages - 1) { + console.log('Not rendering second page as 2nd image is on last page'); + return false; + } + if (this.isWide(this.leftImage)) { + console.log('Not rendering second page as right page is wide'); + return false; + } + if (this.isWide(this.rightImage)) { + console.log('Not rendering second page as right page is wide'); + return false; + } + if (this.isWide(this.currentImageNext)) { + console.log('Not rendering second page as next page is wide'); + return false; + } + if (this.isWide(this.currentImagePrev) && (this.isWide(this.currentImage3Ahead))) { + console.log('Not rendering second page as prev page is wide'); + return false; + } + return true; + }), + ); + + this.readerSettings$.pipe( + takeUntil(this.onDestroy), + tap(values => { + this.layoutMode = values.layoutMode; + this.pageSplit = values.pageSplit; + this.cdRef.markForCheck(); + }) + ).subscribe(() => {}); + + this.bookmark$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + tap(_ => { + const elements = []; + const image1 = this.document.querySelector('#image-1'); + if (image1 != null) elements.push(image1); + + const image2 = this.document.querySelector('#image-2'); + if (image2 != null) elements.push(image2); + + this.mangaReaderService.applyBookmarkEffect(elements); + }) + ).subscribe(() => {}); + + + this.imageFitClass$ = this.readerSettings$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + map(values => values.fitting), + shareReplay() + ); + } + + ngOnDestroy(): void { + this.onDestroy.next(); + this.onDestroy.complete(); + } + + updatePageMap(img: HTMLImageElement) { + const page = this.readerService.imageUrlToPageNum(img.src); + if (!this.pageSpreadMap.hasOwnProperty(page)) { + this.pageSpreadMap[page] = this.mangaReaderService.isWideImage(img) ? 'W' : 'S'; + } + } + + /** + * We should Render 2 pages if: + * 1. We are not currently the first image (cover image) + * 2. The previous page is not a cover image + * 3. The current page is not a wide image + * 4. The next page is not a wide image + */ + shouldRenderDouble() { + if (!this.isValid()) return false; + + if (this.mangaReaderService.isCoverImage(this.pageNum)) { + console.log('Not rendering right image as is cover image'); + return false; + } + if (this.mangaReaderService.isCoverImage(this.pageNum + 1)) { + console.log('Not rendering right image as current - 1 is cover image'); + return false; + } + if (this.isWide(this.leftImage)) { + console.log('Not rendering right image as left is wide'); + //return false; + } + if (this.isWide(this.rightImage)) { + console.log('Not rendering right image as it is wide'); + return false; + } + + if (this.isWide(this.currentImageNext)) { + console.log('Not rendering right image as it is wide'); + return false; + } + + + return true; + + + // const result = !( + // this.mangaReaderService.isCoverImage(this.pageNum) + // || this.mangaReaderService.isCoverImage(this.pageNum - 1) // This is because we use prev page and hence the cover will re-show + // || this.mangaReaderService.isWideImage(this.leftImage) + // || this.mangaReaderService.isWideImage(this.currentImageNext) + // ); + + // return result; + } + + isWide(img: HTMLImageElement) { + const page = this.readerService.imageUrlToPageNum(img.src); + return this.mangaReaderService.isWideImage(img) || this.pageSpreadMap.hasOwnProperty(page) && this.pageSpreadMap[page] === 'W'; + } + + isValid() { + return this.layoutMode === LayoutMode.DoubleReversed; + } + + renderPage(img: Array): void { + if (img === null || img.length === 0 || img[0] === null) return; + if (!this.isValid()) return; + + console.log('[DoubleRenderer] renderPage(): ', this.pageNum); + + const allImages = [ + this.currentImage4Behind, this.currentImage3Behind, this.currentImage2Behind, this.currentImagePrev, + this.leftImage, + this.currentImageNext, this.currentImage2Ahead, this.currentImage3Ahead, this.currentImage4Ahead + ]; + + console.log('DoubleRenderer buffered pages: ', allImages.map(img => { + const page = this.readerService.imageUrlToPageNum(img.src); + if (page === this.pageNum) return '[' + page + ']'; + return page; + }).join(', ')); + + + this.rightImage = this.currentImageNext; + + + this.cdRef.markForCheck(); + this.imageHeight.emit(Math.max(this.leftImage.height, this.rightImage.height)); + this.cdRef.markForCheck(); + } + + shouldMovePrev(): boolean { + return true; + } + shouldMoveNext(): boolean { + return true; + } + getPageAmount(direction: PAGING_DIRECTION): number { + if (this.layoutMode !== LayoutMode.DoubleReversed) return 0; + // console.log("----currentImage4Behind:", this.currentImage4Behind); + // console.log("---currentImage3Behind:", this.currentImage3Behind); + // console.log("--currentImage2Behind:", this.currentImage2Behind); + // console.log("-currentImagePrev:", this.currentImagePrev); + // console.log("leftImage", this.leftImage); + // console.log("rightImage", this.rightImage); + // console.log("+currentImageNext:", this.currentImageNext); + // console.log("++currentImage2Ahead:", this.currentImage2Ahead); + // console.log("+++currentImage3Ahead:", this.currentImage3Ahead); + // console.log("++++currentImage4Ahead:", this.currentImage4Ahead); + + const allImages = [ + this.currentImage4Behind, this.currentImage3Behind, this.currentImage2Behind, this.currentImagePrev, + this.leftImage, this.rightImage, + this.currentImageNext, this.currentImage2Ahead, this.currentImage3Ahead, this.currentImage4Ahead + ]; + + console.log('[getPageAmount for double reverse]: ', allImages.map(img => { + const page = this.readerService.imageUrlToPageNum(img.src); + if (page === this.pageNum) return '[' + page; + if (page === this.pageNum + 1) return page + ']'; + return page + ''; + })); + console.log("Current Page: ", this.pageNum); + console.log("Total Pages: ", this.maxPages); + + switch (direction) { + case PAGING_DIRECTION.FORWARD: + if (this.mangaReaderService.isCoverImage(this.pageNum)) { + console.log('Moving forward 1 page as on cover image'); + return 1; + } + + if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages-1)) { + console.log('Moving forward 1 page as 2 pages left'); + return 1; + } + + if (this.mangaReaderService.isWideImage(this.rightImage)) { + console.log('Moving forward 1 page as current page is wide'); + return 1; + } + + if (this.mangaReaderService.isWideImage(this.leftImage)) { + console.log('Moving forward 1 page as current page is wide'); + return 1; + } + if (this.mangaReaderService.isWideImage(this.currentImageNext)) { + console.log('Moving forward 1 page as next page is wide'); + return 1; + } + + if (this.mangaReaderService.isWideImage(this.currentImagePrev)) { + console.log('Moving forward 1 page as prev page is wide'); + return 1; + } + + if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages-1)) { + console.log('Moving forward 1 page as 1 page left'); + return 1; + } + + if (this.pageNum === this.maxPages - 1) { + console.log('Moving forward 0 page as on last page'); + return 0; + } + + console.log('Moving forward 2 pages'); + return 2; + case PAGING_DIRECTION.BACKWARDS: + if (this.mangaReaderService.isCoverImage(this.pageNum)) { + console.log('Moving back 1 page as on cover image'); + return 1; + } + + if (this.isWide(this.rightImage)) { + console.log('Moving back 2 page as right page is wide'); + return 2; + } + + if (this.isWide(this.leftImage) && (!this.isWide(this.currentImage4Behind))) { + console.log('Moving back 1 page as left page is wide'); + return 1; + } + + if (this.isWide(this.currentImageNext)) { + console.log('Moving back 2 page as prev page is wide'); + return 1; + } + + if (this.isWide(this.currentImagePrev)) { + console.log('Moving back 1 page as prev page is wide'); + return 1; + } + + if (this.isWide(this.currentImage2Behind)) { + console.log('Moving back 1 page as 2 pages back is wide'); + return 1; + } + + if (this.isWide(this.currentImage2Ahead)) { + console.log('Moving back 2 page as 2 pages back is wide'); + return 1; + } + // Not sure about this condition on moving backwards + if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) { + console.log('Moving back 1 page as 2 pages left'); + return 1; + } + console.log('Moving back 2 pages'); + return 2; + } + } + reset(): void {} + + +} diff --git a/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.html b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.html similarity index 100% rename from UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.html rename to UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.html diff --git a/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.scss b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.scss similarity index 100% rename from UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.scss rename to UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.scss diff --git a/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts similarity index 99% rename from UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts rename to UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts index 4cd219cb3..27e5057b0 100644 --- a/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts +++ b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts @@ -3,9 +3,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Even import { BehaviorSubject, fromEvent, ReplaySubject, Subject } from 'rxjs'; import { debounceTime, takeUntil } from 'rxjs/operators'; 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 { ReaderService } from '../../../_services/reader.service'; +import { PAGING_DIRECTION } from '../../_models/reader-enums'; +import { WebtoonImage } from '../../_models/webtoon-image'; /** * How much additional space should pass, past the original bottom of the document height before we trigger the next chapter load diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html similarity index 80% rename from UI/Web/src/app/manga-reader/manga-reader.component.html rename to UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html index aca92651b..ddab7f8a0 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html @@ -26,23 +26,28 @@ - +
+ -
- - + +
+ +
+
-
@@ -61,33 +66,66 @@
-
+ + + + + + + + +
+ - -
+ + + + +  + +
-->
+ [bufferPages]="5" + [goToPage]="goToPageEvent" + (pageNumberChange)="handleWebtoonPageChange($event)" + [totalPages]="maxPages" + [urlProvider]="getPageUrl" + (loadNextChapter)="loadNextChapter()" + (loadPrevChapter)="loadPrevChapter()" + [bookmarkPage]="showBookmarkEffectEvent" + [fullscreenToggled]="fullscreenEvent"> +
@@ -95,6 +133,7 @@
+
diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.scss b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.scss similarity index 74% rename from UI/Web/src/app/manga-reader/manga-reader.component.scss rename to UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.scss index 69b0017de..f9f59601a 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.scss +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.scss @@ -4,18 +4,10 @@ $side-width: 25%; $dash-width: 3px; $pointer-offset: 5px; -img { - user-select: none; -} +@use '../../.././../manga-reader-common'; + + -@media(min-width: 600px) { - .overlay .left .i { - left: 20px; - } - .overlay .right .i { - right: 20px; - } -} .reading-area { position: relative; @@ -24,55 +16,6 @@ img { //height: calc(var(--vh)*100); // this needs to be applied on the DOM because it breaks infinite scroller } -.image-container { - text-align: center; - - // Original - //display: block; - - // New (for centering in both axis) - //display: flex; // Leave this off as it can cutoff the image - align-items: center; - - &.full-width { - width: 100vw; - height: calc(var(--vh)*100); - display: grid; - } - - &.full-height { - height: 100vh; - display: inline-block; - } - - &.original { - height: 100vh; - display: grid; - } - - #image-1 { - &.double { - margin: 0 0 0 auto; - } - } - - &.reverse { - overflow: unset; - display: flex; - align-content: center; - justify-content: center; - - img { - margin: unset; - } - } - - #image-2 { - &.double { - margin: 0 auto 0 0; - } - } -} .reader { background-color: var(--manga-reader-bg-color); @@ -83,14 +26,6 @@ img { } } - -.loading { - position: absolute; - left: 48%; - top: 20%; - z-index: 1; -} - .title, .subtitle { text-overflow: ellipsis; overflow: hidden; @@ -110,6 +45,15 @@ img { color: var(--manga-reader-overlay-text-color); } +@media(min-width: 600px) { + .overlay .left .i { + left: 20px; + } + .overlay .right .i { + right: 20px; + } +} + // Fitting Options .full-height { @@ -272,10 +216,11 @@ img { width: 100%; } - $pagination-bg: rgba(0, 0, 0, 0); - //$pagination-bg: rgba(0, 0, 255, 0.4); // DEBUG CODE + .pagination-area { + $pagination-bg: rgba(0, 0, 0, 0); + //$pagination-bg: rgba(0, 0, 255, 0.4); // DEBUG CODE cursor: pointer; z-index: 100; @@ -321,27 +266,3 @@ img { } } -.highlight { - background-color: var(--manga-reader-next-highlight-bg-color) !important; - animation: fadein .5s both; - backdrop-filter: blur(10px); -} -.highlight-2 { - background-color: var(--manga-reader-prev-highlight-bg-color) !important; - animation: fadein .5s both; - backdrop-filter: blur(10px); -} - - -.bookmark-effect { - animation: bookmark 0.7s cubic-bezier(0.165, 0.84, 0.44, 1); -} - -@keyframes bookmark { - 0%, 100% { - border: 0px; - } - 50% { - border: 5px solid var(--primary-color); - } -} diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts similarity index 68% rename from UI/Web/src/app/manga-reader/manga-reader.component.ts rename to UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index b14bb958a..070d33ff3 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -1,30 +1,36 @@ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, NgZone, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; +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 { debounceTime, take, takeUntil } from 'rxjs/operators'; -import { User } from '../_models/user'; -import { AccountService } from '../_services/account.service'; -import { ReaderService } from '../_services/reader.service'; -import { FormBuilder, FormGroup } from '@angular/forms'; -import { NavService } from '../_services/nav.service'; -import { ReadingDirection } from '../_models/preferences/reading-direction'; -import { ScalingOption } from '../_models/preferences/scaling-option'; -import { PageSplitOption } from '../_models/preferences/page-split-option'; -import { BehaviorSubject, forkJoin, fromEvent, ReplaySubject, Subject } from 'rxjs'; -import { ToastrService } from 'ngx-toastr'; -import { Breakpoint, KEY_CODES, UtilityService } from '../shared/_services/utility.service'; -import { MemberService } from '../_services/member.service'; -import { Stack } from '../shared/data-structures/stack'; -import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider'; +import { BehaviorSubject, debounceTime, distinctUntilChanged, forkJoin, fromEvent, map, merge, Observable, 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 { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums'; -import { layoutModes, pageSplitOptions, scalingOptions } from '../_models/preferences/preferences'; -import { ReaderMode } from '../_models/preferences/reader-mode'; -import { MangaFormat } from '../_models/manga-format'; -import { LibraryType } from '../_models/library'; -import { ShortcutsModalComponent } from '../reader-shared/_modals/shortcuts-modal/shortcuts-modal.component'; +import { FormGroup, FormBuilder, FormControl } from '@angular/forms'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { LayoutMode } from './_models/layout-mode'; +import { ToastrService } from 'ngx-toastr'; +import { ShortcutsModalComponent } from 'src/app/reader-shared/_modals/shortcuts-modal/shortcuts-modal.component'; +import { Stack } from 'src/app/shared/data-structures/stack'; +import { Breakpoint, UtilityService, KEY_CODES } from 'src/app/shared/_services/utility.service'; +import { LibraryType } from 'src/app/_models/library'; +import { MangaFormat } from 'src/app/_models/manga-format'; +import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; +import { scalingOptions, pageSplitOptions, layoutModes } from 'src/app/_models/preferences/preferences'; +import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; +import { ReadingDirection } from 'src/app/_models/preferences/reading-direction'; +import { ScalingOption } from 'src/app/_models/preferences/scaling-option'; +import { User } from 'src/app/_models/user'; +import { AccountService } from 'src/app/_services/account.service'; +import { MemberService } from 'src/app/_services/member.service'; +import { NavService } from 'src/app/_services/nav.service'; +import { ReaderService } from 'src/app/_services/reader.service'; +import { LayoutMode } from '../../_models/layout-mode'; +import { PAGING_DIRECTION, FITTING_OPTION } from '../../_models/reader-enums'; +import { ReaderSetting } from '../../_models/reader-setting'; +import { ManagaReaderService } from '../../_series/managa-reader.service'; +import { CanvasRendererComponent } from '../canvas-renderer/canvas-renderer.component'; +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'; + const PREFETCH_PAGES = 10; @@ -42,6 +48,7 @@ const CLICK_OVERLAY_TIMEOUT = 3000; templateUrl: './manga-reader.component.html', styleUrls: ['./manga-reader.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ManagaReaderService], animations: [ trigger('slideFromTop', [ state('in', style({ transform: 'translateY(0)'})), @@ -71,7 +78,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('reader') reader!: ElementRef; @ViewChild('readingArea') readingArea!: ElementRef; @ViewChild('content') canvas: ElementRef | undefined; - @ViewChild('image') image!: ElementRef; + + @ViewChild(CanvasRendererComponent, { static: false }) canvasRenderer!: CanvasRendererComponent; + @ViewChild(SingleRendererComponent, { static: false }) singleRenderer!: SingleRendererComponent; + @ViewChild(DoubleRendererComponent, { static: false }) doubleRenderer!: DoubleRendererComponent; + @ViewChild(DoubleReverseRendererComponent, { static: false }) doubleReverseRenderer!: DoubleReverseRendererComponent; libraryId!: number; @@ -111,19 +122,28 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { readingDirection = ReadingDirection.LeftToRight; scalingOption = ScalingOption.FitToHeight; pageSplitOption = PageSplitOption.FitSplit; - currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT; - pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD; + isFullscreen: boolean = false; autoCloseMenu: boolean = true; + readerMode: ReaderMode = ReaderMode.LeftRight; + readerModeSubject = new BehaviorSubject(this.readerMode); + readerMode$: Observable = this.readerModeSubject.asObservable(); + + pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD; + pagingDirectionSubject: Subject = new BehaviorSubject(this.pagingDirection); + pagingDirection$: Observable = this.pagingDirectionSubject.asObservable(); + pageSplitOptions = pageSplitOptions; layoutModes = layoutModes; isLoading = true; - hasBookmarkRights: boolean = false; + hasBookmarkRights: boolean = false; // TODO: This can be an observable + + getPageFn!: (pageNum: number) => HTMLImageElement; + - private ctx!: CanvasRenderingContext2D; /** * Used to render a page on the canvas or in the image tag. This Image element is prefetched by the cachedImages buffer. * @remarks Used for rendering to screen. @@ -164,7 +184,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { * A circular array of size PREFETCH_PAGES. Maintains prefetched Images around the current page to load from to avoid loading animation. * @see CircularArray */ - cachedImages!: Array; + cachedImages: Array = []; /** * A stack of the chapter ids we come across during continuous reading mode. When we traverse a boundary, we use this to avoid extra API calls. * @see Stack @@ -174,12 +194,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { /** * An event emitter when a page change occurs. Used solely by the webtoon reader. */ - goToPageEvent!: BehaviorSubject; + goToPageEvent!: BehaviorSubject; // Renderer interaction /** * An event emitter when a bookmark on a page change occurs. Used solely by the webtoon reader. */ showBookmarkEffectEvent: ReplaySubject = new ReplaySubject(); + showBookmarkEffect$: Observable = this.showBookmarkEffectEvent.asObservable(); /** * An event emitter when fullscreen mode is toggled. Used solely by the webtoon reader. */ @@ -188,10 +209,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { * If the menu is open/visible. */ menuOpen = false; - /** - * Image Viewer collapsed - */ - imageViewerCollapsed = true; /** * If the prev page allows a page change to occur. */ @@ -234,6 +251,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { * If the click overlay is rendered on screen */ showClickOverlay: boolean = false; + private showClickOverlaySubject: ReplaySubject = new ReplaySubject(); + showClickOverlay$ = this.showClickOverlaySubject.asObservable(); /** * Next Chapter Id. This is not guaranteed to be a valid ChapterId. Prefetched on page load (non-blocking). */ @@ -288,6 +307,23 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { */ rightPaginationOffset = 0; + // Renderer interaction + readerSettings$!: Observable; + private currentImage: Subject = new ReplaySubject(1); + currentImage$: Observable = this.currentImage.asObservable(); + + private imageFit: Subject = new ReplaySubject(); + private imageFitClass: Subject = new ReplaySubject(); + imageFitClass$: Observable = this.imageFitClass.asObservable(); + imageFit$: Observable = this.imageFit.asObservable(); + + private imageHeight: Subject = new ReplaySubject(); + imageHeight$: Observable = this.imageHeight.asObservable(); + + private pageNumSubject: Subject<{pageNum: number, maxPages: number}> = new ReplaySubject(); + pageNum$: Observable<{pageNum: number, maxPages: number}> = this.pageNumSubject.asObservable(); + + bookmarkPageHandler = this.bookmarkPage.bind(this); getPageUrl = (pageNum: number, chapterId: number = this.chapterId) => { @@ -301,41 +337,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return Math.max(Math.min(this.pageNum, this.maxPages - 1), 0); } - /** - * Determines if we should render a double page. - * The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image - * and the next page is not a wide image (as only non-wides should be shown next to each other). - * @remarks This will always fail if the window's width is greater than the height - */ - get ShouldRenderDoublePage() { - if (this.layoutMode !== LayoutMode.Double) return false; - - return !( - this.isCoverImage() - || this.isWideImage(this.canvasImage) - || this.isWideImage(this.canvasImageNext) - ); - } - - /** - * We should Render 2 pages if: - * 1. We are not currently the first image (cover image) - * 2. The previous page is not a cover image - * 3. The current page is not a wide image - * 4. The next page is not a wide image - */ - get ShouldRenderReverseDouble() { - if (this.layoutMode !== LayoutMode.DoubleReversed) return false; - - const result = !( - this.isCoverImage() - || this.isCoverImage(this.pageNum - 1) // This is because we use prev page and hence the cover will re-show - || this.isWideImage(this.canvasImage) - || this.isWideImage(this.canvasImageNext) - ); - - return result; - } get CurrentPageBookmarked() { return this.bookmarks.hasOwnProperty(this.pageNum); @@ -345,20 +346,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return this.readingArea?.nativeElement.scrollWidth + 'px'; } - get WindowHeight() { - return this.readingArea?.nativeElement.scrollHeight + 'px'; - } - - get ImageWidth() { - return this.image?.nativeElement.width + 'px'; - } - get ImageHeight() { - // If we are a wide image and implied fit to screen, then we need to take screen height rather than image height - if (this.isWideImage() || this.FittingOption === FITTING_OPTION.WIDTH) { - return this.WindowHeight; - } - return Math.max(this.readingArea?.nativeElement?.clientHeight, this.image?.nativeElement.height) + 'px'; + // ?! This doesn't work reliably + //console.log('Reading Area Height: ', this.readingArea?.nativeElement?.clientHeight) + //console.log('Image 1 Height: ', this.document.querySelector('#image-1')?.clientHeight || 0) + //return 'calc(100*var(--vh))'; + return Math.max(this.readingArea?.nativeElement?.clientHeight, this.document.querySelector('#image-1')?.clientHeight || 0) + 'px'; } get RightPaginationOffset() { @@ -369,9 +362,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } get SplitIconClass() { - if (this.isSplitLeftToRight()) { + // NOTE: This could be rewritten to valueChanges.pipe(map()) and | async in the UI instead of the getter + if (this.mangaReaderService.isSplitLeftToRight(this.pageSplitOption)) { return 'left-side'; - } else if (this.isNoSplit()) { + } else if (this.mangaReaderService.isNoSplit(this.pageSplitOption)) { return 'none'; } return 'right-side'; @@ -410,7 +404,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { private toastr: ToastrService, private memberService: MemberService, public utilityService: UtilityService, private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, private modalService: NgbModal, - private readonly cdRef: ChangeDetectorRef, private readonly ngZone: NgZone) { + private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService) { this.navService.hideNavBar(); this.navService.hideSideNav(); this.cdRef.markForCheck(); @@ -425,6 +419,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return; } + this.getPageFn = this.getPage.bind(this); + this.libraryId = parseInt(libraryId, 10); this.seriesId = parseInt(seriesId, 10); this.chapterId = parseInt(chapterId, 10); @@ -456,15 +452,44 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.backgroundColor = this.user.preferences.backgroundColor || '#000000'; this.readerService.setOverrideStyles(this.backgroundColor); - this.generalSettingsForm = this.formBuilder.group({ - autoCloseMenu: this.autoCloseMenu, - pageSplitOption: this.pageSplitOption, - fittingOption: this.translateScalingOption(this.scalingOption), - layoutMode: this.layoutMode, - darkness: 100 + this.generalSettingsForm = this.formBuilder.nonNullable.group({ + autoCloseMenu: new FormControl(this.autoCloseMenu), + pageSplitOption: new FormControl(this.pageSplitOption), + fittingOption: new FormControl(this.mangaReaderService.translateScalingOption(this.scalingOption)), + layoutMode: new FormControl(this.layoutMode), + darkness: new FormControl(100) }); + this.readerModeSubject.next(this.readerMode); + this.pagingDirectionSubject.next(this.pagingDirection); + + + // We need a mergeMap when page changes + this.readerSettings$ = merge(this.generalSettingsForm.valueChanges, this.pagingDirection$, this.readerMode$).pipe( + takeUntil(this.onDestroy), + map(_ => this.createReaderSettingsUpdate()) + ); + this.updateForm(); + + this.pagingDirection$.pipe( + distinctUntilChanged(), + tap(dir => { + this.pagingDirection = dir; + this.cdRef.markForCheck(); + }), + takeUntil(this.onDestroy) + ).subscribe(() => {}); + + this.readerMode$.pipe( + distinctUntilChanged(), + tap(mode => { + this.readerMode = mode; + this.cdRef.markForCheck(); + }), + takeUntil(this.onDestroy) + ).subscribe(() => {}); + this.generalSettingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => { @@ -477,7 +502,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } else { this.generalSettingsForm.get('pageSplitOption')?.setValue(PageSplitOption.NoSplit); this.generalSettingsForm.get('pageSplitOption')?.disable(); - this.generalSettingsForm.get('fittingOption')?.setValue(this.translateScalingOption(ScalingOption.FitToHeight)); + this.generalSettingsForm.get('fittingOption')?.setValue(this.mangaReaderService.translateScalingOption(ScalingOption.FitToHeight)); this.generalSettingsForm.get('fittingOption')?.disable(); } this.cdRef.markForCheck(); @@ -490,9 +515,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => { this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value; - const needsSplitting = this.isWideImage(); + this.pageSplitOption = parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10); + + const needsSplitting = this.mangaReaderService.isWideImage(this.canvasImage); // If we need to split on a menu change, then we need to re-render. if (needsSplitting) { + // If we need to re-render, to ensure things layout properly, let's update paging direction & reset render + this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD); + this.canvasRenderer.reset(); this.loadPage(); } }); @@ -524,11 +554,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (event.detail > 1) return; this.toggleMenu(); }); - - if (this.canvas) { - this.ctx = this.canvas.nativeElement.getContext('2d', { alpha: false }); - this.canvasImage.onload = () => this.renderPage(); - } } ngOnDestroy() { @@ -603,14 +628,26 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } + createReaderSettingsUpdate() { + return { + pageSplit: parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10), + fitting: this.mangaReaderService.translateScalingOption(this.scalingOption), + layoutMode: this.layoutMode, + darkness: 100, + pagingDirection: this.pagingDirection, + readerMode: this.readerMode + }; + } + /** * Gets a page from cache else gets a brand new Image * @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 + * @param chapterId ChapterId to fetch page from. Defaults to current chapterId. Does not search against cached images with chapterId * @returns */ getPage(pageNum: number, chapterId: number = this.chapterId, forceNew: boolean = false) { + // ?! This doesn't compare with chapterId, only for fetching let img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum); if (!img || forceNew) { img = new Image(); @@ -635,7 +672,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return true; } + // This is menu code clickOverlayClass(side: 'right' | 'left') { + // TODO: This needs to be validated with subject if (!this.showClickOverlay) { return ''; } @@ -653,12 +692,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.prevChapterDisabled = false; this.nextChapterPrefetched = false; this.pageNum = 0; - this.pagingDirection = PAGING_DIRECTION.FORWARD; + this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD); this.inSetup = true; this.canvasImage.src = ''; this.canvasImage2.src = ''; this.cdRef.markForCheck(); + this.cachedImages = []; + for (let i = 0; i < PREFETCH_PAGES; i++) { + this.cachedImages.push(new Image()); + } + 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 @@ -680,7 +724,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.inSetup = false; this.cdRef.markForCheck(); - this.cachedImages = []; for (let i = 0; i < PREFETCH_PAGES; i++) { this.cachedImages.push(new Image()) } @@ -714,9 +757,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.setPageNum(page); this.goToPageEvent = new BehaviorSubject(this.pageNum); - - - // Due to change detection rules in Angular, we need to re-create the options object to apply the change const newOptions: Options = Object.assign({}, this.pageOptions); newOptions.ceil = this.maxPages - 1; // We -1 so that the slider UI shows us hitting the end, since visually we +1 everything. @@ -729,7 +769,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.inSetup = false; - // From bookmarks, create map of pages to make lookup time O(1) this.bookmarks = {}; results.bookmarks.forEach(bookmark => { @@ -754,15 +793,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.cdRef.markForCheck(); } else { // Fetch the last page of prev chapter - this.getPage(1000000, this.nextChapterId); + this.getPage(1000000, this.prevChapterId); } }); - this.cachedImages = []; - for (let i = 0; i < PREFETCH_PAGES; i++) { - this.cachedImages.push(new Image()); - } - this.render(); }, () => { @@ -785,68 +819,48 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - translateScalingOption(option: ScalingOption) { - switch (option) { - case (ScalingOption.Automatic): - { - const windowWidth = window.innerWidth - || document.documentElement.clientWidth - || document.body.clientWidth; - const windowHeight = window.innerHeight - || document.documentElement.clientHeight - || document.body.clientHeight; - - const ratio = windowWidth / windowHeight; - if (windowHeight > windowWidth) { - return FITTING_OPTION.WIDTH; - } - - if (windowWidth >= windowHeight || ratio > 1.0) { - return FITTING_OPTION.HEIGHT; - } - return FITTING_OPTION.WIDTH; - } - case (ScalingOption.FitToHeight): - return FITTING_OPTION.HEIGHT; - case (ScalingOption.FitToWidth): - return FITTING_OPTION.WIDTH; - default: - return FITTING_OPTION.ORIGINAL; - } - } - getFittingOptionClass() { const formControl = this.generalSettingsForm.get('fittingOption'); let val = FITTING_OPTION.HEIGHT; if (formControl === undefined) { - val = FITTING_OPTION.HEIGHT; + val = FITTING_OPTION.HEIGHT; } - val = formControl?.value; + val = formControl?.value; + if ( - this.isWideImage() && + this.mangaReaderService.isWideImage(this.canvasImage) && this.layoutMode === LayoutMode.Single && val !== FITTING_OPTION.WIDTH && - this.shouldRenderAsFitSplit() + this.mangaReaderService.shouldRenderAsFitSplit(this.generalSettingsForm.get('pageSplitOption')?.value) ) { // Rewriting to fit to width for this cover image + this.imageFitClass.next(FITTING_OPTION.WIDTH); + this.imageFit.next(FITTING_OPTION.WIDTH); return FITTING_OPTION.WIDTH; } - if (this.isWideImage() && this.layoutMode !== LayoutMode.Single) { + // TODO: Move this to double renderer + if (this.mangaReaderService.isWideImage(this.canvasImage) && this.layoutMode !== LayoutMode.Single) { + this.imageFitClass.next(val + ' wide double'); return val + ' wide double'; } - if (this.isCoverImage() && this.layoutMode !== LayoutMode.Single) { + // TODO: Move this to double renderer + if (this.mangaReaderService.isCoverImage(this.pageNum) && this.layoutMode !== LayoutMode.Single) { + this.imageFitClass.next(val + ' cover double'); return val + ' cover double'; } + this.imageFitClass.next(val); + this.imageFit.next(val); return val; } + getFittingIcon() { const value = this.getFit(); - + // TODO: This can be a pipe switch(value) { case FITTING_OPTION.HEIGHT: return 'fa-arrows-alt-v'; @@ -858,6 +872,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } getFit() { + // TODO: getFit can be refactored with typed form controls so we don't need this + // can't this also just be this.generalSettingsForm.get('fittingOption')?.value || FITTING_OPTION.HEIGHT let value = FITTING_OPTION.HEIGHT; const formControl = this.generalSettingsForm.get('fittingOption'); if (formControl !== undefined) { @@ -910,57 +926,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - isSplitLeftToRight() { - return parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) === PageSplitOption.SplitLeftToRight; - } - - /** - * - * @returns If the current model reflects no split of fit split - * @remarks Fit to Screen falls under no split - */ - isNoSplit() { - const splitValue = parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10); - return splitValue === PageSplitOption.NoSplit || splitValue === PageSplitOption.FitSplit; - } - - updateSplitPage() { - const needsSplitting = this.isWideImage(); - if (!needsSplitting || this.isNoSplit()) { - this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT; - return; - } - - if (this.pagingDirection === PAGING_DIRECTION.FORWARD) { - switch (this.currentImageSplitPart) { - case SPLIT_PAGE_PART.NO_SPLIT: - this.currentImageSplitPart = this.isSplitLeftToRight() ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.RIGHT_PART; - break; - case SPLIT_PAGE_PART.LEFT_PART: - const r2lSplittingPart = (needsSplitting ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.NO_SPLIT); - this.currentImageSplitPart = this.isSplitLeftToRight() ? SPLIT_PAGE_PART.RIGHT_PART : r2lSplittingPart; - break; - case SPLIT_PAGE_PART.RIGHT_PART: - const l2rSplittingPart = (needsSplitting ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.NO_SPLIT); - this.currentImageSplitPart = this.isSplitLeftToRight() ? l2rSplittingPart : SPLIT_PAGE_PART.LEFT_PART; - break; - } - } else if (this.pagingDirection === PAGING_DIRECTION.BACKWARDS) { - switch (this.currentImageSplitPart) { - case SPLIT_PAGE_PART.NO_SPLIT: - this.currentImageSplitPart = this.isSplitLeftToRight() ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.LEFT_PART; - break; - case SPLIT_PAGE_PART.LEFT_PART: - const l2rSplittingPart = (needsSplitting ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.NO_SPLIT); - this.currentImageSplitPart = this.isSplitLeftToRight() ? l2rSplittingPart : SPLIT_PAGE_PART.RIGHT_PART; - break; - case SPLIT_PAGE_PART.RIGHT_PART: - this.currentImageSplitPart = this.isSplitLeftToRight() ? SPLIT_PAGE_PART.LEFT_PART : (needsSplitting ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.NO_SPLIT); - break; - } - } - } - onSwipeEvent(event: any) { console.log('Swipe event occured: ', event); } @@ -987,50 +952,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { event.preventDefault(); } - let pageAmount = 1; + this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD); - // If we are on the cover image, always do 1 page - - if (!this.isCoverImage()) { - if (this.layoutMode === LayoutMode.Double) { - pageAmount = ( - !this.isCoverImage() && - !this.isWideImage() && - !this.isWideImage(this.canvasImageNext) && - !this.isSecondLastImage() && - !this.isLastImage() - ? 2 : 1); - } else if (this.layoutMode === LayoutMode.DoubleReversed) { - // Move forward by 1 pages if: - // 1. The next page is a wide image - // 2. The next page + 1 is a wide image (why do we care at this point?) - // 3. We are on the second to last page - // 4. We are on the last page - pageAmount = !( - this.isWideImage(this.canvasImageNext) - || this.isWideImage(this.canvasImageAheadBy2) // Remember we are doing this logic before we've hit the next page, so we need this - || this.isSecondLastImage() - || this.isLastImage() - ) ? 2 : 1; - } - } - - - const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.RIGHT_PART); - if ((this.pageNum + pageAmount >= this.maxPages && notInSplit) || this.isLoading) { - - if (this.isLoading) { return; } + const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), this.singleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), + this.doubleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), + this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.FORWARD)); + const notInSplit = this.canvasRenderer.shouldMovePrev(); // TODO: Make this generic like above, but by default only canvasRenderer will have logic + //console.log('Next Page, in split: ', !notInSplit, ' page amt: ', pageAmount, ' page: ', this.canvasImage.src); + if ((this.pageNum + pageAmount >= this.maxPages && notInSplit)) { // Move to next volume/chapter automatically this.loadNextChapter(); return; } - this.pagingDirection = PAGING_DIRECTION.FORWARD; - if (this.isNoSplit() || notInSplit) { - this.setPageNum(this.pageNum + pageAmount); - } - + this.setPageNum(this.pageNum + pageAmount); this.loadPage(); } @@ -1039,37 +975,24 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { event.stopPropagation(); event.preventDefault(); } + this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS); - const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.LEFT_PART); - let pageAmount = 1; - if (this.layoutMode === LayoutMode.Double) { - pageAmount = !( - this.isCoverImage() - || this.isWideImage(this.canvasImagePrev) - ) ? 2 : 1; - } else if (this.layoutMode === LayoutMode.DoubleReversed) { - pageAmount = !( - this.isCoverImage() - || this.isCoverImage(this.pageNum - 1) - || this.isWideImage(this.canvasImage) // JOE: At this point, these aren't yet set to the new values - || this.isWideImage(this.canvasImagePrev) // This should be Prev, if prev image (original: canvasImageNext) - ) ? 2 : 1; - } + const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS), + this.singleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS), + this.doubleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS), + this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS)); - if ((this.pageNum - 1 < 0 && notInSplit) || this.isLoading) { - if (this.isLoading) { return; } + const notInSplit = this.canvasRenderer.shouldMovePrev(); + //console.log('Prev Page, not in split: ', notInSplit, ' page amt: ', pageAmount); + if ((this.pageNum - 1 < 0 && notInSplit)) { // Move to next volume/chapter automatically this.loadPrevChapter(); return; } - - this.pagingDirection = PAGING_DIRECTION.BACKWARDS; - if (this.isNoSplit() || notInSplit) { - this.setPageNum(this.pageNum - pageAmount); - } - + + this.setPageNum(this.pageNum - pageAmount); this.loadPage(); } @@ -1077,10 +1000,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { * Sets canvasImage's src to current page, but first attempts to use a pre-fetched image */ setCanvasImage() { - this.canvasImage = this.getPage(this.pageNum); - this.canvasImage.onload = () => { - this.renderPage(); - }; + if (this.cachedImages === undefined) return; + this.canvasImage = this.getPage(this.pageNum, this.chapterId, this.layoutMode !== LayoutMode.Single); + this.canvasImage.addEventListener('load', () => { + this.currentImage.next(this.canvasImage); + //this.renderPage(); // This can execute before cachedImages are ready + }, false); this.cdRef.markForCheck(); } @@ -1152,72 +1077,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - /** - * There are some hard limits on the size of canvas' that we must cap at. https://github.com/jhildenbiddle/canvas-size#test-results - * For Safari, it's 16,777,216, so we cap at 4096x4096 when this happens. The drawImage in render will perform bi-cubic scaling for us. - * @returns If we should continue to the render loop - */ - setCanvasSize() { - if (this.ctx && this.canvas) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const isSafari = [ - 'iPad Simulator', - 'iPhone Simulator', - 'iPod Simulator', - 'iPad', - 'iPhone', - 'iPod' - ].includes(navigator.platform) - // iPad on iOS 13 detection - || (navigator.userAgent.includes("Mac") && "ontouchend" in document); - const canvasLimit = isSafari ? 16_777_216 : 124_992_400; - const needsScaling = this.canvasImage.width * this.canvasImage.height > canvasLimit; - if (needsScaling) { - this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384; - this.canvas.nativeElement.height = isSafari ? 4_096 : 16_384; - } else { - this.canvas.nativeElement.width = this.canvasImage.width; - this.canvas.nativeElement.height = this.canvasImage.height; - } - this.cdRef.markForCheck(); - } - } + renderPage() { - const needsSplitting = this.isWideImage(); - - if (!this.ctx || !this.canvas || this.isNoSplit() || !needsSplitting) { - this.renderWithCanvas = false; - if (this.getFit() !== FITTING_OPTION.HEIGHT) { - this.readingArea.nativeElement.scroll(0,0); - } - this.isLoading = false; - this.cdRef.markForCheck(); - return; - } + console.log('[Manga Reader] renderPage()'); - this.renderWithCanvas = true; - this.canvasImage.onload = null; + const page = [this.canvasImage]; + this.canvasRenderer.renderPage(page); + this.singleRenderer.renderPage(page); + this.doubleRenderer.renderPage(page); + this.doubleReverseRenderer.renderPage(page); - this.setCanvasSize(); - this.updateSplitPage(); - - if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) { - this.canvas.nativeElement.width = this.canvasImage.width / 2; - this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, 0, 0, this.canvasImage.width, this.canvasImage.height); - this.cdRef.markForCheck(); - } else if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.RIGHT_PART) { - this.canvas.nativeElement.width = this.canvasImage.width / 2; - this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height); - this.cdRef.markForCheck(); - } - - // Reset scroll on non HEIGHT Fits if (this.getFit() !== FITTING_OPTION.HEIGHT) { - this.readingArea.nativeElement.scroll(0,0); + this.readingArea.nativeElement.scroll(0,0); } - this.isLoading = false; + this.cdRef.markForCheck(); } @@ -1229,7 +1103,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { || document.documentElement.clientHeight || document.body.clientHeight; - const needsSplitting = this.isWideImage(); + const needsSplitting = this.mangaReaderService.isWideImage(this.canvasImage); let newScale = this.FittingOption; const widthRatio = windowWidth / (this.canvasImage.width / (needsSplitting ? 2 : 1)); const heightRatio = windowHeight / (this.canvasImage.height); @@ -1245,51 +1119,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.generalSettingsForm.get('fittingOption')?.setValue(newScale, {emitEvent: false}); } - /** - * If pagenumber is 0 aka first page, which on double page rendering should always render as a single. - * - * @param pageNumber Defaults to current page number - * @returns - */ - isCoverImage(pageNumber = this.pageNum) { - return pageNumber === 0; - } - - /** - * If the image's width is greater than it's height - * @param elem Optional Image - */ - isWideImage(elem?: HTMLImageElement) { - if (elem) { - elem.onload = () => { - return elem.width > elem.height; - } - if (elem.src === '') return false; - } - const element = elem || this.canvasImage; - return element.width > element.height; - } - - /** - * If the current page is second to last image - */ - isSecondLastImage() { - return this.maxPages - 1 - this.pageNum === 1; - } - - /** - * If the current image is last image - */ - isLastImage() { - return this.maxPages - 1 === this.pageNum; - } - - shouldRenderAsFitSplit() { - // Some pages aren't cover images but might need fit split renderings - if (parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) !== PageSplitOption.FitSplit) return false; - return true; - } - /** * Maintains an array of images (that are requested from backend) around the user's current page. This allows for quick loading (seemless to user) * and also maintains page info (wide image, etc) due to onload event. @@ -1306,17 +1135,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.readerService.imageUrlToPageNum(this.cachedImages[index].src) !== numOffset) { this.cachedImages[index] = new Image(); this.cachedImages[index].src = this.getPageUrl(numOffset); - this.cachedImages[index].onload = () => { - //console.log('Page ', numOffset, ' loaded'); - //this.cdRef.markForCheck(); - }; } } 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); + //console.log('Buffer Health: Before: ', pagesBefore, ' After: ', pagesAfter); console.log(this.pageNum, ' Prefetched pages: ', pages.map(p => { if (this.pageNum === p) return '[' + p + ']'; return '' + p @@ -1324,66 +1149,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } + /** + * This is responsible for setting up the image variables. This will be moved out to different renderers + */ loadPage() { if (this.readerMode === ReaderMode.Webtoon) return; this.isLoading = true; - this.canvasImage2.src = ''; - this.canvasImageAheadBy2.src = ''; - - this.setCanvasImage(); - - - // ?! This logic is hella complex and confusing to read - // ?! We need to refactor into separate methods and keep it clean - // ?! In addition, we shouldn't update canvasImage outside of this code - - if (this.layoutMode !== LayoutMode.Single) { - - this.canvasImageNext = new Image(); - - - // If prev page was a spread, then we don't do + 1 - console.log('Current canvas image page: ', this.readerService.imageUrlToPageNum(this.canvasImage.src)); - console.log('Prev canvas image page: ', this.readerService.imageUrlToPageNum(this.canvasImage2.src)); - // if (this.isWideImage(this.canvasImage2)) { - // this.canvasImagePrev = this.getPage(this.pageNum); // this.getPageUrl(this.pageNum); - // console.log('Setting Prev to ', this.pageNum); - // } else { - // this.canvasImagePrev = this.getPage(this.pageNum - 1); //this.getPageUrl(this.pageNum - 1); - // console.log('Setting Prev to ', this.pageNum - 1); - // } - - // TODO: Validate this statement: This needs to be capped at maxPages !this.isLastImage() - this.canvasImageNext = this.getPage(this.pageNum + 1); - console.log('Setting Next to ', this.pageNum + 1); - - this.canvasImagePrev = this.getPage(this.pageNum - 1); - console.log('Setting Prev to ', this.pageNum - 1); - - if (this.pageNum + 2 < this.maxPages - 1) { - this.canvasImageAheadBy2 = this.getPage(this.pageNum + 2); - } - if (this.pageNum - 2 >= 0) { - this.canvasImageBehindBy2 = this.getPage(this.pageNum - 2 || 0); - } - - if (this.ShouldRenderDoublePage || this.ShouldRenderReverseDouble) { - console.log('Rendering Double Page'); - if (this.layoutMode === LayoutMode.Double) { - this.canvasImage2 = this.canvasImageNext; - } else { - this.canvasImage2 = this.canvasImagePrev; - } - } - } - - this.cdRef.markForCheck(); + this.renderPage(); - this.prefetch(); + this.isLoading = false; this.cdRef.markForCheck(); + + this.prefetch(); } setReadingDirection() { @@ -1395,8 +1175,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.menuOpen && this.user.preferences.showScreenHints) { this.showClickOverlay = true; + this.showClickOverlaySubject.next(true); setTimeout(() => { this.showClickOverlay = false; + this.showClickOverlaySubject.next(false); }, CLICK_OVERLAY_TIMEOUT); } } @@ -1414,9 +1196,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { const page = context.value; if (page > this.pageNum) { - this.pagingDirection = PAGING_DIRECTION.FORWARD; + this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD); } else { - this.pagingDirection = PAGING_DIRECTION.BACKWARDS; + this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS); } this.setPageNum(page); @@ -1427,6 +1209,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { setPageNum(pageNum: number) { this.pageNum = Math.max(Math.min(pageNum, this.maxPages - 1), 0); + this.pageNumSubject.next({pageNum: this.pageNum, maxPages: this.maxPages}); this.cdRef.markForCheck(); if (this.pageNum >= this.maxPages - 10) { @@ -1471,9 +1254,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } if (page > this.pageNum) { - this.pagingDirection = PAGING_DIRECTION.FORWARD; + this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD); } else { - this.pagingDirection = PAGING_DIRECTION.BACKWARDS; + this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS); } this.setPageNum(page); @@ -1481,12 +1264,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.render(); } + // This is menu only code promptForPage() { const goToPageNum = window.prompt('There are ' + this.maxPages + ' pages. What page would you like to go to?', ''); if (goToPageNum === null || goToPageNum.trim().length === 0) { return null; } return goToPageNum; } + // This is menu only code toggleFullscreen() { this.isFullscreen = this.readerService.checkFullscreenMode(); if (this.isFullscreen) { @@ -1504,24 +1289,25 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - + // This is menu only code toggleReaderMode() { switch(this.readerMode) { case ReaderMode.LeftRight: - this.readerMode = ReaderMode.UpDown; - this.pagingDirection = PAGING_DIRECTION.FORWARD; + this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD); + this.readerModeSubject.next(ReaderMode.UpDown); break; case ReaderMode.UpDown: - this.readerMode = ReaderMode.Webtoon; + this.readerModeSubject.next(ReaderMode.Webtoon); break; case ReaderMode.Webtoon: - this.readerMode = ReaderMode.LeftRight; + this.readerModeSubject.next(ReaderMode.LeftRight); break; } // 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.currentImage.next(this.canvasImage); this.isLoading = true; } @@ -1530,6 +1316,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.render(); } + // This is menu only code updateForm() { if ( this.readerMode === ReaderMode.Webtoon) { this.generalSettingsForm.get('pageSplitOption')?.disable() @@ -1583,31 +1370,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { // Show an effect on the image to show that it was bookmarked this.showBookmarkEffectEvent.next(pageNum); - if (this.readerMode === ReaderMode.Webtoon) return; - - let elements:Array = []; - if (this.renderWithCanvas && this.canvas) { - elements.push(this.canvas?.nativeElement); - } else { - const image1 = this.document.querySelector('#image-1'); - if (image1 != null) elements.push(image1); - - if (this.layoutMode !== LayoutMode.Single) { - const image2 = this.document.querySelector('#image-2'); - if (image2 != null) elements.push(image2); - } - } - - - if (elements.length > 0) { - elements.forEach(elem => this.renderer.addClass(elem, 'bookmark-effect')); - setTimeout(() => { - elements.forEach(elem => this.renderer.removeClass(elem, 'bookmark-effect')); - }, 1000); - } - } + // This is menu only code /** * Turns off Incognito mode. This can only happen once if the user clicks the icon. This will modify URL state */ @@ -1621,6 +1386,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } + // This is menu only code openShortcutModal() { let ref = this.modalService.open(ShortcutsModalComponent, { scrollable: true, size: 'md' }); ref.componentInstance.shortcuts = [ @@ -1630,6 +1396,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { {key: '↓', description: 'Move to previous page'}, {key: 'G', description: 'Open Go to Page dialog'}, {key: 'B', description: 'Bookmark current page'}, + {key: 'double click', description: 'Bookmark current page'}, {key: 'ESC', description: 'Close reader'}, {key: 'SPACE', description: 'Toggle Menu'}, ]; diff --git a/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.html b/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.html new file mode 100644 index 000000000..e92dac737 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.html @@ -0,0 +1,10 @@ +
+ +  + +
\ No newline at end of file diff --git a/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.scss b/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.scss new file mode 100644 index 000000000..daeafd50b --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.scss @@ -0,0 +1 @@ +@use '../../../../manga-reader-common'; \ No newline at end of file diff --git a/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.ts new file mode 100644 index 000000000..cd25e34cb --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.ts @@ -0,0 +1,141 @@ +import { DOCUMENT } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { filter, map, Observable, of, Subject, takeUntil, tap, zip } from 'rxjs'; +import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; +import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; +import { LayoutMode } from '../../_models/layout-mode'; +import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums'; +import { ReaderSetting } from '../../_models/reader-setting'; +import { ImageRenderer } from '../../_models/renderer'; +import { ManagaReaderService } from '../../_series/managa-reader.service'; + +@Component({ + selector: 'app-single-renderer', + templateUrl: './single-renderer.component.html', + styleUrls: ['./single-renderer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer { + + @Input() readerSettings$!: Observable; + @Input() image$!: Observable; + /** + * The image fit class + */ + @Input() imageFit$!: Observable; + @Input() bookmark$!: Observable; + @Input() showClickOverlay$!: Observable; + + @Output() imageHeight: EventEmitter = new EventEmitter(); + + imageFitClass$!: Observable; + showClickOverlayClass$!: Observable; + readerModeClass$!: Observable; + darkenss$: Observable = of('brightness(100%)'); + currentImage!: HTMLImageElement; + layoutMode: LayoutMode = LayoutMode.Single; + pageSplit: PageSplitOption = PageSplitOption.FitSplit; + + private readonly onDestroy = new Subject(); + + get ReaderMode() {return ReaderMode;} + get LayoutMode() {return LayoutMode;} + + constructor(private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService, + @Inject(DOCUMENT) private document: Document) { } + + ngOnInit(): void { + this.readerModeClass$ = this.readerSettings$.pipe( + filter(_ => this.isValid()), + map(values => values.readerMode), + map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'), + takeUntil(this.onDestroy) + ); + + this.darkenss$ = this.readerSettings$.pipe( + filter(_ => this.isValid()), + map(values => 'brightness(' + values.darkness + '%)'), + takeUntil(this.onDestroy) + ); + + this.showClickOverlayClass$ = this.showClickOverlay$.pipe( + filter(_ => this.isValid()), + map(showOverlay => showOverlay ? 'blur' : ''), + takeUntil(this.onDestroy) + ); + + this.readerSettings$.pipe( + takeUntil(this.onDestroy), + tap(values => { + this.layoutMode = values.layoutMode; + this.pageSplit = values.pageSplit; + this.cdRef.markForCheck(); + }) + ).subscribe(() => {}); + + this.bookmark$.pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + tap(_ => { + const elements = []; + const image1 = this.document.querySelector('#image-1'); + if (image1 != null) elements.push(image1); + this.mangaReaderService.applyBookmarkEffect(elements); + }) + ).subscribe(() => {}); + + this.imageFitClass$ = zip(this.readerSettings$, this.image$).pipe( + takeUntil(this.onDestroy), + filter(_ => this.isValid()), + map(values => values[0].fitting), + map(fit => { + if ( + this.mangaReaderService.isWideImage(this.currentImage) && + this.layoutMode === LayoutMode.Single && + fit !== FITTING_OPTION.WIDTH && + this.mangaReaderService.shouldRenderAsFitSplit(this.pageSplit) + ) { + // Rewriting to fit to width for this cover image + console.log('overridding for fit to screen'); + return FITTING_OPTION.WIDTH; + } + return fit; + }) + ); + } + + isValid() { + return this.layoutMode === LayoutMode.Single; + } + + ngOnDestroy(): void { + this.onDestroy.next(); + this.onDestroy.complete(); + } + + renderPage(img: Array): void { + if (img === null || img.length === 0 || img[0] === null) return; + if (!this.isValid()) return; + + // This seems to cause a problem after rendering a split + //if (this.mangaReaderService.shouldSplit(this.currentImage, this.pageSplit)) return; + + + + this.currentImage = img[0]; + this.cdRef.markForCheck(); + this.imageHeight.emit(this.currentImage.height); + } + + shouldMovePrev(): boolean { + return true; + } + shouldMoveNext(): boolean { + return true; + } + getPageAmount(direction: PAGING_DIRECTION): number { + if (!this.isValid() || this.mangaReaderService.shouldSplit(this.currentImage, this.pageSplit)) return 0; + return 1; + } + reset(): void {} +} diff --git a/UI/Web/src/app/manga-reader/_models/reader-setting.ts b/UI/Web/src/app/manga-reader/_models/reader-setting.ts new file mode 100644 index 000000000..aae62ed7c --- /dev/null +++ b/UI/Web/src/app/manga-reader/_models/reader-setting.ts @@ -0,0 +1,13 @@ +import { PageSplitOption } from "src/app/_models/preferences/page-split-option"; +import { ReaderMode } from "src/app/_models/preferences/reader-mode"; +import { LayoutMode } from "./layout-mode"; +import { FITTING_OPTION, PAGING_DIRECTION } from "./reader-enums"; + +export interface ReaderSetting { + pageSplit: PageSplitOption; + fitting: FITTING_OPTION; + layoutMode: LayoutMode; + darkness: number; + pagingDirection: PAGING_DIRECTION; + readerMode: ReaderMode; +} \ No newline at end of file diff --git a/UI/Web/src/app/manga-reader/_models/renderer.ts b/UI/Web/src/app/manga-reader/_models/renderer.ts new file mode 100644 index 000000000..530a5c128 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_models/renderer.ts @@ -0,0 +1,45 @@ +import { Observable } from "rxjs"; +import { PAGING_DIRECTION } from "./reader-enums"; +import { ReaderSetting } from "./reader-setting"; + +/** + * A generic interface for an image renderer + */ +export interface ImageRenderer { + + /** + * Updates with menu items that may affect renderer. This keeps reader and menu/parent in sync. + */ + readerSettings$: Observable; + /** + * The current Image + */ + image$: Observable; + /** + * When a page is bookmarked or unbookmarked. Emits with page number. + */ + bookmark$: Observable; + /** + * Performs a rendering pass. This is passed one or more images to render from prefetcher + */ + renderPage(img: Array): void; + /** + * If a valid move next page should occur, this will return true. Otherwise, this will return false. + */ + shouldMovePrev(): boolean; + /** + * If a valid move prev page should occur, this will return true. Otherwise, this will return false. + */ + shouldMoveNext(): boolean; + /** + * Returns the number of pages that should occur based on page direction and internal state of the renderer. + */ + getPageAmount(direction: PAGING_DIRECTION): number; + /** + * When layout shifts occur, where a re-render might be needed but from menu option (like split option changed on a split image), this will be called. + * This should reset any needed state, but not unset the image. + */ + reset(): void; + + +} \ No newline at end of file diff --git a/UI/Web/src/app/manga-reader/_series/managa-reader.service.ts b/UI/Web/src/app/manga-reader/_series/managa-reader.service.ts new file mode 100644 index 000000000..9ea45502a --- /dev/null +++ b/UI/Web/src/app/manga-reader/_series/managa-reader.service.ts @@ -0,0 +1,138 @@ +import { DOCUMENT } from '@angular/common'; +import { ElementRef, Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +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 { FITTING_OPTION } from '../_models/reader-enums'; + +@Injectable({ + providedIn: 'root' +}) +export class ManagaReaderService { + + private renderer: Renderer2; + constructor(rendererFactory: RendererFactory2, @Inject(DOCUMENT) private document: Document, private readerService: ReaderService) { + this.renderer = rendererFactory.createRenderer(null, null); + } + + + + /** + * If the image's width is greater than it's height + * @param elem Image + */ + isWideImage(elem: HTMLImageElement) { + if (!elem) return false; + if (elem) { + elem.addEventListener('load', () => { + return elem.width > elem.height; + }, false); + if (elem.src === '') return false; + } + return elem.width > elem.height; + } + + /** + * If pagenumber is 0 aka first page, which on double page rendering should always render as a single. + * + * @param pageNumber current page number + * @returns + */ + isCoverImage(pageNumber: number) { + return pageNumber === 0; + } + + /** + * Does the image need + * @returns If the current model reflects no split of fit split + * @remarks Fit to Screen falls under no split + */ + isNoSplit(pageSplitOption: PageSplitOption) { + const splitValue = parseInt(pageSplitOption + '', 10); // Just in case it's a string from form + return splitValue === PageSplitOption.NoSplit || splitValue === PageSplitOption.FitSplit; + } + + /** + * If the split option is Left to Right. This means that the Left side of the image renders before the Right side. + * In other words, If you were to visualize the parts as pages, Left is Page 0, Right is Page 1 + */ + isSplitLeftToRight(pageSplitOption: PageSplitOption) { + return parseInt(pageSplitOption + '', 10) === PageSplitOption.SplitLeftToRight; + } + + /** + * If the current page is second to last image + */ + isSecondLastImage(pageNum: number, maxPages: number) { + return maxPages - 1 - pageNum === 2; + } + + /** + * If the current image is last image + */ + isLastImage(pageNum: number, maxPages: number) { + return maxPages - 1 === pageNum; + } + + /** + * Should Canvas Renderer be used + * @param img + * @param pageSplitOption + * @returns + */ + shouldSplit(img: HTMLImageElement, pageSplitOption: PageSplitOption) { + const needsSplitting = this.isWideImage(img); + return !(this.isNoSplit(pageSplitOption) || !needsSplitting) + } + + shouldRenderAsFitSplit(pageSplitOption: PageSplitOption) { + // Some pages aren't cover images but might need fit split renderings + if (parseInt(pageSplitOption + '', 10) !== PageSplitOption.FitSplit) return false; + return true; + } + + + translateScalingOption(option: ScalingOption) { + switch (option) { + case (ScalingOption.Automatic): + { + const windowWidth = window.innerWidth + || document.documentElement.clientWidth + || document.body.clientWidth; + const windowHeight = window.innerHeight + || document.documentElement.clientHeight + || document.body.clientHeight; + + const ratio = windowWidth / windowHeight; + if (windowHeight > windowWidth) { + return FITTING_OPTION.WIDTH; + } + + if (windowWidth >= windowHeight || ratio > 1.0) { + return FITTING_OPTION.HEIGHT; + } + return FITTING_OPTION.WIDTH; + } + case (ScalingOption.FitToHeight): + return FITTING_OPTION.HEIGHT; + case (ScalingOption.FitToWidth): + return FITTING_OPTION.WIDTH; + default: + return FITTING_OPTION.ORIGINAL; + } + } + + + applyBookmarkEffect(elements: Array) { + if (elements.length > 0) { + elements.forEach(elem => this.renderer.addClass(elem, 'bookmark-effect')); + setTimeout(() => { + elements.forEach(elem => this.renderer.removeClass(elem, 'bookmark-effect')); + }, 1000); + } + } + + + + +} diff --git a/UI/Web/src/app/manga-reader/manga-reader.module.ts b/UI/Web/src/app/manga-reader/manga-reader.module.ts index 4712fb2ec..00ed28a14 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.module.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.module.ts @@ -1,17 +1,22 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { MangaReaderComponent } from './manga-reader.component'; import { ReactiveFormsModule } from '@angular/forms'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { MangaReaderRoutingModule } from './manga-reader.router.module'; import { SharedModule } from '../shared/shared.module'; import { NgxSliderModule } from '@angular-slider/ngx-slider'; -import { InfiniteScrollerComponent } from './infinite-scroller/infinite-scroller.component'; +import { InfiniteScrollerComponent } from './_components/infinite-scroller/infinite-scroller.component'; import { ReaderSharedModule } from '../reader-shared/reader-shared.module'; import { PipeModule } from '../pipe/pipe.module'; import { FullscreenIconPipe } from './_pipes/fullscreen-icon.pipe'; import { LayoutModeIconPipe } from './_pipes/layout-mode-icon.pipe'; import { ReaderModeIconPipe } from './_pipes/reader-mode-icon.pipe'; +import { SwipeDirective } from './swipe.directive'; +import { CanvasRendererComponent } from './_components/canvas-renderer/canvas-renderer.component'; +import { SingleRendererComponent } from './_components/single-renderer/single-renderer.component'; +import { DoubleRendererComponent } from './_components/double-renderer/double-renderer.component'; +import { DoubleReverseRendererComponent } from './_components/double-reverse-renderer/double-reverse-renderer.component'; +import { MangaReaderComponent } from './_components/manga-reader/manga-reader.component'; @NgModule({ declarations: [ @@ -20,6 +25,11 @@ import { ReaderModeIconPipe } from './_pipes/reader-mode-icon.pipe'; FullscreenIconPipe, ReaderModeIconPipe, LayoutModeIconPipe, + SwipeDirective, + CanvasRendererComponent, + SingleRendererComponent, + DoubleRendererComponent, + DoubleReverseRendererComponent, ], imports: [ CommonModule, diff --git a/UI/Web/src/app/manga-reader/manga-reader.router.module.ts b/UI/Web/src/app/manga-reader/manga-reader.router.module.ts index 680c7cde7..b97e57eb1 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.router.module.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.router.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { MangaReaderComponent } from './manga-reader.component'; +import { MangaReaderComponent } from './_components/manga-reader/manga-reader.component'; const routes: Routes = [ { diff --git a/UI/Web/src/app/manga-reader/swipe.directive.ts b/UI/Web/src/app/manga-reader/swipe.directive.ts new file mode 100644 index 000000000..b94533da0 --- /dev/null +++ b/UI/Web/src/app/manga-reader/swipe.directive.ts @@ -0,0 +1,39 @@ +import { Directive, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core'; +import { fromEvent, map, Observable } from 'rxjs'; + +/** + * Repsonsible for triggering a swipe event + */ +@Directive({ + selector: '[appSwipe]' +}) +export class SwipeDirective { + + @Input() threshold: number = 10; + @Output() swipeEvent: EventEmitter = new EventEmitter(); + + touchStarts$!: Observable; + touchMoves$!: Observable; + touchEnds$!: Observable; + touchCancels$!: Observable; + + @HostListener('touchstart') onTouchStart(event: TouchEvent) { + console.log('Touch Start: ', event); + } + + @HostListener('touchend') onTouchEnd(event: TouchEvent) { + console.log('Touch End: ', event); + } + + constructor(private el: ElementRef) { + this.touchStarts$ = fromEvent(el.nativeElement, 'touchstart').pipe(map(this.getTouchCoordinates)); + this.touchMoves$ = fromEvent(el.nativeElement, 'touchmove').pipe(map(this.getTouchCoordinates)); + this.touchEnds$ = fromEvent(el.nativeElement, 'touchend').pipe(map(this.getTouchCoordinates)); + this.touchCancels$ = fromEvent(el.nativeElement, 'touchcancel'); + } + + getTouchCoordinates(event: TouchEvent) { + + } + +} diff --git a/UI/Web/src/app/registration/_components/splash-container/splash-container.component.scss b/UI/Web/src/app/registration/_components/splash-container/splash-container.component.scss index e710a7c34..79c6ad7a7 100644 --- a/UI/Web/src/app/registration/_components/splash-container/splash-container.component.scss +++ b/UI/Web/src/app/registration/_components/splash-container/splash-container.component.scss @@ -11,7 +11,7 @@ &::before { content: ""; - background-image: url('../../../assets/images/login-bg.jpg'); + background-image: url('../../../../assets/images/login-bg.jpg'); background-size: cover; position: absolute; top: 0; diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 593cfd3c7..4be6d367b 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -21,7 +21,6 @@ import { ScanSeriesEvent } from 'src/app/_models/events/scan-series-event'; import { SeriesRemovedEvent } from 'src/app/_models/events/series-removed-event'; import { LibraryType } from 'src/app/_models/library'; import { MangaFormat } from 'src/app/_models/manga-format'; -import { PageLayoutMode } from 'src/app/_models/readers/page-layout-mode'; import { ReadingList } from 'src/app/_models/reading-list'; import { Series } from 'src/app/_models/series'; import { RelatedSeries } from 'src/app/_models/series-detail/related-series'; @@ -42,6 +41,7 @@ import { ReadingListService } from 'src/app/_services/reading-list.service'; import { ScrollService } from 'src/app/_services/scroll.service'; import { SeriesService } from 'src/app/_services/series.service'; import { ReviewSeriesModalComponent } from '../../_modals/review-series-modal/review-series-modal.component'; +import { PageLayoutMode } from 'src/app/_models/page-layout-mode'; interface RelatedSeris { series: Series; diff --git a/UI/Web/src/app/typeahead/_components/typeahead.component.ts b/UI/Web/src/app/typeahead/_components/typeahead.component.ts index 8273095f9..2c9984766 100644 --- a/UI/Web/src/app/typeahead/_components/typeahead.component.ts +++ b/UI/Web/src/app/typeahead/_components/typeahead.component.ts @@ -62,14 +62,12 @@ export class SelectionModel { * @returns boolean */ isSelected(data: T, compareFn?: SelectionCompareFn): boolean { - let dataItem: Array; - let lookupMethod = this.shallowEqual; if (compareFn != undefined || compareFn != null) { lookupMethod = compareFn; } - dataItem = this._data.filter(d => lookupMethod(d.value, data)); + const dataItem = this._data.filter(d => lookupMethod(d.value, data)); if (dataItem.length > 0) { return dataItem[0].selected; @@ -114,24 +112,18 @@ export class SelectionModel { return undefined; } - shallowEqual(object1: T, object2: T) { - if (object1 === undefined || object2 === undefined) return false; + shallowEqual(a: T, b: T) { - if (typeof(object1) === 'string' && typeof(object2) === 'string') return object1 === object2; - - const keys1 = Object.keys(object1); - const keys2 = Object.keys(object2); - - if (keys1.length !== keys2.length) { - return false; - } - - for (let key of keys1) { - if ((object1 as any)[key] !== (object2 as any)[key]) { + for (let key in a) { + if (!(key in b) || a[key] !== b[key]) { + return false; + } + } + for (let key in b) { + if (!(key in a)) { return false; } } - return true; } } diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index e250cfe44..027b97568 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -3,15 +3,15 @@ import { FormControl, FormGroup } from '@angular/forms'; import { ToastrService } from 'ngx-toastr'; import { take, takeUntil } from 'rxjs/operators'; import { Title } from '@angular/platform-browser'; -import { BookService } from 'src/app/book-reader/_services/book.service'; import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences, bookLayoutModes, layoutModes, pageLayoutModes } from 'src/app/_models/preferences/preferences'; import { User } from 'src/app/_models/user'; import { AccountService } from 'src/app/_services/account.service'; import { ActivatedRoute, Router } from '@angular/router'; import { SettingsService } from 'src/app/admin/settings.service'; -import { bookColorThemes } from 'src/app/book-reader/reader-settings/reader-settings.component'; import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode'; import { forkJoin, Subject } from 'rxjs'; +import { bookColorThemes } from 'src/app/book-reader/_components/reader-settings/reader-settings.component'; +import { BookService } from 'src/app/book-reader/_services/book.service'; enum AccordionPanelID { ImageReader = 'image-reader', diff --git a/UI/Web/src/theme/utilities/_animations.scss b/UI/Web/src/theme/utilities/_animations.scss index cd4041f84..158ddb36b 100644 --- a/UI/Web/src/theme/utilities/_animations.scss +++ b/UI/Web/src/theme/utilities/_animations.scss @@ -10,4 +10,13 @@ 50% { transform: translateY(-10px); } +} + +@keyframes bookmark { + 0%, 100% { + border: 0px; + } + 50% { + border: 5px solid var(--primary-color); + } } \ No newline at end of file