Reader Refactor Part 2 (#1694)

* Updated swiper and some packages for reported security issues

* Fixed reading lists promotion not working

* Refactor RenameFileForCopy to use iterative recursion, rather than functional.

* Ensured that bookmarks are fetched and ordered by Created date.

* Fixed a bug where bookmarks were coming back in the correct order, but due to filenames, would not sort correctly.

* Default installs to Debug log level given errors users have and Debug not being too noisy

* Added jumpbar to bookmarks page

* Now added jumpbar to bookmarks

* Refactored some code into pipes and added some debug messaging for prefetcher

* Try loading next and prev chapter's first/last page to cache so it renders faster

* Updated GetImage to do a bound check on max page.

Fixed a critical bug in how manga reader updates image elements src to prefetch/load pages. I was not creating a new reference which broke Angular's ability to update DOM on changes.

* Refactored the image setting code to use a single method which tries to use a cached image always.

* Refactored code to use getPage which favors cache and simplifies image creation code

* Started the work to split the canvas renderer into it's own component

* Refactored a lot of common methods into a service for the reader to support the upcoming renderer split

* Moved components to nested folder. Refactored more code to streamline image sending to child renderer.

Added notes across the code to help streamline flow of data and who owns what.

* Swapped out SQLite for Memory, but the one from hangfire. Added DisableConcurrentExecution on ProcessChange to avoid duplication when multiple threads execute at once.

* Basic split right to left is working with canvas renderer

* Left to right and right to left now work

* Fixed a bug where pagesplitoption wasn't being updated when modifying menu

* Canvas rendering still has a bug with switching between right to left -> left to right on the re-render, it will choose a bad state. All else works fine with it.

* Updated canvas renderer to implement the ImageRenderer interface

* Canvas renderer is done

* Setup single renderer. Need to figure out how to share CSS between renderers and also share some global stuff, like image height.

* Refactored code so that image-container is within the renderers themselves. Still broken in scaling, but working towards a solution.

* Added double click to shortcut menu

* Moved image containers within the renderers

* Pushing up for Robbie

* nothing new

* Move common css to a single scss file

* More css consolidation

* Fixed a npe in isWideImage

* Refactored page updates to renderers to include max pages. Rewrote most of renderer into observables.

* Moved bookmark for second page to double renderer

* Started hooking in double renderer renderPage()

* Fixed height scaling, but now canvas renderer is broken again

* Fixed a bug with canvas renderer not moving to next page. Streamlined the code for getting page amounts from the dfferent renderers

* Added double click to bookmark for canvas

* Stashing the code and taking a break

* Nothing much, buffer is still broken

* Got double renderer to render at least one page

* Double renderer now has access to 5 images at any time, so it can make appropriate decisions on when to render double pages.

* Fixed up double rendererer moving backward page calc

* Forward logic seems to be working

* Cleaned up dead code after testing

* Moved a few loggers in folder watching to trace

* Everything seems to work fine, time to do double manga renderer

* Moved some css around and added the reverse double component

* Only execute renderer's pipes when in the correct mode

* Still working on double renderer

* Fixed scaling issues on double

* Updating double logic

- Fixed: Fixed an issue where a second page would render when current page was wide.

* Hooked up double renderer

* Made changes but not sure if im making progress

* double manga fixes

* Claned some of robbies code

* Fixing last page bug

* Library Settings Modal + New Library Settings (#1660)

* Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web

Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fixed want to read button on series detail not performing the correct action

* Started the library settings. Added ability to update a cover image for a library.

Updated backup db to also copy reading list (and now library) cover images.

* Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library.

* Fixed a missing update event in backend when updating a library.

* Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid.

* Trim library names before you check anything

* General code cleanup

* Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries.

Refactored some code to streamline perf in some flows.

* Removed old components replaced with new modal

* Code smells

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* UX Alignment and bugfixes (#1663)

* Refactored the design of reading list page to follow more in line with list view. Added release date on the reading list items, if it's set in underlying chapter.

Fixed a bug where reordering the list items could sometimes not update correctly with drag and drop.

* Removed a bug marker that I just fixed

* When generating library covers, make them much smaller as they are only ever icons.

* Fixed library settings not showing the correct image.

* Fixed a bug where duplicate collection tags could be created.

Fixed a bug where collection tag normalized title was being set to uppercase.

Redesigned the edit collection tag modal to align with new library settings and provide inline name checks.

* Updated edit reading list modal to align with new library settings modal pattern. Refactored the backend to ensure it flows correctly without allowing duplicate names.

Don't show Continue point on series detail if the whole series is read.

* Added some more unit tests around continue point

* Fixed a bug on series detail when bulk selecting between volume and chapters, the code which determines which chapters are selected didn't take into account mixed layout for Storyline tab.

* Refactored to generate an OpenAPI spec at root of Kavita. This will be loaded by a new API site for easy hosting.

Deprecated EnableSwaggerUi preference as after validation new system works, this will be removed and instances can use our hosting to hit their server (or run a debug build).

* Test GA

* Reverted GA and instead do it in the build step. This will just force developers to commit it in.

* GA please work

* Removed redundant steps from test since build already does it.

* Try another GA

* Moved all test actions into initial build step, which should drastically cut down on time. Only run sonar if the secret is present (so not for forks). Updated build requirements for develop and stable docker pushes.

* Fixed env variable

* Okay not possible to do secrets in if statement

* Fixed the build step to output the openapi.json where it's expected.

* Fixed GA (#1664)

* Bump versions by dotnet-bump-version.

* Applied new _components layout structure to Kavita. All except manga as there is an open PR that drastically changes that module. (#1666)

* Fixed typeahead and updated manga reader to new layout structure

* Fixed book reader fonts lookups

* Fixed up some build issues

* Fixed  a bad import of css image

* Some cleanup and rewrote how we log out data.

* Renderer can be null on first load when performing some work.

* Library Settings Modal + New Library Settings (#1660)

* Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web

Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fixed want to read button on series detail not performing the correct action

* Started the library settings. Added ability to update a cover image for a library.

Updated backup db to also copy reading list (and now library) cover images.

* Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library.

* Fixed a missing update event in backend when updating a library.

* Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid.

* Trim library names before you check anything

* General code cleanup

* Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries.

Refactored some code to streamline perf in some flows.

* Removed old components replaced with new modal

* Code smells

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* UX Alignment and bugfixes (#1663)

* Refactored the design of reading list page to follow more in line with list view. Added release date on the reading list items, if it's set in underlying chapter.

Fixed a bug where reordering the list items could sometimes not update correctly with drag and drop.

* Removed a bug marker that I just fixed

* When generating library covers, make them much smaller as they are only ever icons.

* Fixed library settings not showing the correct image.

* Fixed a bug where duplicate collection tags could be created.

Fixed a bug where collection tag normalized title was being set to uppercase.

Redesigned the edit collection tag modal to align with new library settings and provide inline name checks.

* Updated edit reading list modal to align with new library settings modal pattern. Refactored the backend to ensure it flows correctly without allowing duplicate names.

Don't show Continue point on series detail if the whole series is read.

* Added some more unit tests around continue point

* Fixed a bug on series detail when bulk selecting between volume and chapters, the code which determines which chapters are selected didn't take into account mixed layout for Storyline tab.

* Refactored to generate an OpenAPI spec at root of Kavita. This will be loaded by a new API site for easy hosting.

Deprecated EnableSwaggerUi preference as after validation new system works, this will be removed and instances can use our hosting to hit their server (or run a debug build).

* Test GA

* Reverted GA and instead do it in the build step. This will just force developers to commit it in.

* GA please work

* Removed redundant steps from test since build already does it.

* Try another GA

* Moved all test actions into initial build step, which should drastically cut down on time. Only run sonar if the secret is present (so not for forks). Updated build requirements for develop and stable docker pushes.

* Fixed env variable

* Okay not possible to do secrets in if statement

* Fixed the build step to output the openapi.json where it's expected.

* Applied new _components layout structure to Kavita. All except manga as there is an open PR that drastically changes that module. (#1666)

* Post merge cleanup

* Again moving the file

* Fixed an issue with switching to double renderer and the image not loading for cover image.

* Fixed double manga last page repeating twice

* Added ability to quickly save a few settings to user preferences from manga reader

* Fixed up some success messaging

* Single image and canvas could stack, last page on double wouldn't render.

* Stashing code, want to work on something else

* Suppress a concurrency issue when opening a fresh chapter to read.

* Refactored a function into a pipe

* Took care of one TODO

* Tightened up the logic around single renderer handling fit to screen images.

* Added some code to see how long api takes on average.

* First pass integration of page dimensions into single renderer and base code

* Canvas renderer pass for new page dimensions

* On time left, don't use the word left again

* Moved the page dimension code into manga service to make it seemless

* Hooked in a replacement for image based isWide

* Canvas renderer is working again

* Double renderer now follows how Komga does it to keep it simple.

* Double renderer is working really well so far.

* don't use nbsp

* Added response caching to file-dimensions and chapter info api

* Allow chapter info to send back file dimensions optionally

* Fixed an issue with dimensions api locking files on Windows

* Refactored all code to use isWidePage

* More fixes and cleanup

* More double reverse logic

* Recently Read stats page will allow you to click the items.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Joe Milazzo 2022-12-13 12:00:54 -06:00 committed by GitHub
parent 7c8c9b8a0e
commit 83ac8bd733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2261 additions and 701 deletions

View File

@ -8,10 +8,10 @@
<TieredPGO>true</TieredPGO>
<TieredCompilation>true</TieredCompilation>
</PropertyGroup>
<Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
<Exec Command="swagger tofile --output ../openapi.json bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).dll v1" />
</Target>
<Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />
</Target>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>false</DebugSymbols>

View File

@ -161,6 +161,7 @@ public class ReaderController : BaseApiController
/// <param name="extractPdf"></param>
/// <returns></returns>
[HttpGet("file-dimensions")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf"})]
public async Task<ActionResult<IEnumerable<FileDimensionDto>>> GetFileDimensions(int chapterId, bool extractPdf = false)
{
if (chapterId <= 0) return null;
@ -174,9 +175,11 @@ public class ReaderController : BaseApiController
/// </summary>
/// <param name="chapterId"></param>
/// <param name="extractPdf">Should Kavita extract pdf into images. Defaults to false.</param>
/// <param name="includeDimensions">Include file dimensions. Only useful for image based reading</param>
/// <returns></returns>
[HttpGet("chapter-info")]
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId, bool extractPdf = false)
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf", "includeDimensions"})]
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false)
{
if (chapterId <= 0) return null; // This can happen occasionally from UI, we should just ignore
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
@ -201,6 +204,7 @@ public class ReaderController : BaseApiController
ChapterTitle = dto.ChapterTitle ?? string.Empty,
Subtitle = string.Empty,
Title = dto.SeriesName,
PageDimensions = _cacheService.GetCachedFileDimensions(chapterId)
};
if (info.ChapterTitle is {Length: > 0}) {

View File

@ -85,6 +85,7 @@ public class UsersController : BaseApiController
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu;
existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints;
existingPreferences.EmulateBook = preferencesDto.EmulateBook;
existingPreferences.ReaderMode = preferencesDto.ReaderMode;
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor;

View File

@ -66,4 +66,6 @@ public class ChapterInfoDto : IChapterInfoDto
/// <remarks>Usually just series name, but can include chapter title</remarks>
public string Title { get; set; }
public IEnumerable<FileDimensionDto> PageDimensions { get; set; }
}

View File

@ -37,6 +37,11 @@ public class UserPreferencesDto
[Required]
public LayoutMode LayoutMode { get; set; }
/// <summary>
/// Manga Reader Option: Emulate a book by applying a shadow effect on the pages
/// </summary>
[Required]
public bool EmulateBook { get; set; }
/// <summary>
/// Manga Reader Option: Background color of the reader
/// </summary>
[Required]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class EmulateBookPref : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "EmulateBook",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EmulateBook",
table: "AppUserPreferences");
}
}
}

View File

@ -217,6 +217,9 @@ namespace API.Data.Migrations
.HasColumnType("TEXT")
.HasDefaultValue("Dark");
b.Property<bool>("EmulateBook")
.HasColumnType("INTEGER");
b.Property<int>("GlobalPageLayoutMode")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")

View File

@ -36,6 +36,10 @@ public class AppUserPreferences
/// </summary>
public bool ShowScreenHints { get; set; } = true;
/// <summary>
/// Manga Reader Option: Emulate a book by applying a shadow effect on the pages
/// </summary>
public bool EmulateBook { get; set; } = false;
/// <summary>
/// Manga Reader Option: How many pages to display in the reader at once
/// </summary>
public LayoutMode LayoutMode { get; set; } = LayoutMode.Single;

View File

@ -409,6 +409,7 @@ public class ArchiveService : IArchiveService
private void ExtractArchiveEntities(IEnumerable<IArchiveEntry> entries, string extractPath)
{
_directoryService.ExistOrCreate(extractPath);
// TODO: Look into a Parallel.ForEach
foreach (var entry in entries)
{
entry.WriteToDirectory(extractPath, new ExtractionOptions()

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -60,6 +61,7 @@ public class CacheService : ICacheService
public IEnumerable<FileDimensionDto> GetCachedFileDimensions(int chapterId)
{
var sw = Stopwatch.StartNew();
var path = GetCachePath(chapterId);
var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions)
.OrderByNatural(Path.GetFileNameWithoutExtension)
@ -74,7 +76,7 @@ public class CacheService : ICacheService
for (var i = 0; i < files.Length; i++)
{
var file = files[i];
using var image = Image.NewFromStream(File.OpenRead(file), access: Enums.Access.SequentialUnbuffered);
using var image = Image.NewFromFile(file, memory:false, access: Enums.Access.SequentialUnbuffered);
dimensions.Add(new FileDimensionDto()
{
PageNumber = i,
@ -84,6 +86,7 @@ public class CacheService : ICacheService
});
}
_logger.LogDebug("File Dimensions call for {Length} images took {Time}ms", dimensions.Count, sw.ElapsedMilliseconds);
return dimensions;
}

View File

@ -225,6 +225,7 @@ public class ReaderService : IReaderService
try
{
// TODO: Rewrite this code to just pull user object with progress for that particiular appuserprogress, else create it
var userProgress =
await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(progressDto.ChapterId, userId);
@ -241,8 +242,7 @@ public class ReaderService : IReaderService
SeriesId = progressDto.SeriesId,
ChapterId = progressDto.ChapterId,
LibraryId = progressDto.LibraryId,
BookScrollId = progressDto.BookScrollId,
LastModified = DateTime.Now
BookScrollId = progressDto.BookScrollId
});
_unitOfWork.UserRepository.Update(userWithProgress);
}
@ -253,7 +253,6 @@ public class ReaderService : IReaderService
userProgress.VolumeId = progressDto.VolumeId;
userProgress.LibraryId = progressDto.LibraryId;
userProgress.BookScrollId = progressDto.BookScrollId;
userProgress.LastModified = DateTime.Now;
_unitOfWork.AppUserProgressRepository.Update(userProgress);
}
@ -267,6 +266,10 @@ public class ReaderService : IReaderService
}
catch (Exception exception)
{
// This can happen when the reader sends 2 events at same time, so 2 threads are inserting and one fails.
if (exception.Message.StartsWith(
"The database operation was expected to affect 1 row(s), but actually affected 0 row(s)"))
return true;
_logger.LogError(exception, "Could not save progress");
await _unitOfWork.RollbackAsync();
}

View File

@ -45,6 +45,10 @@ img {
vertical-align: top;
max-width: fit-content;
}
.fit-to-screen.full-width {
max-height: calc(var(--vh)*100);
}
}
@ -70,4 +74,29 @@ img {
background-color: var(--manga-reader-prev-highlight-bg-color) !important;
animation: fadein .5s both;
backdrop-filter: blur(10px);
}
::ng-deep .image-container.book-shadow.center-double:before {
content: '';
position: absolute;
top: 0;
left: 50%;
height: 100%;
box-shadow:
0px 0px calc(17px*3.14) 25px rgb(0 0 0 / 43%),
0px 0px calc(2px*3.14) 2px rgb(0 0 0 / 43%),
0px 0px calc(5px*3.14) 4px rgb(0 0 0 / 43%),
0px 0px calc(0.5px*3.14) 0.3px rgb(0 0 0 / 43%);
}
@supports (-moz-appearance:none) {
::ng-deep .image-container.book-shadow.center-double:before {
box-shadow:
0px 0px calc(17px*3.14) 25px rgb(0 0 0 / 43%),
0px 0px calc(2px*3.14) 2px rgb(0 0 0 / 43%),
0px 0px calc(5px*3.14) 4px rgb(0 0 0 / 43%),
0px 0px calc(0.5px*3.14) 0.3px rgb(0 0 0 / 43%),
0px 0px 1px 0.5px rgb(0 0 0 / 43%);
}
}

View File

@ -18,6 +18,7 @@ export interface Preferences {
layoutMode: LayoutMode;
backgroundColor: string;
showScreenHints: boolean;
emulateBook: boolean;
// Book Reader
bookReaderMargin: number;

View File

@ -146,13 +146,6 @@ export class MessageHubService {
this.onlineUsersSource.next(usernames);
});
this.hubConnection.on("LogObject", resp => {
console.log(resp);
});
this.hubConnection.on("LogString", resp => {
console.log(resp);
});
this.hubConnection.on(EVENTS.ScanSeries, resp => {
this.messagesSource.next({
event: EVENTS.ScanSeries,

View File

@ -103,8 +103,8 @@ export class ReaderService {
return this.baseUrl + 'reader/bookmark-image?seriesId=' + seriesId + '&page=' + page + '&apiKey=' + encodeURIComponent(apiKey);
}
getChapterInfo(chapterId: number) {
return this.httpClient.get<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId);
getChapterInfo(chapterId: number, includeDimensions = false) {
return this.httpClient.get<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&includeDimensions=' + includeDimensions);
}
getFileDimensions(chapterId: number) {
@ -194,6 +194,11 @@ export class ReaderService {
return parseInt(imageSrc.split('&page=')[1], 10);
}
imageUrlToChapterId(imageSrc: string) {
if (imageSrc === undefined || imageSrc === '') { return -1; }
return parseInt(imageSrc.split('chapterId=')[1].split('&')[0], 10);
}
getNextChapterUrl(url: string, nextChapterId: number, incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) {
const lastSlashIndex = url.lastIndexOf('/');
let newRoute = url.substring(0, lastSlashIndex + 1) + nextChapterId + '';

View File

@ -30,7 +30,7 @@
</li>
</ol>
<ng-template #noBreadcrumb>
<div class="breadcrumb">Select a folder to view breadcrumb. Don't see your directory, try checking / first.
<div class="breadcrumb">Select a folder to view breadcrumb. Don't see your directory? Try checking / first.
</div>
</ng-template>
</nav>

View File

@ -9,7 +9,7 @@
<span class="text-warning">If you want Send To device to work, you must host your own email service.</span>
</p>
<div class="mb-3">
<label for="settings-emailservice" class="form-label">Email Service Url</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="emailServiceTooltip" role="button" tabindex="0"></i>
<label for="settings-emailservice" class="form-label">Email Service Url</label><i class="ms-1 fa fa-info-circle" placement="right" [ngbTooltip]="emailServiceTooltip" role="button" tabindex="0"></i>
<ng-template #emailServiceTooltip>Use fully qualified url of the email service. Do not include ending slash.</ng-template>
<span class="visually-hidden" id="settings-emailservice-help"><ng-container [ngTemplateOutlet]="emailServiceTooltip"></ng-container></span>
<div class="input-group">

View File

@ -41,10 +41,10 @@
<span class="visually-hidden">(promoted)</span>
</span>
<ng-container *ngIf="format | mangaFormat as formatString">
<i class="fa {{format | mangaFormatIcon}}" aria-hidden="true" *ngIf="format != MangaFormat.UNKNOWN" title="{{formatString}}"></i>
<i class="fa {{format | mangaFormatIcon}} me-1" aria-hidden="true" *ngIf="format != MangaFormat.UNKNOWN" title="{{formatString}}"></i>
<span class="visually-hidden">{{formatString}}</span>
</ng-container>
&nbsp;{{title}}
{{title}}
</span>
<span class="card-actions float-end">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>

View File

@ -93,7 +93,7 @@
<div class="vr d-none d-lg-block m-2"></div>
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title label="Time Left" [clickable]="false" fontClasses="fa-solid fa-clock">
~{{readingTimeLeft.avgHours}} Hour{{readingTimeLeft.avgHours > 1 ? 's' : ''}} Left
~{{readingTimeLeft.avgHours}} Hour{{readingTimeLeft.avgHours > 1 ? 's' : ''}}
</app-icon-and-title>
</div>
</ng-container>

View File

@ -1,5 +1,5 @@
<div class="image-container {{imageFitClass$ | async}}"
[ngClass]="{'d-none': !renderWithCanvas }"
[ngClass]="{'d-none': !renderWithCanvas }"
[style.filter]="(darkenss$ | async) ?? '' | safeStyle">
<canvas #content ondragstart="return false;" onselectstart="return false;"></canvas>
</div>

View File

@ -5,7 +5,8 @@
margin: 0 auto;
max-height: calc(var(--vh)*100);
vertical-align: top;
&.wide {
&.wide {
height: 100vh;
}
}

View File

@ -1,6 +1,7 @@
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 { filter, map, Observable, of, Subject, takeUntil, takeWhile, tap } from 'rxjs';
import { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
import { ReaderService } from 'src/app/_services/reader.service';
import { LayoutMode } from '../../_models/layout-mode';
import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from '../../_models/reader-enums';
import { ReaderSetting } from '../../_models/reader-setting';
@ -47,7 +48,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
constructor(private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService) { }
constructor(private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService, private readerService: ReaderService) { }
ngOnInit(): void {
this.readerSettings$.pipe(takeUntil(this.onDestroy), tap(value => {
@ -63,6 +64,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
this.darkenss$ = this.readerSettings$.pipe(
map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
);
@ -75,15 +77,15 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
// Would this ever execute given that we perform splitting only in this renderer?
if (
this.mangaReaderService.isWideImage(this.canvasImage) &&
this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src)) &&
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;
})
}),
filter(() => this.isValid()),
);
@ -94,7 +96,6 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
if (!this.canvas) return;
const elements = [this.canvas?.nativeElement];
console.log('Applying bookmark on ', elements);
this.mangaReaderService.applyBookmarkEffect(elements);
})
).subscribe(() => {});
@ -122,7 +123,8 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
updateSplitPage() {
if (this.canvasImage == null) return;
const needsSplitting = this.mangaReaderService.isWideImage(this.canvasImage);
const needsSplitting = this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src));
if (!needsSplitting || this.mangaReaderService.isNoSplit(this.pageSplit)) {
this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT;
return needsSplitting;
@ -160,6 +162,10 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
return needsSplitting;
}
isValid() {
return this.renderWithCanvas;
}
/**
* This renderer does not render when splitting is not needed
* @param img
@ -173,7 +179,6 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
this.cdRef.markForCheck();
const needsSplitting = this.updateSplitPage();
//console.log('split: ',this.currentImageSplitPart);
if (!needsSplitting) return;
if (this.currentImageSplitPart === SPLIT_PAGE_PART.NO_SPLIT) return;
@ -195,7 +200,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
getPageAmount(direction: PAGING_DIRECTION) {
if (this.canvasImage === null) return 1;
if (!this.mangaReaderService.isWideImage(this.canvasImage)) return 1;
if (!this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src))) return 1;
switch(direction) {
case PAGING_DIRECTION.FORWARD:
return this.shouldMoveNext() ? 1 : 0;
@ -221,7 +226,6 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
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 = [

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="isValid()">
<div class="image-container {{imageFitClass$ | async}} {{layoutClass$ | async}}"
<div class="image-container {{imageFitClass$ | async}} {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
[style.filter]="(darkenss$ | async) ?? '' | safeStyle"
[ngClass]="{'center-double': (shouldRenderDouble$ | async)}">
<ng-container *ngIf="currentImage">
@ -9,7 +9,7 @@
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
>
</ng-container>
<ng-container *ngIf="shouldRenderSecondPage$ | async">
<ng-container *ngIf="shouldRenderDouble$ | async">
<img alt=" " [src]="currentImage2.src"
id="image-2"
class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}">

View File

@ -43,3 +43,4 @@
left: 50%;
max-width: 100%;
}

View File

@ -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 } from 'rxjs';
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';
@ -10,6 +10,9 @@ import { ReaderSetting } from '../../_models/reader-setting';
import { ImageRenderer } from '../../_models/renderer';
import { ManagaReaderService } from '../../_series/managa-reader.service';
/**
* Renders 2 pages except on first page, last page, and before a wide image
*/
@Component({
selector: 'app-double-renderer',
templateUrl: './double-renderer.component.html',
@ -20,10 +23,6 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
@Input() readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>;
/**
* The image fit class
*/
@Input() imageFit$!: Observable<FITTING_OPTION>;
@Input() bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@ -36,8 +35,8 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>;
layoutClass$!: Observable<string>;
shouldRenderSecondPage$!: Observable<boolean>;
darkenss$: Observable<string> = of('brightness(100%)');
emulateBookClass$: Observable<string> = of('');
layoutMode: LayoutMode = LayoutMode.Single;
pageSplit: PageSplitOption = PageSplitOption.FitSplit;
pageNum: number = 0;
@ -53,26 +52,6 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
* @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.
@ -95,9 +74,9 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
ngOnInit(): void {
this.readerModeClass$ = this.readerSettings$.pipe(
filter(_ => this.isValid()),
map(values => values.readerMode),
map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
);
@ -107,6 +86,13 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
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()),
@ -115,7 +101,6 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
this.pageNum$.pipe(
takeUntil(this.onDestroy),
filter(_ => this.isValid()),
tap(pageInfo => {
this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages;
@ -123,68 +108,36 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
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(() => {});
}),
filter(_ => this.isValid()),
).subscribe(() => {});
this.shouldRenderDouble$ = this.pageNum$.pipe(
takeUntil(this.onDestroy),
map((_) => this.shouldRenderDouble()),
filter(_ => this.isValid()),
map((_) => {
return this.shouldRenderDouble();
})
);
this.layoutClass$ = zip(this.shouldRenderDouble$, this.imageFit$).pipe(
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]) 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';
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 '';
})
);
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 => {
@ -196,7 +149,6 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
this.bookmark$.pipe(
takeUntil(this.onDestroy),
filter(_ => this.isValid()),
tap(_ => {
const elements = [];
const image1 = this.document.querySelector('#image-1');
@ -206,16 +158,9 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
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()
);
).subscribe(() => {});
}
ngOnDestroy(): void {
@ -224,13 +169,29 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
}
shouldRenderDouble() {
if (this.layoutMode !== LayoutMode.Double) return false;
if (!this.isValid()) return false;
return !(
this.mangaReaderService.isCoverImage(this.pageNum)
|| this.mangaReaderService.isWideImage(this.currentImage)
|| this.mangaReaderService.isWideImage(this.currentImageNext)
);
if (this.mangaReaderService.isCoverImage(this.pageNum)) {
console.log('Not rendering double as current page is cover image');
return false;
}
if (this.mangaReaderService.isWidePage(this.pageNum) ) {
console.log('Not rendering double as current page is wide image');
return false;
}
if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) {
console.log('Not rendering double as current page is last and there are an odd number of pages');
return false;
}
if (this.mangaReaderService.isWidePage(this.pageNum + 1) ) {
console.log('Not rendering double as next page is wide image');
return false;
}
return true;
}
isValid() {
@ -240,19 +201,12 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
renderPage(img: Array<HTMLImageElement | null>): 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);
// 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.currentImage2 = this.currentImageNext;
this.cdRef.markForCheck();
this.imageHeight.emit(Math.max(this.currentImage.height, this.currentImage2.height));
@ -268,18 +222,17 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
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)) {
if (this.mangaReaderService.isWidePage(this.pageNum)) {
console.log('Moving forward 1 page as current page is wide');
return 1;
}
if (this.mangaReaderService.isWideImage(this.currentImageNext)) {
if (this.mangaReaderService.isWidePage(this.pageNum + 1)) {
console.log('Moving forward 1 page as next page is wide');
return 1;
}
@ -298,23 +251,19 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
console.log('Moving back 1 page as on cover image');
return 1;
}
if (this.mangaReaderService.isWideImage(this.currentImage)) {
if (this.mangaReaderService.isWidePage(this.pageNum)) {
console.log('Moving back 1 page as current page is wide');
return 1;
}
if (this.mangaReaderService.isWideImage(this.currentImagePrev)) {
if (this.mangaReaderService.isWidePage(this.pageNum - 1)) {
console.log('Moving back 1 page as prev page is wide');
return 1;
}
if (this.mangaReaderService.isWideImage(this.currentImage2Behind)) {
if (this.mangaReaderService.isWidePage(this.pageNum - 2)) {
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;
}

View File

@ -1,7 +1,7 @@
<ng-container *ngIf="isValid()">
<div class="image-container {{layoutClass$ | async}}"
<div class="image-container {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
[style.filter]="(darkenss$ | async) ?? '' | safeStyle"
[ngClass]="{'center-double': (shouldRenderDouble$ | async), 'reverse': (shouldRenderSecondPage$ | async)}">
[ngClass]="{'center-double': (shouldRenderDouble$ | async), 'reverse': (shouldRenderDouble$ | async)}">
<ng-container *ngIf="leftImage">
<img alt=" "
#image [src]="leftImage.src"
@ -9,7 +9,7 @@
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
>
</ng-container>
<ng-container *ngIf="shouldRenderSecondPage$ | async">
<ng-container *ngIf="shouldRenderDouble$ | async">
<img alt=" " [src]="rightImage.src"
id="image-2"
class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"> <!--reverse-->

View File

@ -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 } from 'rxjs';
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';
@ -25,10 +25,6 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
@Input() readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>;
/**
* The image fit class
*/
@Input() imageFit$!: Observable<FITTING_OPTION>;
@Input() bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@ -41,8 +37,8 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>;
layoutClass$!: Observable<string>;
shouldRenderSecondPage$!: Observable<boolean>;
darkenss$: Observable<string> = of('brightness(100%)');
emulateBookClass$: Observable<string> = of('');
layoutMode: LayoutMode = LayoutMode.Single;
pageSplit: PageSplitOption = PageSplitOption.FitSplit;
pageNum: number = 0;
@ -54,50 +50,10 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
*/
leftImage = new Image();
/**
* Used solely for LayoutMode.Double rendering.
* Used solely for LayoutMode.Double rendering. Will always hold the next image to currentImage
* @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.
@ -107,8 +63,6 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
*/
shouldRenderDouble$!: Observable<boolean>;
pageSpreadMap: {[key: number]: 'W'|'S'} = {};
private readonly onDestroy = new Subject<void>();
get ReaderMode() {return ReaderMode;}
@ -129,122 +83,62 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
);
this.darkenss$ = this.readerSettings$.pipe(
filter(_ => this.isValid()),
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(
filter(_ => this.isValid()),
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.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(() => {});
}),
filter(_ => this.isValid()),
).subscribe(() => {});
this.shouldRenderDouble$ = this.pageNum$.pipe(
takeUntil(this.onDestroy),
filter(_ => this.isValid()),
map((_) => this.shouldRenderDouble()),
filter(_ => this.isValid()),
shareReplay()
);
this.layoutClass$ = zip(this.shouldRenderDouble$, this.imageFit$).pipe(
this.imageFitClass$ = this.readerSettings$.pipe(
takeUntil(this.onDestroy),
map(values => values.fitting),
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 '';
})
shareReplay()
);
this.shouldRenderSecondPage$ = this.pageNum$.pipe(
this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).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;
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 => {
@ -256,7 +150,6 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
this.bookmark$.pipe(
takeUntil(this.onDestroy),
filter(_ => this.isValid()),
tap(_ => {
const elements = [];
const image1 = this.document.querySelector('#image-1');
@ -266,16 +159,9 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
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()
);
).subscribe(() => {});
}
ngOnDestroy(): void {
@ -283,62 +169,30 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
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');
console.log('Not rendering double as current page is cover image');
return false;
}
if (this.isWide(this.currentImageNext)) {
console.log('Not rendering right image as it is wide');
if (this.mangaReaderService.isWidePage(this.pageNum)) {
console.log('Not rendering double as current page is wide image');
return false;
}
if (this.mangaReaderService.isWidePage(this.pageNum + 1) ) {
console.log('Not rendering double as next page is wide image');
return false;
}
if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) {
console.log('Not rendering double as current page is last and there are an odd number of pages');
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() {
@ -349,25 +203,6 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
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();
}
@ -380,31 +215,6 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
}
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:
@ -413,39 +223,35 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
return 1;
}
if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages-1)) {
if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) {
console.log('Moving forward 1 page as 2 pages left');
return 1;
}
if (this.mangaReaderService.isWideImage(this.rightImage)) {
if (this.mangaReaderService.isWidePage(this.pageNum)) {
console.log('Moving forward 1 page as current page is wide');
return 1;
}
if (this.mangaReaderService.isWideImage(this.leftImage)) {
if (this.mangaReaderService.isWidePage(this.pageNum + 1)) {
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.mangaReaderService.isLastImage(this.pageNum + 1, this.maxPages)) {
console.log('Moving forward 2 pages as right image is the last page and we just rendered double page');
return 2;
}
if (this.pageNum === this.maxPages - 1) {
console.log('Moving forward 0 page as on last page');
return 0;
}
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;
@ -455,32 +261,37 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
return 1;
}
if (this.isWide(this.rightImage)) {
if (this.mangaReaderService.isWidePage(this.pageNum + 1)) {
console.log('Moving back 2 page as right page is wide');
return 2;
}
if (this.isWide(this.leftImage) && (!this.isWide(this.currentImage4Behind))) {
if (this.mangaReaderService.isWidePage(this.pageNum + 2)) {
console.log('Moving back 1 page as coming from wide page');
return 1;
}
if (this.mangaReaderService.isWidePage(this.pageNum)) {
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');
if (this.mangaReaderService.isWidePage(this.pageNum) && (!this.mangaReaderService.isWidePage(this.pageNum - 4))) {
console.log('Moving back 1 page as left page is wide');
return 1;
}
if (this.isWide(this.currentImagePrev)) {
if (this.mangaReaderService.isWidePage(this.pageNum - 1)) {
console.log('Moving back 1 page as prev page is wide');
return 1;
}
if (this.isWide(this.currentImage2Behind)) {
if (this.mangaReaderService.isWidePage(this.pageNum - 2)) {
console.log('Moving back 1 page as 2 pages back is wide');
return 1;
}
if (this.isWide(this.currentImage2Ahead)) {
if (this.mangaReaderService.isWidePage(this.pageNum + 2)) {
console.log('Moving back 2 page as 2 pages back is wide');
return 1;
}

View File

@ -184,7 +184,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
initScrollHandler() {
console.log('Setting up Scroll handler on ', this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body);
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scroll')
//fromEvent(this.document.body, 'scroll')
.pipe(debounceTime(20), takeUntil(this.onDestroy))
.subscribe((event) => this.handleScrollEvent(event));
}
@ -265,7 +264,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
*/
handleScrollEvent(event?: any) {
const verticalOffset = this.getVerticalOffset();
console.log('offset: ', verticalOffset);
if (verticalOffset > this.prevScrollPosition) {
this.scrollingDirection = PAGING_DIRECTION.FORWARD;

View File

@ -36,7 +36,6 @@
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon; else webtoon">
<div (dblclick)="bookmarkPage($event)">
<app-canvas-renderer
[readerSettings$]="readerSettings$"
@ -69,14 +68,13 @@
<div (dblclick)="bookmarkPage($event)">
<app-single-renderer [image$]="currentImage$"
[readerSettings$]="readerSettings$"
[imageFit$]="imageFit$"
[bookmark$]="showBookmarkEffect$"
[pageNum$]="pageNum$"
[showClickOverlay$]="showClickOverlay$">
</app-single-renderer>
<app-double-renderer [image$]="currentImage$"
[readerSettings$]="readerSettings$"
[imageFit$]="imageFit$"
[bookmark$]="showBookmarkEffect$"
[showClickOverlay$]="showClickOverlay$"
[pageNum$]="pageNum$"
@ -85,31 +83,12 @@
<app-double-reverse-renderer [image$]="currentImage$"
[readerSettings$]="readerSettings$"
[imageFit$]="imageFit$"
[bookmark$]="showBookmarkEffect$"
[showClickOverlay$]="showClickOverlay$"
[pageNum$]="pageNum$"
[getPage]="getPageFn">
</app-double-reverse-renderer>
</div>
<!--
<div class="image-container {{imageFitClass$ | async}}" [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}"
[style.filter]="'brightness(' + generalSettingsForm.get('darkness')?.value + '%)' | safeStyle" (dblclick)="bookmarkPage($event)">
<app-single-renderer [image$]="currentImage$"
[readerSettings$]="readerSettings$"
[imageFit$]="imageFit$"
[bookmark$]="showBookmarkEffect$"
(imageHeight)="updateImageHeight($event)">
</app-single-renderer>
<ng-container *ngIf="(this.canvasImage2.src !== '') && (readerService.imageUrlToPageNum(canvasImage2.src) <= maxPages - 1 && !mangaReaderService.isCoverImage(this.pageNum))">
<img alt=" " [src]="canvasImage2.src" id="image-2" class="image-2 {{imageFitClass$ | async}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}} {{ShouldRenderReverseDouble ? 'reverse' : ''}}">
</ng-container>
</div> -->
</ng-container>
@ -191,7 +170,7 @@
</div>
<div class="col-md-6 col-sm-12">
<label for="page-fitting" class="form-label">Image Scaling</label>&nbsp;<i class="fa {{getFittingIcon()}}" aria-hidden="true"></i>
<label for="page-fitting" class="form-label">Image Scaling</label>&nbsp;<i class="{{FittingOption | fittingIcon}}" aria-hidden="true"></i>
<select class="form-control" id="page-fitting" formControlName="fittingOption">
<option value="full-height">Height</option>
<option value="full-width">Width</option>
@ -241,17 +220,26 @@
<option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12">
<div class="col-md-3 col-sm-12">
<div class="mb-3">
<label id="auto-close-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" id="auto-close" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<input type="checkbox" id="auto-close" formControlName="autoCloseMenu" class="form-check-input" [value]="true">
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-12">
<div class="mb-3">
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" id="emulate-book" formControlName="emulateBook" class="form-check-input" [value]="true">
<label class="form-check-label" for="emulate-book">Emulate comic book</label>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-sm-12">
@ -259,6 +247,9 @@
<input type="range" class="form-range" id="darkness"
min="10" max="100" step="1" formControlName="darkness">
</div>
<div class="col-md-6 col-sm-12">
<button class="btn btn-primary" (click)="savePref()">Save to Preferences</button>
</div>
</div>
</form>
</div>

View File

@ -54,64 +54,6 @@ $pointer-offset: 5px;
}
}
// Fitting Options
.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;
&.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 {
// position: absolute;
height: 100vh;
object-fit: scale-down;
top: 50%;
left: 50%;
// transform: translate(-50%, 0%);
max-width: 100%;
}
// Splitting Icon
.split {
height: 20px;
@ -217,7 +159,6 @@ $pointer-offset: 5px;
}
.pagination-area {
$pagination-bg: rgba(0, 0, 0, 0);
//$pagination-bg: rgba(0, 0, 255, 0.4); // DEBUG CODE
@ -265,4 +206,3 @@ $pointer-offset: 5px;
z-index: 100;
}
}

View File

@ -41,8 +41,6 @@ const ANIMATION_SPEED = 200;
const OVERLAY_AUTO_CLOSE_TIME = 3000;
const CLICK_OVERLAY_TIMEOUT = 3000;
@Component({
selector: 'app-manga-reader',
templateUrl: './manga-reader.component.html',
@ -312,14 +310,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private currentImage: Subject<HTMLImageElement | null> = new ReplaySubject(1);
currentImage$: Observable<HTMLImageElement | null> = this.currentImage.asObservable();
private imageFit: Subject<FITTING_OPTION> = new ReplaySubject();
private imageFitClass: Subject<string> = new ReplaySubject();
imageFitClass$: Observable<string> = this.imageFitClass.asObservable();
imageFit$: Observable<FITTING_OPTION> = this.imageFit.asObservable();
private imageHeight: Subject<string> = new ReplaySubject();
imageHeight$: Observable<string> = this.imageHeight.asObservable();
private pageNumSubject: Subject<{pageNum: number, maxPages: number}> = new ReplaySubject();
pageNum$: Observable<{pageNum: number, maxPages: number}> = this.pageNumSubject.asObservable();
@ -347,11 +337,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
get ImageHeight() {
// ?! 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';
if (this.FittingOption !== FITTING_OPTION.HEIGHT) return this.mangaReaderService.getPageDimensions(this.pageNum)?.height + 'px';
return this.readingArea?.nativeElement?.clientHeight + 'px';
}
get RightPaginationOffset() {
@ -372,39 +359,20 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
get ReaderMode() {
return ReaderMode;
}
get LayoutMode() {
return LayoutMode;
}
get ReadingDirection() {
return ReadingDirection;
}
get PageSplitOption() {
return PageSplitOption;
}
get Breakpoint() {
return Breakpoint;
}
get FITTING_OPTION() {
return FITTING_OPTION;
}
get FittingOption() {
return this.generalSettingsForm.get('fittingOption')?.value;
}
get ReaderMode() { return ReaderMode; }
get LayoutMode() { return LayoutMode; }
get ReadingDirection() { return ReadingDirection; }
get PageSplitOption() { return PageSplitOption; }
get Breakpoint() { return Breakpoint; }
get FITTING_OPTION() { return FITTING_OPTION; }
get FittingOption() { return this.generalSettingsForm.get('fittingOption')?.value || FITTING_OPTION.HEIGHT; }
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
public readerService: ReaderService, private formBuilder: FormBuilder, private navService: NavService,
private toastr: ToastrService, private memberService: MemberService,
public utilityService: UtilityService, private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document, private modalService: NgbModal,
private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService) {
public utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
private modalService: NgbModal, private readonly cdRef: ChangeDetectorRef,
public mangaReaderService: ManagaReaderService) {
this.navService.hideNavBar();
this.navService.hideSideNav();
this.cdRef.markForCheck();
@ -457,17 +425,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
pageSplitOption: new FormControl(this.pageSplitOption),
fittingOption: new FormControl(this.mangaReaderService.translateScalingOption(this.scalingOption)),
layoutMode: new FormControl(this.layoutMode),
darkness: new FormControl(100)
darkness: new FormControl(100),
emulateBook: new FormControl(this.user.preferences.emulateBook)
});
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(
map(_ => this.createReaderSettingsUpdate()),
takeUntil(this.onDestroy),
map(_ => this.createReaderSettingsUpdate())
);
this.updateForm();
@ -497,6 +465,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.layoutMode = parseInt(val, 10);
if (this.layoutMode === LayoutMode.Single) {
this.generalSettingsForm.get('pageSplitOption')?.setValue(this.user.preferences.pageSplitOption);
this.generalSettingsForm.get('pageSplitOption')?.enable();
this.generalSettingsForm.get('fittingOption')?.enable();
} else {
@ -504,6 +473,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm.get('pageSplitOption')?.disable();
this.generalSettingsForm.get('fittingOption')?.setValue(this.mangaReaderService.translateScalingOption(ScalingOption.FitToHeight));
this.generalSettingsForm.get('fittingOption')?.disable();
// If we are in double mode, we need to check if our current page is on a right edge or not, if so adjust by decrementing by 1
if (this.readerMode !== ReaderMode.Webtoon) {
this.setPageNum(this.mangaReaderService.adjustForDoubleReader(this.pageNum));
}
}
this.cdRef.markForCheck();
@ -517,7 +491,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
this.pageSplitOption = parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10);
const needsSplitting = this.mangaReaderService.isWideImage(this.canvasImage);
const needsSplitting = this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src));
// 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
@ -570,17 +544,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@HostListener('window:resize', ['$event'])
@HostListener('window:orientationchange', ['$event'])
onResize() {
if (window.innerWidth > window.innerHeight) {
this.generalSettingsForm.get('layoutMode')?.enable();
this.cdRef.markForCheck();
return;
};
if (this.layoutMode === LayoutMode.Single || this.readerMode === ReaderMode.Webtoon) return;
this.generalSettingsForm.get('layoutMode')?.setValue(LayoutMode.Single);
this.generalSettingsForm.get('layoutMode')?.disable();
this.toastr.info('Layout mode switched to Single due to insufficient space to render double layout');
this.cdRef.markForCheck();
this.disableDoubleRendererIfScreenTooSmall();
}
@HostListener('window:keyup', ['$event'])
@ -631,24 +595,40 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
createReaderSettingsUpdate() {
return {
pageSplit: parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10),
fitting: this.mangaReaderService.translateScalingOption(this.scalingOption),
fitting: (this.generalSettingsForm.get('fittingOption')?.value as FITTING_OPTION),
layoutMode: this.layoutMode,
darkness: 100,
pagingDirection: this.pagingDirection,
readerMode: this.readerMode
readerMode: this.readerMode,
emulateBook: this.generalSettingsForm.get('emulateBook')?.value,
};
}
disableDoubleRendererIfScreenTooSmall() {
if (window.innerWidth > window.innerHeight) {
this.generalSettingsForm.get('layoutMode')?.enable();
this.cdRef.markForCheck();
return;
};
if (this.layoutMode === LayoutMode.Single || this.readerMode === ReaderMode.Webtoon) return;
this.generalSettingsForm.get('layoutMode')?.setValue(LayoutMode.Single);
this.generalSettingsForm.get('layoutMode')?.disable();
this.toastr.info('Layout mode switched to Single due to insufficient space to render double layout');
this.cdRef.markForCheck();
}
/**
* 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. Does not search against cached images with chapterId
* @param chapterId ChapterId to fetch page from. Defaults to current 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);
let img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum
&& (this.readerService.imageUrlToChapterId(img.src) == chapterId || this.readerService.imageUrlToChapterId(img.src) === -1)
);
if (!img || forceNew) {
img = new Image();
img.src = this.getPageUrl(pageNum, chapterId);
@ -674,7 +654,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// This is menu code
clickOverlayClass(side: 'right' | 'left') {
// TODO: This needs to be validated with subject
if (!this.showClickOverlay) {
return '';
}
@ -738,7 +717,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
forkJoin({
progress: this.readerService.getProgress(this.chapterId),
chapterInfo: this.readerService.getChapterInfo(this.chapterId),
chapterInfo: this.readerService.getChapterInfo(this.chapterId, true),
bookmarks: this.readerService.getBookmarks(this.chapterId),
}).pipe(take(1)).subscribe(results => {
if (this.readingListMode && (results.chapterInfo.seriesFormat === MangaFormat.EPUB || results.chapterInfo.seriesFormat === MangaFormat.PDF)) {
@ -748,13 +727,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
this.mangaReaderService.loadPageDimensions(results.chapterInfo.pageDimensions);
this.volumeId = results.chapterInfo.volumeId;
this.maxPages = results.chapterInfo.pages;
let page = results.progress.pageNum;
if (page > this.maxPages) {
page = this.maxPages - 1;
}
this.setPageNum(page);
// If we are in double mode, we need to check if our current page is on a right edge or not, if so adjust by decrementing by 1
if (this.layoutMode !== LayoutMode.Single && this.readerMode !== ReaderMode.Webtoon) {
page = this.mangaReaderService.adjustForDoubleReader(page);
}
this.setPageNum(page); // first call
this.goToPageEvent = new BehaviorSubject<number>(this.pageNum);
// Due to change detection rules in Angular, we need to re-create the options object to apply the change
@ -768,6 +755,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.inSetup = false;
this.disableDoubleRendererIfScreenTooSmall();
// From bookmarks, create map of pages to make lookup time O(1)
this.bookmarks = {};
@ -797,7 +786,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
});
this.render();
}, () => {
setTimeout(() => {
@ -819,69 +807,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
getFittingOptionClass() {
const formControl = this.generalSettingsForm.get('fittingOption');
let val = FITTING_OPTION.HEIGHT;
if (formControl === undefined) {
val = FITTING_OPTION.HEIGHT;
}
val = formControl?.value;
if (
this.mangaReaderService.isWideImage(this.canvasImage) &&
this.layoutMode === LayoutMode.Single &&
val !== FITTING_OPTION.WIDTH &&
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;
}
// 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';
}
// 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';
case FITTING_OPTION.WIDTH:
return 'fa-arrows-alt-h';
case FITTING_OPTION.ORIGINAL:
return 'fa-expand-arrows-alt';
}
}
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) {
value = formControl?.value;
}
return value;
}
cancelMenuCloseTimer() {
if (this.menuTimeout) {
clearTimeout(this.menuTimeout);
@ -927,7 +852,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
onSwipeEvent(event: any) {
console.log('Swipe event occured: ', event);
}
handlePageChange(event: any, direction: string) {
@ -957,8 +881,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
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);
const notInSplit = this.canvasRenderer.shouldMovePrev();
if ((this.pageNum + pageAmount >= this.maxPages && notInSplit)) {
// Move to next volume/chapter automatically
@ -984,7 +907,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS));
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
@ -1004,7 +926,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
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();
@ -1080,15 +1001,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
renderPage() {
console.log('[Manga Reader] renderPage()');
const page = [this.canvasImage];
this.canvasRenderer.renderPage(page);
this.singleRenderer.renderPage(page);
this.doubleRenderer.renderPage(page);
this.doubleReverseRenderer.renderPage(page);
this.canvasRenderer?.renderPage(page);
this.singleRenderer?.renderPage(page);
this.doubleRenderer?.renderPage(page);
this.doubleReverseRenderer?.renderPage(page);
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
if (this.FittingOption !== FITTING_OPTION.HEIGHT) {
this.readingArea.nativeElement.scroll(0,0);
}
@ -1103,7 +1022,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|| document.documentElement.clientHeight
|| document.body.clientHeight;
const needsSplitting = this.mangaReaderService.isWideImage(this.canvasImage);
const needsSplitting = this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src));
let newScale = this.FittingOption;
const widthRatio = windowWidth / (this.canvasImage.width / (needsSplitting ? 2 : 1));
const heightRatio = windowHeight / (this.canvasImage.height);
@ -1142,10 +1061,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
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(this.pageNum, ' Prefetched pages: ', pages.map(p => {
if (this.pageNum === p) return '[' + p + ']';
return '' + p
}));
// console.log(this.pageNum, ' Prefetched pages: ', pages.map(p => {
// if (this.pageNum === p) return '[' + p + ']';
// return '' + p
// }));
}
@ -1156,6 +1075,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.readerMode === ReaderMode.Webtoon) return;
this.isLoading = true;
this.setPageNum(this.pageNum);
this.setCanvasImage();
this.cdRef.markForCheck();
@ -1308,6 +1228,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.readerMode !== ReaderMode.Webtoon) {
this.canvasImage = this.cachedImages[this.pageNum & this.cachedImages.length];
this.currentImage.next(this.canvasImage);
this.pageNumSubject.next({pageNum: this.pageNum, maxPages: this.maxPages});
this.isLoading = true;
}
@ -1401,4 +1322,26 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
{key: 'SPACE', description: 'Toggle Menu'},
];
}
// menu only code
savePref() {
const modelSettings = this.generalSettingsForm.value;
// Get latest preferences from user, overwrite with what we manage in this UI, then save
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (!user) return;
const data = {...user.preferences};
data.layoutMode = parseInt(modelSettings.layoutMode, 10);
data.readerMode = this.readerMode;
data.autoCloseMenu = this.autoCloseMenu;
data.readingDirection = this.readingDirection;
data.emulateBook = modelSettings.emulateBook;
this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
this.toastr.success('User preferences updated');
if (this.user) {
this.user.preferences = updatedPrefs;
this.cdRef.markForCheck();
}
})
});
}
}

View File

@ -1,10 +1,12 @@
<div class="image-container {{imageFitClass$ | async}}"
[style.filter]="(darkenss$ | async) ?? '' | safeStyle">
<ng-container *ngIf="currentImage && isValid()">
<img alt=" "
#image [src]="currentImage.src"
id="image-1"
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
>
</ng-container>
</div>
<ng-container *ngIf="isValid() && !this.mangaReaderService.shouldSplit(this.currentImage, this.pageSplit)">
<div class="image-container {{imageFitClass$ | async}}"
[style.filter]="(darkenss$ | async) ?? '' | safeStyle">
<ng-container *ngIf="currentImage">
<img alt=" "
#image [src]="currentImage.src"
id="image-1"
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
>
</ng-container>
</div>
</ng-container>

View File

@ -1,8 +1,9 @@
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 { combineLatest, filter, map, Observable, of, shareReplay, Subject, takeUntil, tap } 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';
@ -19,12 +20,9 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
@Input() readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>;
/**
* The image fit class
*/
@Input() imageFit$!: Observable<FITTING_OPTION>;
@Input() bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
@ -36,32 +34,43 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
layoutMode: LayoutMode = LayoutMode.Single;
pageSplit: PageSplitOption = PageSplitOption.FitSplit;
pageNum: number = 0;
maxPages: number = 1;
private readonly onDestroy = new Subject<void>();
get ReaderMode() {return ReaderMode;}
get LayoutMode() {return LayoutMode;}
constructor(private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService,
@Inject(DOCUMENT) private document: Document) { }
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
@Inject(DOCUMENT) private document: Document, private 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'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
);
this.pageNum$.pipe(
takeUntil(this.onDestroy),
tap(pageInfo => {
this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages;
}),
).subscribe(() => {});
this.darkenss$ = this.readerSettings$.pipe(
filter(_ => this.isValid()),
map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
);
this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
filter(_ => this.isValid()),
map(showOverlay => showOverlay ? 'blur' : ''),
takeUntil(this.onDestroy)
takeUntil(this.onDestroy),
filter(_ => this.isValid()),
);
this.readerSettings$.pipe(
@ -75,32 +84,31 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
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);
})
}),
filter(_ => this.isValid()),
).subscribe(() => {});
this.imageFitClass$ = zip(this.readerSettings$, this.image$).pipe(
takeUntil(this.onDestroy),
filter(_ => this.isValid()),
this.imageFitClass$ = combineLatest([this.readerSettings$, this.pageNum$]).pipe(
map(values => values[0].fitting),
map(fit => {
if (
this.mangaReaderService.isWideImage(this.currentImage) &&
this.layoutMode === LayoutMode.Single &&
fit !== FITTING_OPTION.WIDTH &&
this.mangaReaderService.isWidePage(this.pageNum) &&
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 FITTING_OPTION.WIDTH + ' fit-to-screen';
}
return fit;
})
}),
shareReplay(),
filter(_ => this.isValid()),
takeUntil(this.onDestroy),
);
}

