mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Manga Reader Double Layout bugfixes (#1341)
* Enforce some max heights on series detail page * Added some icon to layout mode to help the user understand how they work (Robbie needs to css it) * Adding split-double icon * Fixing reverse * Added lots of debug code, refactored documentation, and added some history for wide images * More prefetching code for wide images * Fixed the issue where sometimes paging backwards would skip an image * Fixed up a bug where occasionally on double (manga) paging backwards could skip a page. Fixed a bug on double where last page could get duplicated. * Don't update pageDimensionHistory since we don't need it * Forgot some changes Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
f72459a728
commit
dacf15f024
@ -1,6 +1,6 @@
|
||||
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
||||
<div class="pe-2">
|
||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" [width]="imageWidth"></app-image>
|
||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" maxHeight="200px" [width]="imageWidth"></app-image>
|
||||
<div class="not-read-badge" *ngIf="pagesRead === 0 && totalPages > 0"></div>
|
||||
<span class="download" *ngIf="download$ | async as download">
|
||||
<app-circular-loader [currentValue]="download.progress"></app-circular-loader>
|
||||
|
@ -13,6 +13,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{readerService.imageUrlToPageNum(this.canvasImage.src)}}
|
||||
{{readerService.imageUrlToPageNum(this.canvasImage2.src)}}
|
||||
|
||||
<div style="margin-left: auto; padding-right: 3%;">
|
||||
<button class="btn btn-icon btn-small" title="Shortcuts" (click)="openShortcutModal()">
|
||||
<i class="fa-regular fa-rectangle-list" aria-hidden="true"></i>
|
||||
@ -63,13 +66,12 @@
|
||||
<div class="image-container {{getFittingOptionClass()}}" [ngClass]="{'d-none': renderWithCanvas, 'center-double': ShouldRenderDoublePage,
|
||||
'fit-to-width-double-offset' : FittingOption === FITTING_OPTION.WIDTH && ShouldRenderDoublePage,
|
||||
'fit-to-height-double-offset': FittingOption === FITTING_OPTION.HEIGHT && ShouldRenderDoublePage,
|
||||
'original-double-offset' : FittingOption === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage,
|
||||
'reverse': ShouldRenderReverseDouble}">
|
||||
'original-double-offset' : FittingOption === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage}">
|
||||
<img #image [src]="canvasImage.src" id="image-1"
|
||||
class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
||||
|
||||
<ng-container *ngIf="(ShouldRenderDoublePage || ShouldRenderReverseDouble) && (this.pageNum <= maxPages - 1 && this.pageNum > 0)">
|
||||
<img [src]="canvasImage2.src" id="image-2" class="image-2 {{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}} {{ShouldRenderReverseDouble ? 'reverse' : ''}}">
|
||||
<ng-container *ngIf="(this.canvasImage2.src !== '') && (readerService.imageUrlToPageNum(canvasImage2.src) <= maxPages - 1 && !isCoverImage())">
|
||||
<img [src]="canvasImage2.src" id="image-2" class="image-2 {{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}"> <!-- {{ShouldRenderReverseDouble ? 'reverse' : ''}} -->
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@ -162,7 +164,42 @@
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<label for="layout-mode" class="form-label">Layout Mode</label>
|
||||
<label for="layout-mode" class="form-label">Layout Mode</label>
|
||||
<ng-container [ngSwitch]="layoutMode">
|
||||
<ng-container *ngSwitchCase="LayoutMode.Single">
|
||||
<div class="split-double">
|
||||
<span class="fa-stack fa-1x">
|
||||
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
||||
<i class="fa fa-image fa-stack-1x"></i>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LayoutMode.Double">
|
||||
<div class="split-double">
|
||||
<span class="fa-stack fa-1x">
|
||||
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
||||
<i class="fab fa-1 fa-stack-1x"></i>
|
||||
</span>
|
||||
<span class="fa-stack fa right">
|
||||
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
||||
<i class="fab fa-2 fa-stack-1x"></i>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LayoutMode.DoubleReversed">
|
||||
<div class="split-double">
|
||||
<span class="fa-stack fa-1x">
|
||||
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
||||
<i class="fab fa-2 fa-stack-1x"></i>
|
||||
</span>
|
||||
<span class="fa-stack fa right">
|
||||
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
||||
<i class="fab fa-1 fa-stack-1x"></i>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- <div class="{{LayoutModeIconClass}}"></div> -->
|
||||
</ng-container>
|
||||
<select class="form-control" id="page-fitting" formControlName="layoutMode">
|
||||
<option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option>
|
||||
</select>
|
||||
|
@ -57,9 +57,14 @@ img {
|
||||
}
|
||||
|
||||
&.reverse {
|
||||
flex-direction: row-reverse;
|
||||
overflow: unset;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
margin: unset;
|
||||
}
|
||||
}
|
||||
|
||||
#image-2 {
|
||||
@ -172,6 +177,7 @@ img {
|
||||
overflow: hidden;
|
||||
border: 2px solid #ccc;
|
||||
vertical-align: sub;
|
||||
display: inline-block;
|
||||
|
||||
&::before {
|
||||
margin-left: 30%;
|
||||
@ -197,6 +203,19 @@ img {
|
||||
.none {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
// For layout only
|
||||
|
||||
}
|
||||
|
||||
.split-double {
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
font-size: .7em;
|
||||
|
||||
.right {
|
||||
left: -7px;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
|
@ -38,6 +38,7 @@ const OVERLAY_AUTO_CLOSE_TIME = 3000;
|
||||
const CLICK_OVERLAY_TIMEOUT = 3000;
|
||||
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-manga-reader',
|
||||
templateUrl: './manga-reader.component.html',
|
||||
@ -66,6 +67,14 @@ const CLICK_OVERLAY_TIMEOUT = 3000;
|
||||
]
|
||||
})
|
||||
export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
|
||||
@ViewChild('reader') reader!: ElementRef;
|
||||
@ViewChild('readingArea') readingArea!: ElementRef;
|
||||
@ViewChild('content') canvas: ElementRef | undefined;
|
||||
@ViewChild('image') image!: ElementRef;
|
||||
|
||||
|
||||
libraryId!: number;
|
||||
seriesId!: number;
|
||||
volumeId!: number;
|
||||
@ -123,33 +132,41 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
*/
|
||||
isLoose = false;
|
||||
|
||||
@ViewChild('reader') reader!: ElementRef;
|
||||
@ViewChild('readingArea') readingArea!: ElementRef;
|
||||
@ViewChild('content') canvas: ElementRef | undefined;
|
||||
@ViewChild('image') image!: ElementRef;
|
||||
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
canvasImage = new Image();
|
||||
/**
|
||||
* Used solely for LayoutMode.Double rendering. Will always hold the next image in buffer.
|
||||
* Used solely for LayoutMode.Double rendering.
|
||||
* @remarks Used for rendering to screen.
|
||||
*/
|
||||
canvasImage2 = new Image();
|
||||
/**
|
||||
* Used solely for LayoutMode.Double rendering. Will always hold the previous image in buffer.
|
||||
* Used solely for LayoutMode.Double rendering. Will always hold the previous image to canvasImage
|
||||
* @see canvasImage
|
||||
*/
|
||||
canvasImagePrev = new Image();
|
||||
/**
|
||||
* Used solely for LayoutMode.Double rendering. Will always hold the next image in buffer.
|
||||
* Used solely for LayoutMode.Double rendering. Will always hold the next image to canvasImage
|
||||
* @see canvasImage
|
||||
*/
|
||||
canvasImageNext = new Image();
|
||||
/**
|
||||
* Used solely for LayoutMode.DoubleReverse rendering. Will always hold the image after next in buffer.
|
||||
* Responsible to hold current page + 2. Used to know if we should render
|
||||
* @remarks Used solely for LayoutMode.DoubleReverse rendering.
|
||||
*/
|
||||
canvasImageNextDouble = new Image();
|
||||
canvasImageAheadBy2 = new Image();
|
||||
/**
|
||||
* Dictates if we use render with canvas or with image. This is only for Splitting.
|
||||
* Responsible to hold current page -2 2. Used to know if we should render
|
||||
* @remarks Used solely for LayoutMode.DoubleReverse rendering.
|
||||
*/
|
||||
canvasImageBehindBy2 = new Image();
|
||||
/**
|
||||
* Dictates if we use render with canvas or with image.
|
||||
* @remarks This is only for Splitting.
|
||||
*/
|
||||
renderWithCanvas: boolean = false;
|
||||
|
||||
@ -296,25 +313,32 @@ 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() {
|
||||
// window.innerWidth > window.innerHeight // Don't render double if orientation is portrait, mostly mobile
|
||||
return (
|
||||
this.layoutMode === LayoutMode.Double &&
|
||||
!this.isCoverImage() &&
|
||||
!this.isWideImage(this.canvasImage) &&
|
||||
!this.isWideImage(this.canvasImageNext) &&
|
||||
window.innerWidth > window.innerHeight // Don't render double if orientation is portrait, mostly mobile
|
||||
!this.isWideImage(this.canvasImageNext)
|
||||
);
|
||||
}
|
||||
|
||||
get ShouldRenderReverseDouble() {
|
||||
// NOTE: I'm not going to care about window.innerWidth > window.innerHeight since a higher level handler will manage
|
||||
// window.innerWidth > window.innerHeight // Don't render double reversed if orientation is portrait, mostly mobile
|
||||
return (
|
||||
this.layoutMode === LayoutMode.DoubleReversed &&
|
||||
!this.isCoverImage() &&
|
||||
!this.isCoverImage(this.pageNum - 1) &&
|
||||
!this.isWideImage(this.canvasImage) &&
|
||||
!this.isWideImage(this.canvasImageNext) &&
|
||||
!this.isLoose &&
|
||||
window.innerWidth > window.innerHeight // Don't render double reversed if orientation is portrait, mostly mobile
|
||||
!this.isLoose
|
||||
);
|
||||
}
|
||||
|
||||
@ -372,6 +396,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return 'right-side';
|
||||
}
|
||||
|
||||
get LayoutModeIconClass() {
|
||||
switch (this.layoutMode) {
|
||||
case LayoutMode.Single:
|
||||
return 'none';
|
||||
case LayoutMode.Double:
|
||||
return 'double';
|
||||
case LayoutMode.DoubleReversed:
|
||||
return 'double-reversed';
|
||||
}
|
||||
}
|
||||
|
||||
get ReaderModeIcon() {
|
||||
switch(this.readerMode) {
|
||||
case ReaderMode.LeftRight:
|
||||
@ -960,6 +995,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.RIGHT_PART);
|
||||
|
||||
// console.log('Current, Next: ', this.readerService.imageUrlToPageNum(this.canvasImage.src), ',', this.readerService.imageUrlToPageNum(this.canvasImageNext.src));
|
||||
// console.log('Is canvasImage wide: ', this.isWideImage(this.canvasImage));
|
||||
// console.log('Is canvasImage next wide: ', this.isWideImage(this.canvasImageNext));
|
||||
// console.log('PRev: ', this.readerService.imageUrlToPageNum(this.canvasImagePrev.src));
|
||||
|
||||
|
||||
let pageAmount = 1;
|
||||
if (this.layoutMode === LayoutMode.Double) {
|
||||
pageAmount = (
|
||||
@ -969,12 +1010,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
!this.isSecondLastImage() &&
|
||||
!this.isLastImage()
|
||||
? 2 : 1);
|
||||
}
|
||||
if (this.layoutMode === LayoutMode.DoubleReversed) {
|
||||
} else if (this.layoutMode === LayoutMode.DoubleReversed) {
|
||||
pageAmount = (
|
||||
!this.isCoverImage(this.pageNum - 1) &&
|
||||
!this.isWideImage(this.canvasImagePrev) &&
|
||||
!this.isWideImage(this.canvasImageNextDouble) &&
|
||||
//!this.isCoverImage(this.pageNum - 1) && //
|
||||
!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);
|
||||
@ -1014,16 +1054,27 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
let pageAmount = 1;
|
||||
if (this.layoutMode === LayoutMode.Double) {
|
||||
// Current is current and next is current + 1 (current is the page we are paging from)
|
||||
// console.log('Current, Next: ', this.readerService.imageUrlToPageNum(this.canvasImage.src), ',', this.readerService.imageUrlToPageNum(this.canvasImageNext.src));
|
||||
// console.log('Is canvasImage wide: ', this.isWideImage(this.canvasImage));
|
||||
// console.log('Is canvasImage next wide: ', this.isWideImage(this.canvasImageNext));
|
||||
// console.log('PRev: ', this.readerService.imageUrlToPageNum(this.canvasImagePrev.src)); // Prev is actually currentPage - 1 on double
|
||||
|
||||
pageAmount = (
|
||||
!this.isCoverImage() &&
|
||||
!this.isWideImage(this.canvasImagePrev)
|
||||
? 2 : 1);
|
||||
}
|
||||
if (this.layoutMode === LayoutMode.DoubleReversed) {
|
||||
// Current is current - 1 and next is current -2 (current is the page we are paging from)
|
||||
// console.log('Current, Next: ', this.readerService.imageUrlToPageNum(this.canvasImage.src), ',', this.readerService.imageUrlToPageNum(this.canvasImageNext.src));
|
||||
// console.log('Is canvasImage wide: ', this.isWideImage(this.canvasImage));
|
||||
// console.log('Is canvasImage next wide: ', this.isWideImage(this.canvasImageNext));
|
||||
// console.log('PRev: ', this.readerService.imageUrlToPageNum(this.canvasImagePrev.src)); // Prev is actually currentPage + 1 on double reversed
|
||||
pageAmount = (
|
||||
!this.isCoverImage() &&
|
||||
!this.isCoverImage(this.pageNum - 1) &&
|
||||
!this.isWideImage(this.canvasImage) &&
|
||||
!this.isWideImage(this.canvasImage) && // JOE: At this point, these aren't yet set to the new values
|
||||
!this.isWideImage(this.canvasImageNext)
|
||||
? 2 : 1);
|
||||
}
|
||||
@ -1202,24 +1253,41 @@ 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;
|
||||
}
|
||||
@ -1230,7 +1298,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maintains a circular 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.
|
||||
*/
|
||||
prefetch() {
|
||||
let index = 1;
|
||||
|
||||
@ -1251,19 +1322,43 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
//console.log('cachedImages: ', this.cachedImages.arr.map(img => this.readerService.imageUrlToPageNum(img.src) + ': ' + img.complete));
|
||||
}
|
||||
|
||||
|
||||
loadPage() {
|
||||
this.isLoading = true;
|
||||
this.canvasImage2.src = '';
|
||||
this.canvasImageAheadBy2.src = '';
|
||||
|
||||
this.isLoose = (this.pageAmount === 1 ? true : false);
|
||||
this.canvasImage.src = this.getPageUrl(this.pageNum);
|
||||
|
||||
if (this.layoutMode !== LayoutMode.Single) {
|
||||
this.canvasImagePrev.src = this.getPageUrl(this.pageNum + (this.layoutMode !== LayoutMode.DoubleReversed ? - 1 : + 1));
|
||||
this.canvasImageNext.src = this.getPageUrl(this.pageNum + (this.layoutMode !== LayoutMode.DoubleReversed ? + 1 : - 1));
|
||||
this.canvasImageNextDouble.src = this.getPageUrl(this.pageNum + 2);
|
||||
if (this.ShouldRenderDoublePage || this.ShouldRenderReverseDouble) {
|
||||
this.canvasImage2.src = this.canvasImageNext.src;
|
||||
this.canvasImageNext.src = this.getPageUrl(this.pageNum + 1); // This needs to be capped at maxPages !this.isLastImage()
|
||||
this.canvasImagePrev.src = this.getPageUrl(this.pageNum - 1);
|
||||
|
||||
if (this.pageNum + 2 < this.maxPages - 1) {
|
||||
this.canvasImageAheadBy2.src = this.getPageUrl(this.pageNum + 2);
|
||||
}
|
||||
if (this.pageNum - 2 >= 0) {
|
||||
this.canvasImageBehindBy2.src = this.getPageUrl(this.pageNum - 2 || 0);
|
||||
}
|
||||
|
||||
if (this.ShouldRenderDoublePage || this.ShouldRenderReverseDouble) {
|
||||
if (this.layoutMode === LayoutMode.Double) {
|
||||
this.canvasImage2.src = this.canvasImageNext.src;
|
||||
} else {
|
||||
this.canvasImage2.src = this.canvasImagePrev.src;
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('======================================================');
|
||||
// console.log('Page History: ', this.pageDimensionHistory);
|
||||
// console.log('Current Page: ', this.pageNum);
|
||||
// console.log('CanvasImage page: ', this.readerService.imageUrlToPageNum(this.canvasImage.src));
|
||||
// console.log('CanvasImage2 page: ', this.readerService.imageUrlToPageNum(this.canvasImage2.src));
|
||||
// console.log('Canvas Image Next:', this.readerService.imageUrlToPageNum(this.canvasImageNext.src));
|
||||
// console.log('Canvas Image Next Ahead by 2:', this.readerService.imageUrlToPageNum(this.canvasImageAheadBy2.src));
|
||||
// console.log('Canvas Image Prev:', this.readerService.imageUrlToPageNum(this.canvasImagePrev.src));
|
||||
// console.log('======================================================');
|
||||
}
|
||||
this.renderPage();
|
||||
this.prefetch();
|
||||
|
@ -57,7 +57,7 @@
|
||||
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="series !== undefined" #scrollingBlock>
|
||||
<div class="row mb-3 info-container">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
||||
<app-image maxWidth="300px" [imageUrl]="seriesImage"></app-image>
|
||||
<app-image maxWidth="300px" maxHeight="400px" [imageUrl]="seriesImage"></app-image>
|
||||
<!-- NOTE: We can put continue point here as Vol X Ch Y or just Ch Y or Book Z ?-->
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
|
Loading…
x
Reference in New Issue
Block a user