Manga Reader Work (#1729)

* Instead of augmenting prefetcher to move across chapter bounds, let's try to instead just load 5 images (which the browser will cache) from next/prev so when it loads, it's much faster.

* Trialing loading next/prev chapters 5 pages to have better next page loading experience.

* Tweaked GetChapterInfo API to actually apply conditional includeDimensions parameter.

* added a basic language file for upcoming work

* Moved the bottom menu up a bit for iOS devices with handlebars.

* Fixed fit to width on phones still having a horizontal scrollbar

* Fixed a bug where there is extra space under the image when fit to width and on a phone due to pagination going to far.

* Changed which variable we use for right pagination calculation

* Fixing fit to height

- Fixing height calc to account for horizontal scroll bar height.

* Added a comment for the height scrollbar fix

* Adding screenfull package

# Added:
- Added screenfull package to handle cross-platform browser fullscreen code

# Removed:
- Removed custom fullscreen code

* Fixed a bug where switching from webtoon reader to other layout modes wouldn't render anything. Webtoon continuous scroll down is now broken.

* Fixed it back to how it was and all is good. Need to call detectChanges explicitly.

* Removed an additional undeeded save progress call on loadPage

* Laid out the test case to move the page snapping to the backend with full unit tests. Current code is broken just like UI layer.

* Refactored the snap points into the backend and ensure that it works correctly.

* Fixed a broken unit test

* Filter out spammy hubs/messages calls in the logs

* Swallow all noisy messages that are from RequestLoggingMiddleware when the log level is on Information or above.

* Added a common loading component to the app. Have yet to refactor all screens to use this.

* Bump json5 from 2.2.0 to 2.2.3 in /UI/Web

Bumps [json5](https://github.com/json5/json5) from 2.2.0 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.0...v2.2.3)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

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

* Alrigned all the loading messages and styles throughout the app

* Webtoon reader will use max width of all images to ensure images align well.

* On Original scaling mode, users can use the keyboard to scroll around the images without pagination kicking off.

* Removed console logs

* Fixed a public vs private issue

* Fixed an issue around some cached files getting locked due to NetVips holding them during file size calculations.

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 2023-01-07 09:14:22 -06:00 committed by GitHub
parent 22442d745c
commit 2464a30bc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 367 additions and 390 deletions

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
@ -6,6 +7,7 @@ using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Reader;
using API.Entities;
using API.Entities.Enums;
using API.Helpers;
@ -18,11 +20,13 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using Xunit.Abstractions;
namespace API.Tests.Services;
public class ReaderServiceTests
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly IUnitOfWork _unitOfWork;
@ -33,8 +37,9 @@ public class ReaderServiceTests
private const string BackupDirectory = "C:/kavita/config/backups/";
private const string DataDirectory = "C:/data/";
public ReaderServiceTests()
public ReaderServiceTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options;
_context = new DataContext(contextOptions);
@ -1294,8 +1299,6 @@ public class ReaderServiceTests
// This is first chapter of first volume
prevChapter = await readerService.GetPrevChapterIdAsync(1, 2,4, 1);
Assert.Equal(-1, prevChapter);
//chapterInfoDto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter);
}
[Fact]
@ -2401,7 +2404,6 @@ public class ReaderServiceTests
[Fact]
public void FormatChapterName_Manga_Chapter()
{
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var actual = ReaderService.FormatChapterName(LibraryType.Manga, false, false);
Assert.Equal("Chapter", actual);
}
@ -2409,7 +2411,6 @@ public class ReaderServiceTests
[Fact]
public void FormatChapterName_Book_Chapter_WithTitle()
{
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var actual = ReaderService.FormatChapterName(LibraryType.Book, false, false);
Assert.Equal("Book", actual);
}
@ -2417,7 +2418,6 @@ public class ReaderServiceTests
[Fact]
public void FormatChapterName_Comic()
{
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var actual = ReaderService.FormatChapterName(LibraryType.Comic, false, false);
Assert.Equal("Issue", actual);
}
@ -2425,7 +2425,6 @@ public class ReaderServiceTests
[Fact]
public void FormatChapterName_Comic_WithHash()
{
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var actual = ReaderService.FormatChapterName(LibraryType.Comic, true, true);
Assert.Equal("Issue #", actual);
}
@ -2559,4 +2558,46 @@ public class ReaderServiceTests
#endregion
#region GetPairs
[Theory]
[InlineData("No Wides", new [] {false, false, false}, new [] {"0,0", "1,1", "2,1"})]
[InlineData("Test_odd_spread_1.zip", new [] {false, false, false, false, false, true},
new [] {"0,0", "1,1", "2,1", "3,3", "4,3", "5,5"})]
[InlineData("Test_odd_spread_2.zip", new [] {false, false, false, false, false, true, false, false},
new [] {"0,0", "1,1", "2,1", "3,3", "4,3", "5,5", "6,6", "7,6"})]
[InlineData("Test_even_spread_1.zip", new [] {false, false, false, false, false, false, true},
new [] {"0,0", "1,1", "2,1", "3,3", "4,3", "5,5", "6,6"})]
[InlineData("Test_even_spread_2.zip", new [] {false, false, false, false, false, false, true, false, false},
new [] {"0,0", "1,1", "2,1", "3,3", "4,3", "5,5", "6,6", "7,7", "8,7"})]
[InlineData("Edge_cases_SP01.zip", new [] {true, false, false, false},
new [] {"0,0", "1,1", "2,1", "3,3"})]
[InlineData("Edge_cases_SP02.zip", new [] {false, true, false, false, false},
new [] {"0,0", "1,1", "2,2", "3,2", "4,4"})]
[InlineData("Edge_cases_SP03.zip", new [] {false, false, false, false, false, true, true, false, false, false},
new [] {"0,0", "1,1", "2,1", "3,3", "4,3", "5,5", "6,6", "7,7", "8,7", "9,9"})]
[InlineData("Edge_cases_SP04.zip", new [] {false, false, false, false, false, true, false, true, false, false},
new [] {"0,0", "1,1", "2,1", "3,3", "4,3", "5,5", "6,6", "7,7", "8,8", "9,8"})]
[InlineData("Edge_cases_SP05.zip", new [] {false, false, false, false, false, true, false, false, true, false},
new [] {"0,0", "1,1", "2,1", "3,3", "4,3", "5,5", "6,6", "7,6", "8,8", "9,9"})]
public void GetPairs_ShouldReturnPairsForNoWideImages(string caseName, IList<bool> wides, IList<string> expectedPairs)
{
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var files = wides.Select((b, i) => new FileDimensionDto() {PageNumber = i, Height = 1, Width = 1, FileName = string.Empty, IsWide = b}).ToList();
var pairs = readerService.GetPairs(files);
var expectedDict = new Dictionary<int, int>();
foreach (var pair in expectedPairs)
{
var token = pair.Split(',');
expectedDict.Add(int.Parse(token[0]), int.Parse(token[1]));
}
_testOutputHelper.WriteLine("Case: {0}", caseName);
_testOutputHelper.WriteLine("Expected: {0}", string.Join(", ", expectedDict.Select(kvp => $"{kvp.Key}->{kvp.Value}")));
_testOutputHelper.WriteLine("Actual: {0}", string.Join(", ", pairs.Select(kvp => $"{kvp.Key}->{kvp.Value}")));
Assert.Equal(expectedDict, pairs);
}
#endregion
}

