mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
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:
parent
22442d745c
commit
2464a30bc2
@ -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
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
}
|
||||
|
@ -10,4 +10,5 @@ public class FileDimensionDto
|
||||
/// </summary>
|
||||
/// <example>chapter01_page01.png</example>
|
||||
public string FileName { get; set; } = default!;
|
||||
public bool IsWide { get; set; }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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
257
UI/Web/package-lock.json
generated
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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};
|
||||
}
|
@ -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}};
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
|
@ -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 -->
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
7
UI/Web/src/app/shared/loading/loading.component.html
Normal file
7
UI/Web/src/app/shared/loading/loading.component.html
Normal 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>
|
19
UI/Web/src/app/shared/loading/loading.component.ts
Normal file
19
UI/Web/src/app/shared/loading/loading.component.ts
Normal 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 {
|
||||
}
|
||||
|
||||
}
|
@ -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 { }
|
||||
|
3
UI/Web/src/assets/langs/en.json
Normal file
3
UI/Web/src/assets/langs/en.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"login": "Test"
|
||||
}
|
15
openapi.json
15
openapi.json
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user