View File

@ -1,5 +1,6 @@
import { LibraryType } from "src/app/_models/library";
import { MangaFormat } from "src/app/_models/manga-format";
import { FileDimension } from "./file-dimension";
export interface ChapterInfo {
chapterNumber: string;
@ -16,4 +17,8 @@ export interface ChapterInfo {
pages: number;
subtitle: string;
title: string;
/**
* This will not always be present. Depends on if asked from backend.
*/
pageDimensions: Array<FileDimension>;
}

View File

@ -2,4 +2,6 @@ export interface FileDimension {
pageNumber: number;
width: number;
height: number;
}
}
export type DimensionMap = {[key: number]: {width: number, height: number, isWide: boolean}};

View File

@ -10,4 +10,5 @@ export interface ReaderSetting {
darkness: number;
pagingDirection: PAGING_DIRECTION;
readerMode: ReaderMode;
emulateBook: boolean;
}

View File

@ -32,7 +32,7 @@ export interface ImageRenderer {
*/
shouldMoveNext(): boolean;
/**
* Returns the number of pages that should occur based on page direction and internal state of the renderer.
* Returns the number of pages that should occur based on page direction and internal state of the renderer. Should return 0 when not in the conditions to render.
*/
getPageAmount(direction: PAGING_DIRECTION): number;
/**

View File

@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core';
import { FITTING_OPTION } from '../_models/reader-enums';
@Pipe({
name: 'fittingIcon',
pure: true
})
export class FittingIconPipe implements PipeTransform {
transform(fit: FITTING_OPTION): string {
switch(fit) {
case FITTING_OPTION.HEIGHT:
return 'fa fa-arrows-alt-v';
case FITTING_OPTION.WIDTH:
return 'fa fa-arrows-alt-h';
case FITTING_OPTION.ORIGINAL:
return 'fa fa-expand-arrows-alt';
}
}
}

View File

@ -1,8 +1,8 @@
import { DOCUMENT } from '@angular/common';
import { ElementRef, Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
import { ElementRef, 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 { DimensionMap, FileDimension } from '../_models/file-dimension';
import { FITTING_OPTION } from '../_models/reader-enums';
@Injectable({
@ -10,28 +10,58 @@ import { FITTING_OPTION } from '../_models/reader-enums';
})
export class ManagaReaderService {
private pageDimensions: DimensionMap = {};
private pairs: {[key: number]: number} = {};
private renderer: Renderer2;
constructor(rendererFactory: RendererFactory2, @Inject(DOCUMENT) private document: Document, private readerService: ReaderService) {
constructor(rendererFactory: RendererFactory2, private readerService: ReaderService) {
this.renderer = rendererFactory.createRenderer(null, null);
}
loadPageDimensions(dims: Array<FileDimension>) {
this.pageDimensions = {};
let counter = 0;
let i = 0;
dims.forEach(d => {
const isWide = (d.width > d.height);
this.pageDimensions[d.pageNumber] = {
height: d.height,
width: d.width,
isWide: isWide
};
if (isWide) {
this.pairs[d.pageNumber] = d.pageNumber;
} else {
this.pairs[d.pageNumber] = counter % 2 === 0 ? Math.max(i - 1, 0) : counter;
counter++;
}
i++;
});
}
adjustForDoubleReader(page: number) {
if (!this.pairs.hasOwnProperty(page)) return page;
return this.pairs[page];
}
getPageDimensions(pageNum: number) {
if (!this.pageDimensions.hasOwnProperty(pageNum)) return null;
return this.pageDimensions[pageNum];
}
/**
* If the image's width is greater than it's height
* @param elem Image
* @param pageNum Page number - Expected to call loadPageDimensions before this call
*/
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;
isWidePage(pageNum: number) {
if (!this.pageDimensions.hasOwnProperty(pageNum)) return false;
return this.pageDimensions[pageNum].isWide;
}
/**
* If pagenumber is 0 aka first page, which on double page rendering should always render as a single.
*
@ -64,7 +94,7 @@ export class ManagaReaderService {
* If the current page is second to last image
*/
isSecondLastImage(pageNum: number, maxPages: number) {
return maxPages - 1 - pageNum === 2;
return maxPages - 2 === pageNum;
}
/**
@ -81,7 +111,7 @@ export class ManagaReaderService {
* @returns
*/
shouldSplit(img: HTMLImageElement, pageSplitOption: PageSplitOption) {
const needsSplitting = this.isWideImage(img);
const needsSplitting = this.isWidePage(this.readerService.imageUrlToPageNum(img?.src));
return !(this.isNoSplit(pageSplitOption) || !needsSplitting)
}

View File

@ -17,6 +17,7 @@ import { SingleRendererComponent } from './_components/single-renderer/single-re
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';
import { FittingIconPipe } from './_pipes/fitting-icon.pipe';
@NgModule({
declarations: [
@ -30,6 +31,7 @@ import { MangaReaderComponent } from './_components/manga-reader/manga-reader.co
SingleRendererComponent,
DoubleRendererComponent,
DoubleReverseRendererComponent,
FittingIconPipe,
],
imports: [
CommonModule,

View File

@ -18,11 +18,11 @@ export class SwipeDirective {
touchCancels$!: Observable<TouchEvent>;
@HostListener('touchstart') onTouchStart(event: TouchEvent) {
console.log('Touch Start: ', event);
//console.log('Touch Start: ', event);
}
@HostListener('touchend') onTouchEnd(event: TouchEvent) {
console.log('Touch End: ', event);
//console.log('Touch End: ', event);
}
constructor(private el: ElementRef) {

View File

@ -57,13 +57,13 @@
</div>
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="series !== undefined" #scrollingBlock>
<div class="row mb-3 info-container">
<div class="row mb-0 mb-xl-5 info-container">
<div class="image-container col-4 col-sm-6 col-md-4 col-lg-4 col-xl-2 col-xxl-2 d-none d-sm-block">
<div class="to-read-counter" *ngIf="unreadCount > 0 && unreadCount !== totalCount">
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed" fillStyle="filled">{{unreadCount}}</app-tag-badge>
</div>
<app-image height="100%" maxHeight="400px" objectFit="contain" background="none" [imageUrl]="seriesImage"></app-image>
<div class="under-image mt-1" *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
<div class="under-image" *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
Continue {{ContinuePointTitle}}
</div>
</div>

View File

@ -68,6 +68,9 @@
<div class="row mt-2">
<p>Help us out by following <a href="https://wiki.kavitareader.com/en/guides/managing-your-files" rel="noopener noreferrer" target="_blank" referrerpolicy="no-refer">our guide</a> to naming and organizing your media.</p>
</div>
<div class="row mt-2">
<p>Kavita has <a href="https://wiki.kavitareader.com/en/guides/managing-your-files/scanner#introduction" rel="noopener noreferrer" target="_blank" referrerpolicy="no-refer">folder requirements</a>. Check this link to ensure you are following, else files my not show up in scan.</p>
</div>
</ng-template>
</li>

View File

@ -86,7 +86,7 @@
<app-stat-list [data$]="mostActiveSeries$" title="Popular Series"></app-stat-list>
</div>
<div class="col-auto">
<app-stat-list [data$]="recentlyRead$" title="Recently Read"></app-stat-list>
<app-stat-list [data$]="recentlyRead$" title="Recently Read" [handleClick]="handleRecentlyReadClick"></app-stat-list>
</div>
</div>

View File

@ -1,6 +1,8 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { map, Observable, shareReplay, Subject, takeUntil } from 'rxjs';
import { DownloadService } from 'src/app/shared/_services/download.service';
import { Series } from 'src/app/_models/series';
import { User } from 'src/app/_models/user';
import { StatisticsService } from 'src/app/_services/statistics.service';
import { FileExtensionBreakdown } from '../../_models/file-breakdown';
@ -24,7 +26,7 @@ export class ServerStatsComponent implements OnInit, OnDestroy {
stats$!: Observable<ServerStatistics>;
private readonly onDestroy = new Subject<void>();
constructor(private statService: StatisticsService) {
constructor(private statService: StatisticsService, private router: Router) {
this.stats$ = this.statService.getServerStatistics().pipe(takeUntil(this.onDestroy), shareReplay());
this.releaseYears$ = this.statService.getTopYears().pipe(takeUntil(this.onDestroy));
this.mostActiveUsers$ = this.stats$.pipe(
@ -54,7 +56,7 @@ export class ServerStatsComponent implements OnInit, OnDestroy {
this.recentlyRead$ = this.stats$.pipe(
map(d => d.recentlyRead),
map(counts => counts.map(count => {
return {name: count.name, value: -1};
return {name: count.name, value: -1, extra: count};
})),
takeUntil(this.onDestroy)
);
@ -70,6 +72,11 @@ export class ServerStatsComponent implements OnInit, OnDestroy {
this.onDestroy.complete();
}
handleRecentlyReadClick = (data: PieDataItem) => {
const series = data.extra as Series;
this.router.navigate(['library', series.libraryId, 'series', series.id]);
}
}

View File

@ -5,7 +5,7 @@
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0" *ngIf="description && description.length > 0"></i>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item" *ngFor="let item of data">
<li class="list-group-item" [ngClass]="{'underline': handleClick != undefined}" *ngFor="let item of data" (click)="doClick(item)">
{{item.name}} <span class="float-end" *ngIf="item.value >= 0">{{item.value}} {{label}}</span>
</li>
</ul>

View File

@ -1,3 +1,8 @@
.card {
border: var(--bs-card-border-width) solid var(--bs-card-border-color);
}
.underline {
text-decoration: underline;
cursor: pointer;
}

View File

@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { PieData } from '@swimlane/ngx-charts';
import { Observable } from 'rxjs';
import { PieDataItem } from '../../_models/pie-data-item';
@ -23,5 +24,14 @@ export class StatListComponent {
*/
@Input() description: string = '';
@Input() data$!: Observable<PieDataItem[]>;
/**
* Optional callback handler when an item is clicked
*/
@Input() handleClick: ((data: PieDataItem) => void) | undefined = undefined;
doClick(item: PieDataItem) {
if (!this.handleClick) return;
this.handleClick(item);
}
}

View File

@ -150,7 +150,6 @@
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
@ -170,6 +169,19 @@
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook" class="form-check-input" [value]="true">
<label class="form-check-label me-1" for="emulate-book">Emulate comic book</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button" tabindex="0"></i>
</div>
</div>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">Save</button>

View File

@ -126,6 +126,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.settingsForm.addControl('showScreenHints', new FormControl(this.user.preferences.showScreenHints, []));
this.settingsForm.addControl('readerMode', new FormControl(this.user.preferences.readerMode, []));
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.layoutMode, []));
this.settingsForm.addControl('emulateBook', new FormControl(this.user.preferences.emulateBook, []));
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
@ -184,6 +186,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.settingsForm.get('blurUnreadSummaries')?.setValue(this.user.preferences.blurUnreadSummaries);
this.settingsForm.get('promptForDownloadSize')?.setValue(this.user.preferences.promptForDownloadSize);
this.settingsForm.get('noTransitions')?.setValue(this.user.preferences.noTransitions);
this.settingsForm.get('emulateBook')?.setValue(this.user.preferences.emulateBook);
this.cdRef.markForCheck();
this.settingsForm.markAsPristine();
}
@ -214,10 +217,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
blurUnreadSummaries: modelSettings.blurUnreadSummaries,
promptForDownloadSize: modelSettings.promptForDownloadSize,
noTransitions: modelSettings.noTransitions,
emulateBook: modelSettings.emulateBook
};
this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
this.toastr.success('Server settings updated');
this.toastr.success('User preferences updated');
if (this.user) {
this.user.preferences = updatedPrefs;
this.cdRef.markForCheck();

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.6.1.12"
"version": "0.6.1.14"
},
"servers": [
{
@ -3569,6 +3569,15 @@
"type": "boolean",
"default": false
}
},
{
"name": "includeDimensions",
"in": "query",
"description": "Include file dimensions. Only useful for image based reading",
"schema": {
"type": "boolean",
"default": false
}
}
],
"responses": {
@ -9237,6 +9246,10 @@
"type": "boolean",
"description": "Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change"
},
"emulateBook": {
"type": "boolean",
"description": "Manga Reader Option: Emulate a book by applying a shadow effect on the pages"
},
"layoutMode": {
"$ref": "#/components/schemas/LayoutMode"
},
@ -9889,6 +9902,13 @@
"type": "string",
"description": "Series Title",
"nullable": true
},
"pageDimensions": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileDimensionDto"
},
"nullable": true
}
},
"additionalProperties": false,
@ -13894,6 +13914,7 @@
"bookReaderReadingDirection",
"bookReaderTapToPaginate",
"bookReaderThemeName",
"emulateBook",
"globalPageLayoutMode",
"layoutMode",
"noTransitions",
@ -13921,6 +13942,10 @@
"layoutMode": {
"$ref": "#/components/schemas/LayoutMode"
},
"emulateBook": {
"type": "boolean",
"description": "Manga Reader Option: Emulate a book by applying a shadow effect on the pages"
},
"backgroundColor": {
"minLength": 1,
"type": "string",