View File

@ -95,7 +95,6 @@
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.24.0" />
<PackageReference Include="System.IO.Abstractions" Version="17.2.3" />
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />

View File

@ -204,9 +204,14 @@ public class ReaderController : BaseApiController
ChapterTitle = dto.ChapterTitle ?? string.Empty,
Subtitle = string.Empty,
Title = dto.SeriesName,
PageDimensions = _cacheService.GetCachedFileDimensions(chapterId)
};
if (includeDimensions)
{
info.PageDimensions = _cacheService.GetCachedFileDimensions(chapterId);
info.DoublePairs = _readerService.GetPairs(info.PageDimensions);
}
if (info.ChapterTitle is {Length: > 0}) {
info.Title += " - " + info.ChapterTitle;
}

View File

@ -65,7 +65,15 @@ public class ChapterInfoDto : IChapterInfoDto
/// </summary>
/// <remarks>Usually just series name, but can include chapter title</remarks>
public string Title { get; set; }
/// <summary>
/// List of all files with their inner archive structure maintained in filename and dimensions
/// </summary>
/// <remarks>This is optionally returned by includeDimensions</remarks>
public IEnumerable<FileDimensionDto> PageDimensions { get; set; }
/// <summary>
/// For Double Page reader, this will contain snap points to ensure the reader always resumes on correct page
/// </summary>
/// <remarks>This is optionally returned by includeDimensions</remarks>
public IDictionary<int, int> DoublePairs { get; set; }
}

View File

@ -10,4 +10,5 @@ public class FileDimensionDto
/// </summary>
/// <example>chapter01_page01.png</example>
public string FileName { get; set; } = default!;
public bool IsWide { get; set; }
}

View File

@ -66,10 +66,20 @@ public static class LogLevelOptions
private static bool ShouldIncludeLogStatement(LogEvent e)
{
if (e.Properties.ContainsKey("SourceContext") &&
e.Properties["SourceContext"].ToString().Replace("\"", string.Empty) == "Serilog.AspNetCore.RequestLoggingMiddleware")
var isRequestLoggingMiddleware = e.Properties.ContainsKey("SourceContext") &&
e.Properties["SourceContext"].ToString().Replace("\"", string.Empty) ==
"Serilog.AspNetCore.RequestLoggingMiddleware";
// If Minimum log level is Information, swallow all Request Logging messages
if (isRequestLoggingMiddleware && LogLevelSwitch.MinimumLevel >= LogEventLevel.Information)
{
return false;
}
if (isRequestLoggingMiddleware)
{
if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/api/health") return false;
if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/hubs/messages") return false;
}
return true;
}

View File

