diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 166284164..384d22a88 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -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>(), Substitute.For()); 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>(), Substitute.For()); 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>(), Substitute.For()); 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>(), Substitute.For()); 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 wides, IList expectedPairs) + { + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); + 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(); + 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 } diff --git a/API/API.csproj b/API/API.csproj index f1b6b43b7..4b10eba10 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -95,7 +95,6 @@ - diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 8ffcd429e..800743766 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -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; } diff --git a/API/DTOs/Reader/ChapterInfoDto.cs b/API/DTOs/Reader/ChapterInfoDto.cs index 6945c2b24..5ea7be7fd 100644 --- a/API/DTOs/Reader/ChapterInfoDto.cs +++ b/API/DTOs/Reader/ChapterInfoDto.cs @@ -65,7 +65,15 @@ public class ChapterInfoDto : IChapterInfoDto /// /// Usually just series name, but can include chapter title public string Title { get; set; } - + /// + /// List of all files with their inner archive structure maintained in filename and dimensions + /// + /// This is optionally returned by includeDimensions public IEnumerable PageDimensions { get; set; } + /// + /// For Double Page reader, this will contain snap points to ensure the reader always resumes on correct page + /// + /// This is optionally returned by includeDimensions + public IDictionary DoublePairs { get; set; } } diff --git a/API/DTOs/Reader/FileDimensionDto.cs b/API/DTOs/Reader/FileDimensionDto.cs index 33abd99be..baee20dd0 100644 --- a/API/DTOs/Reader/FileDimensionDto.cs +++ b/API/DTOs/Reader/FileDimensionDto.cs @@ -10,4 +10,5 @@ public class FileDimensionDto /// /// chapter01_page01.png public string FileName { get; set; } = default!; + public bool IsWide { get; set; } } diff --git a/API/Logging/LogLevelOptions.cs b/API/Logging/LogLevelOptions.cs index a00547938..dc927f5e9 100644 --- a/API/Logging/LogLevelOptions.cs +++ b/API/Logging/LogLevelOptions.cs @@ -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; } diff --git a/API/Services/BookmarkService.cs b/API/Services/BookmarkService.cs index abebee475..3b280f251 100644 --- a/API/Services/BookmarkService.cs +++ b/API/Services/BookmarkService.cs @@ -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 /// /// This is a job that runs after a bookmark is saved /// - private async Task ConvertBookmarkToWebP(int bookmarkId) + public async Task ConvertBookmarkToWebP(int bookmarkId) { var bookmarkDirectory = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index adc6db7e8..52948c87e 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -73,17 +73,31 @@ public class CacheService : ICacheService } var dimensions = new List(); - 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(); diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index cb73c3d87..8409e6a36 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -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 GetPairs(IEnumerable dimensions); } public class ReaderService : IReaderService @@ -285,17 +287,17 @@ public class ReaderService : IReaderService /// public async Task 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 }; } + /// + /// 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. + /// + /// + /// + public IDictionary GetPairs(IEnumerable dimensions) + { + var pairs = new Dictionary(); + 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; + } + /// /// Formats a Chapter name based on the library it's in /// diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 17498c1e1..11883b72b 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -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" diff --git a/UI/Web/package.json b/UI/Web/package.json index 13c675ecd..7014d733e 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -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", diff --git a/UI/Web/src/_manga-reader-common.scss b/UI/Web/src/_manga-reader-common.scss index 1e987c050..07f49d704 100644 --- a/UI/Web/src/_manga-reader-common.scss +++ b/UI/Web/src/_manga-reader-common.scss @@ -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 } diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 667c3feb8..dcca5e522 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -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(); } } diff --git a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.html b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.html index 886584b95..b4777b52c 100644 --- a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.html +++ b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.html @@ -6,7 +6,7 @@ - +
@@ -57,7 +53,7 @@
+ 'right': RightPaginationOffset + 'px'}">
@@ -93,7 +89,7 @@ -
+
-
+
diff --git a/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.html b/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.html index 47efc6f02..fec590e50 100644 --- a/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.html +++ b/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.html @@ -20,7 +20,7 @@
  • No lists created yet
  • -
    +
    Loading...
  • diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index ace2887c2..63ef062c2 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -308,9 +308,5 @@
    -
    -
    - -
    -
    +
    diff --git a/UI/Web/src/app/shared/loading/loading.component.html b/UI/Web/src/app/shared/loading/loading.component.html new file mode 100644 index 000000000..45d7c9905 --- /dev/null +++ b/UI/Web/src/app/shared/loading/loading.component.html @@ -0,0 +1,7 @@ + +
    +
    + Loading... +
    +
    +
    \ No newline at end of file diff --git a/UI/Web/src/app/shared/loading/loading.component.scss b/UI/Web/src/app/shared/loading/loading.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/UI/Web/src/app/shared/loading/loading.component.ts b/UI/Web/src/app/shared/loading/loading.component.ts new file mode 100644 index 000000000..feb0af942 --- /dev/null +++ b/UI/Web/src/app/shared/loading/loading.component.ts @@ -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 { + } + +} diff --git a/UI/Web/src/app/shared/shared.module.ts b/UI/Web/src/app/shared/shared.module.ts index 09b4bb0fd..8557ae619 100644 --- a/UI/Web/src/app/shared/shared.module.ts +++ b/UI/Web/src/app/shared/shared.module.ts @@ -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 { } diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json new file mode 100644 index 000000000..04bd8955a --- /dev/null +++ b/UI/Web/src/assets/langs/en.json @@ -0,0 +1,3 @@ +{ + "login": "Test" +} \ No newline at end of file diff --git a/openapi.json b/openapi.json index 4498169f7..9e1c431ec 100644 --- a/openapi.json +++ b/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