diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index 467b55564..e3882730f 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -43,6 +43,6 @@ export const readingDirections = [{text: 'Left to Right', value: ReadingDirectio export const scalingOptions = [{text: 'Automatic', value: ScalingOption.Automatic}, {text: 'Fit to Height', value: ScalingOption.FitToHeight}, {text: 'Fit to Width', value: ScalingOption.FitToWidth}, {text: 'Original', value: ScalingOption.Original}]; export const pageSplitOptions = [{text: 'Fit to Screen', value: PageSplitOption.FitSplit}, {text: 'Right to Left', value: PageSplitOption.SplitRightToLeft}, {text: 'Left to Right', value: PageSplitOption.SplitLeftToRight}, {text: 'No Split', value: PageSplitOption.NoSplit}]; export const readingModes = [{text: 'Left to Right', value: ReaderMode.LeftRight}, {text: 'Up to Down', value: ReaderMode.UpDown}, {text: 'Webtoon', value: ReaderMode.Webtoon}]; -export const layoutModes = [{text: 'Single', value: LayoutMode.Single}, {text: 'Double', value: LayoutMode.Double}, {text: 'Double (Manga)', value: LayoutMode.DoubleReversed}]; +export const layoutModes = [{text: 'Single', value: LayoutMode.Single}, {text: 'Double', value: LayoutMode.Double}, {text: 'Double (Manga)', value: LayoutMode.DoubleReversed}]; // , {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover} export const bookLayoutModes = [{text: 'Scroll', value: BookPageLayoutMode.Default}, {text: '1 Column', value: BookPageLayoutMode.Column1}, {text: '2 Column', value: BookPageLayoutMode.Column2}]; export const pageLayoutModes = [{text: 'Cards', value: PageLayoutMode.Cards}, {text: 'List', value: PageLayoutMode.List}]; diff --git a/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.html b/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.html new file mode 100644 index 000000000..e0993cda2 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-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-no-cover/double-no-cover-renderer.component.scss b/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.scss new file mode 100644 index 000000000..f2d9d0f7c --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.scss @@ -0,0 +1,50 @@ +@use '../../../../manga-reader-common'; + +.image-container { + #image-1 { + &.double { + margin: 0 0 0 auto; + } + } +} + +.image-container.full-height { + display: inline-block !important; +} + +.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-no-cover/double-no-cover-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.ts new file mode 100644 index 000000000..66953d028 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.ts @@ -0,0 +1,298 @@ +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, combineLatest } 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 { DEBUG_MODES, ImageRenderer } from '../../_models/renderer'; +import { ManagaReaderService } from '../../_series/managa-reader.service'; + +/** + * Renders 2 pages except on last page, and before a wide image + */ +@Component({ + selector: 'app-double-no-cover-renderer', + templateUrl: './double-no-cover-renderer.component.html', + styleUrls: ['./double-no-cover-renderer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DoubleNoCoverRendererComponent implements OnInit { + + @Input() readerSettings$!: Observable; + @Input() image$!: Observable; + @Input() bookmark$!: Observable; + @Input() showClickOverlay$!: Observable; + @Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>; + @Input() getPage!: (pageNum: number) => HTMLImageElement; + @Output() imageHeight: EventEmitter = new EventEmitter(); + + debugMode: DEBUG_MODES = DEBUG_MODES.Logs; + imageFitClass$!: Observable; + showClickOverlayClass$!: Observable; + readerModeClass$!: Observable; + layoutClass$!: Observable; + darkenss$: Observable = of('brightness(100%)'); + emulateBookClass$: Observable = of(''); + 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(); + + /** + * 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( + map(values => values.readerMode), + map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'), + filter(_ => this.isValid()), + takeUntil(this.onDestroy) + ); + + this.darkenss$ = this.readerSettings$.pipe( + map(values => 'brightness(' + values.darkness + '%)'), + filter(_ => this.isValid()), + takeUntil(this.onDestroy) + ); + + this.emulateBookClass$ = this.readerSettings$.pipe( + map(data => data.emulateBook), + map(enabled => enabled ? 'book-shadow' : ''), + 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), + tap(pageInfo => { + this.pageNum = pageInfo.pageNum; + this.maxPages = pageInfo.maxPages; + + this.currentImage = this.getPage(this.pageNum); + this.currentImage2 = this.getPage(this.pageNum + 1); + + this.cdRef.markForCheck(); + }), + filter(_ => this.isValid()), + ).subscribe(() => {}); + + this.shouldRenderDouble$ = this.pageNum$.pipe( + takeUntil(this.onDestroy), + map((_) => this.shouldRenderDouble()), + filter(_ => this.isValid()), + ); + + this.imageFitClass$ = this.readerSettings$.pipe( + takeUntil(this.onDestroy), + map(values => values.fitting), + filter(_ => this.isValid()), + shareReplay() + ); + + this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).pipe( + takeUntil(this.onDestroy), + map((value) => { + if (value[0] && value[1].fitting === FITTING_OPTION.WIDTH) return 'fit-to-width-double-offset'; + if (value[0] && value[1].fitting === FITTING_OPTION.HEIGHT) return 'fit-to-height-double-offset'; + if (value[0] && value[1].fitting === FITTING_OPTION.ORIGINAL) return 'original-double-offset'; + return ''; + }), + filter(_ => this.isValid()), + ); + + + 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), + 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); + }), + filter(_ => this.isValid()), + ).subscribe(() => {}); + } + + ngOnDestroy(): void { + this.onDestroy.next(); + this.onDestroy.complete(); + } + + shouldRenderDouble() { + if (!this.isValid()) return false; + + // if (this.mangaReaderService.isCoverImage(this.pageNum)) { + // this.debugLog('Not rendering double as current page is cover image'); + // return false; + // } + + if (this.mangaReaderService.isWidePage(this.pageNum) ) { + this.debugLog('Not rendering double as current page is wide image'); + return false; + } + + if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) { + this.debugLog('Not rendering double as current page is last'); + return false; + } + + if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) { + this.debugLog('Not rendering double as current page is last'); + return false; + } + + if (this.mangaReaderService.isWidePage(this.pageNum + 1) ) { + this.debugLog('Not rendering double as next page is wide image'); + return false; + } + + return true; + } + + isValid() { + return this.layoutMode === LayoutMode.DoubleNoCover; + } + + renderPage(img: Array): void { + if (img === null || img.length === 0 || img[0] === null) return; + if (!this.isValid()) return; + + // First load, switching from double manga -> double, this is 0 and thus not rendering + if (!this.shouldRenderDouble() && (this.currentImage.height || img[0].height) > 0) { + this.imageHeight.emit(this.currentImage.height || img[0].height); + return; + } + + 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.isValid()) return 0; + + switch (direction) { + case PAGING_DIRECTION.FORWARD: + if (this.mangaReaderService.isWidePage(this.pageNum)) { + this.debugLog('Moving forward 1 page as current page is wide'); + return 1; + } + if (this.mangaReaderService.isWidePage(this.pageNum + 1)) { + this.debugLog('Moving forward 1 page as next page is wide'); + return 1; + } + if (this.mangaReaderService.isCoverImage(this.pageNum)) { + this.debugLog('Moving forward 2 page as on cover image'); + return 2; + } + if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) { + this.debugLog('Moving forward 1 page as 2 pages left'); + return 1; + } + if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) { + this.debugLog('Moving forward 1 page as 1 page left'); + return 1; + } + this.debugLog('Moving forward 2 pages'); + return 2; + case PAGING_DIRECTION.BACKWARDS: + if (this.mangaReaderService.isCoverImage(this.pageNum)) { + this.debugLog('Moving back 1 page as on cover image'); + return 2; + } + + if (this.mangaReaderService.adjustForDoubleReader(this.pageNum - 1) != this.pageNum - 1 && !this.mangaReaderService.isWidePage(this.pageNum - 2)) { + this.debugLog('Moving back 2 pages as previous pair should be in a pair'); + return 2; + } + + if (this.mangaReaderService.isWidePage(this.pageNum)) { + this.debugLog('Moving back 1 page as current page is wide'); + return 1; + } + + if (this.mangaReaderService.isWidePage(this.pageNum - 1)) { + this.debugLog('Moving back 1 page as prev page is wide'); + return 1; + } + if (this.mangaReaderService.isWidePage(this.pageNum - 2)) { + this.debugLog('Moving back 1 page as 2 pages back is wide'); + return 1; + } + + this.debugLog('Moving back 2 pages'); + return 2; + } + } + reset(): void {} + + getBookmarkPageCount(): number { + return this.shouldRenderDouble() ? 2 : 1; + } + + debugLog(message: string, extraData?: any) { + if (!(this.debugMode & DEBUG_MODES.Logs)) return; + + if (extraData !== undefined) { + console.log(message, extraData); + } else { + console.log(message); + } + } +} 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 index 302036754..9e4edf3a2 100644 --- 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 @@ -1,6 +1,6 @@ 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, combineLatest } from 'rxjs'; +import { Observable, of, Subject, map, takeUntil, tap, shareReplay, filter, combineLatest } 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'; @@ -225,7 +225,7 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer return true; } getPageAmount(direction: PAGING_DIRECTION): number { - if (this.layoutMode !== LayoutMode.Double) return 0; + if (!this.isValid()) return 0; switch (direction) { case PAGING_DIRECTION.FORWARD: @@ -295,5 +295,4 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer console.log(message); } } - } diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html index 9ed9c7137..b7e9228ae 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html @@ -50,7 +50,7 @@ title="Previous Page" aria-hidden="true"> -
@@ -84,6 +84,14 @@ [pageNum$]="pageNum$" [getPage]="getPageFn"> + + +
diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index edc3a1ad4..54f22e1dc 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -32,6 +32,7 @@ import { DoubleReverseRendererComponent } from '../double-reverse-renderer/doubl import { SingleRendererComponent } from '../single-renderer/single-renderer.component'; import { ChapterInfo } from '../../_models/chapter-info'; import { SwipeEvent } from 'ng-swipe'; +import { DoubleNoCoverRendererComponent } from '../double-renderer-no-cover/double-no-cover-renderer.component'; const PREFETCH_PAGES = 10; @@ -96,6 +97,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild(SingleRendererComponent, { static: false }) singleRenderer!: SingleRendererComponent; @ViewChild(DoubleRendererComponent, { static: false }) doubleRenderer!: DoubleRendererComponent; @ViewChild(DoubleReverseRendererComponent, { static: false }) doubleReverseRenderer!: DoubleReverseRendererComponent; + @ViewChild(DoubleNoCoverRendererComponent, { static: false }) doubleNoCoverRenderer!: DoubleNoCoverRendererComponent; libraryId!: number; @@ -1127,8 +1129,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD); 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)); + this.doubleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), + this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), + this.doubleNoCoverRenderer.getPageAmount(PAGING_DIRECTION.FORWARD) + ); const notInSplit = this.canvasRenderer.shouldMovePrev(); if ((this.pageNum + pageAmount >= this.maxPages && notInSplit)) { @@ -1153,9 +1157,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { 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)); + this.singleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS), + this.doubleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS), + this.doubleNoCoverRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS), + this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS) + ); const notInSplit = this.canvasRenderer.shouldMovePrev(); @@ -1257,6 +1263,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.canvasRenderer?.renderPage(page); this.singleRenderer?.renderPage(page); this.doubleRenderer?.renderPage(page); + this.doubleNoCoverRenderer?.renderPage(page); this.doubleReverseRenderer?.renderPage(page); // Originally this was only for fit to height, but when swiping was introduced, it made more sense to do it always to reset to the same view @@ -1558,7 +1565,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { const pageNum = this.pageNum; const isDouble = Math.max(this.canvasRenderer.getBookmarkPageCount(), this.singleRenderer.getBookmarkPageCount(), - this.doubleRenderer.getBookmarkPageCount(), this.doubleReverseRenderer.getBookmarkPageCount()) > 1; + this.doubleRenderer.getBookmarkPageCount(), this.doubleReverseRenderer.getBookmarkPageCount(), this.doubleNoCoverRenderer.getBookmarkPageCount()) > 1; if (this.CurrentPageBookmarked) { let apis = [this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum)]; diff --git a/UI/Web/src/app/manga-reader/_models/layout-mode.ts b/UI/Web/src/app/manga-reader/_models/layout-mode.ts index 75887f2a2..466fca1ab 100644 --- a/UI/Web/src/app/manga-reader/_models/layout-mode.ts +++ b/UI/Web/src/app/manga-reader/_models/layout-mode.ts @@ -13,5 +13,10 @@ export enum LayoutMode { /** * Renders 2 pages side by side on the renderer. Cover images will not split and take up both panes. This version reverses the order and is used for Manga only */ - DoubleReversed = 3 + DoubleReversed = 3, + /** + * Renders 2 pages side by side on the renderer. Cover images will split. + */ + DoubleNoCover = 4, + } \ No newline at end of file diff --git a/UI/Web/src/app/manga-reader/_pipes/layout-mode-icon.pipe.ts b/UI/Web/src/app/manga-reader/_pipes/layout-mode-icon.pipe.ts index 26f43dfee..14f344450 100644 --- a/UI/Web/src/app/manga-reader/_pipes/layout-mode-icon.pipe.ts +++ b/UI/Web/src/app/manga-reader/_pipes/layout-mode-icon.pipe.ts @@ -14,6 +14,8 @@ export class LayoutModeIconPipe implements PipeTransform { return 'double'; case LayoutMode.DoubleReversed: return 'double-reversed'; + case LayoutMode.DoubleNoCover: + return 'double'; // TODO: Validate } } 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 e2934eac9..c9e8320cd 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.module.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.module.ts @@ -18,6 +18,7 @@ import { DoubleReverseRendererComponent } from './_components/double-reverse-ren import { MangaReaderComponent } from './_components/manga-reader/manga-reader.component'; import { FittingIconPipe } from './_pipes/fitting-icon.pipe'; import { SwipeModule } from 'ng-swipe'; +import { DoubleNoCoverRendererComponent } from './_components/double-renderer-no-cover/double-no-cover-renderer.component'; @NgModule({ declarations: [ @@ -31,6 +32,7 @@ import { SwipeModule } from 'ng-swipe'; DoubleRendererComponent, DoubleReverseRendererComponent, FittingIconPipe, + DoubleNoCoverRendererComponent, ], imports: [ CommonModule,