@ -23,6 +23,7 @@ public interface IBookmarkService
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
Task ConvertAllBookmarkToWebP();
Task ConvertAllCoverToWebP();
Task ConvertBookmarkToWebP(int bookmarkId);
}
@ -232,7 +233,7 @@ public class BookmarkService : IBookmarkService
/// <summary>
/// This is a job that runs after a bookmark is saved
/// </summary>
private async Task ConvertBookmarkToWebP(int bookmarkId)
public async Task ConvertBookmarkToWebP(int bookmarkId)
{
var bookmarkDirectory =
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;

View File

@ -73,17 +73,31 @@ public class CacheService : ICacheService
}
var dimensions = new List<FileDimensionDto>();
for (var i = 0; i < files.Length; i++)
var originalCacheSize = Cache.MaxFiles;
try
{
var file = files[i];
using var image = Image.NewFromFile(file, memory:false, access: Enums.Access.SequentialUnbuffered);
dimensions.Add(new FileDimensionDto()
Cache.MaxFiles = 0;
for (var i = 0; i < files.Length; i++)
{
PageNumber = i,
Height = image.Height,
Width = image.Width,
FileName = file.Replace(path, string.Empty)
});
var file = files[i];
using var image = Image.NewFromFile(file, memory: false, access: Enums.Access.SequentialUnbuffered);
dimensions.Add(new FileDimensionDto()
{
PageNumber = i,
Height = image.Height,
Width = image.Width,
IsWide = image.Width > image.Height,
FileName = file.Replace(path, string.Empty)
});
}
}
catch (Exception ex)
{
_logger.LogError("There was an error calculating image dimensions for {ChapterId}", chapterId);
}
finally
{
Cache.MaxFiles = originalCacheSize;
}
_logger.LogDebug("File Dimensions call for {Length} images took {Time}ms", dimensions.Count, sw.ElapsedMilliseconds);
@ -250,7 +264,7 @@ public class CacheService : ICacheService
{
// Calculate what chapter the page belongs to
var path = GetCachePath(chapterId);
// TODO: We can optimize this by extracting and renaming, so we don't need to scan for the files and can do a direct access
// NOTE: We can optimize this by extracting and renaming, so we don't need to scan for the files and can do a direct access
var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions)
.OrderByNatural(Path.GetFileNameWithoutExtension)
.ToArray();

View File

@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -32,6 +33,7 @@ public interface IReaderService
Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber);
Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber);
HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub);
IDictionary<int, int> GetPairs(IEnumerable<FileDimensionDto> dimensions);
}
public class ReaderService : IReaderService
@ -285,17 +287,17 @@ public class ReaderService : IReaderService
/// <returns></returns>
public async Task<int> CapPageToChapter(int chapterId, int page)
{
if (page < 0)
{
page = 0;
}
var totalPages = await _unitOfWork.ChapterRepository.GetChapterTotalPagesAsync(chapterId);
if (page > totalPages)
{
page = totalPages;
}
if (page < 0)
{
page = 0;
}
return page;
}
@ -610,6 +612,49 @@ public class ReaderService : IReaderService
};
}
/// <summary>
/// This is used exclusively for double page renderer. The goal is to break up all files into pairs respecting the reader.
/// wide images should count as 2 pages.
/// </summary>
/// <param name="dimensions"></param>
/// <returns></returns>
public IDictionary<int, int> GetPairs(IEnumerable<FileDimensionDto> dimensions)
{
var pairs = new Dictionary<int, int>();
var files = dimensions.ToList();
if (files.Count == 0) return pairs;
var pairStart = true;
var previousPage = files[0];
pairs.Add(previousPage.PageNumber, previousPage.PageNumber);
foreach(var dimension in files.Skip(1))
{
if (dimension.IsWide)
{
pairs.Add(dimension.PageNumber, dimension.PageNumber);
pairStart = true;
}
else
{
if (previousPage.IsWide || previousPage.PageNumber == 0)
{
pairs.Add(dimension.PageNumber, dimension.PageNumber);
pairStart = true;
}
else
{
pairs.Add(dimension.PageNumber, pairStart ? dimension.PageNumber - 1 : dimension.PageNumber);
pairStart = !pairStart;
}
}
previousPage = dimension;
}
return pairs;
}
/// <summary>
/// Formats a Chapter name based on the library it's in
/// </summary>

257
UI/Web/package-lock.json generated
View File

