Manga Reader Shakeout (#1142)
* Fixed a unit test in ArchiveService * Image scaling fixes * removing test * Added new layout mode (enum only) and cleaned up manga reader and wrote extra documentation * Aligned code with cleanup * Adding reverse classes for manga reading * Disable options for layout modes that doesn't make sense. * Cleaned up manga reader menu items to link to preferences options directly * Work in progress, but rendering the correct page numbers for double. Need to rework caching logic so we can use existing image objects * Pagination logic is now properly increasing page number an extra when double layout mode * I can't figure out cachedImages to work properly with double pages, but doing it in a way where it handles downloading the image (and etag cache) + rendering the url, seems to work really well * Double original fix, also flex squish fix * Implemented last page on double which will load next chapter. Fixed a bug where if GetImage from ReaderController threw an error, the chapter directory would be emptied, but the folder itself wasn't deleted. * Fixed a bad if for double manga * double class fix * Cleanup up some console.logs * Adjusted the caching for images in a reading session so they cache for 2 mins * fixing webtoon image issue * Tweaked the caching of images to 10 mins for reading. Fixed a bug where after webtoon, single image layout would be selected. Tweaked logic for handling prev/next pages on chapter boundaries. * Fixed an issue where 2nd page would be skipped * Fixed an issue where 2nd page would be skipped * Fixed a skip page issue * Misc css fixes Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
@ -152,16 +152,14 @@ namespace API.Tests.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Theory]
|
||||||
// TODO: This is broken on GA due to DirectoryService.CoverImageDirectory
|
[InlineData("v10.cbz", "v10.expected.png")]
|
||||||
//[Theory]
|
[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.png")]
|
||||||
[InlineData("v10.cbz", "v10.expected.jpg")]
|
[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.png")]
|
||||||
[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.jpg")]
|
[InlineData("macos_native.zip", "macos_native.png")]
|
||||||
[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.jpg")]
|
[InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.png")]
|
||||||
[InlineData("macos_native.zip", "macos_native.jpg")]
|
[InlineData("sorting.zip", "sorting.expected.png")]
|
||||||
[InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.jpg")]
|
[InlineData("test.zip", "test.expected.jpg")]
|
||||||
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
|
||||||
[InlineData("test.zip", "test.expected.jpg")] // https://github.com/kleisauke/net-vips/issues/155
|
|
||||||
public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile)
|
public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile)
|
||||||
{
|
{
|
||||||
var ds = Substitute.For<DirectoryService>(_directoryServiceLogger, new FileSystem());
|
var ds = Substitute.For<DirectoryService>(_directoryServiceLogger, new FileSystem());
|
||||||
@ -183,33 +181,33 @@ namespace API.Tests.Services
|
|||||||
|
|
||||||
|
|
||||||
Assert.Equal(expectedBytes, actual);
|
Assert.Equal(expectedBytes, actual);
|
||||||
//_directoryService.ClearAndDeleteDirectory(outputDir);
|
_directoryService.ClearAndDeleteDirectory(outputDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO: This is broken on GA due to DirectoryService.CoverImageDirectory
|
[Theory]
|
||||||
//[Theory]
|
[InlineData("v10.cbz", "v10.expected.png")]
|
||||||
[InlineData("v10.cbz", "v10.expected.jpg")]
|
[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.png")]
|
||||||
[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.jpg")]
|
[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.png")]
|
||||||
[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.jpg")]
|
[InlineData("macos_native.zip", "macos_native.png")]
|
||||||
[InlineData("macos_native.zip", "macos_native.jpg")]
|
[InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.png")]
|
||||||
[InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.jpg")]
|
[InlineData("sorting.zip", "sorting.expected.png")]
|
||||||
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
|
||||||
public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile)
|
public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile)
|
||||||
{
|
{
|
||||||
var imageService = new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService);
|
var imageService = new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService);
|
||||||
var archiveService = Substitute.For<ArchiveService>(_logger,
|
var archiveService = Substitute.For<ArchiveService>(_logger,
|
||||||
new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService);
|
new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService);
|
||||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages");
|
var testDirectory = API.Parser.Parser.NormalizePath(Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages")));
|
||||||
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
|
||||||
|
|
||||||
var outputDir = Path.Join(testDirectory, "output");
|
var outputDir = Path.Join(testDirectory, "output");
|
||||||
_directoryService.ClearDirectory(outputDir);
|
_directoryService.ClearDirectory(outputDir);
|
||||||
_directoryService.ExistOrCreate(outputDir);
|
_directoryService.ExistOrCreate(outputDir);
|
||||||
|
|
||||||
archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.SharpCompress);
|
archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.SharpCompress);
|
||||||
var actualBytes = File.ReadAllBytes(archiveService.GetCoverImage(Path.Join(testDirectory, inputFile),
|
var coverOutputFile = archiveService.GetCoverImage(Path.Join(testDirectory, inputFile),
|
||||||
Path.GetFileNameWithoutExtension(inputFile) + "_output", outputDir));
|
Path.GetFileNameWithoutExtension(inputFile), outputDir);
|
||||||
|
var actualBytes = File.ReadAllBytes(Path.Join(outputDir, coverOutputFile));
|
||||||
|
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
||||||
Assert.Equal(expectedBytes, actualBytes);
|
Assert.Equal(expectedBytes, actualBytes);
|
||||||
|
|
||||||
_directoryService.ClearAndDeleteDirectory(outputDir);
|
_directoryService.ClearAndDeleteDirectory(outputDir);
|
||||||
|
Before Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 122 KiB |
After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 385 KiB |
After Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 385 KiB |
After Width: | Height: | Size: 123 KiB |
After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 385 KiB |
After Width: | Height: | Size: 123 KiB |
@ -63,7 +63,7 @@ namespace API.Controllers
|
|||||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
|
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
|
||||||
var format = Path.GetExtension(path).Replace(".", "");
|
var format = Path.GetExtension(path).Replace(".", "");
|
||||||
|
|
||||||
Response.AddCacheHeader(path);
|
Response.AddCacheHeader(path, TimeSpan.FromMinutes(10).Seconds);
|
||||||
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
|
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
|
@ -7,5 +7,7 @@ public enum LayoutMode
|
|||||||
[Description("Single")]
|
[Description("Single")]
|
||||||
Single = 1,
|
Single = 1,
|
||||||
[Description("Double")]
|
[Description("Double")]
|
||||||
Double = 2
|
Double = 2,
|
||||||
|
[Description("Double (manga)")]
|
||||||
|
DoubleReversed = 3
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.IO;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -41,12 +42,17 @@ namespace API.Extensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="response"></param>
|
/// <param name="response"></param>
|
||||||
/// <param name="filename"></param>
|
/// <param name="filename"></param>
|
||||||
public static void AddCacheHeader(this HttpResponse response, string filename)
|
/// <param name="maxAge">Maximum amount of seconds to set for Cache-Control</param>
|
||||||
|
public static void AddCacheHeader(this HttpResponse response, string filename, int maxAge = 10)
|
||||||
{
|
{
|
||||||
if (filename == null || filename.Length <= 0) return;
|
if (filename is not {Length: > 0}) return;
|
||||||
var hashContent = filename + File.GetLastWriteTimeUtc(filename);
|
var hashContent = filename + File.GetLastWriteTimeUtc(filename);
|
||||||
using var sha1 = SHA256.Create();
|
using var sha1 = SHA256.Create();
|
||||||
response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2"))));
|
response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2"))));
|
||||||
|
if (maxAge != 10)
|
||||||
|
{
|
||||||
|
response.Headers.CacheControl = $"max-age={maxAge}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -142,7 +142,7 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
foreach (var chapter in chapterIds)
|
foreach (var chapter in chapterIds)
|
||||||
{
|
{
|
||||||
_directoryService.ClearDirectory(GetCachePath(chapter));
|
_directoryService.ClearAndDeleteDirectory(GetCachePath(chapter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,4 +34,4 @@ 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 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 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 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}];
|
export const layoutModes = [{text: 'Single', value: LayoutMode.Single}, {text: 'Double', value: LayoutMode.Double}, {text: 'Double (Manga)', value: LayoutMode.DoubleReversed}];
|
||||||
|
@ -10,5 +10,8 @@ export enum LayoutMode {
|
|||||||
* Renders 2 pages side by side on the renderer. Cover images will not split and take up both panes.
|
* Renders 2 pages side by side on the renderer. Cover images will not split and take up both panes.
|
||||||
*/
|
*/
|
||||||
Double = 2,
|
Double = 2,
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
}
|
}
|
@ -3,7 +3,10 @@ export enum FITTING_OPTION {
|
|||||||
WIDTH = 'full-width',
|
WIDTH = 'full-width',
|
||||||
ORIGINAL = 'original'
|
ORIGINAL = 'original'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How to split a page into virutal pages. Only works with LayoutMode.Single
|
||||||
|
*/
|
||||||
export enum SPLIT_PAGE_PART {
|
export enum SPLIT_PAGE_PART {
|
||||||
NO_SPLIT = 'none',
|
NO_SPLIT = 'none',
|
||||||
LEFT_PART = 'left',
|
LEFT_PART = 'left',
|
||||||
|
@ -25,8 +25,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width {
|
|
||||||
|
img, .full-width {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<i class="fa fa-arrow-left" aria-hidden="true"></i>
|
<i class="fa fa-arrow-left" aria-hidden="true"></i>
|
||||||
<span class="visually-hidden">Back</span>
|
<span class="visually-hidden">Back</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: bold;">{{title}} <span class="clickable" *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">Incognito Mode:</span>)</span></div>
|
<div style="font-weight: bold;">{{title}} <span class="clickable" *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">Incognito Mode:</span>)</span></div>
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
@ -18,8 +18,9 @@
|
|||||||
<i class="fa-regular fa-rectangle-list" aria-hidden="true"></i>
|
<i class="fa-regular fa-rectangle-list" aria-hidden="true"></i>
|
||||||
<span class="visually-hidden">Keyboard Shortcuts Modal</span>
|
<span class="visually-hidden">Keyboard Shortcuts Modal</span>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- {{this.pageNum}} -->
|
||||||
|
|
||||||
<button class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="pageBookmarked" title="{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()"><i class="{{pageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i><span class="visually-hidden">{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span></button>
|
<button class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="isCurrentPageBookmarked" title="{{isCurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()"><i class="{{isCurrentPageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i><span class="visually-hidden">{{isCurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -30,54 +31,57 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div (click)="toggleMenu()" class="reading-area" [ngStyle]="{'background-color': backgroundColor}">
|
<div (click)="toggleMenu()" class="reading-area" [ngStyle]="{'background-color': backgroundColor}">
|
||||||
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon">
|
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon; else webtoon">
|
||||||
<div [ngClass]="{'d-none': !renderWithCanvas }">
|
<div [ngClass]="{'d-none': !renderWithCanvas }">
|
||||||
<canvas #content class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}}"
|
<canvas #content class="{{getFittingOptionClass()}}"
|
||||||
ondragstart="return false;" onselectstart="return false;">
|
ondragstart="return false;" onselectstart="return false;">
|
||||||
</canvas>
|
</canvas>
|
||||||
</div>
|
</div>
|
||||||
<div [ngClass]="{'d-none': renderWithCanvas, 'center-double': layoutMode === LayoutMode.Double && !isCoverImage(), 'fit-to-height-double-offset': this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.HEIGHT && layoutMode === LayoutMode.Double && !isCoverImage() && utilityService.getActiveBreakpoint() >= Breakpoint.Tablet}">
|
<div class="image-container" [ngClass]="{'d-none': renderWithCanvas, 'center-double': ShouldRenderDoublePage, 'fit-to-width-double-offset' : this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.WIDTH && ShouldRenderDoublePage, 'fit-to-height-double-offset': this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.HEIGHT && ShouldRenderDoublePage, 'original-double-offset' : this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage, 'reverse': ShouldRenderReverseDouble}">
|
||||||
<img [src]="canvasImage.src" id="image-1"
|
<img [src]="readerService.getPageUrl(this.chapterId, this.pageNum)" id="image-1"
|
||||||
class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
||||||
|
|
||||||
<ng-container *ngIf="layoutMode === LayoutMode.Double && !isCoverImage()">
|
<ng-container *ngIf="ShouldRenderDoublePage && (this.pageNum + 1 <= 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' : ''}}">
|
<img [src]="readerService.getPageUrl(this.chapterId, PageNumber + 1)" id="image-2" class="image-2 {{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}} {{ShouldRenderReverseDouble ? 'reverse' : ''}}">
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-container>
|
||||||
|
<!-- Pagination controls and screen hints-->
|
||||||
|
<div class="pagination-area {{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')">
|
||||||
|
<div *ngIf="showClickOverlay">
|
||||||
|
<i class="fa fa-angle-{{readingDirection === ReadingDirection.LeftToRight ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'right' : 'down'}}"
|
||||||
|
title="Next Page" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-area {{readerMode === ReaderMode.LeftRight ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')">
|
||||||
|
<div *ngIf="showClickOverlay">
|
||||||
|
<i class="fa fa-angle-{{readingDirection === ReadingDirection.RightToLeft ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'left' : 'up'}}"
|
||||||
|
title="Previous Page" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="readerMode === ReaderMode.Webtoon">
|
<ng-template #webtoon>
|
||||||
<div class="webtoon-images" *ngIf="readerMode === ReaderMode.Webtoon && !isLoading && !inSetup">
|
<div class="webtoon-images" *ngIf="readerMode === ReaderMode.Webtoon && !isLoading && !inSetup">
|
||||||
<app-infinite-scroller [pageNum]="pageNum"
|
<app-infinite-scroller [pageNum]="pageNum"
|
||||||
[bufferPages]="5"
|
[bufferPages]="5"
|
||||||
[goToPage]="goToPageEvent"
|
[goToPage]="goToPageEvent"
|
||||||
(pageNumberChange)="handleWebtoonPageChange($event)"
|
(pageNumberChange)="handleWebtoonPageChange($event)"
|
||||||
[totalPages]="maxPages"
|
[totalPages]="maxPages"
|
||||||
[urlProvider]="getPageUrl"
|
[urlProvider]="getPageUrl"
|
||||||
(loadNextChapter)="loadNextChapter()"
|
(loadNextChapter)="loadNextChapter()"
|
||||||
(loadPrevChapter)="loadPrevChapter()"
|
(loadPrevChapter)="loadPrevChapter()"
|
||||||
[bookmarkPage]="showBookmarkEffectEvent"
|
[bookmarkPage]="showBookmarkEffectEvent"
|
||||||
[fullscreenToggled]="fullscreenEvent"></app-infinite-scroller>
|
[fullscreenToggled]="fullscreenEvent"></app-infinite-scroller>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
<ng-container *ngIf="readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown">
|
|
||||||
<div class="pagination-area {{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')">
|
|
||||||
<div *ngIf="showClickOverlay">
|
|
||||||
<i class="fa fa-angle-{{readingDirection === ReadingDirection.LeftToRight ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'right' : 'down'}}"
|
|
||||||
title="Next Page" aria-hidden="true"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pagination-area {{readerMode === ReaderMode.LeftRight ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')">
|
|
||||||
<div *ngIf="showClickOverlay">
|
|
||||||
<i class="fa fa-angle-{{readingDirection === ReadingDirection.RightToLeft ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'left' : 'up'}}"
|
|
||||||
title="Previous Page" aria-hidden="true"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fixed-bottom overlay" *ngIf="menuOpen" [@slideFromBottom]="menuOpen">
|
<div class="fixed-bottom overlay" *ngIf="menuOpen" [@slideFromBottom]="menuOpen">
|
||||||
<div class="mb-3" *ngIf="pageOptions != undefined && pageOptions.ceil != undefined">
|
<div class="mb-3" *ngIf="pageOptions != undefined && pageOptions.ceil != undefined">
|
||||||
<span class="visually-hidden" id="slider-info"></span>
|
<span class="visually-hidden" id="slider-info"></span>
|
||||||
@ -95,8 +99,6 @@
|
|||||||
<button class="btn btn-small btn-icon col-2" [disabled]="nextPageDisabled || pageNum >= maxPages - 1" (click)="goToPage(this.maxPages);resetMenuCloseTimer();" title="Last Page"><i class="fa fa-step-forward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-2" [disabled]="nextPageDisabled || pageNum >= maxPages - 1" (click)="goToPage(this.maxPages);resetMenuCloseTimer();" title="Last Page"><i class="fa fa-step-forward" aria-hidden="true"></i></button>
|
||||||
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter();resetMenuCloseTimer();" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter();resetMenuCloseTimer();" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row pt-4 ms-2 me-2">
|
<div class="row pt-4 ms-2 me-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@ -130,7 +132,7 @@
|
|||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label for="page-splitting" class="form-label">Image Splitting</label>
|
<label for="page-splitting" class="form-label">Image Splitting</label>
|
||||||
<div class="split fa fa-image">
|
<div class="split fa fa-image">
|
||||||
<div class="{{splitIconClass}}"></div>
|
<div class="{{splitIconClass}}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
@ -178,13 +180,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<select class="form-control" id="page-fitting" formControlName="layoutMode">
|
<select class="form-control" id="page-fitting" formControlName="layoutMode">
|
||||||
<option value="1">Single</option>
|
<option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option>
|
||||||
<option value="2">Double</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
|
||||||
|
@ -4,6 +4,9 @@ $side-width: 25%;
|
|||||||
$dash-width: 3px;
|
$dash-width: 3px;
|
||||||
$pointer-offset: 5px;
|
$pointer-offset: 5px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media(min-width: 600px) {
|
@media(min-width: 600px) {
|
||||||
.overlay .left .i {
|
.overlay .left .i {
|
||||||
@ -14,6 +17,36 @@ $pointer-offset: 5px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reading-area {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
#image-1 {
|
||||||
|
&.double {
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
overflow: unset;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-2 {
|
||||||
|
&.double {
|
||||||
|
margin: 0 auto 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
@ -55,65 +88,62 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fitting Options
|
// Fitting Options
|
||||||
// .full-height {
|
|
||||||
// position: absolute;
|
|
||||||
// margin: auto;
|
|
||||||
// top: 0px;
|
|
||||||
// left: 0;
|
|
||||||
// right: 0;
|
|
||||||
// bottom: 0px;
|
|
||||||
// height: 100%;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.full-height {
|
.full-height {
|
||||||
width: auto;
|
width: auto;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.original {
|
.original {
|
||||||
position: absolute;
|
align-self: center;
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
top: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width {
|
.full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
&.double {
|
&.double {
|
||||||
width: 50%
|
width: 50%;
|
||||||
}
|
|
||||||
|
|
||||||
.image-2 {
|
&.cover {
|
||||||
margin-left: 50%;
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.center-double {
|
.center-double {
|
||||||
display: block;
|
display: flex;
|
||||||
margin-left: auto;
|
overflow: unset;
|
||||||
margin-right: auto;
|
}
|
||||||
|
|
||||||
|
.fit-to-width-double-offset {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-double-offset {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fit-to-height-double-offset {
|
.fit-to-height-double-offset {
|
||||||
width: 50%;
|
position: absolute;
|
||||||
|
height: 100vh;
|
||||||
|
object-fit: scale-down;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0%);
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
width: $side-width;
|
width: $side-width;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
background: rgba(0, 0, 0, 0);
|
background: rgba(0, 0, 0, 0);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -135,7 +165,7 @@ canvas {
|
|||||||
left: 0px;
|
left: 0px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
width: $side-width;
|
width: $side-width;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
background: rgba(0, 0, 0, 0);
|
background: rgba(0, 0, 0, 0);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -20,7 +20,7 @@ import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider';
|
|||||||
import { trigger, state, style, transition, animate } from '@angular/animations';
|
import { trigger, state, style, transition, animate } from '@angular/animations';
|
||||||
import { ChapterInfo } from './_models/chapter-info';
|
import { ChapterInfo } from './_models/chapter-info';
|
||||||
import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
|
import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
|
||||||
import { pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
|
import { layoutModes, pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
|
||||||
import { ReaderMode } from '../_models/preferences/reader-mode';
|
import { ReaderMode } from '../_models/preferences/reader-mode';
|
||||||
import { MangaFormat } from '../_models/manga-format';
|
import { MangaFormat } from '../_models/manga-format';
|
||||||
import { LibraryService } from '../_services/library.service';
|
import { LibraryService } from '../_services/library.service';
|
||||||
@ -107,6 +107,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
readerMode: ReaderMode = ReaderMode.LeftRight;
|
readerMode: ReaderMode = ReaderMode.LeftRight;
|
||||||
|
|
||||||
pageSplitOptions = pageSplitOptions;
|
pageSplitOptions = pageSplitOptions;
|
||||||
|
layoutModes = layoutModes;
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
||||||
@ -118,7 +119,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
canvasImage = new Image();
|
canvasImage = new Image();
|
||||||
/**
|
/**
|
||||||
* Used soley for LayoutMode.Double rendering. Will always hold the next image in buffer.
|
* Used soley for LayoutMode.Double rendering. Will always hold the next image in buffer.
|
||||||
*/
|
*/
|
||||||
canvasImage2 = new Image();
|
canvasImage2 = new Image();
|
||||||
renderWithCanvas: boolean = false; // Dictates if we use render with canvas or with image
|
renderWithCanvas: boolean = false; // Dictates if we use render with canvas or with image
|
||||||
@ -256,8 +257,19 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
getPageUrl = (pageNum: number) => this.readerService.getPageUrl(this.chapterId, pageNum);
|
getPageUrl = (pageNum: number) => this.readerService.getPageUrl(this.chapterId, pageNum);
|
||||||
|
|
||||||
|
get PageNumber() {
|
||||||
|
return Math.max(Math.min(this.pageNum, this.maxPages - 1), 0);
|
||||||
|
}
|
||||||
|
|
||||||
get pageBookmarked() {
|
get ShouldRenderDoublePage() {
|
||||||
|
return this.layoutMode !== LayoutMode.Single && !this.isCoverImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
get ShouldRenderReverseDouble() {
|
||||||
|
return (this.layoutMode === LayoutMode.DoubleReversed) && !this.isCoverImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCurrentPageBookmarked() {
|
||||||
return this.bookmarks.hasOwnProperty(this.pageNum);
|
return this.bookmarks.hasOwnProperty(this.pageNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,9 +376,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.generalSettingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
this.generalSettingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||||
this.layoutMode = parseInt(val, 10);
|
this.layoutMode = parseInt(val, 10);
|
||||||
if (this.layoutMode === LayoutMode.Double) {
|
|
||||||
// Update canvasImage2
|
if (this.layoutMode === LayoutMode.Single) {
|
||||||
this.canvasImage2 = this.cachedImages.next();
|
this.generalSettingsForm.get('pageSplitOption')?.enable();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.generalSettingsForm.get('pageSplitOption')?.setValue(PageSplitOption.FitSplit);
|
||||||
|
this.generalSettingsForm.get('pageSplitOption')?.disable();
|
||||||
|
|
||||||
|
this.canvasImage2 = this.cachedImages.peek();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -633,12 +651,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
val = formControl?.value;
|
val = formControl?.value;
|
||||||
|
|
||||||
if (this.isCoverImage() && this.shouldRenderAsFitSplit()) {
|
|
||||||
// Rewriting to fit to width for this cover image
|
if (this.isCoverImage() && this.layoutMode !== LayoutMode.Single) {
|
||||||
val = FITTING_OPTION.WIDTH;
|
return val + ' cover double';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isCoverImage() && this.layoutMode === LayoutMode.Double) {
|
if (!this.isCoverImage() && this.layoutMode !== LayoutMode.Single) {
|
||||||
return val + ' double';
|
return val + ' double';
|
||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
@ -783,7 +801,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.RIGHT_PART);
|
const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.LEFT_PART : SPLIT_PAGE_PART.RIGHT_PART);
|
||||||
|
|
||||||
if ((this.pageNum + 1 >= this.maxPages && notInSplit) || this.isLoading) {
|
let pageAmount = (this.layoutMode !== LayoutMode.Single && !this.isCoverImage()) ? 2 : 1;
|
||||||
|
if (this.pageNum < 1) {
|
||||||
|
pageAmount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.pageNum + pageAmount >= this.maxPages && notInSplit) || this.isLoading) {
|
||||||
|
|
||||||
if (this.isLoading) { return; }
|
if (this.isLoading) { return; }
|
||||||
|
|
||||||
@ -794,12 +817,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.pagingDirection = PAGING_DIRECTION.FORWARD;
|
this.pagingDirection = PAGING_DIRECTION.FORWARD;
|
||||||
if (this.isNoSplit() || notInSplit) {
|
if (this.isNoSplit() || notInSplit) {
|
||||||
this.setPageNum(this.pageNum + 1);
|
this.setPageNum(this.pageNum + pageAmount);
|
||||||
|
|
||||||
if (this.readerMode !== ReaderMode.Webtoon) {
|
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||||
this.canvasImage = this.cachedImages.next();
|
this.canvasImage = this.cachedImages.next();
|
||||||
this.canvasImage2 = this.cachedImages.peek(2);
|
|
||||||
console.log('[nextPage] canvasImage: ', this.canvasImage);
|
|
||||||
console.log('[nextPage] canvasImage2: ', this.canvasImage2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -816,6 +837,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.LEFT_PART);
|
const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.LEFT_PART);
|
||||||
|
|
||||||
|
const pageAmount = (this.layoutMode !== LayoutMode.Single && !this.isCoverImage()) ? 2: 1;
|
||||||
|
console.log('pageAmt: ', pageAmount);
|
||||||
if ((this.pageNum - 1 < 0 && notInSplit) || this.isLoading) {
|
if ((this.pageNum - 1 < 0 && notInSplit) || this.isLoading) {
|
||||||
|
|
||||||
if (this.isLoading) { return; }
|
if (this.isLoading) { return; }
|
||||||
@ -827,11 +850,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.pagingDirection = PAGING_DIRECTION.BACKWARDS;
|
this.pagingDirection = PAGING_DIRECTION.BACKWARDS;
|
||||||
if (this.isNoSplit() || notInSplit) {
|
if (this.isNoSplit() || notInSplit) {
|
||||||
this.setPageNum(this.pageNum - 1);
|
this.setPageNum(this.pageNum - pageAmount);
|
||||||
this.canvasImage = this.cachedImages.prev();
|
this.canvasImage = this.cachedImages.prev();
|
||||||
this.canvasImage2 = this.cachedImages.peek(-2);
|
|
||||||
console.log('[prevPage] canvasImage: ', this.canvasImage);
|
|
||||||
console.log('[prevPage] canvasImage2: ', this.canvasImage2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.readerMode !== ReaderMode.Webtoon) {
|
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||||
@ -945,12 +965,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.canvas.nativeElement.width = this.canvasImage.width / 2;
|
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.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, 0, 0, this.canvasImage.width, this.canvasImage.height);
|
||||||
this.renderWithCanvas = true;
|
this.renderWithCanvas = true;
|
||||||
console.log('[Render] Canvas')
|
|
||||||
} else if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.RIGHT_PART) {
|
} else if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.RIGHT_PART) {
|
||||||
this.canvas.nativeElement.width = this.canvasImage.width / 2;
|
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.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height);
|
||||||
this.renderWithCanvas = true;
|
this.renderWithCanvas = true;
|
||||||
console.log('[Render] Canvas')
|
|
||||||
} else {
|
} else {
|
||||||
this.renderWithCanvas = false;
|
this.renderWithCanvas = false;
|
||||||
}
|
}
|
||||||
@ -1007,6 +1025,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.cachedImages.applyFor((item, internalIndex) => {
|
this.cachedImages.applyFor((item, internalIndex) => {
|
||||||
const offsetIndex = this.pageNum + index;
|
const offsetIndex = this.pageNum + index;
|
||||||
const urlPageNum = this.readerService.imageUrlToPageNum(item.src);
|
const urlPageNum = this.readerService.imageUrlToPageNum(item.src);
|
||||||
|
|
||||||
if (urlPageNum === offsetIndex) {
|
if (urlPageNum === offsetIndex) {
|
||||||
index += 1;
|
index += 1;
|
||||||
return;
|
return;
|
||||||
@ -1016,23 +1035,27 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
}, this.cachedImages.size() - 3);
|
}, this.cachedImages.size() - 3);
|
||||||
|
|
||||||
|
//console.log('cachedImages: ', this.cachedImages.arr.map(img => this.readerService.imageUrlToPageNum(img.src) + ': ' + img.complete));
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPage() {
|
loadPage() {
|
||||||
if (!this.canvas || !this.ctx) { return; }
|
if (!this.canvas || !this.ctx) { return; }
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
this.canvasImage = this.cachedImages.current();
|
this.canvasImage = this.cachedImages.current();
|
||||||
this.canvasImage2 = this.cachedImages.next(); // TODO: Do I need this here?
|
|
||||||
console.log('[loadPage] canvasImage: ', this.canvasImage);
|
|
||||||
console.log('[loadPage] canvasImage2: ', this.canvasImage2);
|
|
||||||
if (this.readerService.imageUrlToPageNum(this.canvasImage.src) !== this.pageNum || this.canvasImage.src === '' || !this.canvasImage.complete) {
|
if (this.readerService.imageUrlToPageNum(this.canvasImage.src) !== this.pageNum || this.canvasImage.src === '' || !this.canvasImage.complete) {
|
||||||
this.canvasImage.src = this.readerService.getPageUrl(this.chapterId, this.pageNum);
|
if (this.layoutMode === LayoutMode.Single) {
|
||||||
this.canvasImage2.src = this.readerService.getPageUrl(this.chapterId, this.pageNum + 1); // TODO: I need to handle last page correctly
|
this.canvasImage.src = this.readerService.getPageUrl(this.chapterId, this.pageNum);
|
||||||
|
} else {
|
||||||
|
this.canvasImage.src = this.readerService.getPageUrl(this.chapterId, this.pageNum);
|
||||||
|
this.canvasImage2.src = this.readerService.getPageUrl(this.chapterId, this.pageNum + 1); // TODO: I need to handle last page correctly
|
||||||
|
}
|
||||||
this.canvasImage.onload = () => this.renderPage();
|
this.canvasImage.onload = () => this.renderPage();
|
||||||
|
|
||||||
console.log('[loadPage] (after setting) canvasImage: ', this.canvasImage);
|
|
||||||
console.log('[loadPage] (after setting) canvasImage2: ', this.canvasImage2);
|
|
||||||
} else {
|
} else {
|
||||||
this.renderPage();
|
this.renderPage();
|
||||||
}
|
}
|
||||||
@ -1074,12 +1097,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.setPageNum(page);
|
this.setPageNum(page);
|
||||||
this.refreshSlider.emit();
|
this.refreshSlider.emit();
|
||||||
this.goToPageEvent.next(page);
|
this.goToPageEvent.next(page);
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
setPageNum(pageNum: number) {
|
setPageNum(pageNum: number) {
|
||||||
this.pageNum = pageNum;
|
this.pageNum = Math.max(pageNum, 0);
|
||||||
|
|
||||||
if (this.pageNum >= this.maxPages - 10) {
|
if (this.pageNum >= this.maxPages - 10) {
|
||||||
// Tell server to cache the next chapter
|
// Tell server to cache the next chapter
|
||||||
@ -1179,12 +1202,19 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateForm() {
|
updateForm() {
|
||||||
|
|
||||||
if ( this.readerMode === ReaderMode.Webtoon) {
|
if ( this.readerMode === ReaderMode.Webtoon) {
|
||||||
this.generalSettingsForm.get('fittingOption')?.disable()
|
this.generalSettingsForm.get('fittingOption')?.disable()
|
||||||
this.generalSettingsForm.get('pageSplitOption')?.disable();
|
this.generalSettingsForm.get('pageSplitOption')?.disable();
|
||||||
|
this.generalSettingsForm.get('layoutMode')?.disable();
|
||||||
} else {
|
} else {
|
||||||
this.generalSettingsForm.get('fittingOption')?.enable()
|
this.generalSettingsForm.get('fittingOption')?.enable()
|
||||||
this.generalSettingsForm.get('pageSplitOption')?.enable();
|
this.generalSettingsForm.get('pageSplitOption')?.enable();
|
||||||
|
this.generalSettingsForm.get('layoutMode')?.enable();
|
||||||
|
|
||||||
|
if (this.layoutMode !== LayoutMode.Single) {
|
||||||
|
this.generalSettingsForm.get('pageSplitOption')?.disable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1198,9 +1228,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
bookmarkPage() {
|
bookmarkPage() {
|
||||||
const pageNum = this.pageNum;
|
const pageNum = this.pageNum;
|
||||||
|
|
||||||
// TODO: Handle LayoutMode.Double
|
if (this.isCurrentPageBookmarked) {
|
||||||
|
|
||||||
if (this.pageBookmarked) {
|
|
||||||
let apis = [this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum)];
|
let apis = [this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum)];
|
||||||
if (this.layoutMode === LayoutMode.Double) apis.push(this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum + 1));
|
if (this.layoutMode === LayoutMode.Double) apis.push(this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum + 1));
|
||||||
forkJoin(apis).pipe(take(1)).subscribe(() => {
|
forkJoin(apis).pipe(take(1)).subscribe(() => {
|
||||||
|