From de651215f540f8dd25512e0575b5a35902730108 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Sun, 23 Mar 2025 17:06:20 -0500 Subject: [PATCH] A ton of random bugs and polish (#3668) --- .editorconfig | 5 + API.Tests/AbstractDbTest.cs | 4 - API.Tests/Converters/CronConverterTests.cs | 1 - .../ParserInfoListExtensionsTests.cs | 1 - .../Extensions/QueryableExtensionsTests.cs | 4 +- .../Extensions/VolumeListExtensionsTests.cs | 1 - API.Tests/Helpers/CacheHelperTests.cs | 2 - API.Tests/Helpers/ParserInfoHelperTests.cs | 3 - API.Tests/Helpers/PersonHelperTests.cs | 12 +- API.Tests/Helpers/ScannerHelper.cs | 1 + API.Tests/Helpers/SeriesHelperTests.cs | 1 - API.Tests/Helpers/StringHelperTests.cs | 3 +- API.Tests/Parsers/BookParserTests.cs | 1 - API.Tests/Parsing/MangaParsingTests.cs | 12 +- API.Tests/Parsing/ParsingTests.cs | 1 - .../CollectionTagRepositoryTests.cs | 1 - API.Tests/Repository/SeriesRepositoryTests.cs | 1 - API.Tests/Services/ArchiveServiceTests.cs | 1 - API.Tests/Services/BackupServiceTests.cs | 5 +- API.Tests/Services/BookServiceTests.cs | 24 +- API.Tests/Services/BookmarkServiceTests.cs | 3 - API.Tests/Services/CacheServiceTests.cs | 4 +- API.Tests/Services/CleanupServiceTests.cs | 3 - .../Services/CollectionTagServiceTests.cs | 1 - .../Services/ExternalMetadataServiceTests.cs | 9 +- API.Tests/Services/ImageServiceTests.cs | 11 +- API.Tests/Services/ParseScannedFilesTests.cs | 16 +- API.Tests/Services/ProcessSeriesTests.cs | 17 +- API.Tests/Services/ReaderServiceTests.cs | 5 - API.Tests/Services/ReadingListServiceTests.cs | 4 - API.Tests/Services/ScannerServiceTests.cs | 18 - API.Tests/Services/SeriesServiceTests.cs | 23 +- API.Tests/Services/TachiyomiServiceTests.cs | 5 +- .../BookService/Rollo at Work SP01.pdf | Bin 0 -> 85745 bytes .../Services/VersionUpdaterServiceTests.cs | 5 - API.Tests/Services/WordCountAnalysisTests.cs | 3 - API/API.csproj | 4 +- API/Controllers/ChapterController.cs | 1 + API/Controllers/LibraryController.cs | 21 -- API/Controllers/ReadingListController.cs | 5 +- API/Controllers/ScrobblingController.cs | 2 +- API/DTOs/Account/UpdateUserDto.cs | 1 + API/DTOs/ChapterDto.cs | 5 +- API/DTOs/Collection/MalStackDto.cs | 1 + API/DTOs/ColorScape.cs | 1 + .../ExternalMetadataIdsDto.cs | 1 + .../ExternalMetadata/MatchSeriesRequestDto.cs | 1 + .../KavitaPlus/License/EncryptLicenseDto.cs | 1 + .../KavitaPlus/License/UpdateLicenseDto.cs | 1 + .../KavitaPlus/Metadata/SeriesCharacter.cs | 1 + API/DTOs/MangaFileDto.cs | 1 + API/DTOs/Person/PersonDto.cs | 9 +- API/DTOs/Person/UpdatePersonDto.cs | 1 + API/DTOs/Reader/CreatePersonalToCDto.cs | 1 + API/DTOs/Scrobbling/MediaRecommendationDto.cs | 1 + API/DTOs/Scrobbling/PlusSeriesDto.cs | 1 + API/DTOs/Scrobbling/ScrobbleEventDto.cs | 1 + API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs | 1 + API/DTOs/SeriesDto.cs | 4 +- API/DTOs/Settings/ServerSettingDTO.cs | 1 + API/DTOs/SideNav/SideNavStreamDto.cs | 1 + API/DTOs/StandaloneChapterDto.cs | 1 + API/DTOs/Statistics/ReadHistoryEvent.cs | 1 + API/DTOs/Statistics/UserReadStatistics.cs | 1 + API/DTOs/Stats/ServerInfoSlimDto.cs | 1 + API/DTOs/TachiyomiChapterDto.cs | 1 + API/DTOs/UpdateSeriesDto.cs | 1 + API/DTOs/UserPreferencesDto.cs | 1 + API/DTOs/VolumeDto.cs | 4 +- API/Data/DataContext.cs | 1 + .../Repositories/AppUserProgressRepository.cs | 3 +- API/Data/Repositories/CoverDbRepository.cs | 1 + API/Data/Repositories/GenreRepository.cs | 1 + API/Data/Repositories/MediaErrorRepository.cs | 1 + API/Data/Repositories/PersonRepository.cs | 2 + .../Repositories/ReadingListRepository.cs | 1 + .../Repositories/ScrobbleEventRepository.cs | 1 + API/Data/Repositories/SeriesRepository.cs | 1 + API/Data/Repositories/SettingsRepository.cs | 1 + API/Data/Repositories/SiteThemeRepository.cs | 1 + API/Data/Repositories/TagRepository.cs | 1 + API/Data/Repositories/UserRepository.cs | 1 + API/Data/Repositories/VolumeRepository.cs | 1 + API/Entities/Chapter.cs | 1 + API/Entities/Enums/LibraryType.cs | 6 +- API/Entities/Metadata/SeriesMetadata.cs | 1 + API/Entities/Person/ChapterPeople.cs | 2 +- API/Entities/Person/Person.cs | 5 +- API/Entities/Person/SeriesMetadataPeople.cs | 3 +- API/Extensions/FlurlExtensions.cs | 1 + .../Filtering/SearchQueryableExtensions.cs | 2 +- .../RestrictByAgeExtensions.cs | 1 + API/Helpers/AutoMapperProfiles.cs | 1 + API/Helpers/Builders/ChapterBuilder.cs | 1 + API/Helpers/Builders/PersonBuilder.cs | 1 + API/Helpers/Builders/SeriesMetadataBuilder.cs | 1 + API/Helpers/PersonHelper.cs | 4 +- API/Program.cs | 3 - API/Services/BookService.cs | 17 +- API/Services/SeriesService.cs | 15 +- API/Services/TaskScheduler.cs | 6 - API/Services/Tasks/Metadata/CoverDbService.cs | 1 + API/Services/Tasks/Scanner/Parser/Parser.cs | 3 +- API/Services/Tasks/Scanner/ProcessSeries.cs | 1 + API/Services/Tasks/SiteThemeService.cs | 3 +- API/Services/Tasks/VersionUpdaterService.cs | 2 +- API/Startup.cs | 5 +- Kavita.Common/Configuration.cs | 9 +- Kavita.Common/Helpers/CronHelper.cs | 2 +- Kavita.Common/Helpers/FlurlConfiguration.cs | 2 + TestData | 1 - UI/Web/src/app/_helpers/browser.ts | 62 ++++ UI/Web/src/app/_models/library/library.ts | 3 +- UI/Web/src/app/_pipes/library-type.pipe.ts | 2 +- UI/Web/src/app/_services/reader.service.ts | 9 +- UI/Web/src/app/_services/server.service.ts | 8 - .../details-tab/details-tab.component.html | 4 +- .../edit-chapter-modal.component.ts | 14 +- .../manage-email-settings.component.html | 2 +- .../manage-system.component.html | 121 +++---- .../manage-system/manage-system.component.ts | 6 +- .../manage-tasks-settings.component.html | 332 +++++++++--------- .../manage-tasks-settings.component.ts | 7 - .../manage-user-tokens.component.html | 85 +++-- .../edit-series-modal.component.ts | 62 ++-- .../import-mal-collection.component.html | 6 +- .../canvas-renderer.component.ts | 28 +- .../metadata-filter.component.html | 102 +++--- .../metadata-filter.component.ts | 67 ++-- .../series-detail.component.html | 2 +- .../setting-item/setting-item.component.html | 4 +- .../setting-item/setting-item.component.scss | 28 +- .../setting-item/setting-item.component.ts | 5 +- .../setting-switch.component.html | 8 +- .../setting-switch.component.ts | 32 +- .../setting-title.component.html | 16 +- .../setting-title.component.scss | 4 + .../setting-title/setting-title.component.ts | 4 + .../badge-expander.component.ts | 14 + .../library-settings-modal.component.html | 8 +- .../library-settings-modal.component.ts | 15 +- .../server-stats/server-stats.component.html | 206 ++++++----- .../api-key/api-key.component.html | 16 +- UI/Web/src/assets/langs/en.json | 11 +- 144 files changed, 852 insertions(+), 848 deletions(-) create mode 100644 API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf delete mode 160000 TestData create mode 100644 UI/Web/src/app/_helpers/browser.ts diff --git a/.editorconfig b/.editorconfig index c24677846..c82009e40 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,7 @@ # Editor configuration, see https://editorconfig.org root = true + [*] charset = utf-8 indent_style = space @@ -22,3 +23,7 @@ indent_size = 2 [*.csproj] indent_size = 2 + +[*.cs] +# Disable SonarLint warning S1075 (Don't use hardcoded url) +dotnet_diagnostic.S1075.severity = none diff --git a/API.Tests/AbstractDbTest.cs b/API.Tests/AbstractDbTest.cs index ade0cceab..77f978e7f 100644 --- a/API.Tests/AbstractDbTest.cs +++ b/API.Tests/AbstractDbTest.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; using System.Data.Common; -using System.IO; -using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -13,7 +10,6 @@ using API.Helpers.Builders; using API.Services; using AutoMapper; using Hangfire; -using Microsoft.AspNetCore.Identity; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/API.Tests/Converters/CronConverterTests.cs b/API.Tests/Converters/CronConverterTests.cs index 4e214e8f1..5568c89d0 100644 --- a/API.Tests/Converters/CronConverterTests.cs +++ b/API.Tests/Converters/CronConverterTests.cs @@ -1,5 +1,4 @@ using API.Helpers.Converters; -using Hangfire; using Xunit; namespace API.Tests.Converters; diff --git a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs b/API.Tests/Extensions/ParserInfoListExtensionsTests.cs index 325b19c5d..227dd2b32 100644 --- a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs +++ b/API.Tests/Extensions/ParserInfoListExtensionsTests.cs @@ -7,7 +7,6 @@ using API.Extensions; using API.Helpers.Builders; using API.Services; using API.Services.Tasks.Scanner.Parser; -using API.Tests.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/API.Tests/Extensions/QueryableExtensionsTests.cs index 4ea9a5a4b..866e0202c 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/API.Tests/Extensions/QueryableExtensionsTests.cs @@ -1,11 +1,9 @@ using System.Collections.Generic; using System.Linq; -using API.Data; using API.Data.Misc; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; +using API.Entities.Person; using API.Extensions.QueryExtensions; using API.Helpers.Builders; using Xunit; diff --git a/API.Tests/Extensions/VolumeListExtensionsTests.cs b/API.Tests/Extensions/VolumeListExtensionsTests.cs index b8b734c51..bbb8f215c 100644 --- a/API.Tests/Extensions/VolumeListExtensionsTests.cs +++ b/API.Tests/Extensions/VolumeListExtensionsTests.cs @@ -3,7 +3,6 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers.Builders; -using API.Tests.Helpers; using Xunit; namespace API.Tests.Extensions; diff --git a/API.Tests/Helpers/CacheHelperTests.cs b/API.Tests/Helpers/CacheHelperTests.cs index 93dae98d8..3962ba2df 100644 --- a/API.Tests/Helpers/CacheHelperTests.cs +++ b/API.Tests/Helpers/CacheHelperTests.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.IO; using System.IO.Abstractions.TestingHelpers; -using System.Threading; -using API.Entities; using API.Entities.Enums; using API.Helpers; using API.Helpers.Builders; diff --git a/API.Tests/Helpers/ParserInfoHelperTests.cs b/API.Tests/Helpers/ParserInfoHelperTests.cs index 70ce3aa69..0bb7efb9b 100644 --- a/API.Tests/Helpers/ParserInfoHelperTests.cs +++ b/API.Tests/Helpers/ParserInfoHelperTests.cs @@ -1,8 +1,5 @@ using System.Collections.Generic; -using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services.Tasks.Scanner; diff --git a/API.Tests/Helpers/PersonHelperTests.cs b/API.Tests/Helpers/PersonHelperTests.cs index a25af7a07..1a38ccdac 100644 --- a/API.Tests/Helpers/PersonHelperTests.cs +++ b/API.Tests/Helpers/PersonHelperTests.cs @@ -1,15 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; -using Xunit; namespace API.Tests.Helpers; diff --git a/API.Tests/Helpers/ScannerHelper.cs b/API.Tests/Helpers/ScannerHelper.cs index 6abe5b01b..653efebb1 100644 --- a/API.Tests/Helpers/ScannerHelper.cs +++ b/API.Tests/Helpers/ScannerHelper.cs @@ -26,6 +26,7 @@ using NSubstitute; using Xunit.Abstractions; namespace API.Tests.Helpers; +#nullable enable public class ScannerHelper { diff --git a/API.Tests/Helpers/SeriesHelperTests.cs b/API.Tests/Helpers/SeriesHelperTests.cs index a5b5a063b..22b4a3cd1 100644 --- a/API.Tests/Helpers/SeriesHelperTests.cs +++ b/API.Tests/Helpers/SeriesHelperTests.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using API.Data; using API.Entities; using API.Entities.Enums; using API.Extensions; diff --git a/API.Tests/Helpers/StringHelperTests.cs b/API.Tests/Helpers/StringHelperTests.cs index 6ae079c3e..1eac0a6ed 100644 --- a/API.Tests/Helpers/StringHelperTests.cs +++ b/API.Tests/Helpers/StringHelperTests.cs @@ -1,5 +1,4 @@ -using System; -using API.Helpers; +using API.Helpers; using Xunit; namespace API.Tests.Helpers; diff --git a/API.Tests/Parsers/BookParserTests.cs b/API.Tests/Parsers/BookParserTests.cs index 6be0fe386..90147ac6b 100644 --- a/API.Tests/Parsers/BookParserTests.cs +++ b/API.Tests/Parsers/BookParserTests.cs @@ -1,5 +1,4 @@ using System.IO.Abstractions.TestingHelpers; -using API.Data.Metadata; using API.Entities.Enums; using API.Services; using API.Services.Tasks.Scanner.Parser; diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/API.Tests/Parsing/MangaParsingTests.cs index a975cc7ee..82d9e51e7 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/API.Tests/Parsing/MangaParsingTests.cs @@ -1,18 +1,10 @@ using API.Entities.Enums; using Xunit; -using Xunit.Abstractions; namespace API.Tests.Parsing; public class MangaParsingTests { - private readonly ITestOutputHelper _testOutputHelper; - - public MangaParsingTests(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } - [Theory] [InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")] [InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "1")] @@ -84,6 +76,7 @@ public class MangaParsingTests [InlineData("Accel World Chapter 001 Volume 002", "2")] [InlineData("Accel World Volume 2", "2")] [InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")] + [InlineData("Zom 100 - Bucket List of the Dead v01", "1")] public void ParseVolumeTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Manga)); @@ -212,6 +205,8 @@ public class MangaParsingTests [InlineData("不安の種\uff0b - 01", "不安の種\uff0b")] [InlineData("Giant Ojou-sama - Ch. 33.5 - Volume 04 Bonus Chapter", "Giant Ojou-sama")] [InlineData("[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE", "")] + [InlineData("Monster #8 Ch. 001", "Monster #8")] + [InlineData("Zom 100 - Bucket List of the Dead v01", "Zom 100 - Bucket List of the Dead")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga)); @@ -304,6 +299,7 @@ public class MangaParsingTests [InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")] [InlineData("Max Level Returner ตอนที่ 5", "5")] [InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")] + [InlineData("Monster #8 Ch. 001", "1")] public void ParseChaptersTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename, LibraryType.Manga)); diff --git a/API.Tests/Parsing/ParsingTests.cs b/API.Tests/Parsing/ParsingTests.cs index 85ea1a858..7d5da4f9c 100644 --- a/API.Tests/Parsing/ParsingTests.cs +++ b/API.Tests/Parsing/ParsingTests.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.Linq; -using System.Runtime.InteropServices; using Xunit; using static API.Services.Tasks.Scanner.Parser.Parser; diff --git a/API.Tests/Repository/CollectionTagRepositoryTests.cs b/API.Tests/Repository/CollectionTagRepositoryTests.cs index 6abf3f7e7..5318260be 100644 --- a/API.Tests/Repository/CollectionTagRepositoryTests.cs +++ b/API.Tests/Repository/CollectionTagRepositoryTests.cs @@ -15,7 +15,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; namespace API.Tests.Repository; diff --git a/API.Tests/Repository/SeriesRepositoryTests.cs b/API.Tests/Repository/SeriesRepositoryTests.cs index 73ed58a5a..5705e1bc0 100644 --- a/API.Tests/Repository/SeriesRepositoryTests.cs +++ b/API.Tests/Repository/SeriesRepositoryTests.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using API.Data; using API.Entities; using API.Entities.Enums; -using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 260676843..8cf93df37 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -7,7 +7,6 @@ using System.Linq; using API.Archive; using API.Entities.Enums; using API.Services; -using EasyCaching.Core; using Microsoft.Extensions.Logging; using NetVips; using NSubstitute; diff --git a/API.Tests/Services/BackupServiceTests.cs b/API.Tests/Services/BackupServiceTests.cs index 4a34ec3d3..aac5724f7 100644 --- a/API.Tests/Services/BackupServiceTests.cs +++ b/API.Tests/Services/BackupServiceTests.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using System.Data.Common; -using System.IO; +using System.Data.Common; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; -using API.Entities; using API.Entities.Enums; using API.Helpers.Builders; using API.Services; diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index de87b9b6a..af5805cc5 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/API.Tests/Services/BookServiceTests.cs @@ -1,7 +1,6 @@ using System.IO; using System.IO.Abstractions; using API.Services; -using EasyCaching.Core; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -92,18 +91,17 @@ public class BookServiceTests Assert.Equal("Georges Bizet \\(1838-1875\\)", comicInfo.Writer); } - // TODO: Get the file from microtherion - // [Fact] - // public void ShouldUsePdfInfoDict() - // { - // var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Books/PDFs"); - // var document = Path.Join(testDirectory, "Rollo at Work SP01.pdf"); - // var comicInfo = _bookService.GetComicInfo(document); - // Assert.NotNull(comicInfo); - // Assert.Equal("Rollo at Work", comicInfo.Title); - // Assert.Equal("Jacob Abbott", comicInfo.Writer); - // Assert.Equal(2008, comicInfo.Year); - // } + //[Fact] + public void ShouldUsePdfInfoDict() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Books/PDFs"); + var document = Path.Join(testDirectory, "Rollo at Work SP01.pdf"); + var comicInfo = _bookService.GetComicInfo(document); + Assert.NotNull(comicInfo); + Assert.Equal("Rollo at Work", comicInfo.Title); + Assert.Equal("Jacob Abbott", comicInfo.Writer); + Assert.Equal(2008, comicInfo.Year); + } [Fact] public void ShouldHandleIndirectPdfObjects() diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs index 9c7d87737..596fbbc4d 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -9,12 +9,9 @@ using API.Data.Repositories; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; -using API.SignalR; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 72b3015b8..5c1752cd8 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -1,12 +1,10 @@ -using System.Collections.Generic; -using System.Data.Common; +using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Metadata; -using API.Entities; using API.Entities.Enums; using API.Helpers.Builders; using API.Services; diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index d355378a7..0f1e9e9da 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -1,16 +1,13 @@ using System; using System.Collections.Generic; using System.IO; -using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; -using API.Data; using API.Data.Repositories; using API.DTOs.Filtering; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; using API.Extensions; using API.Helpers; using API.Helpers.Builders; diff --git a/API.Tests/Services/CollectionTagServiceTests.cs b/API.Tests/Services/CollectionTagServiceTests.cs index f2fe14a81..14ce131d8 100644 --- a/API.Tests/Services/CollectionTagServiceTests.cs +++ b/API.Tests/Services/CollectionTagServiceTests.cs @@ -13,7 +13,6 @@ using API.Services; using API.Services.Plus; using API.SignalR; using Kavita.Common; -using Microsoft.EntityFrameworkCore; using NSubstitute; using Xunit; diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/API.Tests/Services/ExternalMetadataServiceTests.cs index 27e40c3e9..127bceb7a 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/API.Tests/Services/ExternalMetadataServiceTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using API.Constants; @@ -12,6 +11,7 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Entities.MetadataMatching; +using API.Entities.Person; using API.Helpers.Builders; using API.Services.Plus; using API.Services.Tasks.Metadata; @@ -21,8 +21,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -using Xunit.Abstractions; -using YamlDotNet.Serialization; namespace API.Tests.Services; @@ -31,17 +29,14 @@ namespace API.Tests.Services; /// public class ExternalMetadataServiceTests : AbstractDbTest { - private readonly ITestOutputHelper _testOutputHelper; private readonly ExternalMetadataService _externalMetadataService; private readonly Dictionary _genreLookup = new Dictionary(); private readonly Dictionary _tagLookup = new Dictionary(); private readonly Dictionary _personLookup = new Dictionary(); - public ExternalMetadataServiceTests(ITestOutputHelper testOutputHelper) + public ExternalMetadataServiceTests() { - _testOutputHelper = testOutputHelper; - // Set up Hangfire to use in-memory storage for testing GlobalConfiguration.Configuration.UseInMemoryStorage(); diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index ac3c3157f..a1073a55b 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -1,14 +1,9 @@ -using System.Drawing; -using System.IO; -using System.IO.Abstractions; +using System.IO; using System.Linq; using System.Text; using API.Entities.Enums; using API.Services; -using EasyCaching.Core; -using Microsoft.Extensions.Logging; using NetVips; -using NSubstitute; using Xunit; using Image = NetVips.Image; @@ -28,6 +23,7 @@ public class ImageServiceTests public void GenerateBaseline() { GenerateFiles(BaselinePattern); + Assert.True(true); } /// @@ -38,6 +34,7 @@ public class ImageServiceTests { GenerateFiles(OutputPattern); GenerateHtmlFile(); + Assert.True(true); } private void GenerateFiles(string outputExtension) @@ -159,7 +156,7 @@ public class ImageServiceTests // Step 4: Generate HTML file GenerateHtmlFileForColorScape(); - + Assert.True(true); } private static void GenerateColorImage(string hexColor, string outputPath) diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs index f34711305..f81ebd3c4 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/API.Tests/Services/ParseScannedFilesTests.cs @@ -1,29 +1,19 @@ using System; using System.Collections.Generic; -using System.Data.Common; using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using API.Data; using API.Data.Metadata; using API.Data.Repositories; -using API.Entities; using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; using API.Services; using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner.Parser; using API.SignalR; using API.Tests.Helpers; -using AutoMapper; using Hangfire; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -391,7 +381,7 @@ public class ParseScannedFilesTests : AbstractDbTest var executionerAndHerWayOfLife = postLib.Series.First(x => x.Name == "The Executioner and Her Way of Life"); Assert.Equal(2, executionerAndHerWayOfLife.Volumes.Count); - Thread.Sleep(1100); // Ensure at least one second has passed since library scan + await Task.Delay(1100); // Ensure at least one second has passed since library scan // Add a new chapter to a volume of the series, and scan. Validate that only, and all directories of this // series are marked as HasChanged @@ -440,7 +430,7 @@ public class ParseScannedFilesTests : AbstractDbTest var frieren = postLib.Series.First(x => x.Name == "Frieren - Beyond Journey's End"); Assert.Equal(2, frieren.Volumes.Count); - Thread.Sleep(1100); // Ensure at least one second has passed since library scan + await Task.Delay(1100); // Ensure at least one second has passed since library scan // Add a volume to a series, and scan. Ensure only this series is marked as HasChanged var executionerCopyDir = Path.Join(Path.Join(testDirectoryPath, "YenPress"), "The Executioner and Her Way of Life"); @@ -483,7 +473,7 @@ public class ParseScannedFilesTests : AbstractDbTest // Needs to be actual time as the write time is now, so if we set LastFolderChecked in the past // it'll always a scan as it was changed since the last scan. - Thread.Sleep(1100); // Ensure at least one second has passed since library scan + await Task.Delay(1100); // Ensure at least one second has passed since library scan var res = await psf.ScanFiles(testDirectoryPath, true, await _unitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); diff --git a/API.Tests/Services/ProcessSeriesTests.cs b/API.Tests/Services/ProcessSeriesTests.cs index 0fbe5db12..119e1bc10 100644 --- a/API.Tests/Services/ProcessSeriesTests.cs +++ b/API.Tests/Services/ProcessSeriesTests.cs @@ -1,19 +1,4 @@ -using System.IO; -using API.Data; -using API.Data.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; -using API.SignalR; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Services; +namespace API.Tests.Services; public class ProcessSeriesTests { diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 3dd929a4b..102ea3b81 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -1,25 +1,20 @@ using System.Collections.Generic; using System.Data.Common; -using System.Globalization; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; -using API.DTOs; using API.DTOs.Progress; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; using API.Services.Plus; -using API.Services.Tasks; using API.SignalR; -using API.Tests.Helpers; using AutoMapper; using Hangfire; using Hangfire.InMemory; diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/API.Tests/Services/ReadingListServiceTests.cs index 7157aa90f..7a6ed3e0b 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/API.Tests/Services/ReadingListServiceTests.cs @@ -11,15 +11,11 @@ using API.DTOs.ReadingLists; using API.DTOs.ReadingLists.CBL; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; using API.Services.Plus; -using API.Services.Tasks; using API.SignalR; -using API.Tests.Helpers; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index aea254e51..5addc767d 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -1,34 +1,16 @@ using System; using System.Collections.Generic; using System.IO; -using System.IO.Abstractions; -using System.IO.Compression; using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading; using System.Threading.Tasks; -using System.Xml; -using System.Xml.Serialization; -using API.Data; using API.Data.Metadata; using API.Data.Repositories; using API.Entities; using API.Entities.Enums; using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Tasks; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using API.Tests.Helpers; using Hangfire; -using Microsoft.Extensions.Logging; -using NSubstitute; using Xunit; using Xunit.Abstractions; diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 66f89713d..11ad9fa1b 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -12,6 +12,7 @@ using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; using API.Extensions; using API.Helpers.Builders; using API.Services; @@ -809,6 +810,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.Genres.Select(g1 => g1.Title).All(g2 => g2 == "New Genre".SentenceCase())); Assert.False(series.Metadata.GenresLocked); // GenreLocked is false unless the UI Explicitly says it should be locked @@ -847,6 +849,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person")); Assert.False(series.Metadata.PublisherLocked); // PublisherLocked is false unless the UI Explicitly says it should be locked @@ -887,6 +890,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person")); Assert.True(series.Metadata.PublisherLocked); @@ -976,6 +980,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.False(series.Metadata.People.Any()); } @@ -1010,6 +1015,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.Genres.Select(g => g.Title).All(g => g == "Existing Genre".SentenceCase())); Assert.True(series.Metadata.GenresLocked); @@ -1039,6 +1045,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Equal(0, series.Metadata.ReleaseYear); Assert.False(series.Metadata.ReleaseYearLocked); @@ -1071,6 +1078,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); Assert.False(series.Metadata.GenresLocked); // Ensure the lock is not activated unless specified. @@ -1104,6 +1112,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.DoesNotContain("Existing Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); @@ -1137,6 +1146,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Empty(series.Metadata.Genres); } @@ -1168,6 +1178,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); } @@ -1200,6 +1211,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.DoesNotContain("Existing Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); @@ -1233,6 +1245,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Empty(series.Metadata.Tags); } @@ -1363,7 +1376,7 @@ public class SeriesServiceTests : AbstractDbTest #endregion - #region SeriesRelation + #region Series Relation [Fact] public async Task UpdateRelatedSeries_ShouldAddAllRelations() { @@ -1431,6 +1444,7 @@ public class SeriesServiceTests : AbstractDbTest addRelationDto.Sequels.Add(2); await _seriesService.UpdateRelatedSeries(addRelationDto); Assert.NotNull(series1); + Assert.NotNull(series2); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); Assert.Equal(1, series2.Relations.Single(s => s.TargetSeriesId == 1).TargetSeriesId); } @@ -1473,8 +1487,9 @@ public class SeriesServiceTests : AbstractDbTest // Remove relations var removeRelationDto = CreateRelationsDto(series1); await _seriesService.UpdateRelatedSeries(removeRelationDto); - Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 1)); - Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 2)); + Assert.NotNull(series1); + Assert.DoesNotContain(series1.Relations, s => s.TargetSeriesId == 1); + Assert.DoesNotContain(series1.Relations, s => s.TargetSeriesId == 2); } @@ -1507,6 +1522,8 @@ public class SeriesServiceTests : AbstractDbTest var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); await _seriesService.UpdateRelatedSeries(addRelationDto); + + Assert.NotNull(series1); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); _context.Series.Remove(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); diff --git a/API.Tests/Services/TachiyomiServiceTests.cs b/API.Tests/Services/TachiyomiServiceTests.cs index 1e5127865..17e26139c 100644 --- a/API.Tests/Services/TachiyomiServiceTests.cs +++ b/API.Tests/Services/TachiyomiServiceTests.cs @@ -1,7 +1,5 @@ -using API.Extensions; -using API.Helpers.Builders; +using API.Helpers.Builders; using API.Services.Plus; -using API.Services.Tasks; namespace API.Tests.Services; using System.Collections.Generic; @@ -16,7 +14,6 @@ using API.Entities.Enums; using API.Helpers; using API.Services; using SignalR; -using Helpers; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf b/API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0e0ffa8c7a62022915189ea977924b674f118ae8 GIT binary patch literal 85745 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

aX&6qybpI!Tf$pph4V*k_VsEGbCKYS{ zkkUjY=r||_>!%yR=u_dUKEHI-X_AF(=|4DKYhtn)Lxx$$?Viyi2h1p$t#0Jytwae5E%SN%x&zqM^O(y1Q&ylG={d!+iN zp;AQaacSD&38%HBhkVpf+YYZNV%>wGubnN7@2+VCO)%?8)qrwZN81WosSH~i7YO#%nyK#VRiWf<<_2FJb z%v;jWg6QkdHqN`!DxKY|{}^6X{X{=N$N|z|^x1z$8k8`FQi!SP4}xfGzRlZ6I@0}9 zk<~YjJak6M;;3f65sg9nVX6mH1CQ&6C?;EpQiGPjT~QrH-Cq2YF~dsJQi*he!3CcK zKZyG1?lo{8y!ivC=PoIN8aSH`G(x0w*t+L{)1|L;fZ=z{3@jNB zXu9bgbn;LBf2>?44AL=opNk0G@c<4$hI^j*d5r$Dy`zVeSZ#wHitOSNzmQu+^x1lp zlKM8`Bt^PPlpb|%3O}wcb(ASS9gZ}?LhQZJQP1#Jw6r2{u#6m+TNSYbePUG z<2bZP3t-j5+V($Tobgk?KdzX(&31p{jaDx|jYT!BR@{eH+$prp!99$h^Uz20HZPcA z>1(bav&~)zp1-VyYy*1XG&;~gI(H%`VcxsnrsB_TaOh+4D9~;E@U6D!>A!m(StxVN z2;lQc5Yr&#xK}M3XpOa{gKqNIeoB3I&~~)AhvAes4_*(5x8J~WBTrKw_)2bR2DZ8o z34%b_IO|sZ2|q12Uck>iBpsTzmpkBBs1B@Cem!lLS+fOGn((@NUjBK#Y~E(4C%y83 zl0yMlP?-%lblQVBizk@xB*jB_Xp}xiIeA}5xD6mJM^Y0Y_l!W)YEY~CY|F6&UlvKt zBfv?5t6>T@ai6jm@J6P8wU|WuDEoT!p|)EYrx)$}?)cv{q~V5mfVI3cOh*tPgzC@s zo+coi@6)wdK~QExUmsUC-`z|P zokVXPTPVKLmPF~QnQ;eX>MN^$H;5S0<(C9z_$_Z~{Jm3duz9^Ed*zE=hgVWka_Rc2|9cXUzAQ64 zKNtjLk(mF2UF=9O;o{w(&3Su_N_lv?w#ermnhak!%GnF19!OT;?j?oG8M=itOoltr zC`rC?!pc)P@xzn>w(Y~)ONO<(bJR8NRUjl}PF?MEd)xJPDBu6jVDOH?8({U<2Co** z(|IQS6^TOe&KOW*QH%GiG-`8R?tP{CjnoWrd<+rXjk?OFE4O|u)(5l8-lqu)Y5>VN z!A;Gf=`nR=(u&kSl5jg;>tM_T6BPsWLQ-kEId)czbjdtXIDMz?^vs{}GjdLoe;hS} zR@v9zGb4u#Qn#44mzwduh*-p7F;e{J0CD#+sgrIOE6*3wq4es99gV>GaF<8AQ$awq#qCdzfM&wM1}5IUEquT zy+toSwcuw+w^La_vxCok2ZFe^-`tqJn*+k*4Z1!JL)+S6u$S^k{Yu+n}-g`#!^$*xie7e7ZYdsbICtH3J>_ zr%H+~vA$BH*$QN0{QSOp)dz|ury$+u=RgVF7uc%Gp(#p@ZO!R&OEzp*RPE>qK)$@e z5KTVq{0sn*S8pLZfrWr~XL4Vfu}8_+BB!&|3wuRU9iYUES_gpk1%Ti!od?_}m8RFC z1^`Svj~=N55#O)K?RV-T4z?l3k_`dN$T)3p8|`m{v@l4|lhLD6vY?IOc6LI}EweaH zPd+%}*9TxG4_)@_aMytUjk-)&I?^*OOTMGOQo)ZLE!qpn9QGJPU}@}V#_iEp(bAMV+`F6 z{8o(m;vfd~rXrkn_G?rjT~0g@dUez55U;^zfPCA4T}c=FM5DN51#qtoFgUtgx-`if zfNCXC!%vsmqUYp<(2*TtW3`jsY|=CF612S~)dT;s4oM8@hJ zYhVX&72bNIvNz7|jkFG;&6&RVoD<+W*M6^MCb_!6GH%2o<#&5M`gUAu5pnWvUVwI@JyL^dO@i<85s2L;0|x~|sK1`TJWLCGnj+ZNB=7|x?p zBWPzIGEi|@T-1Levag9+2y0X)2Vt}15@#}d|KR^n3=)J6w>t^h@oZ*Wvg(piP-JQ{ zv6tj;SK6813qFi9w_7&qrIC&YsW2|^8M)xh>2**;eMfm&Zlni{GMPCa`SQPg z(#PXo*q0}^H(S=OUqkqJ?mO%g&9R;g)@*=+>w6$)h@KY|M)Mvv?&a26&lwo_GL+id zcbd!$NSb6gOypNJECD&8m}P5+n?s978;yc+RSkmj2FryaWbAYXI%dpEGuqbNR8#z5 zq}q%nsEVc$mSoPJ>~<%?j;S zV9D7Mh+_3)3}pDQ!+j%&J+H9TCmN2`>2OX&M_mC2Lw}bSQ!b6XO{y?+_h$YjV~CMf zR=_7PuTL~E*M&&wBFLKYb<7C84gXIx!GxR`P6YO8jxh6G=!^e|R1;~*TwBzSbslXN zP9u;HT_}N!cIS)&ds(x2>K%|qHB<;sjDR2!yZ^r`;JWAQH6g)h)DT988kPnY%RZ8SV`FA7RNv@$EW_tc&0Wa2BXp_o{GF!-xbFUunF^$ z0TB?aCSX*2X6^Ms8T16es4R8&9K>OOHXwgO~yz#<}00VVbe zV#uMrA1J=DSV>)gcAr$8OZOM&upj}t_)xoSU&0YW_jhMGQW5B!O)DNpx{$9yaUh)a zkM>U2|DER-G{iEw-;kKM#jh;z24rWeS|dW(PSyp*YlE2&s9YfDu^>o1wjRaRf}{BX zP1rk~@(w_1D#vmWq?bMYahQk75=xAI0g+}3Acc)syC*?%E zGo-9{AJ4L~h#*on&DK(0Q}g|5H)A$UuE53w3H(Ee0N|h}W}>zf9Uz}yfQ**~AyGs* z7msT1G>B1$f^uPG&CSb3kp9sdCoEKm>#4OLYuLNkJ_Npn$T))xsSV#_RJAesgcCsq zHhe?cbvK;X5`XS`gqBL45fI*^|X*IFU&R9U~*8umkzUe6CqpHHla`1f8YT>bk zbSBz|*Pn{!^BTO#Ay{;@ugnKLQ?GqC^dM1xnsda2f5dxd#`UA?Pyf-5Oz>^B{h;5$_B3xY_RO+6(3mt90dupY_*-6fkE@wIsyyKd3=OuM# zkew{8=3FONn(URXVsVc0{TM(RMjrQG4kBJ|v1kC@_7HRHf+L;KZ}HQ*r&gX_5RS`Y z{SaR3d7?vjiU)!xUN&OP3j9B0*ZhywNC2IhovLZ;KZO(jRT?!@&X~lXdD-&fE6E_- z1<@yO$}rowSbKVT*gSNmZn;`IGRq4{2vC2e<>aBzzYj{v{&n=q6#;Qg8y7n-djUx~ zaO|FdxW2!ejexk3j-{iGm6w3Hp0}mf!I4|e5ADDSxPvulEDc5wziH!OXYVB-Eh{4+ ze#^zn#>36^(L*mAW-kwK&`{0wk*kNE+e0fG0ZHmbfT8uY9(p#0xVpn5Fb?>o4s_kd%GKJ2Sy`F+uUqniELQva>0@^`_@!eL{5lJh z<`rIjy7&3k+jF0utmCf+5I*0fy(OR|?GS5rmj3W(VO83*^gqv5-@AVO>W!nH4}aPU zsl|~ftCUWH|F6NoF1&Q?(epN~id^xGwCANoc5U+lmAZ)Kn`TMxy?f=RD#AE-`5!>O z(Pk9TN}r=3{jKhFUXJHrpD4<2mwOi9ZYKBu^4jci=;h^-OG{rq?63>IezA<^3Eqnj zGutN>&2m0;P02&Jqw84GNB6IjT7OlA`g7GzxlfijukMFm;`3O`^qUC5E@BHg3&uFP zhi8_&O>G_xx8v5okKSbH^E%(T~bvKJ&&sJYYeuN7N5DCN0Hz1LVGiP>m8ZY`_?vGM2RI0g-isJ7CFJqRB`$(u%egve|?yTwm-!qj~fGv!()CpIUX2^819 zvmbJ2!#Oo+2+spwVu!ZB5u>z-S2i~Vha#ti`0ebROEm}? zDC%Wu&D!>gGFx?~BV{RiJGEdwDi)mw3wRLcDSl}tvqf+;AS1XiBoy1Xvd3N}T_F~A zPkZ<*UF?GQ;+v%Am+EWE{cRQ^78gZSp)iMcOM7n#`TXk%zBmI$%9l&arTc415&R}a zgvQCepxOSBFpZG#mc0O9kKWd5yJkm^-EyJY4S$K#aW#$H;*cZ7@1`sXv3aVQ zXqw@x!7_c0Yup1>g=jP{<>MVRL(l$z|Kd_sMOO?O4f7Bd*qu>p4#g-_1Yiq3meamx z9n2Lk{1OYlxYM_9gwA6$OPIUJU$XHG`?0kuF@G^@cy3g4gI3g%LB1oh1gWx`Dx%KX zFKbaTeYtWHhh?BUF4lo58~0jtxxUe`-dp=VDZnV`w~Nbk2bl-ArzbW@KqEO*RNCdz zhmi6^OfVZO*{ZRa4frobn`2!^0%MaaY10OdHRNVQlPPs2YDoB)aXP8 zcjXOP8b9<`=<)@CoF5=qQnOHYu=iuI25>$Re(eZjrqZ8d-cUT^JC(0ZinMws8Zq2e1l^F)5b)GBz;Vsmc0t6*=9m$H7Te1^TXst=m)1x2}e z)b?vKiB>%EYmVNG_k+ig=8*2i=7zisop1ML zG-@oT7Z#e>2bCYiw#CvY2= zu1M)9M7J2$9f6N6$>k}ZMVelLf3y5mi_>1L_1^MJ#lJpRsd(Poy>0H~U3q~bN87#a zxLdey)_4z?heq(Ci~R<XRaZ#>|zQG%phOoz9+) zdz~Va;;-*Jf3@*t>Y8-u&6cg_D8?H}JcopQJ-?@BR$w$-zxsp+UR1Qz5z!^FyNV%u34#0D9|`#*xWB#wkf3$g?*o=nD)wId?)-LZ@||@#Nwlr52IxJ!J4ytQzr< z{}*4=GkG5h)MV?;rsJ^B#+}+16)asdCk$zzyT{N2<@+YEFHsZ1f+IU!Ov~RCYubLj z6LT3LWOYR_E5k%S+4^fvG*xr)9j_U7pG7(~UGB;qy43F$HNqmSG1V|odm2U_GL|TB z)In-{lG57S3FmszWoKNBzGZr8cCXg-uX8^U@_k-><=Z3sUz~>Q^JT8k-P^WBKtbqS zE!N9>-x>OFYB)4Ed2ndF_pJ+^ErYjt$&6O>q?}2V4+*m`Q(zr%v+;ZQAXHLoBT($o zi0{C~RVUmeVOT0;y|W@+IRNf?B!emK`K|=%hj_VNiv9^DhokdX6H1RuqZu}GZi&5i zJc;xkj&G*i(0kI^$&z=U$GB9oqDO4w`?ukm>Uvlc_wU2blWKP)ot z7W3P<32pjHl$U_|+ptwVvuXBSsT$o(li{jbNA<0_k2S`G@7AO5M%H>xnYn5DUD!W1 z8F!%@|^|ABXUZfD)%@w4~r(Ax>tB5H^BS)NK=;j ziw{E0N?ggmYx~RhUlGR_DtklMJFAug*E8#^d>ihT+j}@DWo2;ZXP$Y2x!hJB8|yD9 zJCS4O(L2B8$)UD(C?KP{MD(^K{+28TBD9OQBwo$fzf$PiO%W0JnTu`p4>gCCaM}Cd z>UbbZY1dhhBMGY+%`e%5yCR6cF(#RKAA|%H&f3*tZ{8?yoHfkx-PLk{oEtj zCLH&xm8*R8u2%J-Dt+xT zOnXY~4><};CK+zRW;zrX(k4Ru#-7di81qI*NKSemcP@+ZDtnVd=j62QFikA#c6r&w zj~)SAb7yN32)U!Xzpdc|vv0&7sjSSud~tPYZ+alpPQ>B~_2sz9W*|8?j7yW0@tI8s zTk@T0#uU@si{?Bg29|-#pkAfD3*mJ@Dda3C%@aH zYabx$M=Ml1zTUtqQx0S^)*UOD?O?Dsz*H~18~EAB`1nWeQd8Lh-7{_#B8iLA^_j1# zCpSz0L^^zbVI11FRGc=T#W?DAn?SoOYHw&D(NSwHh81o7W(mE-B|D6KQ!xGZT_65bZ_A&GoPwv&;Jzq>XWL{S5m(^?US+;;XG zy|%ie%+fx$6ttB}d{Qvyo|4#+a!2q3#<}YSBG@JNJ_O*Mom`{hUUDGw?&5ONV|=Ev zAo5wT57X%=@G5ZnxsZXf*&mK*tgOe&^h$n|`hV(XW}Fc&mz zWXdzoq<)Muu@C4&4m&;0Hh-jGBrTUSCP?Zz6S-@iG9D)6Ya{>L=>rCuZTGsmf5R9; zXJ<8RceVIVdgV{pAEiklX8u{mwZF?DHm{#_im_fYXFxt1F;w$;fO*a$#N`^tH)cw?VRe(&Dm|c@8xMOY!aRYYw#D}+u9oPuK4WF#)N5E{ z+_kPMmhJS3dHrZn{W;np)eaGC*n1z+ExIlilfwKk^;zJ=97HM0HuV4K0^U!aVzRF+R+^G9!o}mNTh7eWjlzHdx_v z40srWaS-#^K(qjd&~6Q@R@laL#k0ajK{}74=F)O?wh`5jKE`*{v&0%FU45X;wCI`8 z$}Hn4`}{sTsrYzDOtHHKkI^8ffWbIpN^Cj*!ui1bNR0FM^rFvmtFlU$80|)*T7_bR z>7PL2hIgJc_f1aQ;jcuyUlt(6G(4Ukcx$Ju&p+qFzQwB}Q+~>lyQWD$MS4SV$c5pE z{e~V!IT3H;>ti0kc!T_CY`{hl*_d~-`m%u-UYOWAC1;X{d3NSV=j55P3YNq(C2=O) zUowT{UTcGQCx|rpun4P99QJ0K()&(h9=p2HYBY7-ZlJ6~Y9^vqGDvGk>FA;~51E;} zThy2FLXF%>>N9-#Xw^@YQtxE=$wdCrL|)OuSQf1)BX-50V0hDIiSo~;#;^g#I1`ET z{(MbM7fj)haTR821zM>`DUO!7pQ(Te19vc+3f-U`vSmQxa@|~yg#3!Oz0hoR$xQp( zPbK*1qz6er{*t-dISre5I(YI+%(Dy)XaZ(RQ%rkP*xu=&~FZ=>{rBgeye*e3iCdPcCL^Fis)%b(~4kVnc>T*5v#Llk-Y3 zn3^>XmE_jFhbq6F#0L1XkT-bp5;-VRSX0V?di!GAPA7mF5xa#}=w$`NG(MR&l!3>W zc9=<-(Q~sY)_%nsgK_D+-@0#uYcj<{VuQiVOgs~{cS3aqDlzclvj$VmAuTa3yXc^h z%fFqpS=Y1)73c0VZ6Q~Eq&CxCC4xFUubf;&^R6;~jk0Y#mETEvx2&7c&iFP%L!&09 zEztyFAyD%i4Fs*|uz5o>PxuPd#_EaoI)nNGZ5>X&#a1fl%`p|<#V_PlPJ5jW-h26Y zYIi0j?zRNojsDEPp2BG>*tnuGO(mgPzs~^U;O*l!poUJ*eDfal2UJ&bNQoXe~GzzV7 zX9XHPF!M6vmuD<}PzSy6cY7Of5i3s%YAqj$bU#Ky%f|0b{jAYY|+!qM(pJ6!a==@{=4h&ol#e`}7ePO&q3 z%!Yd;-TF-X$Y6fe+lBfuJC840yi3;RC$IIq?Rlk;NN=ttw95;QX$?;0y@V1!v1rhw z8`R-M*}--3$_{KE>0;VVZz`tk$~R7G4CDX#-YVyZ-45D}?of+i*5oxM`*c4cpcCDM z;u9Cug5EGjp&#GQud`Iiw;`bQ%Tntpq;&b`Ghf3DJ;2PV)9ah0O^3dhxSvg({Nd8k zV@@#;b5ECREf~m|Y*(>2=eKwnjaAtia9=kbJ`C;r!*Gn<-q1BaQ$b0&w^?EJccYZA z5;h6*g(M&qTd8--t}ej@T*UY)^uui(;oZ+$ycS>Aws_xk7zGYgjnvumHE8DRUWs13 zamAYRDtV4{+9jMy6>JlY}31MxU>$i%PQ&XhaW zsHK!A0Y{yapZM5Z3mE3@{rc<&V7tM++DQ#C3US$b^i0}v#Ii&f9zJ(g!=?@wZ4tm` zKH--Zpgox|eu+9e94g{ZYc5y(F_d)bLBDk>C9FaDr-O*aIudH_F&=PRACoum1FH0M z60QF96G!X<{T8p(fba*C zW|O(>GeaD%y;6W6bk5Q=MCktkxN zFoRY0CVaLpiMJD(odImpsUD~zuV$d`$25DyZj(i%sC>H+oOk8^6 z{V+5{9VIcqc=deaK%G5VOp4r^-@w|oad-=PH?c6Xh^KRH_S02kxDorISUphc`lXX$ z(YSIY2Kp3TFL1nO?yc3=dqG-4zn#7b{|Y&y_cY?y(o1ybS}{-cK)}TYoYxp#Tg|&T zV|&9-!*-J89b-;en2xk9fyy zrtumVn_w8B!8Uyb*Oa}Nd~rkM!jf5W3o~Tgqq3hqHhGvY_3QW5QFk%k(@*9k!|4a( zKF&!dHRHMkV(!nTD3S|yCaZs2snDi_$&>%$)KPPer&~zeA2qfo>v0mXPCZ4AI62oy zB=ob1d!724o4HC$;Y z_jk3fg%%NGOdtg=3ykjf4U4Rln9c9;(MxRfZ5=$NH*WfPW*6iA3}`X8&|7-E>|C z_Ab6Q<+qIp>$$gv9-F4m)1|aG>eC=xc5YlhWgqth90m&J=*z?AbV8fEA0kj^Fj^>u zAg#f~HX8F{Em`;1nsGzWh2{dE-IGN+j~{jdG?eim}j~HKXBpPc~@$ty{0?DcyLed)%FJz*t+P~Ys{xBeOnCu zf2exzcq;$@f85BPDSNLY`w)^sB$?UU=^#!xwp3;|35AYvh{_)4$X;1l5y?6-%1#_) zcVzvZ=k@-4|M>meE!TOT$MqQZ@wl!N7|TRCR!L8)jaMRq?YmP>YUMF*0G^$d%-Xk9 zGqzDi>HzpI#86b670TJYtT_!~C=!GG)eh;MY!hc!f|%c<`@+ANcJ#GbwLT4&GSV!) zQwgFaM}~~M=AHr;V(hyhRxA(0-LjWUKH53^9dKg8D@#=a+Z9%o=8;aL?S4k?wlM^B zT6pDJ^@VN48JD#B0dk|Z?59x{elrUxO2LpBpW~*^XN_EYA@5Dv29etQ!W*|v0>3IDP<*XtGO<2w z=Cd44W@ei#mLUMgslv{#z|?P1X_e;GgW?~n@svM;?}=}iTs`HHxZysL*_ zr@bgL5rIQo<}#V~o@2srrp2qr%G#3N;QJ3!jnO6I;BVJbv=Me%(4V`HERXi9iN#-?NArRDRcNZ3VoK9# zkyoH2odL`~9nv8_#gD`e>5 z57?%U^SvY84yP1ng3`Vi9bVMNi$0W3(?|1lE(1dRsQQnOlBM65gfRBVwH^Z6C0MWT z0`DI@`eb@lQ0W1$bB+CSmCJ3;wqGyR_916~GZE*2Yk@XnAfrBYcUv*zNj&8ELfVT& z+-zSU^Fw1oL2lpVW#$kDsBMEN?(0h#t&pMmsLDapFA*cnIZ)dv=e*~V+f#70u?>IX z(6Y>$m5%KH@hs-QX-i$S8mWWsoAhAYR^(nhSK#EpaJwYa(8KpOUbgCMALEGU(Y)Vx}QON#7I8a~=Gah7IYjU+=iODcv!dI46_+8?ZZw z^rwd*I|enL1yjY@_9M>xO}VM_$j{FFbJ!5s$cMFv{Qj7H`3qy0wDBYSV^=T2m;5sO zCL2Xg>9!SJ{Xhgb^Fqx|m%s&z|JO0x#f}9s`KD2#5D>d|U&G@FdE1D6>dt*2IB6rf zhFv{H2x!_*uBw^7s#h3x6!(-vh9WW^{u63Luy##mmR+_xZO%f>*w(PDa6v)WuKSvM zWm2Y@M*jE}mR>`|)z7z@DRM0ooKUXGn&~|O?S8Qha&4)%Ez#S&!_}}PhmpVq@1Fcf zO@t|1$|n*4&D)X}9a4dd-mRfTNoLM|bD#C^B1uRd+dq724rDpzL{;`D-yXY|_QyPS zfvb65wH$DbQ6}%0e0>o>Fngws$!-980QAi;{0T;95v13$?S9eJ;dn(ZaXM~Trw=aU zyo&C+xce7YB$CHjZ8gD|iTBD$8bOS3OhL3eyXbuD>E@EMv9cITm z&{IxOdOM1!oc>m}%zr&>xg*m*e9ymtZlJ`oqgauO)NJ-xE0iC49FJfS2dVIM3d`Og ziHF)A>4C<1Q-b_yg27=#|By&psDs~{Ge6YUXng_s)8^8{8~t~b5SMx;KbZVWA9vtt z?^BqGKznL5`PddgGPJ3EAq~&FCcpKoHRcY?Lyp7uIqpU3I6$mEZ{!0G8AU+%EF|pA z0Pte_ElJMS@OJi4t6tF*<0J@^qC%R!&dZN};`sqoF13(n4cSoJQ=Mr@roBhd;F0L+ zzV}1G8nQO@2Tqh;Lv7i23F!7=hK~L=iu>V}S%ce(LvEkGtAACP;Uq$;_j=ghwI5~Z z{}g@TbIu&0O$!AaBTau@gNdc2UzC6ja;VJ>B0&)&PoiCZ0lowXa+1k2N5HW&iaRI4 z*9s)UmCEG=XS(8*gOh{Tt&I5x9TPC?{#J2-=BMr&dg*pv%GtQIfIMUg8TuE!EB>xn zZTsG{VAgqLj?2=uFOLi5LXtEbId(u^-LR{XzG4M!vhEkfm_34I0QlppA)*3d-&UM; z=ATD4I{Z8CarJ8_4;k7j&<3>!lpDu{Dm&+~91{2*U)Ll@m0qE2k{eE0;rJ1e>USRq z=*(P5D|e~&L(N`s_cJ<41^@c>Qi!3(FU;+%@XDi6@hZ>_^8P(+Uw!>~0m%jqW6s%o zELmieKLVOIYnFX3*WGE9F8;D&6Oa{8*zfV=vjSSFMFz>B1nj;rD^w&tdJzdcsqc9^ zNzVpETgWYHgu+MAJxfR>YwrZ|kR-YXwOo}B3n;lEt}KM;z>1;FW~aapv+vl%Ni(0S zK{;Euv3$mSl%-#3KPv}jLbN?k4AQVcc(assZBZT@B$fw{BcRQIu8Ac{Yq_n&$bq># zr+j4g(L}u={bVr{kz@+Wk#5o~wdn5i{)q>*v>GDT{e_Uf9OF^6dE>wjam(CjVM@da zXU*Lyg8Qn@0u@N?-rsLrnyVq2GNu2)KwbM_!Y2$so?KQ{MA`E&;CgPse+!kA;Ik) zMwK?&{<6ce8ihsOA~VJMcSrK zPC$202ad!DhmSNLbV!0`qnSVW&zB*41Fvu(KZJgZ2&}_i3?oyVEzYt(bqFF-MA~VT zuz#4Ez&rqwxxE4`IUB+RF2#A|q18Pmsex#PC-%s zntcjlA!m>mb(G4SIeVZfYBjt6IU96}WkVUL?Sl(xcHxPZ3QafLAdpkZm(CrNIoTwM zmM_O;^sk2`NeL87UTxyV*caJcnyj#_x5vNMYEsr?8W4q8*)v7r#WVXS(Do%%rx!Z+ z72=~oKhyI%>ncn8n^5gl2;`wn+MdY}Nbls}?T{gA;K|-=GN>u4v}s;V4YgF3j_QvY ztERr$(;&IGr}1j480kk5!azBuZgAs(pwKxv&VDj_%V2A5Fq`Ts7;}7}cP&IE;uPNv zjAv(O_HRo)Nx~fml!nscvXfFJ3hQWoqH=%2b!7NHF=9! zR|GwK7Z3?Vtz`~@W?PX;9t-rKoX=OgChj)Rh<#M}LARsGE#%?v<>8nX>RDk|WK)By z=wxVE=~r_NN{mt__hhCq!IEktfi`&k$qA!klbSd?PR!hZ223I#x3f<*8WQDp7u9&r zrBz(2I`3Eog)8o@UtnBIZmr~45VOySE;CJ^YmY!G@FrcEb=fqbuG0vGmz5f6biS0} z?QuksQ$x0+*mU~1_~fBUCeZ;g7Ynr3Wdf1Pe&&{mkk+n|=0nlII1R8Ruq=^h@A^^l zL}^huN*TbLuS-M}g`VxonpA`=@;c>M*INyPF-x@?BZu0A9YqeUPfN)47lNK7z39qP zA)*WjKv#6-^{a_pla25Ce|-G2Qt^k(Mzq0V+6C=`Ua<Z*Rm9ZGW+?DD_$s z7*TPAMDK0_`Uxrg>VK@kj;ZYv2Zqx5i^9ywC8Q5|buE)$b!b^XW=0B8yVbH0GB0uO za72T=ZSCT=qBRlRf=gZI#LV?M+aFv8AhCde)=j2|=NH=+*#J_933natk#zBM=mlL7 z9wpeM)x@l|2vWhg|4y3z5{7G zje41;m8%4?LyiUFwIVCU|Dn*ch#kcTO171>lt8mV_)YP??u<(d{Q&NiXX9I&+mHsv zUr{{NPu=wm#?l+>u(2fr~^!eJ8x~om+O`XV=2C(6ZNb+?OPZV^)}U`fr5{ zEyLsi^w&QrFT83yrS|1;>LHwTCsseKjjR60yq;-)p@s;Q-~7vD6ql%5_M>W}Pp&Zz z|L%UhwbX3|IH^(oKo=F!l=Euw7+?#W?|*X1+uB_;15nVs?`($x`p42E zxibbiq$V|L55mRr0nr* z7i4XeAWzpl2_2FjQk$cVcYbkbIlliHZlv*1@w!CX;}z*4IolerlhonN?^w%rGa|_N z!NLQFTFd31D~4O5eNhwsLUhz^2w>RtlH@DeXFVC4s7CW;A@cq_ud7X}Yx-NBZ|`Rw zJ0>4(-(|3cP)XB|T;mPjzw&9)SX;7}b2}{*sFp-O%YkUB-R8g)QwBf$*op?yi&y%l zUX@4)!Yno=3PTj0n0dNW9Bk29%O42 z0Xo>+?zWx&IX!y}2ywrHYNox~*SB8&&g6#<#?;yXc^|)HP4Es9A<~g|K=JNgR(ca? zp8a=zW9NpLy!U?s`pT!~|0o7CyhdL8x^J;Z&KnP_UiHBmAVq^(&6O1R>C-=4m8$P7 ziKunLU!L1<3}yO;63;)dyM`e;4$8m>!8NleG`*^~3E6mT`Rhr_nibHefApW*aFlVh z!<2s5@P++T^)FwRAF5xyG&4)-9Kp}N*))WP(KE|Fv&CI)vd@XQ#Bb1kV~Gt2i-94Am-Fhyp|;4qtqfiO9Jzft z;laK{GzhcO&hLNYAnQL&Xy~U(A=blV0&V;?``tau%vt@fz^5@dd=7Nrwpe*vq|P+r zpg*{0YJ53t1wH-&XfVf{|05RELZl&v_JQh`7=;|so7-Zz`Ko=N9;1AxxrIWgnQxfiqSlSi62=gVs*lpchStXJy%^UBht{NEwgK~W-0U^b@x z6r)Q@o`^g)_-)B1xz{6L^J)(ozs%kK`h}~X@I59om(~`+Gb6LvD%h0ooPOXn@_ilV z-c!<|se7SL(*D5{&n8s{6g~V36D@f|804Cai5mc9`6zS0z_j3r{}%gJAu+!g_6>rO zfdzh_?oDY?Wd-B#MVGZcfhlB#?;vf0|e7zN|xSvj6KLU({U`!__1KmLi*_r%neAd|yuD&6}Lk%DIU% z{4HPi0`hIDeuo)Z8|5@`|AM$?PKzAAfhNvj8)PFnYMUP0(3YE(IYrU8}AQHdqT-4!; zL0=3DD)ENM+ZJ!6--e#npK@q9ear>SfskwcW!CXvt}fXx`&v`7CiC;;{uNFPuNQ4RWA}C5P;meEY<_w`Jua zw5fyW(rSoYhUY6b*|+Sr3OUz?Q9-O6Y7aHEkphUrp()R_r;e{jQb5;CB0Qg%3zFIs z9%XpK;L%tcAyH6vJx$W7_UzUS>hwy!1(%j4kZziu3_Jh4ndu8l``3euQrr_7PBi9U zTX+>tKo2XCwDl^j{bUrL_Q^tIAq<@Fv^~0NWUhBpfJdjb5?fwsk6!v`33S%X|1k zlu;1@Z4qASn@*ZPp^C4&=laG8t+*nFlYe4;`b7GNznu$FDzi7*fv`ud9ToLGQ1}|I zE|K9pr>04?>>R6^(0oSw05Ek-0x-YNyPLUqpLbfNhnP!j|7+icSDW5Hb`gxfAixn$ zG$-cvsrNJ#YM3}N@s)w5CKn7}8hu?oA5>HoYWpQWkMtp4vO%m(Wd{6@7vm0o8IPFP zQrvJ-xz?wcK6!N^3oOUYLdzDx( zFo63miUB2r+34^H(Mm@^m-6inb-G@I^LzFe28HXfhsT4cSu3WD*LNxoZU>oUrtyZxDZ|t)rSwbmhCO&~N+HX`u|ZX~&`T>QmRiCM_^H>9-Yc zfJ+4|4k=v)kRg4i+x8b@`5w0LuFKbD5Kqt~09<~5hrk5}gmbm*^LgaJTfPgZ*^@X2 zKf0Lywyp#HMLjjTn?Gk(w$%Zg|KEyiRfROFhP4>`GuB?q05(J&l+CH{~$|<0jgSnjd1t0|5 z@0ZGJ4y-q11Cj^+K{DZ3bK7o>cb-mTU!udKFts zf5Kejen|ybLOKv|@{>EFTh{=XWnbFdPZu#~)B$#yxPT=NHXl@`?izTRfQb@Q7}~UF z{Nw*Lco0C@Z^R8lY+ub1k{G}#+Siq-66}Up*Wn3GXsuhe{aF50#Nm%eVs~8tr*fWq z@j14??e3#$rl>VK{-}QNFwP{|MYx&jwS;*j&)@c&UEEP?ld*zrMRkx2w{+Di4+;XS z>sASmX<^#b>Md;X2Ur#WGtgEhnxf(~kRzECzLeHXLTRBLuBu_H=DkU$obcm9`w#jh znVeznncmtB2M&I~H(vj#M?}!)r!09H--o3GDfUt2aHdqWJ*Z4HpRQ=eE55E8{_d5V z1_NQyS)s;E5~ru83tdHKeI0RN?5eIGX^p8Cs{>t1Z zeZv4Y5J!WUGwrdd!M>|MWpge;GsCRb4dWXQZ|s^0D5&za@Mn-8yoJj%?^{-&`fyPdL`zegkf-MQ4hg@OO8;KE1hKvj;gb0H>J zJ()B%JlQX%MLZdsxTkUW2y%@pYVFOUj7s0Lrr>j{tvGCO(cIzi$4gI_y?)ElZ$w4R zzMh2}ee(I%blzlpivPRHKi9Us_j2M1cJvTnu~YK%;)AvidhXZj zKW=|L{XycqIZ6vuvvgQ{_xXBtp4|qm&dUMiS$6y94?^F{5q z?mwE^i?KUlt&EzSh-=?7YQK~ocufU=_A?*+uE*h(mG82ByYD#m?aX>ACuP*y{7*+t z`M2hd*Ktg&lQR6R*7fZW(f8lQHkNzmAMD*^39hg@?~?R=`t{9XMa5Ch|J}!0FNy-N zW}cBAee2`5@T%*zy=oPdDtrxhl(ypC{hl}d=6o{sWSI3lbS|3jMD6g~5TW+CpHscq zMNI{Lp#L?cPweH{<9~$wnqt0*jGG@#_vTyZo;+)Ox_3N{gtOi+8!nUn`9$gJ$s>Co zueZMn{2h8l^1Az*tjbLJ?l`K(yVVEG$-emE^qrf%lH{{eJoo#%oh+=kMcJKO#0#bw z!=iDXbd_@#e>O_)eze*bZT~L$F1RiEMf>CVj-kZcPyEarOwSs6FP-nXY@Od%-D!ur zoDUb)-eoOSXD|Gz8o2!UJ}zlC5qURFwwTxBW@E7mFp##2 zeHM6fe*Ynw_1WF;!&9_3Jt>T92oH^$uFSkmI6x0DbXsIbm2ccGYlt5Apu;?&LHaOx z?>_%n^88F5@@xG}b@D3X?9Dfi8&v)-SfSc~oc(*oH+SBCe!P5s{Os@9+&SiaF3;#@ zTTaj)^L3^jx5*H*_Sw|&+C_(<4T67~(+jm1H&umhdg~BMBTOhxh4ta;A-=yR&i~#R z+Zb!RTmGHjL9(Ngq{M67ATKlO8`>SZ{0Jxi+tba7u8Kj+lAEF|HlrZUg(LmjJGp8{$!ZY#s$K|yP2 zMiKdg&mPZW-*Thz>(zo0^$pFZgu=%7FUJqCh^^M#>}}Y>JN~bb9}-4KRg2#+snLI4 zZXQ9rhFSlrvRi3SCMH)9g0*_|(WZ8DSCzb)syFXeTr0GeRlkzmYNe*~3E>wV@}M~> zBZIvq$|=%C^4mHqNhh|)FntS?gSbX%;W28i;#$^#EPJD%DDxIKAN1!{Z1}gL58voU z7BUX+galq$*K^;W7%ZtAecz^+(9FW^+pqqyU;N`^UF(VOuXb}aT=eG1xL@AP+VjOr z+u*pbx=5zE?EU>B);Apj7%-ba9-G*nMB3}1?^BHvb9KdMHwMqbk1!-#n21%&OP!Gj z-{77!9}0_dm~o+SSzSM zo3;x#1{D@Lx!;;{XcvkxikKCK{|Xun!pq^4IIB^*)fC4oV@t~qggq&#Bt_glnPZn#{@y)T3=jI0FCcTvkrG@96I?EPU_aBM@KRXXd-{M~FnFy{v z(3I)^^m4xFis5ZzW6a%Kg`piy6r>OP%3cH=lG0kb zvJd2c%Sr{^V>av9>LKx4I`80ma=vT0K1y*`_`p0y_2ce}mk*_y zW2^Mcw2Xee60BazM(xkrkoEi7BO?i&f&=#>>RVeccXcgC`YT?sd8~wZ2)^woE7TQs z%@5yxd`IBem>VrsbVFd+&#JI~L~#!E|3`J(t&MZf2ZKF|01m?JR_&sLP$t5?=j#mn9c{?x+Ah*dPKTIbu2 zIe$t0{FBT3D#UijB=dbnyC)OM6V9LQi!qV3kR6i>cABD%FUM+Oo~j#QkcGS@tc+b6 z7Cu)K*$o|FiUID1p~gODPd^{I`c!tVb~eObz?GgzzB&1dwO}1KW@VzP3(84t`DN=p zTwkv)WS1vnmn(IC@;2n3TDEVMlJFw%(~n~E&kD#9 zBM*F~%itd)T6b0pm~UEPWRDWf*XB``d#(JolfBHd_p?7A>Vc0^v{#ra*Uu<<(f5yy zaeJU)D%7-oPubkZIa}VP+{{Iv@yo;8tpSxoC0BaBF}oPmc|PHvIXfHZ#HiEbuVfMl z+M?yH0!vNx&v()T{3(L^NNKtk}HBt+ivlYBWPmOsGe^fxgDn>Hh{;IR2=`vjbt}c7}P}OMV zn{iK#ja8I80?1o+uUR`bQPk-zY`51Tk+g(ml<2=*SNV1oj`fDEZsSOp=r=y~dcb8dq2<+IzsN*7YjznH&|(hlg=0$)4WcD`F zI{yS^%!%Wz#i6`8AOSqC?vbsmvgkbsXrNsFmoWyp!JISnZh0?N7gA%us_Stf6Gm@!z2I z((z7sTIu+!EFO^VsSuubznO3oRJP2hE=|&tQLd`GkvvC`lZu)@*6&0p`mGQ-pI(0stY z#c7-+nDCZbN(2?zjDbWS+c%kpXVX0 zFl`N~L$_$}1XF)uNs>>4fCUpIj!Q1*(?`OmU1dGj#)!fz`X?=QFtWTtlA`?^o7UH6 zrA}v+k_jpderlra&KSt1yj*amG5LWG9S~-g11#+tSE=7QZaNr3i8?! zyk3VD&cA$HQ)gmg=CQ-uA^yBvq;ACUS+GG3WUrfR_L0gj~bHd*#+Y=)ay(C`>trDfydR^6eK;)#5rVNto^{qn{7Rai~c6Tf_J5bN=D*NBa|e?apY$Ua|V7&^}Ey z#z>SoET)mcRb$+f16w$s!7NR1fC|H09}A7Ask zPm`UD7Og9s^n>l8-_Pq_fdrXR2;nFiG|i>D#}%7W4oR9381tVllVl2uC@Yt#QLy+V z1x}QEpG5Zb_MmB3g8TVPd7?G23!v}7r0*-J6j^&bSL{(^lD3Q&x&C#P!O_&6&r+(M z;WiSK?ZyZnDn$m`?Efw;xj&j9t%>7rjo6)Syhn8+#AC$bJsM^0ZGThSL9yHbaLq+~Y zk^aF1>mo2NEv!15X$MW^2%;o*%)vntRB^T3ghFx=u>3}2RI%cbPdO(}Ti;YB*~$r7 zP}ams_DP8{bbXeh==AgDc7@RpRtCR-4svl&Wzh6OyTfd(->u*%IM6uW4!ToBZ`Mu4 zI$rF6JEW62a>R}a^2F6(zeiHfXN-!L3Au9c5ft!x9kM?M!qyo{+o40~31AbCRpl^6 zI*3|Wxx<vN5uMWC#VVJ^VlV0;L?G+lXY#FG&@LMu&^FQpytuJb@ zB3`$%FY8uF4mKw&DTUko!w$btZG9cprNVYHN_s929jZURguH&E^y~(TFSalfl3U&@ zc1JI(a2Co~hI){$X<7j9cO@S*b(^KE!~Rf3k(0lELlOp>K&1QxD|4xZc|c76Bhs;g zI&460=Rp|g0OZPx1v(qm$?W50`8w3&QUSD5xBGMxREV4>%*VL&!@)RvhetWz3 z?xmzxNP4=D)?w}G-Ybib_O<)n==LPA8CtNq5l`!$A(|$yU+2qh9I#3K$%o3LNIHau zXp}42kve*#EMYr3QjAVGdHn;7WNGt1p_+T zkgykS^+UPVOH;p8R#0z=!QX1u4d(bo-Bs(z5hS8B3np!k3gfU;Pb0%8sZ9tXoz|C| zhahU&DW~6xB9D~1joF{$C4TlQjMOn0R4B;0vwa>njRIe8YVzVEWs&%)>~MIxDMEa( zd3TX#HJR6m0!eXNneIXzZ9=%0pZL3Q3sr?9?Du{WL!BhaBg#p=zJ6qkb?Tn+0)o`o1jm&@iWYsg%2WZ^cIzxOW_+Ur5qPM zv@Ira=Qlb)Gd!lUy7h>Ktqki5tJ6)H2E7QH7`I^T$B`UykERxT;Up4Q=Pc9hSvx3o zDxxU})u)P?u07+t)}Y=G{x%N;W=-V`rNh+q*6e4qUlSIUJiT^3GXT_jEOW`yt#T-h zlQx#CUO;nJyPr(`lN2C2(M$15N{_M%U#YHo-Vo@BoX|TzAWnzSde(2M^bHrnO7{va ztU&j{DVC_k%j*kz-G<5EiL(JX0`uAijH^FKQ;)5t4iA(vVi#6t3BV)Ye7;FA9D3UJ z{#_UJn(E|DLd+KC>14N!6-#y-7X~>39gqqDwUAtQdas%9p@_flHyFqKa}xwL{Pt4pvOiubnkrVW>a7_IIFY)v+f;ob6e@+5C1S= z@)b=XtZq#;fANG&Hzhpv zH{NB;O*ds&yo8Yeo*)=ftKw}hETm+Q^WT`=WL;oN6u_ygU{R3TFzfI7fHeBNayRkD3?bO6p?6;oDFH zJ7&-Vsng4wcX1Z5lTzb=AA;7Cmz_b?#()S3vx0hp8X90QgobE0%iV6S;d_%im0AUs zDFLju%rq1!gUx;nT_WNvkRGn{8sSU(-_=# zI;g(jBmmC18Z23jx#!G`{&kse+co`a?oYvUEYh$VbIg$8up{r?oA|O)Gvs;{wK)TK ztYfffoFi*#GnO?~i1b7#dB!^BlLf4=XZqH}!6}K$HCB23xHxeJ_4JK==?apuiWPS@ zrmljnJjq3B>3HYhfO%Q;WA{cM9u<7PDKZVRQ(?GvN4&>`FR2os_nl8;z;|Gd#^8xt9@QoTC={(0rnypKu~ zXKWV~Lj;6xxyrDInZ!N~)n9cFJU`{#T{dN)oR?2jXD(5$7X zcGve7R#gEgp4h77OYQ>V)hrcwTCy-s%~Tcda!a{m< zs_WezTN}9{+h`!?D7wO8)~;7K=#^o)K5`~UYgon4e2dR4EK3ax6U`5yJ;Qjz*xv=9 zjD&C~^7g0VR21@c^`)^{h#~u$5=%@#;T8(d`Llqc-578C!k4>~fjTTgj%RRMz?b`G z%_DGkCq$PbY$U?r9nC^D=1eD%WBzIbVQuMDVQyw1OPmdAepT2sH|*Y!(y2?eS3q?c zY0a~Pu30DW=_lx$w?ZQt89>R$E(~nCyTb&%$eqUamcjf1O?y8h{FtrprhZn#;67w(Ea9I;7tqu7;3hGm_O6prH6#zqG84|rc@)a9;`;ZeU(dq;u(s@r83(xQ{V^sJhhN^%!1<89-J=e3B{{d zY96_zhRBpQn7`p;c)@(X~D)P%m{@9s)&X;r#@ouJi8habL*}1>`QeXVYS``8}ekk)h4e zy03Y3?Rq0;@7i?_jfzK>c`!O+^*u*)B#%j) zqX3_<2KO`+FYz_^Do2AxW}%7C)j=!R4{Cx7X|E?Uo_3x+!lqfpqMiVy3ggu6kX0S8 z|IyL>Z_4^K<5|M@^C!!oI?!c^wVwljJ`^mM%|I(C+Dizrj`1@ctthX`&r+E0;Qseq zvUNzmm(;M(A$tbwOf|KHolf4PMDW6Q7%AXbV^Ut=*SZ{4@nx7LLfox>)&RXdU*;1Q z<0W3>m5nEl>%2!4IzciiaAy@#cF0E5|InQ{ct9DJLR)evtVaQ{>DU@823<8QH`v^)V1q;C-Rp&WSJXC2Tdu%bWMYdp_;WjEae|AJKMr6pmTfwD{ZkJ? zrBDu}#>P!h3Ihf;+8CSJ`v(=}VZ09;v4mS*FiY__n5-;5VO&>!b-5J4*Z>|b zV){29w-a)U{dAOX>vFIeiamSnhSO1e;XcuTT~tYa9K=Y|6*Op?le@=M@)M&*=gSRM zy*9c!DEVnWXGxB;+p|J1C1vfpIH5=XdUym}#U@=kED$^wF;=023Bp-C2}-uLA`4rP zK@(yZI=Gn5X{i9~%;@!0&U+OJKSXxQI=}>#S}XOgtC#ms#hxO-d-ldEeBILdV!^i; z#82}O?tH+KD&FsI#+nm_l{&T)GF*W9(_;1H4T@SPknl-&YCat%T(_KM0(=wayAOB` z-o2wa7lyaHpqs*flrs_uk6vq{c)rVd?9tozAIax0+4zM#&du=CVb1Pzi?L2zehD3V z+=ig2W1$tIW=rl?&iiUOOiE#bgas@qG1J_$WhMvt`DTWC%%3_;u3Kt?qWzYsNH|XfTu=0!6n{B z+a%yD$>}7R8Z(bORbRhj4#n#R7-LcA( z1FsxLI?;0C6XaEtZZlvsj9D#SMTmSSAy8D{!v$QP#Lt8me1=kn1!^a8%`tpN(w>lq&K#xS!Gq=Ja`)NPj;-s)8j zLeKZ{zkTH3EPo;6OXgt&X&oaXa9h8 z+SWG$cp6`xIcUp#o_Q;A+gonvarFNc{_EQl5{~JhL{cwvcH3ETC6mu*yy{Q^Z0a8h zBjn3XnOXQHO)mJ}7pa)=$l+y%#Y~U1?dnEl}I?ta@ybTZvz-igg{i0BxdC!dRt9! zjxmoGS75lDFiMh~NP;kcFMC_yve$?F=eq|W)zFEl- zkPAj8GXWxonT)f5<=I`#<7 zt9N$?@I^t==p&mK^(R-W3++VU;c7}{-egKdR1D#8Qu z_t0qTgpY?xS=}^^2}|9Uvoc74TNnpem#=q>LOF!1fwXGvahWMi&1I*t9$Y{&BcW&J zFcgK!$k4s%zHz611oU^c48)ya$vJ4nT;!E*&sQppkUNn&;)EC_dVqQ7*#r7Tc(-5h zdcznZ;2vs>?)iP;p7&ouX5t$pLmuaRngeOh{}hR1j6Ssqcb56-KjqRz7-!4UsOg*%|Zq;efCk z?n7r;2tAcNMXTX9DtaHVH~cCnw!dg58cu>Gjse=DRZ4k;+l*evvKsf;##JQS1$~v1 zrX<&8r^@zGIIt1DiLkx>I45YN$B+8@mlOsn~;X9t1eX_)5 zEvgAB6GSPHz?z*Ch>erC3_IQ|qn~PqSAl2~9h7V4Fq)c2J`OiR{!SGu^nk_?+6=nd zD6i#}mES!@d(!bhv6F6cGP|TiUrrWo0|d63@gCuiMl+3hss6s)As)>RFm=Ye+jRRn zV5BoLxXiPgy|!#=MF7g+5x(*5Q$ zegrD3QsFEzf#j&-_Hw8=C84tuXzaKE67u%inJDj1d6{nYHlWAo-9U@HVWGqzG2A#` ztucR*c+O7Qh+7&`4RLH*z>9*a-pwj>h1K-$=TM($V(`Jt4GWl!)pF-~_Dks&Ep3mA zY{-d4TK_lgMUlWL)Ib(ZZIwh$pbOw3lPYwx44I|uF4`M3eL@B(W&o_2zEc)lvQu^; zL9M&_FtW?Xpe5|fl-RW$5Tl6+DImY|rawquWUT^8mEG4_1o7mhV(JnJP7a5MM>c-> zGpkavRFa_nE3;6{1wdh_{yPd2x-e3NIB7dQ4Ps9jB2&Wn^vIyb7CanMS&P2S*kv28 zZq+A9G>)_|H#^kdaygA{o(`}*@K7-rUJ)k5)C&Mvo4lPF3!;{k!Ba?)3601XI`L`C-Fh zUqiz-!6dE@1%MSY1CmBenZIm8m0F!|vS;VU;ZWsE1->NDTs4S5C|j-LWocbL@u~Qr z^b#VDF=he2(l_W}PZ4U%z>sMvHdRT-XA6}RceHW6&5>hC$pME-Y%%Q|Wz#y>6B~_;pYrz^ z5wW&9Rr&f^r|0+^Q0UmNQHHHW7th>mFDP_Pza+Kw!RLz>`lP=OIJn}a{WP7jsb!Lr zkl>$ZIwc!{B0Zr-Fl<-Hq+nCn8AS4^oXZX^YOH{@dwF`E`wnz(AQps^$cc76d0YF50#zT>EE0*S=a!Kx&-2AGweKHobGbTPuv)Z ztfulV*{o!r#nQxpjg??UiF8jNMRl1OpT`=Ll|@f-1Yno7^6&O%yM7AWdA0!BLjN6TrfxHLgsi&iKcj_%KmighGu4-eQ}E8_edj}LnE2P0bSx2`qtyDfwrb6CsPlXdgOCh6w-JDR~T+Fjx_5ERJr0y5C6EN#tQKYzldA+>24`C zJlY@YVtrADyyd&qUk0F|ujR+_=Z}){xE5cnkV|u|HJk`$`wM$LQtrbhbTmgYiRsON zwSX*E^HDROTpbZE>MUO2F75+2Dz)+Qcm;K*fa&z#Vx7-pTjWw=%YnsF^7vh>eJvV% z#a%5)(W-c8n4Gc=v%mbkhYW~QGKtY=@rXVOefIH5)nV4gC_Gz@Qur%kFPH@~mJ`!d z8f71Isif@$PAVuZfQOev(B!#}hH7g9%x^Lka@1VI4wMjPfrOP;Gv`?({p*?|ZlX8~ zkBkeiGIu^wq+YRJrwm${Tbwf#=NbQL@DfnhhO=p7=XqGx`6Ry!6slZDXBEboLHF4jQ^x1hx%PNOpj9F_f11kKj%l(zDfFxIZBuy;yNwqhM$uDS7&3>`Q7uTpAUCaP& z!Zjdhbv( zH=@xsRQiGIFXtaA$zps%GGT&EuADJ^$qI~|JKKT)dm9odpH(uN`3kKUKv*)yFSBr~yqbs44(h);2&2S7{*N zy9)J4Z=MF8YUq$BcYT{~Lb6BWB{|IDgCiyn|_p^}8XEyxlvO-j4!9Wr>W z9bh?a_huott8RE*YJziEBVXgu;;Z(d&LIMG7-kbtQ;w>fDW3Uo7vO1vAtv|+Pe!00qe2UX((C7_DZGI6UtFYX3KXT_D?^MqNSib(08cvE z1l(y=R;gl90>0nu1`N`9eyv+~)tgpZAh9JrtB&j&$A$V~M8y-Ere@!lAl(;QQR)r^ zi|-0|`$klNwF-Ba6XCe+UWPust$X54$Fq_GD2t|}>CE}**?&RvtPyMgo$R*<2m7yDV{*iFjAk2a0$WA5R@#7--0SFVM) zM{#@7P@^;;J9B@vyr2%$yS?Ep_L1~0_Fa0IB+nwr6vo?kyNN!I9&VVO_OBz?7yq?y z?qRKFAWQ!U0bDIVrKo#(Zr5L{Z$KShHnSC4=PE&W0vn`~w zM$>E>Tsr=~-*ocBpdu8F@~%AEq6m_PNBZgfwH#Ed{VQ6EtK=ZthP0FD2vCp5=yhU7 z6~YFxU*MTLcs2gJ4E+j*%p8WdXDCtfMbCjQikdz@fPC0{De%+6>&{n4`Zah~9`Jo> z`rMs_qv$lirp3^gzv}9>vGN(z%cvaS8Z|+k;F+TxqK^&CVQ&aNUO!@%Z>b5C% zWAy&al@UCAS5`UdZwRzd!92cv+=d?fkhIcAxkFx0t{5;&uVZr%2SomMTO8TEhbwkhR^gNXez5tkz9&nptQ2Bop^p_hBfh3{sa^PyVi)5g;^KtoV$p-KR}c;8Bgo z$K61u!Eb1-h$#$Xmo+pEuSk1rJkTQf?d45BKis*U9$%0Man(;`fntrocyg~Yz-_80 z-@GDeCfVs`<$LZG&|5|@PXfwhI%kp0&q;A~6FMXmckBgSyJKW0UuaslAYF&B;C!Ub zi<^alFQL>UD$LK_95zBh#If3?)({%?IRfx=vb|l`2Py>9`RR&355AKxqKmbT+`$xs zLsowb%1_$ag@`ZHNc(M}#$5~B5_Q$Ro5p;_HpHu_TMcX_ zTd}2#RT}fU#tl^?uftkD+A&)Bfb~|yK0vpTmJscbj7OsQmrF^LpG$h`YSwEY2~t$0PKA`u)OIM z3T;`$N2S99=bp5F`*PW2X4#zz#&G%5f=2jC9aankU+agPmb%fhhQ8@$wPApJF?#t2RMuYFXO|H_!8;3nZDV`zK#jF5a z5V`SvKm&cHM+{+04Bi$BJ|Y#FdWKmDuSXc=rr*(CA^)L2dm;QFcl=>U1L4aOrdl&v z8PM<}U!+ayH9caS;SusJ)+)U$gS~RhcZLq!J|Fx z?EJ*06;gXau%m^YHFtb=iK;W-WbeoAA#`r2J-=OYqT!^SfZ9cQ87<%|)~SIy_t<1z zyIBnYs{wd5clR~Ck4FGQy|58NQK~yEymp?LDPKa8E=tQ`(LOOeA<)_;QG?SS3CNGe z2@%4^y;s?iOep|ZC`p?EsJvKMJ8}rkO^Nd}9V{4f-VxiuGkL(jse1)zi5FagXj~|6 zN}O<>OfeoGK5QN;SXZ@X5p1b~5Vw9|mSJga!510&lg0Ni?pu|r5`sJJ@U@@&N&?tq z?-1#YXB$FWot094#N%hSyEVf4Il-?U+i+i)P9I&sYm|vR-3d$|(Y7Lvp&r#xzPLX+ z6y!ksx~p(Suea$oiv?O2b%jx@R&u4DbxmGtMgtTK;1{mrOGn_Q6kw*KkElEwS|7kG zeOz~oeBf?`)zHXZP$ttIO=bDPH0_BnDmL0-?YAc)$PwC-^<_u3ezA&#N88oNM@gxj zrWoBqAE?F_vjiPAzwZb!Ig8}xLamZX0I0qSe|V7hCF@&KMg>SWU{?#+fSp$3gN0Uw zmQ{mA_u8ENDVpWh3Y;Z8)KG@2^>Pq$qk8ErdvwnLMVu}^Lo99}f%G!^L>87ti1Z1d zQ%S}j@q4Yd`1&dd%LCmi`<3@p>-+|^cV%|)^T-o;d)JLKmRUNZ8P*NXaWiQleug4& zT!PLwT?+v#n%~-u{r0$b^YVl)l5Z2|%ZBwI7~FoV^KD$|os+EfU}b9n_4ug|Tn?t@ z5qh@zdD)k-a6s11WJKk<3sAm!czci0Gkb~^JEOHa4ShDuR!~o}fw;V7gc0*MZ6 z!Uqx&(&NWRU|EfU_MMcA?y)>m=&pkEIPq2-Z{H!yg+engSjG}eP>AzT+EmgSm z1O?J*g(ssO-?-l*xMZa-?&RMOh@C~=VWOlK2>t{{F&VaqS?q^sAa{^}BVg?-1}#L( z@#WGg;MlJ&3ouMkti` zxo@&x!!l6UE-9r*PCBXgO%c32$(%)JUUjaNg%gZl7e@Chi@49HiJ=oN+;`beE0{SA zSx=q?R}4=lS{AY)L;U^8Cn~WYxiXP+_@wxnUSCyPz7_fs&(C|%CP-B+RkKFFMzOs( zpa=~quw}n3@Q}%s{*l&T!v==Iq-*joB=yjvd;D^`kQOVB#kHJnU&|zuWs!1D@{36^ z>e%>BbK^bn{f~w!%==s|GqqT4ofCm>cXB66C(THjdn=xgxuW#8wkS4oC>)2aHg`18 zBa=IPyU}yS2&#T2eNXDE7N>)D{u|%f=<8KFjlM^P+ zSZxLa47K3cXDOi!M5I}%M>ljL9Yb~>*P@ZP2-6|Z6U4t&_{g~0^Dyoc|>q3fR! zlrNo_$plXph6weNGJax*>t@}2f$$bF02`KdE77n1JLBBgRED)q)6iayuB=kGF=jC& zpm`omQFE8cbUA-?i6GFBA(xH~36qXlX?PKF>Pjo~Vplc`oaGOKsD1KhR3#e{Pn7sJ z4MzhD`-F3g63&;i^E}z^e;<7Jw|REYlkYhA*!g(>G5L<4{I79%|2p`N4LAo(r&IeM z4Zh>x_^-itylnqxgYP*1deHx$!FR_K>@Xk@s3-fEqCK)m*c;<6e;7CfR01}m{~Y`W z3IE}Q|46}qq~QN~DL5k{aB%~{{2N>NY~r46Av-r8$3ILV`@gY;9RDY_kc*G^pSJLC z$Nx3*6`57S9BOT9F6C_O@xR$f^1p_|{trzc`M=J7{ig|hEU|KFVKhN&}aee;d z|2;dw%W!a59gu?muiE>YHv5~}W8>!ghuY(K_QIb}{!0kJ!S#O=0{qX_-d_`~$#sFJ zv6JhQv;OyL5cyw6VE^xGFyqcNW}A4x-i*`aTjgToRJa*nQlMePScs25A8xPtAs3F=-RkM__gB9)Z2#}d156L`Ii z`k_hQIs(Q{RSJqt%BU;kBm3HV*S*&l9*#Gsami&pIVi!+ta9^CZ8?=T-$vN0TvRtE z(7FS^rxQCqzJJ_hmMuj0D~Uu7jV9c=PSf+)$U<~WAAhOUx!x`cKfY7yw1-NG-R54k zeg4qtE7)K=`eoC1yu@iQaJ`e6nK|ZUrOo9iv=-y-&2PuM#)JI{VM%8+%I+jujHl-J zC%dCriiFoD$xz?|PY<{|DQN)XMDNc0sYP*>@XA{X5B-=@js}8yy4KGF`d1Z&2VL?5wj2{*y;A>SAUv(Bn_%BOs3tm!9u(Vf+!LC zxLWc!^U0F@%1{zWcOMh{@oCkj^e)G)Kdh(HCc)&JQgZgS+2u;EjJTJp20!tKkAJ`o zwCO&0mjj)vIGJ{8d|ZWA5}1zLOPUHtdfaz@Ri$5|*gG};yvJ;!J4590#E)^%=kW=C zXPi#X)qwz$11z@CN5K$cKRA(Sbo6~E{m1(UuVSZSjLAT9q|ju=Bdy;X1dvb$%RIQr zc%xw)NGN`^cc+Waq%4EwRs=a(Xy?o2+x_5f`0<{poUx|C`=npoVNaSL+vaBev~L$U z(o3>#nbb&O_HI-ZZmAmv) zneYX}BC1_^x@l}+5(-d5HqN?N)&Sx+t!D%j@0GMAe+WD?6GtY|V3dqm?stI1w`r;} z4}xBaBEh(ZiwgU}bVjc_>`w@J9?( zLnvPPQve*&24bRH2lV#kTnvF$f%WDZIA_S=P-E5ce!Ytg7Egq+_zvucPb+QipQmg7 z8OEEOt{HV5MHCx79F^SIG)574*+A5@ykHK0M=<*-bph%+89;wxbcaMagtG+l+I>6`8LjO%=((C8!{uXwJ7COl_!`n4(`l%VfP81hvDTB0wUtY2c3V4G|#G0G&IH zv$p2vBu)?@@y}GZX$ohC1IuFeQe5%J@6aiu*~8L6nmFoxvPvrHNLfo!G5JJ|!bX(^ zHAb=P`5C~0YIt~!{0z7NMxHtz4|5zv(%JIge03r~2VS>oR5U0 zx;9>Uc^IUO)z>zMW*03+=zq9VJP64}1)Wq`xdBsRF6JOe@lKGFDhY)3CQUAtKz71x z)E1bpzoa8m05t6SZZ5oHwh1)L3*`Vcy|kXh0e{pR1~Fm&)I~4~0yriX#)jv7FRmZb zt;P3q;#CwO0N({29GgU}Y7AErM1|1bGv9bM5`)m68MtEd0ar>jM(b7BMyzN4IVD`R zlu`l=gYf|t{!43A?4NQ7M)3gA@7?p+-^G^Zx-KP)f56>Z0gYhywc!J|9AQtJ`NPq{ zRUgImbBLmb@`2IKKp%@<*2uH5z+uMJ7p0KE4hTFm$4I3V(Ixo%+fyR<{2B&0r#G0q zo__lL%?ZjeTrAH}0e=-B*o2_auA1GreiVS~w6CPMLdie&E};UTNyH9<*!T}3ttCT_ zKE2y`W)u<=8#QE0unZrV(8!*i^QHANR-e;gq7+dmI2^TWtQhwt(hsMUGwDbTyzOV! z{r=KAZpY4@y7)UtoeoYykUzo@puQ+V*AAVMWZog(c8ZZ0UO_@0C32`9PM;G5Alh|= zvqEfe@32{{f!ao_S0FR9&f)!Zu2qBtm|WU9REe|ctX*_md{l2fDUK3znrFCAw8?94 zRM`YGKYhEU6b}nGJWLFbJF5)ZperK@C-<9LJ}?DIkKQC4anA`JUKpUOirgv?h6gcw zbCkg042u#ZF1?3%c%J}x^DWRLCB!bIaiYjqn8Xjgt%9!lUP05)iPgQtJqZPlr+Yz^r* z1Fey|P*Y0x825@gO;ESz`r?HTO2mHboobz%5cL$MA9ltHrX{AMD6F{jof-N57Smx{ zYm|f_;x0}wF`7IMMhQ<+{5)D1H(Xkpjh^JsxeFaN@C+-uj<&wg=G=Jf=^hGh+#3xl zN=DJr2%hYm+rw4VZ`**jjpYjm-Y>a1mjlq2RfVQYLl*ArxXJ0{!M%QuXI6A9{J=VH zX+r$r(!i^UD@kgOer(P?*2fK}3uLAu5sMqFwU%UUA^4mNfd%g5CBlev01XfeNn#?w ziWAhKqPK9JZWn8+VdVof#YWU$-rH_+i?34#n?9?!guFu3!fyF8K09HqZZ#Jgd%|x;qBp zmm;7GgCs?s2qT(qtyS9@CP4rWrI_r-$))SkMAO)}Na}PNdL?Yzon|8-W_r?NCz#_U~TwfiwUs^f~z7y#u(! zbr0dzC2*t9MRMW``KVV$iGkp0#p9pn2jGd0cp39{X>$G=HpCOPeP%yCmF7hE{DM{^ zxaOn0V*Ne!HZBXI`{dmL?bx_t=ArdD5OgkmcUgc zF+K@GqhqO8UanSokQNXm^mT{dq1fo+K3vOBy1)~NSgQO~+%HbV?On67)_JrZsT&7_ z*R>wT?~MnFrn&cE$hAZnvWWxo{_k4&aBBrsK#RFdBoWI3bguX~-HU>{L9bDsv_^#!J zq5|u_3iudH!Y#m5=67%~aXzJ0D0wQfWK^YKKJPJI;4HY|;sE~7_RKRzs~c|GAR%A92CVh?xxE%nT# z@weRRFZio-G{Ff-@l=nq=2JDlyDL@wDO1R$6K=2nVBzpKQ4+j@@R#q@-7)TnE800@ ziv$d|A(#Dm4^^GO*gi)AgAn=N9T{)guy|}L7$;U28y%W+ zyzco%-rsBC4#s4K;)CvJxe+oubKSx5SJAG+vMiO6*tI+seqCI!`~PEEHTVzga-jGTOW&RD|6IaoG6eTZ)&iG z4kv2Vjfw&ybAK4c%_g^$_1oyX(>8f=Ny^mf87Ihwz??jcVJAGp>eb#jj?o3){JX$0 ziCmK(269=Mpl!LtGH z={GLQjcXq)BpU{ch=B}U;n@KsX-u_|OQ8I8`mNq#(=NmbV277gI&mkz_CG}=%miZP z%Xt5Y+;92RizKYYh}*4&{j?H5)1HD3qs@XJPEK+PJAB4#G2%KEYxR%UZRhOmA83%LjE>v|+KJc{ts#@IPXbj3|GO&C<-7R-m{$-V@i z*w>Ul$SHbe+UgC9>>(eTqm6ym;tyDR(BaJ{n4$Ave!1r7XL2-uV)?@C=BXpyBF=h+lZ`9je}0Uxp#@1-%})(Pz?OGHB2GWndP zVy9~sQX>wLB7=fFUP(7~(I+jUkMV>Rcw_Z3pMB3Lvsg++e+g!8Km8fLzJ<8nFG@ST zD_I{hB|3;93i0Qih&8CN6rcbVj za|p$|25Jph7?;?4`gZ#b@`zSMk1neSx=R=O!3~3DK+{YOK2~pex7Q!VKLR33b}%1C z97;C2cS4c+@;j5L&VYtPugcQZec)59a-fXN{lqXIiI`;qIn}x7w@<~N!~qKRbslx8 zENu|pwSQ^1jiov7r1xp{i+PO@f1A{${VEKm8jGnR2ft30tvw}4nuNvZ6Yyi>LQu`L z!){)BusB_G5~n1<5#uKG;f^v16oHtabYnKQhUw@+^kMAeF<1*oJe4piLS~{czUr<> zT1k@@g|ym;z4=tC?56Hx<2*S81~Y9^U&lr_)3h2ziyY8Wclx?Eh$w>>OL?>evYU!8 zUnsB^Qe%grP(GvQpmgZn+m55zzhHiC5~#!Z=gVftN?u9d%^0Ujpc&hlX!SHtdgT5C zNzlVbz!3+zyAQ|Hv*9PKP<2p0?m=uu_sVI{Q>mqQO_IhNG9#@$kg-_9c8L0z^-R%9 z`YeS2eR&riOm4T4X4Ax+Zehz(xJ@t7Mh+Y3f^x&nEb*&bMVBX#;&*sRDECiWrKKKb ziwevcxHBiDc!6@I`@YxY@7=9Zz`a*EAHuqOFU}zS2i2>D^9gsfDQ4@&Sl(jyeMTq) z*656>+6RZR5SshGc_s<@kj=d!B?z#BknN5bdKnysIiwtm%2f-I_adX0<0H{MH$?X< z*rZd6_NkxR+CBZ#QRxn5Kj`TyH$~ebh7AyrpS#QLIB9pX5$r>qG zR3{W~PbqdfA?%9w38_E+*0I*82nSZT6A2J+jj%-lZ1FD4V(KTnN9^hN+mU7&s;ru| zDj(`FnS0Z98mxOI%uaUqTVb+ivoU=#Ll)&zL2T=Q{Su`8@_<-0zRz+-z6U!f6kZq$ z`1u~q(j5g`naG^fsJNwCC`u?8gTA znSlB7-TqP1D@KZJa&sl7wHudxE8{)97C;-AkJ&&JfBVoUzlkUl3+@s(!Ohii<}g@) zBnk$(pVQGgVE6ByKHxNgw`A^xMdhxqeGN8zf&>A+r3hDCrV3VPKYm1C#-x(DPdgQc z!{aQ?A%`lm2D<_Q4qn1n8K>olpTxQYU~6nYJlBNvPV6;7=}<65_?kD=@7w+RZiT-u z_zGC?i4P-bBH`nO34l6jT5wIOP76Sz(=K5E1H03IVE~F0Z($?78AC>{LEVAqvY2xX z5yK{fT~8V>IrQ+HyQBds{bE^V;te#SXI%q}Ay)`lZIFK!yFdwt-4o zO|HBznJe58=|=>)wPdLthx%6yDp?LFehN-7mG3fk8jiSREA7dW04Jc6TXZ^`ydB<) zI2^lpZ<*ao_Jg_cK5TfsUfL(f`s$cPfSoUMUvP^5-o9vyn0AYmgZ%bZIQ8&$gFt$- z3}p2QLwNF;(3}k6j>s&JIq0Hto4m8$W%0M~r3+a(kZfeUzZ_M>-xeP2h3H2zqsjQhhyv!}v--t;Y5x z7vRZ+hO{BPWw6a6tg z&l!crk>3p^;Woa3vN@o@OU|J?IEr>$9NVx_gG!&sVRht~|D;U6m4^v)RB;3i#ciPe zScA)JANKsdZ+X(`gIaoY$g6&IOPA`3bR6V|l-cGCudAcdMyHc*%ZBeBJNqQQg_O-A zQ(e&`Kq7K1J||s-F7OF)T;OW=-(+>4leNzYTy9Pt_J3w|+5dj>U$eR#z`6f%V+T9W zKZ{wMJY3|g|MRTwf1kxA|L?Q7D;|ls-v~iYVeH8f%e;_5XL?I8e-I^mA8qI$T%B@| z1ciStCOoV}_ZbN3H5gV}0*RpqZd(acEX=Jhh zCH<%N`2FiaBg8X>n<3Bny4hhX#0~uPLkyxM_taz1BX8je1~Vh$Bia|zuFQa$DrJ)I zxp(e@{tX$9)4iGDS)_uHj)^N;8dr4@vtji0=~J7-BJ2H|2zf!=j00VH46hQ;#^Wxn z60)Gy1~|GusyL*E^u3l(9gHl%T!bXwG>y{T5%l#sVf9~Bf%%!Lf?=$VUg9Nfi-vrS za9{5S-UHi$q#W#QxHyX`S@96Tpux>F^S1Z4jL{Cl?PSiXclc zN4EtWq8^*jSglEexa`*+g1CuUrEiroKazI&k!fsbQ;`b#$DG8A&?ye1o3O4i+{t;^ zKtaF#VqeoKPNJ`;Q*fK352-f5&CjZCX1_KU3}dpPwpYq*B<=dWHkaU1_}WL%{~>4_ zb*QcyZhk_&Kl?SYAY|N@X03W)5Pf}Q%hf!cetZPoXb$u}@B$UU@RUrcaZCZ}J44Px zi||az+6Hma-y0XNBhBm)&wdNlg0~lzL^q?*onJ>@b)y$lrOs-7gcs|Whan+l5LH^G z1)%j~xv$<$C+#z|X1=YHemVlN$UD$o$$5wzgV4<*NfK}7(2ZKVv|H~RgZ8kE@gct* z5MyuTeAt67NPuMsD1X9IupBAPRyx26NB-dGOdTE%^3iys38g-l68hAyoU zo#gz1=t3(~J#-|->I@U~55nbyrE5v>qmRcy1b!aVg^e4G( zayAq+Kz8X<*j5cCu`8gc29RjGYpY~n_RJQug9GqlCQCWl_q~ty1r#l$i|=}y%Jnyw zMD0=)wOgsGp8>o876+tLRHX4oY#+K2%3TFleofHeX40nwydl%jz>80H(C=mgo{VVC zM<5=k@ePv-k04}p*yI)C5~ac$t8Z0+D2`E~Fb#ZiJ0X50FEi?|Ao$Qp_DF!Zzvb^f%?%%CYa+ZT(IxmsZ4l!^SOdu-KLwFlTlaM?a>ze!!=SXl#_?WL;KM zZ&$Fo@n>W#RdaJ%ecJmj^L=}5?66e-m9K~PL%dPMBDR887R2t@#Wf;zYVln%#;ISi z-IWLmImy@62Pmw$HJ%{6y0xxyhRbNIGsPF?7&7AsY3yD4 z#xhb7=fZJ^vF2Y(jV(#6WAFg72rMc`t=xLM7b&a(m+_c7T8#hQXUMa*ym8nFf7noK z`2IA$R-}cHNazf z#D3Pog=ctIQZy7nz+Q8VrJi+(>WA7jukFFyHn7l}tE;-tD)vrQi3RTt-P?@5$u}al zP}%6Uc?R^P^natj3{m zbI~B7@-Y3j^@21llp^@VYED?)MS+kSWdr!uvknR#sxSC)Yb>4?wKp-9h1Io7-%yh0 zIFI=Pf}EEKyKe-s-qc!&*|eT@hw2IU*G#+7+^#`6K9EKEo~y5YL`mBp zhhCx^rZ2=4s(*j6DTh*C1x?=5zO zXRF}U>fX)h`opvDq+kO2*6GgvsCcM7VGz3U#V(EFm$Sw`VqZgKrq|fU>gK-B%^w}L zR`(6!dhr7-9roi->jNv*`gsO#q+8E6j8Vl7w7g)BO*}YBSgQLOzA}=3(a*J#$8poA zHjrf#tS8w;-$ZaA#vNvHw#%Ojk#oqAI|w%eC|gu3gYPX!6%E{N`e7|aZcU4zbz)4o zko4?4n;aUqrP%VFN)%rM(t2Ca>8@iWAFc!f=&EY_uKi|~=?R)61lR5wr(?f*oLkiI zHAp2Ib^NZ^8=%*tPP)TWd;M5l2G;y+x9pnL-1a<+_qt}i*J*{1!*Q-<5mnRRuUXNE z2L6Uf{7S1gHFIWZJ3q!CzVjh+H^q%TdHS~#j_!~W)0N+Kk=yc}FkXv#xa*@V%Jrq} z_-s51fvkeBZmTkR6w(qX^#Be;-=)isZWkk2_{K73QA>hoYtjVbg@)(4jvi%l(QfnA z01Ue_8XopAwcg9(eERUU3v4*u_ z<50R@ye10QqhA*@EC$uV5@RZMwDfpqlVcD&d&Lq>yR={9ENy;l=`1Q<_?<_&mNd#k zPLffjdcD326+0sN24f5l{vuFAS9uW(EW7yxwl&8hNf|>{=(a+}QR_u>8r@qHtwm5o~4tGO9B0!w99ic6h@fNgxS9 z*FS3ITg?%No^h*OIS8fo7MlX#3{wQ?5=n7ti}l zrs^Yld($FAi#`rvsSFt4Z9;Phg)P7KKvT6~en1_0DWNvYyb`k~wgDC;UY%T2$74~L zCj8NE^=2qPk8(dyqm-CNg)i6-^>ifL!9x~EF&%+-W0dJT=^-ElAl8G0)<}YHjQca$ zq~P0@>{~d8U^==nRTXNFNRHtv% z!T}J}+{JyZq8Vu^R{)-5;rJj2{Zmw?K(*ko37G|P(TF_O5;RWavc?+B>x{=R->@`j zMP?CgRb6BCyxB0+BBJ0Cm&Vwh=p>MrO|&|c;v|Q_6cwJYU&c6 z0yb|i9ul6HeaZr%-=!m&g_q^16g^JsxWL7Go8>G?{#KlYKv35!JBMx0B5`E-y9z4xeXL$PJI0{l?Q{VlBqMiSnO=_va{|laKqK zUvIGgBlLfc^8fl?L&@3E$=un+L(ckx3puN*i-(;#`F~yg>wi{NR}+`NechpIWo+i? zM$W2eVe!G-1!%Hj`Q5Td^3 zxTJ%*X+Ifhr?XoZ^4s`lyq`ZLVG#=k4L zYp^6CWN6)xMyMW^R92ZWPW3sV=tW|te8i8sX<4}GvV8^SxZzQ3mTtV8vBM402*~hp z+{vTD^{Y9w+)l*1Tk9r-Bm>$Z6d)+S>^ID)~14Rr_N7D|h_x{7#X=kX2JnD0Vs%psMw{ z@p|_=pVCyrwD%f2!7IacI;kVDuVTzPi$;I?Bl?TfSF{zwkNyzg<;;C?A**TYP+20n zSed4+jPk&dDrgWhNN?ZzxhIiq7`+8hV8buo^-u28_*GbVT4F8E!?$MQFUDrA{5Wz! zAn=$MX%k@GK_FSupH?wx%zrT!HiL@W!sdngZ?75w(QaF!73}TVzmpdPY_AKgouPQ@hoRtWDs~OuY zZdz4^A+MkK+llvW_qDHyt8tqRUnUg&Y(G0fx*kP+AZ;gbL9VlrZ}4J12NQ5%P$Og@ z%=N2%3odhcp}mxqP^iK)*gt>ustBt0WjiUtT_7`pRyzDNQn7DflM}Wnv+dU4mZrK8 z?$=`i4x50$x;>K|^&sbFhQNCb$tz@+^odb@UxYRET$wWHEYA?0=0a6z6+M#o&{q!q z8L3dbQ7*jkZ8{#eukAgNc~l)il+k;lt9Koe&=pks6J3eWryjvW4J+MZ9G_#%U!6pY zAI}uLcQ|t+5r4I`Z|>$v|3rvW6{udjBi)zXTybfdboakeAE%WNB;zG|s^ldSG^Bib zF=>7&q78cQ{%8|I}N&|3+{9 zUFrQ_=&g(-H#OCT7op?ryzLX%l;qS`M4m|GKLn-mz{=q={z|YPgK3}=@NaRk=rzpL zx?rS2DNQADQGx{#-d)xF?6*de}I_eK)WKm@w#CFkKrK7GSAc3E1iTcA?bNV(I?6>k}4#v(_c1 zP9>TTvrm3;l)U0p2q0w18J^d^H2B-FiK=zq2sxg>Lsy^aIX<#;yvRYyEvMY-tFpT; z+F@+{o{~)SGlxifjIfO>^HZzRY8v%M-Y0O5UJCWPWu7;pdApiS8fg25ZfffGiyVjz zH`Q=tmnO$9oI?_q@Fe^K76eJ`<8O0DkNnMVl?ie$wEP(X4wo@0#F|U7-rgIu_;X&T z`rCw^F|k*EBg|cH{Hh)`nhoE-$}ve2hFaS`)hh0|g5>yEdv?d_cjv;|OJCIop+V0( z^>dn|@l4TAnc8c1N9C;TFF@B+I_ac!9iP=f_4WvFwl~TZPsu?S@SytDFPASr+8M<6 zZKBemL>?X4eSpcbWntXoy~_%89P0eYf0H_Va;G6!h%XZGN(PV9Tyndxs8Cj7WCRsG zdED&b2J>mFi{E1U$5eguWTJ zxK^%)AfFI^rLQuGgN{NV@mFwE4kDhgK8L!P?8t*1ZB`X=RUcwFJRf)ekfG>BIBdv? zQmXD;qsEvyQ|B@f=B)+Wb;DxEBD!QFa|N)l!7>@4k@#~u!L5>mg8hLqa}pejG_3U- zaIL8DJP28oeI5oHWa9*y4)B}+D+Ka5A?SfMyU?6aQ2nLeAb;$ZWrJA_u!aPlVdCYA zmd3uw$4C{M#U$2&c!-NC@!`UK6AzEYjDPccf~J^&A*eJ*TuJEal?zb|L_g?HHsS<1 z7s3OKPcT*r?36LuJgCusp<0jxBGogzF5!;%1rfh%Y2)437sBXGJ@^~MXCS5!!=41l zn?VTN6y^iM9+)1$&k<}B;d+? zq{3IBqED?4P?NEt)Ta@mX{Si|E?_3*OrZV7K(Uf4i`Mk>Xde>at||Luxwul)+ioZ4J>n{JzAn`pak zNClyX&FGd>g|OgL!l%|xF_&CB&Es3+OFUK922XADpMNtrFnE8a{A}?V=5tJ`oVw!- z?o39heW~I!n+=VPvCV?b-3&`v)OgXjTjqF%A=lH-`_O~xmT=Fne{6pB9ta&I5Ty}y z5D^lo6D4!?a}%aHq+CiduzgNF8Bw#Ku8ZW)si-_IVxAPv zUnvu+FfFdrvdmMc8^h6$XVK*qZPIH}brVyRWz}LetWrMB5mwHs$ja}O>eTfc>Y5;5 zG>Q~W?=H@X$mulBG|t;FeN`G)Hp!5ulPB2EXWIBa;#_!3m!>*ALndA(>btz^{Pe1t zQOkJ!II8WA6%AJzck4iI^|H2h`P?4?_U%m38RiSnK8vUB` zj^>UXQ7F_sv>p9s-UUk68tgNJb-1_jb?p4p79y<|ZOxQS(OQP~82uR9SRM*0{u%x> zGlBEu`;D(8&T%YeET0VBopPqtmp*Ul9R3-RpNras_SyHX%nL0%e7$4Dkj03TiX2ER zO4ubDM==NkHb;rmtZItCF*&t4EfN+G4i}CR#_51|l=-mU8b823K>w^<&YkW4nY&X2 z^MT8e_K@?zZ^0Uf!YCX_*zm64-fo!g)<81WABI$V(=TAf?!}=YuaUS>_3{3EAYftQ zDyKQ}_^R&i>+S|-Hj$JN?`xHg8T>IQ6&dwH_J!Nq?^53+XQbpMy(E(*Ws|j-wCjGp z{iO4Wyin)`88Mz071X)p=EsU`=+@CD^419pr(ROs(V}z1cFFK~WvpSh_Irkx#AKvv zm`~w2Jtf<6l~;z%Zrk9wgx1iz_}zZS-KcZ@Tl4!kaB^^M@T5d?pMJQhOr;E-%oePo zXv4Z|t>Hz$&yvL0#5{V=lF5=$>)`s^`YADLd{nfB9RnDqPQ_d8#13vM@8 z%MR${$NO&X6I=9(kl9MTrDg>;{qB~HUmb@r_T=``3unV`hoj7i*Bl~NrOW z>P~n1#hS&GH`c=*u5Eu; zWg|6HbdY}#Kopa<(|+L9beyp`ur$}XX74uz69xATQTQb;Nt@r`jgoDJsQowN<&E;* z@x$DMmQG7QLqCaQ>GQa8sSv4xfu0!G8Nul}-eTTFo}Q@q{+g}#1C9^tzr?+8%J!8w z^O}KHDhmGd&4F98(@fOY3my?ZrTerGS=WsdIuSZKv&*yLjrly*UUFV*`~FiIq+_Ja zBI!@wSD&s^)9QvSWyj;j@V=z;XSw-ai1mk@M~35v3TV0rx77dqkx^M)xjGz>TxD?H z>|C7;s}GZiB^U-GBc6N@Ru85}Te-BziVT^h|ErLr zX>B43o;;Xe@h;UA!Mxq~_U&#FLrhH3T2osQ(TlIiZW?T}u-TAUL=aR&D&nDtr|Ly5 zrHClvrS+g9;vtlxJy{QmUoBX)^utTs$m>FhXbaL~}+MGE) zwY+$1?o{RBV=;hEu326hPtseljZlLDZ+SQi&R~P*gtL+^A7Z5MY?jC0)+Y1ssRz=Z~4iMow8M) zaqjRzD~lpMF7w*W4#y0;khAhABJHLP^h8W378CB~X_V?i$B`t`2uF(Cc7{<;A{n>x zj!|~ny%~vD&HxmSGmzs2bl5O#o*pplI+B+n3CC;)9R+$M*FSoC2q!vZEXi1rbXk%J zR!?BO6W%(emsAPYLG|ciD_HO(5;=}o(HXF89d)RXskcF^W!o?zdqY?VZ#{@9 z3Q>@WsxXbJa9@Y}73FI%jG=7A2h%7~VQ-120S~vLr9pm_foRRN5|&|GHo=IFHUGR! zRbU%7<1t;1fszvn1*Ei6h0&j|Ft4QKo?f_~um`b??|bH)z;yhiYGL7)%CglX@)+>|2~fU>XPVrUge< z^>+detv%G#bed86iOw14oT!S - + - + diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs index e98edfb6a..31983cda2 100644 --- a/API/Controllers/ChapterController.cs +++ b/API/Controllers/ChapterController.cs @@ -8,6 +8,7 @@ using API.Data.Repositories; using API.DTOs; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; using API.Extensions; using API.Helpers; using API.Services; diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 9604592c7..5405513d6 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -351,27 +351,6 @@ public class LibraryController : BaseApiController return Ok(); } - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("analyze")] - public ActionResult Analyze(int libraryId) - { - _taskScheduler.AnalyzeFilesForLibrary(libraryId, true); - return Ok(); - } - - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("analyze-multiple")] - public ActionResult AnalyzeMultiple(BulkActionDto dto) - { - foreach (var libraryId in dto.Ids) - { - _taskScheduler.AnalyzeFilesForLibrary(libraryId, dto.Force ?? false); - } - - return Ok(); - } - - ///

/// Copy the library settings (adv tab + optional type) to a set of other libraries. /// diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index e4233e36f..6ec781758 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -9,7 +9,6 @@ using API.DTOs.ReadingLists; using API.Extensions; using API.Helpers; using API.Services; -using API.SignalR; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -39,7 +38,7 @@ public class ReadingListController : BaseApiController /// /// [HttpGet] - public async Task> GetList(int readingListId) + public async Task> GetList(int readingListId) { var readingList = await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, User.GetUserId()); if (readingList == null) @@ -268,7 +267,7 @@ public class ReadingListController : BaseApiController var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist")); var chapterIdsForSeries = - await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId}); + await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync([dto.SeriesId]); // If there are adds, tell tracking this has been modified if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList)) diff --git a/API/Controllers/ScrobblingController.cs b/API/Controllers/ScrobblingController.cs index 19e9d36b2..ba4b09c2d 100644 --- a/API/Controllers/ScrobblingController.cs +++ b/API/Controllers/ScrobblingController.cs @@ -54,7 +54,7 @@ public class ScrobblingController : BaseApiController } /// - /// Get the current user's MAL token & username + /// Get the current user's MAL token and username /// /// [HttpGet("mal-token")] diff --git a/API/DTOs/Account/UpdateUserDto.cs b/API/DTOs/Account/UpdateUserDto.cs index ef19973f5..c40124b7b 100644 --- a/API/DTOs/Account/UpdateUserDto.cs +++ b/API/DTOs/Account/UpdateUserDto.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs.Account; +#nullable enable public record UpdateUserDto { diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs index 634ced4e9..70c77e92d 100644 --- a/API/DTOs/ChapterDto.cs +++ b/API/DTOs/ChapterDto.cs @@ -5,6 +5,7 @@ using API.Entities.Enums; using API.Entities.Interfaces; namespace API.DTOs; +#nullable enable /// /// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying @@ -188,8 +189,8 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage #endregion public string CoverImage { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } + public string PrimaryColor { get; set; } = string.Empty; + public string SecondaryColor { get; set; } = string.Empty; public void ResetColorScape() { diff --git a/API/DTOs/Collection/MalStackDto.cs b/API/DTOs/Collection/MalStackDto.cs index 3144f6c72..d9d902e88 100644 --- a/API/DTOs/Collection/MalStackDto.cs +++ b/API/DTOs/Collection/MalStackDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs.Collection; +#nullable enable /// /// Represents an Interest Stack from MAL diff --git a/API/DTOs/ColorScape.cs b/API/DTOs/ColorScape.cs index 39d1446dd..d95346af7 100644 --- a/API/DTOs/ColorScape.cs +++ b/API/DTOs/ColorScape.cs @@ -1,4 +1,5 @@ namespace API.DTOs; +#nullable enable /// /// A primary and secondary color diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs index 99d4c619d..547bb63a8 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs @@ -1,6 +1,7 @@ using API.DTOs.Scrobbling; namespace API.DTOs.KavitaPlus.ExternalMetadata; +#nullable enable /// /// Used for matching and fetching metadata on a series diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs index 00806aef8..f63fe5a9e 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs @@ -2,6 +2,7 @@ using API.DTOs.Scrobbling; namespace API.DTOs.KavitaPlus.ExternalMetadata; +#nullable enable internal class MatchSeriesRequestDto { diff --git a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs b/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs index 140c41e4c..eedbed2ef 100644 --- a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs +++ b/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs.KavitaPlus.License; +#nullable enable public class EncryptLicenseDto { diff --git a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs b/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs index d5d6847ba..4621810f0 100644 --- a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs +++ b/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs.KavitaPlus.License; +#nullable enable public class UpdateLicenseDto { diff --git a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs b/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs index 4cb8a54ee..bb5a3f20a 100644 --- a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs +++ b/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs @@ -1,4 +1,5 @@ namespace API.DTOs.KavitaPlus.Metadata; +#nullable enable public enum CharacterRole { diff --git a/API/DTOs/MangaFileDto.cs b/API/DTOs/MangaFileDto.cs index 3f0983067..9f2f19a42 100644 --- a/API/DTOs/MangaFileDto.cs +++ b/API/DTOs/MangaFileDto.cs @@ -2,6 +2,7 @@ using API.Entities.Enums; namespace API.DTOs; +#nullable enable public class MangaFileDto { diff --git a/API/DTOs/Person/PersonDto.cs b/API/DTOs/Person/PersonDto.cs index aa0f0680c..511317f2a 100644 --- a/API/DTOs/Person/PersonDto.cs +++ b/API/DTOs/Person/PersonDto.cs @@ -1,4 +1,7 @@ +using System.Runtime.Serialization; + namespace API.DTOs; +#nullable enable public class PersonDto { @@ -6,12 +9,12 @@ public class PersonDto public required string Name { get; set; } public bool CoverImageLocked { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } + public string? PrimaryColor { get; set; } + public string? SecondaryColor { get; set; } public string? CoverImage { get; set; } - public string Description { get; set; } + public string? Description { get; set; } /// /// ASIN for person /// diff --git a/API/DTOs/Person/UpdatePersonDto.cs b/API/DTOs/Person/UpdatePersonDto.cs index 78eb54aaf..d21fb7350 100644 --- a/API/DTOs/Person/UpdatePersonDto.cs +++ b/API/DTOs/Person/UpdatePersonDto.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs; +#nullable enable public class UpdatePersonDto { diff --git a/API/DTOs/Reader/CreatePersonalToCDto.cs b/API/DTOs/Reader/CreatePersonalToCDto.cs index 25526b490..3b80ece4a 100644 --- a/API/DTOs/Reader/CreatePersonalToCDto.cs +++ b/API/DTOs/Reader/CreatePersonalToCDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs.Reader; +#nullable enable public class CreatePersonalToCDto { diff --git a/API/DTOs/Scrobbling/MediaRecommendationDto.cs b/API/DTOs/Scrobbling/MediaRecommendationDto.cs index c83694b2b..3f565296b 100644 --- a/API/DTOs/Scrobbling/MediaRecommendationDto.cs +++ b/API/DTOs/Scrobbling/MediaRecommendationDto.cs @@ -2,6 +2,7 @@ using API.Services.Plus; namespace API.DTOs.Scrobbling; +#nullable enable public record MediaRecommendationDto { diff --git a/API/DTOs/Scrobbling/PlusSeriesDto.cs b/API/DTOs/Scrobbling/PlusSeriesDto.cs index 75e443d2e..c36516837 100644 --- a/API/DTOs/Scrobbling/PlusSeriesDto.cs +++ b/API/DTOs/Scrobbling/PlusSeriesDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs.Scrobbling; +#nullable enable /// /// Represents information about a potential Series for Kavita+ diff --git a/API/DTOs/Scrobbling/ScrobbleEventDto.cs b/API/DTOs/Scrobbling/ScrobbleEventDto.cs index 298e32180..b62c87866 100644 --- a/API/DTOs/Scrobbling/ScrobbleEventDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleEventDto.cs @@ -1,6 +1,7 @@ using System; namespace API.DTOs.Scrobbling; +#nullable enable public class ScrobbleEventDto { diff --git a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs b/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs index afebbaca4..76e77ae2c 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs +++ b/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs @@ -2,6 +2,7 @@ using API.DTOs.Recommendation; namespace API.DTOs.SeriesDetail; +#nullable enable /// /// All the data from Kavita+ for Series Detail diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index 214a357b4..6aa1ecefd 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -79,8 +79,8 @@ public class SeriesDto : IHasReadTimeEstimate, IHasCoverImage #endregion public string? CoverImage { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } + public string PrimaryColor { get; set; } = string.Empty; + public string SecondaryColor { get; set; } = string.Empty; public void ResetColorScape() { diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 45abcc528..2c5e604da 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -3,6 +3,7 @@ using API.Entities.Enums; using API.Services; namespace API.DTOs.Settings; +#nullable enable public class ServerSettingDto { diff --git a/API/DTOs/SideNav/SideNavStreamDto.cs b/API/DTOs/SideNav/SideNavStreamDto.cs index 1f3453611..fdef82a08 100644 --- a/API/DTOs/SideNav/SideNavStreamDto.cs +++ b/API/DTOs/SideNav/SideNavStreamDto.cs @@ -2,6 +2,7 @@ using API.Entities.Enums; namespace API.DTOs.SideNav; +#nullable enable public class SideNavStreamDto { diff --git a/API/DTOs/StandaloneChapterDto.cs b/API/DTOs/StandaloneChapterDto.cs index 6d8b5423d..2f4cd2ee1 100644 --- a/API/DTOs/StandaloneChapterDto.cs +++ b/API/DTOs/StandaloneChapterDto.cs @@ -1,6 +1,7 @@ using API.Entities.Enums; namespace API.DTOs; +#nullable enable /// /// Used on Person Profile page diff --git a/API/DTOs/Statistics/ReadHistoryEvent.cs b/API/DTOs/Statistics/ReadHistoryEvent.cs index 9d3a9436e..496148789 100644 --- a/API/DTOs/Statistics/ReadHistoryEvent.cs +++ b/API/DTOs/Statistics/ReadHistoryEvent.cs @@ -1,6 +1,7 @@ using System; namespace API.DTOs.Statistics; +#nullable enable /// /// Represents a single User's reading event diff --git a/API/DTOs/Statistics/UserReadStatistics.cs b/API/DTOs/Statistics/UserReadStatistics.cs index 5e3f5aa5d..5da4b491e 100644 --- a/API/DTOs/Statistics/UserReadStatistics.cs +++ b/API/DTOs/Statistics/UserReadStatistics.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; namespace API.DTOs.Statistics; +#nullable enable public class UserReadStatistics { diff --git a/API/DTOs/Stats/ServerInfoSlimDto.cs b/API/DTOs/Stats/ServerInfoSlimDto.cs index ef44bb408..0b47fa2f3 100644 --- a/API/DTOs/Stats/ServerInfoSlimDto.cs +++ b/API/DTOs/Stats/ServerInfoSlimDto.cs @@ -1,6 +1,7 @@ using System; namespace API.DTOs.Stats; +#nullable enable /// /// This is just for the Server tab on UI diff --git a/API/DTOs/TachiyomiChapterDto.cs b/API/DTOs/TachiyomiChapterDto.cs index 03e242dfa..ecdd5115c 100644 --- a/API/DTOs/TachiyomiChapterDto.cs +++ b/API/DTOs/TachiyomiChapterDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs; +#nullable enable /// /// This is explicitly for Tachiyomi. Number field was removed in v0.8.0, but Tachiyomi needs it for the hacks. diff --git a/API/DTOs/UpdateSeriesDto.cs b/API/DTOs/UpdateSeriesDto.cs index 52826f9d1..ab4ffcb22 100644 --- a/API/DTOs/UpdateSeriesDto.cs +++ b/API/DTOs/UpdateSeriesDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs; +#nullable enable public class UpdateSeriesDto { diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 577253ada..92b1b6be5 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -5,6 +5,7 @@ using API.Entities.Enums; using API.Entities.Enums.UserPreferences; namespace API.DTOs; +#nullable enable public class UserPreferencesDto { diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index f6413ff6f..8ef22a93b 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -66,8 +66,8 @@ public class VolumeDto : IHasReadTimeEstimate, IHasCoverImage public string CoverImage { get; set; } private bool CoverImageLocked { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } + public string PrimaryColor { get; set; } = string.Empty; + public string SecondaryColor { get; set; } = string.Empty; public void ResetColorScape() { diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 0937be22f..babdada4a 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -12,6 +12,7 @@ using API.Entities.History; using API.Entities.Interfaces; using API.Entities.Metadata; using API.Entities.MetadataMatching; +using API.Entities.Person; using API.Entities.Scrobble; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/API/Data/Repositories/AppUserProgressRepository.cs index 388ca5b7e..78e2dc6c0 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/API/Data/Repositories/AppUserProgressRepository.cs @@ -16,6 +16,7 @@ using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; #nullable enable + public interface IAppUserProgressRepository { void Update(AppUserProgress userProgress); @@ -41,7 +42,7 @@ public interface IAppUserProgressRepository Task UpdateAllProgressThatAreMoreThanChapterPages(); Task> GetUserProgressForChapter(int chapterId, int userId = 0); } -#nullable disable + public class AppUserProgressRepository : IAppUserProgressRepository { private readonly DataContext _context; diff --git a/API/Data/Repositories/CoverDbRepository.cs b/API/Data/Repositories/CoverDbRepository.cs index 3563f9357..ed13493ab 100644 --- a/API/Data/Repositories/CoverDbRepository.cs +++ b/API/Data/Repositories/CoverDbRepository.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using API.DTOs.CoverDb; using API.Entities; +using API.Entities.Person; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index 7492ba5dd..ef9dfa7ec 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -12,6 +12,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface IGenreRepository { diff --git a/API/Data/Repositories/MediaErrorRepository.cs b/API/Data/Repositories/MediaErrorRepository.cs index c2e932d32..0d3cae2ed 100644 --- a/API/Data/Repositories/MediaErrorRepository.cs +++ b/API/Data/Repositories/MediaErrorRepository.cs @@ -9,6 +9,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface IMediaErrorRepository { diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index a100a5046..0fee41557 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using API.DTOs; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; using API.Extensions; using API.Extensions.QueryExtensions; using API.Helpers; @@ -14,6 +15,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface IPersonRepository { diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 3d43c533e..2296a03cc 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable [Flags] public enum ReadingListIncludes diff --git a/API/Data/Repositories/ScrobbleEventRepository.cs b/API/Data/Repositories/ScrobbleEventRepository.cs index 0addd7473..177ddfb96 100644 --- a/API/Data/Repositories/ScrobbleEventRepository.cs +++ b/API/Data/Repositories/ScrobbleEventRepository.cs @@ -12,6 +12,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface IScrobbleRepository { diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index d80d479f4..31ddc22f1 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -39,6 +39,7 @@ using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable [Flags] public enum SeriesIncludes diff --git a/API/Data/Repositories/SettingsRepository.cs b/API/Data/Repositories/SettingsRepository.cs index 433ab6edb..90246e75f 100644 --- a/API/Data/Repositories/SettingsRepository.cs +++ b/API/Data/Repositories/SettingsRepository.cs @@ -13,6 +13,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface ISettingsRepository { diff --git a/API/Data/Repositories/SiteThemeRepository.cs b/API/Data/Repositories/SiteThemeRepository.cs index 2498dfa60..33517e846 100644 --- a/API/Data/Repositories/SiteThemeRepository.cs +++ b/API/Data/Repositories/SiteThemeRepository.cs @@ -8,6 +8,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface ISiteThemeRepository { diff --git a/API/Data/Repositories/TagRepository.cs b/API/Data/Repositories/TagRepository.cs index 4a7fbf4ab..c4f189957 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/API/Data/Repositories/TagRepository.cs @@ -11,6 +11,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface ITagRepository { diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 0e90b617f..3a46ad2a1 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable [Flags] public enum AppUserIncludes diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs index 0dfbd6393..cb0783a89 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/API/Data/Repositories/VolumeRepository.cs @@ -15,6 +15,7 @@ using Kavita.Common; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable [Flags] public enum VolumeIncludes diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index a00f315c3..fc709961b 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using API.Entities.Enums; using API.Entities.Interfaces; +using API.Entities.Person; using API.Extensions; using API.Services.Tasks.Scanner.Parser; diff --git a/API/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs index 40d1b10a8..a8d943b2d 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -12,7 +12,7 @@ public enum LibraryType /// /// Uses Comic regex for filename parsing /// - [Description("Comic")] + [Description("Comic (Legacy)")] Comic = 1, /// /// Uses Manga regex for filename parsing also uses epub metadata @@ -30,8 +30,8 @@ public enum LibraryType [Description("Light Novel")] LightNovel = 4, /// - /// Uses Comic regex for filename parsing, uses Comic Vine type of Parsing. Will replace Comic type in future + /// Uses Comic regex for filename parsing, uses Comic Vine type of Parsing /// - [Description("Comic (Comic Vine)")] + [Description("Comic")] ComicVine = 5, } diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/API/Entities/Metadata/SeriesMetadata.cs index b3e543315..046c07efa 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/API/Entities/Metadata/SeriesMetadata.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using API.Entities.Enums; using API.Entities.Interfaces; +using API.Entities.Person; using Microsoft.EntityFrameworkCore; namespace API.Entities.Metadata; diff --git a/API/Entities/Person/ChapterPeople.cs b/API/Entities/Person/ChapterPeople.cs index cc0802782..15da3994d 100644 --- a/API/Entities/Person/ChapterPeople.cs +++ b/API/Entities/Person/ChapterPeople.cs @@ -1,6 +1,6 @@ using API.Entities.Enums; -namespace API.Entities; +namespace API.Entities.Person; public class ChapterPeople { diff --git a/API/Entities/Person/Person.cs b/API/Entities/Person/Person.cs index 13f8a9b77..8eed08f5c 100644 --- a/API/Entities/Person/Person.cs +++ b/API/Entities/Person/Person.cs @@ -1,10 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Services.Plus; -namespace API.Entities; +namespace API.Entities.Person; public class Person : IHasCoverImage { diff --git a/API/Entities/Person/SeriesMetadataPeople.cs b/API/Entities/Person/SeriesMetadataPeople.cs index 1f5dd2f5b..caea10cd6 100644 --- a/API/Entities/Person/SeriesMetadataPeople.cs +++ b/API/Entities/Person/SeriesMetadataPeople.cs @@ -1,8 +1,7 @@ using API.Entities.Enums; using API.Entities.Metadata; -using API.Services.Plus; -namespace API.Entities; +namespace API.Entities.Person; public class SeriesMetadataPeople { diff --git a/API/Extensions/FlurlExtensions.cs b/API/Extensions/FlurlExtensions.cs index efd805045..62d8543b6 100644 --- a/API/Extensions/FlurlExtensions.cs +++ b/API/Extensions/FlurlExtensions.cs @@ -4,6 +4,7 @@ using Kavita.Common; using Kavita.Common.EnvironmentInfo; namespace API.Extensions; +#nullable enable public static class FlurlExtensions { diff --git a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs b/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs index f6606026b..cc40491d0 100644 --- a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs @@ -4,7 +4,7 @@ using API.Data.Misc; using API.Data.Repositories; using API.Entities; using API.Entities.Metadata; -using AutoMapper.QueryableExtensions; +using API.Entities.Person; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions.Filtering; diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index ebc233056..fc3314f58 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using API.Data.Misc; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; namespace API.Extensions.QueryExtensions; #nullable enable diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 3d9de9ea7..69ed884fd 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -30,6 +30,7 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Entities.MetadataMatching; +using API.Entities.Person; using API.Entities.Scrobble; using API.Extensions.QueryExtensions.Filtering; using API.Helpers.Converters; diff --git a/API/Helpers/Builders/ChapterBuilder.cs b/API/Helpers/Builders/ChapterBuilder.cs index 4d09a7abf..348696820 100644 --- a/API/Helpers/Builders/ChapterBuilder.cs +++ b/API/Helpers/Builders/ChapterBuilder.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; using API.Services.Tasks.Scanner.Parser; namespace API.Helpers.Builders; diff --git a/API/Helpers/Builders/PersonBuilder.cs b/API/Helpers/Builders/PersonBuilder.cs index 2bbdfa744..492d79e17 100644 --- a/API/Helpers/Builders/PersonBuilder.cs +++ b/API/Helpers/Builders/PersonBuilder.cs @@ -2,6 +2,7 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; using API.Extensions; namespace API.Helpers.Builders; diff --git a/API/Helpers/Builders/SeriesMetadataBuilder.cs b/API/Helpers/Builders/SeriesMetadataBuilder.cs index b94e3e4c3..8ceb16d95 100644 --- a/API/Helpers/Builders/SeriesMetadataBuilder.cs +++ b/API/Helpers/Builders/SeriesMetadataBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; namespace API.Helpers.Builders; diff --git a/API/Helpers/PersonHelper.cs b/API/Helpers/PersonHelper.cs index 193513453..fe437daa8 100644 --- a/API/Helpers/PersonHelper.cs +++ b/API/Helpers/PersonHelper.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -7,6 +6,7 @@ using API.DTOs; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; using API.Extensions; using API.Helpers.Builders; diff --git a/API/Program.cs b/API/Program.cs index 425b60654..77fac9e49 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -56,9 +56,6 @@ public class Program Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty); } - Configuration.KavitaPlusApiUrl = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development - ? "http://localhost:5020" : "https://plus.kavitareader.com"; - try { var host = CreateHostBuilder(args).Build(); diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 30d40b72b..f8f1ef222 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -73,12 +73,27 @@ public class BookService : IBookService private const string BookApiUrl = "book-resources?file="; private readonly PdfComicInfoExtractor _pdfComicInfoExtractor; + /// + /// Setup the most lenient book parsing options possible as people have some really bad epubs + /// public static readonly EpubReaderOptions BookReaderOptions = new() { PackageReaderOptions = new PackageReaderOptions { IgnoreMissingToc = true, - SkipInvalidManifestItems = true + SkipInvalidManifestItems = true, + }, + Epub2NcxReaderOptions = new Epub2NcxReaderOptions + { + IgnoreMissingContentForNavigationPoints = true + }, + SpineReaderOptions = new SpineReaderOptions + { + IgnoreMissingManifestItems = true + }, + BookCoverReaderOptions = new BookCoverReaderOptions + { + Epub2MetadataIgnoreMissingManifestItem = true } }; diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 22bc7ff1b..5fd6e68d4 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -11,6 +11,7 @@ using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; using API.Extensions; using API.Helpers; using API.Helpers.Builders; @@ -73,7 +74,7 @@ public class SeriesService : ISeriesService } /// - /// Returns the first chapter for a series to extract metadata from (ie Summary, etc) + /// Returns the first chapter for a series to extract metadata from (ie Summary, etc.) /// /// The full series with all volumes and chapters on it /// @@ -324,7 +325,7 @@ public class SeriesService : ISeriesService await _unitOfWork.CommitAsync(); - // Trigger code to cleanup tags, collections, people, etc + // Trigger code to clean up tags, collections, people, etc try { await _taskScheduler.CleanupDbEntries(); @@ -915,19 +916,19 @@ public class SeriesService : ISeriesService // Calculate the time differences between consecutive chapters var timeDifferences = new List(); DateTime? previousChapterTime = null; - foreach (var chapter in chapters) + foreach (var chapterCreatedUtc in chapters.Select(c => c.CreatedUtc)) { - if (previousChapterTime.HasValue && (chapter.CreatedUtc - previousChapterTime.Value) <= TimeSpan.FromHours(1)) + if (previousChapterTime.HasValue && (chapterCreatedUtc - previousChapterTime.Value) <= TimeSpan.FromHours(1)) { continue; // Skip this chapter if it's within an hour of the previous one } - if ((chapter.CreatedUtc - previousChapterTime ?? TimeSpan.Zero) != TimeSpan.Zero) + if ((chapterCreatedUtc - previousChapterTime ?? TimeSpan.Zero) != TimeSpan.Zero) { - timeDifferences.Add(chapter.CreatedUtc - previousChapterTime ?? TimeSpan.Zero); + timeDifferences.Add(chapterCreatedUtc - previousChapterTime ?? TimeSpan.Zero); } - previousChapterTime = chapter.CreatedUtc; + previousChapterTime = chapterCreatedUtc; } if (timeDifferences.Count < minimumTimeDeltas) diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 46ba18abf..c3f1a00e5 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -34,7 +34,6 @@ public interface ITaskScheduler void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false, bool forceColorscape = false); Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false); - void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false); void CancelStatsTasks(); Task RunStatCollection(); void CovertAllCoversToEncoding(); @@ -267,11 +266,6 @@ public class TaskScheduler : ITaskScheduler RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), RecurringJobOptions); } - public void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false) - { - _logger.LogInformation("Enqueuing library file analysis for: {LibraryId}", libraryId); - BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanLibrary(libraryId, forceUpdate)); - } /// /// Upon cancelling stat, we do report to the Stat service that we are no longer going to be reporting diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/API/Services/Tasks/Metadata/CoverDbService.cs index 6dfb414df..59214e116 100644 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ b/API/Services/Tasks/Metadata/CoverDbService.cs @@ -8,6 +8,7 @@ using API.Data; using API.Data.Repositories; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; using API.Extensions; using API.SignalR; using EasyCaching.Core; diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 992dcf108..163954ba7 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -708,7 +708,7 @@ public static partial class Parser return HasSpecialMarker(filePath); } - public static string ParseMangaSeries(string filename) + private static string ParseMangaSeries(string filename) { foreach (var regex in MangaSeriesRegex) { @@ -716,6 +716,7 @@ public static partial class Parser var group = matches .Select(match => match.Groups["Series"]) .FirstOrDefault(group => group.Success && group != Match.Empty); + if (group != null) { return CleanTitle(group.Value); diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 5e1a81906..61199d106 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -11,6 +11,7 @@ using API.Data.Repositories; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; using API.Extensions; using API.Helpers; using API.Helpers.Builders; diff --git a/API/Services/Tasks/SiteThemeService.cs b/API/Services/Tasks/SiteThemeService.cs index ab5f4ae2b..3dca14ab9 100644 --- a/API/Services/Tasks/SiteThemeService.cs +++ b/API/Services/Tasks/SiteThemeService.cs @@ -302,7 +302,8 @@ public class ThemeService : IThemeService var existingThemes = _directoryService.ScanFiles(_directoryService.SiteThemeDirectory, string.Empty); if (existingThemes.Any(f => Path.GetFileName(f) == dto.CssFile)) { - throw new KavitaException("Cannot download file, file already on disk"); + // This can happen if you delete then immediately download (to refresh). We should just delete the old file and download. Users can always rollback their version with github directly + _directoryService.DeleteFiles(existingThemes.Where(f => Path.GetFileName(f) == dto.CssFile)); } var finalLocation = await DownloadSiteTheme(dto); diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 43b3d9c6a..119571ffc 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -346,7 +346,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc <= CacheDuration) { var cachedData = await File.ReadAllTextAsync(_cacheFilePath); - return System.Text.Json.JsonSerializer.Deserialize>(cachedData); + return JsonSerializer.Deserialize>(cachedData); } return null; diff --git a/API/Startup.cs b/API/Startup.cs index 262d3c95a..c650e018a 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -41,6 +41,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; using Serilog; +using Swashbuckle.AspNetCore.SwaggerGen; using TaskScheduler = API.Services.TaskScheduler; namespace API; @@ -138,8 +139,8 @@ public class Startup c.SwaggerDoc("v1", new OpenApiInfo { Version = "3.1.0", - Title = "Kavita", - Description = $"Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v{BuildInfo.Version.ToString()}", + Title = $"Kavita (v{BuildInfo.Version})", + Description = $"Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v{BuildInfo.Version}", License = new OpenApiLicense { Name = "GPL-3.0", diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index 00ec84d06..f2d64cde6 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -16,8 +16,9 @@ public static class Configuration public const long DefaultCacheMemory = 75; private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); - public static string KavitaPlusApiUrl = "https://plus.kavitareader.com"; - public static string StatsApiUrl = "https://stats.kavitareader.com"; + public static readonly string KavitaPlusApiUrl = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development + ? "http://localhost:5020" : "https://plus.kavitareader.com"; + public static readonly string StatsApiUrl = "https://stats.kavitareader.com"; public static int Port { @@ -315,6 +316,7 @@ public static class Configuration { public string TokenKey { get; set; } // ReSharper disable once MemberHidesStaticFromOuterClass +#pragma warning disable S3218 public int Port { get; set; } = DefaultHttpPort; // ReSharper disable once MemberHidesStaticFromOuterClass public string IpAddresses { get; set; } = string.Empty; @@ -323,6 +325,7 @@ public static class Configuration // ReSharper disable once MemberHidesStaticFromOuterClass public long Cache { get; set; } = DefaultCacheMemory; // ReSharper disable once MemberHidesStaticFromOuterClass - public bool AllowIFraming { get; set; } = false; + public bool AllowIFraming { get; init; } = false; +#pragma warning restore S3218 } } diff --git a/Kavita.Common/Helpers/CronHelper.cs b/Kavita.Common/Helpers/CronHelper.cs index 77a4e934e..0b40113ce 100644 --- a/Kavita.Common/Helpers/CronHelper.cs +++ b/Kavita.Common/Helpers/CronHelper.cs @@ -13,7 +13,7 @@ public static class CronHelper CronExpression.Parse(cronExpression); return true; } - catch (Exception ex) + catch (Exception) { /* Swallow */ return false; diff --git a/Kavita.Common/Helpers/FlurlConfiguration.cs b/Kavita.Common/Helpers/FlurlConfiguration.cs index 0003546d4..b80dff8d9 100644 --- a/Kavita.Common/Helpers/FlurlConfiguration.cs +++ b/Kavita.Common/Helpers/FlurlConfiguration.cs @@ -28,7 +28,9 @@ public static class FlurlConfiguration if (ConfiguredClients.Contains(host)) return; FlurlHttp.ConfigureClientForUrl(url).ConfigureInnerHandler(cli => +#pragma warning disable S4830 cli.ServerCertificateCustomValidationCallback = (_, _, _, _) => true); +#pragma warning restore S4830 ConfiguredClients.Add(host); } diff --git a/TestData b/TestData deleted file mode 160000 index 4f5750025..000000000 --- a/TestData +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4f5750025a1c0b48cd72eaa6f1b61642c41f147f diff --git a/UI/Web/src/app/_helpers/browser.ts b/UI/Web/src/app/_helpers/browser.ts new file mode 100644 index 000000000..4d92e207c --- /dev/null +++ b/UI/Web/src/app/_helpers/browser.ts @@ -0,0 +1,62 @@ +export const isSafari = [ + 'iPad Simulator', + 'iPhone Simulator', + 'iPod Simulator', + 'iPad', + 'iPhone', + 'iPod' + ].includes(navigator.platform) + // iPad on iOS 13 detection + || (navigator.userAgent.includes("Mac") && "ontouchend" in document); + +/** + * Represents a Version for a browser + */ +export class Version { + major: number; + minor: number; + patch: number; + + constructor(major: number, minor: number, patch: number) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + isLessThan(other: Version): boolean { + if (this.major < other.major) return true; + if (this.major > other.major) return false; + if (this.minor < other.minor) return true; + if (this.minor > other.minor) return false; + return this.patch < other.patch; + } + + isGreaterThan(other: Version): boolean { + if (this.major > other.major) return true; + if (this.major < other.major) return false; + if (this.minor > other.minor) return true; + if (this.minor < other.minor) return false; + return this.patch > other.patch; + } + + isEqualTo(other: Version): boolean { + return ( + this.major === other.major && + this.minor === other.minor && + this.patch === other.patch + ); + } +} + + +export const getIosVersion = () => { + const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/); + if (match) { + const major = parseInt(match[1], 10); + const minor = parseInt(match[2], 10); + const patch = parseInt(match[3] || '0', 10); + + return new Version(major, minor, patch); + } + return null; +} diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index 87ffb56c4..74cabc658 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -1,5 +1,4 @@ import {FileTypeGroup} from "./file-type-group.enum"; -import {IHasCover} from "../common/i-has-cover"; export enum LibraryType { Manga = 0, @@ -10,6 +9,8 @@ export enum LibraryType { ComicVine = 5 } +export const allLibraryTypes = [LibraryType.Manga, LibraryType.ComicVine, LibraryType.Comic, LibraryType.Book, LibraryType.LightNovel, LibraryType.Images]; + export interface Library { id: number; name: string; diff --git a/UI/Web/src/app/_pipes/library-type.pipe.ts b/UI/Web/src/app/_pipes/library-type.pipe.ts index 74a62647f..1881b64d5 100644 --- a/UI/Web/src/app/_pipes/library-type.pipe.ts +++ b/UI/Web/src/app/_pipes/library-type.pipe.ts @@ -11,7 +11,7 @@ import {TranslocoService} from "@jsverse/transloco"; }) export class LibraryTypePipe implements PipeTransform { - translocoService = inject(TranslocoService); + private readonly translocoService = inject(TranslocoService); transform(libraryType: LibraryType): string { switch (libraryType) { case LibraryType.Book: diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 9afa400a0..025e5cf29 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -23,6 +23,7 @@ import {Volume} from "../_models/volume"; import {UtilityService} from "../shared/_services/utility.service"; import {translate} from "@jsverse/transloco"; import {ToastrService} from "ngx-toastr"; +import {getIosVersion, isSafari, Version} from "../_helpers/browser"; export const CHAPTER_ID_DOESNT_EXIST = -1; @@ -46,7 +47,8 @@ export class ReaderService { // Override background color for reader and restore it onDestroy private originalBodyColor!: string; - private noSleep = new NoSleep(); + + private noSleep: NoSleep = new NoSleep(); constructor(private httpClient: HttpClient, @Inject(DOCUMENT) private document: Document) { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => { @@ -56,17 +58,18 @@ export class ReaderService { }); } + enableWakeLock(element?: Element | Document) { // Enable wake lock. // (must be wrapped in a user input event handler e.g. a mouse or touch handler) if (!element) element = this.document; - const enableNoSleepHandler = () => { + const enableNoSleepHandler = async () => { element!.removeEventListener('click', enableNoSleepHandler, false); element!.removeEventListener('touchmove', enableNoSleepHandler, false); element!.removeEventListener('mousemove', enableNoSleepHandler, false); - this.noSleep!.enable(); + await this.noSleep.enable(); }; // Enable wake lock. diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 6ee0cd8b7..4a71e836e 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -41,10 +41,6 @@ export class ServerService { return this.http.post(this.baseUrl + 'server/backup-db', {}); } - analyzeFiles() { - return this.http.post(this.baseUrl + 'server/analyze-files', {}); - } - syncThemes() { return this.http.post(this.baseUrl + 'server/sync-themes', {}); } @@ -58,10 +54,6 @@ export class ServerService { .pipe(map(r => parseInt(r, 10))); } - checkForUpdates() { - return this.http.get(this.baseUrl + 'server/check-for-updates', {}); - } - getChangelog(count: number = 0) { return this.http.get(this.baseUrl + 'server/changelog?count=' + count, {}); } diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html index 7ea7ccf15..711141195 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html @@ -41,7 +41,7 @@

{{t('genres-title')}}

- + {{item.title}} @@ -52,7 +52,7 @@

{{t('tags-title')}}

- + {{item.title}} diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts index 6e0a8915b..08889ab3c 100644 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts +++ b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts @@ -37,7 +37,7 @@ import {DownloadService} from "../../shared/_services/download.service"; import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; import {TypeaheadComponent} from "../../typeahead/_components/typeahead.component"; import {forkJoin, Observable, of, tap} from "rxjs"; -import {map} from "rxjs/operators"; +import {map, switchMap} from "rxjs/operators"; import {EntityTitleComponent} from "../../cards/entity-title/entity-title.component"; import {SettingButtonComponent} from "../../settings/_components/setting-button/setting-button.component"; import {CoverImageChooserComponent} from "../../cards/cover-image-chooser/cover-image-chooser.component"; @@ -212,11 +212,13 @@ export class EditChapterModalComponent implements OnInit { this.editForm.addControl('coverImageIndex', new FormControl(0, [])); this.editForm.addControl('coverImageLocked', new FormControl(this.chapter.coverImageLocked, [])); - this.metadataService.getAllValidLanguages().subscribe(validLanguages => { - this.validLanguages = validLanguages; - this.setupLanguageTypeahead(); - this.cdRef.markForCheck(); - }); + this.metadataService.getAllValidLanguages().pipe( + tap(validLanguages => { + this.validLanguages = validLanguages; + this.cdRef.markForCheck(); + }), + switchMap(_ => this.setupLanguageTypeahead()) + ).subscribe(); this.metadataService.getAllAgeRatings().subscribe(ratings => { this.ageRatings = ratings; diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html index 705f320bb..9255f2616 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html @@ -90,7 +90,7 @@
@if(settingsForm.get('enableSsl'); as formControl) { - +
diff --git a/UI/Web/src/app/admin/manage-system/manage-system.component.html b/UI/Web/src/app/admin/manage-system/manage-system.component.html index c0a613d8e..7792994c4 100644 --- a/UI/Web/src/app/admin/manage-system/manage-system.component.html +++ b/UI/Web/src/app/admin/manage-system/manage-system.component.html @@ -1,77 +1,72 @@ -
- - @if (serverInfo) { -
-

{{t('title')}}

- -
-
-
{{t('version-title')}}
-
{{serverInfo.kavitaVersion}}
-
- -
-
{{t('installId-title')}}
-
{{serverInfo.installId}}
-
-
- -
-
-
{{t('first-install-version-title')}}
-
{{serverInfo.firstInstallVersion}}
-
- -
-
{{t('first-install-date-title')}}
-
{{serverInfo.firstInstallDate | date:'shortDate'}}
-
-
-
- -
- } - + @if (serverInfo) {
-

{{t('more-info-title')}}

+

{{t('title')}}

+
-
{{t('home-page-title')}}
-
+
+
{{t('version-title')}}
+
{{serverInfo.kavitaVersion}}
+
+ +
+
{{t('installId-title')}}
+
{{serverInfo.installId}}
+
+
-
{{t('wiki-title')}}
- -
-
-
{{t('discord-title')}}
- -
-
-
{{t('donations-title')}}
- -
-
-
{{t('source-title')}}
- -
-
-
{{t('localization-title')}}
- -
-
-
{{t('feature-request-title')}}
- +
+
{{t('first-install-version-title')}}
+
{{serverInfo.firstInstallVersion}}
+
+ +
+
{{t('first-install-date-title')}}
+
{{serverInfo.firstInstallDate | date:'shortDate'}}
+
+ } -
-

{{t('updates-title')}}

- +
+

{{t('more-info-title')}}

+
+
{{t('home-page-title')}}
+ +
+
+
{{t('wiki-title')}}
+ +
+
+
{{t('discord-title')}}
+ +
+
+
{{t('donations-title')}}
+ +
+
+
{{t('source-title')}}
+ +
+
+
{{t('localization-title')}}
+ +
+
+
{{t('feature-request-title')}}
+
-
+
+ +
+

{{t('updates-title')}}

+ +
diff --git a/UI/Web/src/app/admin/manage-system/manage-system.component.ts b/UI/Web/src/app/admin/manage-system/manage-system.component.ts index a5042fb6d..e1f140266 100644 --- a/UI/Web/src/app/admin/manage-system/manage-system.component.ts +++ b/UI/Web/src/app/admin/manage-system/manage-system.component.ts @@ -1,11 +1,9 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; import {ServerService} from 'src/app/_services/server.service'; import {ServerInfoSlim} from '../_models/server-info'; -import {DatePipe, NgIf} from '@angular/common'; +import {DatePipe} from '@angular/common'; import {TranslocoDirective} from "@jsverse/transloco"; import {ChangelogComponent} from "../../announcements/_components/changelog/changelog.component"; -import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; -import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; @Component({ selector: 'app-manage-system', @@ -13,7 +11,7 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; styleUrls: ['./manage-system.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, TranslocoDirective, ChangelogComponent, DefaultDatePipe, DefaultValuePipe, DatePipe] + imports: [TranslocoDirective, ChangelogComponent, DatePipe] }) export class ManageSystemComponent implements OnInit { diff --git a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html index 554ff34a1..443047c2f 100644 --- a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html +++ b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html @@ -1,194 +1,192 @@ -
- @if (serverSettings) { -
+ @if (serverSettings) { + -

{{t('title')}}

- -
- @if (settingsForm.get('taskScan'); as formControl) { - - - @if (formControl.value === customOption) { - {{t(formControl.value)}} ({{settingsForm.get('taskScanCustom')?.value}}) - } @else { - {{t(formControl.value)}} +

{{t('title')}}

+ +
+ @if (settingsForm.get('taskScan'); as formControl) { + + + @if (formControl.value === customOption) { + {{t(formControl.value)}} ({{settingsForm.get('taskScanCustom')?.value}}) + } @else { + {{t(formControl.value)}} + } + + + + - + + @if (settingsForm.dirty || !settingsForm.untouched) { +
+ @if(settingsForm.get('taskScanCustom')?.errors?.required) { +
{{t('required')}}
+ } + @if(settingsForm.get('taskScanCustom')?.errors?.invalidCron) { +
{{t('cron-notation')}}
+ } +
} - +
+ } +
+
+ } +
- @if (formControl.value === customOption) { -
- - +
+ @if (settingsForm.get('taskBackup'); as formControl) { + + + @if (formControl.value === customOption) { + {{t(formControl.value)}} ({{settingsForm.get('taskBackupCustom')?.value}}) + } @else { + {{t(formControl.value)}} + } + + - @if (settingsForm.dirty || !settingsForm.untouched) { -
- @if(settingsForm.get('taskScanCustom')?.errors?.required) { -
{{t('required')}}
- } - @if(settingsForm.get('taskScanCustom')?.errors?.invalidCron) { -
{{t('cron-notation')}}
- } -
- } -
+ -
- @if (settingsForm.get('taskBackup'); as formControl) { - - - @if (formControl.value === customOption) { - {{t(formControl.value)}} ({{settingsForm.get('taskBackupCustom')?.value}}) - } @else { - {{t(formControl.value)}} - } - - + @if (formControl.value === customOption) { +
+ + - +
+ } +
+
+ } +
- @if (formControl.value === customOption) { -
- - - @if (settingsForm.dirty || !settingsForm.untouched) { -
- @if(settingsForm.get('taskBackupCustom')?.errors?.required) { -
{{t('required')}}
- } - @if(settingsForm.get('taskBackupCustom')?.errors?.invalidCron) { -
{{t('cron-notation')}}
- } -
- } -
+
+ @if (settingsForm.get('taskCleanup'); as formControl) { + + + @if (formControl.value === customOption) { + {{t(formControl.value)}} ({{settingsForm.get('taskCleanupCustom')?.value}}) + } @else { + {{t(formControl.value)}} + } + + + + + @if (formControl.value === customOption) { +
+ + -
- @if (settingsForm.get('taskCleanup'); as formControl) { - - - @if (formControl.value === customOption) { - {{t(formControl.value)}} ({{settingsForm.get('taskCleanupCustom')?.value}}) - } @else { - {{t(formControl.value)}} - } - - - - +
+ } + + + } +
+ - @if (formControl.value === customOption) { -
- - +
- @if (settingsForm.get('taskCleanupCustom')?.invalid) { -
- @if(settingsForm.get('taskCleanupCustom')?.errors?.required) { -
{{t('required')}}
- } - @if(settingsForm.get('taskCleanupCustom')?.errors?.invalidCron) { -
{{t('cron-notation')}}
- } -
- } -
- } -
-
- } -
- +

{{t('adhoc-tasks-title')}}

-
+ @for(task of adhocTasks; track task.name; let idx = $index) { +
+ + + +
+ } -

{{t('adhoc-tasks-title')}}

+
- @for(task of adhocTasks; track task.name; let idx = $index) { -
- - - -
- } +

{{t('recurring-tasks-title')}}

+ -
- -

{{t('recurring-tasks-title')}}

- - - - - {{t('job-title-header')}} - - - {{item.title | titlecase}} - - + + + {{t('job-title-header')}} + + + {{item.title | titlecase}} + + - - - {{t('last-executed-header')}} - - - {{item.lastExecutionUtc | utcToLocalTime | defaultValue }} - - + + + {{t('last-executed-header')}} + + + {{item.lastExecutionUtc | utcToLocalTime | defaultValue }} + + - - - {{t('cron-header')}} - - - {{item.cron}} - - - - - } -
+ + + {{t('cron-header')}} + + + {{item.cron}} + + + + + }
diff --git a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts index 2a2fbf0f5..df11cad85 100644 --- a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts +++ b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts @@ -106,13 +106,6 @@ export class ManageTasksSettingsComponent implements OnInit { api: defer(() => of(this.downloadService.download('logs', undefined))), successMessage: '' }, - // TODO: Remove this in v0.9. Users should have all updated by then - { - name: 'analyze-files-task', - description: 'analyze-files-task-desc', - api: this.serverService.analyzeFiles(), - successMessage: 'analyze-files-task-success' - }, { name: 'sync-themes-task', description: 'sync-themes-task-desc', diff --git a/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html b/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html index 982a77e84..ad856dde6 100644 --- a/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html +++ b/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html @@ -1,52 +1,49 @@ -
-

{{t('description')}}

+

{{t('description')}}

- + - - - {{t('username-header')}} - - - {{item.username}} - - + + + {{t('username-header')}} + + + {{item.username}} + + - - - {{t('anilist-header')}} - - - @if (item.isAniListTokenSet) { - {{t('token-set-label')}} {{t('expires-label', {date: item.aniListValidUntilUtc | utcToLocalTime})}} - } @else { - {{null | defaultValue}} - } - - + + + {{t('anilist-header')}} + + + @if (item.isAniListTokenSet) { + {{t('token-set-label')}} {{t('expires-label', {date: item.aniListValidUntilUtc | utcToLocalTime})}} + } @else { + {{null | defaultValue}} + } + + - - - {{t('mal-header')}} - - - @if (item.isMalTokenSet) { - {{t('token-set-label')}} - } @else { - {{null | defaultValue}} - } - - - - -
+ + + {{t('mal-header')}} + + + @if (item.isMalTokenSet) { + {{t('token-set-label')}} + } @else { + {{null | defaultValue}} + } + + +
diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index f859b33d7..6c1f83952 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -18,7 +18,7 @@ import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import {forkJoin, Observable, of, tap} from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; import { TypeaheadSettings } from 'src/app/typeahead/_models/typeahead-settings'; import {Chapter, LooseLeafOrDefaultNumber, SpecialVolumeNumber} from 'src/app/_models/chapter'; @@ -238,10 +238,7 @@ export class EditSeriesModalComponent implements OnInit { this.cdRef.markForCheck(); }); - this.metadataService.getAllValidLanguages().subscribe(validLanguages => { - this.validLanguages = validLanguages; - this.cdRef.markForCheck(); - }); + this.seriesService.getMetadata(this.series.id).subscribe(metadata => { if (metadata) { @@ -437,30 +434,41 @@ export class EditSeriesModalComponent implements OnInit { } setupLanguageTypeahead() { - this.languageSettings.minCharacters = 0; - this.languageSettings.multiple = false; - this.languageSettings.id = 'language'; - this.languageSettings.unique = true; - this.languageSettings.showLocked = true; - this.languageSettings.addIfNonExisting = false; - this.languageSettings.compareFn = (options: Language[], filter: string) => { - return options.filter(m => this.utilityService.filter(m.title, filter)); - } - this.languageSettings.compareFnForAdd = (options: Language[], filter: string) => { - return options.filter(m => this.utilityService.filterMatches(m.title, filter)); - } - this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages) - .pipe(map(items => this.languageSettings.compareFn(items, filter))); - this.languageSettings.selectionCompareFn = (a: Language, b: Language) => { - return a.isoCode == b.isoCode; - } - const l = this.validLanguages.find(l => l.isoCode === this.metadata.language); - if (l !== undefined) { - this.languageSettings.savedData = l; - } - return of(true); + return this.metadataService.getAllValidLanguages() + .pipe( + tap(validLanguages => { + this.validLanguages = validLanguages; + + this.languageSettings.minCharacters = 0; + this.languageSettings.multiple = false; + this.languageSettings.id = 'language'; + this.languageSettings.unique = true; + this.languageSettings.showLocked = true; + this.languageSettings.addIfNonExisting = false; + this.languageSettings.compareFn = (options: Language[], filter: string) => { + return options.filter(m => this.utilityService.filter(m.title, filter)); + } + this.languageSettings.compareFnForAdd = (options: Language[], filter: string) => { + return options.filter(m => this.utilityService.filterMatches(m.title, filter)); + } + this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages) + .pipe(map(items => this.languageSettings.compareFn(items, filter))); + + this.languageSettings.selectionCompareFn = (a: Language, b: Language) => { + return a.isoCode == b.isoCode; + } + + const l = this.validLanguages.find(l => l.isoCode === this.metadata.language); + if (l !== undefined) { + this.languageSettings.savedData = l; + } + + this.cdRef.markForCheck(); + }), + switchMap(_ => of(true)) + ); } setupPersonTypeahead() { diff --git a/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html b/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html index abd2b7260..77b91b77c 100644 --- a/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html +++ b/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html @@ -1,10 +1,6 @@

{{t('description')}}

- @if (stacks.length === 0) { -

{{t('nothing-found')}}

- } -
    @for(stack of stacks; track stack.url) {
  • @@ -21,6 +17,8 @@ } @empty { @if (isLoading) { + } @else { +

    {{t('nothing-found')}}

    } }
diff --git a/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts index 7257cd55a..87aae37b5 100644 --- a/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts +++ b/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts @@ -22,6 +22,7 @@ import { MangaReaderService } from '../../_service/manga-reader.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import { SafeStylePipe } from '../../../_pipes/safe-style.pipe'; import { NgClass, AsyncPipe } from '@angular/common'; +import {isSafari} from "../../../_helpers/browser"; const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRightToLeft]; @@ -35,13 +36,21 @@ const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRigh }) export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRenderer { + protected readonly isSafari = isSafari; + + private readonly destroyRef = inject(DestroyRef); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly mangaReaderService = inject(MangaReaderService); + private readonly readerService = inject(ReaderService); + + @Input({required: true}) readerSettings$!: Observable; @Input({required: true}) image$!: Observable; @Input({required: true}) bookmark$!: Observable; @Input({required: true}) showClickOverlay$!: Observable; @Input() imageFit$!: Observable; @Output() imageHeight: EventEmitter = new EventEmitter(); - private readonly destroyRef = inject(DestroyRef); + @ViewChild('content') canvas: ElementRef | undefined; private ctx!: CanvasRenderingContext2D; @@ -67,7 +76,6 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRend - constructor(private readonly cdRef: ChangeDetectorRef, private mangaReaderService: MangaReaderService, private readerService: ReaderService) { } ngOnInit(): void { this.readerSettings$.pipe(takeUntilDestroyed(this.destroyRef), tap((value: ReaderSetting) => { @@ -250,21 +258,11 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRend setCanvasSize() { if (this.canvasImage == null) return; if (!this.ctx || !this.canvas) { return; } - const isSafari = [ - 'iPad Simulator', - 'iPhone Simulator', - 'iPod Simulator', - 'iPad', - 'iPhone', - 'iPod' - ].includes(navigator.platform) - // iPad on iOS 13 detection - || (navigator.userAgent.includes("Mac") && "ontouchend" in document); - const canvasLimit = isSafari ? 16_777_216 : 124_992_400; + const canvasLimit = this.isSafari ? 16_777_216 : 124_992_400; const needsScaling = this.canvasImage.width * this.canvasImage.height > canvasLimit; if (needsScaling) { - this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384; - this.canvas.nativeElement.height = isSafari ? 4_096 : 16_384; + this.canvas.nativeElement.width = this.isSafari ? 4_096 : 16_384; + this.canvas.nativeElement.height = this.isSafari ? 4_096 : 16_384; } else { this.canvas.nativeElement.width = this.canvasImage.width; this.canvas.nativeElement.height = this.canvasImage.height; diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index bdb49ee89..8fabaa35e 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -1,11 +1,11 @@ - - -
- -
- - + @if (toggleService.toggleState$ | async; as isOpen) { + @if (utilityService.getActiveBreakpoint(); as activeBreakpoint) { + @if (activeBreakpoint >= Breakpoint.Tablet) { +
+ +
+ } @else {
@@ -17,60 +17,62 @@
- -
- + } + } + } -
-
- - -
-
-
-
-
- - + @if (fullyLoaded && filterV2) { +
+
+ + +
+ +
+
+
+ + +
-
-
+
-
-
- - - - - - - - - - - +
+
+ + +
+ + @if (utilityService.getActiveBreakpoint() > Breakpoint.Tablet) { + + }
- -
-
- -
- -
+ @if (utilityService.getActiveBreakpoint() <= Breakpoint.Tablet) { +
+ +
+ } + +
+ } + diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts index a6469a0a3..3001f7175 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts @@ -11,7 +11,7 @@ import { Output } from '@angular/core'; import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {NgbCollapse, NgbModal, NgbRating, NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; +import {NgbCollapse} from '@ng-bootstrap/ng-bootstrap'; import {Breakpoint, UtilityService} from '../shared/_services/utility.service'; import {Library} from '../_models/library/library'; import {allSortFields, FilterEvent, FilterItem, SortField} from '../_models/metadata/series-filter'; @@ -19,28 +19,15 @@ import {ToggleService} from '../_services/toggle.service'; import {FilterSettings} from './filter-settings'; import {SeriesFilterV2} from '../_models/metadata/v2/series-filter-v2'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {TypeaheadComponent} from '../typeahead/_components/typeahead.component'; import {DrawerComponent} from '../shared/drawer/drawer.component'; -import {AsyncPipe, NgClass, NgForOf, NgIf, NgTemplateOutlet} from '@angular/common'; -import {translate, TranslocoModule} from "@jsverse/transloco"; +import {AsyncPipe, NgClass, NgTemplateOutlet} from '@angular/common'; +import {translate, TranslocoModule, TranslocoService} from "@jsverse/transloco"; import {SortFieldPipe} from "../_pipes/sort-field.pipe"; import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component"; import {allFields} from "../_models/metadata/v2/filter-field"; -import {MetadataService} from "../_services/metadata.service"; -import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service"; import {FilterService} from "../_services/filter.service"; import {ToastrService} from "ngx-toastr"; -import { - Select2AutoCreateEvent, - Select2Module, - Select2Option, - Select2UpdateEvent, - Select2UpdateValue -} from "ng-select2-component"; -import {SmartFilter} from "../_models/metadata/v2/smart-filter"; -import {animate, state, style, transition, trigger} from "@angular/animations"; - -const ANIMATION_SPEED = 750; +import {animate, style, transition, trigger} from "@angular/animations"; @Component({ selector: 'app-metadata-filter', @@ -71,65 +58,53 @@ const ANIMATION_SPEED = 750; ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [NgIf, NgbCollapse, NgTemplateOutlet, DrawerComponent, NgbTooltip, TypeaheadComponent, - ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe, - MetadataBuilderComponent, NgForOf, Select2Module, NgClass] + imports: [NgTemplateOutlet, DrawerComponent, + ReactiveFormsModule, FormsModule, AsyncPipe, TranslocoModule, + MetadataBuilderComponent, NgClass] }) export class MetadataFilterComponent implements OnInit { + private readonly destroyRef = inject(DestroyRef); + public readonly utilityService = inject(UtilityService); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly toastr = inject(ToastrService); + private readonly filterService = inject(FilterService); + protected readonly toggleService = inject(ToggleService); + protected readonly translocoService = inject(TranslocoService); + private readonly sortFieldPipe = new SortFieldPipe(this.translocoService); + /** * This toggles the opening/collapsing of the metadata filter code */ @Input() filterOpen: EventEmitter = new EventEmitter(); - /** * Should filtering be shown on the page */ @Input() filteringDisabled: boolean = false; - @Input({required: true}) filterSettings!: FilterSettings; - @Output() applyFilter: EventEmitter = new EventEmitter(); - @ContentChild('[ngbCollapse]') collapse!: NgbCollapse; - private readonly destroyRef = inject(DestroyRef); - public readonly utilityService = inject(UtilityService); - public readonly filterUtilitiesService = inject(FilterUtilitiesService); + /** * Controls the visibility of extended controls that sit below the main header. */ filteringCollapsed: boolean = true; - libraries: Array> = []; sortGroup!: FormGroup; isAscendingSort: boolean = true; - updateApplied: number = 0; fullyLoaded: boolean = false; filterV2: SeriesFilterV2 | undefined; - allSortFields = allSortFields; - allFilterFields = allFields; - smartFilters!: Array; + protected readonly allSortFields = allSortFields.map(f => { + return {title: this.sortFieldPipe.transform(f), value: f}; + }).sort((a, b) => a.title.localeCompare(b.title)); + protected readonly allFilterFields = allFields; - private readonly cdRef = inject(ChangeDetectorRef); - private readonly toastr = inject(ToastrService); - - - constructor(public toggleService: ToggleService, private filterService: FilterService) { - this.filterService.getAllFilters().subscribe(res => { - this.smartFilters = res.map(r => { - return { - value: r, - label: r.name, - } - }); - }); - } ngOnInit(): void { if (this.filterSettings === undefined) { 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 b957ab658..bffecb1e2 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 @@ -9,7 +9,7 @@
-
+

{{series.name}} diff --git a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.html b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.html index c89257c0b..cf3639495 100644 --- a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.html +++ b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.html @@ -15,7 +15,7 @@

@if (showEdit) { - } @@ -28,7 +28,7 @@ @if (isEditMode) { } @else { - + } diff --git a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.scss b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.scss index 42e26490a..fdf0c0c95 100644 --- a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.scss +++ b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.scss @@ -12,23 +12,11 @@ cursor: pointer; } -//.setting-title.no-anim.edit:hover ~ .edit-btn { -// opacity: 1; -// transition: none; -//} -// -//.edit-btn { -// opacity: 0; -// transition: opacity 0.5s ease-out; -// transition-delay: 0.5s; -// -// &:hover { -// opacity: 1; -// transition: opacity 0.3s ease-out; -// } -//} -// -//.setting-title.no-anim + .edit-btn, -//.setting-title.no-anim.edit:hover ~ .edit-btn { -// transition: none !important; -//} +.non-selectable { + cursor: default; +} + +.btn-alignment { + padding-bottom: 0.5rem; // Align with h6 + padding-top: 0; +} diff --git a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.ts b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.ts index 85d94cc2e..af720221a 100644 --- a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.ts +++ b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.ts @@ -8,7 +8,7 @@ import { TemplateRef } from '@angular/core'; import {TranslocoDirective} from "@jsverse/transloco"; -import {NgTemplateOutlet} from "@angular/common"; +import {NgClass, NgTemplateOutlet} from "@angular/common"; import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe"; import {filter, fromEvent, tap} from "rxjs"; import {AbstractControl} from "@angular/forms"; @@ -19,7 +19,8 @@ import {AbstractControl} from "@angular/forms"; imports: [ TranslocoDirective, NgTemplateOutlet, - SafeHtmlPipe + SafeHtmlPipe, + NgClass ], templateUrl: './setting-item.component.html', styleUrl: './setting-item.component.scss', diff --git a/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.html b/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.html index eb4ffe331..24beb3329 100644 --- a/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.html +++ b/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.html @@ -7,7 +7,13 @@ }
-
{{title}}
+
+ @if (labelId) { + + } @else { + {{title}} + } +
diff --git a/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.ts b/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.ts index 6e3cf4d8c..9a7a24ec8 100644 --- a/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.ts +++ b/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.ts @@ -1,7 +1,8 @@ import { + AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, - Component, ContentChild, + Component, ContentChild, ElementRef, inject, Input, TemplateRef @@ -22,12 +23,39 @@ import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe"; styleUrl: './setting-switch.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class SettingSwitchComponent { +export class SettingSwitchComponent implements AfterContentInit { + private readonly cdRef = inject(ChangeDetectorRef); + private readonly elementRef = inject(ElementRef); @Input({required:true}) title: string = ''; @Input() subtitle: string | undefined = undefined; @Input() id: string | undefined = undefined; @ContentChild('switch') switchRef!: TemplateRef; + /** + * For wiring up with a real label + */ + labelId: string = ''; + + ngAfterContentInit(): void { + setTimeout(() => { + if (this.id) { + this.labelId = this.id; + this.cdRef.markForCheck(); + return; + } + + const element = this.elementRef.nativeElement; + const inputElement = element.querySelector('input'); + + if (inputElement && inputElement.id) { + this.labelId = inputElement.id; + this.cdRef.markForCheck(); + } else { + console.warn('No input with ID found in app-setting-switch. For accessibility, please ensure the input has an ID.'); + } + }); + } + } diff --git a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.html b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.html index bb181e331..2dd12c0d0 100644 --- a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.html +++ b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.html @@ -1,15 +1,21 @@ -
+
-
-
{{title}} +
+
+ @if (labelId) { + + } @else { + {{title}} + } @if (titleExtraRef) { }
-
- + +
+
diff --git a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.scss b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.scss index e69de29bb..15a6d7ace 100644 --- a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.scss +++ b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.scss @@ -0,0 +1,4 @@ +.btn-alignment { + padding-bottom: 0.5rem; // Align with h6 + padding-top: 0; +} diff --git a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.ts b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.ts index 3b6f03242..5e10137a7 100644 --- a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.ts +++ b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.ts @@ -26,6 +26,10 @@ export class SettingTitleComponent { private readonly cdRef = inject(ChangeDetectorRef); @Input({required:true}) title: string = ''; + /** + * If passed, will generate a proper label element. Requires `id` to be passed as well + */ + @Input() labelId: string | undefined = undefined; @Input() id: string | undefined = undefined; @Input() canEdit: boolean = true; @Input() isEditMode: boolean = false; diff --git a/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts b/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts index 1e7365629..8314307b1 100644 --- a/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts +++ b/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts @@ -28,6 +28,10 @@ export class BadgeExpanderComponent implements OnInit, OnChanges { @Input() itemsTillExpander: number = 4; @Input() allowToggle: boolean = true; @Input() includeComma: boolean = true; + /** + * If should be expanded by default. Defaults to false. + */ + @Input() defaultExpanded: boolean = false; /** * Invoked when the "and more" is clicked */ @@ -39,10 +43,20 @@ export class BadgeExpanderComponent implements OnInit, OnChanges { isCollapsed: boolean = false; get itemsLeft() { + if (this.defaultExpanded) return 0; + return Math.max(this.items.length - this.itemsTillExpander, 0); } ngOnInit(): void { + + if (this.defaultExpanded) { + this.isCollapsed = false; + this.visibleItems = this.items; + this.cdRef.markForCheck(); + return; + } + this.visibleItems = this.items.slice(0, this.itemsTillExpander); this.cdRef.markForCheck(); } diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html index 69ba7c827..8cbac271a 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html @@ -20,8 +20,8 @@
@if (libraryForm.get('name'); as formControl) { - - @if (libraryForm.dirty || libraryForm.touched) { + + @if (libraryForm.dirty || !libraryForm.untouched) {
@if (formControl.errors?.required) {
{{t('required-field')}}
@@ -52,8 +52,8 @@ diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index c1b6871b7..15825a1f5 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -28,7 +28,7 @@ import { } from 'src/app/admin/_modals/directory-picker/directory-picker.component'; import {ConfirmService} from 'src/app/shared/confirm.service'; import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service'; -import {Library, LibraryType} from 'src/app/_models/library/library'; +import {allLibraryTypes, Library, LibraryType} from 'src/app/_models/library/library'; import {ImageService} from 'src/app/_services/image.service'; import {LibraryService} from 'src/app/_services/library.service'; import {UploadService} from 'src/app/_services/upload.service'; @@ -47,6 +47,7 @@ import {SettingSwitchComponent} from "../../../settings/_components/setting-swit import {SettingButtonComponent} from "../../../settings/_components/setting-button/setting-button.component"; import {Action, ActionFactoryService, ActionItem} from "../../../_services/action-factory.service"; import {ActionService} from "../../../_services/action.service"; +import {LibraryTypePipe} from "../../../_pipes/library-type.pipe"; enum TabID { General = 'general-tab', @@ -68,7 +69,7 @@ enum StepID { standalone: true, imports: [CommonModule, NgbModalModule, NgbNavLink, NgbNavItem, NgbNavContent, ReactiveFormsModule, NgbTooltip, SentenceCasePipe, NgbNav, NgbNavOutlet, CoverImageChooserComponent, TranslocoModule, DefaultDatePipe, - FileTypeGroupPipe, EditListComponent, SettingItemComponent, SettingSwitchComponent, SettingButtonComponent], + FileTypeGroupPipe, EditListComponent, SettingItemComponent, SettingSwitchComponent, SettingButtonComponent, LibraryTypePipe], templateUrl: './library-settings-modal.component.html', styleUrls: ['./library-settings-modal.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -94,6 +95,7 @@ export class LibrarySettingsModalComponent implements OnInit { protected readonly TabID = TabID; protected readonly WikiLink = WikiLink; protected readonly Action = Action; + protected readonly libraryTypePipe = new LibraryTypePipe(); @Input({required: true}) library!: Library | undefined; @@ -119,7 +121,9 @@ export class LibrarySettingsModalComponent implements OnInit { selectedFolders: string[] = []; madeChanges = false; - libraryTypes: string[] = [] + libraryTypes = allLibraryTypes.map(f => { + return {title: this.libraryTypePipe.transform(f), value: f}; + }).sort((a, b) => a.title.localeCompare(b.title)); isAddLibrary = false; setupStep = StepID.General; @@ -134,11 +138,6 @@ export class LibrarySettingsModalComponent implements OnInit { } ngOnInit(): void { - this.settingService.getLibraryTypes().subscribe((types) => { - this.libraryTypes = types; - this.cdRef.markForCheck(); - }); - if (this.library === undefined) { this.isAddLibrary = true; this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html index f2ad3c572..a592d0a7e 100644 --- a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html +++ b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html @@ -1,117 +1,113 @@ -
-
- -
- - {{t('series-count', {num: stats.seriesCount | number})}} - -
-
-
- - -
- - {{t('volume-count', {num: stats.volumeCount | number})}} - -
-
-
- - -
- - {{t('file-count', {num: stats.totalFiles | number})}} - -
-
-
- - -
- - {{stats.totalSize | bytes}} - -
-
-
- - -
- - {{t('genre-count', {num: stats.totalGenres | compactNumber})}} - -
-
-
- - -
- - {{t('tag-count', {num: stats.totalTags | compactNumber})}} - -
-
-
- - -
- - {{t('people-count', {num: stats.totalPeople | compactNumber})}} - -
-
-
- - -
- - {{stats.totalReadingTime | timeDuration}} - -
-
-
- -
-
- +
+ +
+ + {{t('series-count', {num: stats.seriesCount | number})}} +
-
- +
+ + + +
+ + {{t('volume-count', {num: stats.volumeCount | number})}} +
-
- +
+ + + +
+ + {{t('file-count', {num: stats.totalFiles | number})}} +
-
- - +
+ + + +
+ + {{stats.totalSize | bytes}} +
-
- +
+ + + +
+ + {{t('genre-count', {num: stats.totalGenres | compactNumber})}} +
-
+
+
-
- -
+ +
+ + {{t('tag-count', {num: stats.totalTags | compactNumber})}} + +
+
+
-
- -
- -
- -
- -
- -
- -
- -
+ +
+ + {{t('people-count', {num: stats.totalPeople | compactNumber})}} + +
+
+
+ +
+ + {{stats.totalReadingTime | timeDuration}} + +
+
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/UI/Web/src/app/user-settings/api-key/api-key.component.html b/UI/Web/src/app/user-settings/api-key/api-key.component.html index e84881ec1..0bc927229 100644 --- a/UI/Web/src/app/user-settings/api-key/api-key.component.html +++ b/UI/Web/src/app/user-settings/api-key/api-key.component.html @@ -2,7 +2,8 @@ - +
@if (hideData) { @@ -14,17 +15,10 @@ + @if (showRefresh) { + + }
- @if (showRefresh) { - - - - }
- - - {{t('regen-warning')}} - - diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 6a03ecec1..59dbe8d2c 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -337,7 +337,8 @@ "regen-warning": "Regenerating your API key will invalidate any existing clients.", "no-key": "ERROR - KEY NOT SET", "confirm-reset": "This will invalidate any OPDS configurations you have setup. Are you sure you want to continue?", - "key-reset": "API Key reset" + "key-reset": "API Key reset", + "reset": "Reset" }, "scrobbling-providers": { @@ -571,9 +572,9 @@ "library-type-pipe": { "book": "Book", - "comic": "Comic", + "comic": "Comic (Legacy)", "manga": "Manga", - "comicVine": "Comic Vine", + "comicVine": "Comic", "image": "Image", "lightNovel": "Light Novel" }, @@ -1576,10 +1577,6 @@ "download-logs-task": "Download Logs", "download-logs-task-desc": "Compiles all log files into a zip and downloads it.", - "analyze-files-task": "Analyze Files", - "analyze-files-task-desc": "Runs a long-running task which will analyze files to generate extension and size. This should only be ran once for the v0.7 release. Not needed if you installed post v0.7.", - "analyze-files-task-success": "File analysis has been queued", - "sync-themes-task": "Sync Themes", "sync-themes-task-desc": "Synchronize downloaded themes with upstream changes if version matches.", "sync-themes-success": "Synchronization of themes has been queued",