@ -1240,11 +1240,6 @@
"color-convert": "^2.0.1"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"autoprefixer": {
"version": "10.4.8",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz",
@ -1273,48 +1268,6 @@
}
}
},
"body-parser": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
"integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==",
"requires": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.10.3",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}
}
},
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -1324,11 +1277,6 @@
"balanced-match": "^1.0.0"
}
},
"bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
},
"cacache": {
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.1.tgz",
@ -1382,11 +1330,6 @@
"integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
"dev": true
},
"cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
},
"copy-webpack-plugin": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz",
@ -1453,16 +1396,6 @@
"integrity": "sha512-7GDvDSmE+20+WcSMhP17Q1EVWUrLlbxxpMDqG731n8P99JhnQZHR9YvtjPvEHfjFUjvQJvdpKCjlKOX+xe4UVA==",
"dev": true
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
},
"electron-to-chromium": {
"version": "1.4.212",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.212.tgz",
@ -1505,35 +1438,6 @@
"dev": true,
"optional": true
},
"finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}
}
},
"fraction.js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@ -1572,18 +1476,6 @@
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"requires": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
}
},
"https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@ -1653,12 +1545,6 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true
},
"jsonc-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz",
@ -1802,14 +1688,6 @@
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
"dev": true
},
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
},
"postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
@ -2042,35 +1920,6 @@
"util-deprecate": "^1.0.2"
}
},
"qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"requires": {
"side-channel": "^1.0.4"
}
},
"raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"requires": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
}
}
},
"regenerator-transform": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz",
@ -2111,11 +1960,6 @@
}
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"sass-loader": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.0.2.tgz",
@ -2167,59 +2011,6 @@
}
}
},
"send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"requires": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
},
"dependencies": {
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
"serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -2247,11 +2038,6 @@
"minipass": "^3.1.1"
}
},
"statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
},
"stylus": {
"version": "0.58.1",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.58.1.tgz",
@ -3042,12 +2828,6 @@
"integrity": "sha512-LjQUg1SpLj2GfyaPDVBUHdhmlDU1vDB4f0mJWSGkISoXQrn5/lH3ECPCuo2Bkvf6Y30wO+b69te+rZK/llZmjg==",
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true
},
"magic-string": {
"version": "0.26.2",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz",
@ -3360,11 +3140,6 @@
"once": "^1.3.0"
}
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
},
"minimatch": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
@ -7844,7 +7619,7 @@
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
"integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
"dev": true
},
"codelyzer": {
@ -8559,7 +8334,7 @@
"dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
"integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
"dev": true
},
"deep-equal": {
@ -9473,7 +9248,7 @@
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"fastparse": {
@ -12236,12 +12011,9 @@
"dev": true
},
"json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
"requires": {
"minimist": "^1.2.5"
}
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
},
"jsonc-parser": {
"version": "3.0.0",
@ -12373,7 +12145,7 @@
"levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
"integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
"integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
"dev": true,
"requires": {
"prelude-ls": "~1.1.2",
@ -12439,7 +12211,7 @@
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
"dev": true
},
"log-symbols": {
@ -12973,7 +12745,7 @@
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"needle": {
@ -13116,7 +12888,7 @@
"node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
"integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=",
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"dev": true
},
"node-releases": {
@ -14259,7 +14031,7 @@
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
"integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
"integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
"dev": true
},
"pretty-bytes": {
@ -15141,6 +14913,11 @@
"ajv-keywords": "^3.5.2"
}
},
"screenfull": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-6.0.2.tgz",
"integrity": "sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw=="
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -16017,7 +15794,7 @@
"type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
"integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
"integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
"dev": true,
"requires": {
"prelude-ls": "~1.1.2"

View File

@ -45,6 +45,7 @@
"ngx-toastr": "^14.2.1",
"requires": "^1.0.2",
"rxjs": "~7.5.4",
"screenfull": "^6.0.2",
"swiper": "^8.4.4",
"tslib": "^2.3.1",
"webpack-bundle-analyzer": "^4.5.0",

View File

@ -7,13 +7,12 @@ img {
align-items: center;
&.full-width {
width: 100vw;
height: calc(var(--vh)*100);
display: grid;
}
&.full-height {
height: 100vh;
height: calc(100vh - 34px); // 34px is the height of the horizontal scrollbar that will appear
display: flex; // changed from inline-block to fix the centering on tablets not working
}

View File

@ -14,6 +14,7 @@ import { SeriesFilter } from '../_models/metadata/series-filter';
import { UtilityService } from '../shared/_services/utility.service';
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
import { FileDimension } from '../manga-reader/_models/file-dimension';
import screenfull from 'screenfull';
export const CHAPTER_ID_DOESNT_EXIST = -1;
export const CHAPTER_ID_NOT_FETCHED = -2;
@ -235,25 +236,10 @@ export class ReaderService {
return params;
}
enterFullscreen(el: Element, callback?: VoidFunction) {
if (!document.fullscreenElement) {
if (el.requestFullscreen) {
el.requestFullscreen().then(() => {
if (callback) {
callback();
}
});
}
}
}
toggleFullscreen(el: Element, callback?: VoidFunction) {
exitFullscreen(callback?: VoidFunction) {
if (document.exitFullscreen && this.checkFullscreenMode()) {
document.exitFullscreen().then(() => {
if (callback) {
callback();
}
});
if (screenfull.isEnabled) {
screenfull.toggle();
}
}

View File

@ -6,7 +6,7 @@
</button>
</div>
<div class="modal-body">
<div class="list-group" *ngIf="!isLoading">
<div class="list-group">
<div class="form-check">
<input id="selectall" type="checkbox" class="form-check-input"
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">

View File

@ -5,6 +5,7 @@ import { Member } from 'src/app/_models/auth/member';
import { LibraryService } from 'src/app/_services/library.service';
import { SelectionModel } from 'src/app/typeahead/_components/typeahead.component';
// TODO: Change to OnPush
@Component({
selector: 'app-library-access-modal',
templateUrl: './library-access-modal.component.html',
@ -17,7 +18,6 @@ export class LibraryAccessModalComponent implements OnInit {
selectedLibraries: Array<{selected: boolean, data: Library}> = [];
selections!: SelectionModel<Library>;
selectAll: boolean = false;
isLoading: boolean = false;
get hasSomeSelected() {
return this.selections != null && this.selections.hasSomeSelected();
@ -49,7 +49,6 @@ export class LibraryAccessModalComponent implements OnInit {
setupSelections() {
this.selections = new SelectionModel<Library>(false, this.allLibraries);
this.isLoading = false;
// If a member is passed in, then auto-select their libraries
if (this.member !== undefined) {

View File

@ -26,7 +26,7 @@
</div>
</li>
<li *ngIf="loading" class="list-group-item">
<div class="spinner-border text-secondary" role="status">
<div class="spinner-border text-primary" role="status">
<span class="invisible">Loading...</span>
</div>
</li>

View File

@ -21,6 +21,4 @@
</div>
<div class="spinner-border text-secondary" *ngIf="isLoading" role="status">
<span class="invisible">Loading...</span>
</div>
<app-loading [loading]="isLoading"></app-loading>

View File

@ -473,7 +473,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.navService.showNavBar();
this.navService.showSideNav();
this.readerService.exitFullscreen();
this.onDestroy.next();
this.onDestroy.complete();
@ -1203,13 +1202,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
toggleFullscreen() {
this.isFullscreen = this.readerService.checkFullscreenMode();
if (this.isFullscreen) {
this.readerService.exitFullscreen(() => {
this.readerService.toggleFullscreen(this.reader.nativeElement, () => {
this.isFullscreen = false;
this.cdRef.markForCheck();
this.renderer.removeStyle(this.reader.nativeElement, 'background');
});
} else {
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
this.readerService.toggleFullscreen(this.reader.nativeElement, () => {
this.isFullscreen = true;
this.cdRef.markForCheck();
// HACK: This is a bug with how browsers change the background color for fullscreen mode

View File

@ -20,7 +20,7 @@
</li>
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No collections created yet</li>
<li class="list-group-item" *ngIf="loading">
<div class="spinner-border text-secondary" role="status">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</li>

View File

@ -47,11 +47,7 @@
</div>
</ng-template>
<div class="mx-auto" *ngIf="isLoading" style="width: 200px;">
<div class="spinner-border text-secondary loading" role="status">
<span class="invisible">Loading...</span>
</div>
</div>
<app-loading [loading]="true"></app-loading>
<ng-template #jumpBar>
<div class="jump-bar">

View File

@ -6,6 +6,7 @@ import { ScrollService } from 'src/app/_services/scroll.service';
import { ReaderService } from '../../../_services/reader.service';
import { PAGING_DIRECTION } from '../../_models/reader-enums';
import { WebtoonImage } from '../../_models/webtoon-image';
import { ManagaReaderService } from '../../_series/managa-reader.service';
/**
* How much additional space should pass, past the original bottom of the document height before we trigger the next chapter load
@ -129,7 +130,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
/**
* Debug mode. Will show extra information. Use bitwise (|) operators between different modes to enable different output
*/
debugMode: DEBUG_MODES = DEBUG_MODES.None;
debugMode: DEBUG_MODES = DEBUG_MODES.Logs;
/**
* Debug mode. Will filter out any messages in here so they don't hit the log
*/
@ -153,7 +154,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
constructor(private readerService: ReaderService, private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document, private scrollService: ScrollService,
private readonly cdRef: ChangeDetectorRef) {
private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService) {
// This will always exist at this point in time since this is used within manga reader
const reader = document.querySelector('.reading-area');
if (reader !== null) {
@ -453,7 +454,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.webtoonImageWidth = event.target.width;
}
this.renderer.setAttribute(event.target, 'width', this.webtoonImageWidth + '');
this.renderer.setAttribute(event.target, 'width', this.mangaReaderService.maxWidth() + '');
this.renderer.setAttribute(event.target, 'height', event.target.height + '');
this.attachIntersectionObserverElem(event.target);

View File

@ -26,11 +26,7 @@
</div>
</div>
</div>
<!-- <ng-container *ngIf="isLoading">
<div class="spinner-border text-secondary loading" role="status">
<span class="invisible">Loading...</span>
</div>
</ng-container> -->
<app-loading [loading]="isLoading"></app-loading>
<div class="reading-area"
appSwipe (swipeEvent)="onSwipeEvent($event)"
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
@ -57,7 +53,7 @@
<div class="{{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')"
[ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? ImageHeight: '25%'),
'left': 'inherit',
'right': rightPaginationOffset + 'px'}">
'right': RightPaginationOffset + 'px'}">
<div *ngIf="showClickOverlay">
<i class="fa fa-angle-{{readingDirection === ReadingDirection.LeftToRight ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'right' : 'down'}}"
title="Next Page" aria-hidden="true"></i>
@ -93,7 +89,7 @@
</ng-container>
<ng-template #webtoon>
<div class="webtoon-images" *ngIf="readerMode === ReaderMode.Webtoon && !isLoading && !inSetup">
<div class="webtoon-images" *ngIf="!isLoading && !inSetup">
<app-infinite-scroller [pageNum]="pageNum"
[bufferPages]="5"
[goToPage]="goToPageEvent"
@ -130,7 +126,7 @@
<button class="btn btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter();resetMenuCloseTimer();" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
</div>
</div>
<div class="row pt-4 ms-2 me-2">
<div class="row pt-4 ms-2 me-2 mb-2">
<div class="col">
<button class="btn btn-icon" (click)="setReadingDirection();resetMenuCloseTimer();" [disabled]="readerMode === ReaderMode.Webtoon || readerMode === ReaderMode.UpDown" aria-describedby="reading-direction" title="Reading Direction: {{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}">
<i class="fa fa-angle-double-{{readingDirection === ReadingDirection.LeftToRight ? 'right' : 'left'}}" aria-hidden="true"></i>

View File

@ -176,6 +176,7 @@ $pointer-offset: 5px;
top: 0px;
width: $side-width;
background: $pagination-bg;
max-height: calc(var(--vh)*100);
z-index: 100;
}
@ -194,6 +195,7 @@ $pointer-offset: 5px;
top: 0px;
width: $side-width;
background: $pagination-bg;
max-height: calc(var(--vh)*100);
z-index: 100;
}

View File

@ -1,7 +1,7 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, debounceTime, distinctUntilChanged, forkJoin, fromEvent, map, merge, Observable, ReplaySubject, Subject, take, takeUntil, tap } from 'rxjs';
import { BehaviorSubject, combineLatest, debounceTime, distinctUntilChanged, filter, forkJoin, fromEvent, map, merge, Observable, of, ReplaySubject, Subject, take, takeUntil, tap } from 'rxjs';
import { LabelType, ChangeContext, Options } from '@angular-slider/ngx-slider';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { FormGroup, FormBuilder, FormControl } from '@angular/forms';
@ -30,6 +30,7 @@ import { CanvasRendererComponent } from '../canvas-renderer/canvas-renderer.comp
import { DoubleRendererComponent } from '../double-renderer/double-renderer.component';
import { DoubleReverseRendererComponent } from '../double-reverse-renderer/double-reverse-renderer.component';
import { SingleRendererComponent } from '../single-renderer/single-renderer.component';
import { ChapterInfo } from '../../_models/chapter-info';
const PREFETCH_PAGES = 10;
@ -41,6 +42,19 @@ const ANIMATION_SPEED = 200;
const OVERLAY_AUTO_CLOSE_TIME = 3000;
const CLICK_OVERLAY_TIMEOUT = 3000;
enum ChapterInfoPosition {
Previous = 0,
Current = 1,
Next = 2
}
enum KeyDirection {
Right = 0,
Left = 1,
Up = 2,
Down = 3
}
@Component({
selector: 'app-manga-reader',
templateUrl: './manga-reader.component.html',
@ -138,6 +152,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
isLoading = true;
hasBookmarkRights: boolean = false; // TODO: This can be an observable
getPageFn!: (pageNum: number) => HTMLImageElement;
@ -165,6 +180,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/
continuousChaptersStack: Stack<number> = new Stack();
continuousChapterInfos: Array<ChapterInfo | undefined> = [undefined, undefined, undefined];
/**
* An event emitter when a page change occurs. Used solely by the webtoon reader.
*/
@ -511,7 +528,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.onDestroy.next();
this.onDestroy.complete();
this.showBookmarkEffectEvent.complete();
this.readerService.exitFullscreen();
if (this.goToPageEvent !== undefined) this.goToPageEvent.complete();
}
@ -526,14 +542,22 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
switch (this.readerMode) {
case ReaderMode.LeftRight:
if (event.key === KEY_CODES.RIGHT_ARROW) {
//if (!this.checkIfPaginationAllowed()) return;
if (!this.checkIfPaginationAllowed(KeyDirection.Right)) return;
this.readingDirection === ReadingDirection.LeftToRight ? this.nextPage() : this.prevPage();
} else if (event.key === KEY_CODES.LEFT_ARROW) {
//if (!this.checkIfPaginationAllowed()) return;
if (!this.checkIfPaginationAllowed(KeyDirection.Left)) return;
this.readingDirection === ReadingDirection.LeftToRight ? this.prevPage() : this.nextPage();
}
break;
case ReaderMode.UpDown:
if (event.key === KEY_CODES.UP_ARROW) {
if (!this.checkIfPaginationAllowed(KeyDirection.Up)) return;
this.prevPage();
} else if (event.key === KEY_CODES.DOWN_ARROW) {
if (!this.checkIfPaginationAllowed(KeyDirection.Down)) return;
this.nextPage();
}
break;
case ReaderMode.Webtoon:
if (event.key === KEY_CODES.DOWN_ARROW) {
this.nextPage()
@ -624,17 +648,36 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
// if there is scroll room and on original, then don't paginate
checkIfPaginationAllowed() {
checkIfPaginationAllowed(direction: KeyDirection) {
// This is not used atm due to the complexity it adds with keyboard.
if (this.readingArea === undefined || this.readingArea.nativeElement === undefined) return true;
const scrollLeft = this.readingArea?.nativeElement?.scrollLeft || 0;
const totalScrollWidth = this.readingArea?.nativeElement?.scrollWidth;
// need to also check if there is scroll needed
const scrollTop = this.readingArea?.nativeElement?.scrollTop || 0;
if (this.FittingOption === FITTING_OPTION.ORIGINAL && scrollLeft < totalScrollWidth) {
return false;
switch (direction) {
case KeyDirection.Right:
if (this.FittingOption === FITTING_OPTION.ORIGINAL && scrollLeft < this.readingArea?.nativeElement.scrollWidth - this.readingArea?.nativeElement.clientWidth) {
return false;
}
break;
case KeyDirection.Left:
if (this.FittingOption === FITTING_OPTION.ORIGINAL && scrollLeft > 0) {
return false;
}
break;
case KeyDirection.Up:
if (this.FittingOption === FITTING_OPTION.ORIGINAL && scrollTop > 0) {
return false;
}
break;
case KeyDirection.Down:
if (this.FittingOption === FITTING_OPTION.ORIGINAL && scrollTop < this.readingArea?.nativeElement.scrollHeight - this.readingArea?.nativeElement.clientHeight) {
return false;
}
break;
}
return true;
}
@ -712,8 +755,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
this.mangaReaderService.loadPageDimensions(results.chapterInfo.pageDimensions);
this.mangaReaderService.load(results.chapterInfo);
this.continuousChapterInfos[ChapterInfoPosition.Current] = results.chapterInfo;
this.volumeId = results.chapterInfo.volumeId;
this.maxPages = results.chapterInfo.pages;
let page = results.progress.pageNum;
@ -755,6 +799,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} else {
// Fetch the first page of next chapter
this.getPage(0, this.nextChapterId);
}
});
this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
@ -984,6 +1029,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
renderPage() {
const page = [this.canvasImage];
// After switching from webtoon mode, these are all undefined
this.canvasRenderer?.renderPage(page);
this.singleRenderer?.renderPage(page);
this.doubleRenderer?.renderPage(page);
@ -1027,28 +1075,24 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
prefetch() {
// NOTE: This doesn't allow for any directionality
// NOTE: This doesn't maintain 1 image behind at all times
// NOTE: I may want to provide a different prefetcher for double renderer
for(let i = 0; i <= PREFETCH_PAGES - 3; i++) {
const numOffset = this.pageNum + i;
//console.log('numOffset: ', numOffset);
if (numOffset > this.maxPages - 1) continue;
let numOffset = this.pageNum + i;
if (numOffset > this.maxPages - 1) {
continue;
}
const index = (numOffset % this.cachedImages.length + this.cachedImages.length) % this.cachedImages.length;
const cachedImagePageNum = this.readerService.imageUrlToPageNum(this.cachedImages[index].src);
const cachedImageChapterId = this.readerService.imageUrlToChapterId(this.cachedImages[index].src);
//console.log('chapter id for ', cachedImagePageNum, ' = ', cachedImageChapterId)
if (cachedImagePageNum !== numOffset) { // && cachedImageChapterId === this.chapterId
if (cachedImagePageNum !== numOffset) {
this.cachedImages[index] = new Image();
this.cachedImages[index].src = this.getPageUrl(numOffset);
}
}
const pages = this.cachedImages.map(img => this.readerService.imageUrlToPageNum(img.src));
const pagesBefore = pages.filter(p => p >= 0 && p < this.pageNum).length;
const pagesAfter = pages.filter(p => p >= 0 && p > this.pageNum).length;
//console.log('Buffer Health: Before: ', pagesBefore, ' After: ', pagesAfter);
// const pages = this.cachedImages.map(img => [this.readerService.imageUrlToChapterId(img.src), this.readerService.imageUrlToPageNum(img.src)]);
// console.log(this.pageNum, ' Prefetched pages: ', pages.map(p => {
// if (this.pageNum === p) return '[' + p + ']';
// if (this.pageNum === p[1]) return '[' + p + ']';
// return '' + p
// }));
}
@ -1061,7 +1105,6 @@ 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();
@ -1094,6 +1137,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// This will update the value for value except when in webtoon due to how the webtoon reader
// responds to page changes
if (this.readerMode !== ReaderMode.Webtoon) {
console.log('Setting Page Number as slider drag occured');
this.setPageNum(context.value);
}
}
@ -1107,6 +1151,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS);
}
console.log('Setting Page Number as slider page update occurred');
this.setPageNum(this.adjustPagesForDoubleRenderer(page));
this.refreshSlider.emit();
this.goToPageEvent.next(this.pageNum);
@ -1122,13 +1167,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// Tell server to cache the next chapter
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) {
this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => {
this.continuousChapterInfos[ChapterInfoPosition.Next] = res;
this.nextChapterPrefetched = true;
this.prefetchStartOfChapter(this.nextChapterId, PAGING_DIRECTION.FORWARD);
});
}
} else if (this.pageNum <= 10) {
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) {
this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => {
this.continuousChapterInfos[ChapterInfoPosition.Previous] = res;
this.prevChapterPrefetched = true;
this.prefetchStartOfChapter(this.nextChapterId, PAGING_DIRECTION.BACKWARDS);
});
}
}
@ -1144,6 +1193,30 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
/**
* Loads the first 5 images (throwaway cache) from the given chapterId
* @param chapterId
* @param direction Used to indicate if the chapter is behind or ahead of curent chapter
*/
prefetchStartOfChapter(chapterId: number, direction: PAGING_DIRECTION) {
let pages = [];
if (direction === PAGING_DIRECTION.BACKWARDS) {
if (this.continuousChapterInfos[ChapterInfoPosition.Previous] === undefined) return;
const n = this.continuousChapterInfos[ChapterInfoPosition.Previous]!.pages;
pages = Array.from({length: n + 1}, (v, k) => n - k);
} else {
pages = [0, 1, 2, 3, 4];
}
let images = [];
pages.forEach((_, i: number) => {
let img = new Image();
img.src = this.getPageUrl(i, chapterId);
images.push(img)
});
}
goToPage(pageNum: number) {
let page = pageNum;
@ -1165,6 +1238,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS);
}
console.log('Setting Page Number as goto page');
this.setPageNum(this.adjustPagesForDoubleRenderer(page));
this.goToPageEvent.next(page);
this.render();
@ -1179,20 +1253,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// This is menu only code
toggleFullscreen() {
this.isFullscreen = this.readerService.checkFullscreenMode();
if (this.isFullscreen) {
this.readerService.exitFullscreen(() => {
this.isFullscreen = false;
this.fullscreenEvent.next(false);
this.render();
});
} else {
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
this.readerService.toggleFullscreen(this.reader.nativeElement, () => {
this.isFullscreen = true;
this.fullscreenEvent.next(true);
this.render();
});
}
}
// This is menu only code
@ -1212,10 +1277,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// We must set this here because loadPage from render doesn't call if we aren't page splitting
if (this.readerMode !== ReaderMode.Webtoon) {
this.canvasImage = this.cachedImages[this.pageNum & this.cachedImages.length];
this.canvasImage = this.getPage(this.pageNum);
this.currentImage.next(this.canvasImage);
this.pageNumSubject.next({pageNum: this.pageNum, maxPages: this.maxPages});
this.isLoading = true;
//this.isLoading = true;
this.cdRef.detectChanges(); // Must use detectChanges to ensure ViewChildren get updated again
}
this.updateForm();
@ -1243,6 +1309,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
handleWebtoonPageChange(updatedPageNum: number) {
console.log('Setting Page Number as webtoon page changed');
this.setPageNum(updatedPageNum);
}

View File

@ -20,5 +20,9 @@ export interface ChapterInfo {
/**
* This will not always be present. Depends on if asked from backend.
*/
pageDimensions: Array<FileDimension>;
pageDimensions?: Array<FileDimension>;
/**
* This will not always be present. Depends on if asked from backend.
*/
doublePairs?: {[key: number]: number};
}

View File

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

View File

@ -2,6 +2,7 @@ import { ElementRef, Injectable, Renderer2, RendererFactory2 } from '@angular/co
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 { ChapterInfo } from '../_models/chapter-info';
import { DimensionMap, FileDimension } from '../_models/file-dimension';
import { FITTING_OPTION } from '../_models/reader-enums';
@ -13,41 +14,22 @@ export class ManagaReaderService {
private pageDimensions: DimensionMap = {};
private pairs: {[key: number]: number} = {};
private renderer: Renderer2;
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);
load(chapterInfo: ChapterInfo) {
chapterInfo.pageDimensions!.forEach(d => {
this.pageDimensions[d.pageNumber] = {
height: d.height,
width: d.width,
isWide: isWide
isWide: d.isWide
};
//console.log('Page Number: ', d.pageNumber);
if (isWide) {
console.log('\tPage is wide, counter: ', counter, 'i: ', i);
this.pairs[d.pageNumber] = d.pageNumber;
//this.pairs[d.pageNumber] = this.pairs[d.pageNumber - 1] + 1;
} else {
//console.log('\tPage is single, counter: ', counter, 'i: ', i);
this.pairs[d.pageNumber] = counter % 2 === 0 ? Math.max(i - 1, 0) : counter;
counter++;
}
//console.log('\t\tMapped to ', this.pairs[d.pageNumber]);
i++;
});
//console.log('pairs: ', this.pairs);
this.pairs = chapterInfo.doublePairs!;
}
adjustForDoubleReader(page: number) {
if (!this.pairs.hasOwnProperty(page)) return page;
return this.pairs[page];
@ -67,6 +49,14 @@ export class ManagaReaderService {
return this.pageDimensions[pageNum].isWide;
}
maxHeight() {
return Object.values(this.pageDimensions).reduce((max, obj) => Math.max(max, obj.height), 0);
}
maxWidth() {
return Object.values(this.pageDimensions).reduce((max, obj) => Math.max(max, obj.width), 0);
}
/**

View File

@ -101,7 +101,6 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
this.navService.showNavBar();
this.navService.showSideNav();
this.readerService.exitFullscreen();
this.onDestroy.next();
this.onDestroy.complete();

View File

@ -59,11 +59,7 @@
<ng-container *ngIf="items.length === 0 && !isLoading">
Nothing added
</ng-container>
<ng-container *ngIf="isLoading">
<div class="spinner-border text-secondary" role="status">
<span class="invisible">Loading...</span>
</div>
</ng-container>
<app-loading [loading]="isLoading"></app-loading>
</div>
<!-- TODO: This needs virtualization -->

View File

@ -20,7 +20,7 @@
</li>
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No lists created yet</li>
<li class="list-group-item" *ngIf="loading">
<div class="spinner-border text-secondary" role="status">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</li>

View File

@ -308,9 +308,5 @@
<div [ngbNavOutlet]="nav"></div>
</ng-container>
<div class="mx-auto" *ngIf="isLoading" style="width: 200px;">
<div class="spinner-border text-secondary loading" role="status">
<span class="invisible">Loading...</span>
</div>
</div>
<app-loading [loading]="isLoading"></app-loading>
</div>

View File

@ -0,0 +1,7 @@
<ng-container *ngIf="loading">
<div class="d-flex justify-content-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</ng-container>

View File

@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-loading',
templateUrl: './loading.component.html',
styleUrls: ['./loading.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoadingComponent implements OnInit {
@Input() loading: boolean = false;
@Input() message: string = '';
constructor(private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void {
}
}

View File

@ -17,6 +17,7 @@ import { BadgeExpanderComponent } from './badge-expander/badge-expander.componen
import { ImageComponent } from './image/image.component';
import { PipeModule } from '../pipe/pipe.module';
import { IconAndTitleComponent } from './icon-and-title/icon-and-title.component';
import { LoadingComponent } from './loading/loading.component';
@NgModule({
declarations: [
@ -32,6 +33,7 @@ import { IconAndTitleComponent } from './icon-and-title/icon-and-title.component
BadgeExpanderComponent,
ImageComponent,
IconAndTitleComponent,
LoadingComponent,
],
imports: [
CommonModule,
@ -54,6 +56,8 @@ import { IconAndTitleComponent } from './icon-and-title/icon-and-title.component
PersonBadgeComponent, // Used Series Detail
BadgeExpanderComponent, // Used Series Detail/Metadata
IconAndTitleComponent, // Used in Series Detail/Metadata
LoadingComponent
],
})
export class SharedModule { }

View File

@ -0,0 +1,3 @@
{
"login": "Test"
}

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.6.1.21"
"version": "0.6.1.22"
},
"servers": [
{
@ -10059,6 +10059,16 @@
"items": {
"$ref": "#/components/schemas/FileDimensionDto"
},
"description": "List of all files with their inner archive structure maintained in filename and dimensions",
"nullable": true
},
"doublePairs": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
},
"description": "For Double Page reader, this will contain snap points to ensure the reader always resumes on correct page",
"nullable": true
}
},
@ -10527,6 +10537,9 @@
"description": "The filename of the cached file. If this was nested in a subfolder, the foldername will be appended with _",
"nullable": true,
"example": "chapter01_page01.png"
},
"isWide": {
"type": "boolean"
}
},
"additionalProperties": false