From b40734265b8094f86aba57b73ca94c905808d19d Mon Sep 17 00:00:00 2001 From: Fesaa <77553571+Fesaa@users.noreply.github.com> Date: Sat, 4 Oct 2025 22:11:06 +0200 Subject: [PATCH] Social interactions with annotations (#4068) Co-authored-by: Joe Milazzo --- API.Tests/AbstractDbTest.cs | 2 - .../Extensions/QueryableExtensionsTests.cs | 544 ++- API.Tests/Services/AnnotationServiceTests.cs | 234 + API.Tests/Services/OpdsServiceTests.cs | 423 ++ API.Tests/Services/ReaderServiceTests.cs | 124 +- API.Tests/Services/ScannerServiceTests.cs | 50 + .../Services/Test Data/OpdsService/test.zip | Bin 0 -> 238889 bytes ...light differences No Metadata - Manga.json | 8 + API/Controllers/AnnotationController.cs | 121 +- API/Controllers/LibraryController.cs | 10 + API/Controllers/OPDSController.cs | 30 +- API/Controllers/SeriesController.cs | 12 + API/Controllers/UsersController.cs | 18 +- API/DTOs/Filtering/v2/FilterField.cs | 1 + API/DTOs/Reader/AnnotationDto.cs | 6 + API/DTOs/UserPreferencesDto.cs | 10 +- API/Data/DataContext.cs | 54 +- ...251003110154_SocialAnnotations.Designer.cs | 3925 +++++++++++++++++ .../20251003110154_SocialAnnotations.cs | 61 + .../Migrations/DataContextModelSnapshot.cs | 20 + API/Data/Repositories/AnnotationRepository.cs | 71 +- .../Repositories/ReadingListRepository.cs | 2 +- API/Data/Repositories/SeriesRepository.cs | 52 +- API/Data/Repositories/UserRepository.cs | 31 +- API/Entities/AppUserAnnotation.cs | 6 +- API/Entities/AppUserPreferences.cs | 59 +- API/Entities/Chapter.cs | 2 +- API/Extensions/DataContextExtensions.cs | 17 + .../Filtering/AnnotationFilter.cs | 25 +- .../QueryExtensions/QueryableExtensions.cs | 55 +- .../RestrictByAgeExtensions.cs | 196 + API/Extensions/StringExtensions.cs | 9 +- API/Helpers/AutoMapperProfiles.cs | 10 +- API/Helpers/Builders/AppUserBuilder.cs | 19 +- .../AnnotationFilterFieldValueConverter.cs | 3 +- API/Services/AnnotationService.cs | 72 +- API/Services/OpdsService.cs | 35 +- API/Services/ReaderService.cs | 1 + API/Services/Tasks/StatsService.cs | 2 +- API/redo-migration.sh | 27 + UI/Web/package-lock.json | 44 +- UI/Web/package.json | 1 + UI/Web/src/app/_helpers/browser.ts | 63 +- .../_models/metadata/v2/annotations-filter.ts | 1 + .../app/_models/preferences/preferences.ts | 14 +- .../app/_pipes/generic-filter-field.pipe.ts | 2 + UI/Web/src/app/_services/account.service.ts | 11 +- .../app/_services/action-factory.service.ts | 22 +- .../src/app/_services/annotation.service.ts | 25 +- .../_services/epub-reader-settings.service.ts | 17 +- UI/Web/src/app/_services/metadata.service.ts | 6 + UI/Web/src/app/_services/series.service.ts | 6 +- .../details-tab/details-tab.component.html | 48 +- .../details-tab/details-tab.component.ts | 14 +- .../admin/edit-user/edit-user.component.html | 12 +- .../admin/edit-user/edit-user.component.ts | 44 +- .../invite-user/invite-user.component.html | 12 +- .../invite-user/invite-user.component.ts | 62 +- .../library-selector.component.html | 42 - .../library-selector.component.scss | 3 - .../library-selector.component.ts | 98 - .../manage-metadata-mappings.component.html | 46 +- .../manage-metadata-mappings.component.ts | 30 +- .../manage-open-idconnect.component.html | 507 ++- .../manage-open-idconnect.component.ts | 177 +- .../role-selector.component.html | 28 - .../role-selector.component.scss | 3 - .../role-selector/role-selector.component.ts | 136 - .../all-annotations.component.html | 2 + .../all-annotations.component.ts | 30 + .../annotation-card.component.html | 28 +- .../annotation-card.component.ts | 51 +- .../annotation-likes.component.html | 13 + .../annotation-likes.component.scss | 0 .../annotation-likes.component.ts | 60 + .../view-annotations-drawer.component.html | 8 +- .../view-annotations-drawer.component.ts | 2 + ...view-edit-annotation-drawer.component.html | 7 + ...view-edit-annotation-drawer.component.scss | 5 - .../view-edit-annotation-drawer.component.ts | 11 +- .../book-line-overlay.component.ts | 138 +- .../_models/annotations/annotation.ts | 4 + .../src/app/cards/bulk-selection.service.ts | 1 + .../card-detail-layout.component.ts | 4 +- .../chapter-detail.component.html | 8 +- .../chapter-detail.component.ts | 2 + .../metadata-filter-row.component.ts | 16 +- .../pdf-reader/pdf-reader.component.ts | 1 - .../reading-list-detail.component.html | 3 +- .../metadata-detail-row.component.html | 23 +- .../metadata-detail-row.component.scss | 8 +- .../metadata-detail-row.component.ts | 26 +- .../series-detail.component.html | 19 +- .../series-detail/series-detail.component.ts | 7 +- .../setting-multi-check-box.component.html | 46 + .../setting-multi-check-box.component.scss | 8 + .../setting-multi-check-box.component.ts | 159 + .../setting-multi-text-field.component.html | 19 + .../setting-multi-text-field.component.scss | 0 .../setting-multi-text-field.component.ts | 117 + .../_services/filter-utilities.service.ts | 2 +- .../app/shared/_services/utility.service.ts | 4 +- .../manage-user-preferences.component.html | 98 +- .../manage-user-preferences.component.ts | 181 +- .../volume-detail.component.html | 8 +- .../volume-detail/volume-detail.component.ts | 3 + UI/Web/src/assets/langs/en.json | 74 +- 107 files changed, 7615 insertions(+), 1402 deletions(-) create mode 100644 API.Tests/Services/AnnotationServiceTests.cs create mode 100644 API.Tests/Services/OpdsServiceTests.cs create mode 100644 API.Tests/Services/Test Data/OpdsService/test.zip create mode 100644 API.Tests/Services/Test Data/ScannerService/TestCases/Series with slight differences No Metadata - Manga.json create mode 100644 API/Data/Migrations/20251003110154_SocialAnnotations.Designer.cs create mode 100644 API/Data/Migrations/20251003110154_SocialAnnotations.cs create mode 100644 API/Extensions/DataContextExtensions.cs create mode 100755 API/redo-migration.sh delete mode 100644 UI/Web/src/app/admin/library-selector/library-selector.component.html delete mode 100644 UI/Web/src/app/admin/library-selector/library-selector.component.scss delete mode 100644 UI/Web/src/app/admin/library-selector/library-selector.component.ts delete mode 100644 UI/Web/src/app/admin/role-selector/role-selector.component.html delete mode 100644 UI/Web/src/app/admin/role-selector/role-selector.component.scss delete mode 100644 UI/Web/src/app/admin/role-selector/role-selector.component.ts create mode 100644 UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.html create mode 100644 UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.scss create mode 100644 UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.ts create mode 100644 UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.html create mode 100644 UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.scss create mode 100644 UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.ts create mode 100644 UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.html create mode 100644 UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.scss create mode 100644 UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.ts diff --git a/API.Tests/AbstractDbTest.cs b/API.Tests/AbstractDbTest.cs index 6471cec91..3be2c6043 100644 --- a/API.Tests/AbstractDbTest.cs +++ b/API.Tests/AbstractDbTest.cs @@ -12,10 +12,8 @@ using AutoMapper; using Hangfire; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; -using Polly; using Xunit.Abstractions; namespace API.Tests; diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/API.Tests/Extensions/QueryableExtensionsTests.cs index 96d74b46d..aca7eb801 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/API.Tests/Extensions/QueryableExtensionsTests.cs @@ -3,6 +3,7 @@ using System.Linq; using API.Data.Misc; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; using API.Entities.Person; using API.Extensions.QueryExtensions; using API.Helpers.Builders; @@ -15,9 +16,10 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] [InlineData(false, 1)] - public void RestrictAgainstAgeRestriction_Series_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) + public void RestrictAgainstAgeRestriction_Series_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, + int expectedCount) { - var items = new List() + var items = new List { new SeriesBuilder("Test 1") .WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()) @@ -30,7 +32,7 @@ public class QueryableExtensionsTests .Build() }; - var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction() + var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction { AgeRating = AgeRating.Teen, IncludeUnknowns = includeUnknowns @@ -41,23 +43,28 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] [InlineData(false, 1)] - public void RestrictAgainstAgeRestriction_CollectionTag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) + public void RestrictAgainstAgeRestriction_CollectionTag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, + int expectedCount) { - var items = new List() + var items = new List { new AppUserCollectionBuilder("Test") - .WithItem(new SeriesBuilder("S1").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()).Build()) + .WithItem(new SeriesBuilder("S1") + .WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()).Build()) .Build(), new AppUserCollectionBuilder("Test 2") - .WithItem(new SeriesBuilder("S2").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build()).Build()) - .WithItem(new SeriesBuilder("S1").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()).Build()) + .WithItem(new SeriesBuilder("S2") + .WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build()).Build()) + .WithItem(new SeriesBuilder("S1") + .WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()).Build()) .Build(), new AppUserCollectionBuilder("Test 3") - .WithItem(new SeriesBuilder("S3").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build()).Build()) - .Build(), + .WithItem(new SeriesBuilder("S3") + .WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build()).Build()) + .Build() }; - var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction() + var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction { AgeRating = AgeRating.Teen, IncludeUnknowns = includeUnknowns @@ -68,9 +75,10 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] [InlineData(false, 2)] - public void RestrictAgainstAgeRestriction_Genre_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) + public void RestrictAgainstAgeRestriction_Genre_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, + int expectedCount) { - var items = new List() + var items = new List { new GenreBuilder("A") .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()) @@ -81,10 +89,10 @@ public class QueryableExtensionsTests .Build(), new GenreBuilder("C") .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build()) - .Build(), + .Build() }; - var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction() + var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction { AgeRating = AgeRating.Teen, IncludeUnknowns = includeUnknowns @@ -95,9 +103,10 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] [InlineData(false, 2)] - public void RestrictAgainstAgeRestriction_Tag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) + public void RestrictAgainstAgeRestriction_Tag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, + int expectedCount) { - var items = new List() + var items = new List { new TagBuilder("Test 1") .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()) @@ -108,10 +117,10 @@ public class QueryableExtensionsTests .Build(), new TagBuilder("Test 3") .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build()) - .Build(), + .Build() }; - var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction() + var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction { AgeRating = AgeRating.Teen, IncludeUnknowns = includeUnknowns @@ -122,13 +131,15 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] [InlineData(false, 2)] - public void RestrictAgainstAgeRestriction_Person_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedPeopleCount) + public void RestrictAgainstAgeRestriction_Person_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, + int expectedPeopleCount) { // Arrange var items = new List { CreatePersonWithSeriesMetadata("Test1", AgeRating.Teen), - CreatePersonWithSeriesMetadata("Test2", AgeRating.Unknown, AgeRating.Teen), // 2 series on this person, restrict will still allow access + CreatePersonWithSeriesMetadata("Test2", AgeRating.Unknown, + AgeRating.Teen), // 2 series on this person, restrict will still allow access CreatePersonWithSeriesMetadata("Test3", AgeRating.X18Plus) }; @@ -166,21 +177,502 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] [InlineData(false, 1)] - public void RestrictAgainstAgeRestriction_ReadingList_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) + public void RestrictAgainstAgeRestriction_ReadingList_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, + int expectedCount) { - - var items = new List() + var items = new List { new ReadingListBuilder("Test List").WithRating(AgeRating.Teen).Build(), new ReadingListBuilder("Test List").WithRating(AgeRating.Unknown).Build(), - new ReadingListBuilder("Test List").WithRating(AgeRating.X18Plus).Build(), + new ReadingListBuilder("Test List").WithRating(AgeRating.X18Plus).Build() }; - var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction() + var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction { AgeRating = AgeRating.Teen, IncludeUnknowns = includeUnknowns }); Assert.Equal(expectedCount, filtered.Count()); } + + [Fact] + public void RestrictBySocialPreferences_SocialLibs() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(2, [1], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(3, [], AgeRating.NotApplicable, true, false, true) + ]; + + IList annotations = + [ + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.Unknown), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Unknown), + CreateAnnotationInLibraryWithAgeRating(2, 2, AgeRating.Unknown), + CreateAnnotationInLibraryWithAgeRating(3, 1, AgeRating.Unknown), + CreateAnnotationInLibraryWithAgeRating(3, 1, AgeRating.Unknown) + ]; + + // Own annotation, and the other in lib 1 + Assert.Equal(2, annotations.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + + // Own annotations, and from user 1 in lib 1 + Assert.Equal(3, annotations.AsQueryable().RestrictBySocialPreferences(2, userPreferences).Count()); + + // Own annotations, and user 1 in lib 1 and user 2 in lib 1 + Assert.Equal(4, annotations.AsQueryable().RestrictBySocialPreferences(3, userPreferences).Count()); + } + + [Theory] + [InlineData(true, 4, 3)] + [InlineData(false, 3, 2)] + public void RestrictBySocialPreferences_AgeRating(bool includeUnknowns, int expected1, int expected2) + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(2, [], AgeRating.Mature, includeUnknowns, true, true) + ]; + + IList annotations = + [ + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.AdultsOnly), + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.Unknown), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Unknown) + ]; + + Assert.Equal(expected1, annotations.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + Assert.Equal(expected2, annotations.AsQueryable().RestrictBySocialPreferences(2, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_UserNotSharingAnnotations() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, false, true), // User 1 NOT sharing + CreateUserPreferences(2, [], AgeRating.NotApplicable, true, true, true) + ]; + + IList annotations = + [ + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Everyone) + ]; + + // User 2 should only see their own annotation since User 1 is not sharing + Assert.Equal(1, annotations.AsQueryable().RestrictBySocialPreferences(2, userPreferences).Count()); + + // User 1 should see both (own + user 2's shared) + Assert.Equal(2, annotations.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_UserNotViewingOtherAnnotations() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, true, false), // User 1 NOT viewing others + CreateUserPreferences(2, [], AgeRating.NotApplicable, true, true, true) + ]; + + IList annotations = + [ + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Everyone) + ]; + + // User 1 should only see their own annotation (not viewing others) + Assert.Equal(1, annotations.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + + // User 2 should see both + Assert.Equal(2, annotations.AsQueryable().RestrictBySocialPreferences(2, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_RequestingUserLibraryFilter() + { + IList userPreferences = + [ + CreateUserPreferences(1, [1], AgeRating.NotApplicable, true, true, true), // User 1 only wants lib 1 + CreateUserPreferences(2, [], AgeRating.NotApplicable, true, true, true) + ]; + + IList annotations = + [ + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(1, 2, AgeRating.Everyone), // User 1's own in lib 2 + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(2, 2, AgeRating.Everyone) + ]; + + // User 1 should see: own (always) + user 2's in lib 1 only = 3 + Assert.Equal(3, annotations.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_RequestingUserAgeRatingFilter() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.Teen, false, true, true), // User 1 wants Teen max, no unknowns + CreateUserPreferences(2, [], AgeRating.NotApplicable, true, true, true) + ]; + + IList annotations = + [ + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.AdultsOnly), // User 1's own - always included + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.AdultsOnly), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Teen), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Unknown) + ]; + + // User 1 should see: own (1) + user 2's Teen (1) = 2 + Assert.Equal(2, annotations.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_CombinedLibraryAndAgeRatingFilters() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(2, [1], AgeRating.Teen, true, true, + true) // User 2: lib 1 only + Teen max + unknowns ok + ]; + + IList annotations = + [ + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(1, 2, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.AdultsOnly), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Teen), + CreateAnnotationInLibraryWithAgeRating(2, 2, AgeRating.Everyone) // User 2's own in lib 2 + ]; + + // User 2 should see: + // - Own annotations (always): 2 + // - User 1's in lib 1 with age <= Teen: 1 (Everyone) + // Total: 3 + Assert.Equal(3, annotations.AsQueryable().RestrictBySocialPreferences(2, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_MultipleUsersWithDifferentLibraryRestrictions() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(2, [1], AgeRating.NotApplicable, true, true, true), // User 2 shares lib 1 only + CreateUserPreferences(3, [2], AgeRating.NotApplicable, true, true, true) // User 3 shares lib 2 only + ]; + + IList annotations = + [ + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(2, 2, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(3, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(3, 2, AgeRating.Everyone) + ]; + + // User 1 should see: own (1) + user 2 lib 1 (1) + user 3 lib 2 (1) = 3 + Assert.Equal(3, annotations.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_NoOtherUsersSharing() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, false, true), + CreateUserPreferences(2, [], AgeRating.NotApplicable, true, false, true) + ]; + + IList annotations = + [ + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Everyone) + ]; + + // Each user should only see their own + Assert.Equal(1, annotations.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + Assert.Equal(1, annotations.AsQueryable().RestrictBySocialPreferences(2, userPreferences).Count()); + } + + [Theory] + [InlineData(AgeRating.Everyone, true, 3)] + [InlineData(AgeRating.Everyone, false, 2)] + [InlineData(AgeRating.Teen, true, 4)] + [InlineData(AgeRating.Mature17Plus, false, 3)] + public void RestrictBySocialPreferences_RequestingUserAgeRatingVariations(AgeRating maxRating, bool includeUnknowns, + int expected) + { + IList userPreferences = + [ + CreateUserPreferences(1, [], maxRating, includeUnknowns, true, true), + CreateUserPreferences(2, [], AgeRating.NotApplicable, true, true, true) + ]; + + IList annotations = + [ + CreateAnnotationInLibraryWithAgeRating(1, 1, AgeRating.AdultsOnly), // Own - always included + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Everyone), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Teen), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Mature), + CreateAnnotationInLibraryWithAgeRating(2, 1, AgeRating.Unknown) + ]; + + Assert.Equal(expected, annotations.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + } + + private static AppUserPreferences CreateUserPreferences(int user, IList libs, AgeRating ageRating, + bool includeUnknowns, bool share, bool seeAn) + { + return new AppUserPreferences + { + AppUserId = user, + Theme = null, + SocialPreferences = new AppUserSocialPreferences + { + ShareReviews = share, + ShareAnnotations = share, + ViewOtherAnnotations = seeAn, + SocialLibraries = libs, + SocialMaxAgeRating = ageRating, + SocialIncludeUnknowns = includeUnknowns + } + }; + } + + private static AppUserAnnotation CreateAnnotationInLibraryWithAgeRating(int user, int lib, AgeRating ageRating) + { + return new AppUserAnnotation + { + XPath = null, + LibraryId = lib, + SeriesId = 0, + VolumeId = 0, + ChapterId = 0, + AppUserId = user, + Series = new Series + { + Name = null, + NormalizedName = null, + NormalizedLocalizedName = null, + SortName = null, + LocalizedName = null, + OriginalName = null, + Metadata = new SeriesMetadata + { + AgeRating = ageRating + } + } + }; + } + + [Fact] + public void RestrictBySocialPreferences_Rating_SocialLibs() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(2, [1], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(3, [], AgeRating.NotApplicable, true, false, true) + ]; + + IList ratings = + [ + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.Unknown), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Unknown), + CreateRatingInLibraryWithAgeRating(2, 2, AgeRating.Unknown), + CreateRatingInLibraryWithAgeRating(3, 1, AgeRating.Unknown), + CreateRatingInLibraryWithAgeRating(3, 1, AgeRating.Unknown) + ]; + + // Own rating, and the other in lib 1 + Assert.Equal(2, ratings.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + + // Own ratings, and from user 1 in lib 1 + Assert.Equal(3, ratings.AsQueryable().RestrictBySocialPreferences(2, userPreferences).Count()); + + // Own ratings, and user 1 in lib 1 and user 2 in lib 1 + Assert.Equal(4, ratings.AsQueryable().RestrictBySocialPreferences(3, userPreferences).Count()); + } + + [Theory] + [InlineData(true, 4, 3)] + [InlineData(false, 3, 2)] + public void RestrictBySocialPreferences_Rating_AgeRating(bool includeUnknowns, int expected1, int expected2) + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(2, [], AgeRating.Mature, includeUnknowns, true, true) + ]; + + IList ratings = + [ + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.AdultsOnly), + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.Unknown), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Unknown) + ]; + + var f = ratings.AsQueryable().RestrictBySocialPreferences(1, userPreferences); + Assert.Equal(expected1, ratings.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + Assert.Equal(expected2, ratings.AsQueryable().RestrictBySocialPreferences(2, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_Rating_UserNotSharingReviews() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, false, true), + CreateUserPreferences(2, [], AgeRating.NotApplicable, true, true, true) + ]; + + IList ratings = + [ + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Everyone) + ]; + + Assert.Equal(1, ratings.AsQueryable().RestrictBySocialPreferences(2, userPreferences).Count()); + Assert.Equal(2, ratings.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_Rating_RequestingUserLibraryFilter() + { + IList userPreferences = + [ + CreateUserPreferences(1, [1], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(2, [], AgeRating.NotApplicable, true, true, true) + ]; + + IList ratings = + [ + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(1, 2, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(2, 2, AgeRating.Everyone) + ]; + + Assert.Equal(3, ratings.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_Rating_RequestingUserAgeRatingFilter() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.Teen, false, true, true), + CreateUserPreferences(2, [], AgeRating.NotApplicable, true, true, true) + ]; + + IList ratings = + [ + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.AdultsOnly), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.AdultsOnly), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Teen), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Unknown) + ]; + + Assert.Equal(2, ratings.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_Rating_CombinedFilters() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(2, [1], AgeRating.Teen, true, true, true) + ]; + + IList ratings = + [ + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(1, 2, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.AdultsOnly), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Teen), + CreateRatingInLibraryWithAgeRating(2, 2, AgeRating.Everyone) + ]; + + Assert.Equal(3, ratings.AsQueryable().RestrictBySocialPreferences(2, userPreferences).Count()); + } + + [Fact] + public void RestrictBySocialPreferences_Rating_MultipleUsersWithDifferentLibraryRestrictions() + { + IList userPreferences = + [ + CreateUserPreferences(1, [], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(2, [1], AgeRating.NotApplicable, true, true, true), + CreateUserPreferences(3, [2], AgeRating.NotApplicable, true, true, true) + ]; + + IList ratings = + [ + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(2, 2, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(3, 1, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(3, 2, AgeRating.Everyone) + ]; + + Assert.Equal(3, ratings.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + } + + [Theory] + [InlineData(AgeRating.Everyone, true, 3)] + [InlineData(AgeRating.Everyone, false, 2)] + [InlineData(AgeRating.Teen, true, 4)] + [InlineData(AgeRating.Mature17Plus, false, 3)] + public void RestrictBySocialPreferences_Rating_RequestingUserAgeRatingVariations(AgeRating maxRating, + bool includeUnknowns, int expected) + { + IList userPreferences = + [ + CreateUserPreferences(1, [], maxRating, includeUnknowns, true, true), + CreateUserPreferences(2, [], AgeRating.NotApplicable, true, true, true) + ]; + + IList ratings = + [ + CreateRatingInLibraryWithAgeRating(1, 1, AgeRating.AdultsOnly), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Everyone), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Teen), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Mature), + CreateRatingInLibraryWithAgeRating(2, 1, AgeRating.Unknown) + ]; + + Assert.Equal(expected, ratings.AsQueryable().RestrictBySocialPreferences(1, userPreferences).Count()); + } + + private static AppUserRating CreateRatingInLibraryWithAgeRating(int user, int lib, AgeRating ageRating) + { + return new AppUserRating + { + AppUserId = user, + Series = new Series + { + Name = null, + NormalizedName = null, + NormalizedLocalizedName = null, + SortName = null, + LocalizedName = null, + OriginalName = null, + LibraryId = lib, + Metadata = new SeriesMetadata + { + AgeRating = ageRating + } + } + }; + } } diff --git a/API.Tests/Services/AnnotationServiceTests.cs b/API.Tests/Services/AnnotationServiceTests.cs new file mode 100644 index 000000000..85d664586 --- /dev/null +++ b/API.Tests/Services/AnnotationServiceTests.cs @@ -0,0 +1,234 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.Constants; +using API.Data; +using API.Data.Repositories; +using API.DTOs.Reader; +using API.DTOs.Settings; +using API.Entities; +using API.Helpers.Builders; +using API.Services; +using API.SignalR; +using AutoMapper; +using Kavita.Common; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; +using Xunit.Abstractions; + +namespace API.Tests.Services; + +public class AnnotationServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) +{ + + [Fact] + public async Task CreateAnnotationTest() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (user, annotationService, bookService, chapter, _) = await Setup(unitOfWork, context, mapper); + + // No highlight or Selected Text + await Assert.ThrowsAsync(async () => + await annotationService.CreateAnnotation(user.Id, new AnnotationDto + { + XPath = null, + ChapterId = 0, + VolumeId = 0, + SeriesId = 0, + LibraryId = 0, + OwnerUserId = 0 + })); + + + // Chapter title + const int pageNum = 1; + const string chapterTitle = "My Chapter Title"; + bookService.GenerateTableOfContents(null!).ReturnsForAnyArgs([ + new BookChapterItem + { + Page = pageNum, + Title = chapterTitle, + } + ]); + + var dto = await CreateSimpleAnnotation(annotationService, user, chapter); + Assert.Equal(chapterTitle, dto.ChapterTitle); + } + + [Fact] + public async Task UpdateAnnotationTestFailures() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (user, annotationService, _, chapter, eventHub) = await Setup(unitOfWork, context, mapper); + + // Can't update without id + await Assert.ThrowsAsync(async () => + await annotationService.UpdateAnnotation(user.Id, new AnnotationDto + { + XPath = null, + ChapterId = 0, + VolumeId = 0, + SeriesId = 0, + LibraryId = 0, + OwnerUserId = 0 + })); + + var dto = await CreateSimpleAnnotation(annotationService, user, chapter); + + // Can't update others annotations + var otherUser = new AppUserBuilder("other", "other@localhost").Build(); + await Assert.ThrowsAsync(async () => await annotationService.UpdateAnnotation(otherUser.Id, dto)); + } + + [Fact] + public async Task UpdateAnnotationTestChanges() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (user, annotationService, _, chapter, eventHub) = await Setup(unitOfWork, context, mapper); + + var dto = await CreateSimpleAnnotation(annotationService, user, chapter); + + // Update relevant fields + dto.ContainsSpoiler = true; + dto.SelectedSlotIndex = 2; + dto.Comment = "{}"; + dto.CommentHtml = "

Something New

"; + dto.CommentPlainText = "Something unrelated"; // Should not be used + dto = await annotationService.UpdateAnnotation(user.Id, dto); + + Assert.True(dto.ContainsSpoiler); + Assert.Equal(2, dto.SelectedSlotIndex); + Assert.Equal("

Something New

", dto.CommentHtml); + Assert.Equal("Something New", dto.CommentPlainText); + + // Ensure event was sent out to UI + await eventHub.Received().SendMessageToAsync( + MessageFactory.AnnotationUpdate, + Arg.Any(), + user.Id); + } + + [Fact] + public async Task ExportAnnotationsCorrectExportUser() + { + var unitOfWork = Substitute.For(); + var annotationRepo = Substitute.For(); + var settingsRepo = Substitute.For(); + unitOfWork.AnnotationRepository.Returns(annotationRepo); + unitOfWork.SettingsRepository.Returns(settingsRepo); + + settingsRepo.GetSettingsDtoAsync().Returns(new ServerSettingDto + { + HostName = "", + }); + + var annotationService = new AnnotationService( + Substitute.For>(), + unitOfWork, + Substitute.For(), + Substitute.For()); + + await annotationService.ExportAnnotations(1); + + await annotationRepo.Received().GetFullAnnotationsByUserIdAsync(1); + await annotationRepo.DidNotReceive().GetFullAnnotations(1, []); + } + + [Fact] + public async Task ExportAnnotationsCorrectExportSpecific() + { + var unitOfWork = Substitute.For(); + var annotationRepo = Substitute.For(); + var settingsRepo = Substitute.For(); + unitOfWork.AnnotationRepository.Returns(annotationRepo); + unitOfWork.SettingsRepository.Returns(settingsRepo); + + settingsRepo.GetSettingsDtoAsync().Returns(new ServerSettingDto + { + HostName = "", + }); + + var annotationService = new AnnotationService( + Substitute.For>(), + unitOfWork, + Substitute.For(), + Substitute.For()); + + List ids = [1, 2, 3]; // Received checks pointers I think + await annotationService.ExportAnnotations(1, ids); + + await annotationRepo.DidNotReceive().GetFullAnnotationsByUserIdAsync(1); + await annotationRepo.Received().GetFullAnnotations(1, ids); + } + + private static async Task CreateSimpleAnnotation(IAnnotationService annotationService, AppUser user, Chapter chapter) + { + return await annotationService.CreateAnnotation(user.Id, new AnnotationDto + { + XPath = "", + ChapterId = chapter.Id, + VolumeId = chapter.VolumeId, + SeriesId = chapter.Volume.SeriesId, + LibraryId = chapter.Volume.Series.LibraryId, + PageNumber = 1, + OwnerUserId = user.Id, + HighlightCount = 1, + SelectedText = "Something" + }); + } + + private static async Task<(AppUser, IAnnotationService, IBookService, Chapter, IEventHub)> Setup( + IUnitOfWork unitOfWork, + DataContext context, + IMapper mapper) + { + var user = new AppUserBuilder("defaultAdmin", "defaultAdmin@localhost") + .WithRole(PolicyConstants.AdminRole) + .Build(); + + context.AppUser.Add(user); + await unitOfWork.CommitAsync(); + + user = await unitOfWork.UserRepository.GetUserByIdAsync(user.Id, AppUserIncludes.DashboardStreams); + Assert.NotNull(user); + + await new AccountService( + null!, + Substitute.For>(), + unitOfWork, + mapper, + Substitute.For() + ).SeedUser(user); + + var chapter = new ChapterBuilder("1") + .Build(); + + var lib = new LibraryBuilder("Manga") + .WithAppUser(user) + .WithSeries(new SeriesBuilder("Spice and Wolf") + .WithVolume(new VolumeBuilder("1") + .WithChapter(chapter) + .Build()) + .Build()) + .Build(); + + context.Library.Add(lib); + await unitOfWork.CommitAsync(); + + chapter.Volume = lib.Series.First().Volumes.First(); + chapter.Volume.Series = lib.Series.First(); + chapter.Volume.Series.Library = lib; + + + var bookService = Substitute.For(); + var eventHub = Substitute.For(); + var annotationService = new AnnotationService( + Substitute.For>(), + unitOfWork, bookService, eventHub); + + return (user, annotationService, bookService, chapter, eventHub); + } + +} diff --git a/API.Tests/Services/OpdsServiceTests.cs b/API.Tests/Services/OpdsServiceTests.cs new file mode 100644 index 000000000..8d908caeb --- /dev/null +++ b/API.Tests/Services/OpdsServiceTests.cs @@ -0,0 +1,423 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Threading.Tasks; +using API.Data; +using API.Data.Repositories; +using API.DTOs.OPDS.Requests; +using API.DTOs.Progress; +using API.Entities; +using API.Entities.Enums; +using API.Helpers.Builders; +using API.Services; +using API.Services.Plus; +using API.SignalR; +using API.Tests.Helpers; +using AutoMapper; +using Hangfire; +using Hangfire.InMemory; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; +using Xunit.Abstractions; + +namespace API.Tests.Services; + +public class OpdsServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTest(testOutputHelper) +{ + private readonly string _testFilePath = Path.Join(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/OpdsService"), "test.zip"); + + #region Setup + + private Tuple SetupService(IUnitOfWork unitOfWork, IMapper mapper) + { + JobStorage.Current = new InMemoryStorage(); + + var ds = new DirectoryService(Substitute.For>(), new FileSystem()); + + var readerService = new ReaderService(unitOfWork, Substitute.For>(), + Substitute.For(), Substitute.For(), ds, Substitute.For()); + + var localizationService = + new LocalizationService(ds, new MockHostingEnvironment(), Substitute.For(), unitOfWork); + + var seriesService = new SeriesService(unitOfWork, Substitute.For(), Substitute.For(), + Substitute.For>(), Substitute.For(), + localizationService, Substitute.For()); + + var opdsService = new OpdsService(unitOfWork, localizationService, + seriesService, Substitute.For(), + ds, readerService, mapper); + + return new Tuple(opdsService, readerService); + } + + private async Task SetupSeriesAndUser(DataContext context, IUnitOfWork unitOfWork, int numberOfSeries = 1) + { + var library = new LibraryBuilder("Test Lib", LibraryType.Manga).Build(); + + unitOfWork.LibraryRepository.Add(library); + await unitOfWork.CommitAsync(); + + + context.AppUser.Add(new AppUserBuilder("majora2007", "majora2007") + .WithLibrary(library) + .WithLocale("en") + .Build()); + + await context.SaveChangesAsync(); + + Assert.NotEmpty(await unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(1)); + + var counter = 0; + foreach (var i in Enumerable.Range(0, numberOfSeries)) + { + var series = new SeriesBuilder("Test " + (i + 1)) + .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder("1") + .WithSortOrder(counter) + .WithPages(10) + .WithFile(new MangaFileBuilder(_testFilePath, MangaFormat.Archive, 10).Build()) + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithFile(new MangaFileBuilder(_testFilePath, MangaFormat.Archive, 10).Build()) + .WithSortOrder(counter + 1) + .WithPages(10) + .Build()) + .Build()) + .Build(); + series.Library = library; + + context.Series.Add(series); + counter += 2; + } + + await unitOfWork.CommitAsync(); + + + + var user = await unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); + Assert.NotNull(user); + + // // Build a reading list + // + // var readingList = new ReadingListBuilder("Test RL").WithAppUserId(user.Id).WithItem(new ReadingListItem + // { + // SeriesId = 1, + // VolumeId = 1, + // ChapterId = 0, + // Order = 0, + // Series = null, + // Volume = null, + // Chapter = null + // }) + + + return user; + } + + #endregion + + #region Continue Points + + [Fact] + public async Task ContinuePoint_ShouldWorkWithProgress() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (opdsService, readerService) = SetupService(unitOfWork, mapper); + + + var user = await SetupSeriesAndUser(context, unitOfWork); + + var firstChapter = await unitOfWork.ChapterRepository.GetChapterAsync(1); + Assert.NotNull(firstChapter); + + // Mark Chapter 1 as read + await readerService.MarkChaptersAsRead(user, 1, [firstChapter]); + Assert.True(unitOfWork.HasChanges()); + await unitOfWork.CommitAsync(); + + // Generate Series Feed and validate first element is a Continue From Chapter 2 + var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest + { + ApiKey = user.ApiKey, + Prefix = OpdsService.DefaultApiPrefix, + BaseUrl = string.Empty, + UserId = user.Id, + EntityId = 1, + PageNumber = 0 + }); + + Assert.NotEmpty(feed.Entries); + Assert.Equal(3, feed.Entries.Count); + Assert.StartsWith("Continue Reading from", feed.Entries.First().Title); + } + + + [Fact] + public async Task ContinuePoint_DoesntExist_WhenNoProgress() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (opdsService, readerService) = SetupService(unitOfWork, mapper); + + + var user = await SetupSeriesAndUser(context, unitOfWork); + + var firstChapter = await unitOfWork.ChapterRepository.GetChapterAsync(1); + Assert.NotNull(firstChapter); + + + // Generate Series Feed and validate first element is a Continue From Chapter 2 + var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest + { + ApiKey = user.ApiKey, + Prefix = OpdsService.DefaultApiPrefix, + BaseUrl = string.Empty, + UserId = user.Id, + EntityId = 1, + PageNumber = 0 + }); + + Assert.NotEmpty(feed.Entries); + Assert.Equal(2, feed.Entries.Count); + } + #endregion + + #region Misc + + [Fact] + public async Task NoProgressEncoding() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (opdsService, readerService) = SetupService(unitOfWork, mapper); + + + var user = await SetupSeriesAndUser(context, unitOfWork); + + var firstChapter = await unitOfWork.ChapterRepository.GetChapterAsync(1); + Assert.NotNull(firstChapter); + + + // Generate Series Feed and validate first element is a Continue From Chapter 2 + var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest + { + ApiKey = user.ApiKey, + Prefix = OpdsService.DefaultApiPrefix, + BaseUrl = string.Empty, + UserId = user.Id, + EntityId = 1, + PageNumber = 0 + }); + + Assert.NotEmpty(feed.Entries); + Assert.Contains(OpdsService.NoReadingProgressIcon, feed.Entries.First().Title); + } + + [Fact] + public async Task QuarterProgressEncoding() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (opdsService, readerService) = SetupService(unitOfWork, mapper); + + + var user = await SetupSeriesAndUser(context, unitOfWork); + + var firstChapter = await unitOfWork.ChapterRepository.GetChapterAsync(1); + Assert.NotNull(firstChapter); + + // Mark Chapter 1 as read + await readerService.SaveReadingProgress(new ProgressDto + { + VolumeId = firstChapter.VolumeId, + ChapterId = firstChapter.Id, + PageNum = 2, // 10 total pages + SeriesId = 1, + LibraryId = 1, + BookScrollId = null, + LastModifiedUtc = default + }, user.Id); + + // Generate Series Feed and validate first element is a Continue From Chapter 2 + var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest + { + ApiKey = user.ApiKey, + Prefix = OpdsService.DefaultApiPrefix, + BaseUrl = string.Empty, + UserId = user.Id, + EntityId = 1, + PageNumber = 0 + }); + + Assert.NotEmpty(feed.Entries); + Assert.Contains(OpdsService.QuarterReadingProgressIcon, feed.Entries.First().Title); + } + + + [Fact] + public async Task HalfProgressEncoding() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (opdsService, readerService) = SetupService(unitOfWork, mapper); + + + var user = await SetupSeriesAndUser(context, unitOfWork); + + var firstChapter = await unitOfWork.ChapterRepository.GetChapterAsync(1); + Assert.NotNull(firstChapter); + + // Mark Chapter 1 as read + await readerService.SaveReadingProgress(new ProgressDto + { + VolumeId = firstChapter.VolumeId, + ChapterId = firstChapter.Id, + PageNum = 5, // 10 total pages + SeriesId = 1, + LibraryId = 1, + BookScrollId = null, + LastModifiedUtc = default + }, user.Id); + + // Generate Series Feed and validate first element is a Continue From Chapter 2 + var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest + { + ApiKey = user.ApiKey, + Prefix = OpdsService.DefaultApiPrefix, + BaseUrl = string.Empty, + UserId = user.Id, + EntityId = 1, + PageNumber = 0 + }); + + Assert.NotEmpty(feed.Entries); + Assert.Contains(OpdsService.HalfReadingProgressIcon, feed.Entries.First().Title); + } + + [Fact] + public async Task AboveHalfProgressEncoding() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (opdsService, readerService) = SetupService(unitOfWork, mapper); + + + var user = await SetupSeriesAndUser(context, unitOfWork); + + var firstChapter = await unitOfWork.ChapterRepository.GetChapterAsync(1); + Assert.NotNull(firstChapter); + + // Mark Chapter 1 as read + await readerService.SaveReadingProgress(new ProgressDto + { + VolumeId = firstChapter.VolumeId, + ChapterId = firstChapter.Id, + PageNum = 7, // 10 total pages + SeriesId = 1, + LibraryId = 1, + BookScrollId = null, + LastModifiedUtc = default + }, user.Id); + + // Generate Series Feed and validate first element is a Continue From Chapter 2 + var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest + { + ApiKey = user.ApiKey, + Prefix = OpdsService.DefaultApiPrefix, + BaseUrl = string.Empty, + UserId = user.Id, + EntityId = 1, + PageNumber = 0 + }); + + Assert.NotEmpty(feed.Entries); + Assert.Contains(OpdsService.AboveHalfReadingProgressIcon, feed.Entries.First().Title); + } + + [Fact] + public async Task FullProgressEncoding() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (opdsService, readerService) = SetupService(unitOfWork, mapper); + + + var user = await SetupSeriesAndUser(context, unitOfWork); + + var firstChapter = await unitOfWork.ChapterRepository.GetChapterAsync(1); + Assert.NotNull(firstChapter); + + // Mark Chapter 1 as read + await readerService.SaveReadingProgress(new ProgressDto + { + VolumeId = firstChapter.VolumeId, + ChapterId = firstChapter.Id, + PageNum = 10, // 10 total pages + SeriesId = 1, + LibraryId = 1, + BookScrollId = null, + LastModifiedUtc = default + }, user.Id); + + // Generate Series Feed and validate first element is a Continue From Chapter 2 + var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest + { + ApiKey = user.ApiKey, + Prefix = OpdsService.DefaultApiPrefix, + BaseUrl = string.Empty, + UserId = user.Id, + EntityId = 1, + PageNumber = 0 + }); + + Assert.NotEmpty(feed.Entries); + Assert.Equal(3, feed.Entries.Count); + Assert.Contains(OpdsService.FullReadingProgressIcon, feed.Entries[1].Title); // The continue from will show the 2nd chapter + } + + #endregion + + #region Entity Feeds + + [Fact] + public async Task PaginationWorks() + { + var (unitOfWork, context, mapper) = await CreateDatabase(); + var (opdsService, readerService) = SetupService(unitOfWork, mapper); + var user = await SetupSeriesAndUser(context, unitOfWork, 100); + + var libs = await unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(1); + var feed = await opdsService.GetSeriesFromLibrary(new OpdsItemsFromEntityIdRequest() + { + ApiKey = user.ApiKey, + Prefix = OpdsService.DefaultApiPrefix, + BaseUrl = string.Empty, + UserId = user.Id, + EntityId = libs.First().Id, + PageNumber = OpdsService.FirstPageNumber + }); + + Assert.Equal(OpdsService.PageSize, feed.Entries.Count); + var feed2 = await opdsService.GetSeriesFromLibrary(new OpdsItemsFromEntityIdRequest() + { + ApiKey = user.ApiKey, + Prefix = OpdsService.DefaultApiPrefix, + BaseUrl = string.Empty, + UserId = user.Id, + EntityId = libs.First().Id, + PageNumber = OpdsService.FirstPageNumber + }); + Assert.Equal(OpdsService.PageSize, feed.Entries.Count); + + // Ensure there is no overlap + Assert.NotSame(feed.Entries.Select(e => e.Id), feed2.Entries.Select(e => e.Id)); + + + + + } + #endregion + + #region Detail Feeds + #endregion + +} diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 5b65f295c..9c2086874 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -26,7 +26,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb { private readonly ITestOutputHelper _testOutputHelper = testOutputHelper; - private async Task Setup(IUnitOfWork unitOfWork) + private ReaderService Setup(IUnitOfWork unitOfWork) { return new ReaderService(unitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), @@ -52,7 +52,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task CapPageToChapterTest() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -82,7 +82,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task SaveReadingProgress_ShouldCreateNewEntity() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -121,7 +121,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task SaveReadingProgress_ShouldUpdateExisting() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -178,7 +178,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task MarkChaptersAsReadTest() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -219,7 +219,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task MarkChapterAsUnreadTest() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -267,7 +267,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb { // V1 -> V2 var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -309,7 +309,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb { // V1 -> V2 var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1-2") @@ -343,7 +343,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb { // V1 -> V2 var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1.0") @@ -387,7 +387,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolume() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -427,7 +427,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolumeWithFloat() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -468,7 +468,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldRollIntoChaptersFromVolume() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -503,7 +503,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldRollIntoNextChapter_WhenVolumesAreOnlyOneChapter_AndNextChapterIs0() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -544,7 +544,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromSpecial() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -575,7 +575,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromVolume() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -601,7 +601,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -628,7 +628,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials_FirstIsVolume() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -657,7 +657,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -696,7 +696,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldMoveFromVolumeToSpecial_NoLooseLeafChapters() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -738,7 +738,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldMoveFromLooseLeafChapterToSpecial() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -777,7 +777,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromSpecial_WithVolumeAndLooseLeafChapters() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -817,7 +817,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldMoveFromSpecialToSpecial() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -858,7 +858,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolume_WhenAllVolumesHaveAChapterToo() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -898,7 +898,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb { // V1 -> V2 var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -940,7 +940,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb { // V1 -> V2 var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -979,7 +979,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldGetPrevVolume_2() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -1031,7 +1031,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -1073,7 +1073,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldMoveFromSpecialToVolume() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -1111,7 +1111,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldFindNoPrevChapterFromVolume() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -1141,7 +1141,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldFindNoPrevChapterFromVolumeWithZeroChapter() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -1170,7 +1170,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldFindNoPrevChapterFromVolumeWithZeroChapterAndHasNormalChapters() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -1201,7 +1201,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldFindNoPrevChapterFromVolumeWithZeroChapterAndHasNormalChapters2() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -1247,7 +1247,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldFindNoPrevChapterFromChapter() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -1277,7 +1277,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldMoveFromSpecialToSpecial() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -1321,7 +1321,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldMoveFromChapterToVolume() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -1356,7 +1356,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume_WhenAllVolumesHaveAChapterToo() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -1390,7 +1390,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstVolume_NoProgress() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").Build()) @@ -1434,7 +1434,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstVolume_WhenFirstVolumeIsAlsoTaggedAsChapter1_WithProgress() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1").WithPages(3).Build()) @@ -1474,7 +1474,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstVolume_WhenFirstVolumeIsAlsoTaggedAsChapter1Through11_WithProgress() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1", "1-11").WithPages(3).Build()) @@ -1514,7 +1514,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstNonSpecial() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) @@ -1580,7 +1580,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstNonSpecial2() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") // Loose chapters .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -1655,7 +1655,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenHasSpecial() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") // Loose chapters .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -1686,7 +1686,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstSpecial() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) @@ -1748,7 +1748,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenNonRead_LooseLeafChaptersAndVolumes() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("230").WithPages(1).Build()) @@ -1785,7 +1785,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnLooseChapter_WhenAllVolumesRead_HasSpecialAndLooseChapters_Unread() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("100").WithPages(1).Build()) @@ -1841,7 +1841,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnLooseChapter_WhenAllVolumesAndAFewLooseChaptersRead() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("100").WithPages(1).Build()) @@ -1900,7 +1900,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllRead() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) @@ -1958,7 +1958,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllReadAndAllChapters() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -1999,7 +1999,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstSpecial_WhenAllReadAndAllChapters() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -2058,7 +2058,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnFirstVolumeChapter_WhenPreExistingProgress() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -2111,7 +2111,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnUnreadSingleVolume_WhenThereAreSomeSingleVolumesBeforeLooseLeafChapters() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var readChapter1 = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build(); var readChapter2 = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build(); var volume = new VolumeBuilder("3").WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()).Build(); @@ -2178,7 +2178,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_ShouldReturnLastLooseChapter() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) @@ -2274,7 +2274,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task GetContinuePoint_DuplicateIssueNumberBetweenChapters() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) @@ -2357,7 +2357,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task MarkChaptersUntilAsRead_ShouldMarkAllChaptersAsRead() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -2397,7 +2397,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task MarkChaptersUntilAsRead_ShouldMarkUptTillChapterNumberAsRead() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -2439,7 +2439,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task MarkChaptersUntilAsRead_ShouldMarkAsRead_OnlyVolumesWithChapter0() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -2475,7 +2475,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task MarkChaptersUntilAsRead_ShouldMarkAsReadAnythingUntil() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -2544,7 +2544,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task MarkSeriesAsReadTest() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") @@ -2585,7 +2585,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task MarkSeriesAsUnreadTest() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -2659,7 +2659,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task MarkVolumesUntilAsRead_ShouldMarkVolumesAsRead() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) @@ -2717,7 +2717,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb public async Task MarkVolumesUntilAsRead_ShouldMarkChapterBasedVolumesAsRead() { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("10").WithPages(1).Build()) @@ -2796,7 +2796,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb { var (unitOfWork, context, _) = await CreateDatabase(); - var readerService = await Setup(unitOfWork); + var readerService = Setup(unitOfWork); var files = wides.Select((b, i) => new FileDimensionDto() {PageNumber = i, Height = 1, Width = 1, FileName = string.Empty, IsWide = b}).ToList(); var pairs = readerService.GetPairs(files); diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 304d25d1b..0930a0690 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -1057,6 +1057,56 @@ public class ScannerServiceTests: AbstractDbTest Assert.Contains(postLib.Series, x => x.Name == "Futoku No Guild"); } + /// + /// Test that if we have 3 directories with + /// + [Fact] + public async Task ScanLibrary_MetadataDisabled_NoCombining() + { + var (unitOfWork, _, _) = await CreateDatabase(); + var scannerHelper = new ScannerHelper(unitOfWork, _testOutputHelper); + + const string testcase = "Series with slight differences No Metadata - Manga.json"; + + // Mark every series with metadata saying it belongs to Toradora + var infos = new Dictionary + { + {"Toradora v01.cbz", new ComicInfo() + { + Series = "Toradora", + }}, + {"Toradora! v01.cbz", new ComicInfo() + { + Series = "Toradora", + }}, + {"Toradora! Light Novel v01.cbz", new ComicInfo() + { + Series = "Toradora", + }}, + + }; + + var library = await scannerHelper.GenerateScannerData(testcase, infos); + + // Disable metadata + library.EnableMetadata = false; + unitOfWork.LibraryRepository.Update(library); + await unitOfWork.CommitAsync(); + + var scanner = scannerHelper.CreateServices(); + await scanner.ScanLibrary(library.Id); + + var postLib = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + + // Validate that there are 2 series + Assert.NotNull(postLib); + Assert.Equal(3, postLib.Series.Count); + + Assert.Contains(postLib.Series, x => x.Name == "Toradora"); + Assert.Contains(postLib.Series, x => x.Name == "Toradora!"); + Assert.Contains(postLib.Series, x => x.Name == "Toradora! Light Novel"); + } + [Fact] public async Task ScanLibrary_SortName_NoPrefix() { diff --git a/API.Tests/Services/Test Data/OpdsService/test.zip b/API.Tests/Services/Test Data/OpdsService/test.zip new file mode 100644 index 0000000000000000000000000000000000000000..db38cd60f186684f798dc54876fc2c5ecc837648 GIT binary patch literal 238889 zcmV(;K-<4iO9KQH00008033@oTRRabhijw*0CVXB02}}S0CHtvWMy(KY+-qCb#yIZ zZf&2;bMCo!@3+?e%vxPN{p+r-{;le& zUsZkk_w?^?018z_6-5960RTYo=Lh&b2apHcCMLN>LVWwyty`p|x9^bC-XkX?BY#9i zeV>+v>FLuaOphP4aRPbRo(VjE{FqmUPe4RWQc{wYM@~^zToEWCDfX942uMju$?uXg z+`Gph#{QUH>_5!!FM#{Ehzdx8i3sik2=5aR-6#0n4S4jYkVFK30r3BW=*CS#Vgiy| zf2{Nb1O)%R5kNrr$4>lv7I2q{06<7VMDgd?Wh&r*7sR4`x#hi*XE%#ZLS%4LE5~mo zF1rTXF8e?I%}Z~-|EwQIZ|C06L;Vu)Acg9kLxpA#S15r)#b3SZ>oK{--umS;w>)Sb zBKP${J`wV`=*Z*oJ&SwnZ^`sdJP*G<1l;MXG|MCeporD3c$k2k(mgPGI*S{?+y zeA4&k53jI$zPm?Vq^H;R7iT-yVgTk&I{b3vr+agEB3Fpt%7A55CGVe!0%(Wzt(gLe zPJRPK=DL=z*q1EHpYeG=>pIMX`kcQ8{4JDCrJ=(S?xl*}_Sv=oIXTksLl2hGA3GRv z;N9^e^*-SQfxmV+n@q@g7xKLq($8*X>O6AtlIFYEdGpN=bGeVJ0aBUA2dl3qWsGEa z_+JWz0s#J(r$0j^PO02CM>kh4$2YDeu=AmS|0)bK6FV4J)~dr{gcsb zx|?*KRF|*`;J*waouItD?V#+#;Dn^ew*aR7^fo1YaNqPrFkk#pP`T7n`sMcMA<4G1 z%;@CdRR`I5^AsPtGhtruWcE2UC%EpW5WRd<=nw< z1~ZS3A6 zYi+=PgsG3fXIY`|*RnUbF~~ z82Wsp`m1EMCqF?**l16RUmS<&2Fp7-D}FB30ndt4Wk;&$ZtAzto9{EswqdvZMJxS~ z88|JQv2G``75nPy7Q7O!MKhjLaxMU~V-}tNe;0xr^YvjEq2Bb-$xm@V=}yv@j7*hM zR>&cX*MR>B{z0~JN9Lf66ugnhNA|t%>lfCecq2V7`pT9%_z^9+IYz5h9wVH#*5s*L z-?Hyh^ZaNGmgOa`rvm%*_R zD||(Hi}Mb;CX>f%VcK@J18o@U01=-t+NAn)uqOi<2&O+Ah3*C-cYC#@yZo&! zp5udER{{Tk2wU$JQ!bhO1g>oqT1J!!EJ95m*XN*~`XjlKJl4BGQ72iFtqYsO?Xyrv zH9Shlu1%|?89TnC%a^0eN0~w6ui`!!K3?8mr6!TpnL*=#F;%a1w>h$OKBwKfJNHvG zW>Q&;nzRg2BSCGoSOO2=M$7BMS>I~bfjnNk1pKG@IXNemqN8SqPm6Bf18#~$>(0Yj zS0wrtz3<*#Qm`Lo1_F7@t@P&$h6A_79S>~NG22Wr5|d-BnE0KD7o|-nSt*7)saDHi z>G#h~*DI$d5tff~q1Y-s?H%32#g+(TJ?WYhdv9JvNk_9w*EV6^vhH-FL8bCK`z&mu zzuh7a$mL;HmU~vKh}cZ4M4I0=i8kOL5Gj>7`-1d7!2c!)b}Hajl9>+y|DS_sT2Z># zVmimUl^A79HZ%qovrQzNw*G-vgT=k+cF5-fmJN->s zW0#m9`qAj)Ew;@}-{W6P)Xi=(dI6we zXg8|boA%-ASztZZ<;qUt?fKp{(tkN3KnY?03GknWVDp4qNnrLxLeyc+iZ?3fSJw2x zUIsGG1l%)!(UZF(k*9vOVlg}5DRFqwSCBQ7j8~uOQ>POi8#HW8?o9_~&rGFaJ6!AX zfb2*1!YmF6YN9xeG$4?*Tm#N4?t|!~Tdzvy_TjfT$AkPa9ri{HB@H?IFq|8ETKf@B z^PojrC^N#f6j}EObP%}chEQ$A#2)vxbOZhYAu?H4=~SVFYqy#AE!bgwgk24W=O9C` zmeG_0W=cvbWR1MmeVQ}Fm5M!?VqHAz_&u-sB=3QzsIapRy61rZGVbXiAj3@yR!(vJ zMX~|)4rddk#N2JzG2*e{C@x7?d2l*p`u$QK%;rP+wx;k^1jCP-1wgtp00L zPEcKTHk@@*h0Do2eW$V7p=M0K1}KfR7cMPs!5KKR0(teh4EQ$Xb2s+sji&-IbXMX~ zBJnLJk)S4S`XNC*R*x~HbE$ECJft|uK%iwKwnFK~ngbkOss;l)`At_cTqI*&W_cJ) z9L_ApL8D0u(?vifFeUqE3V?qfOpQYW-f}6c4x1^S_Vj$+2X>1^;`QeBK`Q(;l(PR_ zv`5q?Wugz`7C^RmaL-{JQ)rYCxemJa2G&u_5hW6?#Rv0?@)G{C-2S~vQa$5LDe0f+#WiF&vEU6x4Sd@l=3+Cu~Sh@gp4I{~X7? z=4vUr@vc{yXkF9Rq*rBME5*{^%RSh#!+<8*c!RRssf=fT10>Sn_iaKq!cd6OHC&%& zh+q|w1PF(%Lu_$4cCm5!Y93YiW1<2ycgtWFE!Zedrxngt=-{6-kXB~hZ7{ug7z<`w zxlk0^yqTx=HQ3X$q9r>MKH=f*W9U)&Co;}{4fszJKpJg)l8~`L)zUs36D97ppW`wA zgaz3Sjzfct%I~(=PE5oy;saovIB95Yx`${{$)(uXXWb#qfa5DM_xLnH&sJ+!ld(R~ zltl8SKv7mSx)*+<8ML^5zJfP$b>n#|qh};OgziA&ob0}@e{N|AOh^-uhxuNNECPM^ zEHd9e%R0C7v%=in&O#?IsUM+(-TomXI^cJBH;QxB!!cFUHW$A&##5q@k!!{PbWS#@ zwB=B1nxZ<}OEbp#rEk8Dt+nq(S@&Ty!prEvk*o|^qIKH4EauVJLC)dyk@?Z4*hP_S z<2syJwR1J40e70y0IwTby3}j3%1$Nt=4Q@m&9g6_(w0h|8E+d(knkfB>Hme6?)YLD z8{qT?EL{!G_#R-wJ!Hy(w!&5vH?kfU@qMb2kX?0w3vEMVBm9xuwb$Q$%Cx2}Rdy*}S>>)T1?2b;{c ztPGVFo~LsCMZ`|M$b8-2?~6igeOg*T3#|GK1emMl=f}~d*Xh^1`2i6M_{W2!O1+4` zM%L5HOaS2zQ#ae{!3z4*%2*L4`;SCNgr}y6NSpxi$pza}SkMr7MopOY96~)b)3CDy z=D6?T2o^}hIQDv%(OI;VPb5yPh@+7cI5XM~yHj3;R~Anso^EEF`CBYHsGtoYvCdx8 z_D^*$Urx5=S_yY1$qfoSs2z_!&pe#^Ux+WHG_vSAc^4!y*-_PjI7rL*{iB8LJU&Rf z3D$GA2r*}RZexfV=EC!L=^;*Y4Z)fX)gFR26HWAYBKa_QBl;ekH?kZ-iH?Ljw>=v` zba%PXKwMV1IC7g$kS(6H?3UTsZXN9AJQyeeoWi6b53fScNf5i zkcM8y>Eiz-K?%VQxHysly(nb8HD!T@njm<+aYs%jr(Vs|^Fe-2?hBZqTXSQqSxzd_ zJS;qFQ2`=1A-p!0NeSj#&Q{}DQ_aIo#2r$p{0P{}l(jDe0%Q+NTWn7)t=^{cszm$# z>i>6@LdAY3UD`g~a@Z%yO)uWRrd&ew&rv?Am%L_^*z;PiK)%dxlL9Nd9+uAmVdKwL&WQM^g#&P|tt=#t~Sp>Ketbe+m=teBSG4ntF@)N4bfKt|4 z#)CanVeTF15Fc!x^q5=TKtRpC;j+^GKBcs5!-jSxt_;0JDH@0#XddfnZz)CTy|y8F z6<8B2-NeX}^@B1vVH~RI@2YkV--kS(Y{?LV9Ci3zxC}pA-`;II9L65lA`uQ*uec5+ zkh%WY8kuqFeeu%d{k$Q_fnURg=hZ!Wv<6ulzJ_Qnuj~7i2x543yZt1vRB9>#bJ{i> zI)8O1qIl67Tf8WZSucyl1$Bz#`bhwown0b6^NluY8+#6>@a%Y)F4oYwlF!@O@R4B^ z#~Tewvh^vZ%qu}H;k6tP4Ws~ZnN>nLLySk@C093pf`-{EJ(4TXHirsD(9-#G%{mnt zWS2ZTi_3<2Mu8x#>BX&5)ej zfYHsU^#)YVqJKgAjslmbyAL;BE6HpC=Ob8Ice7I?NRR*3!KxPySu&&bEPJy;V5q_0 z8qb@GinA-TAJp(?v@4!$V+*qZAzG9cG<=I-=WU6A|ML*;9kunnbp;vg6)guUSGT2R zjoCZI-tj4UFP+DoWneSEgTCE8WinL9l#CZzpry}js37b`YUQb2iDBZt!ui+7jl`R? zE;8=RIjQkXhAmT~ZM`elOYx6(tMD=+EeO5IEqnI6IM~qUqR#-`7{mq4NgsQewa=rW z?(CGVQf|Fgrw!UjV1U_8O94{_T^udgqg(YczieZGrS;^V3?q^o$=AacTN4aLqAwEpEQ#SDrkcKT;_ z%u10RdU{dwNKa$}6l#(#fTl_>Zn77f?66C=*SPq2gwsnO1*KVw?}PTmKj?bgouB)+ z{dL=U3R^s}B~7FdcqB5BN^=2myPe6Ke3gvKLHP+i#X32n2VgwtXM$ikW%jLY8c*Ny zm?q#sCxm-uVpIUE#EA~k%Rrk(@r&hS)hE)7!?sG^ak>LD?MBJ8p|k2RjHiwJadKsq zSC>7gRnfb|%!v^{QjZpAd6Bb|3773cfR?pB+a;k**B{0;c z@L^P;AaqN`B+Rqgo!4>Fv>wX?DsS~1$9Af%p5$4=zPh_3Ha$+_+V}7H&iA$>})@gTHUAF~fuj?!IF2N`lM+MKY z1Y$SQDeVEUwXbFIRW2*b!*N^lL5S7}3+oQa;{ZsJ`=bR!JMO6I&v;aYtZsQ>6AIMR zjj!8oIsU|6BZasqybFq^F)=>L^z5n+GIP;Lc`Dp{kUd;hvYC)EMSZw}#$32qMM`Ix z^ijva{9SW4jy7q^{CwOvh9-kn)I>1rzdG=Q&oykPg*3N}bE;(xS>$PifRsz~kD_Pe!HhKA4?*1)kndl$Pc6}HCO@lk`@PqV8KW?5^jPhu-h3{_ua0_{Dv*)@#{ z?_{D~)H0*skD6{P4~OdxR8k+K?C+PrzJoH-tvi$js}a)(-;LSk?qR2)3o>g73A|s~ zu%U?fAZh-;?LHdk$@tP*vJQLHvZkS&P?A+0Peyr*IJQn25@X^c6}x31XC;WQ=*e;NFxrEKZ$)`F!OpFd+b1>bHI1Fq#p75^F0v@cRDhh; zCRsyBvzZXVzO4)HL9)P>*53u1HiIC14nhT)-wddaOCbrqPcVn-(4(8Hqoe2*!! zA78b*5}gh`Xtnpa6P-1Smgt4^9F$|nyDSRFm2llf=@(cjzbi1f)!GuF-=C%HR~zCQ8Udy`e2Odr8v(opzd zQ&oy71C3;(uc29vF9TI%wB~v&S@Y#;l_RUYanE^-!`msfOYy$!fq9o)*Wq&~IVpN!dhvm)KrCne= zlqrJj;zl?>Xh$adZd3&hlI=Py!*G#`L@d}_SWGQjWi<(}<5PxAH(fdLdQ-$=rwFt= z7cEy*CjD3fOKVn7W=+x!)J|1PWxAd*aWC@Aa5h_>I&#+{7KQkL1IzIHWglJfHx29W z?CZT+cG^kh#Q2%!>BOgOGpUI$EsAANU;~l5+rlMd$dqLqrpdlL+HG)KCLauAGOq+x z!n0$9OH+LV+)CeN1y*HL*v>Rh^}aR>1Y6dK`R#$FYi_4M+C1WM(Of&rRa|<*SOPh$ z|Anzt3}_H&erwYftcZDlY=~%-+~R`??4^m!felU5E{h4 z9~WyuyPHjqkBL9unP2pS`US|Q@XZMoRdhT|+u(YbCCJ_E13VqBUl%NLv(s-o)Gcv` z`ECD?FQF6Y)Plgni~;bUgV0piu5k9 zV#u5kfzf$g*bkmx?407OU~lH_HtVU1OVse4$0@Z z!K^tdS2+ltScz8jbnv}VY=&N?s{ItJpEOM>foF1O4F$G39QS%VO%Bg*$0;LyAbIuc zue>ds>NO0Cl7|c`y#<5y)gb(23>xW6oGgYWww&OE7wu$U#}_t9)hCs6eHqy}D3JX``E zW?+(qZ7=}oF@a5eJr~)tRVJ~GJ}Noq-d z1@r2HnXPyA=ZDuiq*f%vn-DudEy0_vZVV})nHG9%DbQ4%74Y|?3Er07#Kl1Vj)eyy~r|#>iC^xpSUw zdR|qRl6VekOC;x30kEXq2iC1XbpnhvTQbBNW}Ht|HTS!zs$bzzUFcwWzd779{Op}L zwZLUE{qCl*H64$pbg6ByP)Q|XLnDX{2e#J3v0;X`Sw0cB1Qux9T~1`I1MeP%Zaj++UF)`s-P=SW0 znIIC?HhpsL&S=%IZHuh!fp$79bVXvnd>(y5<#>$YHO1={_nLdKfUN7o{dZn?K?lVq zG3%$i3V&s0`Hrv?8Ys_V-2YK%A|b-7l^&)58LmGziQP)f$XLg3oA~(&y3WrCaY+sf zIc+BKRkUD0ESxoz29Fjr)OQ+$)&+2TNU->sUP>Hq51p6mn+lJ4W$3&+d}6A<7il1o z6^k^cbT?_#!6M8K+o38SX(Eu7aavY`XOlsi-09+&T7u(gC|*5iZOq(r+h~uwHFaxG z*SZY0IgH~=LR3Pe5-A6t^zaT7F@_7N$CShYNA znH?_0R&Lp<**=>`VuPEfc3%+JV)d7P(ijLk}Ntx$QIM`z0ya(UcWLm8C$Yg!$YII+*|DNL(Q{JL{@OX8H;WC z4^Fk-n9XXL7*I6L!uVB)h}6OTSM+j^5NEPHtd)O7QZtX#6K*W!*fwm}#^RTF1#xk` zUsg&g>Qc)LqMdQUu8l}t3%j-@Y0vStXy2*4iGLuuQZ9z=m|fomxpp)c=P_vMZl^z$ zqcKreiK8B(KNZHS2qSxe7GS|LsU1kaor}FKSV7N9RV3I|%MoFV(Z=NY{r3@RDrSG}Yl(ey8TXDQ1 zQOuf@uQq+0k3gL<#@)lNd(UadZq}+cJMJKk#-@a#!#0Tw_FT~1vMyoO;K@Lw6Sic> zEE*)KI>s_lnQcp-gZ2kGM-K{&Lb?8?dw|YKu4^Xq#><#rTo@bnUZk~z3bhqeM1K69 zGnxuVe$*6a%ZdXva@gRU*FQBMpxPmEdgYaJDm0Jg%{Aie+ubTzB@ZutuYhxEB0mj`DWcd|7P5Emdz5n&921a#i2>S`Saq?~SU<0Z2X zy{Cs5jyq@{jXSLq=z@Pa5Ih7Afe>I;N<8k%*SD_D^%dZ3fdM{m>)z5s?Dv*IeA|`Z za2$#~9s8_QD-EAMvWkp)jo5}Mw&B~WGdgTQxOZep-?fUTqQw???6!qV*FK&|E&?~2 z{vWB=;G*{fti3)0@b}`jYtXAn75^$B%)~a0DVXhc5O1lXYua#H)y{%Ev&vc(eVSsw zwK!f+VhAhJ!r_#@6sM`nW-j> z+&CO;TGw%8t!K~4HK+=B{2UFpe?~GUgcncOdZPc|t$JLY8>PPM58UqDAtGFaUkYR+^k$u7ZdA1e)Y4POu^4TMp z*oB$v9d@V`9s%}PA#hnApJNd51mVT;#(!L_DKMOc!M&tkI}Df2UPS&B-di>9qG8~o zRi0{C6DaMFu7gfwH2K>_@ELqwP`WiXKZtEpwl?*ELM&J((x0D*3`=o$yR)TDshZH| z*cRDRU#O<7=ht7_PZs912JxfNWiXuGb2Jih0F$*>F$jj1VCxTLdEp{FhoddFVt$CT z(Q<1=OObzDAV6S;G2Q~-(F+h+OPr|#O-~8C!+9YZnd~iEty)xDcNZA#TV8W#ajjCG zI}Ocm;f@NjySb>KPP^S|lTdyH4C6~dwXMs~J)4CY+$&-65Ir~Ci7?UK1W{Wi7q|1c zsMQJB;1FH5U&o#B`%)0NcR6~TAHff_L02LU;*2KtbrT7B;xMUm3JMh$}sMF}E_0h>xu>bstsgd3T=0dEE=%!HT@8^N zf(VS}V@<_cvm>6AN>*r`>+;&ZglLO@B5=&v@X<>rYRqj9NM=bI7iv~digKmoYQ2VM z)#=}UK*|}U!2x{d+*~Ca=CI{yKeSbP9FR%5on{E^T6Fp;HV%4%(mSbrYwO%PVM=Qg^(RzV*V|;$xH)Q&2=*URm zK*XYOS-7-YpBiSGWLD*giqA9`I~cCgusTVnjV)?o0-kWsC|Wjm+W%ctD}(_;>7#T4 z2h=DYTlTefCk8HqhbM=x=cu72pVCLSj4MQ&FEH~?!I>YRC9`U&#wJm$U0m6`>+Ad6 zUg@CFoCNwIT#%0@B=Bm}t4|^;(A5Hkzp$P-i^ zOSkveW?Ufu?22EWc-J%WRH&W1V22A{35Fhvb()~je1q3V&=)5uc2fVRMdk#RHZ2p4 zI`BIKk2>gqctNkdU>ksxHG;MZN73TrPp&e-l zHQAV*NqcmOC;enOzmwBFOQWeT1OI@^YAwY6iW%sqks4)M2gU|05;Lvk%7pBKS%b2` zx2j5pmKp==pQphSWDqWR3qQ!R1Z(~L#ND4ja%3H^UF!J)<2v_Jqr+Turdk^`H`h{R zbRMtYQfuT7Ws%(Ckr-@5w(+W%n4h|zcpVnF>HGwtu?RQDqfJZS7VZJ2SSKloFJyLx z#m(n1CEdhK6<09JCz6VKJz8-PS&G`B2^0h`5^k{iOa`GY+|@5fnhYHtJu)5Coj7^e zV$iLnT?W3Hj8RRRj0mzW zd1fmvDO<6s3{C#ZfO_t90a>Dw$Ch&=u!4aHT_7A-Ej7=sp8MZ+7x5-po^5^F?k|{L zbMjn^S4$SQxM3~DxDB=j7QmZ70863i=m#^s!D<<1P)0jHxL$Ia8qBoi+%?!P?QJre zEYGg)Cv4sXmB)+1m>{`r76g}gKHK*s3!-3Rz9zTx^Nd!tEAeBg9oqscp8LR+-8|YI zu67f8=pekfj@o%&q7tD7vzrFY@wkcS{0yrD)1q5&1TBcGbm9&TZ4BP;7v^wl!T|Rc zS)+!K7m?QA-8Me*v`8LMk8O1RIF#jNq2_^ZxyQp315&>nwiFpp&&MG71P#SKRv5XU z)o*IVhP{5mVlt09$m}^STeA)hXQ!JLugmVk?ENM?@Owb-PL>k&%k3LAcDvw#b@mRX zG6AiJK9+QSe~*@ma7xo6GNEW&G$1$nh4TqrON^EtgWhqx|B=a%y^Ev0v`SU?=%R-^ z;_F0dP#vtA-hcOK7(W*!=##w`9wS0=ERG4>LSb7+V;KF6*EV(&SUcnKq+m5&ZiK5^ zOt@>@(AaU~y5ru_;0bDVsD2T$#(hjbJipJqluWihEag)odfqhDRxegz&X2-xDm6OW z=J5vCHR23`TYLP+IbX4GVkQ)&-VEzoW@=Q&K8--X#Q{pGja;obGh>K%b-+DLjx~OrGUe;TG zd~dO{>#rv41JlI?+na;tYv-@bA5fea`fWPphV{aq%Kg>wj2DTF>!B9mQ&eevt1q}U z(MX3x%;0g=$SC@Yq^gYmxQf*h8X!M=2icIvR~{)lM;Coi3#v7Z+ukq%8q^X2MpFBv z2qlIg;jB8nyZe;%Gm}C!W%F&K;~x_-JYYe4KlZ_(dP(qJiEmBG8Qf+D7ktzd!?ZRJ z<{g@yip})qwKhQ!^JvKKIG=O7i(w29$8*s3jx8&@jX&W@aqH0N;R}ec-PW3t@3iq+ z3}~ch&%_`b-as~I|vPvMESS3RE*&kdGW^RE~+sNT5h-O=ZdrD zlAWcg6Q>Zq?mvSy0l}KKoQs^HsY9={ z4<7P&SxUqrH~3MqOz*p-{(Ap*PuE6&l&KWTYpX=L1-1u?$gsEIT+|3Ss0|0rRgkZ7 zKBnEV$MY=-j9l9^J28x6IENVzkZqN%e+y+!(w>tC&U-~TExqD-9YxX-TTLnxQppCYI4}aurDyaZJm+Sxz}{j*zxD;*6g_BdQyg?1-9J^+ zTp(^{{T=JGdXkGcnQ5OiK)1|c_GTzM*K(co3`O#|U@rSKbuH0QALE}4=L;>bCk`K2 zOqm@X*_j^5dpk?M+upOTqQ(xIjA;#@1TDJUXaTDE{z6;&(Y%#js$~LqsaRdC-kkrPtVVuv=TUBaA;4GdYrp2b#*dm4d{v;g8Hc+3| zRJ66ISv2fqk*Y8CCWgn9RU>?;Ud$KDW7l1;uj7D7v#W8V_uVvEFK$YqK`;_d=Gh^u z?b2J^cmD0N0f-E-S+>MTX_x4&K@iUTuL$ypCR56|sQec2<9P2GxCkgus;o1Xdn;Yj zf7bx-vK9DvB3pb5kG2j@@vY&~?XFt^%?^o3G-(=PAj1V;cNgG!k2(`TNEr8e!ACvu z8g!xT!&!?Mh)`ex5Wa~luf+D1Ba@5mXIfNjIqX4gJ*{2$?R86`_%wWb+{(;alFI-* z^YGVjA%3)74dw}lWmk5FxQ`2{lL7u7JVTC~&Ee++jW>g5ehNd88+VqfHqi3d6_FC+R@zt)S@g)vj zJY3mvhH7J@6Hr0%70=;0F5eC-vz&dsz^(PUag5zQ6$*CSw{d7l(AdAXWMT2Sg#DhI zL9g?Po7s^kj=LYiGZ8s<8;IYEP3SH>}JGR|F4>kwBbgH34-hxzl8aShHc z*z2j8XM)?C7U<*?zJ(Q~JhR~T>ce^ZWIBG2lFIuUd|T6BN{k+-cc`p@o}3TKQ}4P zZ3z@F_nmd_*xC9a%_$GN90TMqXd4MOq$&L=wG6>;@|PQ~`o>5}BNu%nlR*kDO0#XA z>qngskv*H<@~PBx+_nr2cRBTmFw~|bZl)duohm-MIXK3a-e20?%E*6@H`fyc_&Xrv z6BjJywk_i{pKCQQIBBS8go(2klo-baws&@dnT=N%_KjnVifj6enjTLyad7Yn8bP8w z+qW|lpgzC?dnj7ZJ?@}gEjy048p+*cjoEUn0X0CfZ}wN9N|5a{D(5F~cj78^=C(|v z`gQZMpOe}7?XiFi)k%ca`o+QuEyL=k#UQB5n|91FBw0G64&4xd?}fzrGuqRNV`}}2 zm{1-iaB^ogdp~%bWj#^Fn7~R!yae>LP7%C0N{G!oHG>YyLyiPJ>TD}*6lVq}GzULr z^hxnR*}igS!42#z=n|`dOh_N`51=X4vqjC^!WW2*cI%YH8SYVT#F%FZWvU7E8c74O z*AwStfai%w*QV1!b|QpqkG%hFuQ7t^VD5+km=B_d=iwA$rKnQ0w`HQhJ|EkLbHYRR z_@-t&R@7INtxP^vW&40uge82&Stk+Yh1@ON-tNG=4%;%e=XCT=3Jx(I*L(|_wgud+ zLv>I6V{NOh)_DVpC|u3Z9d>v3B%~83r;B)Y*CmasuyJ?9>mga9EGp~`u1*-gier@= zd!k7rYWoKjS@oVZ9sKFT!BNt<@RrROdVe%S8J`q?OS<>5E_&B*J5MQ_3}suyzMhlD z5?{Iq`7E99p0eD6c7cGcF)_-U5qE$7uLNgS9ul zRLR&W8VxEzM>pwclbCtGO5%1UIBOk@lE$hrB`t~1R?G$m#<-Q5 zYu#u56M`8pBkT^^9XuCP#*k{B9s<*a0(hpZ(U^P8?T+KqbY>)QQ;nry@ zHF2ueZ=dghDIpf1bvHV|B5Q?}*%=MsHtsp!0`6CD7wvjW${`UV#OtND&U@7dt|pWl zGsX>G3XdGcjj`MFYgye8MYC0=JkYu;4s^Rd{M=6N#3K4ACeP1JMtXh@ug5SuaVD@U z+Ss|;EDzM-7dqiSM4j(lAM}=qPLS!^TY}I(gr^z&@F*;7V+ya?Fm9>)-H;s@thQcyHQATf=*VMlA)|N9^@1gA;sTQyoTrjL)R$i` zWu>GcO*?clJpP8W#z5FD7YF3sI|w@V$V>38elmH~4BnSGv)<)MhuKa8P8#K5ep|

GK#cQq<{H_;6_g*ndDcLtFFtuzgbEhg~dDSjO>NhaPJFm0qbI(d_h z#9JHN{O?yDP|lz@Urc%9cgug-Lj~L87=}o_ zi~-P2J5ohYcVbFdHhr+b_Hs*&t&T@57%VL4K^p)n15QFc`pkngt$BQ$;i+mzpgQ2k z+|D7JJc(V&!%O7IGvEzm#%xyRq*>V_H{RyF#LU;hiTanrHzKzzEZgp- z?9VP`_Xqq2aIA^o|3*%?@0rH09hx5X0{&w#m!8YWx?xVPo`qztNG`caY+Q?cJvsOn z+sXNM)8TIbWJgNo!SKO_j`#U*Kw3bEz~9OeejYsEY1wk%dlD!!cx8F_|Fw`s)da0x zk;Mn+@ELVq{%XPk{x&3=(r!qRgkyi0WX5GjbDq|Lv`p|@lD~}OPxQ)4&MjMT+bi(g z5ufuAXn>69-^gu5M(sSLY0V#*86tX-7m4|Ax7o7L>axe20>%Aah+Z-CorI9B?Y{v0 zO^6giULFQ*2X@Rz=f#~bg{1eBjQ?e3gy&RsNZ{(DCHT?Oa zza_xQzA@4*`^;_R@?X`rtG1pC7jlca*6-_QS2E9ze*^A)7x|kX4DS5PL>B?wfZ3m< zQJ+NVZ_R!XYzdAZGm9)J;je2V*HHg02)GxNVAOuTR0Ypd$TRwSsWfvw__w5-1kL{Y zZrND{-;dw1yz0`5909zD{o_!gDjA!hOWVKoD&3Pin1sD)h04iRIeM)QN>SnFQD3x+~6R@(Q4hgRF#e*<2k*UwHawaS40e z)n03r*9PS|yd(Yd>pCt?~RoY394*f2qk# ziAR6yn|Xq*viHIbW%NGP#YcX3j+>BcHB$?C_pVV?Y=CFWRxfhxLPl}Df6?h+vuG;M zcd_2jr9;)$bFs!uH^%J3+H4#tQz}y?wm;yx^lia!->_5|Cn(tRGp@xGm7Nn{1zCd+ z`!2Nn;*~fILMlaCigo_=0!>NTn|a!WFol|VR*Cc!eLrT_ir|k2{5v3fcApc%N-MwI zy{-N)|2Z+C0KzLH=ncTX7m|uke_SW`^qW>F(f8xO9PuEpqF?&~;2(VUbuCc(QTbUH z$4x*`LYdO^1x#stnYQ?hcI#$fO#^b5`&@o4D8DB3*4e$tPXOt>r)xpdM4LYV_d)^p zB1eKO-$~a@^NsI#bs6n=1zMJkf0I27W!9=q?Rp)Q8e|zcP4xYgTI|-XSC+xN(yhaG#Ru0X5AN zJp)4ulE+W^U$d|Z$cC~B>b!j+1QdQLr>%dB_Kg?&Gj|Wjdl5yu-?M;Q1OxyAO2Dt0 z?Y&Nw%MYeCt>@1U$y__PSM&OTt^YLr)AUc%KTZEM{nPXxF?Ch*YV2Z!*4K;h z&DTFrzX9h8*xvxF?c}!|*Sx81PLUP&I4JVAv*mClt2%`rg3@ycr_w(+?*Y80ZqJTg z`h{WnN#41u-y?7IB~~0$Kbv4AyPwcQ!tv3u1kKh!`s(YIs8AKPKpQm-c;oW+Mt|#N z+14$iblIzG{BGV!v34HV}71I?`~?svU8(GjtksT_6 zsw`}!KyGTm#^+)P5`f0_9^{U=Pl{?*rztrfb>xKzpYUGC>gX^6Eew--2MMX~&VRaX z7Db)+8z3a}i*G0-q}1bWWXK^`2#HqwVwQiJ3fXS}e{Nk!oss2o|6K4l7E43!FQT+J zrLC1*z86qy@xOe_Ja7Eq?n`3jL8}`cGIbbm%c754VMNh;&1%oF!F?-wX>O46qY%(F z#_x6TJu%L_OQK&wYjHAwVcERvyF_}oohgavlAc`WxDm-GZ*vLqOS?%I{Q?9Q9rb@< z{tbxaSTQj(GCqP(12z&K^X)BP@ohO=47VKb52L4k16qfF19mP}yBsc`|8m>TuvG}^;<4M%(!I}{1UK`J zccrCDj#=IP;Mc2{W7hQaRq(6K$Lw*Gi8!D-INUrpq%daELC(_=Q@6P>G=@H9c!+6; z`A8wNOMTM0(gf2IPGi*#0QeX9SGBj3`V4E)Ppa_A?XzKAECd7h-!oJ&^p=4N>Babb8`vDnfPFn&Tvdo{t2nO&XAY8hsh!$njP4<_#89k!}=;DN&T|Mx{X7oWF)o zw%e+9i|!c5dWndaKvH=qGA4$0(?UUr@sVhd%fsMg{^puT4G6RJ*=FI}JNQ)2^}|XO zoNKvi#t!gfxlVf>d0ILZcBVw|8_>1z8*ssct>rA3*GrWgBVOffDBqt&D@j!LSrc(O z%w!FC&6Bok5hIf_HGl2S)gyyi8>%<$7J^?Z>#s=v(7E$*?t6R0N}XqtQ?XHK2w8M^ z&$*d}NVr=>Q>M*Ge_JCEB**UupY`8-SWY>E)Y{jEf&z&qHeL@|_wAcrw zC4xi9F2^8Y!l;NTUu7|+$JSp$89&wPha^fi-1fQ*Lh?oYMi)dS*!|Rxq%)FuB08d!Res0q>{x@J(2=eYXphL9lPZjGr zscvDu{;?HuF&9$v8xWEAVPWYiu9N_tYir{YAq+6Rl`GLwO5?lai!Zfx#v%J{R*)H+=*w2-Bh#5Kk4i47AIi4L zjV}XhT%eA#TpftR=u1Qs&7q?3DAC>y4|_uh8=hOwuH!p!XL> zxx&y@=N4~YDB%=Q^WbMz9~7K{$uX5?F`9!_th4V7@^3aXHsxO>PFw$40wF#(_!;(4 z)RWc|)0pyF%?&m&R$6KwgG4Gx-YZ)1 zwtU*Bk9BDNGAy`+G$?o3|N3FsQ{Ljtnn`pfBi}VL*=={zQM{;XUeENDqX8qB%Jpbd%i`&MKTN`x`*@a0qf${9ZoK?TgHlJ!Z?070d3y zg0Ail+DG?qsa1Ghi^($^2?~b!zbh>#89phTVBcb}W%@$!UR|^AG!n7;&^@bWWkz9c zO<@jNGb16w-HIN*X8#TFkU=ro{%ZcyR+h@3r0svzdf^zdT;8Z;4kghZuLN8?0)tHa z;(z!;nCE_dXU-L+{0+GDvt%Dz^Y1GvpMJW#OmJmS1U`);?|g^+rEvZ4*>)k*H@-Pa zH$oLt*Brt)yEoTnF!pu{$uWmbkZdU0%G-QBiz9)(EvLyoWse$V<;b2aci%wf<;+## zZ-C`LlYb`vO#YetGx^_~XnT9ta_qJuC!GK~zJd_f^}x>THz9m((ly(QqsHN-Q6GnW zx1U%vOT6g^JB=iye3cgok-N$s9_Zq$9|J$zV329B-2NZ7Tj?~5Z7x8a1b>s2P^S79 z{GuPfB!)RT^E1?(pL1}K)81#45nW}WUl`wbwD|9N+=cLKsuWH&;x%6v?g zH_LbOe{Und_?%Yv*x<$PU)x?Df+sZekAk5jr|({EkM%0 z{d6yRDM@9{{;$ga>7$w{g^Hgs#b0&_zF>d#=GMvt(f5L$!*-d27Nt0+3cZ3pjz8rf z@xT&=Jo$9l>|JWQ9!%TEIL~+3JG%Wv{6~EI)0$iVdgsgPuZOanMIKglTAr_d100&V z)?&g*egoncPpc2YHqAwDg?#F|^*_DzpF97b*+G!CozVtWkiFYy%qaHg6!d3tG3dxT z{|ylJVOvi9)gPp$ymA?td}ul!_K6V??f4teQu&?HSIOf3R2bwtV~E{H(n;ZyUm@{n z;Won{StC_-J(!8W82mwQOCif|09loDQQJ{f;EUit=X+5A zzN-OR)sD9?IREH%_cuV3!G_ag$S2PM&{)GUt}->&kn$VwkzNM}u~``KV7X)&5+^{p9DSmv=}ZlUTalk)05D&jEbvSIGJvc}I?vzJtLoJNz>GftDXz z0HEaSwM(hNBFQ%u8|HK03j!6U)A1lu`QHF((@xV1?UCCWLN_=oLdqUBOJ?7^br0L< zRG?U$wchr1%`q?Z6N#$F+vl1m2bUz*rP+Kgm1MO?DW00@=nESM625$55@s{GZx$a2 zLH!T=Q$sRJcmkd{f@<-Xk!g-)@wOne-m6pI$Pd2(Q|`Txz)NXX6;-K^o5tx`r2JIA zLek1FRv2#5s5+kAvl4?7Roj1+R)Wj$YPN*}Riu*WhNPXq+ugB+~wzsq?Ab zXh5q>`j*3o$dm~MCi&p1*FWT7EQC8etocj^cCulB7ryjX-8UYa@`(XbR{ZmcJ;M75 z*}IdQJJAXC@XvjpX(YD}7VZ$4OQT-jq3NV(D(Z6`Vc^NE9_;t|%G0HC%)lc}P19AuQ2uR6 zd&FUqQ3vT1o8F_`!smw!Ct#BIRGSnJSHu?!W>cS2-HFIkdRIzKJ~5SWGwek#%xIy$ zbuyjD^I2^?lqAC9xdD9fs>mTHk`5mcCUetuyAh zkxA5LX5)3Ja4VbhrjECut+9wVGBR#OmdB+_Uu39m2wTNHTkmrk=<=NB(SSA^Dc!+~$=K&Tvv*Qw=~HJDnX0TUe`4XI28t!G zHK^S?ygSP2^Om{4u{sq%5rov=*9Uf_WQbH-5vU4vGDA7oh&?gW4VAI&*t*_iFP1v=532;))?bj-lWF!Zk{#6VU6&PP^Ws%Hx_QpQVG$yp zOE>Uim*${mqDuHtQh03apHVY$q;Q);xg)$ls{3o!;al}m5x3K=HMCJ4Uz074QD!{^ zSCu|qOT#5Pb~60d)@}FJ0%N25){+cPI>bOGXHN{I`5Tb5y=^(uB7E!&i}ZfCf2IFb zk03^X==OWc%Bup`(3hsi$FD=~i}>xGq)}>5XzN_x3YqdVg9$m={gj2ZMsf-i^>5>X zllAA0D~u^_2%W;2G&>CE#~XfmWD;+Bh;}JTIBY*&Z!X?Em`in-^K^YT?3Gx2ZU{36 zr|lTJARoWvNP)G<%kaIBb734bEFF$7#b=Lgwa%WdX+?4~4;+;W=|;ze(FIUH zO-fRbWUT-Ko~7<**`YNbu5Yku;g)(PvO|Z1z%I?r$khuV%E!f`g8ZgvdmGdB6LGa+ z`Gz&c5T&jNkT>#E4X*m+`5R$Ba_e)Nl9oGqN~-O}7fR2E%Be<8ygnJIZ!u=f=j#iN z+ACSCxICsJm)4VS`+}-6`E;>iIV_QhtI#mrANQ&lnK7tV3+^mkYnznKFU!k$_0 zG(N0xcW_9BBhSO=4)R$6sz9R_3|zD99|{U3YM(GEAKZ2JcqD4nbH9kOelUzl5gYU< zi$lf%F*SN8g^C9-JJmtu+(zD?lo@gTCBHztDOEt%L&kmY`lJMBciC;h=!x`Ys!iH# zuVuNm_-K^UkVo0_d=ZP@X&-4=O28VdHbn4TRUbvXRgIUE;~BucXGm$#se`VCM!X44sb{+Lsoa;}Ms zpS9F&>*n_#hSeRPj#4NF#|j_%c=G;wde38)Bp_+E6FZqky&>@>FV>Id%qeC_2(r%n zLQm_uNGp05F7ssM@oxaB`S(XYRs)7|9^~`}YX{@CLSw7XM`TvQGMMZha{>)YOxx05 zBBuCP+GwKdA0%qe)bMRii!0Zr{s{;^v)_CaplhKLy&?#Bb^~J47OwqG_RDZ=!KJ5e zyD1dTzZv`Bo9Jm$PAs8JKf6W-y(2M$HW#%tMUbMXVfS1NN?E8J^>pDOQx_u+8C(Wd zxk+CZwQe{Gdxg4z4}a<;ZbzJjcuz(|>2w~puUjYk8*nr6hs?w0+cDn)Ub%G{Ve2Nr z1{1HHH27S(URAC{oaIq0lK=2Kx=BRxsy-5^e@?j>G3BJ{kbj>(YG|oPbAT+@!7j{U(`AC7rwhuF07=F zph>qA55o*UjiW4>vVLgGa#JifKXCuHeZsm~NA`HD%6qQ1b>p^hZT?HG_$@#~$(;;c zsi-vfpt47Vj%op=%=4nvGp&ZLH?V_|ervBUMVR7Dk}dQZtXh~f&J_r9&dg>X00noi zWU{Dl-y6I(mY`ZP`L6F9=tlVCi5tU&{>%ei37o!I?L+Zrf)uxQ@6aT*B{)6ic~D8Z z4F4!mY!&XrP%%=;A6#hIgY_nf5U$ET2>?PA%Cm%aEbD<=)fy#B2LUlslOFN{@6t*Q zg{L>C>2FulkaAIK)Fz3HK>~VIx*kbt68gI_;&f&SE9bWo@5ue6o6b)c#>m`y^{P=k zUaDOGs-WryCB+g$yS?3evpM}W?RO7Ev7dhQP9lSA?5fLA4^*@FrSBU{Vy*1UKmF+l zZH~e}wJz~LIxweigx_L89r;W9X*h#VD=?|W%91wUoUI)UOZI>i&v&HOq2_HUXVA+E z0}F{p%XdX)L-e=kZYV@;w)^O)K(hd^Y#x6<69-BS)V}*z@^ptB_lRaiC)4yx{Yd&F zj|=J}$UACn<=fRoiK8nD_9E<91wM3znh95L=d7nQr9ks-H~Tk^EKz3SSIHCtK_c;B z4gpV1wK~8PfhhdhPFVR!yYnzYZ#F1pYFx`=ZOrB=DB| z(}>8L>>{MfMK@Gs(4z4ClEZ!_eZbqZE%s4uaqe@3M=R_6LKiOQYG=X^De-SW=zQ zeX9qF?t6ZPp$2;6uGRBf=5j)OY+udQ6*`b5U2Q-3nu(d$G9jM+5FB)R#MMyVkv(fwZGY@@T4bZ!^6LV1+UphvvQ z?U8?jfm67Gxwue$ZVeQAZ9+yJ?TX=?STG}xe2OhFr-OWr+JGp)Xq+z~9T8yJrd3Hl)#Ef8r_!bPWLb(3(%Ioor zN`sTjA58N*=sl$Nu;em>dA+UztDbWoFTheDpq(|RVZNG}$B+V5-8arpnczb5hf=i(!##MA)> z10-Tb?t<$-sXpQfYY6IZ=EI$)jPyBgODan8WGC&A{W zC3nNGE=`mC8-D$WQF#^EUZj~@vf7#`37{a&Ea@+26>!cl`ROt75+#$|lQTgw0jX#K zUQ^bL0*Hd622JWsU$Z12NqT6!`1_MTBM6MgpdRKnZ*{m6+mT`8HgQHk_*6il_E zvG|pY?l1}xf9e9-pOpr!`eL&+WdfP)g*m85p5-UrD7bK>%cs`3IK4vWJ!|n;dh_nQ z#(8aikhJ=I@!;$gQ(!ufA>G@7aT@iy_px+IXfb%KhW?4>PWcpV(*a^;(wXrrreVs= zI(iA7USFBc_8UOi{iiGJpYTH^(g#r6Rg}EwswjL*m|0wUSo}_NbX7JubqWZ<#$0AOXxT?Df#kG?nTl(aJ>Fykl15Ca-ep|lEGtx9BC6G%*(_P3?lfbh|o> zj!eCFtxs*VW;JmzwS?dxvAZW6O0$c^Fd?-ilgE$4NSQxS`F2drRTB9bzI=Svqnhq) zX5Nq@>DW@aEf{AxCV5QxK;4<3fhk@H@2eV}L>v zpB}$fkGkW#UZ28~m26)8_^T29wd^NLZMM8JrFgJqIqiL2ySltyp2~M+j`>+)jESsR5|kp_HR#lGZVe)g>vDI^Jx!(y8NXOZC|MJE{T8uxO9etfc>Ogxkr@~wOBNB8i9>4$V_1|Qik`Nt1kjqccu zYWFD0Wlz!z^EIa#KQ2~OFRu0K#a9?C-&>CKQ+kBUj)MbV)0fkUNsCobFgXB@0&($0 zF*}!vGi88U=Tg-rhRM<&3en&0$sAga2z4YWeR{wO_hRP8)@0j7C%#vLy|5;R(xh#T zTD9H?ZNGf{e zzfL+nUMjI^deHG`=b5?DwTfu>8bWo>dz`n18CtIxisWi9iEJyBSJu?$yW_ZLqJ5lw zZ!dmrVDi?#9c^o;-O^&vP$hu;_M|emhi*E0Y}vIA#JC^+*$nk3q9J14T5gW)vD%!s zI27bH_j{seFp^?O%+x8CfH~eJHw0#++opqloQxX6cF^07 zBW?HDfY~vIlOUqjQQ{blgZ`~b=@S5PJz24VbC4$~?}zWRma+RvDARNO4iy88a27N1 z-Bo`oFY-ElCA&~{sp(&S@3>|MiuMCtJ>RV2io4*aBC0ui0>7-6XF( ztxj0@O&2RQY^OdL)=l=wJO8DWS&d?K^~%29dFQj2*MdOL`;mqz*i|v3EL zCQfJ>_c^#L_oS6M4m7g0w=VS?aLT+7VA-6Je}LXfKzpU``yN~EJQKcRcJQnxpI6x4 za!!9*<+K1IZE%~|w==u7PP>jCDOCDw_tNz3+lESgg$pCu6vM;(TlmBgalOP($PERZ zm+gGkSN!LSJ_VE`{yTW^DoaA)l0Rk4iVIs)Q&Lg3S*Z}1JPAG*Nf#47>c9gMbb2mA$R!qN=G{1FI zSfEjmFv7!)EZ-FZ_eaa5E>DbRR~T%q8)mH^$K}`%KbCh(AFs+DbTEznT=TX<@-Ww; z$8a7|GG1xpa#Cc|G`P=R;5R>V(((Z!r zSDt6pg2k~_VnG=;#bZ5GV@ur?#z8Skjc*$5sN2%;>93b}j0ZErA&=sd)*qS>HC%VA zTCyyW>G(y&w|`wHbv(e33Q?EQ3>*koP`}-jILlwnX_BwC#oXQ&@nI_XMh833nHIIN z-?uJ4)Hv|J$J57^t*Pckzjntk**Wp^VS95unK-7=o|~kLFF+93^6_@-EQC7^Y4_|& z_h6c8>6y3iwkM)7}A_`a(5i3uxx4^FH~^5HNG=eM@i6Q`LnRsX|Wa)?V&G znWx2@suA!{qUeuqk;|Tg8d1`8MB2cFTQh60YW?w?Hcy%a}T_>@R~T1c7Yu7n>}`eu{k zEt679LT6G7T7gbi1AER?!etkF1M(GdqF{{vB^i~-@_BHclaLgVQIFHTFS2L!29vf` zy04w;srEXj1Xt>QOwdi3{|3A(Gm6_bNuf6Q_{8y9?_u z`Q@<$vDwI$$k9u|aN(NrigEE?lPuspFE_kC)$?2AMB8Ezks^-_QxWvFBO-2RrxT62 zqp4zHj*QrQbE%tu5#_hpvndhKJY_eQP5VwS*zNI&7&+VV!u_A)!fxT6Ej+Wcrr!Jd z>491@((`$$rKpi63+B6QfX1guqjXh)gu|WD_m%ak`uh}%ha9$_V+fHYu;ADg8Q$Lj zZ0)lc+_$klZ>OWYeGUU|G`Mw`9ow15B|T4`{)i{?s z2*9s1P%BYPg%j!$5s|?%c4+!2KA@ zrTQ_8eD!?SgJExDAlwpEzo0s>ItrI7=D6eAeBa3EnV>U}qo;4E`D02-S5h)0F{*tq zrfu)O;cTy}Q&{8Wpit?)PE*Y6>I1K*_%Vip39y|5s52Vdes6TtJY0+WwzzkDlJ|S| zsi7coDqc~Q%*w7`Y`+0U0`sCD67^OKo&dyVGJZA*jyxxYT$B+(u<)2txx0fVD9Z;XBD}N4wzF|H8;T9;hbQYdbH9h@jaKNND)32w>)R4 z9a$ZF28v4Uf9+Jg2GqFaJ}pL}>)VT|&Awd#_~2D;m%upLuK2u)v1crP8YTEumM*;< zzMoEIY6I#f(!NN3P4QXaPsJe3iRV06(*wx(k!Q-I8aEw0^eq7X9NgcNc^AL9Z>m`x z8F`$Uq(yQM>WuPB;Hi1hpKea-RX64D^t^V+VP+jKDxn^@v+rQDcIx5BqE=3VZl%S%Dpi89#W*?P73J8D$BoqS?~ z);2>Kr#EdcX840`Sn=zR!FOSwlo%9c-C;l5#AvKi6FxMZIBkpFtx7YM`23bwlJthC z%rmDhn!r03Hp*J3D#4Z%uPYh9h%98Lr1aH1C4DE|<0rtOqd-7 zYo3M9MT$8IsFc|}uhH#aZ!}dU4}S0)un{MzqW4DDMpK)D(dLaF>Ybe_iFlRHOu4Av zt?`vC%nhFov+-{;pC)1q!Hm8-xZOi^^K~KNfOvBSmyrHmQMAKN4O+=<){TDIntS(u z34*^n(NZUCa7?bS&d;O8^t)Cn)I*e4W`7b!C#6Wc5jhQsqGm9VfuM(>Z!3s4a;6_k{q=1VRI_1gXX*z^P zbL#E<7$RjJnBL3Vq4MereGm%aC{(^x`5lIl1D9mzDV&nixM6yphgx64CNcGf-+g!W zf~n~n!t%@apNdX@X?m`)W>XmFN4_u|2M+DN$PJnyO>(20*UCd{60PvjK)j1K=3Gp< z(mE8Dt@d~t!qm(a4PWHt)xLIwi8TP1<*1L8`{*W*ew-z-JwJU~DYO834i1(fiOjlK zb}Vr}g@Xz!`K<0%q|FcRtxWq<)h8nB&((_LS@%r?4$I;C%bHY-DehEGe?kC%)mL~U zI=@S+%Gr+$CC(Gh13iX$G|k6`$(`AcWjK8ok-4i1Z;07O-hb%syrnlp^>`|fXslLf zR`E}h7+P#43R7+Jqoz;S)sW8y$a=GG4&}o#5S*`KaiN?aO+}DPMv~4a+6|C^_=_9k9Viy%YQU=?^(-S}fEzBK~>_$p04K>N$ssJ1};jr?_m zxA8oU$0Y?X!^Z)R@2O69@o2i?_^9)!$GqoeLzGhjyq= z*^5zWN0fBG*fNu&^w}~2G`V_qL|-w{L|66bDc>F}STuqg7g&L3EkCfZ_;<-w(~S-1 zd>-vQAu{FsB-dF|;wgM(B9vT2imK4X^|b|2QJY|$Kah#HsFoiB-EB4B8j_?Oc~oeU zscjFyUcTq()TP)KpvLJD2SG{tAhKKPQ*`X-{TNNQ!lPO;&i=E?0Dk_c0wDPWtF09? zYxk;+1|Fbib7MsT)$2yzRk(A}9X1|WK1+U=>;cgW-GrvpZV~#6@_^Eh(bp`VViSlN zg{jh1V_oXiaa3GziD={T%R(FLu8X3li6brVPEUUhKEHif<(N7BQ9x`lhBM)i{9a|6 zk}EcDP9;B6y~N?Wq5FZ`hVc5BB>ji#2bpfz3x22A={zD3T73n}76KuNU&5Tc(cON}^b^3nU?BfsK+7e4C34-r$_%L5x^2FQnvr42c zg`1M>1xktSvji#3G3|v|#Ry?WlQw1c5u-2Irc>nm7d-2Z6_}hf0Ocve=TkG2KJQ&q zo&*tt&XbwuDGoq5)o(y;&GpBR`pr*zUsoTjs;)&>b{I^C(4o3tJ~0s;)a>+M|Dn^a z2CT$eRgF74Hf2kDBnUE^m36G#-}xOZ(#xF~$? zlag1tt#ptTN$=o-gdS%;ihmcgDdZf-F--pwf5Xzu=iEFco1Gibh*zJLEBHuFGWyRw_(0J|k%M@;<1(aznaBPB`;? zo#GmWS2q8exOULX5BXQ|?Dn4c{kOnR+`WktCu?H=T3j&^#N`&C!)F}!(a2C^WoWz- z60#m~`$V!nU$Y_wRAf*q`0_#jvIhqa#^|I`K?%2H9K8$6nlgbqO$icO5OD2ojQ1XZNSCjqPtiv7hW z$l-bTcuU3LEAu-mLhb)~75$NZgSv78~Vk@Kl z!nq{j1dCB_sqt8N{X#8A{eD+h6y%p?<_O`K_C3R<152mv*ZbWvq8v(=UfMe4Trw)M zV&9b)>fGj=?*8PR-=UCs;UWH5-|Zvl=jP}M!ShuR#cj_o$L)g8B-1)dv{XBtOPTN0 z=yTOS-&w~O@|y0vkOA(RVOmjYYI9Zj;a^lRt0#FyrM&Vn!WYn#(DC6S=;aoP!}8`` z5Yk7F6j#GM*Cu83cbL0P3gUrAuc z&!tW07OFy%|_}hP0U18vJfKy#XJ!Zen%s6r9aa1*vx{j!_X!T6?%jW}fp~m*2^w!Q> z$v4yq=@lLY=*|6kQh0?Z;pU(fy`uxg3-(tL5iV8tpxOlA%A*9C_)vo24i4kV=d_IC zG;S881_r#fw#!c>wVXq+841=uI4XNjs#KyYx+xk`mgyin2dx@%`Fz3$`7;AA6!T3q z)K_1Qy?r;y@36(eva&fF$6L2l+}StJFHOvz3# zH|7kbnBAe-{g=I*sdu8^dPU4_Wyz{5xg~pup7d^hGA*?vbtbitn4Hvey|TIYUjGO{ zxnfF4VUd?J0O)sc$2d}Al>p`xO9{HwRoG;`xBpj6ntz= zlUPKNMfAJJrAs~6;P4Z@g;U7d34qX5>a!#5Ldd_qvC1985E$n@zFm^;dJ@1_eTNOl z%#QZGPBuSy$a3dfua zFcg1H8OazHctkIjE~sMdsQqH(2&CWJ)zj8;s3|jlONj&vdoR+$-X0}S0GU@R_%Db^IqiAJ;!<$U* zH=e-aLeyC+Sm-FU8M#PQ8ah6`9PH%qvA5blZyDrl$Y&%##aGYMj+|CilQ_ZKDs2_b zvxXZ}tabxE>6eb)@ib5?+bOQVH+RosegmGR_1Bf){RTT|%=0!;g8fHVpQflmj8EX( zg90t$4ilKY4-yb^eVaH$Em>loi&MkL8e>1UEu#hX&Fu9DAO~Lfu69X67HRe~@3bzb z8zKUnjZ2mLfkucbY`m-s?djfpCC{0~jI*NawSw{I6p1lV{-SAw1!H&wW@n{kzl9N9}mP6Yz93~3zpUBaj(>ZyZ=CqpCQ7*!%w5RU&GH-xNuZCs69mIS_U%JGKD_otsd0Bx-7gXD6by)({Rx~ieFIsC0wm{ ze#vArup!27^PXU!QQNOB&%P>-e!oawx@=Ingk+|9PHfV{4)z_A{~JIVSs zaH3GLtzDF4_I7my?o?N#^o4NP%>Ux33KbWSMhqwa>0QT%Yf=PDr|H(wXa~$bpXzY> zc~~Rz#aHg*Cqf8L4 zAkwlbFwiesS)HY7(2UZ1HpG%|#M`{yLrP6Xdu`8}y$(V8k@J`cgfgHMP!A)yw$FY% zR|1ztVY2S=Tyg(O_2e9j(GyDJWVhO0(?=J_ka$0eIe6PTIW>e#FAQTGfMg#z%WR2@ zVr%juyQ4Ui>KF}H#Bp0a4Ms#tkptY)QiTt6S?^1K?`Hvz1ZA%%yeRZz4*lf5V0TTe zhYgCVT=7Y88o9+q%^J}3ZP$%xV#UH<*Iv_&RH8D*x#Lr@eq_?XD`j_#ViM_Csd9RP z)^hr1Dl(f$#*viJvhD_pDI)gzHxRoCie(2z>sk&%0y^pOZjqwPqj*v|27s^069+J* z+}*Sz*~o`8Vu9gwiLY)m-0u-m=SwNMmG$}=KQ83aVI0g{z`yewF+?^$`Pm*9O|(y%uhuIk-< zvSq~ZME4DeLt;`_n(O<|HGnh$yBiw^9RVk_E>yp!94}wbiPEE9u}!kG&fR_&!Z<0L zPg@9XqZ=-BJx)iYg52K)iZ6 z6;eGdEd}(QY@C71=-g4$#_YsQQ1be}r+xNhEicXUm3n2U@7+%WEAH!`oWxIT)Er*j ztICE-MtW_J0E$!g-jc7}w75k`!B8SE-4F7% zPj9%y^Vb1RVWPT;Aq;fmrjdl3x_N8|e9JM-x5t~q>~L_!r=qSx?+6?@hO6}NDAH@I zcgbM`6b#b=iWJrpZyYt~+HNsNP#>KZsr+2vsJ_oe#3uOAM9>ix1t=}+8-}l#5`dtl zTHoZbhDu-tIo_Lk`PAmOXdZFJZKKxT!52O}WfW+EzZ^?D%NcrQ(AVaax}aS)^?~|+ z8<7fm?FVJ~%Ea3r1er6sr4OkF$A&4>@r+@n=q-xMRzTfFs#YEKUno$ z)+pCH(DjDiYJ*OFw%g=FYxw;PE)q+Da6?=RAIi{70XW(c>o^#LLeP!NmMYBW@$?Gd z9YxTh8x1>$KoC~$gS=G%d)H@RLI?$n4PP+pQ6!VUMnzfCFMphNUyhTZ&oxSSQX%`I zan9v&KBQVcextei=bEr18>6GXg32rQA(>TuI!N-pyXq~ZQyL=?FHPHu)cKK4`T^@} zB$?+|eHxPSjd7|~2EIq^;Tji@f2LKLx8*}}5@Bt}F6(P7bu-Wj{ql?cxgia1h}r4) z7TmzWm>91u@9wL4>mpURm-~+4BcT@6sF4rJ@6c1|nmENG?5Y^;^7rH2Awh+bM;32o z)Lr{^XNt<)#M6|P&h>(wIT;5H*}2|$xCc1ofjC5cX*k(=*Mz+uTB1LHF# zw}gF9!d<8u;qyqIGwtt=5C-4ca88DT>)zs}O?tnn*iF5k)4%*bzkTWkddM(45^f^l z)=KvJY04;BgN1kLZYs}jfRGVpRZMIWUMJ9?fqlxDQB%_QaIPR6X;*%xAj5c zB>qG-eUC`U)b@dMG<2q3lbb#d&*teyRz4fb$SkEtlq9W^CXRCDg;rV_i!NS?xNiX?Xh zZ~o}tfTSCDGd>S}`gu7#bttc*Nz|&6Gn0?4cjb)uWI_UxgDN}@jM%+{n;3i^{_43x ziBbq7oszoU){_alHluezem@`Lq&DB{koc`02UsYGwWUTr6lEx?QN$5(1kf%xR|m*E zn?I)VptdYcw7qE{GP0_ly52L_Dl%q6Y3<#irOx}L^TvAWpW&+N1mPiX@q)=<(N&~J zxrCol&jS$(Id6Q3Hs8JXD99d3PDTWAoRfNe<)+A3J)D@>h~*6FJOPU6mc z{$=;;6|>}{rai=u_Q+7l9PJULM-4lb)F=NrEs{bb;WF0`oQ;|)LgTwV=9jYs5e_k0u7MAQp1a{~G+vD`xFKklXE5aQA5@)QDP~^M4(`BK^msd> zp0d#m+>PYO4=w+$ve0Hc$tf}GH;8Fd5}bX}df@mQFyZl5R*WjZzVM_9U>`+7GXwt2 zQFf$KVa8@l0$PQt7DrwOstZ&vl;vL5`sE1a?5MT`^tTtUTpEq5L{Rvea5hnEJbuO* z>1R=56&_XN7<~6*)K)IVQxS1%4ti+A<8|VFCZO}tZ&|GlT1ohrgOB; z-qa1Fh`F5;prPe-n=}3S&E)&3`Bj}F$mNbLClJ8H-)plovN&0HK}9pk=TY{wD9!_} zg*|YV_I%6d5G2M_Q5lCz<-}BQ*A(g(k13ISzwt~$Wlbo9l6R<( z)ic<-H)4m!pWk?4J=tG7tSS?c4`ACDN+Kcgu9Zz$EwG zmkon`W-liQzlt2}zPeciaky1U#ItFprfA?bZ=eIzzq|V`egQ*KS_(H`R#o=J6<~Y; z6*@L+jv3uuGOH-{%kJEnhgRgRRpN`D8^S%Fk__JFn>|qlUFA~!}2Btg` zk*+RH*_x?#%S4Q6HxcO=>X}t`XTMi}wp-;WbF2F1g1|x6yuH=Z=WV_7g01u$eaz!* zsL+y!yrg#iZVcJeqKzK#p8?-%_tN@+CJ+DxrO}O&`vIgq-2VRRPOLmC-gk4@EikAO z_6!BxTQAn;T^fYk#9>-@SI@j-e5bYyv&oge^eWz~pu^RmS(@*DiR{OUHm<^wj_;v@ z$HxBEr=cW|ri4*CdPu5ZN=bL>DmU#Kmsb;F--sE^ysso&*d{U&-ch9Sz&^>-%BPye zQZxj$ElQ7YcTV)gifk5EU$)L$+RbX8=wui~W$3u8Kj?yMSL>$~x}C+Ez2_k|lItHd zeGFI>^csgdzSB(|ee$3tPYxRsW$^x180(-?%uZ4}OWtTpBqE7LDyB`Q8VkF}w(4S( z`$}u*8|Q$}ww^dT(f#YNtxaQs`FTi~z|$kVb2g}(GuOmEz+nN)o{KR8y`X1s zdUw0LOu}h)M$p0_bhER;jA-&8GJH`jbxuM!(dbRO2DkPh&&ULAazo=?i?_jLOruq@ zr*a$o4ZDw%M<*8*t2jfy0YUpjwoO0Xt3V6RjAwCFp*K9#j@>Y~iw&QBtDqi6U@oKI zy<_(&K4MFDSMy>2Xd@TBtrnsyqSuf0v5QRt==m4iTviv*jnA{?fpL1aYAvE69qox zX$l-$LGo^))=OAL_A~~2X9nk~r{dXXKks>c+JCiCrG3B8O{Z|~nKOu3btlqd)z64~ z(}(LffIQsx`ShZa+@#W4hoM~Lr{a?F{tZbUowu{3N(u&4+kpv;*IREyKGSfpF`o4% zdw7q!FWcmQpp0gKIN)b10EcXy%rEh&lVoSDVN%ptMYKH{vrioHK3aAgFvZ6jCa4tv zoS)qgXG@(8P6|gfa0)>x=_`6XD|@^g*Ool@(3iJ~3!<~nf#qXm{O;)JD!6sUqxf*b z0&QZnqoLlXIiKJeFftx4&;%V0kONhrzD4U zPa^8xz?-@^*@J@ZhdWDm-d^1EWCaRsn0+nLd1J$@J@@^gztH`(61m?1B!R2GoKFjr z+RiRW$Kpdf$5~&JTSR&LS0#%#M>mBI8l0X<$P1sf-go;{P_+t5817KN`b=4x_3`PN zkVHF|$t2=lb@=69z5+ad|7(Ba|6=Sfyy6PlE?%&4cMBTa-95NFG}34iT!TBoN$}v% zxOX>=H?9fp?h@RCYe+KhckkS}?>B4Btosj~wNBMjb?T|J_ixirk1XbTu{#4hac-l< z&3`8AYx2%p=962WTPEn7W;2y0*@41pFBbUrQ*kFJK-NmX#*gmiohXbRJzdb*6+JQ> zKw=Y1`{HE|0>2Wj%<6=&f?ayGlioPPs6#sDwntlnXNiJ;Xc*0$C2y3etb={Cq9yQfX5~DZU@<{>;9+ksCW0iUQ($iB zmN-gj3=}hfmpC+8j7^UzLJk(5NWAt)kH&cLPhomGuFj`RoZL3@n@!-xH_h@7CL-w^ zPHpn;Ht-^PEG?`*i*j45p(=Uqj57TbTM$EDS?hw>8hxq2Cz!KR_N=4_v+cEWX9SS~{HTZ<#~#>7exo&PTdFD~hM39xtnPUs(p#lu^vS z`=?(B@VyjBjeSuffZXTSeanyt$z({{eDHzrz62phH*$&n+r?xeYFBkfcGzcc(}z{g zknv}h(&7-R{e|*N69rd^yZkiOT5t*WFugMjN6P_4hbPHQ?l~!(Sb!Xt~lZ!K;uu`7@<)Yedfcj=Dq-;#e6X8DeN0s5M7paYv6xCiAqiV@whg zifAgtcvWA61)PUgrsR%%sD6IxChW0{TxAXKPM3ua+~51f<#xG zv>XVYn5Z6qNU$266TckQKfqr0LKA3b-dW2P=}F_lv%ons>6bz=Qfh7jO|WsBe1Wxi z)^~f_AyLp78|6ncCtLOxU7AJTxKb`TT0e!{#TRm+U$j#6{8xnoB*T?e)IDdJQr6~k z0Ij_sD5j9Y&oDLp$z+)-Gr!3(wL!)3&(=;C0Yonm*I3_2%Ogw5*t=53D0dgF!dwVv zVIbqxkW`AlmMVX__j|&%Pe((2qoI#XBfbS-PH>NZj4YCWFd1}11nt?c21}#C$H`=D zp~UYkk{21HiRiWK4XLGwnXeuUE2npqqg9jfn6^_{-x}@9)K;CwpB;5enEJ^R>C9@e z+5)iiWoc^BQ%zFr`khRQ;%(;LuSPe+S^j(EiyTO{h()p$204s6%y;?+4))1Xf6NDz zp}nE-dJz8p0zSut_55>cEPZ0@jg$+r69OB-{vq}sfEn)h3X+kji{8MK3~q5ec6QuS zbRC}B&XkX^hx5c|^Q4ym*LW_{VcD8t_j6L|W_`2x)m~bsB908&Ghc!BOD>|kgX^5R zGcz6ZG8G63a=*|$7L;G}zQCGAYD%}VJc7_?1ZWlHWWG7nDVnj;g$=$9#3&oNZl0PD zF_|F=-q7FJ&()TRD|U~nH3LScFmB?>2XblJ3?5;e8g`~XtlF=J{`6dp`^NN$fx$bgApLW48r{-S*kZZSEruTLeqvy^gZ@{wXaP zljHsFcI)kRogFQBb*g$h^%HTiJ%07b?m;3#$khmG2sTP-l_L-;VjKf%@+4TnA*c1a z_oTmN+J726#cfsnUK?$0YP_$ee#T;a2W4H~{a(X9^$)-xj|RgPm1ca~Zr7|;8>0zC zqA1;nfrot4iFXRFr*cQ}2mOA^o~;A~A&J3x{xHsou<0tmHJ9~%Cy47L#m?^kJ=21V z)A5NXtETAiwWM0R)T{&Raehn8Bsoq|8wABNzo)5<=s{ya3 zss{#h|KAW~a%|&_2AvrGGxp&lb>ts@VeR~%)s7;T768moCAK*^(rpQn<^MT6F z`1O_PW$&=lT3MJYF9KfpgVzF5S$e^Y#`JE9+^f+RNey>E9YB;5j{A*tK(3W>tRc#1 zR4*-L`RZ@R)#Tmx@w&N6Zeo#s)ekvezkVsYHKo=vZeSA3iJ(K=6s0Zj_Lmx%)tf`1E;{{GgP_(qtzH5y7q>fSu3*}<&&)l>nkh+lnFOXDA)OF&4b zl+`|npCuP5@oVA(!{3}$kCjd`E-JZwwcq~$-Z;z;9!V@_3N)UAv{IOv@u0FWus;5i zwAm1yaFLtIu{2>sV2VN$zYrk@;g__+=Ih+NA~y4C9g=W2AG_{VQJ;Z%%vzg9a_)sN zTx8@6lJyha7qP{V`Kyqtls7|8_M0=nyh*rTANFUc39lo&txaO_2OqC#(fct!t1rGz z7pp5ZDk0`>9pPRLuEuC!=~qC)kaR*NH-KE>r-b+=)1Toz0aC_!w*~Y7lu(b?{MtCP zPmCQ&aEWtn3RZHg_UE~d#ykRW#iJR80n8IPTRI!yo~zbh_j!@L@kFXs=?e7 zVo2iH;SgT|5N*;-bv-8LKd=6=>X4Cn`j2k5s4fl>bCd(` zv%!J3T4;`bB;GWFAK895Q>TZG7~*_Jv=eF%uY z!<@sdyKa=P+^qifPDjOaGj9cN=99Gzg1q>{d@;p!G|d2-b8F!iJ2>a(=@1ECLUTdU zpp(`vi-%OAW^%=Q$Vd|%^eYtKdl7RcE@@dg6|D!SA)-%)GR`^EeosY7@#2ILDqzF; z6MRA|&(K>O@A!$q!sB2nt5V-T!w40)nP3Kla2TryrF>a}xww)z?B=FQ$k~_d1p~J8 zFjpRfsoMFPsI$@679;WgHqFmS684(GDx zk|Wl)+6dL*UN33TU?d7P+8{eGBPrjmW?J^ zJ_Gm5Ik|XzwSR!IT0;kG148yB^3$MhVM%YVS+#6(0 z;)`lW$x9>!3UC<@d&$_$6EXwxVunW4RAiY+4N9DU5tb?BcT5F3yC_?Yx; z=)7C50kz{xEL3!RbEJlLpRE63h{oP@zLF64{v*ko8Y;q25`_HmRNsCHm%2T`mrPXQ9Uj=eC+?VFH5))w3Pcp;(W zlbs(|dQ86f4^st7z#p)k@J&p9n+?g*wB~R123YBD-aG3@!%W{67;$1mAH4gmkWl88 zVFts(f5KFVFPmF)Housp+Ax}iA@Q5=ZU07gFEiySQqm;7JlY( z{OUnm6=fQvZ|u{6tn=~c#OlIIFG`DoF5V?lyPy~sBT42ef#36EL_i!A#>fh>oRi7b z6Sc8QP2Lk-VUAAx-0gL`1c5TY8)ZvMJ348Ao9XxaT1qh#1u_`%e5=nZBgj@j|`1yu{WjQUmZ zSB)6YB;%a;g{T`W0XF?0r^?kn1?w#eiD!yt)v?C#Qq8^~UGf!Yqn|Z(=n?$t(k5=B zh6;&K(zcn4A(l8I6{lKiec$Bb=oTcuITI2>A~??e+x+TVFU-0}dYZo0o+v4yO**%q z?PO>anYB%bvP<2+MIdWaY_rdXqr&^*74aO#?*D0bT`RDheN>I}Eo(j=n|z3d{zF{;IjE+eTGQFExu9}M!`F?hMQ9b1M`LK6Ng`}<2*HpFS>!J!g%M{1yi&I7A zTvSFqDtXv8rM=LlUWydIj||Zll`2bgUCn$Y{cHNcZrAkHIC#E2;7k;Qw5ScySyv9oVcwf%g zLQ`1PK1axLA$5_7q58e+ve?9u6TAK}!Xy+cf2LWb(J^L&ZGNb#(oHD_!rNaIJuZR2 zB&1sti<$Q9Pwj?BJym7+Lqe9ebW7wVv+_q8f5W$^e*m@F`FI31u-SQCJ*Nq=Zs@l4 znQ8%yk!uEmWJ)XUA0WESVZIH_ zU+h}_^QVYMR$;=gyo^fe{fW3-SWUW^3c_`rw(3hux&)SSVao1SIeYT;hw?bAerNU)sR;roA~u`yC%9;lUXv9wk zOQ^6Y(pdlGo+Ll4wiNHPx5^T-^O2#jWg{8O?M*iGuy47{gD;i5e8vRK^Sgc_73x&`YAo#KAh}jG>DwIR{OJ*wf(_e z%Lbm`OF8*KGl5C&feNqip0XM>26@((`#f`)3hh5nlw8GC zf1u+@gue_gW|ZiX5n|Ptcv50F2;92qw$7nqV$EkulHT$KDV=g_v{CVg+lyH@t_NRQ z6dP{NZI&4ryxacXCMfmxa&74^*M+4k?Ni(l8vDs>0`m~(Lz}NzK4~5>5FZ31V+8kf z91G?pr!WGBs&ssOt7+B6FQP((#5k;NzD3fo>QjqHDRChk{}ujl8DvpLTv@VZWQ`&f zd&>!a%1j})xx*LI9+V+90ein?3JrYsba}bA(Ns-a7t|qF$C#`tH+YvPs*-V(MDP7f z&al*ynfyi5dr`~o`Tz^89+Jv*J>f>MG<$67Dw62 zB%JX3rC|ALmq1#auf6KmPC$!j2A=NTB-i^XIC zf8%%KKLE0P^+z#(^71{Rf}1A<2!6N2?Z2*5X^2&&@Y{jIkj1R>RAILo2`Kh`jH8OWI;-0oQKlAD(v7(3hY08;2(A_t0FfWOoD zA2aBKqvauGLW;=|+asJ(%tU@>*InZO*<1Kvg5x&!_(4DKPuKPrmOZ-S0vX5;g?!22 z+gmBfQP@d!gOQF=@KYdk2?{DckZ51+37>PBDA0G8ZQ>_TX%VclLK55Ri#hvHniXIH z4_+g%IeTkivZNw9_z&Q|=;9cIDW;jC4Cl`}3weMu@=3IJX;a0iojN~2Y?HhKx;~0d zzC!FCdMZXe)T$_amNmXohuJGE^7U7dx%Kp<(+FE|wbJZvEVTsOyR|`3c|*g*6yHgi zr-+t;bUI?wSu8BLUodp#KneV-qV9HV!r$0?z&5kF7k5ElD$9B+L|nUx@2bW7s6(;3 zncyPbU+Nx`tbVqyV>!(}euTcN8@m~!oywUsXkDOiUV~bZc;VIdaV0}&aYMI7!eHn1 zNn@zQ75454r{3+N0?T+dy}u{ama)!_vhg=N#+O;_mH>Lr`*vvR`~?OLuijzIA7LP7 zNQFZzq$Gx|AaHC!h8>P>;Yn`4UuJ-XpyNW!2b5yyT=385V!wSEZ*fF-J5hjFuD#mC zCg`cR)j`C1WqXrEaBq8bdTd|h#U9Y?`$5;C!`fVC7lLO@Dt4NsHW z=I#a}T8jiRDCtAQo?y0+A$N00hb3!L?IyK%YKwd=W6;zRL(QapJ*k`20`wA$ilidV z#LDG~n=a#=+|yVCw-1}MQYs|+FcR~p3Q(_+*x7?lASVvIt8Hv1{_lVcI$*IwycBnh zz31Vfgu;+xL{G10-$hy$drXQ0QcwW))<`nRfwwif%XIZkN~VE{@7CnHa9%v6H9#D$ zR+o6K2UaO&;^aQ_p^*b@^Z197ro~UAG#zuyFM^M1o|a9b;reMYfcmLcl39=LTMZ%+ zL%j2IH%dw0DoNs{ssqZ|DMZdsAfk^bnx9Qfa4A=q6F|hyJjR5g+HLhSc_}=nbl<`? zoXA5kBSyWr69a#6HhU2+F%w56>v$%WM04Zekn%xNhxQS90!a|PHw$mWUmwL03_|A&Z|Jt)iT?X}B8TkpcO3{j6@kpZ-uFytBJjlZL*E{yJ}tqB6$WF838j zl}l@}e@;pcNncmIf&EFSZQ^;RGelp+)$$3ymGL_w*M7d+16$vAf!2O+Yo3G?hD9WR z#q4R>ijlAPjUY=Ob%F=Ym>Rtz{$Q)um_xREuKYcTjSjM6^7ZJ;g>%QkL3}-aj8C#e z4aI4tx5{YeNQ7^|&}1{qY1yhSM~vG06I#P2vPyD7Fa4qd7o`=EaECntA!bi)6g*0w z5)38OFOIVfq2wDF`OAAYeR-xM?60uF|7=He9fF*!V>YWX z#n2x%3k?>X%V#GoAC0^P=Qi?( zEeD_s=%m9dv%t{($Xq?-2Aq4XWIffC|D;^4*ZNxSEuIQFOeiK_d!28LD3pQ_plYDB zL#@#Z*-ACK-1Cm};>rCcCR(fKf&3`^SQ20QW-bgui1u97=Jbc}4k4!Td-Pn{YDKgQ zDEt|9BV4~No%T_B5-ELksGW<(JV$P33ks6uQodfQr&nxvdD!fkja9sp8mxbw?CUPM z+Yf@)ih=ZCqK*Po%BqvhpTY?B2TB{oIGT|sU&i$-Dxyls*Nx@?&ToXcGT*AqbD3Gd z#|sA}-T--inq|pP!HoQz@o@39v17atW_hJy#Ahp1?Z+-~U}k(W;HUUf`v~@FU~W!a z^iQXSgkByrv1Oz2ju-r*60uPClW>y4lms`daAk2J(eE}spPHLUc-zhQFdYW0kK?oO zE6P_*Jif8A%2@7CwrgA%OqbTTtazrkn{R=++}%*X8fenzH_OsINC!s)AT^e>kcLVR ziSemlr?Yx5g{CJG664da3wNCsi_`9oz=u=ORC-3DxY~dv?bDjL+?vc)X~-xP`U9-E z1H$k9c#x&_4hNtY&P)@*&yyE|W2FjYK$A%>*dn0u6eHZxy2b zMqCvbsOdcT#@G(?&Qx1f{__uGzCm06*w^~(o2URw^n)4pwx{5rrm+k2(D_hKE$wYe zFOm@tE_>g1-&sdA{&G?~=)m{MG?qSGdSn!g(>$7s9?o34A;(TFwS+?LQ{2I)06zsU zgs!j~Dg!x*-KQ4lGLLWbnxWTiCkQRd?zQ1TXL`CoZr)abs-WB9!V2ss4+ul1txC7Z z#PBf&+oYJfhpt`nzpFTUq$Fa5EXk!N8Xagujzi2c7#)eyBvl#QY^AyM zDxT=VR-4aGj(%^ZAgZ8q>4Q@Rq$@QuvY;VI*2w zf-L;3c_M=gS4dk(5kxgU;DZXkC*|Oq6B6eGpFHPb)9ICZSi ztmPeKm2-zaTbH$+6aN@4+D})%@f)#a^I3EHmxzq`S&Em&peNm{etDknn;11Ob0FMA z^0*okie6t!)3QRQ2IR!iR&J2X@&M0ZHz*_==;>v-o)tDP-oh;D8s8%=`K>$6<*6FL zJGO@-ArI1SRuKaNaLFx`47Ys=o_xZIcnTgGZ zn-u@Zjh7u5UjJ zSRJ2Lz~66~{0I0XxhJ{$>?G(TW5V$B!*8jlq>GiicMmnkzMxl5zq(1)LSWg0izuv^ zx8g!{00(tscC;bN7=DB@c^aqbYVZEh;Dya^`&}hit%JWxH*seZLB$_!;SFRPIbJJq z<%+?qjsgjtwQD994!Ea8H;V24wRyn)t z673u$K4!p%M(%9#U)HN1^AL;6Oi!3(LGG$)^lP++o2Udp-2 zxx4_${~OaUi*$E^_7^sZI@WvhV98P*4o|o4I<&Q$EmghyS=W0pT2mgJ!7 zMm*hhb_TZ5ox)XB#~l3wj1e%?x0X21mog^82eqxfV;pbTU|F2C_a?ws`(+|P)NDU4 z?D-yC%GKmZM{Bij##1m?oji6|r@BsUk6nH@Dg7_anr-m=e*je)i-V99{!)zIMxv!H zD(bMs1*d<2!V(kL={Ih$pg0Hli+eYXH^0CG5#-0lp-0ghj_kTP^p4JY9pqt##%*)_ z-9u#te})dN;#&ALtT2C+`Ti6ZvFig2ndSdtPGTze^$j*c;C&Ik_BC@OFnI13LmDLB zKsA=qY7HE#j6ze7<>-{yimfrz-?r4#^6ZEs@4*w2DE|qpZW`=oD=+EYY<8<{z8|?} zBKn!@GBT#YtW>UnX>U^+JaEx|*&6w9L#P;;rltCAzx89<*=qaeT^MjCwLmgT<@ae! z#wuF&`%j+j(n`m#7r8$EnC@hNc2e0UA|ESuNAo?SMJXH|aNcXfuaWU7l~CG_3h!>! z!^-K&%$z*RPHb?!56=-=YKSKT)AODZo1`8AMHaG?=w!B2b@j3%^7iz*$HvE_yO+K_ zh9<)acdB(kPeL^M5Bh7i_H4BuGr%uYV#t>+B2b2*ue&&T;aBtDzBvd7Q^ZqcxYP%jfn%MtIxT?yYj8hqbjf$}AVFw(g#L zr)o+WrbLEIxQ-7l?}qNQDfe|dJT+n1cGm)lX_>h{k?B8GI`O5f74sYarXntB`jmty zSBi#f>*5kW@&vfpQf*6BVbg?O*ko5+d|4Ru*8EwMOmq4N*3U`KY7=>Qb@cv~Orh00@u8Tz`_NRNUX! zPU(J5Z$a;Y7K~&%3#)2Q54XD#&A!4Q@X#?pk($Un5!ifQt&{$jhu$>;gSLwFCja6y z$^0cwLWr7NVID}p?zn?=!hAr%Dtd8IF%e8ZyB@8II}u-(&C1h3-zdcRv=&^z4$=S2 zl#|RHm$*XviAq@u4(SYx$n#|pWjSe%2%w-DFdoe*sH{{P2%4=n66T&~ieeVdW`3E4 zd>bO1&y+{m>$;jr*hW1KN1fwJ)!5Cn7D1z`bnrz#{hrKb){qBTA#hL^bj11^nNtY& z6u{kS0pAMUFtbX~F@KOI;R&QvS4zQ8l`d3>6E?_&GHWHXk=UhQChDWI`~!$~6HfJv zjW>hkWXdgvnU?m#U)QtrtAFoCr0*$uxebM0e%N$~FDxFIa8C+Cs<*vuI-}uAL@B1{ z^OKm(LwoD|xi^n>wL)EW;>LqcnbEi96iH937H#AA?bv_5t{MLW+?d2QqLplE<}FY* zcZJZZ;u|2d{;jTP3Z6^t203jt7{z0z&RI-TmlPGihr#-AHcLhIv|3*BP?V}-9_1WY zu$W+ao?3>rcD>`o!`~u>xjXDf_*+y|(f9u&lh2w|=ba;b?~>p;9|aacvm)jXwc4Y& zb+gp1O(?VKqSpSeoEN+IPAy$Bz^+{teB@>F)hY~8XY4L!o0r}}VW^{EY6yWyxVm<9 zW7Aq3L4uJN2I(gezWGNY1N*w7;akhSwPqs+`|@a{?-Sku>$7!ozRvOsZVfdntr7lP z5-=ky4BLemU8MOW<~z~qvWI;W%03|DRDTvECX4!n+ZS0 z@nXh^c!Q5KddhSG8X4;QVSlPWRI~PD^kgh3+d>42deW!Nn9Mu+BJ3-z0UXRsD}q1(HA8=zc?QSwl_MKm>JDjhZN>0H+7*U^}x? z?(xgKOS06lbm^Y6dtY9xbX|}l9Bve+3Fe^ut?SW0Ool8^vRdJv?4qv;wyyz{HRt<8 z4Ts6X2j4Fh=@@VxUpu?nByDJ&XpY@MKh)ACmGOhnaq)qBaj6$cwnyCkDt2r3;sdLD zB_do%$1x|0pc-vO*oxOk`Go>ncH1umfT_tqb+A5-y35-jNz}Z)Jksbh<)lS$3q*Sn zJ2XH*pXPc~uH|&z6ziCu#OQi>jBh)@xV9rDMzD@=&ux0P>(p0TD1B#5=+@2gwaEfy zPyqEuyT%|_Qk9(&Ik^=xLEnOWHGZ9!)7W>C!4|7em{kN{v1}>#wtImyWevQFgU{XGn_-?~9_ z3$?_hPTE`iex8kQvvoUj@Z!ck2^Z9~^7RktUT}ZoNK>uH&+W*c9dSMdXaoASwGgxyJ z7a%!6xB1E4L4t=MA1^uZz+;-DSwnIK^Ty_;<0c-plRoAa>N*-=!;Il_)JXR4UmJIz z@?`7_{{TS=X{uwqKS~f}pp4&iqu<*}Tr6>Xs`aZsDYO1hXWES(FC3v36~h=v5R?fm zRPn5ArUCS3S|E6+eW5EgeSrHP-z1C*%!HjP-&_Y2Y^giX^nULKO*DS+d-K=QY0sYR zoha+Nd>z<)*hvINOZCm5oifR@aF9fRgy_M)3(HWn5k0yz(}w6Y#MbGEcjQc z;^zC$7|`WICWi#R=?p*n_|z{l)*{2MQXyNj-|dGW^Pd&OpR$WeYu@h zmAjef9U7;j1mc$>-wpZtlpUZ%0s7`2AiwOb_Nv&2IeFJ2WMpjVmc@4k*8IZAr< zYfD=Or}I|3VOpX7`*-w%R0ff75qZgu;Yw(@FIUx~5i=W??<&H+*$DRDT72crLI=oX z+h?wVI$>g{jw968Ku)iWLEqKLho-$`XPrw@nu|>Sw2nLUWBL1B#i_KfoCQ2Di;lKB zKC^CP$NaaK&zJMh-sgiw*?gk*vr(3k!8*O#q&t{(X6P!|<15l@*)C|HU(^xTxnh|z z-2+Y48dxY6vu@Y#_xlx6g@_0{)+b?U5@!$)y(Iy~>i#1wk8mkmwL5&d{rBOzqMH%> zGJ=32ykvJ?jheJwsHu?sZe$BN6JBehWD-|2%Y_bxno@DELgtylGfz7**&=d42 zBJh@t%7s^r;R8Iqg3%Jdx@e?(PE-C?F_sQ7);~w<%y8ztbcX2zzv?8q1&H_w5^_$h z-W_Y{HOr0x~Ev?Wi|_)xnNbxaf{GCxS$NG%QKz3H5xWw$d-lx$XR@>mnikr&(@$@0X|b ze}Ls=VXG>+(Alr63>c;C%2#cfF76*r9T#VuEOFR94n5pItST?!cnmj{Q3S#5Bn3XJ z{2wyftDjcKIj*Z&Z5wm%gm|=HHOE#jBmtWe`o5q2$w$xfX`dcp91PU!5-*h7gbA(| zQIt5+<{n}UoMOrq<~VwDZf7_Js%F)$S6SoaItM;@k0H*YM7AG>lXz-&A(<2^D`w-i zsX9}Wcv@@D9E}7x+QA)*+e33QO8Hi(<{t|2nnrHlcDvmqco%t*f@FHi`)Hm8vMWn^WD;o!SeFs}Sa$i#G06Y6ts01u7OkRn{RqP-{Mc7?hT$ zA-6qA^SEF=K*o+M*v*oRQD=6P}OvAU$sXZjyl!iM_`PNfl?u##pT1XLSu zB4j8M`g>6C60JpdvC+Qvg{Af5sSb(7#`ci6A1?ksK=2}YL;`Yzc>MYMltis5xAT1) zCxgJX1kruwp#SYK{-p~nUZ_(2@I|>&ngB;nsGZDBOwkm_Kld&vQArPGWM1|cJ<%eU zXF>sEc)a8+H8MK zL_}<}1mvnvz-Wjub{qLeD!i&^MED>J>M@Ja1$VyKM5)=2-nldw9Rr--w!!)xl^z$V zI0D=*C2YR)pQUb=1g+&N6IRDyHm9WNfxT<7L!4-JE#_N;CAX|Ta27y(MKltiFU@e9 zSe1Nf#wM1`B8?$V9*V=`O2Wa{vQZb&LOYYFNS7SN^LOT0J1uWzQ-{EV4I+qJXK6gq zy3tqStX{uKsl1d>>}#v`haBvU(u|<`mnn8PYZKKPS;ZplDyKa=3f6r|)6LgNem&+7-uH{hqA4^VTl?9py%XkO*l6OJJPUytz6uQ&tsxHP48#j<6iz_k*uZZuq|NuOj<&NH%q6>CU%BwBavc zVnF}@2?=I(8$7YHv8AUl_7THm{{YSwGwU3A*)o3Q?#=>i-}<*3$&dTOWe>gOKjvxN~FJItNQt+^!thzJx{92U$X0nCL}9~ zL8R-S<<7FA)QC8D#xP+NA~8Clja5kRg{*$)84_zsU={M^V};pgg5P0GyCc_I4%r=T z^3R21oKJp`^JK|N;3)jM#9}OkLE|CL?mW|~vjBnwA-+XU?I8uzX2m=4QYDra61cOD}K;>#%qJoNwjExp&X)E&kX;b=#zns z=?hWCj|oiT!aJa_AGbTU{T8S>{S#mOQ#jxS1>nyvl8AKFEG3d;@p7`9)Ly!jYVwM8 zBNWlQ5)%@)Fn)!JiRK<~on^Bxq-M}cK28bX3R+C9{7sxURY*ey6#8sL?CC$BiWtl& z_K(e>G-(rgD;iLt)N_hrnm2|AXuufaVtE0aU0kTFn8?;$gACynnCfv zGsN~*fOB;)3=ZduQ2$h&?eTS~#P|Xnw7F=*J1^jJggLhu32(X~YOc6!sY^vk$cKG9B9FrjtZDQUg1 zR4IBsCG(rOj$Lc5_Vc0JHeM)6K0aPv{jqyr6vWwGe85OMghbZVxqaJNY=Es@$Oq)D zMXAqH@7>{t4>&R|1pCuDN%Gb+ZjAj7*l@4edIHJY5Yc{ih+`P%RNZSxK)3t;sHa>N+OS1nbl*Qf2mq z)Ti+R6$-^dj>~fcp47}I24)usI31(j@*!m(@Z^}W8I*-Sc8_^jdz^&ywP_^mBiq!A zw~SJka{MJmo_D@#29?=xI3S_P)bq=NTDi9&8>|U*Un_vwZcwN#LlQet&TcW~@60Zw z7t;JTj3N9)i%>;5j_Mq8U;9qzik*K=)_<{h) z4d20wFS^r>z?wcq)z{r5YKjew8bSjmRzvAvJs#QiVq-@lRzlB7ujbPR9pw{tWKvJd zRyYr~5a8yUQ3~A>#sXytk63FTX(FxE1D(i8xLOBr$2!H?tnRk9Ip9GeVr*n2v{0B$ zJB=b^I*$$iwH6jl!r8X{mG~}?#_1gvf8qShb!gd#EK=LaDkLI@6z8Sr=Y`zoTn@UP zh^ku_h17RBYJIo-%JNVu-{3Bx%Egbkc)}vPKBUhESV@$->oz%_v-HGTT_1|57TY3% zQmc_II^I=LjZkOY@#O$NEn;42PiUq*Um8w7agK!SCUNu7G)M8#>mLy+P`=S!s0doy z2*-{f5f}+r#r*#yB!3<1C5_)N4`aX-jQ6$n8!@hdxa7)2D<--kjuo64x_{ zeY5B|V5z66AH5!Fn(5l<6Z-#|*4(3-{KVlTpUzxt&+o&`DTqW&J|!>2As$l6h*xMuguiFb z6Pklt7qv+ET&56{f2Ist`ztyi?*~h&a8L45am_dTJd~|Ux*S)*qma(?xo-2-{mOB* z5~BE?=c(k&Zg4F?e43v`!c?O{*xcwm$>VAngJsc5pv1E;mVs2wX|nKWxHWF`p9ro?k5B0S<_4Fi`Cb!(YA{iX&%RsY3~I|6cwNFmwO* z%*^mFzsHcC6?wa%8(O`))n^xvxX!AZmab;3T(etA+LAwSS=*eFPvD|EbggYP4Lel^ zPe=N3Myh?)urYIE_U}#Xs-@qZCE?@e^r;`gg-VgOl}-YCD`b!)m{*HobMk*hROPf|DNl77{ftF+Dp&ZsgZX5|{MVCLSS(&f5>Dt{q2Q zp`$Zd4*HQj$oPdl7Y%|p(l6*9aI!pyF{>~XVfI%65NDpdv}@-CS7n>u1B(TV6 zCIM*8uafD-8j0WJs2^&bN7Jmz+3Xn8Q=y4=5$x5s-{iOvw@_kc!h;tlmT^NEmKALu zRG*+t4!*cq&jNm8EI=*zK`zr0sCgl~+7m-wx&6VJ`xo^Mjv%|C0n4_quKrkUg#zg} z;|#^YTQUjmxo!%skB1x$M~k7kK!wUv5gy>P9n93dsFJKkufmbk)+zNM8GXstUhfjr zyojG{lURw4&ia{1F|#LCFXl*(v6u}@YMsPefK-MG0`z7={=Wu;cuFJ1il?P!_q->eBF^U(Am5TwfZr5}y^PooidmG~Sw`0u(b5 zVcD@(0fTHL^JJnYQ>NA2GqBrq>@gk=rZS{B_z~28}$^_^MVhv z;sTP&GFNQAKW(Cr5R}Fkqk40}6dhX!*VnH>`n>fBV<^O=l`dQk=F7?q}^bc8x+l|PBp0Q`S= zQ!Su>Xl1-Rm1_JPQ&KA*>}s4dd|20X%5cMPp7Gs%wc0GKvW84%^zp7E6XZfbNS9 zxPR09z$brw2Xu&_Z**~#lK(roEOi+CT6HziqxFBI!VFi(ft}}9jmcL?>a&%wpqGxG z%+?R)CQrAXa;E~NeySYeCNE}trYmJyr_)h8dBS`#mQxC)bjc&=^fntM=O+t)}i2l$8Y6M4CC`#0n9fE zBkXZFyRpA`RCZ_zZeMH#32J`hP7U_BL7yvC@uxz3lOVd*pVQlx){jIftFGWS)yA<` z?knT1%Q{n0>Z6e>`<@KB-8ZJfpn^v-o8cdTb`UU7_?P4V$|ncCCi*x}3?+9Y&TvVF z_-mVitcB)zYX|cz#Jl3Sn2(F?Z=H=@tl0`in!KU*6>K5#&0TNQxAZJ;?T5O*J!wfl zy3V&(KhD&VofSESPj3}Nb8fB>=lkE%`n#&ui@(UwI_s(tQtAHR5VdlTUDQ(ByWl$h zV_o&_5p1ASz9s*i4mjbnrtAUDSaa#ZB#Xw^-0b5r6od5m0W$i?T zFId=v{#Ua4H`l|dm~oBH&$OdgzHx2mGRaPX0x`^n|4nV$OBnJ7JCg4#_{xjHE91Y;@9M#S95{7pZ>cZC7g8aGA|%8!c*I4LOJF# zYHQ9)OJ*~v`hDw!n1#y&RPhWGwQ(blL|vAuwHGeSK3m&6}yX^Tn&$>C=0D@^>4>tj~uc28{jbZyCNO?@5CR>e&gD z2w~&R8CB_a3t|#PZtw!}9;DB@RQzxU{e&Z)R&u%C<89G&ACJ;B=`fRgz?itNxfpCo z=fe70ZRlgNJxnSX`QF^qNp#EQGHFQBj1@|i+liwD&3A8Sw*eqOLV*=ydWjrkos!$RrW#=s$~4 za?#>$JcInd%nTTGgFIO)NJ4KT!n`>57S^KpXBxd=-++-@T7qlhuOS~GE!%E7Bh}LpNiRZ_y_96!jHBuAZBFyuGk*>1>er~PVNd6EeJGJ+ zc--8j3X5WL0#`==21}n#libCQ+vd8yOncJ_ghqRt1e(r(@4#MX&8SPc}KV z@a4=yH6r|FUvCoL9lSf|y8Q=OU`jqC&g!dF#r+nMu;F3#g|a7q0EIynzi-|M%GyPP zC?fS0m(;ssBt+N!%971AuHxq>fh^0`&8k!MnptJH0Oy?aPW6h%#jaz`%M%D|QTb3n zth{Jtu-2eLma8B`PNKhPW$`li6?fw#?`Bk2Z9}=8Iuybxxnh!oDXSvYI3TJLGDqwxjGZe=!xXyWS{d{Ku6A@LG+zY z(sVuL-H+A#nrE9UmG35@1QCh!uY!^%+Wd?w#P-dq`=t>llB|@uo5%gHGIVz>jWg$` zJWup77}5=MUf$V7&yW+p$zD?2>G`@3nyv0V2fdq;w;*5i8!FQeP(X9+v;|1E-xZCn{Rh( zLMAgPPLb08boe)MdyZ=oW??%$k?D}R+Wb5AML0=}Ue-sI1;OU^)(ST32*4sZXyqxW zb1uNB9Eqk9d04M~=vmY#5(o!m{{!g3VY({p2B&q(W56uk5$?LzOOyByl zNRG8pN%_s+OpK9@IHRTGm^=tOcjL)t!C!+jggDqQ2Mo*-JO7s4(5$`qf@1S>ap?wi zbUrs%ww{d%W!JN|*@G6#%r2Ih9(}@4tRD3C%}@h-v|`nwgn0NW&z9Y2om~V>GU)_B zBP7%Bv8z2YN*1(clDTPTyx^j+o}<5OGyeehS-!dn3toa>!N~XvkVD zc2#qSIM+)G?AB%=UC*^Rsx)dZ4JBbNWW!YYi>EbZzW2222Z}5XGoDn#rx;6$M+hA^ zUfC5CRFLwnByP<*>#2o0MP=tVIFa_Ti_-1WqIq?KAq=y6Xe`amh~=Y80`PB;Su7}bCub_ih8O+C|P9!X7%)6dnwsuz0IDbE}pvWD+pX(f2 zw4wP22z`vLmb-cx4ub1#zz&aIF{oCmD+(#NtRA9PB!`#Kwa(2y&`*SrYG0VdvmXro zkyHNkxeoUQPooc`42l_-&VZqth_Y?~<{N8vXF5`d`br;}5o*OcIW~09_tieuFX^Li z?b3yTOAPsyjF#{T_4+%`fgvFmyz%22w@e%zbWNee#j=s>595;%1oI~f&MJQ*f( zUhS0%)8V)!sYU1518cE&KgH*iotH+*Y6Ny^2a*efT|A{VvfP!;q$V^fi%6rCL=Ov` z(|*`+e(V50UFRa7W!aV)ez=BSx{Q{05+}TBIYH$R&cVet=-^lTOvx0FMOs$yH?`OR zJ?%VfT1KSi8>M@Ae7xC?7vj=UCaNaX<|7Q`K3=D+R~;GXtC?tmFyhXF!dPy?+L;m( zt_uQq^Yc=5zfDcz=9%A(Ezpik|GJx|e0clw&uprS%{oeto*D`i*72#VW8U&Z zGHW#2yU!K|>!SG0s0Zdv5r*WlZ|bQoR71OL9T<33(s<-EzGD0@^$>qinEs{kqDZ!# z?O$;Te?HprEi#BSG5%4}@73Vjmr%w?zkp?$>fhf?Cx2;lz@m?4N_!y#Xa&e{j?M8z zuw07Rs9GT5KY*8c9HAF>NCq=KhLjN9!(k8#8C_)(pU6}Ydt_DtZXcWMD=XVKPgus? z#mmAWr*gYOY!T5y=*yxGVRf*bHTRKO0sFLGLJd(LE(ZCU%Us5MTzd2fizyYp6E}Lh zS_VcYO=ou93#L)9<$jpRhwV=T5V>i#F;vAw7jSB^Lb9uW44JG1+{Z;5R3vQ1h<0~A z9^XY8#%@>ks&H|=$vw-qUBw^GFTaNvrV4(X&9p#<;v!3$yjgSP-%X5PIFZk5`_%+G zz*B!F9@WP;{Ro2C;1}5_bA03hHDx$AVO0QZk*;-gIGdjk!==rccdJOxiYRIqlFh~P z5OMwQ)oX6YrI-@UT2DKw!_9qj#F<*MxZ!6uav#oOpsG|~B(EU(w3v+xYRge3e|Gfx zJNzegE3L>jaRP3FzLbr4EY{Fr8kWm>dy-WQY}z+UZR13?J3sv`S@m^-<2ajPv%`iF zZb-z?&lx#=D(LxhkFtQ6kWX6$!_70s%3m-!p>d*bV=7~~r)nOY;)nR`EQ%3`pTw0~ zUvl>+dj!BPQs~xpu^%38wA`234NB6;tj6T+u4!CA<~vthy+3XT{-CJ-S^g94*Np1S zljuuy5Nqdz6-pEwgwNOLbx9T4&wwX-O|#Y-_-3Z-Ylxa4BSErr)J$%TtS$+EZr?oh zM`JxY+oD38EC-XxCS^5wrl}l($X8ls1Fj^w?{ab}wLGbkLcZsu{G9Z;*mFPHR^3i> zqd;}xErkDfPFQ69UB+XC1I}Nnx!Y`;!Iw9^8gL@}u{UmUdi8lOEuZRQ;^+l`2?ek_ zm|0XDh1nyC-S8e&=mbUWxK5yaOx%+0FadJ7Zn(mo_BQ-mCXHNxu}h6r8u6Zd1IE9K zFKum>#BSjZIe4jWVhM66W_SAEh5H3}Kd>6c_z$r2c%;p&Q?l^{laZJsp7NWnUNa zFf5zWY&qWSMM#}aoI1;xj3Vov3dkzwpOLlbec!x3_HY}lNw}~oSrIu zncL2EQe|=0SJy9)>lT}g$%<+34tzM!)ED0$q3#65;?vJ_8hb2|hq-u-ll{S8V9%Y2 zilJ(k$wX%4G)jlDoQk$`H(1YHD10rz6xT02Z>eWkT`<4u$Hp)F)l73jSDmqyd{6*lQf5pwxKgDt0^18%dgQH^&$na2SA2 zBcSMY9HA=F&?I(^3}+DMd76_pn*qeVS?B=pejGR1=7UD?1XI?k5nnep9zEWx|)m3jN)dwn?T;~sIMEz;$>|~#vd|K6HUCtnOyC@R#qUKhIK)ZfaBaQz@;V( zOwS`%F5=Fd{N9>pB!eA~6)1}%`*Y4I*GM^s;rhC1%*`@;@HCSLGQ@_1cYEJYFkNCx z`u)Ru>Ac-}5DS^kq+Vv}T;RtApJX9NbwhOqLzAqvI(G3AxotD0OtD`WgBT0i8ebmB zxx591Ez^r()r6v4z3)|dobkVMu?dF~@L5-Qabebs-JdS1X@6;KH(_OQLXU6sRv2kI zNNf2AuphU+j{K{Tc2zNIZUD#LQ}{MWp}C+sGP(2fb&LSXNknn6R?}-NcT*&CJRQ>w zvSAf7K$j#LX)90A+R&9Q&`$2S7DS6-Kr&&Ku8*$cI98)we0sQJKFLWJqm0^BgqBhp zsPBvn_y4V)Q3U)?$&;zyK`jjMwFPU9uavlNtNFW4E46qnKb0H^S8IbYqa}woYyZ?q z-5SmnHU?5@mwHeM7Z_L789nkD`4(j%SG3k+Q*A+A*CCl60tR&55MtS?PwV=aO$wRm z3J02@n;cuShFd;O>du5w`ye}b&6%x%zD%?Og+(LYAsLsf6-3|n{<~yC?I~#y=FJm+ z<)71xQ=mySp2_Q(3{GON@6tMeb?%ZWyF#IgsL8BJXEMEoH@;$&yN5gJbkQAt5vRtK zwqjsZR;4~`S-v(Ge5``*+CSG_4#o}q-Yzn*U?8s+$2+l}APGY9zoEQ4^_LJl-RIvJGjN)GxoetzfNyDok@XIkg-PV&efJC?EU~)=eS5R#Bs{(WMOce^n?cCYrp2Pp&}gYP-1M8N2eQc*l?y6;LpCK9sbgKMaigs z0Mc=JQ>QwNO%sQz(!gcDQ70du@^!(3<3h8>0fqhx(O4tkR{L_! zN&op}bQ36@Z9n)&(y~S~<4$`#nE3p<)qZ-M2dlFu&jN0&l8;S>cFrW0dbCGNI!CQr z1MXSJ;Y(fnIr`vvC$1Vml&GvapYW~p1UpU)t(_v1`mfVRQSU(tJ z*wXRHzNbK*p)KGZ75ic2cXxBoxr8$@L%ztWL?w2;n6@S_mdaa!DI)f5UzA5CBFJ@R z3frsnz=-&?C4#5jlK_RnNKX< zffS;ZK1I3a+m#dkdIr?QW#7u3_S^51i~2SRBc}6s9NlN{n-?7vt@e0;(I^3hNm}sK zPA}`908b*5k7!0ssmCqPl>}eF8(_{;Z2}qc1PGpeZ`$$fOdgRM_XEsGPvy_6*I;L1 ziZ6B$IeoHJuwlRme5s#`B&T#0 z2Lun}G&=cI-hANvlSDJZ#Bd-ATUmBAK*Ii-OBU`rZkXNKNH*9$TnnzE(vQuBxyEd-@jZ$_A{VQYmj*(Bw zl$Ldg8G}Sx_}9z+P~2}~eS)21)h@RcOl>2MKifmu+DjH^!=!}eEwt00rJTj?he{t^b#Ppw472o;1#p@H5i;+sSy4fgd+O(qedflp6n+bzfZPv+m(LBzmh-u zR~}K^y;Z`0t~nR6;id_MmH*DZBY2g;s%bg5K~MgR7&lqeChphj{8o&>=67aCIAmX!`5oT zYr0bv`_n9&QQph&PQLwRwc8W+&~!Oyl2;~JPicWos;Ybf2U&`)ZEe40QHH+B(ZyXh z!5%uk810VxX}e~3?}SVJpQn$JeAYsDmRw5{T&hYt)KS@lh6w@K_InTwTx$@p0}mj| zmd98z+oZ(ch9v(Bh_!JQW4P%Xyc){e>|3!NF248Wj@^lv0>8t;?^3wEB zWQbxwBI_o0O^+s*8(RO#9v^3GudY%d@Ym$W6Re7cZ-}PQ>4bQZ0Tpfj-cX=+Wt;3j zK%r{Fo4Ok0e}H_t99P{(ytYql^i`v;NJ!K~>6)5CJs^dK1@jHzr^DKq_wB|RgT_t$ z4xdR>CEm((`~$oReDz+k7fmjf$bhCBfB7|EC;{bj#sd%|64VwWs{LIJ?~7?oi>Vi` zPoz;Jt8>nVB9=VlW6FUaP{e6*8?Z38_Bs{-$&>1D$-k?)Ur%ZXueyIv!_t3s5N?87 zX=j@t@k8Ho{ZG>}p9R7+bt$VY2R#_zO$`pTBQ5VG6po8Q+@NTWhV- z;&*$xs8zv;lY{3<=;%NPbkw6os=tvAB~IK|@sHw3Ow-gw`62bjdFe~rm8bNjK~s?} z3}iAi`z80yo;k9JsZsBJ;ap}cdggeRyzoM<;a_6F9;zw13a66uatDV~5oK3?r+*Q< z*5c>yPUFL~OWDfUmHznih!}r$(TXm|)wbqq;XXNf!b29?TRB4%a^Kjp{Vzh`e+w5b{;0K_&PAZe;k*I0 z`wQ{}wT6@bnlX^}i08hr6H_63PchiT8HS!mZRTKp`$2{4?I*`Ke+?dGG<% zrm|XXL=ymGXen%_XI}drfy9j`5cKT|H(kLLP1*y5xwnLMt+aAkFeuFX<}rgAzrhUU ze=!5kD2-B*&t;xZf=|C6XiZEaV9uQL05L7Cz9IbR7!^52h1C((|DlC@h0tT7%IYiy z)Gq1?h4RG%Y{tYI-hSDKOewNJNzQPR6hCtQS=Dq}Z^g>E@H|_n6;&wzjz7!5Sj$)c z$e{)y$a)T#@YHU+y3h+2>O$?y$VbH;)q40MD^Sw}U}kG?{duxN#jZ)yr-WR9DUmEt zCmWgy`Ii2LvTrul2n!^&PAPm>PVGAAnb|)Y@VLHvj)^dcHL&N%58O7PUGOSj2!Gd4 zgXt+VuK}h!T7_lR_;~%Yf3=z$c;6sM^8XMQr}SS`IL0ZxTl6pb<)wCQ?MF026IxI9 zhtXu!EuTV)Cuq>J72da$uKN5kc{kt8v`LoMbTyf*=Ppk0SP^m1LxfSVC;y7mFfeV*j`?0Ni1}{W> zG;nMSGltF=eHubKnm$gY$|e!`50F=w?b7-emt~*z6pq?s&5~XGIv(LfN32iGOxAh` zY1Ge_BVHhWlYUSu;W}mP@3$Bqv((al0?nJM@KIJ)P;ha}tF8o}7`AmBG3V;Cwlyz( zRW@ugVsUIPG`*)~N?ap0no~0>%M^L5b2U-&R)>+9ZngzE5favsjNLVoJ-F) z4fPPE%`lVvdxqc9`=2g4-0YyXiNfZVUA3dw(80y@eyd8bkx@8#Qol^*d`JxD$Pr4|; z`}idV2?d(0G*I!M9iG<7v5lmz{KAdGI-r_r^-{n;v*(l-lDj}(+ZQsizT`f++HzN- zopWyKaMNj|lHzQX?-L%OWx^grqOVq_E^joY!n=-R@RpWy5%u%4(`5&O6TwtZUBs((6r92JteDI%wU|dWn6B0Lt&=^k7SBGH$q^Rna z8wo-%UtP!mOZ%-vpvE1tuZw`l?(_{YLj!Ju>IW8s3R7OoPdootH#^|vj~&5TqOKm7 z6s3PtmAm@vsAnTP0D7vq0gLG@cWWeN%G96eOaIBOC**whP9V;U(zmT8F!1m62+v2e zF}mGWf)eGC=6?X2RExCvot>a=^S&d+SEBI?26i>Q5&*>GH=hk+$R<@S<1fXcWvViu z(oC3!Fw9G*9uGOi3}8DdVS7rsSOQ#}SujXX#aUx<)0ATnapAR%Veh^=GOZ#@e@l zEHWylHA6a?S5+i~YJq&ysj}dQK=WA~QioZC{ylqOaL6ycP>B+4o8Kh%x>9Bf9F~XaDM4XyruT}$!q!h0`sEcKWuU$R^b7PY@zL)H6jpw3ewlv&p-~V0p0zl&^Xwm{ zg!15i{uDZeg{`CD09lv?T%`wf;RwEYiIQR$XUt6n0 z*eyd`96rCbxU4?BnSH-wOPxN_AatF=`Qg?@=1`YPG9XjvL(z1pm008gsf)d%Sx@{_ z7s2eTWsEK#ilo@PzE)+~iLSEbK2A(3`sT#W0-Zq|El?58 zkS0)?olU^{vu}Eg=MQB6Dd@OF=fXup zG(nmsxoE^`eR;g2FCavPpp|EFm^0XkeEYWN+N)!GU%rMwV0QUy2yP~?Oi&VKvvEoB zEoS`X;=cIuV%=&7e-LuYROMgZ46B;3E5_lzJ`c`07`)bm1O5{gAhcMknB-61qIh^- zA`)$LdbtmT=O-^)VAZ_TotUrs!t5Dg&3zD@Ay+Tzj5^2hYU60GKAX~|7W1A6WPd4# zXq=AXg-tx_d#A0AxZ@uS6m>Jo=7EnqTXrqhIBqiV*Uo!H8j&{wrtfCy*^MpCq{2(G zw^r>N1=$|Wi!ayzG~>ev{wf_0cUhII5B5H;p7Z`3hRAm+#}s!?2AW=R&q2z0XS5K6 z7_?&PwvyNue?*&`bQ1F~_X^}H!%moQcfq>fPV{>y$IC@OZ*nPM-uii`e?74vUpvQ* zP)T9ZiI3IQwWD)J>!{dLPhB?`%@nh}q%y7lZ&v5__Fxm$UyLx;7vhrB%)Fraz(t~< zUB*%_0h$(S$^9d+9UO_#ih^mhI^U_4;{EE(&c*-IfcUhYaIPmGR!;6n!sQD6{)(9F zHBY@xVo#{6crWS#31Yj9D*NTLnMSiGv<>N-^v=vfWmOfbU>fMNGujTt`Gv~KU&_OA zZpvqURV1_ivKS8J@X)Q+ALlfZsbDO5=gN$a*TC9GzRy>^Zz*}Crw^KJoA`ZS%k@{7 z-;CC5Klj%7kH#X1J?fd=SM@U`rK%3=4cdZzr^3=-uJg_RlTgWUmUd#a=xdG{mrw$z z9AZ6m3!?neN&fTL63q0!lJw^GGrlz@{7*<=^taSl`$G|ifv;}2`O6JW^XsMvyjk?M zxl7sg;ODICO^w`5{H2on@Mk-eP)MEwdvlw&VzL)^*MM=>?}QLd`vvg2VnQDG;wPGx zk1~rp23K*pk)6Ze9544wd2aQ$FRPJ$iUg7Y{OrHQg&rr1(=ua zUDkSqC}MOG{5&Vn&o7vu69Bd{HYIZ-msU}rAHg4*&wws{ImKmSV!H3%=w3CxLtc*W z|L%mQDR`IkphWAMZT6$OZ#D%-Y=^GvT2crJseVPM_xQn|`1W0V%v1Am_i?__o`{bW z8Lg8DUUAJ0zJSrimZ{T3<=0nTcLe?PjIUlZSdN5__df>}74-61@Hdl=VupgKgzb9& z5(rH0gdTSxBv{?pC<;{`A$WDlXxsHcYWi8}E%_P=ApVoRTXf&%-dZy<_Mkf5#gmlv z4=Nhdq7Pko*PYnuQ~h)EMxv#t;mLB6I=oR3vOZOxzHieOCiK4UvbsB&#_}Bse_1+Z zE*2($w%E3#AXAWp6S|dL*%W;=NmA&Y{pl?67h>7l2nd8 z(SDck-XI6~^*pU=_?}nQXX$k`6*HFg>w=WN`73=SWKoqC59!#$-VLB2$F*5w} zQcz{k36>wVFX~6d!K(wk(!$FG9;9s_(2SE| zwNtOGF8ssVHr|CwWBGoF!yh78FqeVKUgd7B29kZ%4UISAb89bqyRvV*+e)Y`f4u*x zX!o?B0f7P{Nb)W5Hu;>akkJ2sMU(yP|EXwdtNd#(38O}ztAXFR(XWo-W*fM!v=QP) z$5Hkw%CzENuYU3UC^zK(mxq(Yi1kM9Q~;k&yF^pD+5X%Asg6^1kTDffS!r4Lo=gt5 zNRC+Rtj=6bcy!iy6MG{aJ6KiEr~RK1(E&=Q(9SyzUq+oD4l*Vy1|{uG-AxTlI+s|y z$SNjDh#oU~l<*l8lZ^VYI~!x_QP(Qk#?M8NK4qb;?*t%`=gk~Hv)~+QEQOl^JB;!A z(MGCQr<*Clx7_32FmC#3F}L<=FdRWCJmz5SmI+J;Y2!zd`nozLWpwt2jbj{M7`+90eMxc{oTg%dC2ajB5>aaFBrhrF?#VCN4`*!-zdH3@l*=(f$oLLxY_r^S~EqcRtiOLtyIOXl? z-;fr@i^3dDC8s?hRjSB*FcyX;0pt6uaNKKOVTiIezpL#2yx{U+g(?Tdx6ohN!izW) z3T}?ZwYMUWn((xxzG=9>`nA*XB+Owcxm9qQNe()$+|S5u_^BZbDr(_}M0O_koMVs| zAoV=b1X@va(b)7)G%O*xaAVfiis^bA2STiqnfv~fcho~Oj9b8S%`|JdRbI+bGu$I{ z!)OFW7{fqvQH~mUE@{N69CUXlW#(UZjhehTN4GuzIr_aFjR~N*D9x1+wqSNI?nvov zRz+s{AGZ)^cg~7ES6-!Yge$h|UB#NVk|Rx$PWq2bx-Xz5vYyQ-IFe?3f-?&IWT{#w zJx0}(6>8z!baEn_*-25MGT!A3cUi+OAW!q|70pj(%#h-*a%XXZHe=Z6n8G>m`FO{in{1KwBDeWs`Z@zaH{+ePn=TbhMM#i2q`8f1`jDd}uHS~>xP z7#{L0aMB$n;0I(+_Q&hn4nfQbY8#)nC0pvrM^^+%j!C-E;1cE54`0G;Dw!CrBf$le zozJ`1t7#{1@H%RgrmFJXQM6RDsq4f9DwEe2)~{5`?wwCO6HzU@Pe;vyj14p0r_i)a zpFZ@tAZQ=1zvV}_Q>h`)4VY;^{V8}WR7t*3Zc<_|Cvg9^A)<{c`yEzIN%o%#BKHBZ zt9T~^GyJ7;$`^bndAcL=$H5JMGIR7ENEyBlt2=B(0|~U}k-+V{n68v6o6%f=`c&0c#Lr;)ph<4iJ9`q?4Ec=;aSX$HLPx7LX*U&2i z!Cs#4+#$j-LLO794&nEEHAV9pqe_+*g(|C7&VFTBlp{EIsu|eQ!**qU3v6jnz)(C_5`It?HX-cKC0hNll5w2 zKUuolYUNHhLW+KH)23TROh)Y(H?k`9px`>2-$siq^fVwg0m;L>v16BVT)Hkna zz+&)uSP)I&X$qz#M;dj5WkXWkEj;h~lY#FNC)2aQg!U5}ef-z~Bzkx26LTL4{cPG- ze4geHd+~0^N+F`;Sr!3W{ayYzONcA-CS(ds_9D$gwnjCO z8pWbmTyta&`{XPlbX$}d-umTNr5SD+3g-peTZP$#t%&8;(tH#{d!wB!#2B|V&>Bs0V++zj# zo3z~B#~*@=Lu1Ci+nCJ5)XPj7->b&l9R`WK1sAzK8V@kj7_R*{;pC_I!?*lIzD&oci>j?-o;c5ia<{P?R+NGwjDi?J$swx~7s*qrtK{^xIScMxm z9)HUzT&{e6O~-A?SBe|{-nNy!TtJ$lh4;^z9tq=pZ-Q3%IkmFG3Pq)>w3REp7j! zsK82l!hz<0y@Sv?Bb>`8^~yr0jQmOOJGrC~af4a+4w);>>$WeXZ(z2p1IHI^R=lgd zK-pUxG@<3q7FH|OBDu-?(k30m@l<~+&?IZF7im*sqhNtZnW0GOFu`t2x<@H0)YiO8 zolIe2Ez3*uxGP*J)ZF;@09HkLHj3hvjh;U5fvDAb+Np371uGunuC7;Obu}a<*Y68W zx5^P(jDr4$?$iip#gIjv3!uZCsd?(9WH3twx?7nRcT;oAz>Unh^z}->c^;mt;;{2DGNNhdv+>L>+Thd{ z|C_s(kr`*4VOfry%4cWdKM5&~N2Hqyt+JWrNnLh( z7`wiWod;}QJl^VQFC0o%yDKLng%FmMpKfxcTWBjYlYa3!pKNP#0P^{~ofBH7Gq9)q z;F2YkKLY{v*Ryyp(xz1y{Lw2jTJ&#zt9zgum@gz7T>(LD5G^35(<0?Sk?G{)a%*2S zH(!L=sm%D>-^8_Dysq9+fpy~Y+-{^yyldtgqeWdM>D-ge+~vMI>&3RzFG$f8g%bZ- zwbP$ldvBJ7Ky1R!#^)l0YfO&*{FwV8H1f_L=v`QXe>5IBk8qfJurLqkcEvkyuQJm` zR3E}rcuAZE{GS_()N2VtR~$2sV#Ewu?I@Z~mO9BRkm~XbJIyNGxt!kAQ&)|6@-dkr z3zLXNpwK#c(Ii+KV?I{+XMCYUNT_YRbL2bipN=5IS|^CTE5ze(NA3d1GFuCAb?pU=}svq>V9Zo?Crnu1m6j*f4Zd2 zi8B_)wZGh*D-lufV8$=wYI8_E;tIxoktqYBB2#0o3@BqGlsn%2bQ~Pb_6mZdS4c{c zD)0#W49@eZvpqDCUuRo^GTR;80Oj(Ffptar)q+s22O6aNR z7&R)|FsoA8RD7go4L6Yge_OYf{txR`z+t?%)vb3v^5BzSL6-yPy&2rTLd6nCo%YXb z0rwvk6YAnKlMge41xD}iyPfov@BP&!w=R$tM_P^PW+0|q((I=PWh(0|x}Y9*OIF7l zY4AZgs{fA3+!xluzuvQ@S%Zj(d7$K`hZp~%rk0Oy-v$bXCYQ#S_nQ-)=ZP}oj3j-5iq(d3Bj-?ouq0GDW-i&^-<;|~*4rMx4Pbqq;}K3u zaFTD@Bd5X5y?W{QkYFo6z}pic&>u<{cCrBbDSLQUY#Qugb!MGacU?XZ594}G{0w#d z06}PiTT^l*XW^frEg@thEh_wxB7&U`}+_Gn4 z6j8n$&MwS4dr%i6;kwEnF0Bvl`O2KAzpZcfrEwBc6eugHQ{bH2q^_nr+UBGPltSz=rQ%l77z9aSx2V1_E zs|j7kst2sb1}#@iuI{HYx2VMRdtQ@$kSvdR^D9;K4@sdw`8ls9 zf_Tp7HLr`tqy7Vzq<$yI;Fn+TBFD}&t7Hpwq!3wWx0tZd_k5qF||LMj8S|DBd~v_hl0GX8=5K4 z7}vyNld>+`cYFoi&jO$w%=4>k`9b}Ys6Y)lhom%D-fH}`teYKH|mIPpsnQqMG6;qo7C$!VSe)O z51@`pZU9M{)D(?rP6-=r&G+=#iv7hxh9U{Q-1j&Xe}%M;FTl-=L!Mbdp3P%vZe&FO zN~ZjUU+=H;CfdoioCBDhZA33DfI~%Tm((`ECOL0+<$#YGA@uPmRA5erRh&BdMtl5| zH)x*Xr^Y1$6rf{MbRvchxdOK0v)OC``60mivGxhA6Z~A+-%}XiM;qWW7T3k_XVslX zeuU#jFEyOzd?I)~_ZZhGC{MU+S>m`98{{Jd$p3wnQ>Z*tYdGI}W|#vZKftC6QPqDu z1Bxq@Tx5nc71;Zf$tdvFs{#au#W$ulHC_-5Bh4+Zf?G1xRmMh3kzmUm18Eo2jS6H^ zub-^!jpE&Z*yiz%HQ*6BqJ_ZD4k3kRmaO21FUCBNxEe;dH$KHm;zRB2*r@D>!vXrh z>3c@RALdbq=ERS%Z;A2k5445Yp&9%+2^ZBXb*GhNjMcIK0Q)+2NA}xwzI1{MG;ao{ z6xK!b!b@~&jD6ab9n!a(?%&YM;r-qEXp$*|X;2wMp>d|ID|U0#jb_%Kf!)qb!s&Ny z7Bg9M$`=5rwRXwu*x1gt*URnQxxIJD>X@*Mnkb<=jnK~D(x@s(;JI_Oh=yYqJQ{~y zO5T(*(p^aAL+&voQ6v+m0#eH}$1UpLyx504Eps+7jn^tsz8GPTF5T-4sU&!L)Sk-Q zzel5E7`Fn%a7l)C7?}>@+8sN8~S*B^fDSrX-(o^#2m1W70D5#3Y1rdJA zHR18<`*@OjyJrC%fMvEDJ9T*ry`Gf1z{FUxBordJj= z9;?Iss|2&5z#xWo!9~7G^x3l0Ft}Uelc#rmFC z5BL6A9mgOHyCc8m8SkL7(THoWosJml2Ma+@C{X8?wK$sVdU><<7Ujk2vnS+aSbN`^ zA8797oyf(iM^sn&C%D3W+EIcCGsbH;Vo7|WWDDU0rDe-pm77JKEl1J9MW!9q9<7ZU zo5)-2i*WkP+3LDY!3AuNd-B(4r=HaxQI>SN4Ok%wJMLL-lAuiW@^U@tbn2yT=8TKv z9CNNs`d0l9kP2fQc^Vnp9(R*5&N%hOOfbbiBR9}5EYi3t1(bc|l23Xlp^9Hi9@lt)6Ki!cd^x%eSll!lr= z_VoFH289w%?7SA|!}VzbENuuSZ&c8S7)e#IJQTvlwBR)^=)k6t8v(rg$@c#v@S=20 zJN|E;EC-pA*zbjB8URs1uD{^kSS29rhE_1n#cgh8zq$)>XJGb&*|Ix&Z!T-_u=d~z z!G&*@QC9lFe~QyGJKQkLSuEqBVo4|>Rf%X^C?inyC}`9`wBNU2#af_G0 zmOFBMASQxE%~xf#F(jD6FkB+uSKKpOcH7@lS~w~HqTq^1cqcr#d1r9o+$g+DSk3pI zawG2Do8!vVwNTs5hZRtVnQC*uC+X-zOEE{+^r*&}UO1AD4b_ z26tF&X6<0OifM_X0Om!!e?hoJ?eCzuB)zP%cljlBM%&si;|rg#CW6EnpD{7n~_mHJ~B)4n&J&>p-ZN{O=etb==_iSOZ%7?RmfP zK_l&3-d#t)In~v2CP4slD+NS^`P>QN9xwZvVwN1-Mw>SbJ7wklq6!kJb>8=^sb7kV zD!Fmn9940ppA8iqY>*jdCh>@BCodwKlVn(_zPbvlfa3MoI5v^JKGiDO|Gy}E%c!`% ztxvab3zFdOZVB$e-JuF7oS+4Ty9EvI7Tle}T?4`0-GaM2B)RAG+qe6F#_0EszTelV z{b7&VYtFTP^LZ{T#5T%Mx<}9nM2wmmN)BwrDPmIT;FDB}s#rNbm4SBT+jxas`R%m} zRFcb!$g2-&ozqCGJw5Y}92m&U;mS?M?7~-;UeIi3y25wL14U6S{BUg8ZCon%p zsV%!ZagY!ZYt?yH_I`gV%Uq3-2L0G`rL>noh1xJ|{PTrX@A5EN+BPq=@P>yZ5+}&J za)z3Hfk$U7b`rveu41GanFtm;xv0>*o``qjF$@zys|9LaH4nb6D9^W@);2dpF@hH0 z&AHT_#oRo92sRy)KO1Fd`~?UldN$@Ubvl4q772cGb8zHMhIN4IjpU;M)p9X9c)bX| z=OO{5i&w2{5i+v25ccM@QEKlxdm5c?08<#0}9#9@{a?F z-Ls4W_+l)4lCPuL$xS7@yXYXh75f6bXK6jejtC}eq`cYhaS7svY9AzQ4JTzDyBZ5D z>f8{(yVLJd(I+LbL*#Mn^Gnec4$0D|2qd(jMK;8xGno+?1`cfg&JE6E2*>_6FFRq^ z&RK0(a}?9kZsj3i#=_R~e*s-{EQ4BuDu*Sngq!|N4N8V8xR6!W-f$&lsj+j#-fH65 z9w?_PWx(Ebk+m<2D08Lx^ibl=01G%n!&o=gp=z{KYZsqXhq-z=G$$*bj>U<|kCdJD z8@4!4kyFF%tsA5ybJB8CqN#{Xm%>dI_9Wy`?Z8MIV_r@o`&+O9wHyo09s`Tn>qH46 zgVrop5NYiw!`c9q#?jR4G-_&3x3_PEJ8De)kINRRD{dQFo(nrNpI#JPGTSMcwo#vw zdGP%l-qV8cU%>mhd7A=g3@EYnhY&pcOa3)un_m~o<9dr z(VB7D*jRdu$%BNVe7@z9ElgMOv&??4SoCDO+_65SJ!~=F#r_=YogY}9uU-wRL7w>|9SL0o#( zlwQLjcC|c`#2!LJU(^O9qng_>wM;R3+0JxFK|}@+K2dx@!yHUhjx{LJRD7nL+Y2v^O1I%v|m&VR$qlS-FiT)-3;UJ zhU5mDwADT+3g>^k4fr{Y9_u?RrF9LbRQ}&3E;8pKW|-(!--YfW|vkkJl%q!ZVp;KlEJ8P0A?^WMT)P5)J^Z^A(=$6xN^q|;2 zeUXa7J<|#7<~P;+dgQwnn(Idhr(~12-LIBR92`)8q|7(3&k>f=$rC5ZDcdh9v+iNkiXue zM)b~R%ZbUmsWtMKdkR?FD9Rc6wrR9#CG$qxy59)!Z3w*u6h>3yydN(KMB$R($DV3U zFINp=sLF!5K4LZLXbAz?w7&RTOE@d3g@vBnJ0N6ZUy`=y2W8r2W-!KI7(qz4t7>r)kj1?JU zz&q!mp!gIkbxn2TGDFe70EIIMS3|$bhX~`1>SyskX}RkV6@lZIm7$Cb7yI;SL=;!S zyeVu%b|Y#8QRMM&#CtEi^s-;s8U z7q~Pds(;qnPt7or$OId)r)je2W5un2L-Jv4wJ#~sE!H?wELN;@@T_(h)|=P=Qj6Ao z$>O&>^XyXgI34&L8Cq8|$6anT9WOtux1S1QSgo2Z`_**aJCUPfIR{lb(%=CHJZp%g z{u;Q3RPu2Aco!?1>0FhWT+ByQs1R~9^juXa6c7$%@y9=Vh>`sq>gt$K1(Pg1C}Z9uYIuXfdPz*z%iV4TrMVu*q)rC{z9`SM5243YN&k>j>Fjfq^&BYSO{GLs-hQ{#pm!v*`apV1 z(&+G6_7u9p#8CfLl>AaHj>62LVqDLXe-!gZ2$;lepioH&x(ciGxD-3Ll*+VYOcoGK6dggxS?aOlk%!61 zqHQUX=M=dgtCvLrH-QbS1_}tBTbiVEODxeUhiqZkfDR5g2IeD*552pRDe+=pGJ6E|k9%KRG)z8Waks6rXM$D&gjJc^?K!n%ooA z*;!HH(1_4dzhvPI%>L<>Vz>_e8U1l>pLj9mR&ZmN50>F}xsvX(rgz74!>yrOnCnzk zT*_B;d*@QU0u+kt#P;trfZsQ9N&gFQf&`h~Y=v*DJu z5JZH9(L2b=h1mV)%pF(ko!CEIT(Maydl+RC?xERlI^gzY;cM(kbf<-CIq`GDdBwAV z*CzURa{ssm*%lN&`sT_Wq5+6E2v}Xa@V* z46N+gPN@$krJ(hfE=2gu@g}3g$LQnYPX#jk8 z8~q9YhVn6fM92adyQXGmY~J6SJdAVtm@HSQ%qj?5l3F{qP)|Q8(yxf3drjtqC@N;X z?5&FhWL#*SV{4WhTBr$A5z-NJQdvNi!gxh%&k^HVvoh14j-`LHh1_l2_2mK_2 zte^sce_tXKR9@)}v9*$P54^P&U5HG;9lqY+%kqB`o3ztp+T$7xqGLDY%) zO?&Ludh6!zX)ZRY&J_DJV!+e8KMe7+=;(9L`d2ckxoh|(N5$UpS9EXLuP*u7p$I?n8 znk3s#+oKDt-~}L1&yG3_0kfsw0;_U${pDPWVrOKqui=t%zN+)Rbnd)~&?vXC%@~B- zeIwN0%)zs5M*?0&lkZLV_Yg}aQBwK*g_Fs8H;!FL(k3S z%jiBU4-8))J>K)-0&ABPJwBowS$0_&^N(m{PJu9xj*1kG$^V_k&ZDt?73RJkAhL~xX~D$Fsn8Z@;9bUwyOaAxsMH6k1?C)}DvUfr zg;z0wS>cDnb^TNM+GsJgc0=D7)zd{DaY(+il=15Ey1orsn$Z8yT0uMExXZkN`AivT zKE=9VXbm>dK_DSGU}3N)-jQ$~&Fc8-4Xww&!9hD!!y!vrvv;mD(9Kn%w^trrV6*a| zh*^QTl}kR-z}D!+qVYs(P^4op)}nJxeN{JZl){$no%@A#C@-(#dj%d1(c+{%XRbq0)mD;DWmb+e%e}W_*JZiqZw5G4!gD&D%`6ot;M}sw4>|W1g4>MM=s2$ zOg(}4Y`U9%mh?t=riDaYs{~+kt2l+*F_xMhQ>$^y{ldOaM;U4Y%zp;p)$Tie8fv1y z`3|FSg!jW&f^#i@li`!e#e-$}03WNEy?!KVQA=kuW!v|iE@^xk&-p>*NkMWX+oWs8 zmOq$S99-%Je{vKY928GkXzh*bvmIxIjAi+c@Y6{N#@Vlxg)l1Z;P+Pzfu*4Q6{u%u z<ofFdIRWLy%wjE${&%lZ@TK0$&rp2=(yN_eXbjD4nmh7@P9w> zpl~F4A{BF^-d&|7M2N(XXlZ7qEa*V&Mov1JuZbc^fxCRgBaS$s_&y1Tqop>Gz%Wfo z6lZk~i612js?aIQ94;>5V>v%~R9Cv1FuiTn`Ggw4|F0UD7< zvc~f&2ce4iB*kb`-8?>K~y0xKS88%K5OG*2udW zq}%aH)*v&>-GBI{U@^FhGj0dPingW0zYJ<*)T)j`JZg+=A4c{TU2t`2iuosWaA%!{ zSMlu%n$X?+LKT&7Tn|6)4Q6}f#(#f*;{!Da%~)XbXasdE@155Qse2`VKF7uVsxNd{ zg@>3|EGlFGCG_hsbi7B1gP9k=OJIF<7w;*pG=S1CGp47w1+>?#`UHE|kX$7to|^ z!!Fg#-=9gIPl}KG)1sEJ8&$sYwF^ttbylEogu84ZFO*-F9+;_Dwc2sfWpTOH1t9NM z;gE_bOpI=sxC}*w)57nGh11|BRHhX<>X!P-jG7mDF^g7dSe|xnkndnnHCK)X6BAvs zO4#A0sHF&?;;0@4ili~CP1ol-uD#6=9lJ^ER}7!eblj&a)2#;(xfjuoR6GoRl0qHJw<_N2R{!sU%9jj-lH-FWBuLkwB&=;X5)K zm>ijiny3xqk`@H+LYS$Kl8sZ)45OheDfMpFYhe#981er(84xR2khRpgeh8@ZCogp% z9_<3z>XmDxNzTfB{mQ8HSphhBB($*H_{j>0{j&zFt>I@ltJ=~b?&i$C5Z1^7LZ5nf zesWi{iI=k4n3Ef*GG8ABk!(!f-I_D8*+0J+mm9snpIx;@l+=9 zDPK$F7I;2tWd(HCE58YQ z23_S}Kr?cu(swWLXcP8~H7Xyvt-}G@zP`17l$R7$U960JeK81dt`~3-|6dWDng5F5 z#CR`!ve)z!0z2r>@ju=c-P>zoXxM7_W2%w&NCxOoZhsGm=O#Wwta zId&=QZ^i~pwgE(oaoi~CIK*V~J+Nti3E0TW(Tc|&wKXo7 z(#%Y|J3-2IzmcGW!S-y4(@)MNjU25&fBwjHbRc~4n41p}{M_208q*$jA+7=_3W}9w zOj5xutWjx7Vott>;aP5Yt$ussteVH0-RiIKOhPJL<0Ie1PAF=qq~dviOaK*41O$kW zSC|kSV?YwckB|d{Fl(P9go07|%9t)WPLA*HKG4M;f;Z0NU0M99)la_r%{#;Rr1RJ~ z^_LSVp3BekduQhO5H!b4?E{h6HaUiI-)Xm8JF*zu&KVuLiK4)a?zNn*&z`bg=(10( z?vGimN%5!g>xJHLLZQ}@e?A4D2$H|gSo*>CdX$eB`q_Dpm1_cA=R`HYxGP~}n)75g zauku0P;0aABk0q07T3rhuqXNT=pZJs*oiLyFd~NnpQGSjM|Xk>G|N)Ko$YjsJ6&nc zC3u7VtF)D6JHi7N zqo-S57}~`&Og=1cYe)0eWoKvkrhOt%W<}y1Q~ikE;2;CF!FhMoENa*v4R*bos6Xb6 z2*BD~dY`Vmy6d-7>n>?;BeoA-SbIjlF(Nm+gzyvEHXT;8YRMN#u3uae<`lPTzaA4? zJ3gVTVPq&sYCs@SHdc$4^H!^!ECww*QTU!mD=o)So7m6G>Pg4tv&-}$K*++$U6FatAh=7O@+-dsXdVb`6%04R&~^J+F0Q)Hku0+`YQA9+3S+B>4y1~%s~0jfVEGTL#` ztdpC!O}UDXsN_k=9`wSeYQc6^fq&{B$@f7~ol_lZPEu#GyB+{omlN(UHOP(jdDg;n z@!kIbz-Eyo8M4nspU5ohp<-h}5_eAnhLqt)CnF5d7#+fCCgS6Q?v&Kkyaqu`lCjC! zc$sfWA%H2Ed)JGngXoo1P_c8szH7$*&x!px{ z5Vz!V`ZZ*ubbFqGcqVfceiu;HTK`*Uw1IbLm@#aaLXKK#5-*~RPWBI5d?{qM(V`MP z5X$w3CmGksBNGo_j}L7F>45}z`I0;hacyT6KcV*ISzT14j%Kl#U zG?t(HFfjq*-CU)pMLqBw?W(DBA{1Vc>t%?k=BsLt5uC3YV-i3v)LmkI*HdeVq5N?M8(=Le`47(x0oy)##JIGJ|I*z2*#W(y-CVqV&)6 zlq~9_9!BX?u9*&fX4m7{F3|jPxji}{rT;Xe{Ojs2XNT@)UhyE@p=u_lVxxWGp=6m8 zxJ8XeO6Hm{-~jzMy>^X}zm7_3e?sL8CHXwU$7Bw9+gC$M{54H~sXU24cpZJg5H`Dk zA}QaLkL9ya*;e!nf1E2Kfxm!}$_^_BzHCax3fWvQB~E8056K}{ATUat+H4|6&wi8x zajt-^y;gri$>7AoaFV2HoWuQ=dyQXHkxjGYfm*Xz*W+XJa>FOtaG~+Q%-qqOyEUCT z7jgTK`d=!2>lSz3__@MfAH${=uoPRe$4Xq8jP00&)}fYLw3WX^cj!je+sA&7Sh}GJ zM}`Uo*sf@Oqsi*mZA|B)9rl zCO!vg9MgW?R_U+g3aD89C5^rJu*o22RMXhsM{3hqH|etKl`P!d2o!5-YS>cLAUF~G z&O*)H)_t1&ebkNS47Go~WfZ0-v+vDA;ISi`dSLfisJMMpQ<>d!MCC#OvaNd&S>cz5 z(_lplnpBzyzxrscxSFuxjVY5AVkJ+;UAE7OLA5tskU;w0dLgX{uGQz;%fdTS_Kb0_ z_8DCdOXA%0fsN-j=j^13LR@tuGwgj3Tl=w446Vvauru!PWhHQxJkfGN(ZXT1!Lpd7 z83_vHN)P`ZN>r=~bar4HE(F`1GKY95SD9rXH^=g{LN!6C*fX+LgQilXdZKilf2PQ$ z4emCtO*EBehg>yNavhMYzE|nB-fkrs`p_3#sc))&Ns|E)Y1E1asxd`}NuE4SdnF;_ndM z@P_!~)b5Upsb{hWii$HZEctBTQ!-=NA>O&tso{(E$-M1mn|7oiFd0c)hD97d6>?r! zGBzej%~AvIRAy%SCwqgeU>RN*RDQb%kG{xyz5z==Vv&TW#Kv8maj%Z55fRs2xupI5 zez;!b!(r%-r=WS#*#e2rT01Z66uyw_%&iEvz56~PY zW|=usKIIbwl9KmB*1e0*ZeaBGS)f+SP)eLQr~Y(A9RX3eoX9ZIT7&aiM#&m=WaL+@ z_)k?aEU=zibOpk`3Qs95&G{8|8)>ft3!RR1_Z59dzM?=9*QX7;F*T(-5&(R;7IbG` z+@bsTE2w%J5=1)gob**nc6;O8vPoKCX5iM^dNO*`wvSbt|DByBtp)M>i@qvZ?Tmod zAFLc~J*xFp6Q1>G{o^r8mWdvH7#gknZ$`DFDncqeDx6+}vD_+$ic%`GQDcHtdf(A| zuB8ol@u+hClt#`X8G6~%B><_8Pg-zG&AE&Cd~(9;)p-Q6ZRN+(m*2a`la=8Qe4AId z5Wecyd-h$erq_Eg$8Xc=VC zBC;6a)~ZDP+5u-HFxGJOyE=S`ow@r7DKdKZyOrkXh93*V3X*$JTqEQWX>wUfHLio4 zF^YOhzD6P!KT;0Lm_4B3$xH_Em9VU|Qv)^xtA&cwAL6kXb3;>1=UOD$XF6F%Cl9%Rj6SNM^;mj76CqUp2-Fgj|a24BpFtqtIGF0U3-3BWIe)B66OLlO^vZ-^Zn!fpOVVz$% zs5{B3zpC6+dcoVkCPF7k6qn<)H8hz#>3J(+IiDldES2#;bLM;k_1wX#_@!r`#R!xs zI5TteNUXeSC_(wHU*ZpFjhI_D0_rYUT6|_YJ#DRA3A0F4TBzFp!CY{N(S_a^g#4!@{ zRa(BS#;|}IEj~mE6tXP67(R>w9a&$(1`}3q5=xP*#LU>SmI!36ZQj8CKO2f2XLi%m zm&$C#PL1e+6kpyC41!0+?gq<15A>8Pm+9xC=%_Sq{O@FpIDs$96WMwyjZ87u52LX- zcg9HtZt2z=T2b1~uEk(zvJtW(y63r5ajt8%UgSey(a2a4;hkhvQvk|V!fS(EB z+xFd0v#+iHy(hkgY@umu2S&a69)hG)6rePHhdcUJCFCSLV;anYdg|Xh| zeKh+5i7(0Y?bWd}=%^s6VZd6RbDgk~PCOhXL|#YZbjg_JHWY$BMHFt5Kcxo+(uI@o z=%o}3Eq?)SI&}*1sI*bgE(L3KYLxNF63>H9uJ(e2_sfF1@U8cwIuL?l!HiOK_2I6d zN2oPTkhprLbo}uf26enZb5%^UYyD7{Q=_+xIq~yp;nQsc0g$j`upY^q_HnC_CN`C#e#Jnd( zqe+j8L>Uo@lE~Y~fv`V)opdafc*dGqX;EBVYEBnr%kj;M;cc&i+bpf_YS$dWgK2SL zB!p!Yvy$|l1%-I5H91}s=E!qAcdvQ(aL1A8?>gkcdhf^2_eN^`6Tb+vB)Pdfi|W(jd-8_KAFC4SI*rw zd}B0k^iC!Oo4b(qL%FMK7N%{20@Elbe_0lyua7p{B8=Ckc`0pxQi07&1?I|j%7d;h zHNTH#isq;ra>VYf(g^%AxllDiLG~+;s6G&(d--X zd)!52lrhkL=Dl#p%$FJV!5M~`%DW?_oZMugo`IazbcysVE9+BIJ2yNGlHX=aWhi=e z^sAG&DIuhkEY$3F;g!GNKUyYt=B`j!fLRH(J+?p@EZLCepawDUt!=#62I71JsKL}B zZH!0I4Tj?>7Q-Ay)KKcopIzIDcUp%baErFtv=qRuTXavrJ^@P*JE*R0 zc}5^mGWk)mFn%q@hlHzQfKO7Q4D&A_GJ7;)EU#z{Rd3**p=bI17zy<01@`l=`KHtgf4vD%0a&ED=HZ?(-e zk@$QDA{MJX=l>D%4ZmY*&N|eT|8#4qL~3_KhlzKR`Qxp3c%M*0K3K)7yDE99K|fWu z=Eyx?dNPEJX<^ik8vjEiPa?kZFqK;b|H*o(YLirzqZ#KOA*JfRBw{3#N1yf|5}L29 zej)l=++`lkd0Oc$RU4TN&|}HWPs{DEO`9fPm8LFuMFoxofq6o7?b^X}3RI>dgQL^{ z;`G`grrFxDeNY1nLleaH4R6Q88yqM>2-9&hC+{CMOQB=FxPmq=5XJuS_RY{QuTYSz znr@fc_%s3?Sqm#@n6U<6HK#ltgEO(OFIN$z1);q`%%$RI*$&!gD;vTPY_phul^c;= zW&167%;*ahI^sMT^2NhljKqs0lc+q2<{#aAKS>5JXyD^ILh)mAq&}7W1(bw}ORq;6 z)3ym`zyNnlVV#bapf=G=t~sIPUk_BWW7W)B|QZbYoxRL4UL_DV*!iUyp@MSWf_ z`&TPtlb86Gxhnf=&J-n9>dfA7n3mlQP0J;sOvnIKg~F6Bdig@WkO{gE`18(7Kjc*{ zeIH>b3WOZ3jyDAQ3y}%P7HBMrbQKz7esK~$|Eq;)jX})uqkde&vT-`Sv)Q(*7+K)g zeR^Z{n{XVR?!wOn4F^nU-`K)=tP8o3{Gv)8zVT5HMS+2I2}PHkZmcpKs+lwmHcT2P zmlzr>!B(2zoBP8W$2!X&zsT)R7H&zR9I(Y$S-h$@=J#3{zYo0esnXf!?P`^SU6`Kc z@FsazQXkmTyJHz#*Yid31+Ij6-|XE~%A-g9SJ!an)9mlf5Xw-0l$L>Om0;KazW;*P zXV0T6l~A-^nbVhA@`#{^+}#UdsfbL`mQjC7`@BBUl@*Q2q9&|ixgHXy=l$$Q;@$mbhr{NsItgSNkKV|D>m%D{b$NihqLeWc*ssF8rs}?S)f^nuFr-95S z*eQsimeL^+z1wzjfq|CPRhk+|mDT%6R6+Enu$tBDV^yg4b-ve?2`J%Zu? zab|98S+N4IEmfiSz5Qxxl)Yvq7I;!xuXy;V?M3G#G}lE2KG-OZX1I|QE1-*yCprIW zAIx*#6nn4hs}y3sQ~ifaKi<;gFJK+=31!v`%Njg6bJcWlNV^SKnk)ZG`{^TRlp7%n zM}<+|y8L(#pB8H!r+z|H0`o)|(teJ9c*3-^U4uwzw>?J12DCYr{)J#GGh5Mt_b)&v zD%<*KW^|@auIVWCXdsGfQ%(K%JX)>Cb0(@&VJruPVo1EHGPVf>yxa<@Ow(^d1(_`+jnx6ut8(CyG; zsuteX5oPf@5z#}bg&KJDOhGp5$E$J&G^I*Sb2cbzs)UODBCoV*%xkI(alKd1EuWgV zLueOMNz^BVdabiG$5^^m{KLCgBWshX(v)>eYpNV)@I{YUhm!w;TB#{wNyT3P&136L zIS-ZthAQTKOPz)b@t()wbcMV?j|Xp-dod!{ide_8-%VrLH}dtd?%|*7&4HVHUYre0 z7Bb#gSEajzH{Q)|I&SSg&Jms>$NvVWf3@B54|TPs9L8Q~I^W=X1)clTP}dC~MK5;4 zS#8On7BMgGeJRwmY@61s>M+C4HH`*D1%3%M8xyh7iNirnDptj*dfB zR3f!~9?_u`o#Q+G<1HK zegajVKA#!oThX$>zSOIfAfwD2Yda9A+Wg#^rGTRS1r+)=q}TvI1*Q|k?fNjI)YKJv z&n#Bn@)b*9Xr6-nFz+EpL&BKYxuXM>eF1DRy zsU#V=9~gVA1K*-~9f}(#Xz^LrcRmC@Nh#JHNdz4igAJ>XM<~bZBVbrE8xfBsW~A^+eL4$ZC0AWTf;BD?&7}7>gCs z#&IgGlPtP93`jMsqy!iPPBOlAiz~04TZLM~<>04Kn?I=PlM=x1HrSXXx zoNy7u=5r-{-VT|leckl0G?9)RMt&VC-9jivYNYHW^4&5vQGVJ=10J5UEJVud^$Ji4 z@8KtBiC?~rsHAyss5GYXk~)~ezcg=7L6MYF`{=i=CBtkT7@31ewYyDS$oNdfSNiFNX;6s%{s{zh zCDdu6Cg{#?v-t19?L2>WBt=rfb8|nR)*YjC10Dg_0E!8-GDJ+_EX2uyp zMQD(bDiP<)vCgS@RQuAX*TUOvcSe%I*+$T0)FAOO=fu~P-+9Z8F#AuR?yr-N z%(5kl+`McH^)E(4WB+m5vbclxagxARe5L2zm5!dTH>@++`m-i3W(&Tl+|YYPMb|s@ z_L7wajU&qzVZD__Wx)Ityl|&t3ik}c>ZRZn@qrmPz*3EF*eoyH5Sd)QcS8h4N_UYGN*=fpOlCPz`X-kuM402+mihLwiR>PN@J1GdHd`_*zq@s^0QXwa z3)5%XZbkEaK@DS&6oc3Ea-%u6QRTqU|KRZ}MI%gR^BJXBZ^%uCs_!(}=U9J>+Tny7 z$A3{QH|l(=&Sa0E)w#1r&L~%DsG-CI5K9SMRD3Vaw!WB)r^p;?!b@wN#z8gU*%PQ< z$wXxK>iYqa74*{=J?}QbqiOIfYW;muS2}xv;@#Zt*Qop zm!*FUiV-g$h#*tA-cV0_tp2=Rz?QuG>j&7miPg*NuzA62rxn*}`}CgfxDJ!*G)${J zfB0CdGtrsMh>NfFAp=u`SNP`&i^0}JQ!aX1fTs`x$d@BVl>JVtJ=i;gaoKq29>*FN zwzZrdnl{bT?WjzLG2y&ywU$w5G0d`!m;le^Krt^r!VAFRypPJ;^&UKAU>O=sRSegO z@oPL-rn7MTu(>Qyv+A>$TI`sDt7v~6+z=E0*6z3d!%ochi*Xaku9!w*mk~iSCY$sV zxT0HjT=~I+bU{hIL9r<3!@!-0m2M7NNNEQlJDRM_P>_mA0il=YUG=Bh#YXttWM=hU zb0_iTsDeiNL_vR<6e9WoD$v*6z7+3B2ENRJ1{b{jOCT zBoj~X5ffd>A=pj6)}=d)2~Y1RkR#_fsFV>uFEYP=F_J#ODZjpbqu{;Dzf2U?2@Fo3 z8gDB;JYm89+2UVuy`dy4UWAg&rGGigJ;HcW$hbp-8aozEwS*nn2_c+#zYfF z+n`#|jTaVz${%}Dc2-pxlI?~O4R!LXh_E$Ql*DVbiFx-5MmE(&EJ7JB7Wt}C>~byU@K(u0lCDR=oDxn*$^P$PmqYm#Bd)uFkJ)S;El$9m@pW^Lq+p+;5=lQ1lL!G2;3 z3w#KQZNny|LSN!Kme8_#$ovtl z(N@y}-7@lw7FA1}P09k}W6wDvL6)K(ks4}1$_cf{0pJi-FR zx)6sZugjuQpb-Hjw$19kJ9~DGWhgY%J9;US_mkrq@LA)#_xkR;h4U<@be9g-`%NcPY$7+_YTW=`_rj-1=!Nl`QwS zM@$5N0hV~x>JTKr{`6U;mYq=3u(4(F=zwNk_1wXfo)E`qHz5v{<@-<6U!2dgu8R7I z0bXKQUeqbu-CpSfj>^jG!WU6sOcpmx{=<=bm>8SLcP33_A8F2uCI6-D=kjAUwLMPT zTce7F>Gh-c8nk}DAg{47Ii&3?2;~^rUx3RLw{TY@Jn5$~dtXM1Y+@Hy87;pYeam6} z5BZW8jX5(U;xB-{CAD>W-;Yy> zy6IcFTSEnSFj%N*hDp81JEnn!snRS(nA^bo!|_6cz)XtRhj6=Q`n3UbAYVgS*EW2c zkL%$VZ#QGhk}un#i9Y;eTA+p5*ez}bX&Yx!dVEdZA|5r zbcCBU!7`>af7*?jY~qMFqrO2_`=*|pHYo$X4A~HC!EPeO)gVW;@+3#rwfHivRw_;8 z>lC-Z0S>TZao=72MeWWy)24J=k1=UgGyW4OpIm`cqp8gz@=q89Px(lqFZT6F2<8dX zR)0!Nb5rs?v0{i;FPR30Hz00mK92o*zB5YgSqbSrg>DHqcZinwTP;+BPUU>p(j_gQ z0y;`>4ws+vSJL;ekl>IuwIF-O!KI2c*Jur;Hyt2@TR#?d3@Vy?ifdKFs_n_5B9ZRUJtmE`lg?+5>o zHB^7GQPVHv7gOQjugu$!S=tA+sTPBIwhp(foU!}1H!4n#1y}}!8tt}_^+$@56-F9u z!BF8qNV#H0VX(pP>1$s>ZKl$WoiQod^{viV^Ww4`XY>F67lgE5Nx1AZw705TmUMmj zycnF;D$TLh4YZUG?k;!7?90^WX*QUoz<*@I-C$)X()4X7CEWLC^0twr`O7!EpJ8Hh zghxYAa&xjJylgSSLQvoqM*QWs4XK(9{il3Lu0|ue*)5O9&4S1@`kQe)?@w1Ie`C_A2$wzMFH||& zyzl7bFcm=Fs^j+7Pr`*a>mRfN#|krh0%Hx5r+sf?Fu^j#AG1cP=Z4rJG4fVv-#qAw zb3Fc0vsQ88MS5w&RkwZ+e}To%&5E4$EziZ@3eP*v7ctTl$zyn2HlE$$*~xw*g6 zRCu~zP793lCErUW92_i6BC0HAE2-GsDi97AD-W1~+Q2zdMe zRY0o0H#Xl)Bn&@;8%~=of+{sfXU(YmmN@oZF&}*)eDBT8hTGLiiqv0Ykl;_#h!jpx3xgVhl1r0U zRC%ABLWeuwYOAP+c(gJeGqbh{IDa;EWhvdmG=vtJfe*hk_P-_`9M3m$KFls{7{)HP zQD{%j#p_rsRWYBCiZp&v=F#xc`<=saXJh~6kWaF-6PWAdt~c#$H=6hedL%lYi`piA z3H0>EudHzt+3Uyk;alWYB*BF;YJ3xSYq_PmspbXs9q10TaXs^ys486|wfdobb<7`j zE-R#y@W6t6+G4r8bB=-|b}hKYaAm~o@7TKc^p zE=*H?obMveG6D1P-=9iaZ*!>q@wVmkrE&A&b^GvOfHq)n9tTyCvZ(BirjWeOVZ8AXxJIkgy#gARff2-Y45pg@&q z@~0$~|C%nhA0`zfwR@^BC z)$G?)#(v`SJW^5o;Z5zWzPRwe=iSZC6{QPI3a>vY zMV#oh86u-g3&!{GU;9ayFVZXC6F5@}JKh0}4_ST>wf16iu$Ee$L-tV(M0*I+ow}I$ z`%+8M!^6f_M&jRQ&s;#VmN9b1%@s}C785zWD1ilyW8t`P*!PkC%S#D@AuPVWe*u_- z-4zta1vEY3SraUAoli23L4pxE-GRjfxdmBsHs(a{UW}fqt2a?j{^ztO95X~fP#^+}y?N=4rI0OMX98J(M7Fv1 zLkawLiPh24Z|0V(xPEHP@BQQUgBeUki^#L*dj?>`!1o#eZ#duhA>eLCbH^L7u#>m~ ziA*1QB-I!FLpLIM3gpOhvY3ep!+B4Qg7@^T@_b1mNRz|iC&M59f&z-^V7+czg!hAP zo0f38zP&}U%g=3blf$+rJTa>-dbg!v7L1m&611}H8SwV=ZluZ;PpFW?x*z{mGNltA zu8gI!sl`Dsi$M2-y4N&lW4-vY-yy(@nF(QFj4p|_SH!f|Fpetk5W!k9!a)QoNpb!H z2QSPG{*KxR`9tp!-cMILc-=nE_oN~^&W&WvfgT3YM0&g61-k*Oe3QR`+juO_M&}iD zEvmd=hTknPYzTTS9Fz3$ZnT}aE_XS)p}VgN_ilSYT-J0x>Q&;&UjT8>_t(l~nr$6v zQe6QO3stC$b$qDrEa%P|P~F~-SU0lUCQ5hdL@UZc8l6E__};mgjBS0_kE+Z0#;jL1 z4!6Nl!{I|^w&sF$!>__@Ti(15aauN16IFj)wmo6p39))28m0YpXd{n9*^lG-hYHduih^I} z0hB6Jg6R7oGy~XZgUW7t?x6~;W zKigpZ{QdrCx2EgXvSn2iZc&-N_rf(Ll?}&LsN9X1lWL-c7vG!NorgQ>E^C z2BajWfCXp6NRpHwJmk~(6`H0Y9>}!tFK6JB*}m|zr5Q{kubF*kbr zyWnzPWU^evlBVevBg%L5fMy|en%oaRI(#_+G;?5b%bThJGCjvNsWnbk$(T3o9=f+X zI%{nRW0Auis8F)Vry42$K6l|)!3PAHRDJT&@$1WjLPFV;L49O>Uir!HKQhOgABCsE z#&{u46F4L5#eR9xja;btq(tHQpt1(JH%y{7t_w9_-h8Q?S@^#`bC^Rnohw`}dzyxa zNe42k1bdW_2_MN7|I`w#kNT43hG{BW)|QX#Ve6PkMvF5sW^|_{nsy_ zo?H}7A2tDShTr+6K1odMHSpqJXglBB;Y)jXX&_`<#zJy|6+zCnePD?BEqjJeIO z=9X-iO}7idul^$FZX|m7>s8(~X(fw6B|&rk7T95ttKC7cuqFyz%rjP#yf~AYeY75u zx^P_bu)C7(t!t^w#m)ThzEI~YvC1&F@L#|MFH!I@septILr2isV0<@&*4QnsIr{*N zwh<4#h{UZH!EECCae<(+mLy~CX~bP(Wb1pOMmR@&(z{5>6r)R?eJ4K%pNMu@Dls1F z5POpr>sS3rj)BS%L_Gea*;uu&GAMr5UB)&)95SJ&8`3~s{D3{ZuLt=_Bz^4@t{F3`PQBmq{ zgzk7lWk7T8&WIZT|vtg=T3^kQ*iIU)wwnY)khgO;6& z2OGZlZ=g4yhcBwXmoaM@@`xkmH)U1%ZVT&460?85h#mUg?yOwJl((ese9wjc^HKbd zIiVzL+x`&NgM{H7@ybz|D+~)rV3+tAIi0Q|CIaGi68yh~`YY%ORC(Vr8`Nv6bYOO3 zK&1gYivJ)pQZ_XgAB$^!%ZyGX*V|ofvVdPr+Ivt8aC1t_9c1Y$3v35X$WCStx+Q2V zW`x2gy=On}^w3a9BLRq;Q@zxM`8RB)p~Hn6@Wzwb8thTK{*mIwiH~J$Vb~^d)QlTz z$6{}WdLn(`3_u(|yM}pJ*cyVU@Nia+J~pOCB#Wjr`8gxh{euS8m+Fn}8k!L_Qm2Z4{bOXHnlnEa zS`k9?IHYsnpf%tMSuLW9T$(s~-sg3yS%cjc(@1=jru{-H*~IBo7AR+W$-~XQWta*A zP*;6eywfE?ckay#zg6xQ+841tZMQp^i_hatJ`VmdhaLv89SZ6r`l4ESv4Xyk#_qyc z8*zOu?#ZA|zFh8@{(p1Id{R$pb1Dh1QpJRW+dHejm?-XHtq`F~5G|6Z2$uk7UcVtX z*X+i|p?#IpFPJ5-eJeAK0rhuU7B585&wG0Mb8>p5d;rOVf}HF0oS93 z@DzzRI9J<@3;-j<-Gk_yHsspMBd3v@-*8cPUe(kl@ZJ&LjQYZyeuD!MPb|pS(5^g) zmtD@|_hTF@)Ul2P9?JC_^cQg~^oZ<^55c3&ZX5uFpMR)#oFqD|Va3`MQAp?)Z~c@w zEsoBbQH>YASfcNOyBu~VrfPIG0Ht_N*>QnWLe9y>i47uEXwh9t{3rgBX_FCSlmAC) zCveD(telq$`$`U8#6vxXib*?ZhEqp}OZ#mcyJTLl#Z?c5bcJcEZ^V@Wek`JPZhWY# zi~)$p1(5vk)&6*(c_D=kTo@o$?X>1vC#7%@e=)0F`kT|V>4VmXhc8Ja_t*KR}BY>8Rwg}#Fl|`9C;78R3Yk6+=a@iOwF;%=~wB-M=(pPIm zGb$>q(&1f9#J~F^my&~e6%|LnX%Ru(5M|5KOhfv$rCBwsiH08&&AQe!5ilupOUFr6 zuy5awe7LzenY7TR%q|0ydE(`JP+!F}wfWv%2JoRAIayf%E*_tc(r@1ub!F8#&@c;k zHSU-i3V+2rSyiV9^RQw`cDp~*<;UZfs@os@3Koe0D-er{hIo|`tY&0BJz~55xRgGE zspDKIefNIkL|cbzL4O7otxiqXu39I9xYNOv&XF1?KTienoBRwZ8ocO`Y>zA|@BG4k zE}nL{u0`SZK5jWW_^5k4)7viXj2xf)GHlz-&99EOXxZU;_+G9>u|~7Aq%cFY+pw=% z^N4Z;^N59$YZ_5gze(WHx?rM>s#8P{kTyWfpLxxym{e0IsFl8(3NFRQ7W-U%rY`Q= z>}^W`HvcyE+3B*dcQslaY=${LJB0PP+Vxkw*k2x>ASprnn(Hwv?0eT5*D(JAVoK>n zjrSz{^CWC+3oPJM=ev4hEGH{XF8d8j+IMk>oi@r|Cmw%Psb`ep3u1{|?`^mm1sAKb z5)IMl%&~$nMI)2c71T*G1s|?ys-o=Hh}6s1J!HHx0v-d&%|2-L{kgYIM^O9q5fZCi z*w7Pss-utMPyO7hD%3K3L|)k*f$_=zTtVu6U|!YQje{Onhqkqy)z(lR!SQ4JOPU6& zi^lK~1ALgRmN*k%mH_|N!2MT}kEoMEnyy}0TxZkmS3TI3s*n#2b{y3OY|mXvS|+b? zjFT!e5Q$U11s=UN{UrE;Rcw8`U)$u>CwdRnb(<5FqOWqJu`+Q7X~J8Cvc`E%az6Md z@s=KkGZph>1e505*~n5CL?sgM2VE0VN;wasWavMoC}G!GKaBH@52`-UqmEv9 zGxD%CZI~Dq+>RHjEEONMj)^)C;8`ui1x?TujpFMF{U#ptH~b68Hc=yl^%lpTEgXK{ z)U41;U2@A&$@UWi`4x3G7F=VVSNZVVtF!mudLOMu%y0F#ld7ZSaTnMy9TPxKbDBmM z91JUNtGQwoaJ~;;XBZ}I z;tPBgiA}9GydPf1P;179`${ut+jyZ^^Gd=?%@Dm7J=Fu=>DiS+N?6EI$mMmIhXD)1 zKPQF(|3lN4IeZhuwc*I|c4yo40q+@01-xQ&)oKA5Sa7 z{2=opWECw`*xBaw&JF^hKC2_}%O;OZbL`?4F12v~zTfaC)_R3S2iF?#F4@shM9$`X zPtO{FD7~7V$nCeu--taI(}0EmCxYOX!DLz0#+}Qz9Tzp9$qOe0{pd#1t|g*@{L&XA zNY9ts{xgSF9^Y-hHBm-8xU`xvQ%cJh0{*g(i!3qc!gBt_sXf6T$an@XG_ec!mAQrH zI0}xSEJp9_y~0ZVjl$KJ;O z>l-U9P<|)9M`0NFAg`U`xQ_^o%IGTk_ zbQ>6X%1Mv=&G`xTdakTM&EkxcA{X z!y#UYBW>-s6)q%kCum8hW`^bsuhO@F`=AvIR`aL}_i(L<*ey*bp_&gc*z8Jaf1}Y7 z0z&t|M~a-h?^Z#$Oc`qqqzW|QC~wAR5u{5%$iYOws%MO6re@f@x}7T^K>Ad zQ!A{h4PiIYt&mr-KSwloVClK$N4V-GiJoh%HVr{nyi8LJf#Df<)u8G;@#XH{p$2Uy zLde8kKY8f($r)4IB;OOK4{Rri4?8a**z$2RdBDJsFuhff3unx#44U=$L(m2lmyj#7 z2Dbn$^8sfP%?rrkVBR37G=aal${X)5;QaGg`^3J(lng$rP~Zf!pvImNFi?yA--YPF;RFsxe$o_2Dq|*wa$gi)HNAqf`LxvTSsIfK80IBn$RAd)qQe zqv&O7&y|UsC4eAZ2E&sS;>_m#RYfuAxw^?JKW3;s`nOj-R%56%8C;JC@wI<$+3Gzz z%y9KWF4w;DmZF1J*0z|%>xON}YNNz|J3>oE`u$A?`6fOLvWT)I`N!v#$8LMAnD4G? z4I1yuAA3|P^o*a!U3O1GAtZc%X_1ctGV7bv;dV*bZ7y1ra!pKV?O?)ED3w!WO(fi$ z9vM5E-P8U+%%{xm(*~ge=Z=^ynjxGB-5XSF!Rh2EB(HtDC9ONNd{I4qi{Z1T)+lUH z)r*tnoRDsn z?8@jYB`$YKPRatoL*|*v@31R6hyvBf`6TB`z6+?5kKC+(;c!p#ZHSFh`OgOlKWDTI z97BHqD$v9K0!m)^6jSLy2ww_kL4v9ut`d|@Zib1+N(+#FNwJYIj|q@?9v00BB*1@)5QhX+&%#z=Cg z^0sWTEbR|C5*aGok&XK%mYvdd}RJtDiRers~424?AmY zhu{`efsKr%PweadF>8#9cNLw^2K^^cTmF@gEk*^K%=2X#s$kH=$3LGK2JM^9rk#&< zd6f2ir%t^;{VtqzT47ia&R?b+l)asI*jaKh;(S?pYU*e>JlNMWX$HGmS#_s#2BK zYZ{aiX52CyY^LrjfA`L7#b*im%T?uGLh(T2vsZ`mdtEjaQ;SJq)l>-kXLHn?MJAdl zBAXNI*Y$;YL6&R$@OK#wL6t^ssNQnO>p5KjHUXko6O{E9G*C*Sg|;)ysjKg|W93*bhCi@< zPHt?sxGz}iNT!;OJIeo_Sw^p3Bb4XJF@KTA>L$6NL@403`aZp7a-Cg z^%pQJwKQ`=lM(wTd7=JJX|`o;b1_0}5RNgqZdv7D&XJKQKl?TaDSc07oNri?LO;$0+_nrH&0`Gu(9=rx zQ~#!oK(pJJaGMLNnla9DQb1oaIJH91I9DKOMzq{7__b`K6oN{NLgSY4=Ms^o)vGBX zg7;9H1sVu%BUQq2&0GnP;Wd^KM^WclquUua;8- z92=st96%|utF_5N+Uh90tl5>U;_?!;d1mfUV>D3h$q_Y(9EC?|?8$($FBK}HnCehJ ze{d&-Z?AaoFa<+^!95!FyQg2>QlW)>lbv~HAow{BL&;{#R+@Tx>MN#w_p`n4`XO3Z zw4ra@LxEUpMNz!H$1b7L#OoeCw4DS`E8nbtnlqFzd@J(3XJL{kQO{E&o3~y_(EPG0 z2ri8NCFNepssb1-xviGcA^1I=x+ot%o)%)%H=D1{ZXliyCHfc%r1ZLq{DtU(D~e6N zkk-T%fTkXi&QB{@EIjyj(Q8X71v|{GY!u#W?_4H5%*D?Dh@$N(Jpj)LrNo+OX`I4(!E!~)}TDts0Nq1vr z`&cn#tKPbpAP|k#3iPRwRr8S~9M7HNR<^Ka4G6yCI+!vE{xJi#=tm0(dl{e|a(fpo zgF!>g6j&(OY?xwjqL$S$*nf6lwKTEBqeJCps1|?$$9r}@Bh!?GvlKvd>2%uYASRhL zVACuw|GT4{T^$`RafLbvi@k9$u`i3#{O8_gqEw6=H68A%<)@lS->oOwrGVgc2eL&? ziv~@l#tN(L4CfAqDr zcrdCRc~UGxZ1vxqkFu*|de6&3W>J~qLesC)322e;^*r3MKGKG^AH@Ob;q9|TX(O4U zj}Sab2|Pt~cm4u;E7kSSw7=NOP|y}z9xXcj2&YTRTW!-)v7n{nUbXz_S`xkfgz_uY zcca2dCecQNPLqGVk|R^$$BZvtxuV#t`X>hr#T5GW#z0B%N7DtNnhp!M{~A&xeRK9t zi^XT@->l9&= z)jy7{5nOz%G8N#@q%-lUBY?}=5{$%K5J|Ce`XBPduVf2V1S?e$>FlrL7^xb8Yb1r(-3T>H4V_ZD%M6rAh9i&}0#QTDpzLZjOt7SM+(t zYe_#B!d3vUEFD(gM*5y9^7$@4xtneDpIqcBXCDIpLKMZc(A#gdZp11r@?{L#u`|ow z9BRmJ@2yhv?Uga9q$Zv*7SF#9;>0T9gqfA z6!BzURVFlXme}Mg*p!VpSLV`~mh4!Da(-Jg?QgVy6^`2Qv6*wTCQ24s?SVfu^&CVz+ zP#?9??GA2$B4>IIg_3*p*4nG<=&3Ln8SPrW1q7?Yy(P>~8@wg!w2P07sY0r%&bNO7 zyrLW{X;wln)#!M;8WI5={_kKsTxHY`(%b7Bwj9H?H)wZp)0z|=MPV>P)3EAP z{0m-kNjt?o^n_yK_TMQ#y}tlTi6+%J;!(s2Qjbc3we46H#}9elENeVGS>FoncPh4O z*0DH}N#8y5KG?F#|3aMBrVFZ}zNZ;3WE6lqN+v3`n~S;YEe1)F-b%ro$M}MlDg0e! zeiGn19xQ1!+ZCbiE#G#^$JgGW6=nw{^~ zGNAF`tkMuzd1bx^UQJJz40%P8F7x`9+vK=~P=Y(6nZ0lVZwvgH`oDdYPMy!(6Z}hf z%pCT$U@O}sy#{vfa%Fupsa1;{-T%s{Dc*&2lx+cPjEYy8j3BGfWQix?J?(h;w0O6F zujimG{U7TsHw*b(_54N@Gd4G!e*XweA`<*g@fVOD^Hyv--&o&ngO01vWnrL#8Q=J? zmWC}-{26nV5LTXS+gE@OKRY;67QMlCnk&F@#@X;CwQT;y>^BK&fIP^%*##?V4zgKj zFcw*dldUf0fY5keTydHfI3c`|;2c@}7w~Cg|{@38wTio_U^mH)w_Hzx@=U>7G>U{m%Jl+NQ#+K%4lE=B-VJAa<)w zQ!3$Lilw}q*&9;X5o5C_|88E(C}%>T!U|3#N;c)r5=pCp5!6Wa(%Vk_LTkQx=MEn( zu<Ju73fWEULj&gm=?M$s|HZ_#4M(AW2Hz-nvnOP$ZYKxGvgF5g6nu@DC^^yNSAjY=w2cP4Q!SO9Tjkdlhi+@u6Mg>!BWXdxX zQ*Z?)UIZ^&Jru4uDy%7*_*`H+14fe#)~04@Y!k}Jh{NBNamLyV5&X7DVnXzY>UNb9 zPpEo64c=1r|bU!~{pD%#1$!ZSL&VpV(+}x`aU%@yU(4hA8wC z<(z&FPO<+c+zPi1`KoT5XR~BaN|TP%lOu8XXM*)gWz{NP=kX))v)(n|vrr>g`LN}d zIw3fcm*HT8QvdKSUXYPrzr-_1mEN>}c>SdJl9K!~qBC{h&YAzOwg~J_eXa#R_|J8f z`cE-0S%F6$Z3@7`s#l(S(WJ7qFD@k2-Jk31j;S5My+Z5nfX*$Nr&M;+rFEIhil9 zQ21Xhw7h0|2d@_J7$JXH{8PnDHaP@Ed+Jszez)>dlh2m4SaN=E<**aaO+vbLEV+U^N48Oc@78Bef^Y(A>Q(Tzw{j}EHJHgJIk zCGUUzXZKplwxCi)?n=L^P?^TpMqyIK9KzkM_9}&^YC7ysYJ~5k6FHOobj*1;d6CLdc zev?@8KTGo^HI9U0X;;~fZ8?g{D z&a9FM_)k$1V+5R?{*>_9UjX2T$>sKRejxPRXY|A&#i-RaaPJ0BupoItgT~w`hP4Ya( zS1x8&VPXF~1DRxe?Uw!NoklC!_o^%9Xk{(SeAj!LjQaL@_=uz1hEqKv1yj#zujp?b zQPZB5F~U$Z3+CqA;XGoUM)jz&<}?d|x2S0N`kk@6QEiN0TdX=JvC$U}%lauc<^zSC;$!Df6?zSyd2X<V=ok}*Dhj53Gf@fKc*|6$$M{$9z~7(?wf zSW#3>mKRy)qN{zl#^Q)FP}ED2GUJs!CI7NMAPXVbvng0|iKn0KFLsEop631YdWoq{ z^b;id!l|0AkA-1UsPadE@cyn^e_DmE`Bl4ZWT(7?SK`I^{hE(vc|&kcbjH%5K@pTV zDD;g7aP-4E9;wp5>OLwwLl_9v?B+YjNvzbxQcDb#ncy)@d+fG0vDzEAb}#eYRyoBCFe#n+Wy{~8*d;j;)?p4U@S(Ze^Zw>v&g(Q9E#9(Qsg8?OB=H0)Kl zjCi&!YvjyMx@x|RMl?C5b@W+scLHzLtonRML6Q^f-zo#?D+#$c_)V+P*zNFL5;5e+ zUDh2*WWr-cl=2goJ-i>nOhcLFT&tT2U+Zf93D2Qs16$rUNdPB8&^hj^nU5OnvIAa| z&j~2>w&-)Vm;f|v)jvjK&cQ0hVEx6*;9ER$vd{|$Ohq=AOy*ptODKVoFCXZjx?^N@ zt=>bBP1uDS`7E!DoC-Z|Pv zw$S~`=klEjvRDzRvj*-8_GIxjKa76myz)O9ru_&FJU>9TnNMC$E15;wHlFi{Aq!;{ z#4EvZh3}25Z3KsV7E#feyb6aJDqXTbXa53R2z$4rIdADJ-&HUIcibuG#x-m~M66>Y z@Xipg`H8}n4^lYMpdWpz3nPF-4}CKhx$hbr+bOkWNep}x5w zFgVdjeNy}xGv_L?FtF5BatTlL_ z^+>~t`iUb154cOpmxPV3$-*#dk}%-DPT?dSKwtlz-;pfM;0VySy^ZZdA|i?GM9^*;V+<bBI?3^+`W|Fy>3b0=@6S^L+eOs1 zxWPqygN=D~BgP^)PS6sOS-heme5@qckn1_2c{$_y9{fM)(7=d-)lzfYuish)IqGj} zPFv?B%R7J2jWR}_4IR!C2cdoZKHBYi1gHaPaF{?nfCI^ z?IM<+BPQdfaWwkB)$=nq!cV-6X%yFm73LQO;zD^ zuK)EB=!2II2x@&Ov@C*0=<1_ZQyEvju7ClxUILAUh89z)tei}JW)S>2-sn8NQ0I0t zCJ;Tk4Cc1C@@!&;w0Y1oBErQoHT&%G`RE3RbB9=G2YSAjL5mSNRDt`0^~n387zg+U zypkfgrBUW$w!b7DDOftub29#>vfJ=NP_30De^A}j?w7sS4z#Q&Zl>}H!(Bq-rYNA36(9wTEoE@3B4dLoIiq}C03Rs_=RfDRfhk1jgVb33DeyR-(LPJHlVMVUm zmVqoV3KYddSM*lH2E|M~XlOBGtWG~6ZX-qFqp35dbiPLyIl;`;WKL*F6`b@@;tKOv zj4YjTJ`KZWE|#=gmZn~0(kFp(3#FW+Aw~y3t8`ddt;4{xlyDo2&2!`aUn|wshm^vq zvIWNlKb0CD?lB-e0_WaVVVWx#qoWZ9)ze7wLays>b`$xg&Y2(MBJfT?-+E;f&I2q$m|KMxR)!AgX{7 zT9@cE@&T>l$C0G9iVfxSEsb>>p2fv}i?@J6SqeSv>sp?93Uw!gm{J^D_&PSXgp(;% zt+jN;`d5-BmfT31Sw6gn6uv6t8|3tp&$+QUWCo%O=&(TI1b%XgVGCbHyoN;3q*@vf)&!k`%YRN?LEdsKZo3s_-_!m5I6f}&4%M06eqyF?>!|#%q zqx?vFw8u&@rV;T?x(vp>=`&^Q-to|RIiaTLy|5_CtBp`@C{~eA{droD9EXL!pY8ST z=p`VVLyqtm8%l0YShH_F@yf*i^8#mLPz)gR{N@Mf$Cva`S0!qx?Zckijx z!_>A0+}2`mdjM}o#W;IHT=O;GcA^`4*?aEa^t|Jyq-rfDuv+eHjtcp-u)~%r@F%$* zxgZO@$k|&Wr^pY|7v_A{xPC_>OX($U52uhXK^d4BffkcQlQWaXqz#!S=NP%>q?>}W zM7E!0#9ngNOrF0CCOx%n`C7-|F7XGhdfz?up4u?%;A}YiDz$}m7ls+mjej;@sviCvo%`4ONV}(!BevL)743h z<(}F;VC+1&zYX}3k?GK-YJ4r+_#j6!bgPto8CA$REl}7M89dA_e0#J2!NRcOa#Rhb zetf639ou?;!ecoD{uVq=!`i+0haSeRWVyi5UZ-$R4@rf`l7j?2M9T@dCkUz1j9#kU38y*gk6P zL#UkLL&ZA3hg;Es{zQijVcA^Cvo)}=Cq)_}k3Yv_qcRdhpkwjKS2M;;b3;76C(y#^ zxZ@Da^Mkh3euuQ0GAGXJ5R)l*Yv6xnk#)Y}sxcGCW=>Ze>E?HWe=BztZ?thfr7}Lv z%;a0ahvGa~+{M)5YThI7$XYztryE9iug6qHsM-l|vfZpp$Cf)s-wJU~V~C%mW!l5% z(vB4+lK|~9vT<9`r)P$ukqA}&EB$K zptv_xsF7h&u}CJzzl!N7sNM0v6~>F_OsK6;w6P$xwI1m&K=vs`!^bFFiNU0_5hT6Z z)*IX*%TB^fD^G(i;EU7|=%64k$@a3d>h$FJj;(DHK5s=M!@KNhCl;9P zHb3M|u>xdOVPMOJdGOi{ju>Yh$&T)~E2I%!28NF&sheB*$xH@0^oOt*?3M?uivz70w5*96_|o2c<69(Tf(t)wf!`w7G-u58RwcKXDOBJHA*UYx17fPx%Za zTkbh}5x=a0%^GuT%_j_$DbX5Z-e)b+7;;cB)bMAWf-;l>&W?BP7Jv_itwD;=0s9y2 zDN37rk=|0^>_>x)z?Fr))G%2b=H#Eaw&8PCr{(NrNoDc1BRUj32A+lLsh?FcD;cjd zIHHR+8khO!GS&Ey`$#PF z)u0AGh{1<=zUvH5Spm)Z8Mh+RbuZ%4(avj`J$($~X7l#gj|k}m#-(}$m=r(g4UCGk zx&1RG#)|mmjR(tVHdvA>4!URIc4w{zkdhD<0qV1v*;ib1DugjQ0Y}#6t-nro%wEM& zTMvqp3zps7o4~Nf@cKnwmKfr%Jh9Iqo z;tEOzjlL}J4vz}sti{D6B53Qna!;WsDV)3Nl|cext)eSeUkTs4bS3+KuwLDTjFru0 zD~{I(2SB*Eb$Gw1F4PzTt`$o?0IpsXurYqgV(XTxUJ4{ragsZB)!c3nKIYdoEk`~{1T&nWaz$*A0`#_3pWb!b;!@xN)?qd zRQ|54K1r7=F$O8A`P>ir0~MS0o~5$=p?m3pg-GnMGp@B{H0kao`5k%BiSLWyg*>Cn zL|W{s+)`rE&%Gxk^WLUF0~&nQ`4xGahg(%%`Q5_m-^7&siAdc2guHOSN@+jL2dWN{ zsruxY(ig8Dxkc*k8io})J1+Y{Qy``OrGy5!iT*M32uI0JU?dr$K`%diVvjh2p z#)_T}n$QU$(aKH-P<-O9n+YzMsbS#qaP=yL<}UzJ{~$XcWb<>NNskV*7W283oF^{& zOwZk$+^nc_*Yr9jfGAg0)x4jM{W?1`f$K!;$&T)uK*@@zJi6nYO*}T3=CKiT8 zzYM-uRM+Pf$nfSV6#tV$qu4hG_4Kav>W?Nvs^+iM90z$5VCUYV{+NBR_z-Z39;R?} z_VV10s$%{Dv7&5T;HPt;xJHeJid5;)fxp^pU?X&oVGx!!420Vy8Jq`D%bno`MLegT z&SJ#q_F;yUF%1AW`M;mwTWz{zvGZJuwg5gdN0*mUHEaBs>w1<%xpA%wb=r^(dZ$?y z^)9)ZZGW{QmA5Y+zg^u>I9BD1%vMml^w6`JlBd@7l1l%-&H@T!PCm~+B}Y68zmb0y z>tmG(e(I(%Wf7X#)wwgw1luxXfpJYlte1snWF#=67d{wQ&=d2OX7ko zaAv$l7qtS5F?A)A)ql_utW><6_8|3pHaT-ay1uY2m_z2TXiP?xej94`&NpVVoZry( z50&1eiW0_KS~Vas2XC;IjkSQ zl{dp}dwKoNwPeo)wMkWz=h_MdrihkjziLOTRFxtT@=>p_{Zxr&f8<=R5qu zx6JKjx=ZR{K`G!an_v`wtRM>sikOWarGyhPUQ<(VY*Le1P-n10w*AWHmedU$n@aDd z;yiwoT~VKu@wSw1J1GMjEwK+0&2Y`KAGec$(cP#)b`S zPnNLex#6JgbLxB_KsSa1W#lF8B`(8IQgV#iO{o{owU1+7`IAucZi)2{&+y6CTxqg( zSk|L!bH~_Lgt<<5R0zb8%5-SjVVcu{5_lCFSL@3y9zsoo!-><<>U#R@6EH|42xh@& zPS>}@=9koMl`E}!70`J1prrV2P4>H3`^!om zCahH4#qi7lAqRd*I=(01)P<>n6p{!P+2*;%8O=6OMaVoc5|ZIqic3&pCl$eyCz-QS zLb>xD42cjPbjO&it2X>eHaV|SMO}qL4ibvKU`tbr@9|3Y`WD)Jn=a0zUh(boAyc+X z*2j^1+rH2(5uWF$@lC6=Ooj)9Onxs`ds+#`TzS8TpBZ|u?4=6xs!cv_B#t8rH&L80 z3j&*YUp>dxLo4O`JAJ=$Mex?hK#q>(uANE+k3r=}uK7yGP|S)|ylnL~Ohnd{HVs)6 zelbx-+=c0zCB(V-u_;QGy?a-Y{3~iC*@!+8RoHAB@BF+b2$hgyd;mp2y1y+7*wQx{ zV^Sa4XgyKaV99p@TmX;?@nHUbMX0s|wpD^$KX0)B#Y?OhRoZkIMzu>k5cD_%UGk0I zr(`4GTb62!p7k@)gi?mHsz`wb$C|#CeZr4#uXsxSJja?;5~I9%#NDF4FRT6FykP$- z!dBP`&~#7zBigWOo&Cg4Wq{}mBk^7x@~#r2@8fbbh$7w`TPNIqa0yCBg1zai{Ld?2 zsd;BI_hZZ@pKY1k6E5Bwb$AEnA3#LD6+w{7jG*`PJr^@ot{*7D z9jb66iY|nrTfx{M&OmYbKdq&~9M*26Pf1CZ0r)RR-UJ_D|Lkma1PNGo<$hc~Hp#fj z)zp|t8K`ey4W$wPHr#{s=&5`A;J@Z+uEy-$6{Z->N*JLpXTa9%{#~7x_w0wmmkfe4}M33{}{*IGJJ$WUkFpD*jAKb2a>;?MU-XG+S~`d=?gB- zjo8yomlCQpIG#Q4-LPe>JLx`ro5M37X~27Mf&ho5g~fM5R~n@^#-qeaCobPj*V{`} zCc&{P=OjLF*sfPD(J#n0)w;PR%F0a?ru8JKe-xR#%Q3H4#qUTtOf%Uz(;ZC@byG`} zv~E|?x(qFEP=98KfwF&q(q-8CTOx3O<%%{TeEpU4bqp((2VhX0ti}HWqd*e6mF1z{ z!E?3<(<@!g@u1C*xGEHZcD=VZG8aC^@ONoxy}J4ovDT38eV)lP?`CEqg8vtw^ncfG ztfl*g*$~)SmEyO?(PnSySuxy zaCdiihXM+BrwR(_dcVE**qNC8}#6%8;&}FM#mil3v z2=hqt$LMVIB5GyP)JUoLO>DwDKI)gCyV0wbJpi#KUe*+Ybwu)i1bh0 z@-?SoOsUd#-Oe?_)J$#?enKtF@(Zc60B3zjwA@{y>X#gaHg>DU5^1!w4MZ>a zb!Av7yuZ_eRYM!Y8pniwtn^aGA>7wMVJW5U3G_9LIH)A6HhTYC=UVy+{7)TcAHx9;40zRAy>iY&fi8U1#r7@?rg^ zfk$Ut0%|poSt(Xs!{3p|{lktD5{ItXu9NIX*I6yyZ2yGA>l1#&v=SihwaA_F54*%s_usLTCysGjSR8{K6B8m`k))$nEENY?rb!drxJtK`-<1BqNNr9dwe9Z(Ck)S ztG@RMU{N5VaN6P0E&p~a{hc@-L9jU|cn#FtMO{1=%Nac zb;bgXv)Sc$`3dOwPA~bVJbHBJ`ZV;hcs7bMDf(;LP4kt-F+8de71o85oO$k7>r_p9 zSzi^8Sc58ytvzGtxf-N6+O~}*c2gm_U$P6MExX4h*8N$zx3U7UJMwBeJIi85jfLXS zFz;PLJS~MQ44II!`#(}w9D~d>2=2)AlSN{wNvSY4y&r~uS=4KV$!X$V$S}q79d4@2 zZqD%RjQ%qI!J}fU%vynx-qcW6mZ`%YLjyG3W39NR~H}_$DRGTl&wUgPYj|m zv?{^16RcMKCG_MRo5iIiJy>E}-7Sd{YZEa1n&-CxpSbw5vm?@^R_#}y1i;T?pLJHG zC1&Y@G>D>CZ*)sU{GoFBc2*(WvMu&|sa5@941=lR>(354d$lWxqMAe@0KD5hpzinK zkw?wrUh~Kp*48hfCc!Wyc^XHh7UASTXk5O%g*UTHn{Vps8J&-qqD!aY7v@*qc!VO zD_6O;w?{3iO0_4Pz__Zgw}kJR$c*uW!%!3saY&?(SRteAI0=}2v-ZpHvp)XuV8xhX zqGd*tkL02A_UCg3k+~hxECk3|RS4BIU$Lv!s#O(oKnn?F*Kcx=$bvLl)P|{~^Q7$V zEFH*Nj`;Q)+nSD@+WH};OS-M^+tglNL0<&s1p4|Vx7k(t)Kse?^uDsv>s;60wPDfz z>kAVOG`zq^;_u!{b_W;ymPF>dR?DiD7y_5b{`4)Pmd~Y9W0J6#BhKN49GFq;DOj?H z##i+%q<4!oiV~lI^e@t(^5o`C=DFF_wt86KPukZTag@~Wt9eN16v%o(Yxc6$JC1xP z#fMyzE|Ria_?4g;~Z9%C3vJTVQ^HHT_~Pra{N--4teV+W8!F^5sR}0 zcDe|Qv8d4P${St-PCsb6siFbu>o=D&Z871H?KIzjC03)voENgZ2Y&K@`zXP@Ge{C1 zLc3H{3{L<#+OrI^VzwA_?Sw`3x87>Y;jlWhGkD*K{8Xj3TGbf6a}*-{8YwTa2qrD9 zoGbtN)-r$giCfEjSkuS1CaxXfh@~gG9>HJrL|mzx0`|hG9Gpv|6D))U3uQcX6VnKa z{uD{v+y!(2X7g9-(;FJvIA=T6t22UXFW)r9;w!exxucc!kfkX4xorlTx9~DQ)X=Ro zxmwe+FA z(eqUW{#Y=x;Ykg;PbJ~#y7wM%CNV!56=L?_yxokg09$xiVdf^%`=dwGrTM-m79SX} z^KxRp(Nz}K+g&3asLvG3Zf#(yTC6j`y5TE&(+)#<7oswTH6Z#{X~uZl<|;63BzM*D85J;=rp zK7zA>+Wx z9}8#ug2dK|sKMIZ+F0%BeGYi%y}!Ufnpr6!@^+;AC5Cs{LG+B@8(3IRMm-YkD~$3* z`*MgKSk7 z(&)NqztABm6V18cW7J_K9LhCGl`0%+-aUmEY8<^PBmQb@ORiz0UE26njhnb^3(3AO zD%zg^Z1EA>;CVcHnZCrfeMj=Z{W06Y^>eTY-~R#Ok@0wgZmp`bY3$F?Cm;YVnFcMA zEPq%`9&O?ZG^3f$=YO{}x->yUgpY0g@<;R&a6AY5b9|ZW^z8RLUAhBHU#0PBls{ZW z`(@Q!v2(o$iP{<5Cx9%Pb`w|d;d7=!CApUIc@ImMQ>{d>ulIzbU+II-z_@Q(hO>0buvA*`vH;yI& zF#`3SY@+At0`3&lp!8haDU?XLm|{WT{4S~^L(dV=+t*www9?=A37Fk13;F~seFDtJ z@eX4;-uxuAVabwc|GxVInOTL#VN9+|UCNOHUAVgfoj z1JaOY6k5p);O{}HvTU4&3{Wrv^ZCgZWoIDp;O&Pw_CFf=f8qhLa*e~Wx~pjAY6=X& zCDqjrwoiaS&d+LT0p*oCb&GmD5AN;x*SNrD0a%o0blh0>%SGl-z*kSViIl4DWG%P| zEV`coM__$aLL=hZfb{f8NB)aIg_(&Wif0@(Xs8HwLh0SU##qBoibs!}b=mL;TP8^Z$y2Rp^~+B4D3v@w87QlpG;a zf7nuQ&f47JIkswr;SYZn`22hObjGPcr7-%UkCHLVcDpn@yp+V07qb5x*>1P1z9*jN zAFsJS4)5|kZ6D?OTb}@xI=fFmymYRFG|Inz|F1g#zt=Gk z-|?t9cMVooG}Qhg-)M8Q^k;byCAs?Ide#O%0)m%|V*wpOKXsUiT}W zsocXg!pynb)@9qol)K6OykNIo=H-BPz zPUX*}!1x4&==z`gpH^Dz^fBP8UTRZ@WR5+eC*EwD^L|jjvkY;k_OdXE7JV_-{L`Cj z_d#Ba>egl+`4L)kO_>X+?$NXOj%+qGq2fh`2h2$iZPk(Cm@#xU!&UURIAS}X^K;e!N|Y#*WB;* zPrxq}wfmU|@IJP0J8wiNC^Shlq<>YgCu#iOiPEu2ER-y45Jbnu5`QiR^_Md>= z!yhLGpMdL6z=DKsejnn;Y-KnaN>*IPC!qJzzw{>#A3485@6)&Y(BQ@P=j?nlDN&N@ z4W(?SW;G}F*fRo0 zD!H0ZfZy_+-uTL^!BY11xQv*@AU7_4k&C%e6AR=neQU0&SuS35JRAqoE@hW+png-H zVZaqNkFL=|VWtGFLi0rmhGp2-@BP+_#dahx-QxiFc%(H@w->BL^ZG*qV+=oovZ|$8 z%8*ItETF`+h{{-C*2yKe4y3}<_kNW&IBMhA%Qor^(JTBVKdXQ1z0%(zvuf=7Di0Yb zk=S10as41Td1R&NjqPTtpGX6SoO^@I?J3)4xw>DLZg#W}@$m%0e((5)M16^ki@lx- z0@bZuzkC9xG{-G_&G|`?j)n3^F2^=m*GOx{w<=qujBE=8A!_2cTAi|H4?iKJZiHyN zI72Hrh{tKjfvj!~2deTG??75qMGCDB$(GSp_UTz7(Rq5j{jEE3dwHY5LVt#_4Jh2N zbC)VEa(&51xY(Y@*;d^FgNTK)p8!dx{D-Dbz%6k-SfKXOGXDk`IFZc)*XoOrGIHQ2 z0Fxm_6fpPa+c6P9nx_%0@-z*g8EXD_}EC z)+=IQY;5a}6kQyRde_X1Sdz`?!)I)am~u&0GIjfnrpTX-lp2j8!yw)%b$~q`T5MBe z*GP$(R)95$>@Kf^>-Xv>;H~e`*Co^1&_aJklxzfH0J!<%%nxa?QiFn4jNJMx-2D@9 zRSh4BP=UaawdhE+X+2*SxhOSZI-_7+gHbrf1-M!@!lg5^Ym<6ISW`6vH-<`hP1|!{ zb2!erd-(k)#>IMNjUUNXAAvqW(chv9r9=3_pLSM-iA2Gw=hB6`qrN1{bHAMa5Aa99 zE~AlSi+`>8Gh+lKg73Qhv826eZsgxZGcUtx{xmMCQ>&giz($P z@2*j*mL`eDVn>7UB7>?kCRWRR+59rs0(VQGetquL@_mKb63ql?OUXS`kEtNGS4nGb zq6zZcM$2i^^q{weEmJUY} z(MFG#h^S3(66^9X@Za6Cm+Y@v(6Lt4BagMHE3tGZ;Ly&)X^VL=uPqOhG1Zbx=|19# zdxma2zx~;6Dn`5-Gy3&=bD5=ujoMisg5l&@c;lGhvS@Z#{mS#|9;0x${eY%3G6Ukn z@cRfOL{?L=9JVVndPIZJQ+q2b-H#sisg1H|#jNG?g`Gq1@}?v;yOp*Jx1NIHH)#xC zaJof}%=?Hk&DT~v3l0t4y@YSv?lt=K?I;{l$!8gbZ4V|%U5RCmP$cQ0Tb)?s^x-(O z7#wn=dnZ!3hig}KD>5{#56-%C>iB5{pR}S{*3k) zPu?~^0k7l!fAW9imA@sv zlUIBsz6%_^lfRBV#@|Rh#r2&myZ!55OCKul)Nc*%>mQeHB&$51fc(d};(whV{&OCG z#_vDrU*gIy@fd$bB=J)G_6fNE+t&EGK*=Y-(ZAa5pZHr_!N4J*z#$=^U?9OE zz`)S}5a^^VLQ0SrWJZ{*PKgWTfv9Z4g+LWUWygS^Bq(M<#o(evEMt>F3gc#KSqTn;DAIvl^^hps2Jc1QtX%qxVLbBxlHFQcFgsra*gq@*kiSRqfR_ z{H1vOFOo4$@i&~3yAPuHb$;d`*h92Go~ruQqv648KX^oFoxEA|?>zZ@cM(m}I_f?F z(FV`)sc2W>zjz7))=+L+%@nbyP(E>A9GI-Vq?Tk>%!eg4GTZvYj+in1YzU_^kys zGRk#LtA=5w8NV7aAN}U|$%p{5Bn6Pw(0u}EhZ3Dob3@Ix)$zNZ(S?kbyYTP6VHp6w zFoSnexvw15YBi&_b)c{q+^f(~Qr$u$c>#2(4k#Mi==C|rJJIaEkplKGbOv&CJ&7hY zLhBjNYFAQ_KLN9Z;9>@iX;ILL73L)^IG2X_gxiO?_BNBa0udV6t-ZfNYmB#*{Yc}} zE!1J~&+x@ynICmChJ5o#*Q$$J(Ptq;>4%?y;?5$X(!wnr4#z>@3x`>x77@*Z@r%Ox ztr)RdU=aQKrnVp|mY89m)h7VyaGDGNaBTSy%bWNhdDHp*2^eqtJ9}(ChCckqS3UvU zAKw-q{%T>(de0Y!3Ib6V;lJuNA|Z*G$%75x{-R~57ft>rAS(XWhm{oOm;YJ*{)V91 z-$#H7n|Nsrd5joLs3@k@If&OxYe&|HhS)D-qqt%`sQs6VZ_*WjTLY5(!m6&KJ><&x zQW#2V3|iCEetM7&V2CgJQH-M3HPvuVp_MWH885SYtqFvhMhD3JyA;< z&?@>lLHO%nJrF+tR*Q;%`0os0+!BFgAFSB^+u>lVOt1I_TAQLE+YDohPMb>8S3LZ{ zwusE*=29%rgQSsCn@0H8aVk1f4+?UotV&Od?PS6=~DPvw{+;Jl~|5g0IivQnMOm~iH)>(k~ zvQSafJhrUZ9No9B*21u+>swLW!Oq8am62rgKQ#~=rqp{F3*Q^KK+sac4uroQN53b& z!FN(;Xrf%^W9InJwmnNYYqW_rBC0=#=Pn^zJRLCY--p&hMV{S`@c;c9Fr#0#8X@#q z*M|FcEOzFoh5C=ZudLuOa$PR-eK{&>uE-bFLwQO1NzvJp z=^fR8Q{(T9t0-0<`^2hUeTH$rd-6vwcF>u88|niw;8&VqcSKs|2gwIDAxVIHzdH?qX^+Rb{H#@U(7!E2b3@wEdu!R#80;(6;!!6ZuIvB9rm^Xni@A6 zwIR(HgwK=7)(VE+fqJR#HqA-?yAO`%UMiPjBqy5h9Y)tQmC&`;M?XyErRwgN?!5%Y zYA(p7r?x8=J`jfwdcs?XeZ_wCA7=4uW>Wvk^A*%*`r@*!gepcuv1sdQBauqo zU@1^7J?rDJAq}xgI!uN+zC<;v)*m?|f%SA09@yc1|G`xMH^SOc_FYJLq=f zaKG2d4i8lxh8B>1P+=inVxzVhuj9l5b3}~$dM*p5@O`rY>3%2;-l7DbD{1%S2yv6a zTXZ>D&X?_u!J`SCq9RHrVh!4#qJT2wE$ZQF5@cd*Uu z6~sSK-W_cVy>~3nd)CR7w46K$;*2DZ$gHG-q6cxxQmR%^1WAaf+m|jR9HN(m0v%l2 zuIf3*7)n@>-jsKQgEr2!jbop}_ydumQ3^`#IO9%~3nQVE4%$gNlLUf)MOz_yLSjQI z?~40ohXU|S9je@I*egDDyg6ZBActDw$V!i zF@(*YSy|mf4W={|=v&8wRBZ;}3&@ z>(cK091Svx`8&?i4bgf9javS)Cf%U4Vf3aHj8$6uLLZ~{86k%J_Hy@clBUYqrZOs> z(6q~;nJjB9UX>a0G0M&eH)yi^SgLLe!(eYq2CeXHaEe>2omoB=6*XIAf}q-psGUY* zD4abuG%zua`G|l(sLV|;w%zE0WDErAm6~(6P7he>b4g4-xgw=_K)|xETtW4TXC))z z?w}n1p)wP)S|G6Ub;N?7KoA~hWiXhvR3q#f`K*pCp=U2ajs(M!u9BIRvQVL6hqs8V zZ8?E9GFyR<0oUsN>`UD&3}p4)vZ9+Mc`6WHE!?EjXCIXmrkmz{M!z#H%pw#j0y4`( z)!l3hN*&lhWE{JUtcc5i?i}w~_b%b+o~@ci&nT-7apc*!S+J z+hMQPAm2t-41N!d1qBSixvXVgzZYd< z389d%vmDfDt1CrAZ`T*@*QB=ughl#cUvttjQW^e?CdGTtvOa?Xjms~jKhW|qHn#2< zHu>&#*dNy`xi19PvjVStqLeG0%o-7+P`+PjT+yRNAFUpTYKh52Jcl5`Y$hluiPCR( zoWTza*g4g-=mA>{>eF(uL|L<=%*L#=??mNF%;@RQ4dbq zs<6|+Az^c>UX9C6xeHdy8AVsRu8X9S3v3SZALM`MEHp|neAgE$5BSmBs7g~| z3JhcQJKYrPcaMpAAE7LnK6)TW?46;iP0-FBkk*GS2ypPqNKjCdVpsDmH3!oQmQ93+ zD2W;DOpu<_c1)XS^T<)f{&Ax*PmVo543LE-It_(h;uM;4XtFtN=Hs!gH~O%UJxc|X zUy5#f30CJLy%Qdy(Hq>BRcMmrFdxLe1KuLwqy|q$D%%YIlCid9c3xzn$41yeTq;Uw zN`xA>Ep6o}tqYTaq^mU$AkMnS$RPi6tju-F0)dzF2)lus)ndlPT1t@W&euYqtg<72nB=K9^(g6@)3pvyeV_9*Wnm_ zLFlJ&*0*b87UE#TLb&KHGzzb$f8Xrpp-)FPOAR!Fmf$eL8bF1HOd^l7J|Go_5nft5 z(-0=xid>D%B5Y|zT~M!~9IKD=VS}|5#6ybuhKCF$>Yu4Ej0jKsd;T`a1tthx)6c2d zK_IIf)02fRb1_pv7;#Hbzxi@hv$hs_yz;b`xLKd9og~2;q^lWLL#*rO|n@CAqw3!idT6UgH zPDUxH<5!%l3>!P9XXnYnxyo!?Nh4VPmJ%1!PK+!#LJlI8-yaxW)Q4gZ2}@1*;hn*y z0b($L@2KOOa(i2o{O-bY< z{-f9pUtysiU0imf?>#94I{`f$TwNtNf0*P7xDmeJhQ^I){B6>rbLXC8?rl|}CtUDW z|Me5#ty3|LAkGX%F&#&vD?E+@SwS&&i=(*RpMex|1o@`qI!+s5Yi`{Al#+&)gcJ#* zr5XSrphzx&anYJ2BPL{RCb7%XITwz-R~v}f)m3T_+|WoZST7NUPBH}!DUI^dDD5R~9>PCpT$Qz?UN9YW9wCM}AA3*K zoX^Q<_+*lB{J1)e0VI8BHzQiT_3^c83Vqb74%F#^`%O6}yVgRGL_x5BX92CAR%x%I zDjwt`bkghhY<93RXLn1_E@dfRj6`_L9mIatNK0xmR=#0H=Pr0js=rV(+1*&k%}mr^ zd~hWswCWs%qcRQ0l7^9B;2FiFIi;iQA%0H3jW8if!S=m>p_jF_=&RzEvHSou%v&C3 z(S~uUlsT1KI-0)VIqo^`lVt%E=zLauMFf=`>Ovz?E6`+pdmfX40)46B;-y5a^_-|_ z0`|L59o~M@t1eh1-Rj2U*LCX?tOCewHRTHRYM^r&3!Q4?Isf@2s98tXs3bN~KH`GL zVx&@@a3ko^m(?=j#{++5H)AVju6z!yCo9b_2kv{Nhg6xsPTxhkkwWXIGW>z7 zlTm)w3Jsq@Y>_F;Nm$l-x-;QnqS-9}=dlhDXX=N2nWmEv&Qv!hYK**c?*S4SZHqe? zyo_ue?*fP}<6EW>qC%f_3-rM|wRnxEY4hyNkeATi>!muN$w4vrL0@&-+#OLKnE0qw zXoWOp)09a5Rgqd)j*hVv!NrRlhI!IvL{nyRu|Jh_XPh3QSi@m#vC!xeF(~vU_(e!v zk$01xu1YnLoXABo>V<4!!B7)vLDa8|N`4qc4iFXbnqkFC;SN&NgK*O-wnEmLy_?g0 z0YwOBpYBHR5z7XO2hqS@5aLnx4(844ScfpwvoVeti4bplYo%YxY1fDP?qE3P4ES3W z?j9ofv8il^l-cxXh#SWXVVhg_1aJPXIhvuNwOBhqbS(+!n8K_dwcO{h;`Jaf2dOtnzpkd~yauZ9{%wPuo6 zCk^R^dckG!7e=i5DD$8HMjsJ%Ek!umVU?sjmf$nC9{L`-4oUla*ynUfN5%4MB=-@e z+bCHsW=()5TPn9qCVoP*FDoJQ&13PoqS!MBw+LOq zrSE0ukeXTCV7i3OsK5kxGC)8d>&qias^gy`OOeym=i=-WK=ACq(qaFiIEhFX zUw$Kd8g}RoIo}>`^8g)uMqXhV)#9gx54J1C*038rtGSPVHbZZ6%u6J7$cWG;SKjIj z89NS4J*qO*iTz5Ef5txNx!wK?Tc0>(N8OME%ATdv&CM){0*eA)0X6DSl4#=a7ILxO z99o`EG>HSo^~;u9=Aq?fB#k&;XXbt^!NvgQjn7X3GV&Y6f=Q4^Q$O6wZLzMJwAH z9tp5sV$qxvAJM1S>(f6jK2z)@F?_y?+Ebojc%?`keKva+fwFSjNGVP1c=(@Hd z1fwxX=&5a0+S~ircc3v|(j@aLJHh=`vDQ022KqA`Oe99vcjlhIf47+v7I_9Y>$A3i zttbYup#okPI)i?sFFfQpz5Jt(u>LlhfR{S!v{@b2`%!YE`l1BpG#SbJ!X{-YKDKg z)t>nfXpBHo%3Fnsb4viVWji=`3AbuCfG&{J_3J5Xqn ze;?=G#4x{yC-2jhWU zxT72VS-CG8%7=^a4=jw?1)<~h8QO4+)38ul(B#T0{q3i`aPdVa!rZdzNjl?%djNO@)^2T zSIhzfy-Hr6d93BB`tHtst}IFH@+8jED|+$fXT6 zaU`OgOB3~CUi&U}z=NETT7W*5A~ykF(()<9hD=r`kjugQm+A?4raxzQNV9DngR1#J zB+G1trVg=-0wq>du)2~a^Vb)n&8$IyigU?{#E_0qaJ6t%Hxqk5&R91DN%O~}FOOxl z?d$8l_#k5m=XC1QWt&DMs1VI=_(F6>OK?{@9&MdU9<#p7J#>>PkQqG)ELy^6mAX(lWqOY?tgjQ zbfRYu;v!6gs0Z)NJ_MNh0oH`&v;m#0nd*Q<-b2ZP=o3>C<`F6~H$27W&<8*eyeTBA z+j*&|xg(^A44`i}Z<84iK_&tZ(CN$z$fRJ|4FvTQq-8K0Md9$=oP&;pd_CuX-l#Ja zd+moXbQl`C*O5YdeFDxEZ^zk?`X1@Mk2(t=Ldprj)FDvY`6F_xNRZ&ht;yB|0I}B!4ukCM2|M8o7)`7)w+@b*t6rerO<$xN44}Na~3LCxUHe2H?S$UHj=FSsjWJOtjT#8#YMr9 z4#hTS3HN@T^QZ}dqyglCRiugz3myApaiCRj`IbNyI?Pn%X`Gw@(}DSdy&2>REi0Hj zuXa&sZSUWRfQtJD-#ujsbHFdE+}IS(2VRgw#lapIH$GFEsw|n+`Ucc_tu@qgW5IY5MkjAu+s(Ct%ka>nqlQ*k#|1{6{0-c8f+9 zoP@z^jbkEj#cJ_T_dJA_d3Us0Po_fh5L;QW`1?B8(-hdqC63)UsnL?HRw0 zlsky^!_1em9Er+3=8$z>7PCG`o>e%=8{GPyu4s1K9FFm-P-8){s4yW2cjBek>aUbw zWqZL)OJ+--j$wUc&*Dscgd{qqJBKp#vHb5hTJXLM{#f7iboT3F+bK3U1>vvf(Q2{| zL@;Ieq()f&^F?xAkhoBwA=vzl`X#6W0EIv~LW0=yi}^SgoTj-ZaX2h(sxVMwP+Ltk z0((UyfPOHdHhO!)Ma?AHo|urIKK9oGfF7chqZUIu3j3%&`mrU?}!>onzzr zfcNcGGSGjq+=`OHe&~*cBsIP-Al|fgRaMupEg2%3k7#oRhvNw1^9<8#S2hX>EY|2I z0!4dTBu*d8sf4rcw8x%L7WN~fPGxC^Re1mh$q`fZmksSnKO}T+8*`ca@!( zGh~j^sf~;J<}^W=N~qj!jxG)clPZbd4d603^(`GlU zB&+dEO$D<1mZaeqoNpOqNm4MlWkktDI}yrSr6gE6RKjyjXh4&DIsGToavX^IGr69r zc|n0D=cdG#F^p~hisgK+lbx$-Dw^dpMxfrGDIpOH7#?R=J9;@4m?n6c$GFdb8CqaW zsvf#Xf``YuXUw)1+`{KXl@5{;7MuKrze%=5#;jO}0A47z4rA&Jb|o=H8SE|_DQZl; zTca(mp3_x-BZR$W1Yks#*$ z-BSlX%!I9pcCxT~8VeO#$|UtC>{jS=;rVx#>gGMT3(2`Ljdciu-m|(-)lth0zxxjs zNl~RoegYuQ?CM;k+!(`NRO#HFtYakk)xt$Se)Zgb&?S4K?aXk5_C`9?mCU{$)L3sN zUWTY0rhqFz^mSy>lu%<}jK)%?NYL@tquUJr;w`!=UL=`BdZ8)|-p06=7idnNm=$1x<=hnO)u=eFe2qKxomMC#+19Z^^67Ylx7AfrDo;^dSMgtRd*8{o&%5wF z1V?xhny7f5>p1x4zQo+-G`vI+GtFMzEc2_jsYMd+p-bGzUN<}YwNtCUx4%@g;;7cPAzo$5C zK?<1TNq;jc<5mrYss%57+=F_M-Z`Om#RR8V9EXxf$ zF0&agB&sCv1=9h<&DRjz`W}k#JiYm&Ld3piSDc=0{Zc}o_Wy*d%Q>lS*RGQ#;YB<{ zrzcrCY`MPRh0%Xok~(<6K{s;QfZyC5Tde+}DX~UZ{v)}HEBQY=0*{bUN_`M^;i{b4m4hUh)hJQ2XW3zDZfNJ^>b5HL>RtP-TahHSvOFGNg+`@KJ#2 zH^W%&{#Lc9)h#1S*el&+wBsx7N(dpojGa713;?y}2I^f&2g{W^!cKrDX|QmG^e(et zRk1HMV;LEvOYMYsn47aCY z-l=Kw!Hv?#@`U&VP>Lb;`+$qSpZ$SK_64Gdrym#A?0%XnbLC_8{xkP3-5R7DN(bSf z8wUo8Y=t9RmG+@q;>0Uxnf z&rkSV-W)y-zmd>er~md-UziYl;uFAH2ULayxKee89K%liP7V6av~GyoJZ;i#+^p8k zVjK1Dg{g>k6ziSMNV5J3K)Kg5$9ZUP4`O_fC?!{RkNBJ;mMFKsh|3)C&6^VqR)ZBD za0nX!zH(cIi%l&J7vDn}0=z5yqG4M_PYWeI?gCAFS!Ri09R+c-Q=%4teU2&>RQQZ6 z@e2AcBZhjS@&SJ>EZB2-MWTWolfJ3axm;h7f}$K{N;>jDRQ;SN+U)s0RI7Pt2q`YA z|JHNyezK_eCm{1meL?x06;DIcFUVCi>c325g}y~b>8uZYT)2yLPVv&_?Fs|+6HRWlpLsU<$$d#&f1ZMTPaI2me%{8t) zZyYCdgZZq&>YVTi;Mex5zh!}TCTSmDOUU^dXE0qyDH@$==jVr@76$gEWHK!rmvfxQ zx@&(yd)OIj#mej$GpfnTAPF(kXtPdJ)JM7qmZ^q`SMn2}q2n0yJtUaK`d7|!!Sk?x z|0f_1GYBk<23cwSPT$%q&j4?q+vEn5PcL`tM}>E#11WqH`qm0GB77n+2o4heSAQG` zoEd&mH+h&tt213%z#RS`agU?4VbZQFS>~qV=mRc9m3F|nb_^K|FQFAt+DW`saCjVx z|GLyk>&R6gNma8WyeVMNXWwFcpeu}%VMb}GFP!m(qWb{w!d@H7Vvox=fVq~S1M6vf zg3GpByDO?24`4%u`?fp?%O3$s$KGVPV;F|-JdFssJ2!?Rg;rRLcsa;=uSe@*VTAXQ z<6jn@?XC&~HoU5B4X6eb#2GSviBmT&7nv3wL)SSIKQ~OGAT0X|T-p&8n~K*W^;mK{ zF3-?$sXU2*FpoWwaW5m&+oX86+E7#Wn*gV~IuzCVxhnY=djI!p1(@k*J2F=yK*}_~ z5g2hZ>PSEm6}zQ@Iy}CabRE(*hi+#y%M+P6DQ zU%3icPqq}r(q1O|1Mwk<2<^r*Cp?V|H%d$9c3dQ`@t=l|<4V1u{U_aXZR~4_(siB` z7zP;D@ECv~d?cODxPkG6YRsha3k5%DWKX0XS!=HnH?~GD$bo0xlHP=1A8IC`@3wMh z=htgIDd^}53Ys(^1_e9-UO=J0*Huu|F;=FGbny1V9rj~+Tvxf9nA){g1bCgEj365v z0)t?_wY^Z0J@kxt0PJ9(S9dNhdD4(Kh)enA99i)|7GLMWO-neAWxl*SN;COdR8e5j z(>!g7c=-$I3^zl0 zzLcdjd~EOVB84* zG1tF<=ba;j5Vy;#>Zvwdf{~L!Pdn1=2p)k24N%j(fJcu?HD#w+8eHi+cH#x20b><& zaz>ZoV!V^FvKlmwVIdQ1Zr_?kwT#T1C z*96g`YX23oLYzSqBE(kJ6|G~+9p(!1#(9f!39&g}K+96+PM`{`kjq#M86(oEiG|tx zAqr-vV!S+9e0E<}@ZZy-x}o4U<3f3+`42QuZJgKGY0$D?q3enobVqrhkGMj_VXsGm zL%$$lKBFe7&YJ$twHf~l0_~(O>jFyZ0OT=LTpk$wtIl4vTGod|(rASh!50oAh(?|Z zYsmzm8%=uYjlGVkG>|o39F-xsm@li-*6)-w#uG1mSmBW)LH_2I+ll)GNQq2+VVu_U zCA1tgF|?nP7Z32*sGU4is2cp0#0Np76vmiocsAXNjiFb%N-V5@Lg}C@tjMs`Nek2&6~NW{`N6SdKwk?0NXRoQ|yi)uuWid>>eByCf%*TwT~` z%OC||i>$`NOw-uQsC(2TZI6eymAb>-z^MFuWp3Q8cKD9^GDWnfw>?^N4t3Oj7JDn? zy1G*o$z`E(~%P!qLln0aPDF(z#d3{_a^MXt! zbgR$sN*!XT*t}{o>45XFvpem8hN-Yv2bTS$J`#nbt1|>G*8UZ3_%JfyAB>t@;?eO{1xF#Ca6x*c5fWLn40jx=`KH$i*O& z8!00^NpO~=gS^42cQ|C9Pr$I_ox6v__rmkKZ|iDWkyZ|J)KSd?<1a}1)=-R>!$058 zi10!Ocz{HX9@6KRBt;1&2@29O2bVzvi{PhtB-%}0Of>Z4=E}nPCQ=8C%^69MoZ|#6 zW7;xjXW;H0!$^5hBxtsv+Pz97RLNsQ#m>-42&kZc*dtkVfpgEG>{?FPF3FX}*Rk$- zw|;uZffIC|arT$1n8t|U6d{=Wy_c&kCY)m+8@OPxpX!f@d4_qv;w+idX`OWl3ZFZ5 zCdy%;3t~y+^Ks(JN}A*nEm`t#QMYv5;r$jsvc&O39-KhgkXT$^2X|ov^x+*?KLq`8V1e(qeE}8UfvUrk)Fn0eTRMl|`WwUfPKz zwAGjBoar+9As?beJ()m*s#b~vzgWG_v^0gkA~SijTkg&hhXDq(Tn0QHhg_K&s8^%{ zW7h7a#nTHBR8Q#HgSUyQJ#rtwOqiCa=V#WF}Hb&s`gI8B~yd#wklD_aM zadQ{v1IJ9Fs?n2x@0UUpB2{P-qX(h?Oa)>@`{4dXi_$#+Uts|6b=G;21xp0O&LiE? zavK*<#dsup9HGLB6y_Ib`ed+3v2|k*GD`yM)8@^1|Hgkeepk?or~0MYP_b@&o|D zPz6IHmSg~fL^ZN9f4lq#QJ6(jlTe-(BQZg#VGw204pX%vGd}2H<1#553V@q_&`Gdu!12GVk!8-nKRH4nTS`2CdCMb=a%(RzO zIuzj$JdF?LhgMSDurH5U_ZZD)e#T9a?8WZ`69t{2!_m&!{VyLZIC+gbc8o1OP1l#dJkTbp;>EJFfT|)QRFmTwIXubeuqXzC zmh>538U1hAp0SJ|5*8wrOVgEZq^u0E+E6vq30Nd4TT5NxFl{N?E%{FL@iFzqjdu$O zjr)_RS0oEF4$L{l{=;@!35y0Yl zvmipd*{NMKMc^~nT+?V_i;ZEN4@e=@j@D=jmz^E&+Ya~}dV~_FIusCj&3iWtLIFjv^H#oZmRT0|ul60a{n_;rL6cBR}?!*tCPe8=cT~(b`yK(_L zDw(zrNJDA9V1YYGy=Ge{`i(USiGM2KOiHm(6TZpomI)V%d0IjLPW;}>lbC>el>fK{ zRcIX;2}yhq9viO0T#xnKqn&qNbjYnXQCe9?3W(3Jyb4!4uIcQ2Urv-fzYgtHJS}NV zzNViMp5j9XqeAh7{Zif~Zk52ajG-fj$Do&dg- zIiFCSxV{W`(avLl8*+x7UmWPd!icpI56U4_sJvJW`#nkdIZ4r{RPe;zD0pG7BMnUF zcr3a8NSuFD5r_m=psUGI?94OAz%$|&eTuTF&_~gwrYSsWpc#myAdh;O+!EPtSvos} zrHWlfpQeD?=4`7h?G6nfUZURpdZClz>{>-_`i|rkp(Wxd9n2zs9u>XagbGElM+R-w zyKOHO)jKhPejH{ENqf-!-J2La&$t2 zHa$;!Z>$YYs>a9XuVf;{MV4Z#owwo>YfUK9tY#Spc#sU-RBxNvK_L;vr6~2;TEvsv z74Z@+?h5y#SS8;Ox)uAO?J9JvwkPUank)9(!%ZT{pX!O*nBM+<0^eewcx#%4uBQbt zwS;N-2+Lsq%@>y`AQ=N5?#aeGSGkvsif$`91aA=)jN~e%x?M1bl0T;haJY+#z^oAk|n<$M1$KL z)T61askH$^1@+W_^N-zv6;EB$FL(f*e&o-|b27vW#~}q@hoAS^WzkuPXgK>W9>b1} z6K;++QO~s65g>`H9`5k5T#Vij8}Ue9`B=P!kv}AqbDVj>xvqP&WqDxykk?yGC#I_s zVf0 zkr1j_;{UReqS_vBDLC&}3HgL|z~$63vGFFS-&ldCaXK@jT^}m9G&8sn^yCGOr$mJN zmm|l4B8&*yd6#~<+C)2Yks=)M0|DmNpMc;NFA~f#(71hJsAYkKZ$~DD8i?+^5b?=L zd{8(gPTtLaafef1e6RsLYY|7HjfgEQF9^&0!O{Gpbp5Tce%dKJP&zQ7fg-jEfy+&83Q`02&$>0A_ z|7C{4ODFS>L*hNPg3>QC(CEWK*+f~@WU1{;?Dv|YDnhzMjazQ~U$Km^B50(}#W+Gj zZ5%ytzEWC0%w$_6!!QPT;zl(V<`Th?i$)UM2v4$*ZJ8y%j!{(?l@9v|LkC|;QdiXB z7%~0c4QNXAt!d+hTpm6!r>d)HgQr3>ihGw>(OBl>rd67mB*ud_digN(Wkno9^`9k$Mjlu%-zBm7KoDRoXEwSsxeH8-sz;P?IKL$XqV;vHGNMj}&HE z_>r^HGk{Sx(nPyJ8reNqqBA~}JX@)_bT!pCPBoJOU&$6~JV;%|p8du;@ZEUS(8%Nq zRKFJu<;^%5Nk&JM4opZ>u;W5Mk7Wkt=hXb2icJH?s7J<>n{KtehziwYXaqU^Fu3AE zF4_gGOs@=N$v26$#5{TLEQ9X!7~$6HjS(NC3*3t9?;d=}SK)1NRGBBsg15^PuM_NC)alS%(9AZL$g!-pB;MGLnz?1-PVe2HwHCj115 zlSWQ$R&UFc>fZii7h0e1gle|mU?b^X*{ZaK;bUk3Buk#(`bZ~t;tcg$rP6eBt%kl? zOu6)XI!HLEYUJCdP{%@NJzVV(H24C346Q=obm-xJeDh@d0?pd3L$lrBUVeWN`E_Aw7jjnN#|GEA`-}Y*C4=(VAE+r z94l+f&Kj^XPhBorf%CEl;OQ8DU1L9uX$;9%LgWVBaMrjnJ98uNCaV8o<%xhY@%T#% z*F+9wP(%0K0L-j(f2Cvd>3qh$zFNl<{3>o!+Og?MG_2Tte<G$ zBpGNAYo?yG$?OhXDbX+i)*jaA*S97jR+^L>OTj}K2E$T}O*CyVJWkX{K+!iI&QNsahgn`*tZi8r?$SuM^ z*HA zCK*NoHwXK6YY9iO>bxI7ZBl-Bhuq&;86-;k=4|O`C7R?PT0RZs@LgbNI~TtGuo3H! z8Hg`29#CEb*g$cq2)^{ch7E%K;4(l1DExB328%aT#aCeC`v~QzS>%O*-?pp}=!b)c z3ltb2{N0*GZ5a3h8TI8N?EpJNsNntq@@`(aA&qfE5)!4_KQM#lePX^a5eLO=?WVoV znJLAjhy$h@f<;gk`8b8WydeQ*J^wbH>`Z(wK$Q5E;uFxZtUUcasQNw}9;HPxJdF_C z`kqv|Jr4^FD*z~hS91HI7$6QPVE3Fo-JN4!$ojd=qt0Sczb!{ljWmp}dFQO| zst;Vl^RC_60JhZ(h({MJwJBA8JHV8^xEoyuS>4+aQ9dAd^ILfn$s>xG$U%?Q8#+~~ zra=Ykb^;`Zdqm+e&wh%ZJqTVEDwqQp+lmqsR$k*AWwaX?$3Se*I*G_d>>Ts(6T2E#R_&RznpxdmFQ2mY1tTPHgpEZ zr-Wrw1RI^9^M?pZFHM0{3ew+ji+)1`#_zYzhZk{K44$;mfgK$^2uvcc%_$j1_ zs%Ai1jCqPFoLh3s0r*+t-2*&`>m==pJZa$miNgAu6q)r%v?Nq)%mPvwB7z&Z4PSSi73dq5A>TN$`Oc5_ z1Spy$1w)z*hJ}QTILu1HxJ zk!m=(C((QMg&b=GBDkT}v)on8{E9&g8gYLPUKL%4pDOJ%;Lu9734KiTG;78g9?>jj zTE2|#XVop-xfls*O?EL!y5pv@k>$4FC1{EcBN=QPUfTe}67=)ISVSlaM;TVH`DTgu zPRdW(N(zadwEaeQU;7-JAk6Yr5JSCNM^7(b5`vZWM*W(cmF08~U8QaYvrTF8jZiB1 z(q=N@i<`hSN#Ob-PseoRaDeOXXpNpsg{CvBu;|Lgg(dGe#24j+<~&LV9_WD1oV`2> zjhvXD=LJbLFt}|`E{^@LN8eW5w#j0TzSeVCzt2ma6KgKEyO6(1LZ&i{$9{=!RtkDR zNYiufEGW&}X0?&OFZqV47 z{CVildJ1%_f8=Zc6bfMxH2D{1>=Dz}UhM}J$!+Ez6+V%5+kn#}JJ3D#Fz zjW3X{8URMGLv1N)GFBe^qgcJ}h=M$5Tj7bbPCucNI}qpG^hlVPV=Qz4|EWQ%T1}jm zq@}~A6;q)u?{q)t8|!lOvl;azmsS{kqbeF@NbQUKg&nHTU!@#dF$x8SHi-6yYzwTN zJ2~#vJ|p(~ABi3mEX$GJSG$Ef&LFxrJQhpk|BoR#c&J*?&tJ1N$Ovi4Ma;YBS2rMo zWXP>idKeEG0MD7(e(E`VI3z8F#D-)`raIt@Vgcv?*VS(^O~a*3eMlM?u)lN?FjjcUF8lRCN4|GHM5 z_3K`+)Yl-d!{kpuvd(P<-u{&2H+#?ko}*w8u4yAIC@poPAKxcY{Uz<35)~koae^ zZ|_P^B0yBgk9ZnMuReS~qXSsFH}G>kSUrV}Nd_H^EH;F)iQ*wwGqlZ5h*ApgpHK)2 zbFAecb?>Eh>`g+jQBj}Rl`Zw(PQ^!3NYL!su_5V<>7d_ve-v-eP`>1#=<$cbGgB58 zzP0f2P!JZ#48hGWE>);+6#FY&gb z+V*QwB$P|@PoO5^2Y@4f3%_j*T=xS5lJpSJXW0MXCy-8xYe$83L4>{-VF=Mf zhTxjdRotWblO*R!%tY^uYu*KExkQ$oXCIZqJ=&Gni47iI&N@z*_9!3MSO>dc{yYJ5 zf}1AztP=wBohYBDnxZkg8#S%1T07j^iZw`@n( z$%?0;l$m=^p^+52zHMc0loxPHu@>YYcJ!A&*}(j8<^DTzS7;B5nr*psU zCh{K!(oJUiRGelbI*rv;sL3m1Z0e}=Ai6!ZfQ}aOZMBEgQKH%3@24B0Zt##iB6-EZ zVwieVHxx{wHmzz$q*JUOao%q$De6cCz$thic$|vigp(dE#o4%!#3IqymqIh#M7zd` zcavmwCFKMDLI|Pf5{zYyNH(XIHH#~%7L@%L_}<$UGl-s|U@Rypbe1*th&pFRB5bID z9ZZQ$@!4S%IT|rhKU?;@+P}3j4JFhu`B=TD(gcoOC!^?!$=N41cC^Io!3dIQt10Mp zg4;U==>N@!*neP

+lYAvu`3E8zQ+`#@_oVIVDIGC<+$K(W9f^~2f#5uUr_KE(MA%X*-5 z=1mgd(*RJ`WSDIOkO!0OrVqhz;q{y0qQeldTDc0OHQT;^okKi19`k2V)-)^?%wUjw zZ;@?+fUcuVTu2xV5HKN28gPY@g|dKI9DYF)f`B%pkj=Wwljc_XFx{SHy~^d-Qq*u_ zb7aq0rp0ms3G(cYzE{l@Ob6x{PP+`0+_rD(IBI1lSZv|< z7=lVi@QhW_Ik~tM8v+>}5?g=E=?aF-Il{&FkRU}W%5zg!98&kPKc`R@p$dsmXR?jx z)pv_rJ5x$VwFk`$*&OrQ#VujRh0X^LL3)tvb_bnngd+BzHoLR`_gyL8&IQOfdc9iL zX_6M23#n1Jd4I#l<*#Y1pVQvEP8&y67Q)EY_mN6+AS^qUmUUIfOd|zv>i~WNtYyHq zte!B$u7*U>hk0vTzmBh@s7qLWEfZ@E%OAoNn`oE-W`gyE@;5g_?+aTvf`2cyUZQuI zKakAdUR)2MEA~OfSqE6J6kTE~6m7C#h;NZHE*sB_5-HUSvvHB?d%(0~xethG**YsD zV1xApmN(iJOeY{K``Fics<+y0$!o%qo|I9=hPOHP?=*su$8{(*&f7{=b9D$BGwtOgO$ebEU!bN0Gqm4v&OhH)b%fhrj94qVZtrl}tpJ3dJ3Fa#F z_0KEEVJZVry0#@y^0-bDKUF0qNT+sLMQ0#@;xv-K>k~i>lZDxwe@S<>EW(D?NPcd0 z1G~g*Zf>cDU+PL5?Kp4@Ti$0+$Zi5d6Z>adT~UwXb-PJ8zCKDrLtSM6u_TDLwn#Wm zay+swss#}=dmo6oHpGQKvs0|BDux}CY{A7a-?tfhs{(Cwp5R1ArEMGx4T%N}f`bnK zuPhU2|74lyV1(;IEb3lf&d$Tcm$OM-;Qv?4$nD@khd_fklZ9%arOSeC^Hc8K|{ zTjHUQl(}B0_$FQOEeER1=rGP+d@lcu^cqa1^;+$UnvooC3t*1Mlo1zITvy{AqZG1H zt@}q^;3dK8bwLP4FH#xRL*$`L&~%$|U6|JqgwMKhXSpH)ohFgof;<_=xhq&W9p*rI zpHqrv@hMSMbE_RCHJ>zN0BB8l9dS^23KhXeZK~34Bi$BDGicbyAEvj8m<%u;DCdQ( z=cPg|u|9$@t|{tsIVYsx#Te!?SSu<8s%;(cig-d>Q+3!%bcvD7#3W=5qvBQ;BdGnt z0<$biDEK^@xcz$-bAWj)5xCS9Dr!!LNK3%HtTPj>79)uYJEAlSSg1mJ1Q(+B~0ROKqrOHZ%=Qs zW5}}9z*!*(Z4NVVSa3RmS-4@Bc>Ij4cY`DM-{;qgjlf$m67~Um|Jvkfq+ks4ldu!9 zKn@==%)I&%mFVz^JmH+fxcVgxwI)Gts{o)YA`)3nuBu=r`@ykRo>Vw^^7J^b%T%I< z%ao4hNH}x?w?-?Z@j%9S6!v^_Q!M))1M}CuW{fLr1ES4DmTUZ3|W1r^yp6pq4{+s$y&bqEv5mSwGyGG-i#XWx5Yk4p~ z7EZKkvm!8fMRns-%2q;2vfFN}IXUbIG`9dl#lXP>*Wu~Rp5+;PVq3oY!DVO~5tJe0 zEdsXrwP&T@4#$4|iU%{RrJru9P7~PHNC`~5M>JU*D8C)wCzjHH*T<(gU0N5GQsGWM zcB`YrDuo~TJsayK2_Li(rl9_!akVAl^it3OUNz$ za;(%CHHDPmhL`z6PQ}ypNQa3Hv^r>~PAzLArZa>+UbmJg38C9^^d~C;r9JDv9gZuC zxL-$w=@tv5hM9y;gaNDUL57${3P$Qbb;${ZE#Lt}=Q|lnwsIeP7hI-ugRngfXik%Z8$JyQf|EEr(p3M`k3=z%bzq3)3{QH@n%8}U*U zLESE$^-%*usKzp|#gV9Y4_td{(UOC3%v?`^mxY0@I-9dWd0|6bMsdOj3v?W>6b2L^-*}ZksnR%+MNK0dJ1?>)DSwMPk{ZuuW5n$`4`DE7@zS}ej!l( z5(HLH-}DM+50MV2yZxE;frD3^MrTqH*%F2|#@5G`4+}+r19m~L&j!&nYYA$hqyz~5 z#5X{&q-@c81dv=WKVxZ}O1k{`K!y)HD$1IIYMqvmCUP56gyjWUKLyp+21%zXOAzE# zXohF&hAQt?o;4W24(_)PIt@fb5MHP1VeeawtaFecq2e`6@P98L=w*Ly5J#3oO4A1I z;%)*VFeVU@e^G_%`5fKbYpcN~%b)SCtKkqqI(vZ_@CZFH^9WlSHx5wSd91FX1i}mh zp6L2BgKI*I_l1jxpcba{|MEb<9*YVZ^;L>uJ=<_Qs=B!;8lD)ljIBr#_`x*@Sou-a zVCN8By9Gg5EMnJW@M@zkaTWvu;xKUf(3psVe;Wm`aQz(5f8b+G0IfGMTyL#yER0K(%l$ z;IS9CkrXKs(Uhc~eN~!CRuU5LZPk5>EReEa!dOEhgMAgMY~Krz-<_p8(-8nZo+Ncm zd&wz_IRsv0jM7oo77UPDK=<(|X~5;93)wJen zlG`tsgiiPU%gJ_2e7I_#3>JmYQVO<)JmGxQsmq>BH*^>yv3!T*! zl%fYBvf6=dG;-2SlZG`lpuUH0iMRLHpF8chq*S3F8F zVcgHRPXOT>b4b7Ozr4ib>YNaK3lq?% zz8z$}f_GR+?WW{)EAOmiLU`Mu%@h0lcRDco%C1Oef~lN>BreRgd8{dE`Nxu{gQR=s zew#cgW*A1j2G-C9p7fl?>|5tk3Y?Mhkp(_&=Q|9uPe{57zFDgTR=&oXsNzpbvnqx-aSrhc%by8B|c|N&XZ^MwLvII zB=cj6EPwQXLNk45;JuZ@ma$r|X4n(Px?*odBun*9MDi1d_iIt1DKtU{xHVV0b&D^m z9!{t1h&di1QwEFrvfFy07XpC1Wa1<)*Rr#^VP72|+ROS5{ZL!Vp%{ zNh-s5<6Nb-`mjtXt$NmRzyUIl029jIvHl2c4l<^alto9nclZ!s@Cdkv=TcO8=cE>d zRbk}hmsGXPnm5H0&vjA4F)UDmtQbi!6f+Kd!;@evPb~j)wCpf6HJ#1#5;mVC18kum z0{WIB!0ux?k5cF&DQMcqCbN#6jmD4*@(8Nj&G}qMEE!xY0&4;~aph=?zPN~IM!PfK z0d1}ty~TR3;|mEHmT}D+g!;{v8zN+amK40;?mtrDevT8-Z5Yi%=o=@%WTZ4iZVjRJSse4VBbt4*&u?f|Btrv-G6!A z!;RPdL(dlkM8^8C9EK~%zV0RY9i*^-OGqZrCCGV^qw8syaOAVqhdA@C{782HSzFyTiJk;EBn@HuBnS zL|dHRm5Xj58sO4szV``Eov1n z$^=E0>`>$MrCH>+BlCJk`}%W1Pjq)D@CaXg*|`~VlO6;yr39yX5r1zwu?AaPnjl;U z7xu|r<)SNNSR-98+N`Qsh;r$jmvsa zJKt2wOCBigzaUbMgVbZ1kc-KTYlGBM10ivN^T=vf7+=}@{KkQc(TxCBPJ_(d<4m2I z=JJAV+%w=}cy^&|Oge+q%V_mUoWw@P*t{TU;|v98JQ$zdrPr6K-jp3^Oo2`R?M{Pr zIC1zusEBEB=X_{Qo=L3SAsd|#mv4YVZP@U6VB0Dv0R<3@>6u_&bo1cz=P}Q;W^QL3 z#2IsPshI&vj;05PWfm$Ie`h|{1mMCJ5Q(5WvqFe(c}FR*svq1=F^Or5Kb;1_ngb!I zgh#YQxQX3|vpV5qqP%f|%erv(8(UD}xw%PF95&qae@DNYVE$qNxBd+e%Z9=9-d(DQ zFkJ)ktAn1{j<&tToW#t2nZ=sW7ns|l$luNZuHHv~;B}uG9VKV0)5&!5Gqb)0)t&%d zsx?)0Y0mqozp(belGT7?eLu|a;JA3e-rMiB{8%y41@mWkutU_F`RC%YC8)0CJo_X~kG+lEVb5gzPVE9?eQ!X#V}ilF7XaD~c;Hz%sGGWXN16KI!+QCAck zzNwgmEpQymc#);UQlglwzlJB&sACU9k;1cH2x)FY9ZSoB)^@)nsLcC1G_@ZgmV$Q9 zNq7)PLMal;Wi#9@uJraY2Qk>qe(YTIOR#9J={4*<}RJ)1lW+*;ZV^VbiZ^!=+}C zk`lI?p&o_l%&u=P2mF!p$RIZsY@b>|yMPUy z7vx?Sr;_1Zn$k}Iizcf=8*s6JYF~~PTr|g0TQ=od2ydZ2p_Dv1SF5=VfAy$<1ywGoh$vSj<&CDoG-7*W!w64>Y8&%ukA#dSI1of{# zrr9!)Mh39qu*N@*mwxHUWz4H`MyJwX`OH8K%i4w}M^L#USAcP0sa9eARl6oyJ`@@H z%da&4P&Nd2Bq$9Xrdo-x9ya_d+lLOT0{5b)>AQwibc{=K@%>T&&OEsehZQOa+St3k zj@E8cSC?HR!;nj#OH9Y*SmwmtGdsDUD4+Ejpi zWumkeqL6brtqZxfwhbKo>fSYzqx8y2Hs>4_vJT*uh2+-JH1uVjsynR1X?DNWd(6AR z#=3OPhKbXY<96S!wUItUfRp#Pad@b4MrxiZNjuhUnTyKx59+>eD~ECglN7YhLl%u; z#Wq#V)&!!ig(lM(wldt~qHU{x30QPM-~_$MW^AAQ|%Q#|;-;0YZ3j@Q` zYo~*VqdZUxUQkaiRCy`ZgoKDyrWseN8YP2R6&MmhTIO#gW0VsP=`4k1J(g8a&Yd)c zIfd^@dEs|s}T4$ddA0MOo+-wp^-qOOKv=zjQfmFm3tN7WK7xU2EzXmIT zbKZMVZN1iX-K7dmMJ}a4oj;W1rwR{)`04^xFnH!S{Rx0pbPEJUFhXMYnzXK8u}Gfk z!SXRnIK6)YBHYZOS_>*z;ZBjXI@96%{v)Q=AIsX@7#IXC$XhmJYz4U`XCxFH z3-_0E;wlFWD>ZdPQyD5`G4d~rp+NMimRR$gt*Z7LZ+j*O*nzjn_cFn zWzw(pt;kz+sT1hT1IJE?X9QUs${L0|5G;OPF!w!#VYm1aj(tr@FUWVj@SnV1UdLK1 z5m`7_IoS<(HZLWPKd<5bGUCD_qk>mS!a2;iI+PW8ii6$Jr1!Qqga?^0I`Zl)nf>HM zMEEn%topsE^LlH%p;fM0CJY5Ft4V$N9rjfY%ooJ|pErNL{7SUaiUwnFRt6CNbh|@v zKG3;|pVioHa;*58cdidkjv#c2eiRB}&1$ z$`%6)a+xA_n;{l2%X&R>O6`QeC~z%`9Y&t)P8D`Gx`yJoof~f0ot8{0qNUQK7-G*Q zvDT>=SCnlv{0Yvxmcs;XI4nu7=5&j_o(IMNvnmwTdcFn3D3BuQmLsUBb3tPaO2R8% zSNYFy>Fzt*28e_{HX8oQ|8St7d>WysRW3N^?cqPvG30__b^0N<(49%kXKmrf8zmF9 z_x)V5yw85av5y5T109N>lFNfQG}B=-eMFm1`o0k#!pG$=*$*>XI$j|~qF#@Lc`mq( z1lp!KY~QHjG6N$aQ6<@_saaaUO^8cI_#Z>Q&-B=s^bkBJYR-nF#kAZ{udL}f4=J6K zMORoVWE{x$A6OsFS6?*3A^r%U^;IiFA-rT)PUz2L7mwBHj8i)>3QE$@`>PYI3y6_Q z0rOJYR`d!mO|(SU&ige3-`!q2 z8Rwps5W>M+Y`=Ls9+o7ToRn+CBad02XkfHkVSbp)Io@j%*USPx4zqfz<;tIfzKx0z z$hEyfgGY3q%6tM=7Zg9VVSZ!{hhtn#w zO!K^GR3$0Eq*WXGp@eT~i2?3}ATslj2mAC10B3?W4d4+l(~b^h(jk=X174y**FZ$P zB`I86|FWF$G*k^j;$fCy94o^msZQo_VV-g6&4G^|!-h#b=P)DWpYKu}cq8st)~4J@ z4?d@&DGp(RBRym`+~T)hcbwdIGyb|dc3V%35J*DLqVxtPWPd-1@R`XP8Y&;;wRv@>Q^)R zQUoLbSS?L-qg@+tXzl!U;aJ4`eBCG|Z#V&P22d$53sWwakPxqidFmFVXkuA`Vp&?7 zn=3G{3!B`&hi0IV-wT#!g0|6X-c!V3{u-ey?ovf;pV{%z+3ss>7Y{K>@ip!^p440I z$yn*VFci}5UF5PBiii7NGn3TM7o%r}hxNBu2RvF>d0|#dPU?yC=zlo8{2$c4TsR+Y z7Xo;Xo?uDc?&L%&&Dhpor4rFerINtju-PH0%qU1+#1NFf5Psq z$037i2`cMIxg(~2r_{Vc1w_Jp<8LTkGVBl1*v*=Ru`unY(cPmVeq^jE8@!GtMK((7 zmP_e+K%=wcO;8Q&nb#E^9oPCAWFdH-z=RWNo2D(K_fV{lRy1>!gfh#*8(daL%KJ}j z_I`5mx4L7+#(-M64AZ9@?^v~4P)Y-j;$K3;NGlXbZCcL@Du5*w41`>XrrGs0LSWLw zB?A3Na6jEn34#|FQtM-eUF;Gb&?f`kW?Z;JS>Ww_l1^8m8EE-!ImkQe&hT{yK-&{J z2|%~$psM~Qz^AEU4haA|K*Ya;qo$#Ui(4)j>8wK@i74l}R1#m@obRUuk5pzGoc6nkR}ct=KWcyPp&sgvCv2f3y0F@jWIE)k(DgHZ4J^<%NGRuxKc2}k~}X7=kQVj zhb2SA&j0n*1j6rh@1`^p!S08|c9aj8VP!CeK*K^E`Qw1nf7+{MO!q=KDR`3v>N$Ld z5CmDTBDr%_w=5c?C|Wsp4&B#_Ew$}f?+bF5)-{2+{PHaFdD?|S$!OFzM0ce=X7h3T zM5C|v-N8q^+yO<0iR2Y53j11uFPcKZ{jx1MIHqMd zm5AFs$cm5lg;4Rf-vZgN8CUO1M^=aPJk&o1Yz!jIsQ+>os?mP1=j>k`&-0Mld!}-- zWJ3NK1jUh1=_+V^kJ}I7i{G;!g~@oI0KpzCpH?$7!K3&hNUocNq~j(2k|?(?+=#Qt zjCUpV0-ei%%wQUTe9WUz#tBFh|7F;W9xaxS;e4|pb>KkQcK2x z?6?w(n#|BB$6qj~lOoVCdCmL%jmF4Onw(Fpvdg-}WygY!wA5&iNeZ`}B)~duBxbDF zdel$Axk@n`g1M3Vyvepo5L#*o8oZd;0^U_KaSpeQv^%EvGmPK>=&3Jt>vyULvtlr? zQXP_HH-fogK<}OhUuX;D!3-!tMn4)DaL~OdfQ0`a-M3g;kw;{HJg8%ZXY7qrN&T!3 zl#G#TN1SfLEkQ(!nYc*%XcpO=i2bdZj6aut5kPj2LJR%a}F$%PZ z2?B&%!*I2y1aXswG6pEpK`@*rzX zV61DNF?tZUVb*h;Vh7@?q7kAs(Ls?FMOVZZLNg0{!#DjlLi-=RREL`vegO!=3jrj% zpkc)6QqDtF#R_b*rOzz^%F7`;m{t#KMl96Ay^I1cqKh4PyMzUcGcaS0$?Ll506<7( zWI!5&52i>tf~@fl?@ZQ+uum||aV>wO4Xw3!N4O-cyuH-_q2*#_%n?)&xtNO1P<3>k zwGNEa!hX1o3T5jnZw5lMQoT+plA>w~Y)ZH;-J*tB>RxkB#xrY-w$rNn1u~NO$%D(M z#$(D_$<6Ws%n8?faKvf=IDcKgU{e2~_Hj-W!-Pr8&We+(BISr2M+{cYl4IX|Xd|o! zxXNVbAzyNnqL;srd)pi<2k~HyRuZAsT7H(<7?QiCR zjr71}sr&A2Kj)~#=)XlO+kbVrL(rV!UH8I!uTJ7y|890#ey=ron*{^;voERQP_a^v zaY!zj2@k|m_`!;=?mPZDFG+91JaYk=eb~=ShkbJesyDi+K%+s^-i8fDd(7@o_F9P* zL5aYaPi$Kbj`Y1)uOQGufCGA@PttzZ>J)!upYdL@`3=-NajFDh=+tH2)_Il$I z)TjMNKy#aB0DNe6!sz zfNI9}OF8zEEb{Aqc1$gQop$7+?(|eq*dOQ7Q++?KR=jF?rz@<)Ekfcfkgga0u`>SE zIP*pX!^=``?uV`rES6i77oK4_ zMor}M``sJsB*%byYe4mSo=I!DIuWUY=qMtwl)HtuofG;gExA#hv{c&F9230!4Ii8= z6`#ES1$g{t-ac`BdlaFm&sXR}aF!wGr5!wI+0z={8bVO7F#El5Klwn{JqG?a{7N74Dn11=giRi+?(md^3^S#E*uv;7ZK?0+jPnqV8j4 z*NS$~Uh>k+P|+>ibf#9R@AG7Z5hF3M^&m5ZgpRxGDp8SU zx$;t|ROi%VKblx@9iGjb2}s6G?B!t&V*7`fG^H8BXIw-!2hMv=cCakG@!mwQzId@- z0*gTFuk`#(4XGtqa3R_T}&6TUrDP$wAz3>Jl}xF zp2XIJOrrA%ip0UeyZ8m`ZysDO(#zle{FOI~v?^&U&e+XTMMgb5`WgKf#- z!r?`^Iu?&OGW5L(aFT@%!H5x6A+DJu^`nuU3kbc!_*hrE21qgl{=D?($C)85rGd@k z`N$T)v$SzYB*R(i*j8(rXh4Lo7OPi9i6F-hMMx~^Wu|_NBZ6=!o{D4S+4L73l04a3 z^RBXgCS*ykI3ln#Xet-3@OnITIMsSLK9MhA`#Vc=2OjS6;qVU&(q-tlOa~h#=!SBl z1?-kchr=chDP-AZB5)&dOQGg8iVW2RJY|9>1!@rCx+&tv0|0yqk#)H5!@$wzi}eTE z?iT!Z0!bPiH_lkKfhQ%M68=$3$fcjRRFf5Zjq7nhifG54oX{x9vzAjB!jm zdn#Zej*yY25#NJ%-im$8VYOm7=C!N|&d*rzHH~)Iyf5q_a!4rViW00h^p?l{$dFp-J_90D{i1f!!<+Qw9PG`Nx>sh?MKrd8A;MzOO2i!sV7SORze_7o#* zL+mO@HFXlaku{`X#|1q5bi0xrM4%y7jo zfPsG#-?nXD{-+5G<#@nDjReu^DY8WA$eRU)(DL=+A8dkYWQUyo?)0Zht%K5uzEQ*) z4d}ZIC*J< z=3HVYh6AX>W0Te=&`JG5h16)xD-Neo-YzlhMU#A~cJYpTSY)_|!oDRAYv8oV)K2_PYVJBpC)H^xOH}NT$>^`>G z`O_!k2RoSLsC4_h#6ddZ1Pn!J1Hw3|zTw0YdW0lPW>+!d{spM?fAP~Nv&M>xnzGMGIw9=N@I3 z*u{GS+FV0rJR+rbC5+1N?R>I|?b`Q};il-r%~sY^4ZP-FWOgWaL-}b>lAnGy2<<~} zNz+b)GbD?rlftF%ZGJdqEO7yep{r)8g2MjqJoL*m>2lVR!j59Ad$oaIfb$aHAS3<6 zZ6K12Ss7oqDsPKaYmWAv?W&DA38{;5XKNBR7=+ivmSkU}l$G zLZ+P6v{mpP&U!@N8vfpg&U%6g#6Btq|Ji-&{6hw3!xRT(3dhLOE!cPd!F}V%ESLll zTO#MB^H{fsIyjTDnI{lrW^6(h-tziRwj0prK*?)IpbAZddA|UELQSHazk%noEsGL6srU|K_iS$rGuNrg}XQl0|?$dxbON(eVrqEG+_2sL|l=`H|IN2 zdqiC5m3{~S9S-RQ7?eL!$QwYH|L2RJje3pazK@%bAM_iibijVjp0ZFo;0$ji)V6bt zJJ)WU0*1iD*CN*ky$}MNZ$a`K6;doPI=0u>k_*yXW-c|0|5&A*-Z-CIWF*_2LbP$ofw&zVqi=-v)%O8?}$>nV7m#MxMyn+ z|7xtI;XM}t!JUT4oyQp3^)dLWc$1>Z$M)=)PF4NEsbf3NM8i5iZCQJztO82=+rz>v zpPLqV;EFhW7#y-?7&s_B4X7FR8MBlw zi;ispVY7RMYIrLA0?eyF>L#LnYrND>Q>cDY%Dct4-_^+To`9i(CBEV*gQrPqa3nV} z9E)&1FZ}EgyMHl(<{dn zs@u6>N{Hj|HG>6u>Z+Pbq(G#3fjRA=m3h0*hv<`0JRf0QT6k@(L_p-N$!GWBt#fbP zho44Ib_@zOwB@KlAyS8P)yPU>gJ>HlAB6{s4AP@l_@U_JY#8G!(qz=?S=Pd(bDLCX zJTCA;NWEs~qu#VUo{c(8_}LoWpV5f36`_N|oxQ0ammc3y*nDd`;y6t6&gI*A7#%-< z0lJAqBW_;QR)4K80&WfEnvd4_&4t3sISh!|wNAV9lh3etp!78>&q8MJ3Hyj$#(VFV zQnlviewa!MzXxf@y9SY|(`P;c^4n%w`d})5GG$l&aY<-+)FbEoTz-j#fa00RfToH4 zNxGoS!(5#7s5iAr6Q*)VRX!}6PQCM9mKrV^#vM4aYP(>{fnfr0NMrR?1++yW;wYWJ z)g%4Lv_p9(&g*nwsTV zT%oZXg(D|F_!(;E{JG&yu#fUw`Huab8gZ3`8mZQ?^Q)VTS66GY9l(X&pEh;!Wq7tF z;VLtH2diP8d4475=#q7`Q&!D-tm$gqCc%0=1*u#qDLUWNIeWiN?k|Ou1>)hYuUfA< zVg!i|ianz|F*Nq*f8O%@I)D2Uye=q4dU_~wbNX|`jH~ViA@`#{3EiXoj+ff8xCF6Z z%mOz7SLDl}b3sL7`8#Ek_w+A$)JWme2TDCQ5;#sTKb9RfhE`r)l#GWV3;c!=F3F#! zjq&`xB5>+FRm*tO-=qa*gXiM2|3v3p%F_u4eZyi*&AI{E*wRWJ6uxj&bsrtWZMcy< zbgVZFMC>y80+NQ!F1vvaqX?WWA7N1v@`hWR_+t>O#RcIY=WVQ~0-GKS63R_CzVu~| z7e_U72^E2A@Hr=wsL)Hjmy#N}4k0a#g+9xB&9prfVmc&0#va2$ANrU{yH#C>JT$x? zsLNe-7T{2Y&PLf~t&+0GVc96+L^9=cGOwnZ!~(LGH#JO^p&PIHsyRREzw+%3`K#$f z(wHz=$OS;$mV67(e=VdM@;Xx=4GjBAE5pF-$s_l~%CFqHzUBjAkmUevo4tEW20iZJ`3K^4w`=iZCu8-QquRe{xR?H6a084|B2aeewuJS;#{pvbVte8*KB*?sjy8#} zRg@>MV!YaK-J%#p+jtvqs0 z(gdPW8X3-0H|bkoJA2oK%*6(&M1x(LJ^?&-nTwwcc*Sj?OfSM*+!?>N&#L zrp+;9eWSG;I!3X90;)R(b& zdnW3N7XF0cQmedK4}PCsOyVzq(8pXrK~-qY!Yf0NwDgi+d1D1$vVL_mKVspOy7~`8 zGdfB>%k6i(uFmgZvL2!yyt|~Nrq&RG_w)Rf>pb5N?Qp3dsVxp;qA2^)Odb&7LIA(F z^YA{gBr*2!MGQ^I6)@!a#RpR2%Dfb>6#b9gfJnA7qrBbe)4j%xR_SM;{iAec#E~@p z?-N)j>0rs}S7>RMjL#))3D}uv8V%3rl>$BbLC=Uu1pJ8zEs87ImetX46SPu1P!0jt zDay<3H)lWu2tI)IR-1vi3Grgq5Ff)b9#3r70Db^HfU6o`c;?osT4zF!TS5yUjU_O-S1>*^|{4s zTqhNfWEQVr^~FI`RKa%R80SC6;kYa;!Y_2mQ*rmyg55!$&c+t#IIG7w`^t;AqIAq#}n!^ zTA)#J&TEz)mbH|2cR%RFWkj%7Rq&0(A6cFh@feIHa-J{rr|t-$LhvAd+!yAtT+Ntr zAN48wkyvCV>B`#&%my($k6Ng-`;63=CbbF6xXcCklp?`2di3Eyoo`^4m{~4|B!m~d z%stXf-kGm>{l{h>9Cb%65}sSrcjRd|MHh2>launHmZ#duL2_iJhu$RL9Yqu(DaONmNO6REGver6qNjRSfuxoMv<~cK&Q-{A-0_U*;r=YeRZSLIfBTV?8A#N<7AYmaj2HpG%>p% z&qhSbjM;a5(1&s%Oh`&!UKnK#vJYpZa}8g^O^azhMWhhS|j{>+(;1qb1h zUkN%;SEon)(EDznXG@X-aR_wJpHpsZ=(0;llr9RT2iQHasrA=!3d^0Up5?URsE26R zGfa%iB;y|fWNefwCnfHciu!Y$Jimx)#~;3&7icq>VDhZ^a|xaqbdm9Zg8I`RbETFPv)R1~g3f ziHGD6Id$IkOCEe@O(%=ze^Q;}Gr5J|GzFOhsyyLWjd^Nta&KTC&P}wGqejy!)g)kV zz@%qbjpe{>{FG^RAs%yifKSy=pFqzzWPhd@WSg>6V_^%=D=Pbr?=w@6ocMI>xoJXG*QFQtiZq|^wHiS@P zr5F41!$}GC=!uwlLegSx( zZ8y1dpE0wdoMR=ygp^$jj^~(A@vJ?vJ#wh4#XMsbF8+KXCLJ5ce+a-4MpU__Gvl& z-zpnF;9r*iH%W+lvnFufo3TptC`KERk#P|_->l<9xe*b@h=^GY*9t~sD-w4Hk6*6q zfQdSgqJ4eqK6;j3bpA~He}NpC&k6c;8>yLGL{qgHsZZ2S21Vxe4FrH4VtpwevZuj% z0}h%H-Omb>R2j8`)HD9_J<yoQ2$Wn?$~E;V9BbXY3uDC2sGXuu*Wh>BPG-C%=^+!%p@r}1n4Sxd7L8Abl=VQ%r=Pu%q#!$#(JxxCQ?5!*hPPWBp znZe0sOyd{i+j?HqpJkouJeAFv133529!{VKgn{tA`tW7$qFO?eRlyz1)Xrnf7)dp|JgNv zu$Z|X*J>V_?{_$qubK^{iSv0f&EZkMLe}nFelF2br>DkpxZxl#UI_I1kh z9mc{5cl=KXFsdkI|$BR$ie7E}E>|pb$hWKSg*idpuJ7jV@M#S2f z4&^BbqgyMAqxJ;Yt*kgd?@tPdVtDTK#ry?&V&v{d?3i5;3B0s5JOq+v-r4|JcIuC@I_{bQc0~VM&sU=PgEmP=)8cz@nIvlxj1$?od7lHQ{>0fU5 zFlPrC@xr|z1{vS1=UVf5Ydb=J$Qs>P^aREyU%ga3C{rPfUr{GV?JFxUJ^cPMzo9eO zm1ta%68$L@LTmI*GbHGBHFb`!_vzv4m;KK(XLTm+?u_tNR~MFK3%srC%MpymWZB3c z7u7h{cmv7K=jnhEd_pnlouUmD+%u+pd4@g;Ct~C^bl4p0V-5h&1Z#`ZN0#W)^a9XP`2%I|2A=d6I7ah=Z#wIM z_I>C$)~O>bwWdan0JPCSMC&K}l1rAC_qnb&lR@^rCHEoZ8Ty^X+Xa;#vfbp5UUJus zbAD82KJvMlOqA4$8X=LKdR!||hJ$n-updwD*RnPuW@V}XhF%xjD|c7uHsxOIaQgc! zBL;p&pPYC@Sr8V!D)jpBMvFv;VO{Mg!-${&E3Ss?P z2P&X45;T}Mr^60suLDQ%4fY^b?sQH^{Z__$nd7lq*EUo1KfVP-u$54J4HU5_qnhH~ zGHdcHQSwl9q~nngF>kD>rk{HBzfCJ};r0c|IV^fBh7_N{MXHrs!}_a<9~iG85<`OZ zSX?eky>SLRSBss=qg&>_N{Ha^aTE?_y$Cep94n_FWgGmX)*f5thREEJRDF}|Ae-@$ zb*=brCq|wW?7IyPdh%FR;VXzphb1zdEv(f4g z6y!)Ct0PY_`EZ0eH-k<<`JJ4ovG}*6#QzILN%t2ZE0J4hF4WqGnWa$ z5QwD1q@?d%O=I0tnS0w=Z#9mmM&_HoDNW;(cbr)8+FF2yR*lxlc3h6$yGaFQCN^H- z)!X#Q9;{Q(jAjCm8t)?oeP)-U#>FarBmSGfb-~(w+^+dK>8`ejwv7{X-K;N7oCT`9=`PI8Y=?r-d3YEplC^H79@T@TNqY z?JMcc6n1O)+8_~#ZKK&310yk;J+%)?{g8hWcGp+DfvDApvAg^e(9M0sON4|h+Z#P2 zjda}i`>JzfsiR#QPeLGhJzbP)^!V-rCgUctJr3~qXQ9Mx9_+*qNdaSk2TfacR^38N zQ>2cgBZn{q@Kz8nY_RsEm=OYG58_D+E<^$-$ick4Z0ihY^iN#?=AKwMEGvS4z)uMU zWQ1NbxAs41=Zh4kRI*mb$&M68-+C^$J76|9OYu+S(8{5;^&G*SY~Wejx0grC(}!7X z3X%fhpA2&bSa>G;mIde*=s?$uDoGP24Ld?q8*y7;9l6Fw62RG|NM?Bs8}vIl;TVfo zSk7urDEOV8>0<|51kzQ00iPfQKm&RK$aw-nV_#-|m2f$f$Q$C09)(x5eIRtq>2Jul zh?B~1Q-WEL<(S$I9LbrS@vKb8_bhQr{9;rslBp0#Axthr4eXC4b^gJu7NZtUoj2?6 z%tnv#fQJIGuV(NX^HX_vBPKqOHD*i@LJY$a(cB}RMkr<`gNDdmDa_47$7h<4#}g>g zr<_CKHtvmGkBa%yx@VfnT-jM!;kFu}fEbJ|)Qoh%(Mu+#MNBi*$68XWurBR6}@< zTK@bxXX*PTaz=3m68CM)H`RX$QgQ1E znUGKt;*U57zUd|?PG8|MBQs~1XWYcO9@3kLI<={Joq&8d=lKAt)xjc40YtyoT<*EO zCAucdkZU*>o!QKI=N?He}nf<6C;s6AM8Dp#qh$bqi!K zNe&8kPTxGZocB-HjsFGs>=qwxm^hns24|gM`+2eEdfUOVsAQ4iG&=CLyxC+~Y>9(3 zc(WE=KS~$Wcj&~O9`vNK=D}y*@IAT|=AF9e*LLTzhFCQ-CIdx z+sXYjjinG1DDP+O55+%&&ir1>i~2h#Gio(?s&T-5SVik$tGm)BQ*4mYo2*MDUhgMg zLEKciwF^wMdW)>^CeTpI59D_t8Jb^e=X{F8iivS@CooUQ(MkAW2eExJ!L-R z6cWoOSGTZ|wARV!G@AMHp!gfCy1UA$bS3I+R1(F! z%mfHzI=y`&S|Gh_JB3~AxsSxY{kiFW8a>G`2${NT3ckEx+T6_;HMG7Q<>Yi6X?3f_ zaCKUehvKr~QgMEN>=f@B)2GnIgXe+Keyi?1>s-xNpu72H4-dsBO_>X9Uqj$|m@p#< z@5+_cB35;Cv{6n$b1Yoe9*o2SKaW(i%|G;kbzfwAzAz;6P&e4pe=aa6XFFR51xH~R zht2O2_X5z+a%6r1PA~LDdC5N*^S0WU%5N9%a=D7^%%>is@f4rH3S(~3mFWQOf*(x0vy979~p>kQuc;hKqmvl`?i0TpD zf|Tp(0Ioy9#?D%8O7$@5!DnE;Dbiv3i{h>YB-U{t$P}UwqAXd2#1u$#*zYA*Qv+UOT`V3s7>TdJ-2&5SLRF}-W8035QI%wW!K1Gh!x zgK5<;$LifnQs8_Bt)04k&L8lSH9G`3jj07d)6_9T!J*}EozNu++FfG~8#MP8TlvgW z=E*6x$ccA=G)U*y^*a4L35n0Rh$K^DqpqUB21uIjVfzsUN|M^AhBY7)J~vZ*LVxM1 zg*4xBbZdPtXWj?xtKy_Ya)`Ogi?9H&MgW1=m%IVj%cKjSy$Y%jNLppZ1$xv%gu_0g zX(+UeeCev17kd6Rg)Zb3k4>nt7F*d17d7Uv@o#+WPW}kWGk)wx5{%?j=eF$BU;C4q z)CRan37&Djwih>^%sT?TIwX1nM)?fY`Fp!6p6)|BO!oCPI3hA60f>(G-kqGUymZ9BzmbsvUgGD?t$c)6?`%>6Sa3pV3moMaPA5X#cCsBNM z$lQ0*lJwhlx^N&V^e7w;DJ@@iS`Sw*e_$9@DJ98oh{YVs()DtVef7^QE5i%boOcKh zgCt#ik_+_OqK}*vOL;=Wv&IG?NxH@u$z~C-Bav_tdkkU&{WHfs4W|`*Y5l~LU06xI zIg)TIMf0jZ7fw2uXN=x2Tx0l*&n3*(KF|3O^HE|GSxOAuwD1}^ccB|A=9G(8J4^nw z0k%i)w$Tsq#=WK@A}KFirj4M$bL|-J^znrG8WXiaZcf*9>rtC)cGb~*D@H!0 zaR?l#*+yv54LMGk`DD_bt_#bLnE6KePMxsUD}wQeOl1>ayI-D2EQLY7%!ky>g?wB< zX3lam|F*(E27?7tWXr^NTRo4-miz&;HOWbnG^X@vm40p1hRO)}KUl_p`JySj&dm6X zk_%(lxtLfzLhEr{Rg=7aUewmEIVyumzokca%k zOZ0c&W}Uc@vXl^Ht9Ui8=@~sEwz9}q=C;tE)qjflk4IVyBf{u&Vb@qW`xn@W$IW_KKk1J;;H0u=j34gQ9ez6 z$#H{~k(k0B6EB66vfYJYUz{kcu=-CX@4RUAh3$FNNA}1W%pRY^fssi~B!(6Yc%!tw zY#Q81B-fwji-K$Q2!z;Ve3r5!y1^LgzBBY0RR#9&yb{Mr&93yllKKNp#I2hJQouH2F ziwy7L>#bXbnm;v1dIQv>C^AE;20j@qaH z=w&VS^&^^r>B2Zlc`w@q21~7da9F+H{kxQZZ?*GcQI)UKkoB4Dl_2+8NGCo8B#AMm zBh-FlxK>f6*<~FON4A9@j}9oxJYvTqB0y~GJ!i;#o|`MI6`X^&rRr>~#xTb!JeERS zFpJ+~;xNP4)s=fqiEx)aCx7=5cyeCo2gz6x1rN1UCwI{Ft{$Fw6T2 zdGj}BPx{4==1yaRTk;zddzkS~P&S9-M@;(jsgUPR>%-)Qqfs{lSY9|T^-OkmO=Ma- zcE+9qaV%TBOX!bk^XPBb66zVd!-6J&zL#whM(C-YdP4JQ_xmGYQaI!=O}%`)xxDq; z{Rn$4_Bll$0lyvtgqC?=6@QHySBexM-1CIHkFm3d`eg-F9{ajn)PYQuLxN=oGc%g8 zgMfCXT#+)&nV)QK&1IL(#8{pdJWVC=_)+2I96I^WG5I^~bEWoQfczT!Sm`SbiYFpu zZ2|@Aup(*I;GTN+JS+Y@tSqijR9sUoaVL`yZF|Wp1Wx)ALtwai86Q^)QFku{uZI@n z7oZt0S-t>F*c)Wf?7=Iyl%dmTn;7W4&^2h8``7u#7rZUU*0QQVuNO*9-gx!%qA&!P z9`1cL6x3n?L>BH9;oQemSUZ_YB|bDic5pIf)_B+Trdy2JYp;~`&m80AFTi*ymAs*K z50mZE2r?cz?a$g!@IVX%%etA;vNj4=C?x5x-}@VBqWnNBif^{mfqb~brHkod>sAI9 zJz;bI*6U{~`Gq6xiLFMNt9c*zv=bWh8X!~N#vN2{j0W3PBTipajZ>drtl4Eq{nvqr(Hah6(1n5iAv+SIXUGeq7jcl3|Z#R1A|J>6=z zlB^}0Pd)Y&_WJQnjHz%;|F6E*6yZ@|R0hc-Qsyr^PkKx0tLuSsndJX%!5t9-$^QZf zru7Ew45>(1plLPP8&+;iw7o^-`k2h2EKf+xsjE zSM&ikFflIqvi<$u#*)tDM$c(WTDrH0NN zJ*&pDBMyuuCo%n6+Bi}x_k3B8<>6fAB@VKcIDqKYm<2(G42t%DTvnF4*_12bsr~u< zFq`TXwP>4%iDeWUhny_?VYp{aAtj6$I&vC=I;1;3fxEQLp-buDp^-v+?@VkZM~0y3 z*47pLPM!$yDAS&}LHs+qsM?SESh#}BF9U<{DweY)2{nsb%$lnUkXXw+(92Sdf>`3= zoHMd4)(1PfEXCEi;e|vaB7ZS_UUA}Go+`RZPxBS- zi(B&M{9f7)C+zyxap9__Cm)^s&1s0}Fs~jV5Hl(-L2<*JdZWk>c%7ZNPy;xvzShqGTT~_VbkL_HqtF-6Mx3n=g+`ZFWkgt8Ue%b2b8B!yT7W+HUL0LRoi6sB zncZ~pk;bM$@_>rTh+K2P642mn#UXtxULPF#yqUoy{Ux9R5}T;qQ%g>h4($^4c&ZUh zMmhuxlaHf1N+Uoi2F4z8x%2iC8Mx9D&rGL_?4(4t+E%Q6sbr7O5d1KoH}@IyZNn-V zZSJ2*n~$F>6rWs``4H+=;tSPwE?9np0(hmGk|DF5uiMkmx;n;iSv@VigkDyK!Rn;U zc;vPojE@nyi0W9bTdFcMzrMoiVV|B9-|j#f>BJ1@VF)MHRyV3-!^w}aLh{g`YHij) z#jn^xgrBTjr1jD*-)cy2>7w3vu|&>`k`RGL#nFzHg{nBo=o>KX=B!!;+5;b68a-hF zyDtj?9&gsjYp!pTUoTLXP1;E=qRb+7D!ns{122`aKs1_tKvEp(L5GbR0ed+gxmfq2 zl3Mp6a@c3Tl5bFjHYW!j*}z9cJ-wCSnQs}s9=`zcm*;AKl*F#`?5SsBhUhRGTpYP& z0*ak79_s$SEXV~$j!p_6NTowa%}35@!^_6Zj9*>%OvALNI>Ep!05VR3iu=dMZZZ$c zK#?}RBKyGk57DE@JZSZr^~}jaCCJ1x2i@d_2XuSBHJ%;VM9j4cAbRV`_(SJ!eSArK z!tO+`FYwiUFH$shGxeQtV=B-9IWojyW2o?^j-kSWa0G{ro2?*6^k|}J6+fIy>!zqj zA0gI$CxnfDsf0E0HdSio!bz@mWWw9AC=P$6(w|FmC*S~O(55`l$Y04n6n2fb? zOl04CR$+_*fXh@i8!kJz1AAXy#G~ELJ`c&tMZP~v69d7q#?Z%HQ|V*uY{})|LWqIji>Vn^eYLlG^=_`7+%+ zNCw0!I>NYrd^^aN9u;0s7sO#nH;;OBlM*hT?1T;7A5u@tT`S+Kb6{3llrL}nV*arr*Qf)o2Wsw8u> zZDVN*jX;C?a{!3Yi_O&8Caw;)*bI6~5xb4oV>e*4(;dmz=6tJC)}y)6;VuckGSl9;9z6Y zUI?76bwV?Yu^me@1X(qbkF|kmSZyAo3fQhYE>jDFO>0bym;{?s&Ha#^)obkgAys|@ z#tV1G8W&HLyl_|0(Keg#F9)vHe^RVSOrQ0eCq=h6mD zt!C#aX=g5|mnk(Do}g8oM9J}(k2jZ2FWbI*75>js5+~HeQG-LFw(7>OBy3AKb%k~s z`7q;)>1s0;3-0cKO)Z!xlcdO@7QK4Z^$jOKvhJb2dOfzRHUpYLU1Vzk5`bPogoh~+ z2?+{O51z0vpgh7cZ^eBe=4a|3WZ0RNc?s|*ZrKKRIBa@ih*{soQR-*lE!+L!B9tw}m_3~*)lYd-a6M3bsLu#)=XTL&cn)il6$G z0GmmJo6qFD@$#RMKw#u8hPG5?SVv~Igiw;Olwg3fCk`Z)(TYXJwv#eBLMRQeoX}QS zT5L_hI6oE@J!2w?@#56y|LB^MUvm?sdKeIn=Crh>F0R7MCx1+t7(<0IY~C`#7Vf#i zic!I|R@+@V8i*k(2zq4Cx|Tnj!8AV2#hYR>pRNE&K(@c_d}tl6hHj@OsM9!kje$o0 zX34!~X=2rUecf2@q?1yPB_Q&BTRSoX&0a8<818&jC`^)aVurxgMFR&NE=++m;u{4+ z&J=^fvGNZlUV9h;`WmB-M0lRt9>bXAFwHv0NEs*%GbY`)OretS+Z;A5CD*K{8h}Tn zE@SbPLVz>E`_goBkfn!+pPs= zG{`}QdAf7e4>47nBC{7K3{P#}B8&8!NtOi2KiI4S@BwFVFylNAg+ergoz^=SEf606 zbMy{Rk-SQqPo(q>te=zv&;&g0@KzgV&4B1fb^Bz(uAB8B0hPSXpis@iFe)cL=|QUi zpM(y#28%SKE&+we9$kP#veS1tq&FEs>||nn)Jif4o%)**?M;9^4UImFX}Z+ce1!@g zFq&aC?5*`kH0gkiA6FMkdglZ?4xexFTD%#sgCTF)D57O23Jhb5M^i71UfPXoaWePw zlSA-StUoQOA76A^y-|0qu~|0^`xn+ zdga=5vLDrxd?la$$bxa^!2psRN>up%MZGYO+Qd}4f<4*Y!fU{mu|gyq!}sw3=n1A~ z@&MyE@(=wtuKk`{6BefhrJtH8LxvcQ4#kT%&}@|K#tScsg3=7DMFvu6C83)YHu5=%4I~?Bnz>zvAB{ek0g~&bl6mL6$albhOzbmD-KQXnUa1;>U=)W|SC+&8i zS*Bs8pipO>hJimqTYmbT6)-JlGm9eh{4=*BMPE{%PHIjYB=2cp)2R6&m@#igNzY`2O|KXGoAHtQFVW=v>joWt z_RIsg2c7bOU`*)V+mQo7hh#C8w=)S{2hE|He9C1{UQ>G@t{`#k35-jUiTt`~>%@OZ ztur08;QK^!8BO}@F|XC}3FRpmmXCLK-e?Y5f1Y42G$U&o=tQ>S5QmCt1zqEEcVP;760PVBAQg5yt5}9hDISw7N zdrK(DHx9enOcqni#K!FQt_>mP(%lesOt&EEc{sl+z@9mQH&#S0TI;K1-rFwm7s<9v zOAZ!N;782znyZ7Cf5&cy3f-YgEIQrk(B`{4|Nz_>M+kJHkz z2Et+FFUa-r`=SW7egSGkDq4*zbxmTiO+3kWDd{Tc1f?%8egR@avR_@tl3mwDq02js z(C_-2OycNH3SGn=rJJ3uAqex{6SJg_So@L4v&9379!oyUBdtF~JupvquEy686YYTb z<*1=;;{%LI2jZR(ls}LfAY~yU>npYsgXEFYTq}(8i530$bRQDe8rn{O6iv?K97ZJ> zjxV#U*Ifucprf%eL zWC0ci*Lx4xo%|hpIZUMPMKm^gIcUWY3Co@*t%U|1u0i9Z|z^PemNHmQ=0w$3^p29Y2tRG z5=ZfXPhoNd%t#;|RskG%g@7ZM6< zI!A-@^RUW|=QM2O?D@3g8gYUmPsj|D_gV-Vfho#(nQSp8%fv+FS9k?v@q+x2dc9A| z0hmDXP%M{^Sg{C>9a5*|3(pL+vzvyiI24%}b-30>5_GK50LE_H^^cWdaR9Kh>6OrN zuryj1N#<0Lb#pB(R?_G$J~OSn_0QSQ0l%X?*H~b)IWnJB4#6yMOP>da%zaXF@Lj<) z1UE3ww*l}4c-=SCyx9XLQ{Uk^YOnKUZE9*0mWls^m#;bvyW&U8^pq4J9?s5^ zXD=^^+}WbfytI5MYWT^_4&4Wr^*XlP$IzFKzhtyd1anyDJycU&CM^pvb-~+ z&$~YirN%h$hbHf6zXfghJHb6?Ef`6>BCoFQ>rJgjdj+P6ic=e9ek))Cbv}U-!tn4@ z*uyJ3Sb(;I>RC`=T*Ic$tNg*_97dM4l{Sg{{S|EB9R-Az>gWyc6yE7t(pp9NJ6FWl z*o=}cyx8_C`$twaY^bNYciJPk_HR7pXR_g&a*)_WTD86}T(A24pV+GT$k_;2nZ()z zglI^ABOFsg6*SAXBgt&NbtQ9P$r zxE*~u8L#5&%1G!q2zHHUN zlbO$q?43s0QPbNtYkPX|e^I7)WeTK9Zf6SMc!nmso5k;cd49g@UWX*J?=DExTpuZN zcl2CwdnN21og>wTLA-Y`Q=&q6*bU<`3bs}@X|;RjVVs4Ghw9JSi?Wr`kLN(Q)gFMb zeEvelTUY>*+}?c@RC0aY;WI=z9P;P#p@ra_&RT8pW4GHyy_QnZwS_Hy^tN0tq);49 zsvjJZzZ1vAl^ip~Zj(8YjFtj?C)7P5`dX$Iez|z?m@lA1P7;f#Tm`hFA9rsYHRsy( zFHq!OD6Ui@7rplRi`SoTUWLXv3eBfAoft;;_HS$*O$pybE0T~_LxN~1JaIc5g5>cK zhjs-2VX|PAEs zW0+a+vh?}W+&A5#**wPb*o#2hsz$zCP>gc`ZJ3gPh`%_KmTE&CF)gkowT(Pho-s?+ zvrtdwh+tF0;< zCrEtS!FOcSk0|WT0}erz&)RBx(O~ox2s;Y#>onC`#@m=VMY$}p7*ob47pISE>(KQp zlTdyYo!IFsS~?y=RVq?au((!G7pJyyY9QL*0mKZI-N)U|z=yF58YYg1x<9I%u$bamOrqDlX=G~8M85Z9!jLN8(u<;;<`_*NeoybM zBoTn71T9nna#9dwzZKNXVJ%Sxx6{!0;pN_?I#Vok$yp{6pu^Ufou924g-QGRlQw`a zSX(fkh^-t=3en~|)H$M=sR&qW?QHE)A2T^X86r_`ja?1N8m8|tg&>C+LYJjh$#qI= z5pu|abbEw67LJIcY$`lFjp?*BowF2|1@7@wx{`YRY0vi>3rKy9Yz=FjuER6Mv}~^QeyEU4#uwgE$4n>(VEuYk7o{orLNGFd z+6ErxeLVCsN=?*+1_p?9@;<2wa-x{81;X5@KT|xPEil*>o=CY~FgYDY#k&^oKx-qP zpE~mtc%6DZ(mgE?#;qe%6oR3JSJ)rbVpP;l{$8D@G+# zlSoL9F(Ia%p~UHgfCmhlnnox!k22V0&>li}(WwDod0{CD*Lv%pj_SKCH&4UDh|^xx zwMSK;uTt8A$Mcnqo7=+wPLK!2Idg5cXOV zK!FOI0Tu7~nN61%+N61Lh3BvWZEko@2dT-9S4f~yELz+ZfG$d_r|K5*?5H~FE2-v7 zDCa3m$Y?=fIchvao~`YWYk`EglAT?BrZER`^JMW5lOw7vF&FgafQE8^D8C=`wOy|9 zS7l;+ez>%z`JKTDrpvyV4v^UDQ4@xmy|E$<9(6lAOYJfa`o%`lFF@Gk{<~GY-4F4O zOy(8F8flq~J<<;OV^#c4hhJ@J8{<~dMIm~|FvB)%Pwz0p9LA5tio}hl{XW9bUi`on zEZ$xZoMrmOxIYqE2bokC>~wx3!E+83&!O~pa`hzOufwe(?z5jBTB`Inpw|@;Knnvv z4mJh0TMd|*2W3o;gn3GbPq+Am!iih_`kiZ@y53Hojy5W~2Hx-b+eM3V`~r;5K_8M` zroYO<&*ojB?OvYmkfXYE*%L#3fL&gvedAYu4uRm*heo(E5&_# zA%0)ddp}{Sk1G~ABDE_=JJeNzsYu$FwxBXg-<0%$HWJdtx|zT?E#NCiFBpj!yN!V3!J095TOaHVSd#bmvUmshW=3K zeV_DQ&Nmt26+1K-!Q!J%GD|(yuv8oX*-Cp^D|#0A(L}{w&8z^BaedCpEa`z9stV4f zvR%_e9U0AoIES_b{?k}`I$c5;OR9bhAyHZZcF6fZl{aJ_c;LH)eWEx3XiEu^5T7{> z5%a3yMr&g{gia(Ze-zAB!vO?M)B3XL_m$LBO9C-FL#jB9p^=0VBX6G1)6O_aMWFAf z4>TAQZiJYP^wMBU1X<&_7hy1R{or8@gEY#_C%z1PXz~mh2C8=JSR{8Fu|bozs)Nif zgZMbV2H?q1$n%VoGtY86k1VY3}FqeOgBmLiBT87fydWB6rgK+z}(WjkV-5>m^xXmvK3zv zJWaSO#<2e$>k&gAxtrvesPYxL9=bt>AEd9m!wLOuY$gadDNdOPM_4|=cKoF3DGaUC z`F$!{R)1z+f_dZ!C;M0SoN4Va7+tD5>Xg%X!pAdH94F(DfGyR)LY+Q01!IjsHUj0i z(7cuY(i!=a4qE~EQqFc{cPZzg34eP-OBnpCxzT__Ox1Hon=h_`xWYf_fQXV^0k>vc zu_*MMc{1jtxYon~afmD5lZ-~z0^wHbNV{wh>g9R{e!cCmHgw^o5K9!zczt9|`t3J? z#W+UtA>NfB7cBxvafp5CUyQyM1b;Y(T>F;6jGLxwXV^74&9bEFX=~)>Mnw2I=AA&9 z90D=_LG|@xrvc)ocP>vr7&QxPppKzopg{3P#4;zY>|zgr1VPx$(5~?-U0kqGh7L#L zma}UvML4E-2MhM}?+09DVb)e!_5yaZc~HOP{!5CWfmd7^=^>aUWEW+Pya}O+fw98> zk|qD&cXWL5t6E>Z$_7@;v_%V>?IR-z7&U!wC3Z@E+8 z<|#obJLO+I_{KtUeumyk06-j)1<>_>t%>LBrRW?yiQIHU?2AK}6(1Q3KPU@3GxI8V z>O{_0*i3x7JSlLu_{N=28kFPpB1*SkQ zFdXvFn4el2V34HTYCJ-6#l)Faj41pV273a0A&a^B+KK~?V#6oDQ>sa-)G+&@qz_L1 zZzbE_X<-vl=+9ZQZMaq6;y?CjVu(NC>8ltYT@ z9?BZnltjuJ|J7loS_vVWt+7z6qV_7O$U0$2DptOe%83L3yF@0+0GO(nlL99qUbpFb5YkC&r*>3$9|4N-hr#Rh^-~Eh z$a#^^<4MIQ825i(CQQw5F9{@ufv?FI2V_Zyg9KPqV+uVr2K5HWb3>#@kWY2LU`uh~ z!Lc%KSE)B=>pOuY9iMNRzmi>s5%7D2t^aI^2I1b$m5=BX^8-Q)Tz1*`xONfYO0t>G zrd}}tkkM4s3}(%_STY^az%EwBO+6ltA!9dn-NFs_6AR2I0>zb= zwld!*1hd*pNQk<3XZD?yR|!U96D}`^xi5Xae|F*xzxM*CXB-%zL3_h+Lb??+|5{_adinjCT^(;Ckq@ewdE9}i9pz&+&cSsk%ZXm44w z$udwSmixX^amz%;@aBttXx}%?KvAUPvS$$!`oX|UbEHbM%M!D`AF0+(^lKT@Q<=b~ z`~@YRt>L&%`c8~Fbqus=evz(dU+e}jbve=Ns_Q6pTRk$>O!dY~fc?Ar+V|52+cqEI zYXz}u8=>S?lV4f+V7yq*)1AA_$eBZzIz90*BL6j-`OZl5#n;L%!51I5DcWV>v24QY zddN{r;D8@{miP-oBIQe0!fdSab2wX%O>hwVD}F14pkLNW)e9k(FO|{gx#IdA3mJ@0 z=Sh`iiH<53XaDYoXE}VHyZ$^7@74on)V|}Bpp-fHKq{>wkqsB-sBRHZZQ-y07UrW2 zILdi9nLslFb(u7d&gSKTv!|<@am%t_3d$qX5|YHgGgQ)`UjX62{aMNWj0~VqHpRy0 z!G^XDp|A9|OL+B}xZW3{z+m!F^K_}WR(jrpa2K;h3CU3Onn`ee0OW7fjW40QiQ;r% z!LWKLOIuEBGm(vf1~`&wXd$AHk4tw#JZvj{>rnT*8a#-!1kfvB*0VKed^!@bN+5nC z|Bd!=ekAeCmz9~~baWgo^PG$Bf;#l2_%2DKfQY@K;3(ZP#TGN9`Q+<0Qg*BzfD)am zq>m8>wE0S-3BqiJ$El()3OkPfxJ4luLJTSQ1R%^0li1V3?1( ztpMGr)PkFlJ)s2sLogH>I?&rN!K%4DfZ#mcO4$Zq_|)AAhe3lkBKpD7S4#!-jqzWAY?axS zSDvw9^aE%{Gsb1MB*k7`5wsmC>6aJmIf8VHC;zc+xfTa(mvHeJ3j>~;zS~Hl?ZYk| zbmnf_11}D+-5DvQS}(-h$B5Wr6S%$MPcVsK3SP(>Wh?n2oKM3e8O5AlZwNIenJmlc zC)j>96iBf_cTtC%!XXg53>+|wYm4Gdv8rR~5@2*Ic*BzrF=a6a?VcSIU=DSEXghCH zM{Jx7*n?G&^AB5>qz$_S$-5n)H;ma0JC)XF&Y;2!W=4QU(aJV#3iVK9CDeJ98g!k7 zk3N$WR$d>fU&sqm9{cv-mTNx5OXz5i{3_M0rqRSeB?yw&92W$wQrU(RJ|4<5W4dg9 z6KoLgvaxUjq%;O6IitVosmWW!VQyR@@>CpPi}v+USujTOr*#{wJRAZ25PbzF zz%F!o%#~&LY@v4wk#DHXtmA6}LPEobJ=6W)<^G&SJm1L}6RaFmiuE*1(cKhmRUIYj zLAi8UO}=n;V(R!?AyDdndZ>~>e^G*);M^tB+#1)2=Y0RurK=(oNP>m0^@Cm%UBkZ) zXLtQUD=H|=xl5QvZkB0Y;iYCw&O?^vA$!FP%p6HB^vKdLbX||F_Sk#ypW3$b(&j1$ z#O?$*WA2`B);5|>)uUzIm&~my`3>wu zKm=Y|YwlL>KeUFYnTu>6%ku+HF!YmZYr6-yz#q2F^PsUbWEUbkh1^o4XzWCRe(2bi z0k?SHOE?FJ^)Z;m5#sF5$3>K$V-+@nGW||-YcSoXK&^4CZ##*1^3+MFW?CadAKtUA zkPMS+im$~$_kscE&My;ecaA(tSy8KmwsB~`0Iz2vR)78i+&SlIlYKM}wIX|L1c_mV zGP2C@k-6$)WxRB*o=+zzWphxv$akvO29UxbApnNGc0R1GvL%|jex728F#5Z!8hm1&yr4_j_J; zrdg3WIntlaKUkTz%6en?>ZwGD&trSZXlye0^A(s5k0ISWK$4|vew;&vOC0k$rW^sd zJHPmsGE^vL%jFNZAz|_@q6$UH`Y~^YVB`?@C%!MubkDpXx{2eePEQ1@uOV8A@Cz;B z9`+gC`-F#;Cq)^)+xnC=&*vTfDa0t*_ zb^lm!eI3_H_{qvEfO_ja_c21-=d3}BKL8aM^kIg%$Jk3Ddk?G5Ld~gWg&nN(&Rtt} z%-kMImaxGT8RQs>n{AX`>R4R}0`ivsa&yS zeS*gPUd%|(-_1;7Mb-Vn<_;s-}@vBa|#=yxz%huNG!-BRomx*+bLeQnBO1}d6v3JQ1 z=aNPYx=i5d7@Ds)n}u>;y}zLIN8@`fJ*|z@h0mNgP}OF0nJv7x_UL)Gq>OpL0CIx$ z908t7ww9b@D^^cTK+hLm445GXLq4`6J|tYd|L(_90Q-grqkEHDy{vNn9;Rzv?*#bK z@?wBRI%|iIoAmuS7ll2=2!y1?jsF-rn65&l8?5j_8!aD0yDhk*J8rN_FD(G1)rQO!_teWd%g%(k~N_}oC(;0mbb6@F8!6k?B z`n|F`X8b0Lt@J55zcj6lnN-*u>lW?u2^s8j0SXi_C6|~ITe>4LNQZlMvnM6!7{CIa8Azrud#xs)J0+4WOKAj8453%v-jr=(9RA=5`exi>^LU&7!E}_zm^^;sl2EQ>TdjOWQbOf3w zP@Z|u6|CeN24~)WPcr%mpHrck@8@$Yim)5DxQ+b3RP1MD&GtXWIyQ9Nmz9YJM4rrF zfiMOC)dpqU+UDIhEtp}t^g_V-{;dA3(0Of$n zf3HXeFG)*~0~E8VrjhepFg8IE1>LH1M4S<*AKpum+W&~m z@CjRA>KU%C!f-b$GWWI;kXpVp``N5&jNCIR`c#Hx+9w4}@gA(@lp~tzWsmtpslGILM}j6FKOg}~L9;FV4n%@wej3aP$hS;BZudNHfk+&hH+1sSz#sXGz)F41o^#*u zm~uTnE$L_T3lW>!)H-18tJHbEsMZm7Ghu|tW%UPz43vP$814c2aj_g(fd&(4HJ?O2 z`G{U$NXvjZKG|qUaTlJEp?H#hpIFM$AV+MAH;Z^kwSReg>pWS3pbaoT?DuNo?R}qY z95TFQuwy1COR~=r&MoHMpAM|&C`p{|#C+?(vEy(*xxYQHth)-ziu|lGyJ|GNub<=S z+xHSnKdHISqyG+l%S*2K1uzKZCg1sEJcc58b~fsWM>*ht(@x%wZfVoGwn}Tdfm}+X z!hu1?O?OS<82O%Myng0m7^hX+f&3`;gQE$m)WaIGB|NIZ1%GZJ#l_qwwZ~~51r4Om zT^a?2Q}`)PyoaEf_KxZx_3^xF*e`&J>AyOVJ`jk;d}ZTJ`r-kceq1W=uDSYVi5j`w zKLQ@&f%bYGcgIWqj+t~kBqI_u^W&t%HE9*YVeZf=8t0pzYoB-PUtz|@HNS5myC+9_xhiv6J) z*5u^Ay|Lqm1PkAs(b2o(L$9q(wO2xdLy>F1D*v=6YN7=Fp!}*Zm;Jg-7Z{%+oVTF{ z4{Wm=v2lXJxFDatnhv9m%#0!{EL7;RTucPj%2@teoO5--@9B?<3drffU{k z;L;K|e;~$;ongLfF##kS4EPFn{eJ#G80vX`U`a_e`N*{}q@xU&B3Ulgm-A2AS`Q5& zK=WPpiBXLWnJzF)DGHydf95q7mLQ?6l53@@42`hi!{F!ht=a?RxRrrwF!@OP`L=F< zH@1sZ{Wr#aZP*${)hyD<#kS*ynGE50mL}Bl6~sMsN)IFvhA*QuNa%h#w*0egS9pT~ z#^zX2>`XEfnhp(3=%dT{e`T3SCAv7YJqh+Mk@V9=;|x$OiE(wza*D{{mOXDl$Bzh6 z8l%`hl?}%u_S7-K@FYRDK_bG}4>GZ3vKM{Q* z^fQ>ml_VDH@%v?04EDz#+52vsQ_g?ur5#Z`!8hYEcCTe0dYz5*m*Z?V{H7_ zj5FTlw>nDL_)Tnde2=CzEb&63!q37!y)>Jl_uKR#kjFQU~v<77oi!nJY zSVWBryM1(cvA*|R)&@GangQ}FGmc`i*mV!@ctU{vL{!cxQDFk;z!}b>L!Ki0@pXU5 zeh33JT3p1kIitRfAM04&`B)Cg$=4+i!)#t5vrLP_)*tq?+o(Upk}d<#`^5oXY{Q$H z2Bojh2)&0oyFQfoOC$6#S5>HDKCa7dnJrv(H=Xul+`Em$Mu>A*6fbs}{NwejVV+=C zi6AY?u^`WsvPS*NwsBmx@^au}e*}MSlz5H|H4Pv#Gy3AhbnM^eVAaE>=_A;ahU%=N zggjEY`;cFNsUP#_Qh&$4oK~$4AMO?CeBI9kO!9|qB)+L`TlbHYzH>zuo#{tKCH3dr z(imWWZV&$$IoOK^$^A;ru*3B~If}2>J_|IT&@M83?%(N3c~`tE*7}a7yNr5=&hFeul>`cio)zY&SJ_Yy<@AO!T}7=f zq80diwN`$n(KAPqd6criJI^-;qQoIo?^!3&@Wt;th}iyvI)5Bq%(03`SpImd^&a9& zKw{|5hT@d?vpJ4L0$|&4`Z zzqChg@qO>w82R-Kg`K421A|PzP@O>OM(AK>#>d$=lpo$x{RjdEU~tUjSIABMmmu=zwF z9r4~SdEL>F)=i97Pdw&&K{_&ZAMEji>AS~sqNTW|*AgRw)8q2o#T?H=JGn&_84w$- z!s>b^h}Pv;XY7oZ1;y@OlE?RtMTzxC!05o;CpoDv-jZq^yPU7eJZn-vz>o^e(rc+y zoC58F(Z$%iq|D%0XXH& zWLPYoHwmaagEHtThg@8zH!j94N1V^u0$#!>Ft0k8Q~ECk#infFfjwTTYJS8wVP3k$ zH*;sbD|s#&BH2zR>5(=f3g z-x+?vqq~HNl(f@Y(N82T4Pi+?7E|IGe(Zt9ibkEoNDhjp#;2K+U}y-BG2(+=8R z(5O>ne5NZmhRRE9jnqXFC*@XN&VH8~8=&~gFFooiM`1IZGs%aBjp>f9+o7Wi(M<4y z9=+r|B&9|lEVfWsKw7#DEfFJGocoy_=u_Jma4@d_co9>}AYF+;ASdf~@i$J;*-!3wQcvRr3!)Tkf4f^Q`D0%op@J_ zN`9w8-K_?ENfxxt{62kjPX`rX23}A_S-lSPIKhPZ>ClI013^WM!^a)M|Lkw1LnDhN z_JaHZL>?WCGYS8#JSz@pH4&VCS`wfe!e}P=t{ngn|5FEo^Z`j(@=ukNd3z8U6R}n7ZBZ4FW8ue}ZlpDnY>Jwe^T#C5w?Y+%s>#1xVhD3< z-s)PmPU?D&fW;~D8pE{*Go#dA_8FeUuqWLluZu9F{bY zyBD>L2lT-)e*qBA#|3`ajhgz#oB&)%svLmu;`*ov@cBVS^4q;mFNc*Yivg|a#e7`3 zafl$_Lig+MP?%jJQLgWn_j07$axKg{g^3Ylv)7_A}Cn4~hPfS`a;h$F=Xdk-zZ+ z8+y1GZ0DE)7%p&-GF*%>JuKo>Wx8EO=oIlM6ytm~^>Y+VWT))8`H;V+@!jAvujuAk ztx}AE3WFoH=c}vSlNb-Iel%(MQmh6+ww$!TL3cM0BM1+CD~iXIZ5{UFiUhh=CKG|L zumVat6Cx#}v1zTyTyG+9nVGv^DCeB($@1Asaqt9h*7>IsPZIHLp=+H6^^uUb-h{}M z%yfm*_vlSwzaw8`S$nVagfm9ifq>P*X7EGcYPpZx*ZPsT3AiD;CjwL%-iL8dxl3dH zs&Zy#ri2QwyI0salef0_@9D%{?*8_a)c5)%?Q8J@Ox{1}CYj|RiFC;>->r#Il?o;L z=EPrjFWo({uOYM8eM^;=_y_7f4LVQdZ+>?Adbx2k)J-(3a0wG*-r)dru{j4L_?ff% zQzVGFl*gq8?j09XBp6&6N!G&*t*UdHGvPgem^DS)xkk>6@u&1(t|>g~4R1H)?`Y9Z zd1s2nOQ~={AFbP1a2AY-8iTfm4@HbZIq#EVz)9EF=1Gh~@isj7p+~tUwucULFd|aC8d9U^|t85)r<&ffl;SP|7g<6wcGX-m; z09`gkuyJ>}EhL8JX*)R1lZVdBsDU{~SXXybiTF$Pzwlx7KYH)!c9KL;+K<>btdNhd z;u@*BlHRh{BB_+6h5Jk;>RXQc!UDQQ#j6ZX^m3}6>&((xHv z1@a4CQX!bHrao21TOW$QIk7^vF?9PVk5G}zSs#>8DWd=Q1u$nkorR8gW?KpyS4hwS z4Axa8?)tE^z~P3JhlPBg21F3kK6{9@&llq7u8!tkgkx?bx$?qLxKf*#|7$~{3*TAS zV~IvSV&kNcRgw!`OwGmCy?_4wgFO7C@kKu{nX4M?X4cD3GYjn=oWXeXXjQwBCq7AH zy(Dtc_J8M43SXr!hoOr;7%g@tX4YG1`tY&#JBGcK+9}(_P#USVK8E46(DUA8*=~`D z288??oBRbg3ch0Ibl>PD4kZ{euFls4SC;EV<}-VvF*2Wu zBth&$y6>iPhpO3$DGL3L2UvEF6*(<#EVuSq1iZUKD^NLs7S9sl`##14Se!|1{bjzg zzXkrTas7ycz>{vvR^*?3K9@1>WEiWi2Z?1W68qyIcx}YfW%kuw| zulf^zArw@8%=JCUI=WpayVgexdUQfK}@ChPr)mf-r>6+-BBv{DeW%8<+K zmwJ}BB>yfcS`u_qrN_G6exTKoEYY4QmH@^D;Q+oWMPDObXqp`W5WQssT~R2^n|?9D zz|$_B8K$sp9=?Y8FI&Z%{_7}M<3?k8Cmqkzvt;KKRTGfBS2>qAtEs6B4Iw7$8w(S; zOr0aA-*NEeQaRo=uD1LL)uIHc)8EuopJ#$j1#~y}72mbD$C5D+Z5WK=AeK7gGs_WL zg8}wXfKqlzb14?7pBhHTk)=X?7o$2R(SxR;iti-ODX>_)MR=@j?JsrDVhfuaPT`@5m@bNRGLv#T)%_)K*NJ@li7Ki>mZnxzTrN*-3M z&VbHwbE?7(Z3&4O+aD8A6!@H4it+qE*B4$9Y8Pvb!Ln~>SynxG87P4jp91kITRzvz zcB4V?x}Ej5NLXk(O6nsRh1vEFq17ZL<+7)qN9HC5hVu^qcc?ki;c1k}yHEb)2?jem zN<2wpl#wfqTCcjo+G)0qGx{ET=Zn1#+3P$cS|2&AKy%j7di>rRBoBmF14rx*CSW?h z08Z8JiyaI}-M$;gHihkG?wyFP%kr+c#nR3w)^T%ZfEO7H&=upl7(s<)rs%actIQFV zLI@Gc_!D*EE8=SVo_hp+py7`u&!;sp+jj5eiwpum}%>DIXILQ*V@MyTA4~xb#ewO#;;M?70sGQt@vRN8-R%xS$ zE-lVe$P0v%NYN&6M8ADb3YXP4aKOZeqfqYy*C9o#bzxY8xB!D{B{>gqDWNp{YjDL9 z2D)mNQL4J{G=-axAD)b=gCGgVI z8!Wt~t+U<=j(wMfDF1dG`qbtT9m6g4@KGy&a>g0Y6P%N(UTc9uagU%uGUu04RnY-V z+GTl17j9{1#RFnH2b zX5?yGXY!JA@!Fg$o_e(A{uvjI?fKa;g0eqa|qD9*;lu+XYsvlTENiIlOiMI%X@!x1`?U9`aIO|+6@WZ4JnH=KSG1+S35r zNY-Sfy+5_@nPb&TVC}GkfN~kzBR#_hmC#ewJvgtz?qk<5`z93(E7J)(E>}g0+=BcD zEl;;1+{7kgtO?6+oN-4LsnqmO1qsZ)IX`{Bqp8;t$zvA!JBFo`XXg+f{zqS-dr|ke z-_lPia&9!kCo-4=fHIocgEh`Itp^x?S_8Z0 z!*HaCmBNTTm8ZomH`xE>A9m{c!gww}z0!Ny_mxyO@cU~W&uC^`rx!O4wVf*&3|LA! zz67}E^zbZtjR*2-&zFhFn4j*m5Jv54K5d^@f7E4AN8`E`_@g9^{5q9Vf<%xS<1BBw zE9?1@*voFV%R#m%-GYPt3roX~%@iJEy-e=TpZ?UF-&^=rb#Lk`ib*vk;|il9L&PB@ zOo3)2#%M=^!C#V^uXUIB{I!i^ZI|3@E!12OyCVOZAA7PwBY5^!LS`sD;{hHRlf3Q2 zW;$xuwovOC3$#IK9Bmk~a8T(cF9phq6q@s8tbS|ocGLr#z7mxs@&XmFI&O?>o0<%` zrYqb`{j&V%#xVeQ7gpXWY(YMxL5j-9l%7|Z6gNl}lzA`Gm(vt%!pIR&DC}PJHsU6( zk~xdWTH|*H@Dxvd=AA3x_JY(*qoKHy6%kJI|7Og|@ynA}Y@Gc9JSJYUKS|`Xf79-4 zBWk3vniU4yKq<|fC%sbGT@6>{zc#en5=6c6&j}Nyn{i-0c8YIwYXc7~RG)EAp4l2T zZxl}wFXG}$J>8$? zj$R9o3~iQcQvcz#VGkllhBcX{IdCO!!}O@Ba?W z??aG%4Jm*A>-qnTXB&EjqhpV2Jg*D*T-UQdl|^kXmBQz2Fkq2nXOAA34w!`-+`>7I zHU16!qafPp|F%ezs41k(=q`8(mLfRZ#AK2*3i<^QthveNUE4H|`<=L4)7$T7UFl-} zPh0E4NlYt0082o$zdnH5cr}Ptx${}SA;dvA`-z+Ez`uGu|HuY^0jRAwv+T#arU-_F z6K_Fs!?_nYVJhrl5M-qV!8iN~BEr|a8N+pJiIFlM0sQ1rJu8U@zW{iybWRc>7Nfwl zMkiBqoaxvxf6Mt@q2)Auf&1NmQTR_s!M!xkE~F^^ezb$+JNYn&OCtXT9n%XTrSyB~ z;Oxpw@QNDP%~rzbMKz8^69~Gb(M8)H9Kz1dlixG^TcKQ9A30Gk>Y{f0yePGIB&_(2 zKEX(~h|P4c4L0OunZdexw94*)$WpOAbw@jVtDBf2TmDEw*bTgemL#rIABUGka(<4U z7$E@&mwD%Hl+29|^!Y!_LMQ)K6?)6K~=O~{T4$0~*)iHW&57NkB#csEkD9-n4PpkwF{=Y<6av#h$ntCTtmmA(_S z80q{Lr+pwH!_MIA>o#(4_5jwkHv?>Mog!2K-22dlbYJaiTJ`pl)6vK_V;=Z870)9M z-=IFz&P;xQBJ&IDdU=ZVgs9d}^i&pc{D+^)5D$OfZZuJcm?PTIPZ4j3X%#ZWCqr+< zimlQ7Au%-5r=txORyzj0Bn8>5O8=`W>_7d1Ebm=sPJ7#B8e*a{ZkzC4yO^KzALn2+ zu&25+ufmcXat6SUb_{Vj4!K7h+mE^rLpctV3Puvs2=@wbHZ>lPx~hhG5tPz_``8m# zn2(^by!f6Ol4^K!?WjZk)^Y6B*(J2j#Dp8{lHsIy{rLkp<#zQg(hg$UtB?Ti&8By=drv3rw!KE zYQK_(vM4DO+OVXst8!DqAG)xxpDrdHIx$5u#`ri13!o@8Zm!DIH>$VN(4$G>rjl&DP6t*N(DGrS+(iJ>rxQs8jOQ5#=ykq$a0N85pCodm zq7cp5`2k=T=gV7zf=mX`CUKRx3A=1nIyx+4ja4DP_XaKrke%oq%2^}C%d)pmyHB)K zHoz$@E$PB}Ia=T*w8Bi71loAv@Q*NNSdT2pwxRi1rK*`7}4ip0$CTC({pPYmV1u%h5jB0U)l& zFn@1=B>QUY#f)kXuK_nD?}80+LY9b3K)-5a!emp~n`zdz(_BH+(UE6KF5}tY71yV+NbB?B2TpY86g=Lbq;hFqQ+fjQ?@)E1e$NW zf_pU;s8%l~Yr7-DMO>zG1~)b1`9hF}G|LM^s*f;iQXs<~B`=#7KJ)&5C7$JGxFzjk zK9gL=9Kjz*&_e9^SYP7f)#I8a$HDo~M%V&x!lFh(N3uTUA%TZ*7#s{oUOh{V>@l1Z zt`Q9d6hDB3d&fNn;D^Zbia$>IoML#`vM8~k@8x{>Ud{%G8K2;hx5{vhB59$=7(V6V zggiQIv@K@(C+pzyr|#S6s@JHG1_oI)q`iEXShel)_VGATBM;V;bj|e|u_YHz$lFqq z2nGN|$g7;73Tm!I)c6yJj9r$cA-LH8N7sLVvlagR5iP6@o8XaaEt9DV;D2k%0R?WYDp6B`gpWpTU{alyiTuIKl&-;Fj z_x--lsa0J6_N3q0qU6ao=}$*_C`HvwX=?)|rmyVLSr1=neaixUS%!@3t}Q4xT|bjq zc}=lVVnvs1sWbTci@4?x!mQ3))7IA3UIwa68kwD&ZJZr$<4Z!`e72wgAr!6po!1<_ zC_3o<=Z8ztFR3`}S0vnSY5boW)T@mJ%H>SQ+NBcGklwlGKSiq&X znb1r>W*|T6>)d4gH*Sh`{qr2E`VlZMJ-@zmEBf#h>b0AaM1W2Du)9rYn32SulV+qf z@lbNcJ|@m*id{uuSR9e^M*2-gg3AwdmL(Va60Px-H=IbS#*M|aI#ac3JKl-Wt#P16 zFR?CAtXl4_y5{J7)0g50`H^+7?Yl#M688H{NhH-AtKS3H8$RTp*@Z?+laGj~q#25l z)5_#^5&1($$ZmqmBf+uM9wi!f zDx6I{Jz+n)^}Rdf=l(;B@5ev)o%*)^jPa;=HDs6eemzdn$o9L&pDU_dT-E19rQFMZ z`Bfm{@9nT~-kQ%s0w(;KCN1UaAD7s&!#m6}2(33bjQWS*i+?oNq=%i#(c?#rtyai$ zTv+-vh8DZUbd|z3uk$Z0EAd9uS3)zxTtgEzos@Aef8Lojz{Wv`j2_oQxuKQ+Z1TCA zyt5q=H~u2&fW1c{^xUUV12i?Fu|n+!0yc$dz4)I#b2PdQ)qI738LUQ0{?*-IOVqMI zr1Pg{BHKq&y)TRL8&`u9`v?n~l{aYkLx*qc`#jpz?zv4T`y-vIHscF45tQINuh=Z2 zMsp+c`_=10*5^$EHKqBM^<&Jm2J~I#Ow(f3{;UnmC+zhZC3l=NjnoE>|1*1pNyV&q zossVs*7r)=9ZzPipddZ|afOy=-{i-&tQ+sBI(GMj{25C4TbkmB4PXiQYh0K+n10s% z?luk8_v1)Yi1cE|qftT+4xq8PG`%MRPY_oXcXnAN+`gp9`!?nAnpabNZ@Gr`)sR!9 zC4czxY0K)0V#_Y;=J$=Mr(H%NDf801K~uj8iFJ3M+={;e{bbjfaq5xeRP(ptl?$Bd zgL!1r-}H%L{rXyDG#PtRYop<#zZ+sqEk5b&x6|+Q(pc-p9`a{K+&c}hcq{3dc=iCE zllgs0>xUK&s)Mb)TO(MU9o_Ks7N7x;K?E}Bh0|FaDrR2)&&_`gbc4KN#^2dCR+U|I zAvpEeQAcI-_rc!%%rIC?zfwc{@!7qJCHD<7tk-QRM*nHdPSYcGrfyz>FPtirs!9XB z_T#m6W4bgM$<41pD65-Xeues1nJC`ItU@lOnorkFRO)jB)z8#fh`j&KwO;O6h~wNP zOzIwRDW+bxQBX)-`B>_rKMLf&aHG`q-Vol6YfY`u1yN_DMOfUjaPrI6K9J-NvB(7b z^6OO-D~lex#ks#vOa|!Tz{gj@ytsA<&L*eRcNXsWhbNm|g^9w+=G^5 z)^5QuR#=Q)?h`|(#V@ijiI&Ga(+0NwMs^lW8&vSvxa-k_!TEYY0@B!mY7&7UA(2=e z)p4h5@wIFbT_hq+gWOQ+=%1|kyFS$tl}*owu3ajj>kRH=6xxU2*Z)g_RDYr3p+!HM z4kCHb86PW|6!h(DR#(4NErpGhFzC9-gzKlr1GnR@rbil!^4vkJzl1g6myMf~i*r2Vk+hfAP(H#K1}^~^4ZqL8QqDvIn)lqEwt|7UMVDi^52 z=G`s(QaPMOg|9AHJ^JfFC@%HuaYxyk{2V7L65?h^;+sXNMugdNALB3n$t;v4*P!GV z`@DDdH=CbZ@~jPPrjjyAhw(Ih>6cisj>%(&G!fMJa|D9HRudNhEp}u{`%#{AXksWx zzm9@xfGBalRJ&jNk+0gLlsAe?Z=(ZRG@!p)NFJ8QQ`8hYEe;GBb!oc4WqPH$q?yIb zYna8~W8GM$kpVU#+gla-HbP;)MB$R8w(y*c^TkBt6%4fTO617gtx&zZ0I~;9DENv@ zm4E*4bfJOTAK|j^O-%pp!GzEb&65pBxZuOyd*1KH)>5boGX5MXYorxsYsQkq7W-Jg z5tgR8g8NlFuXi@658cHqbhWxe{-=2>c8W9i>L}1ob@ZsOvZtd1Xr&CilF1Y%)ZaQ8 z;n2^jv|O#--lU;nyqseuJCA>}MY! z3l}W%xf~u~rqvt(A*MTB;Ov&&*8YU@dM2?r+a%)n++#Ql26=0gnF=Q;Av}Jfc_LN(l&BtlZU?m#AZ19Z?dD>CyRU_DyfTM}j$73+;GX8mVc;V6 zx&rox|L2*e?uQAQmR_3a?cZC{-GZOr=Ls0V6+Cg~#JQjCskaxZXR?kS+v165Lbr^1>7^U^d%s5V)*L-@76Fr|KWmIuKWoSe`f>XmUp%+3 zuC$jfOY5^$O0MI`Gl7~p$IjnYXKE5flK4nF$2;+C4hl}cVCqXO=vij8KPJ5AL2tTX zf_2}e#Dh@#w&Z*gznd3Ic#ha%Wi0;HL&6b(z425`O5xum;)sd%j;IA0LRewX-Q0QekL{d;xZZ}Y?Wg0MN)ERDY zj{93sW*KiJR?jxjlPg5uO>O6hGE5jr{L>+nMI%L1iM#+T{?}$|YzLcvvM}EtCO@8Z z7&~+Pzs1fd#NvRYaO6(Fcl!dp!;kn@>X_uv|?oxS#bsGryIsY$kuZWQEG8N2<`*BJ^leMierY1lY*cw_t6SCK6r zJg=Xsdt`}lXRXJ}a`PvGFBj0&R0}Ok~3rR&JYfse&*}jM%Fa15hQFW)&{ZG=bLxQhjVY^R;}c=if>m}KieNm z%D6kT`VqmZ)WL76E3VwV#5YCtwk(cxO^qAd8oxV12@Uovd)n(xb@WJ`%I$-0IbA;L z7fpg>Wdfm%mG)aJ$+B`eLhfavM1Wa4AaTpMjJ2BQqf79JW9b1&8iSDKa%$3)o4H(p z*TE0A6hkBB`T?(?D8KUw+8bM?BhRb)v=g3Y>dAyjJHMqjkG6~klF3LOGhKaQ`mcr4 z@+Y6{kI(kave_Dk)7;di9e8J-S(M4&nfTfz!JqbT_``1ODT8j|=)>}t)j841kMB#9 z*1AMIWxbYeK2dNGnmO?4r02H5TM(j#@@(U{*4&rZk^%Cia#F*-lC;NFVL z7d+yYTk*a#sgGEG^{1M`2{~*j+bIHuNo#sjxI9tOGe3F?FM=R0m|lw$JM>@KfPQ+3-lBPsK#cfnA%7vGY-yK1v3J^ zC)X7!eiW#Avt}EKzgAf0FDZRnI=BtUx; z*pYWR^=zK!{T@IQCqkVqY!twZ9U@Y39@&5hP$;6&j1v_xZfJB&72hFzah zCfX;i~{ zug4#_RX6oT@2UQ}8{pHjG$rHaJS>>SX!HOi0>Pc_WeUO>n(xBUWa}yFI`pW5B-p0!d z;GWt^gaDn^QUI8f#q9Xn5nJe;p?Ld0CV`-m*_KH z{yy6~camQ$2D6;Uy_#u#0sJ~)VLxBMuDQ3HWoAjJz3d~SQXG64Ht&*k{zy3q=dq^y zvQO5dq-g1Vc{~6{Ml78~IZmBWJ$}kScJdV4Jh+OJ4p6m|h@)0`KDKkrehrY(8v zu~4+%`Gd@2h{IF-KqkhzZXO64)a(^Jl~CQDzY%8Q_~V$pMsZe~vjdJ$K0HRw`G?P? z-B4Y}P~fF#?!^xM)YxW!Y-oc0b*efOv8J|dQ{h+rA4y?d zQL(dma_{WfnMAeB%4DwHtp|bs*PCG-Ms>U9t9W9nUMl$5Yk*{q)A+ah?*mq*3*f1m z%?`75QaC5hM&tU%_SfDIzn?0}{OU4AWPI|S?}~qWMoo*q+9G%+Rf4|&NQC%lD=q-Y zv+G=QV`mad^3Jvi0AsrH^&1Xm$Y65(OREIcXtfF46epek)hh$9d>b;JFB{!%EE)58 z7n;R1?S*@Emt`@db0vXWnwHw~EZ;?p3^HPe1o_eqP*faQNB!PX%#(LqKss`zJbr!e zvjh`YQ2&>xc2{ocP@`)lt~y#!HFtl}B)b6{ppvVR#Ko}7^p>^-Qm;2M_H0cteTg!C zETL#)&^vfg?a^5e>xhHs6gixeQ+Y8mO6P7_z*nK<8E3(5H>pJU&0{h5=(JgI_yEpY^EISK8{PaW@_o3*9yIJ6?<XGdFG zuQS5ZuXtQxCHuQ!zRcPlDP=eu7)H6)p?CFUI9b=febBijX4}-ibjgdXK8tjupFwW$ zs?eZX0+PXUEn?K}0%)%ziRMC%KLL6ut|hF-jxNpY&vC@)74FeBzuZE1e{sHCq-1R6 z|NY@)2p&7OL55ddkG?Br+qjr-8eDxC`}WVZf~J(K`ZFz}H^h?iSXi7BTb8&7o6rki zY57k;6ggA!KIx6QJ)PUS!c!N3q2&cIqQ^9w_b!n9j}Wvu?5~OYhwv~l250&3I|e;J z%t6l%r3*gnu&7x&COnr$Fz@tzZHwTF5zBcIT)YHFN~EPP)K;Hg@q8V3Lu$vkBR$Yg z=JZ*Lp~OlW@=!WAvgw7d{!Po09Xq!ANVSeo`zS5z_hPDvcb3dDW(V!w-WYCflbsum zdcEMInt-Y<>FKY9pI_UvrH-w=uT4(*iuA#+zc!!lQ`gQrnDpsJ-||zGaUuRh#v$mn zlw23~C8sR$jbc@dXKKb38#KWj>R;~DK}U^(^AD18+mDIVyO{T};#R4m4V{_nHS*5s zpFmeDsRzua8p6}_%LQewxLb(Mw6Zqb^bE+dW8Qfya^=0%qwzcw=Gg6gT)Jdg{4V!3 z{udF`g|cR!ez|P+etgpIQG6>2UGe+A-TCCQtkSUmIt(7e+@$n(>J?qMw_E0Y1ovJN zYthcQExXuNN$W&^XlUZV4aWw!?d1#8;3()D7pi>7M)b_rv$_NRm+aarQ}V;A+jiH502Yu z{LawmM};_nkiyeXGPL9zY+UFl327;ndHN-68@)Z=?*h2noKdoaJnd17M4tz{Ww>w% z|MmFtrp;N|rcryNx90n3U`I)D<s~)_zKG&J}_?XRNucHIl}50zgIyE1?~y7?)4 ztHNaY+-d15t;zJi9)?=V$6!eY{!lnM*h`R3Ia^VA=^aBhl=G|?@q{VWu_%9+XTm1- z{;wKC9bePavnTAr^Nj>;ZVQ|E#1DjbZE^@uZ9t$^=Ip}VkI!%OP%_kB02(Vd-HbAT zqVZ3CSzoPTxn_^Tzu(qUc1($DH`e#nE5BuoS>cd8NXz+z>6Yg-kL13)GT(b$AUQi7 zD$@x2Lu-$VB;ruJuUz~l4pJVE53VGXZ-9@bbtY0o!sxa;xE;p_zUZ7pr5 zPFVIm7(5$6Z|9(iWEu|dS_luLghlTg2wy|ZC6!X3k!j?=mFO6!+xJQv4?vO)J5oyR zBJF+~kZa0xX?H-eO8Y7+Y^EVd^5z%W^lB7hqdkbL3mKRtgnp49EmgALO%cYJ?w2WOD;G((vwv5n8jZY8+gtpdlZRx+?L@nu zyCyH-i=PeYLekSFTlgrFN`70kvcpNs_!E&PUGf)E;SSHEly?7|hL`b&pmLH7j%vUx zsY6;lkLRv2CLfu8BYhaw@Nt?Z7wioi#_(?b)ozN|^YUz?!>B!$G7l;U_!RB%$(eMD zYV)|w<0>gHO;Il!&h~qG0kYXB%12pZFwx71gUp=6#7UJ@m2C1r7wJ#_JiK(%_?C<_ z$dO$sM05PN8&*Ghf7dLxR~qf^{9H-jCk97`)h(k~90f6V3s9Moiy86{17Uudb$|s~ z_jq|Ryk|#s(wRfwEeBmnv9#EIpwz@e?)cX>GM%cH+43a1DR`Yzi)x7N=T*_HABzo? z_2o~baZK1-s#QiPl9gwdx(!M82zibsC-wdM=8#VA*T-Xp2W_su(M{UJ&PFcH77-$8=IgUdc04NaA&-0Wwxd4E=XWp$ewvKeQ^!?_bffE`-#n zUtu=2k!2=TYLRA*rK=1vjZ9`MWbO{TMkNR=$-);d08~gsVxw2{ZI|VyptEtG@ugvE z-KC@M4h$r2F4gwU&tYBh3jl5r?d(E3yv4z{}Psf!`^*t>6Ie_shwO)pj7Eux0QL4WQX$?Xo0$DMI z3O4UM(wq^{dko>*s`ks=ypABn`$3TX{l+p- zjfV;3YH+Lb3?^F2cEusDlWgw`prSB2tV?sEcu3Iwq2wrXgX>AMXYO=@h;E~k=l9z1 zsc9~r5^lF-`0u;Gb2H$0%;P9sW%mP~O-RW6(K4QGZUkylK&mn|t1IS_$E zB9Y+A93eGHe+rXNAKv*>sHeBLi&<^Jr;tM=tWy!!rY9FjT)xluT((9nghXuk&N4?T z#E-%r*(Vn<_AvojC`Y{HhcbPTpURiMXH~L22TqUCJoP4pR;+Ot_>C92H$druOw_dN z`s6*&g({64ZMqvl?(7oNw}olPJyN2&NByXjMAfuNu9Levj{+9Kcq?-1Cqh&6B_e9H z*2c>3+u45hdcLXRWe9lzCb6sN^Z*J|U|W?>^5-XuEx3kwvP-oWrj3ya#JYp71d)wSu&OCJGNKbz z$ii)8%K&6NH|h8G@1Qi!i~|#pTPla2y*R=5Dq53E*^U-MHe}~?Nvw!s)t{f=&@@i4%!&WljU)BaaW z#3GtB?Xwa0dOKx7P6RA&b+=;UI}b0F77MYE@e@m`!l%8+A8mK;C7zsT-S?Ral-fYZ@=$JKY$SFFaB#XUAu=hD;Xa;y(%KogpFxcg?iChs> zN2(@b1fBJ%@`*S|Ja>0EU0;oPc{&yMK754avPGF3>(JN>0FQJJgy+6tPN$-2^h&7T z+GplG+QXSXi_N ze2kmwZu4YKc=YCNw}T!COa^D#v*XWUe{cbSQ6I;9(ps+TH1X7vU-hhT>#l=UrdsQ5 zQA#fjuW&ndXE_R8MbZ84q_)4yK{hNJ8*Z?{dYff#iThP9ZD+E6aK08YP8#Y-(cKeF z$~Z%nR`aKa14BcCY9e(?fAQvSfMN{4`3^mEWF$?FPns9Lv((+VZr}2YY;nys+5su% zDI|puw907rduDyp@B2a~jBXI*6b*igu5yPYlcAtA$VP1Lr9_c0c>HTzw-47jtDEkc z_75PB>+Fb&Z9n2pd$0VX+cYtrqU?1#*oEYZhS)G=4#UsFSB!@WRUj{(+2=?-vy4!hd5_k~^MSvy`KIxmrc1ncyvt&(9*YO4bnL&FJW@D1n9&|M zap*h#!y&QqN}7kb8?JRl7cjXaxY(4^==qVjOKYg$A%WpSc3?=7nWw&KLpoTcuMX^T zxLvNe3zBWc6#*ZQ6W=Na7TPp=75A%=a-9VT-ryhpE?p;Nkvk1!rU)WFYFvY5ca9pLn%coa){>l=VnK%B6<##`;{Vn zjoYQE8@k9#9!fcS|B~Jw$xrRmJ}JK@SfYqWl9!luPf;|BXaVOuS-ZKhJdzsnq_| zb5LDC@hvE8(@tlCA`v+ zNTPYuqmiwU(rAVa#9?@s>P%aE#?ALv?aoPX^)-LN$dyP!Ni^5+Dk8UENZ3uk-oJw1 zKvt4bot*OC+z=TRSa3I}!UiLvE)$*-wbTuVkfWvhge(ZZQ&T>bVK9DNakXDGR-(-p z!O5_sax~LBwfH@w?v57^CDnNdB^g_}GcE?S3m8Kn$cR@cxixhp)N5*n^~$86+2m_~KO3#*RQi3Sr@m3s08(KM zjpx`fy_e`7vcBT~dKy~lPRgVlQ>Vi?H9$h_^;x9q(f_HK$j@MuZqM34j%)3~lO5v8&=9rQlBZ>4##q`!Ea+$*|krT&Gs ztnJEe$G3qpUf&Q#CZ;h*)pCt9jE(J|rNg|@5RtbN%8cXZY9|vmQ z!?~9~7WK=!vKd=#s-{m;cP@v$H~eTa`Q`U(-Ku5|9|hl6eBkugB63mK%kpic5tcu( z9Y64CORd|NiG2JdQA|wT3vRnqUO<-F$kY8>`2twG@zCo0)m~K|AIXoRAXbO(y#hmz z$<>XD*8z|>Db|vZ(x?DN+9RGxHX-{s)#hYyK$ZTZFJg7lg&+1NmEMY~sgd@*&b`zY zzr*jwSoGy*3zo^A-qsL;Zc^W{8V^?&cQs`VrV_gv^fc(!Qeg){qed|?(Np!=eY5SW zkIJ7GvBrF4LhKhbFL~)F;r5mFt{TA)pzjJ~s-8?b5wOK*hPwm6caoYV~Zjr?eIlxA8yK{>nN%V0G zOLeehuDj z@uU1+IJzDpac2=Pdg^6WiGiEkcU<6S0%$e7{f!-Z6%u>&aYd^kShKp#RvZA9Sk7=< zfM4>2M%JNqB1s=T`y-{euQJjKWLEpjNg%XU^>~nS)Np(sXqsAtHJeAaLbB!~XSCOj z3I}reCBj5UYPTdBE%{i>B#`#T0XIt?dFcGIK(ZmctBV8 zr>}1Pv!Tp=pz;^zcsg2Hen_1;{==@mxMP0ztiW#9E4(2oi|O&RckxzVb6%_93gY+j ze|l;&anM`wIHq^dbR`_h7Dv+2Z5>7TdvJ*V?EWpEqDnV5>FX``saEbgC~AxH5&9?D zN>^<;h5VUz$BJs1WN+uak3yKB0`2)^Q~YRJM@f} z{3ue}BDgt^u8nDcyIYW`mohhvHlkrg16aQc6y4+Ce{k>XgLvV;DBVZyK`C|Nd2%sG z_K5?!TGeJ&3bpa@q91s2W;LW4))L=gCCazlOd*p~2Y-|eK3brTmK^*oH_^YZQaa#o z5Zh!_vWjY^A)GU)(J!FIK+evM^6{jdT~U6J$^}_w1P8Mk<7AQ0^8-faH@AnfNA~=c zIz1oL=r_~${pM3yQkKo*>Fc_FK>W_?9AY1lv5ky^kCJ%*mB z)%H(+DHLi+&Gz&ZhviILF?nfN!76pkrghzN5#=XnaZu+)V56@4MbPmy|3*hd6h#{XWZ)QC%w`%yIvLI*K zQW=V>8mpq7b+_!NzFRfd{Bj_(wo9iz?X3qP+n1ewDRi~#eeo&CH~sm}GR9M9{F1D1 zW5{YL9#V7-cv5uw-}bJk_hFv$%XgW$tt1HAm%)k536a*ddyoXq$#OT7*TQ>bg&K{b zWKsf3_T0WDA5n%k5k^6K?BUA$b{y<4eJwIKZGon+>RzVQCjL3S->j>442BA}2 zLvZQ+l^9VG_0q~d#K*6;yy?q~&MU?ayByL01qjW4wT8W~h|e{=UE}eLNvkV431Z03 zrEm_E8v1UF)&Xbkl^3c2mYAk#^z8bvdoaYwM<}*7G~uaSoI=F#(=*4pFhmU5-nN8D z(8E|7ZhK48B5%gTYj=Ku5R~w~O0@&@t~XY5Lc*Ok_?G4098T65O(sMZXc#Llqdeu$ zH1L3EFe!}|Sr{M|{?c9a8lu z-UH+mKQI+*;@$NMIoaW)J6JWFfx6x4^ks4jB+VF-IehLHZ4{488;u28aIn(eg1*F z#dk7-%IDk+k4$z8gJ$8J92__jut7L$HqlcaJ=4}6PIa!h*awW0>HeTe;+~2o8D3JX zcVO?Hf|QdNWj#6C?PZ+~ruDPxSfa4(@N%?~6-DvDF0IZhw-Bu_!jT+q)A52cp5lA} z72T6NZ}6prqANd_AK*hYzd%vlW`?C2ncW96v>KkeuN7p(m3{r&xAW3bp{G%Z7Kd3X z3>t+JD*aR{kfEm5E_7s5LZ#Em?vx?+M4HDfh!gkRmcEfRX^C71Z*`v}>Ww9}>Dmhv z>;P)$5Z|kilI|awOp;SOo#r{19sZ<=&WHO-(~Tyss*Zj>a+KolzwyEk%Xi(P3w5Sm zXDv&-Rp%^l8p{lpIf%`erD-v-q;!aG*M;M)XrNBcUgVS zCM)#Xtfo{$0*3kiecrS$MhYe2rF?2`w7?q^=9h6B46CI@lJ(4dpBm72r2u)-`9w#q z@4JqhFwW>pIICrWn2%dJ(AR|~kL2+o^)@x&OF$uD{0;Y5@vZ3q>Biyj1uFXGf{umN z5CxSFhuroD-O%>~CycrJ&?Yt+&b#6k%C@4~#NA(Y;ia3mgKBNHOT`jLagAv?yNy;& zx9iGT>vTif8!zEwsjoAz!!uQd$LOQ_qt{f5kw0SggoV(QpMlA3VBc`|&?&Jiz^o_LU_%H(u*g zTyj`Nob5xt#MaM@ALz%Y5D_-R@m6GQH@?W=j&p|?Jyhb3A8^nes@Rh~nmTCjz-HRt zyxJ%*QVS8jDhKJ6>kd?*{>ees4_*k9q*e|+&*RT2=8(t-4?Y%rWq^14_*M2!^EAdS z7UroI6;p!&cvrp;$f^w>r9A|@Fgb3Kc9A7{JtOAiK>7lpEL0AFmWz?c3Wf1;O_yr$ zFPB=cf(lh4SZbl;XHwLsc4=*JiDyA<7pl@f-YjdYDJA0pR%7v?;EB(k`YXp4z*d_x z`{n}??;DJwGdknxBr4P!VWc(kM>||!2mIaDcA44(@xy?m z$24icLQxZ!w#&-vc#Q9aGFs&MwJVQywbWSL$R7=C-PP`>-*H8BXgUTLQg$}6+9zP> z?bE888ph=d1#!L*WIu^S`N7fhNn}dwWsd;Sf4u-Gy@~~>C`iY@{A)ck+Roo=Nrmr( zggek?=`)hOg=)R(rcljM^M)ON$2`6+Pd!uu;~Dwu^Wv&lKdSQeW&nz_q(2laGI)wP=bL_NUd)WJOV5sxr^YOEA%J zNl?}TGtO*syY7}x>ur;I^_1BH5^f#7OFYzo^t9=9De2!zCO$^2nN~Lo3P06*$ZH4V zi)bLtah-gDLS;sdsg*O%;~=z!PzkQ8|2eO$f%PS|)(KHX$#*YTh-V#~eGwrYY2d<# zr&zW5?MyH4YKBPe-VUB#4tw_d1Ks3D;D)pk#RN7~lBYQzwlf=H5rnx8uP88b0m@|A z$E6O0)u@bWe-gh4h%7-Lb@?WQr=%CHt9Iz-lBG8e z{qdfRBHYg?f(v=Ny&T{#qRHMCgay;YE>9*?lP)!qaYsIGGvG89teD8h{+y5eHPX#n z(QFK7j+W{{pedtyblg;wNE0wxz3~hw(4Hw1ds{YH9yl$igYS;?o~Lu@KG}hkNhCOT zgosSeIj65NClud!uahoGtJaIx@9AI_Qajo-QMc1Z7Q@Xinf$NdRRb|7`{Zy3xDp^| zO=$$%%Xme*+QR9IIqt6HyWP9aH5(AiLg0U#%|2aluG$xv_>zFyFRy`*vSBlskE+`Msd8obOM5Rv;PEvP z?5Fl9gciq(zM)sn^beINzMxuGtv0@%!Ze}JYwOIf|DFb-A;=}}qDwGcf}Y-Pr?7hE zi6)~r=IMxw9x?N^$NQZkHPZc!)rgln-n=P!fW&R+w*yRB*`BEefvUDn6MbJ*Mw%of z=t%(FCOKI**!c>t+w3LwXXu$~b3L*98lnJpJ3!lW%EaE)t1pRNZS=4|0r&Heh?P-k zWHvud`3?FgzYmy38;Z)e1Ip3@gX0?{??-ln>-$_on&@Ri(rf(xYL{}mde+7zA)@!K z68~kc{*gccl0X*idp`3$wFx^_s`;9VByEw*JPg&l|B+P6npDEMjkXhZTSmkZ=LbP1 zEWaH6VJ6PC3GoG*zlB4G!UAql{@h#&KJwHc$g9-y*|dBC{a%n|$+D9WUv`z|Czp?> zIuaaev0{VW+abrP;jG?bMW-yqDmM@mUnFK1tDsRHM-Ke8-MmAkk7o=9sm|el(`rK< zJ4a}w5=jRglen%6zLvXn-RwCJ^|47ySUzVD4h1KpDA5@U=+zw$k;Q09cj%fc3$m^= zS{0R`iuMK$EE+i6PIUV1)2<{WOGaJ7PfGd;8NW&6G>J;jdg+Wtw!qM*22F}`ebbUi z;to9keS zQ{nQoAwckYI8=_dR{2 zX#2-E`}3Gw7dzw&`RrvN$A#jRfRN^+l3vZ?S}hLxVB09SFUru2j76Y1YFh8EHHYR` z9%uz%t)yL|-R%ym)veqb{EgZ)bIYgwD$8E$Q<1Y&an@=^ z>c_IUX?{r3bfnHhSOl4@?bxK9^8TifZc zy!3-4MI=I3Sxm_vVp)TP{WeE#C>J5Q_$6Ght+l4G1G-wLsdR{nql77Qa~s!B_aYGc zfU4zl_ap)U`hAD+Jn%V_n`g`MO_8!nK04=OD)&dMLPqGFF!j4R**Dmy60E{Ym(#=_ zh2;?aX44SywF;>Q+8lq)9zUA)EOnL?P=@ukok*J(j?_fb+R7+aQlHCx2kIGmd!GzC z`Y$w<%hJWIoaOG6jD*xJa6Uiyj`iP<_)7hTk$zT%!`gz@vI}Z*08>D$zlZE8x9L=& zUVE5ey_CuNqYAddsX!XXOASIwg6E&00v^laLk)Uue@lo(Weqc%-}G%X6yRgm<~X9O z@vej2H&obgBH55mz|&{2&Bhj@$nRtf5&^f{`0mRnoc) zY0jncyYgF$vdRul8|V3EX2S#5B_%%=SsASi2%ib0`83{H*g(y4phfB}FE{BC6$($dzBAHmf3@oi`Ildn zNkmx)9cy|O_lGHLv2HI-9HeC(27!(O9>oo;?4B^!FN|HOtO9Vsd(Rpp+z5cIK6?_r zIe|b@x@niE+_*QbkvXkh(w=UnhgMUn9AX<_@@bGAIf$*yQfF@djBWCeZG7%0?~@wX zbt!(9=T57w1qJIi^V-Sw+$smYKj&xU5dNE6e`F}suR^4eau3E?l|^NfUvgK}6v;;U zC;DIVy-puV0Z(X#5-ILTp899Iycy2rM?URw{LcLAB}l$8#imYN5&IYV>%q@LH^L;l z>PGg{YIyU<9PDKwKyW<$LW*N@7Ng}7y!2Xo$hMXV{gUc-lK`Iw;%31oP6zm09U4uu zb6#xQu@KemNil*C!ny<@rx+*k51VVX6kAIvvi}pr)ds&^T6h2ykb}qED(p;G0nj=WE~VlKUqFyzd{MvT*gy)v~!bH zLDHQ?BvpJrhdt_j_tR>+#(!k8uRzg{N7ZMA8;1X@;Gs4zqQ=_m3Kd!W!XV z`x5!{=!#d)n!3H5+&@trH(*lSZK74T-@%rI>WFecx~y17j@+lW5T$a*kt5jaL{&4~ zh>_VM;j&>F2`uzCg+wX1Q+y#3)WUPwIL0jK*JlsUH62VgU0JVz^9oZL+o|hsk`viT zr512gZbwddU;6>J#89;?M<|Sz&s6BO>lbs$T3P{7?+=fKgtcoSWM0o#t?MP1LE=c@ zz1o0R5x-gu71=I`SF{GePLYHP@LG|QQ32*1VB{*J^GaBzn5=%@qqwiK!>bl=s@m!E z9dLA}KosIxTy&2|Xc~z!10`*14<{Oc%$TTxdGt*4(89Jm05u4S1Kz^!P5t>xrr@uK z52=OII>g~UKTX?Cn@#4Xsr3|pAN9i(X}^C)_sG#8BQxX$m8nnMQJ-7L`oDp6$X>^R z_&y0>h7G`hz(-4;4(~OiPm^~l8IuYsFM#5~&zd5J#y50R5fRK{)l3r~iXz~5*g!Z{ zguvJZ(2!>ZGG9La01Z07I@G6W?wXw35S?=YY%{7|01Wo08lT1NF92c$LlZpR1X(OU zS#<#%IM_!aU4(zleV}SNzuKFlwfT82k=^GcBH`{w&>b|KfhwPy$h|ZCkn;x%U~;VV zmS=I%k8x+yk$^q5d`eYccRQ7AbN=9R`?5<&N$Cu7@jr}_r+2d@d^&qhMJ%XPw|0@f zYrVg_+I^0OE`UdXYK+YVQ1d!$Irm&V9}LXaQIdfCY&ykVZ~Wp0dg^(pWkX=Cwf0(S z>DP9yvBoA}&A9u7&fx+6U4pDtwklnmboJsBbp;!T95SX-XWVTIFDI&%8y}Wzml?bI z;HP+rh;^0qQ`;4MiO$`xE+(uSS8oa@7F%ZZaM(1>WTnwv0Dp=cvuG8BC$kTX4U6Lu zwrx_}%AUXaqWya_d9cnen8s^;R8;(u{EjNV z(A3~h1Kl&U);oeQB2QJ*D;2FNb7jk4;5zs8{8QFQx}d6hx3d zLY6b89cQ0k0*{NtIp31{ts_sR<1nE9oFw6nx%1QoaDONbb3ItFS8HJS>4%u|2SpMZ zC+#~&1yK$^#+8m!RHG;85(H(=+Y~JPzWSFxg+&$C_k5t2=KqDLVWiYdd^ZcF~iM>b-vkn$w~LpaFV_< z%xbU1_~YCeY5D`OlbdA6`{o_cWftWdx=>fR0LO>U*p~(s^}M@V&|!}p&R$rMXgx5Y zKFJSX40&-YmvaPBW_HU%hI+Q%DS>P20?3u9JT;@zINlB5NpqUoV`P+zh`Wv?Pf&#v z7_0}NGqT6AR=J~weT?>y)kV@9TM(@>X&yY9psE+xGS|OXM3nOESY)jeCv=cT3StaO z_U=swBCOIM9;u5!_|O|Dn*P*V9P)^m>F7?`{6D-@ak7uYyIP%(+LpTIP@TRn7t)*^ z#?o&ww*?f-lPDPefqqR0$FK%JQuBKOYyaSo7`w(@+ebGx2dCIM;2d*JwqkJ-MAl~V zvNLwi5{l&@iSpiaeotpi zsF~RI2#Wb?)D}zGaUxdSdy*Lk`m2weeYgm&ZaGF7lNEa6I(Z-HX!N$B{Gr9ftm-&) zUdyP*_?W8~3O}N$vx;2T{IAnR`SVu{o}E`#zxs=?BQAeV6j(^@Q@BmOmMDDt^ooof`D$N~)v7%Di1HRG>k|^+H&b{d{V4mlfP`#sYiv)|xw?zAy zVe@g{yUsr2-(~b_;l2C#fYjU4rkeT8a{+PwbMn=1S_j*YWBj!UfKn^TYNA2VocGL8 zp~gLQt93IscHyhNcdA~W<1=Ti?kunOl!KhvCTQL5L>_x=N2Hli(~$sM@^kB>pAj$_ zHByh#Oa&gA^C8O)60w{2OJG*nx0$gVIQ={Yp|f|{8YJq4bWJFzn(Fkg?if3R-!ynq z1u0o5hvQmO&W5{$7_i78PwJ;7QC`wM^dKy?FLs0b{u4~MJEejVwB$;Nwpg-a0QP}d z#bJQC*WY*NoqQ77MlM{=Tp}gBn(0qpe&-)jzaK(y>{w}wA>rHB8pqo4)bdN7Wa7I_ zt)t?EvyU%~sM>~dkV8M>pAoPr(mIt&`5=0&@lY&T z`_V8q{2M-5NyVf-Q{mxR-z5YNM_~r!(cdMDFvs2(m&hH$^ORmzm2xxH;vXPRmVnv$ zG8NnIQ}C@r^^LQ05a-kz}->EPGp| z@0JE-W9p+r>fdyKSVo^|9y%&d$Dez*o8L&KMPUwQF|H`>eHh-r!aD*)737e7CRx z0beZk&ar0J>`((qblJhPHqXrsOYBd~YGtrma5H9wK&-hm=1H#+%$3by-*l4u*-W+tL zmF)I+{%dM4aUTutOdFi7Mb@2ctSM5Tn_o-Eyq?ch7tz?n83hF#R4Igw=t)f3<9JRmpZvULjnsV~_P^@l?z;uDw>=o0BXI|@zZ;JG{u*kf^u{-u5MkJKBv!b6 z)~fQv=%dywfjzyLR@2+g4S`!6c8_0}(429R`-@tg6AQjN;>em`ImOwViMs$$JZi@e z&*Ah*ESMAf{oKDnfjNm1A_zCP4NzKeSP_l>^g%C0)s_}bd%(={*O3sPeI0%^qnpcL ziy!;tk=Z#YKORze(>d7o>&HnKCG$%!%>(!_21Ra_)fc-7QFwQ%pIjo3lc#pwI@LLj zZ-*4*&!QaLVSUkiahRk!x|%GF6S)I|@*0!1wwuP@%_A$J2oRn~9<|qVegV8Sa#iKx z8__z-g+T5bqBID_wZbUQE>$ho!Dn+UFNIn2eD*Y_q1@|Bq=%+>R&tibw=rku2}|#( z&)qM8bpt0+copkYx*39Q3-ZNGz_m=6>+JOk&FRP$M9lD@ir0t8C!+bP@vNz^J{^lT zpF9QT`EIN|r-w(8U;D6giSe zNUEk>YFWm+I84;@}AoK7^ zn%=G^JAEG`jxTeA^z`|cANR#a>1RXP;IxKcv7S1H#eED4tqPIEs}t!69%dlboIVdw zQ`J+DW#0 zWmPQ)KN}`Wu>tI(=?d~y672Ln>PBti!#Q_rv9F1!4LVSHq9paCEjBb$TMJ@DwlB>4 zd1dB4W%sgNZm-4_A%i#7)|!$|pC;jsaxbmwPc=?^&X2J64x}+hiGr+?{0?o=oqh@P z$8USmj#T_|U5pS_<@gSWR>#B|J?n3gQ+jt8QN<`}v`-{D+iuLR;Qa>{9e#hOLD&OH zXaAg|2+~TPJeVW53mI<=cMy7{m(4|HbncB>`33N1x^w-b=zEajr5=LPO@FpTXMat- zDt#^I2z8Tzj-0ho^XsiNus9gtu20oV`vx`V70P5>B`)XNRZCN-KssQ7(!}MP5ez+V zjA0Iv`yhyeOcP@!Dck0MA{m0f$}?+irlJ)Uz`b<=lUlVom8s;cOVuhH!75YP+Kkw4 zVIIH5te+F+BYiEr!J}2Bd>pybzmcPTSF>`D{|6>dlnBOjT~UqvyM@aw2Rfz?Fg?-z_aP*a z?eE7jW9;=-G=RGBCmHb)!NQVw(ikJXCW_56UqS20QHiCbSvm4fM(ZK6AaK+ zwZt^1qqQQ*zTL+ZfDpN8``aaOBY!$7k(u&iR6`7{SaQwW`C7L@&O!Rgv-rstzM}_H z`&8+3Qn4@io{l^!48o1-?slOl(>!pbowio>%Y8z_0D}rgWf4O&O7rXzC*J zA5FqGoAK9CHr|}jwIYwK56>*_?1zsekwHqsa!n4Vs(2pzXxa&8r=^#&~%2fu<$z5cG zqaYK`Gc8bz4tt6=$~dj9$StL8^ir6>^aJ)+K>2&b0zIV?Pc&QKnlhmD1m#cWz6OQK ziv5jZ)1W1vXOsJjux}?h&1xX?|674i1s!Vh;19(|75FkQuf^ed6vaz~qorUhf0tx- zwe&U7R+p-|H^!1a4^VO7MFcDNeIq@UsiGvSebo{Lg9sRupM`Toij3-mVFH}IIrHQ- z2IAUk{M@kzQTE2`j}9-f2WOPE842eZw~E^=k@yW`BGFw@uE7dSXmZE}n0yxi&y5gO zh-8#4&Yu{RXL>JVbVYRR=Y$!8ip~^nghBX^>L5=X+e|W?#q?p0x{Gs)3e0l~SQ&KY z^g++5V!+4BC^jNeGJKrI|Ni;#WlWF?w4oXRzX+HH-uWM_Dn_^8ElGYL2gDvZ4#(L< z8ya0oxENV7#EJwx@xzA+(2X8gF?&>u4dWAr0BxU32GB|r6gY&q>4?SMh9q4-cu4r_ zGk_QN$8xa!KF?tG)i&Di(PF+b+r$ishTSGK|ej~ECC4gqS*_SGIYbQpY;ejS4n~~-ua_x(bh1#vPvj~_s14>OtC8%ZC6d;-MN;*&ax=e-`@+l(hgYDyLmkk(dY$Gda5M*e$=k+M))#*J}=mV zjYcb-erzrQfi$9`5YgR2YZ+?#QVXD8-!9jxkUh!~>#mrrdZm9adMi=foiNz$Taw)# z093J!C<>CylxEsLkKQaRi*thp=eW6=v>~-tS~GDPrn+)Br}BYqO?W{$~ACnD(dL8oQeJhEbNtd<~OLs-yzGz~@oqM5oGygC7#oIDOt|@F_pfK~R_po&eX8FF(OD6UVuLsoi5+8^ zdp>G)Tt`V6Jvpy%&|5bTm;S?l0qoxtJSU0#bfoQY`or$-DFs3`HtG>{PVP*D8FT@} z6wnW<*hi36*rf}syoCtXPQB{-OwJ1TsZ3^h?HQhJ!sML#*wQlJ zVNDm`=B@DuYszleHN zQOMw)PgT@!d|jvmik<5x@f`)JjS z))IPiT$epAZuxWTF(K~-KYQ|RK4(e$wY0HI9*g;*a2TL-tQM2>zDYe|_jUMc5lnB9 z|EoJGldd=cLu&e~yA7kPt5oX(trD!lDdD$Phy){vwL* z20AZH-0*h#P>wQv!Fc-pHdO38(~-IkXbEzyC*9;ea#{4sk+;i+(zS=gceW>wNS7YM zdCv(?j?u4I&lK+j_>uVq^-IfGAE!B=_y2}Gd2m-yfl%xB`qo1}D&Q*tcaFJ#QZI_^ zNTky;`TH&Zv%Yf3IRiABa)XmZ3p04h+!iw>2@Pksl6GcD(kp#ZIM5n4WU;8w-^%N& zLqV>v<4;#VeSmf3m=)Kn4WulcVaUIA<*5>kmM6<&B8;^GBpY@sv4{Cn8BcdAtUIJN z08~l0ubD3}r?_PTHa4@%pUCJx+%`w|=(yx(OhcAab+ZIamPo|t#A2-Cr&$_LnbW}s z94}_VMu<)4Q0vHnL^9hq8Y>3Oa}?SLx}F^|Y;Ri$a%KhaUQ^cH@fU5qWB@SlOu2@F zcY(#s972sWOY0KgZ7`y>IUnp}1!L6`6lo z!noW2p~al#l#R6l142o6s~>I+DEi0Bbfb?yJMYFx6+Y7BI8)PHVsl3&3oM2hEqeF0 zH#o^*U4f;alUbo>mWydC;x$hkhc?wo>B!jW)#jjWS3gVB%+8=|n;nkGfZ zjCH+qCyf5{=Vr@;cd?#2JlWjOA^v>+e{RZY3YA>*Yikx3%u3fD#UgZ-f+bcJ^J468 zr}z6mutHq`Sg<-OX%>(bmw|PII^#q@h(UFU01EO?bz0kMQxw``51rL^>|#tXtZ=TP ztU;_m*$2bVNfz^03B@ePgAJ4)v_!rW%z|D9P5<-ttN*xVV)9g;iZO+rt;ATgHDQY$ zOf{h?f%H!@!gD<0SV$IV=fZ|EPmJFTB}W9SQ|ch8a%itt(FD@E#1WKx5$2_M5&x@# zJ`YNKMN+liXjU%iPzCx+WhR+zmz>F9o#&~4Q|PXp?`n9D$N?v8H8a&>bk`2B8j;Qv z#>E5jQ}s}9NnSl+cV`L1*AU|q~s0m3nK*_UbVQu+DQpQ@7K1YXX`(QqihyCW0tt> zi{$nS6ELi98U*7j>7kkO+N@=2jwSNk4_f7~qdJOsV9`O`{Teo#SBE`8+kX-IL6-w7uUiQorGss-G;kF`rs$ZNi zeAdb~0v+?1a4j5t_GyxAGH7;MVyJ4+xGj<|!lLl1*@ zS0eABQ*)~z6XUGp2%^jLnSfeNu?GufLpT><$^9WFwoC;^v2v(6H|9!575x6^{n-h| zM1@IZ>>#UO=5%iP=sGNMh^c{(sV^+wg;p&tAzO23!a|9|<3+ynb{uFqp@$LgR8mOu z-Bm~NN0kC$kO;~Mjhkl4kDGQKtdis?#uuyAMC+w{u#l=wvwCh9R{cA=mC?|)ia4_X z_MY~k1GE*(?1ZtuR|ae8(|bA+pPb2`j&WlCNh!4&2s;N44Q_7sEg{Ad)FceUdDWur zZINSQCaC;J8}d?n_XC+-4z&QuXy)i7QWt8K>16GqjERj9KbD5t4O62y0A|0`Fa9q~A-)MH35s&s@D zhx7?>=$4I#lhe@Rntx4eNMati2QOX zXidYz9EsM+yhAVem+{F>Qgo;3asZpyd%5(O@R<~F}Qv+AJU zT^5j&w+7 zV>gJjFa^104RwV8sUxHOlQV4N{1bZfV=U{hI4bb7viAfR*j47+JKNvO4zBza!w7hu z{j;D5|00sZ0V$YAS;LO~WEm}T4^F3kwJ5|bLHf=Rh1O0u0oxA#{6Z5k{)P#YeA~6^ zvWMrP=>9NB-`Nf1W-vC=LojLbFo}MpQI84oRluSnM9_Scb({mdN41}g~!Wua%@F|HDm5>L|Usk>U+L3aJ$U$$yg!#2ok_6z}RW)Cs`(IOIKB)CcT7jof0V?aC3rY^W zX<)2!4yPmuXM8J&{HT&km(nSKj-c;K@ZpH1Pb_NMJSUTwJLlZf7*mgoi(x31kHRVj zy>W7BF#b!Rj%6uZ-huZ-^7w7ESCB(>#IQ+pZb<{B4Z zHJB|&b_w_i0w<5=5b3O~mktW%n>6!$jmSeo47eizy?3Sj!|DsUieDvhBKj)avUB$) zFtW$Q|ELeYIaoZILz}7=;2SY_3eoCNHHsaUFW;FFdvl)P8*PJ+e0Dbq4BjTlyvMZP zw1$6i)#Qkq_MB@i6pLBAomVvK_}!d5GH6i6Q0ipp)5{A$sNtc9ELk+cJg_zdBX z&->hJxsm_+#GCW%oIol=*`8S(GtBTnollCpmZ(*_A>JCjml;RvVIPJ=sAe`#_JweVz1*gs^`loU$@tUeh2M_a*g&YwuG zl%nW%qpk;1+>W4vXeAYIMK3?csIeC_#TjSlWE#@`Od8hq=Vf z;_*i4ne8(>W0f7a%leZ z^@iwzB-}H(!{hRo9~Rqbi+u*H<*VP&iNcL^J$BXc5yJnZ&3-Z`y$E*+CdT6F>k<-< zs9<#wbKccZ6a{h*me`TIL^ry=cL-vsviVq+nK9>;-qDzxZ+)bv-Km@kGW@5`dCgCsc zi8Pc_I>J%UOAxhQW)5|@AFH7?6iDmxEA931_MjEkSL@*J5V-H1&o%hn$?`Sm!)tag zlt5_=gZhRhaE_NDmpIGaAnmWUc{5-I;WBMlEHRsIUgtHQ(O22H?Eqa))!TgIb*f@g z?{!o~@v?B>#IYpCY!tGPO->Bt|GIB6A2mTni|yYazRP+>tM!R7;N#2LVR-K#96iiz zHWmTq|2MD$i@t9HzVs*+op^Q<;xcg=54VpC&~(^0c|WeTCGF=JK1OEqsqzfgceA)4 zqu1mTwkB_?OvN^AmL)WlW|ErKlX5pShX|g`Fx3oSVh&%HK<|O6;kAthG&RBI*QJ1w zNlGzuJt?tyJ*a82S{3$0)~G(T0r}DX+8jl#LkWE%8BHfCcuUJ4jbOo&`p)p>2jDu+ z5B}N}^O=v+u6L$qwONn3oD{+n2M%=G555Y)(5!hdwipb)l5k z^GfprHI0$9LJO;L=2&X(4_SSo4T=hNim`C3$&eTM&OEi;SByw~AyY+PXzJ8zIGia! zPeYoP5;|$zc`3rnDYpmf_bK(7;My>CwtCHcocv?W0uGl}I1Z9i z-G@I%?t|lvW0<+Mxmy)7ABaWvwgr$*r->~hT$EQ!Jko=8Bm+6@LJZ-Q^>GO5T{dqM z6~X^fFyQKz0sFP3ZJ7X|L)QMJIO7nC#V2Ooq}DAmrs*fB>he9JReN2KjalN*@K3ht zoF$=;A)dliQ^OduO`5aQ;|8?vS-&<%Q`LRv#KxG;wkA6QS}VvSX4<^o*jwZ3BLROz zN|G4X8^Ckge#o<(gzmSSB{8!?3p!WkE_vX(ch87jvwM4JydkRH1lcEFcln!=!PW~v zcj4w^w|CZ{&e$sI(wGzee~th9((WO;0*G*}rzfiBUUI9NaOaPj?$8eb#CUr&*^0$R z9pD=Ms~Bx-;E{4}68$6)Zk79=WVqDBDU`;jO$@I)iGn&c4p8K7LDeDy<~4a_ibvm{ zuYgO)2A{o#$ok%g7PG1z`5T*@pZD>8WC)+E27ZpDD-nE=Pou=OA5Q0V&6YwcDrOnh z?1ySao`mSkJ{^`Aj=HCQm7T}&b|rFxXIKRq4Bnpiyc7rhzj@kC%#FEkXkR^6vqi;` z(V7u_#fJkGEOwo*Jwyjxt|Zj=jwdo`9PC&ue=6xrn`9b3hOnxfATCvjntV9#c^UVR z)=ZO1Lg~YSZI`>~zxdu)nr>z={<TykQTE#F9kS!%$u<`z|qK-f{P`er}yz4@A zCTz2AraGiPD1ix5Ob1rs^Cvsv*-k!)CErKMRAv#2Y2n7v7!2}DNv7w#x_iU@urI$_ z6>=|tsVK?&{i;Q|?|*uxTk{XS~FBN8;1#BCmsn znFA*8c)8)5U$szOtqspz>cSQ3H6BEO>A6da-P41q%pHSGT&OKaLr^eNZYRwRfr7Js zBu|UWrG&FaN|OD?{Ku$D-u?Qy&6-5&%L86=GR#mQ;}kAwx(%xl^Op5D-HwjRdZA-0 zp_u7XMaN^gTVsqiZYrsCCLw8GNq^b|_gzz`od^+H3uaO9>Rpi@P|Pt9NF^!9qvAaUOT3Jx>H6%6+q` zePnh?6L$-RlWk=0kbV_S)BiUDOl9zz+B|3dUnRhsnIBgxe{2_fwE7lK%rGg%t)?c( zine-%R5{aBPeV(bkf|1P^zH(w44Z=3j7xv{`Rl)Wri*bTK)NNxYA68fRc9L zL14{fh4`zUF>l;QcO|qhzO#i_EN^=zUx01j`Y3ZU!4=Qo7<^vfi)YagL{y;d8z3lOWprs{+>mP;IBv85G`L0Cv%|I-v14=A?&k`rrlz5 z37#WvhD?%W$8P_hEMxGt-AJ}7&diSaB#elrQ8H24j1&2M8J6xMU7Qk^rcIisF%6RJ z{7hh)aztOZ>YP_Y8L(BJ$!2Rw*5{k2?s?OT&l<`MJLVJrw5j} zebS&f_qR%+qBosLFD-NdBr=HFeofdEDk{QI)d@bLo}#}OTDe-n8n-p4kzJ2|4ac-~ z+R|`>m?9Cy>6dV5h^rL~wT~++=&@GIxFX{|GYEaw^c!?;y3r`;Yw+@cj>Od4xez1D z$HX`kiQ9ufsF%bk! zcXTmaXF{FhZqa`6Xt5?)q<0y<4e=4Ze)p(~fS1y74PiiK_!B<@i>xz@f7rPuqP>O45;+i>Jy74-K*@^Htr@ zd)s5iL^XSl=lluX9|R(n%y{zmiU+u|MI)xshUI2#Z2P=%{HmS_TAS7^1kq;fM)USs zGK5@H!gcWc7?tJh@6&b-RosbEPx5h|yt9h`=denMr1gr5^j*HENI$Yo`g;DQf4y0- z@%7Vj_4YR!9bnk@WoM(scOY;|B|huir~>vUmzJulLcaF%2EwGn{X}%AMIB+lS9vHb zFS;J^AMt{Td~i_gL9khQ1Cj1f+pe(m8lmV;h5Ic+uE22QOfV+<5JLfVBt{E*XqREB zNUV=$RLf-h(4zji*qx!6qAY+Pm#FzcSJOpow%4RvN8sCF{?NfqBV|f%0lce%lT^Xj zPyUb2x|L73^6pd;Vp$vb@OiyYRi2ho(A8v1QWt6Isa}C=KaD?UtZBq{*;^rL^e2>a zwpzgqGYiF+z;E0GN-EA{du2gLkcK3)lue`!jR5XL`#TgU*fR@E4<`J{UqiEsnb_B68>r2krHB3Aw_@gh`+eB zIogbRftq?P_o~GGPnMD`QLEh>FoAsy@%Qhs%&5-0V&!lXf68%8DZ^*{D(h;=faDn2 zUnw#x*&av)igfl=DsS#kTLgRAOV@htEp(=OO(-EmKL+H9`_396W~%ru_0XqV^xc?B z_h`MO?L4H1%Xo<&R=JClu>e?POjL+d6f8dG6)|{< zk-v`oQf{ELO<;M9PLLjE#ZqVdk$U{uXxTaLRz>f-Q!>XM<5W6Ge&!mSr{TjqDK6FX zJ_~wbD{BH2_0>Pyv~&IWsdTo&anL1FoIT$0AunteAqo&v|Ba?2xQ*}! z@C#V$P!kx(vNQvpcJ931z(R>1&4TRvMdRGjHR=Q}zYo2d)*WnelT809{={Q)F+DiG zTui4YWcgZ&B$@v+ft7Otq$b8h0w34&(c(y%%mL2RC|`tf3m`u`$0U*cGkBU>Cfc#u z;8j47NDKd8d1i&_$)&W>xMx=wuC9aQQANJDz^`Euhfxu%GGT=f2}ooB|&u!QipYfntXmf^qu4GGT>@`@ts`N$@N z=E?dSPc{ojw-a7(R8cfv&&9BMpHn~|)YIMG1jP+pI&@BYqSPfTxehM4XwuRWVdm`f zC>e68dip6d@wF1I{DU#mr<`g$$Vd zuGu;DF-N^`HMnaY^WwjUojF`C04k2$uQNsD&@7Hl69`lz)!OqLCDUGXSGM1s)o;KE z#BDiwxo|n7;|Xdbd>PP~CtH=2vBV=4?@6TFjuylFidmH<_*?@Af|?^k|gYH!tFw&V8>|aYuP+ z)bcFz@sR@f>Zox0axrh*iF_cfa|&GWb__8~2YG)OlFYdNmF~V5=nk4aQ+uI4_T^3A zXhS9n?>P&#Cw%H4jT`8sO!-&%!ftYAqZyGN?++~4ounz%w^0%c+!hx8!>-f(M8upHLeicsACW+&a5)daJ~AROl=GyFN! zG0vmx?;G0n_w!^j-idiQj!W>&2mz?_B*ou6<%C5=zNACGvZ$_i{&B4{PyFsvNo*2s zQA%R&SqV;Fb$hLV(L3qud%=>hnfM48brgw0SUJ;7TV6YUG zAI7R&nB~7}2ijRjC<U6d4UodMJh3&xL|~mwY+gJ#Y-9vD0%tuhODd<)2a1$UmPw5$tVJs{)Y* z@g(iE?M}jivSl*GsP|Q7V!ZV|3&BH2zO>l&cw#Ca`4VWONFS1F7%TT^ZPBTAu zzMa^Nb}&ruJ*h(W5(RC|tr(6=nXjF5+dZW~+cNm)#VnlV+K{%9Ns$3D0zKX)sQebb z`zDxQ(mb`w#@~2&+fxk)7XSvtn3BPIrlxtft`$G^q2a%>Ly&f3PZmq_bHb^1bzR=*R&3Eta=5l>EUf?*9!o$p*uIiiu$@T zGT2HZz1|QJQKi^?z=O~OJ0MdX*BjXN)V(RMBPIBEBY$9dM)7%YyR!XX+S+3eJ(Df@ z5zn2yIe&N%{%0;g)gv{(jFNpTlLx8}YR7sSyurfv?x5}ezl!8>?H>%2wac=yyTa&x zb7t2*F_;0yPF4ox#JP84$-141s9H$= zvD#waUKj2<%1~PBFD;zrZxYI8MI+L$Q!$p=sIA_o^?C*7HRVf)7CvHzHx-~=OSXbt z(meV5_elCVXfJ~4=s_k)Y?U4YNluA*-S165kkO2C)Z>WRkP^{p$2x-_(i$2kir}up z9ok8Iodr#GSTI&YMQ}p*4kz50Dv0j^+@V<{vOA+0d!AR$T0i%0M38yz|9_G;;KOt+ zna71vX0;Iy$c(IT|26W*-Ti-;?)G-0r>;V!3x z9_~$yytuK?Vf!LtrcAF=yzSa6i=VBYbKWPVj03q>`5n7uiK}ybPqA^`l3LrqQhtox z0xf+vufJEuU@O*@buB@Trhf1VC!Ir%;h^U*QBKB&VyZ|&ywgc1;PC8+xkFuM*b_*h zClThj89mH`o`d-2?fvS34J~?~L*>tG|Lo=q=tvN|D$KEWcIwfySU^bX{t3$27Wsug z#mFq-miiYldJhKeaso&%tMaI-7cp72yMZroP4!pgJ%k3-@6X|ulJ2iijtmK#cd)*+ zW>UHp^77X^PLMYL9}6zB-FG)%%(1kDk0h)bJj#@Gub;kSa8vYu^Pw9Dg=kzE(B=q&a6fQuHJDO3)PpePT7JEeZ$MANm zTnde?L{i?M?jrKx&xy>askepw^SzU;$Qw4} z(q6-@;tn+(7C-XE_-9Ob&#b0uz4_DT{WS0cPR6eIhTsD!uU#*<<}Fwh>$L-QvhU;L zz6(~q%=qO*FvF~<^X0FNLijSg##~F>qbP{UFr1AJiyfi-# zYyV|1%NJSwoJx5Qo3g)mE%(db>6;a|oF)7xE47QO_3MjRN>nl+{LA{s*#E2F9*!B- zH12^U69afh_E|6wGxh(wBno7YA+}6U@zMJHvcoidXHmvL zs&^4lDrO-@0oL-M$d`&0%U4|Q#C0oi&!aemKdSX}^9zY9)ukYR> zaUJ>^!y$h?4wKA%D(|dY8(=f|HGa%r2O>jFu(<&0nKh*5>izOP>NGRfPWOnVG$+^OUwG@^QD6~b|S71yy#dpBJVO%rk z3*dy&;_gLa=A!p^Xix51)C`mSyfb~*f>oMacBWkJ zp*o$ZYDu_jK#tzaIArGE?44eU*YIrZA6uPl6jSAmt)`d^D@Mo0f_85@t5hJ2LZL=|^S(Zu(nGW56I zqs>_m^#2KAq5oHX72Tbm#!&qM(&Gqi;B`~?@LD!u97p!C*4D>RGybdhymK65tG5=J zNbIsXR4gUaE>6Iz;axVEcv_vIvR*EZd94HpLF^q#Djj*dhMhJfdB_D4`}k?ZWb4Nl z9WHf0E&x@IeL=bq|VS^)JiU}r;hat<6%wp1XSA=ly_8B5}Hvg9c(?ZZX^IwwJ zG15;Ym6xEAz!!Vur2+G%3e!UOn>Ba}M%c7rThO`*@S}l9V z6?rATFtl6KcV_HxW-dFaoR|st__FsSzwZWLBnXioHE*ufWB!{e2cG@s)TvqIGu&9y_^Clh3t#RvI#n`rzun>&As8H6p z@F1g;hjBl8b^;LPDVfe-M!lVDE%BUB6g4Oqp;-A`vXIIP8%n|7ca3@b25o7)!X(6Qv0fuRl^xWqpce%zE~PUyGU9 z5LeY^v0BeML-SVO31lkq{_&@SYd*PDVglZ`1L1*JmSaZ-Bu7a7Gy>S?Q5sQfET%n& zrs?Q~f4L>2{OyFE7Rc&sBHZag=1FND8>;vtAU-`J2vD zU9FX^bIr)an5CPVuHN#7e;LZf;fD0l|Tnek+%_ys8Vtgr#U4j5UJwT$Ge~?vA~I7`%}%k2Q8r}BJFD@ zaQp#7T!~PPipiQ0`T^VpAlfNnhu%zu{+-Cy0gg-4$y`?+3cTN>k7t-|Y*3=U87Z_?zF6++kk07f9b;pQ^+|R_RhRLK20#)0(h*NTn3r zge;{6jX84H!}%D3&{NF|ieLsJzG~S|PE(7Q8u3{57=w>hQ!+=1Z-|bPVRBXrDhA(; z{ZnjSG!J#Lof3bsn$Y-(j8Nb~kq+`WsYL3b?#{nlD!m`SXE(tnhWkOlyBl|=LolC{ zf^Mo1lVU#w=m-U)mN`Kg*B#^B8B?xh3IZ3v!_T;uz?xG)Tys(7>4@w*3&6}(r%iMw zK%3j#Zo@c)_%E?kNA7C_y{aihfUw#crOvVVb9~AcjBDO-5*Iz%C_0BgY zq((qiZwEN`XkMBg9og%NaIJ)URsZHTNscgh{~O&-@4d8Eq;f}QPwt0}eRKI8X3lRV zF}gu7Vk<~uymiMYzmzsU=29qjCGxSC7vs8Pf$Ahknl50eUF6K$*Q*4X#kmcDl+%N4wCoBI5ldWN+5K&t@&32j7V>CFxE?P=6!m;k zjU#>fA(>XxOh9=t;WJ)tsmgxC)y!=w2-yL&lmW%_xbik2L4to_phF!gkOg?hH^n?! z>;<8X_SsaAi~5RTiRNYLR{l%^hewh}c8kaH=!7gTZZ{ygf&-Jglzd<-R{m`~C<{wk zJ4=!Zu~9QBy>-Qy4?_6aynh{-`tf=bWuYsZ!()qkg*lbo#^bf4Bsh`iYvLZXC2C^I zu!ex=5+uHT1obN8=}z5Waw&U$K~|QS=5OG;9>#~JY-ll+Pcn-{C`IoW5#Y_0eiS+g zemSI1dRrMAqn0xKUSPpM>tiB>`wB)r5u|Wrnw5KsNK6kLz%t@j)n|nX~BlOB*LKOnB{Y|BIQx5>+1RF_u0o$%jB&h}5P$rcx&9Jmd>%erqD_(KYkmXnQ4kb6hthulTt=s|1qS#?HD?A zvqG;eOnS^MMO&U*y)vw=DgUpq9=4RnR$CxokT};D`awiPlh*+6>{Paf>s$Fhvl6$* zKPi`}gLYng2*oTbU)Xs8*B=f({p+?!sNuBWF;^hmp$h}HF<&f246aJ?0@`L=i)%{+ z=IB)GL(>RIcB-K*>XNG`Sq>&dBmPN`@Qw%kQ>-NsHmvo$1rAg#T;QBxdzae@CBYJ^7icmYz}*$mN)Z{;eebJ@@D86r`$Gm-in) z(J&R72oU~s&?H0>VMf`>*2?K9fFJ2_rixTtMT{iIfbXbH^cN+Oh8VyrN#ie=h#~y( zKB)Q`#b&SG`g9yOV+%W^iQw%~Mb1^vJHbF1?wra|jb~4f;#=6(lK7W1n6*-R^vjm7 zNtqd^Z6dofhTY8tL`?Lpujo{}rs)j0v&$3i$MG@g{|J;~S5(uR)4!UG$;&RzyqZL< zMfctg3vk?w5w)X`D2S!we<4xb6hlEJ10!R0uzst`1P8iPKYj-LG#UOWgAr32pKqsTAl{Eu6!@})ffH#c7w?Srh|HDDk`x7U;|_WQpkQXI zV|~w{dVCw_{B#4@SPIvMM<|oc;xTYN)70|@FWk;M!6d@;Uw${nK7(lh?|#jgEkRm+ zh1sxe#p|J5UzmWrf{21&TfTdMBULr9@5uXYS^4|v>7Zbdhk3^XhK3}!Hmw+~PLE~H zl22E2*I4;nBE3Kz1tx0RNwK~=eYC+zMJ0G=;alzK1yy%y$&s4!Mx@ROv?Ubm?k zbP}D2&rk~Q^X4PO^ZI3Yu6?;&vyM<7yUL5(Aqt2@xJ7Z@?IR5#-OJ3guCA}a90p`R z;m<25Z1zp;sYrlH|E1l_d1AWI`rT)|U?Q~p0phxpgi%wJc&Q8RBU~VJa$Zrk^M4}c zlwU#;|7b&4538z+yabEdRXH*_9A5*Z9pThaJ7br`r=-5QISg!wP`o6KPj8@Wjm0g z52B)l|z#lb)L4i*bW46Zbl0|)X28Olh~ zjDNJ-FOi2y7)ARO3u*0?i&}a(GioLAZmt#n{=g?@h_o8LxP{uP%4-V!1?piPA@%3g zW8L1DVrqh{%hRy$hCaffQG(F1t<)SP3*xL%1U+2u_ zV%LdDr0}Wg8^Gh;qyJhBsRWnyBYU8N{lzA{qY)Z!{mA*FXz%=7Ry!u-w&&$x5T4Gv zHvw7Nw;<$M3=Z9XBoxD(l4;gYnv^=Kx~%LuSMzXjaXYyWw{-R4k;R8+?3rw%%6}db zLJ=3jvEQq0yE8^JEPGBg)xWC=;jxv9f(9rI!>Rggtm7#cRL^y*OBh+&Q_GP!L>+(; zY5TESZ*QwiD+3I(gbMA4MGIo?V7lC9Nj4$q8Z^B3zw~LvovyEws7!@8_#TOlQ1|aE z$odVy%bheYUo`%X4=p0`H-8Cw{BPJIoO2uC?R%2*XzVt823z~N3<9oC$f~KaaHGSf z#pcZeXpIUv8@KT;UrsR51gQxPF^9I_{m0+OBbBfP&K zF<6y*Q^9`?jHu>md#TA{#6dTJu6#LdCKH$mp>|^;6=%MHCa+KS=9;;$PxjXJT8XqK zFW|yu%s+*k65S{ix8R(|HfV3w5 zLMVcN=X#)V2+qKF0~loR&mf4R5DEy-g8)AMB`@PyE5rERN01-V{6Nd)onoM*X@)?y z$5ocda;$P0On7h>FM+=|Xc>mr!s}L=@?w6W>abjv_KyiXDuzWCzPo*9^pSayD^HDR z;kYpb@8zVd^j<{2id(#FBiHv2n=53HDv0Z$i8?;X-Aob1$(V=T%q_lvflOJNxhi-a zr^z3F_DeDNju}b1d$3`Zv*T_>Q>j?~GXY*NN1cCc=S>^CD2Jl=*jiRugHy5~NFj?@5V4rmj9q@5+F zBVqI%!1G}k{~#PMeK;2=r|U;H-{^)tLF(g{A?6#Q29VYl&V-M1vQXduRPO0&8_x`& zO3!tbKWtbIoJjCPE~Lu%MSFSjCK-hyK>>ZcM~U=FICWp7O_lX#Up@(`NAwf4nlss7 zYhl5NzF*aFFV_50BK#tz6vT7Xtm7z3JE>24Kxq~Q{R(uWmI}zOCZ~;g@@K)QoA~>5 zpdDH!U9MS+jko!~q*|tJBi}2FY1q$BC7u!(PYZdWjz^g*BL{aP(NR+IEd08B(nf|s2+IHq!IIX#Aq@lJO}yV=ZMl}+E( z4d5N9*Nb;=CVchM@Y`2q^v6%tOaE6IndnY;dTAUm9{MkOMhhNw^k1b6ypTF0)cgM( z9K&FDCFVyLnD4qgd8w$*eVXIAQqmS%%hyn?NzCPJJv+6K_nKG#t170$$Q~*ohpe!Vgl7^@OrT3; zaU{F)w=*$Vv{t^t$eOJP)ymo$k7j#5!@F|FVSB>OdmD zz&V19B5&uW!R-J`P1R+spmLl~UDp2}mrnIgHDhh;squ*hw*~=D-Y(miH58QF;jT=* z^3(}@Tl2jVW`bgYAVWG+gIZf!A8Fq{qHcs>;t~~^;?dU*%PV5nX6-^(^tVyl0M}J1 z@Vfd^f+>mxF?%e!U%l8iwTYlacwjHKD`~_}_XeQ?gAEaCrGgmSiNjQ!M7byvz+}sn zatrrmQ64rk9gUU|@01FhB`mZ%++n2LX(ol}tGZ>1=kY&bkkB;VGttDyTVQPPaLoO| zy<-xHKjP2C+Lma%mx9-h04eqItW>X=QOAAF)WpY6ptZmAy66$LsA!UQ5{?UT0irgj!Q@)h^|35r_ey1#D0*)S$&> zwP`^%Eng7t1viDtqyOn9gHdwH$KflbJu^PmN$95{MUw({hxWE&&E}qf(Y2zbC&sTn zY)0E@0=vm`IepHn33N{g2RzQTf<6 zT6aH%KGJf6NtFM4QvES{cgc~{!Cbkwma$D9^CN-UXpy2Ehr*7PMNCZuA0SEmiF50W zjei_|Vn0@`fFnXD(RNBBM=9E|Puu<*9K)|yV%7L$jKH%lN`UC+_fp{zBP}S*aeUyv z-cwgEiGcE=q-js6saJgcZG>*vRCySW2q^H5<go|74$$r=xh-|q<6;a1u74VZR+Ra z-oCS4bO<)xKM4~p!+P-pj4d7z1XGoilx(@jl|0vc=Rbr+IwkP7I)}$-2u`N;4Zc0} z0k_*-k&*u;@WA5Bv-y}5;D}N1YG&ud%k&r@u z!0>vU48vbU-S(509{<;%X%duXVt6;|8MkIZ=uwhs6*k2Z_y)jcd}8!I8fQ&%%!}r` zboUzw*29=v2={c$+7AtfG;aXeOX9_$D5Evt*J)-oO+mcNu5jS?b41CntIhS%c#6^TwuZWa32f?M&^!nAGzWoU z|9=jFz2GU2hxrX)m^vY1>?Iw49dI2e3;=9~t(C`74TF=oy#2Oa)GQv;!jsU<5b>B4 zqmkv?VULJks>}a%k0b?zC9B>}MWKMQ7ZJpK;G4MAtp`1Q;EV(s5kP?o)jqz&L}%nl z8ZXkla!+jvV=1_(Yi%Fe*WEM@&=U+(4sx^py8QR5ebq1 z-#jVV{$IWpb($-NdqiZ`%WB`C^Ihp#$?z0Vn;_# zCIEW%K7oX0*w06{O~eQ)p9$~rD4HfT$<`CrTFYdysrKgAHD-jBMTX|r`Z>APC216= zg--8>(QS^;iSy!47z!>$NPZ`%eXg6%&e-JTTpAQU!U(7iIEC7=1HE~<+$_A_`+_pfJ1rhA=4^yd#dPNSL!)c?9 zC9TH3`!W3dY4A|L>KRaKSTwfX!>!rFxtvLRid0mw4fMcFx*}l=Ap9XTgG;!7H~xSD zQ29}2e}lnTzd25l^`V9kdCQ$x_gboiMZTsTlIUx>586PG6Y7 z>{x9+@;j%ta$|gN3n|z}iXOj}+5bQFNdJ?zvWskaHk|H;Lknv^+Mm_Gn5lfWXcI(y zHw)F-tCM7hl&&U){-(#@0HhH8%~~JlYI%IOwWRqbm>7)*PTFJ(7PdFp2)HqRLe_qF z2|)u4YdD25HscH5RF~EfcqMAw^8v;}zx|O)Rs@cD-fRM0VzoCBHyZGU-8QwVvGJo! z)TykWJt$_g^f*^C3c?(IK(h3FP)+J~8lE>(7sTl$?zH=u1G>kO7wt(8oOH+avKz(+DbmwilAAbt|Hk{g7(RW#kX|&V@`wc(h4)=+>?91pwC+P!6N0!#_lL_(<&Y9M}e<=BOFK zAHIXPZb4wmYkR8+0lYp z#O;U5Nuv{NBRQDL@)vHaU+>>e5S$oOI}yrV(QFT-W-X2ga_pj^L6WHeKfYS^U!EA4 z>s}yv`!BKj$rPDS(A(%D<6jrA7ia@QC-5@_!W-XJMR;9ZzYsCFD8J3ZB9umoFrRCB zLHrNWdc>LHc8Qzz-B7A1^hc~Bm_h1cDVIm|cj?rJWTv7MWm%H@gCS@ z4U4W=`#w7@e}2)paETXvoZ>geww;LEivO(8NDHpp)`XDT%oxD5Ef5(|uY9LpY|@bU zfC5(LBk~#%$AL(sgXeYp*J$u>aqG59R_p!ZyS{!yoXWTIVERS3!OyI}mf|o~I#X$d z*NevEFOkx_EBd3<3)BE5CDfd+OtDRgR9ZW0MCW}BM;C~0ipzIN5h+;ySA!oi^b0!> z4Oe^-7eD*eEPxpH7Y@Nd3;{{+G9Z&3>0A%p7@BnLE(2vbk9x0ODX*pdZ6ET)317pLyT{R)H1J!>hL@arf({R5 zuXFMqsC=iQ;)AWPFNJ>-=(Z!M;D?3l2CI2heiRG;C1-tj>Usk(>bx^~gbD|Qf8!R% zzbBv}vSk-x#(e|G<%ZJj49Jt5$1d;x>N*t`tghniJ2%@g=}?Nhwh?rWvb@B*LfT1k zPPv&R^xRN}86>?q%W!(VCZvbej09&y@9 zja4B>BrG+%eI#JmnnxBs_^I?rQ}9&op}r>Y1z*lfAdh8$5q#Y+= z-`qE2-tHq71EBm*TdsbUqaVJk$r+|fAfn^9L9`j=G?VfW;A<1`+N)})JiY;J0YSej zS&K9GDKUkZiB1I^=5+)dp7}-^Si4gr_v4EVYUXDA9oCcIK(QFuv z9VQXikr0aDK38Ib3-I(ysdgB1ufS}k(QobcQn!h>+SS+TgmK7z$|cY36{?X_CH@pj zx*I^9ZNJRv*PDKT2sIu6A0LkpkAQ%X01x*Az{95oaPtt*h^bN$5{Yx!M9@m8d4Zuu zbfWaI|AznLUPc7KEubrbA5J)Y6H)7YyPI+T>?&jOB4FCjpz6nUCCN4AkL$wUeobZn z-kQIO3Jrj6`rQD&-vFMT^8|hSJy`A=R;l!OuM5uHcLN~UNvEg`zuvj%h}#awy~gwH zwP*GA(Ye5sZ(GuN#O*7}W^y!ZjuszIoufCtUU?dBRme5vTyXB(03M0`4wcb;Tg3Tt z%h$T~%4~8g>}~mlVPI(2R`hn~IH86}_r1vrkw?huA<^xls>z)3sZh!5a!ZlMqv%)1 zrUK>_%1K?975w-9bs==Rb}*V1C&lk5s(zl=$c@y9OdapVe?3q7-L!cbx>~OO`rtVH zB~S-u_~N``+x~>}rTmphN%NK5#Ac|cmC<%6Z~O9zXO(;r>OL1VNj%3npDV5JGIAbb0)tSMnu#42Qkco*_#f~ZO;{rDPwEyC5prLgq4bzn zJWD#sSTE8k*6!G{c9SaL^&W!RDYgYLofELO5~6el5vbg0!?5Mnqsx47^~FxzhrD{!99M?S}SeeqECCorX6nnzx2hV^-B^r#H@@L$@xnCN0( z1^=Wd-_B3anw+*P0Vt3CA7z}WC*>V8&_C|s$8o6!o%q+p`y$-K*2MciSx3G<@x1Fb ze=klq6FdUODqGriVQB9*=5pvSujGJ^#+JK|juh3k{Fi?b&<^%v?EClghX|t{i`YiPDendD22A7FSdqs_CQG>P1AL7*~{I_y(eNRyJ0zJm`=*tr4}uw|%Jqm?Yobca9& z%r9+n7yW4$)yQUt01n3UxwhT&prcSn(-&7DpUp$Yy=OGdI7FpLnG!EkQo%!{_Sd_p z^a`eJm9bM#!e^5kRV@XXnW@HRE01fx&O2!K#fs9D%5m=HE;>Dk=i@(o?<@sHq|Q(jit&HQoXUJ(<7ksQvFD&z}sAI&fX+Ion>*qK$_s|w>HvkdZRA%2^vAAO=&eQY4gXf1JKMA`(6k;i5bD;_j$>A$4 zS!~#8C4;)AWLj|1|D&WE4{lb>_0eAsf!ICb%v*LQm(KMh z|F4Yivnn4@xrAJX$(5<7wsd;6I%ihdfN#J(Cq8NDaH!}^IPmcKTQa71 z7jH;~_RN%nWGo-d9uBN)x}B}+eogfnda}~7T8iB|O{E`M1u0+Uy2=E5$!YB+q&yaR zwkC@wbU<_AcxQSeLt87A__4^ETSy3pW;jXQ@TA0hWzc1CWT^T<%1HI-u5IfIQYOl_ zku_V=L52A5e7K?vMxn(2p$z^nH&NFb)Lfs3rlWCeO^3`r!$xbh)?#ztug68uq(tk4 z#peEZZBJVc=_r|(k}n^ISEYWEfA+IIs_Cl*{|PcS^-){GkLaABb}1$or&WEe#;5#@ z4D!62r74YEYHY@4UD2bEET6==IJQ(`HclHW7EC3`HuPF_CEvb=LvS$t!^JLJd+#So zS9Xq#2Se^BX3Cnz#eN(c3s&k6P@Mv-%rrS&z-gMaZ-qe;>**M|8vs}zb;z2v9M>;1 z{qOg5i0y}eX8fp%ufAZ!y#CR7PLW>u*qZp|NOifEslE`qva_YU7cYO!yFb-q;| z9Kxybcdk2P2k+gnXtqVn61T6`44Q#BQu`;lFv{MA@!~=mq1H@oWuLzP6AV&@)tUuh zrk*IS;kd_DWk^_JBO~iG=e%tCJ6)DP+Z_(9112*rX49?d>oKhy>?@0VE^J#elOpHg zWTsg+fNi(4O!SI>m$Hg#*F@kIDUqz3igoJbpCc=xhP6vna*HSEI+fWxW*N)ne4A9RHH`4t(+eI4>J`Y9{F6=vMXu8+I{+%1~i zYlW%2UgNMd?6nGM_D0EpGn=Z`_dTpvImfFSDe-7j z232&#N2fGecwgI2i!@v2(E+!dihXa3qhT{Pbuw})G6+&OllfTivtpV#0ua1U%k_@eb^H23a^XMRU?0kaP zcgF@`Q@Wk;wlYal%jzd_({u&zd*If=AeuwPXjd*vnw3JpEb8FWb_hpO#rFFG0cU*| zVPjPZlkdIhGI7&%;BsBaOn}l2fS3iC<&y9E z7!^aS!~$|yu1jT;iZo1;LUAZl`Dl85SguoH{b~koF+Y{e$NnbH&@uf(w6}|YVy?JR zw!y(&efgMmgI!HN)3+~4&r_1Wkxk1xulfNQb|oL{Dw6)_?jq^9t=WoW^k{5Mb5y{? zwX=LD1x%3HS$~fFI>~#(EWQ}Od)a1Bad)-SMo9XDR?Nzwh4;6?!jSOPDzg>grry)P+04JYXHnPS zDC0^)4=KjptccdY-*=52<;P7!3b1bImbUG+h=Q^iR?oXlJ}G^HZjYO5VD&m7Khx`f zvi5~H(hRYG|Mh?S}8o zsA#irr3&Ilz44mdFMyxuv<{H@)2|2n&on&!rTE>K(^etdLZ0$jl85styKL5Y)rCgn zc@FCidF}s{8a<4@1g2)TbOy)y`^hz~7nwQLuZ!jA2dTwB&mLDd-o0NXlH`{*O(xpE zeAlAEEtLko^p_>CE{<+e=YDgN2jeK`)<1mvd(Sn6Zvb)Znjh|;kVvElw%yA=C}HdY zzb{^verf0{1M$(1G9k>5)Ee~QP`vCE800@jo;n#v-WMp8;fx;DJyj$caAlVZ%slb( zY#XV?ei+PF$51>N3bfR!I}Wmk@3AM8KN*(uXtU`Rl%C89(@h8)!i-L1%vZr@jTANt+1qVPD&|A3mU!N31!SLMgW> z8XBVMAj2M)gg$nvc-o=3CZj05v^ZX6%DjCrF>O(~+7)N^dz$PD14$RL*RJ?VSKGkm ze{TNV&wcR00jhq6{>NQ3-*OeWbQ0CchuyY_pGp}CbF~@^VG+Ft0)joBr@Sm``*`-y zhXkxAOWXwutgQdiZ!Zl0uui|0BV?`*7s4zgE+o8mN}re759~7w=M6O2NDS338kpN` zlavGLuwY&J12i7IyT|r|Pq#*9p_q^3{v)nWAqW00+HaPN4bTQd8)-R^&A3#=^qB4G zpfS#bHa-!(N9FDIio~&z(nB35x`Gs0^ldA+z3t1^`981=^;sI&9AcGPT<##7Q`Rk> zr<@A;usA&3wkZCcv;R|z-b|^lrt|%@r2{gY@nCz;uWVJApvEckE_TmoP5oKQ1}F7T z&7kMm@viLlHTvLm1)nEFk+CC+S@Yp>*RffvN;cj%0G-l=*L1a2jA^g)@6r404?29D z-PUh$1=$wt^?~fnL#lUkgqqt_g@Py!^_Sm9n+4whCj6$h6JD<`SDaDKd^4bc>H~4kjqNl zCuPt~d{xev4_#Epk{YjteMPqNo7w17{h_`Ka`9TIXAC!KvughWcOKF(V-o5)uZ06DO88b;nrG?l` z;Go&P8a#}y@05KYV{r|3PdT}=&*)^unq7!_~U2pKiZFX~9+jJ9KsNz1% z8*h;l7$}9??H@k9TAR_~jeVXP+W}y&t*HRXt_4=25Bd-lya5ica`utFEIn@%?yh0? zhLo^)>*js1w+&Ua?e~qbk2vh>q)@5R+7*_a$^1>t58UHQqU}4{yK90^Un`7TzmS@4 z?e+A@-~PC}T&2R2?WdB}svO1DS;+zGfR02KcS$T`Kk z19kE9g;8Bq?3H~^@3DmyR~3PBtlDMFPjeWYTxs^_?cde*UUXcltrt^rH}R7@t<7$; z#y7ryyY+dXddb#xL&-N_O3_K1B29*J*m~|aOaA<}+_WIO?jAPq^n|xj(WcHqkAoiZ zWElHpkyuuSZ+P{blEbbdE80UI=YVPI87cV>-%Hg@%+%G;XEshZ{gl6#6hH~sZrl_9 z;)*|aS|{!FR<(PvCpz-rNy^_D{FTT^w;-`u%w^LIYoB*}QnlA$N}%WuH$N0>&)(g> zbessp>awqPU=~sqy(jk+y(TwjSo$=3>ZEANQ&2n#JEfM^G+ho@)+x)GVo1I|as7Tah`;gubH$l4 z&7-UzIn)Yeoz2l?boX8fo$M*=!;>@IkBC0>?s+>a&LLI;lVtl>GfUXJ4zvaM=Q%Xj zDFmXV*iP7~R_N4N>`YA4Kk4-BVt*^Kbl0&I`f4@J2i7m6Iw>U-BG=iGvKT?UPa!34 z9!d|h+27fGoH$%iy@M)$=bZBLhg`s=>=SmRe&0R|{zAW^B~438qoxPn-3$IvWp<~x zMk3@31(*&p&{90=8ieZMjAl%%OL@UqoZq#GmD`z+eDs#)@SrVCAnSXx$SfZwat<@e zvw901Sx#R6z%@O)`WkeiGwSga+|_-mD+0cJBZPn6o~|XDEnGg+<-;OP>iAmz$%wv< zEWMBQ0i^`@c}oYdH?2I<*BX}GQmSEdW2fnyos_HuX@rQ~lkqn$uhljSZ7|T}Q7pVa zf=A01X@I+RA2%v1((5}1?qdHe?R#<$ZQTI8#>6_m zSJaQI1Q~JWaEo^7F9*!O*#K*@%3r+OTs>=ouL1$mRY?`>IUfG#+I;MRg&1jvBuLg(L_6?Op}*;~<)ub6$hLHb zNH$x+a{cl#_<^L8hv3o|FE0P?>2P>>|Mdf*?df>--uW@&gqDDNn1iX7Pb}DNujgiD z992?(BMl1~!TpL465TF!@976DLjzG20Fr*+{@omY1C=hQ5op ze1hAiyYg|&sFzpv=3(Kq11iuDQC zUKuXxlh3rTs4;TMcNRau(MnxiO5fl)4}$l8bj!Tgx6vovgEmI4pb(b6)S)^S(HTAC z8eJ~Uz*iD_&PuBdLIr}VpMQ<<*FUXO5q&Z7G?j6NBXkw}P~Ud_CvN*zXaskYzwa|( zGn%ZFrzFljxILI8OMIoGIt-_$|DCH_>K5{!+&ZlMudZI!ktEXQ)&GRw05U)oO`d&k6ZF!f5jk(8F6>)N(5n@o%Bi5u zpPGT5OCF>l^5a&e);lqcn*JjGhQ;Qn#-=$`)8b~p{vQ#m4%-(qnk({ZiNixad?V}` z8%a4l)P1JTikB>XW`CwXAD-Zp4+{wPg)TfBUMKl5eQj}!Uf4|7OrV{&anL(JNRgD= zfJLAANxQyA%ShTErLgq&2P*=;zAdM@pYQohH*K58J-L@Gp(4UG^*=VkyF{o&q9sG@ z%}ZUy6$@)|p}meWX7g=%tcjK@cXc=5@3m#6;L>!ms4MtQnIoI2i_wH>Z-v0n0EG80 z{~CSqYPDv+pTq$wx2OTTjc6HvS;G=<%kIbBFU7P;F-03!JZ8xK?J>PFzSiOY%?PneSzB^Dyp|Ssy6?>?5%^qTAfYY8E z6{g^&#e_C!l4V}5L^tUz$;GUJt z%uP?2-D7hA*-VcioyQMT@-#mG#yCDq#%{Ck6ohJ;5*hrm-ro|+0k_n;{cw-tHJCC8 z4pG0T++XM9+c3On$hu=_jMA#nsfBC3RN*N^wY_YmfXUe$5N3U0ja(4d^PYJu-||nT zNrg{t%>sX3*la_ILw_wubUp3*D2N{?59$v*(l%<-1nfaBOq(w`=8a>Iw_v+72jbr zd*vgttTq~phUa-o!!*gw8o}cRET zct!Vbnl9Zszs%X?aemyQ@dOXcWElhsAcHft4iqR}(&Vr`;p<1=03HluS7taALxTD+ z>_tlbYX##5`Pf6yMhB}h{{Fa?k1{36inXKqGY)|mXYH_|R5tP>V#SnSzm;t3>ZDLj zKe+}TCO#k)ov{X+ab@df8|H;`gn^JzYPGK5K}zEKiKlfOX3OJF;f6V#w4A z|0(so{?*Db{CQ@DPnZLAseXZ}Vb?#Z8h#x38{9k{fa-HJeq5zq_ffBmws_Y9`7W$nWm|tGxyhM?U}Ug?Wzgd>l6FAK z$ogbX3&5Hd2UhDUS4Tj@3ZTn^LFmIAoct^E-6!@JrMnT3@X@$SVFAa?t~X zxg6ZRB3ao~4AFE9jtp<)qfx7UD!JMrd8sgNWk3Ca=51QVPX!9@eo5l{>&RAzKP=wg zn}wvZdLDVNVpfY@dV=gRE5#t&I*=~1zTJxoMl&6x; z%k*D{#X2vlmwlt2R7OSobRJ@)?=zK(G}~rLl1gD2);iy74F|1FT2vT7_kzPycbN_e zTIHMyWl;OGdkT>qZxlw@qzBUi|Dm+Ys^63_4WkreY9s}~bYe3c`X$6uL2FYe;^2RO zK$V9Nc0ZM0C@TrlR(CRBKgW@@bk7Hgt+5VTEANauc6Z324i%Ho4b&4>tB=hUDD(@5$>gQ#j zlob6?sa}t3o}}iwg6nXGP(Fo$xKTdFPk3^qYx#?rusY9+iLA^3wTtDE74Mc(>&2-D~jC%6<=jMs|<2yddH-Hfkza}D87yKChew_BNVE1&w&nAtgumNYb1wgmftHg?{Ra({_5KuddVM*QieroZF$jAS zB3x1XQA1H%h8?zw!Kb&IV;6|%L&&Z<#~pkWN}oQVcvZV$+MHu~x1+9xy{3hEZTns9 zS~KZ7hw7?)n|$-alL!$X8)0}O-|`4|tEO8(k>S8}g6`^cOGc`Tyzg29wrJqby(QrX z&amEcO2!l!Hs9D448J)}pFQok+t;r@v`dp!_r`nnIYFCnm8~Er>Esh5MddElCB1|F zpb_$eh*thoPU36V$P6O`f4N{cqd(FPdHRDM%o~cuVLVExHBa1*c%J@=E?`V+xC<6! z+o6nq*spv~V_roi(32bIgfuh{$gq#YSXGT3IlS(Vi|w(!olQB^^fwP{MBVc#fAPZ= ze%^Tafo?E`g=dBsq&4A6+b-gm&@z%OZc*O6@Jr$sjl0Owge$}+ zWj@{WG5ehUFNEI78;^qb zM_b|-Z%ykxMX@*YX)g+jp3WD*=>ow|gTO6&k5^g+e9fLbO{S8P&E$&9kdGEW!!|zT zb5BVEdpo%K_V315%G(YSacW}pB=ddb^N?Bj^{9)tiKaj4^ee9I*c1%p6 zbYG^fwcMr)ZT0(j8=B9{@=+Qd3r<#26t7S21K~P-gSnTQj;w~vPoD7lFQZtoJJPI5 zv9~F1(=2p+htZJtkT2Aj>&Cx7F$MXhven7l$KFNB5B6DJ4FvY@{-^L$RVW5Nq%15}dx3~w5L;Gd(#)5? znWzaKLmY9niImeUP{9 zk3&D*e|+a}uNk|z2uI-I;A#!WYR$NO;o;eN>?g#Ex@i!b6z!OlpvDWG^>p~jo29qW zW)ygJuQ)Wlf0{{{tw=Bz83ajY5Wl40u1iUEWa)3TNU0Js*XK67rL2g%N` z>aXc$1`q^>5%(ckV^ls;n6Yn^*8j~iYzj0+bvL&^#8J7~4%^++!``k+H|c_&(z_}9 zISdLvS%+LHBf%u-gW+AiLH0X38)@s9>u){HN~fYfHM3ch!tdRk%qo6`SYsuZE;!wH z@P0C6^02NBH8LJh>!YC5)~_e7O+E2@x$h*{><@1MveoXVRj|7uf3(@3r5Vlm`@Q;0 zIG^j(WVX7Zzx-l`Wt^l5`Y2fPxm4N=5z-G;FZ=T1(fJV=k4Zf1eKUM<*?cvf|~7S zwCu3N_m_hx^g@nq0LoR%(?O3IYMF{!H!rX5J3Wz`IXoL)Ym4*|gegKh;`rIhJrM8i z>aWQFA0I8(tVL+PD^ks&5pZi{jcOKgC6e{gU+xgr z?vUf8nor)S8vjGswAioH+x-Q=VJ$cJ((JhuW;Oi|#mKXty4hA^c4pJ9?9dEh(?7!d ze=#eu1g6eX7zG$@E7BFee!}_n#qf0T`mBenvs6yn;aTUy!SQ|}rlQ244SI|CANFzd zgbzois0RZNhkQmp)K9pA7|-p!x6bnx7<2~Ffr|McyyTBEu8koLo_rm9%$MqVlIn1a z*T~x-et=lk@NEItPt8%^?#{y3NE(hH|8NS9%)YC+qpLf9=Oy|_p(0h_nXR$&KOg#D zah`K}Uf9a^y#%s{mu>zVuQ`7js(1r%_3g}g{q^(PFaO%CH#r-+$6v*4OgeIMdPh>$ z+yIU@q1%4YJN4WDu2Z_oPTu4mb-7%<-J~e{dS0BPF0W$z<@d&da+IjbZZAz?XUWdw zozRM{jNf4kDwS7##~!T|RS!rWP@QWBKsX=HTsNEF0Onq*Y<;E?U@ZF@_joIqb7Cv> zPaW0Ot0Qml#QB@%qhM>@iR)Cu?Q-3U8-V9d5rOjC8s!qrt?P2nCtd;^bsUAbr<0wg zLVv~WE$VTapUC|iKj&0Gxf`mizNexO-d6rI8Toa4gE|>F+SChYe!7~zjB@cYt zRF1EQ#I{1i4MGi)D7Qm3LQRJLbp>8rz5lX3ULlaAE);!TrPdzqdILD}6SOv-N?{Kx0EQYGc-{aEwQ{VhkIq9m zsvbPZY_}V%M`;FrUG?=)^KZ~=Vi)eT9}(J3$6)3%XACS~*iFzVb7SvgT0UxjmNqUg zUbXFz9?Yg$hJ4!HmtfY<~-^X&Am$hb!6fl zw{r8HRJ0Voh}#3pV3ktom-)qh9-+!F;#J-~2c&8x{{6beZUngcdIK1r9KQ}r(cKEx z=7{pWP*rXQd{4Zy=EB*r7e{U#y;+}67SsPR=1w%lUgk(e?n znueaqeS&2!8@mV|wFh1Yw4&lrBd||YdGG%gz7nh44vm6&XOoG4v}w5k_!O$gPMupD zUNfFHGMyd)CYoADD)acKvH-Kn5DGrruSt6!J;kI+zy z63=JaG8d{OX`avX9}(JJv4Lngg~aZz3Katvn84)W(USn{Ro<){z$!Z0*Ct8q2Jm62 zLI3yN@r(3jNgMiWAXEdAm~m+ooi^AlrV)xu<3GB5&t z4dC|{1ac>S+0%A1{Qb|qKwRuI=Shg-{(iWctmO@0bUE)YlDiy5N+~KHqsGC8_4hkZ zVG2%#WL#49^W#$ovq{KOsGcLP%Rt-0cw-v+tXE4^*A4b2f1;dq2axuO5*ycFY0CKD?@!lPo#wDou*nX_Y$Ce~Vkq_S04sx4 zhWhaAvA(pgB=9jukz&)2YU_KYXRUe?Pgc;(O|PT8v!4fU)J;6SZo2ib?BDdpn?6!Ssy#Ntjt#7 z7Q&dG`jty*G8kXZD(_T!fR)5RPyrzgt&U71^vT^uSF)`q{PxagG!P&6B)mfui-!XW zXPzFh_}H%aQgkxu4S)@{9bO`e@wQj-n2JOxt)hNA?oOa0->V(~@1(v{%_B*|WK>Cv z8=(jNpPisx~xX|#`HWT{6UUI9(e7=fLfQ3CPq z0uxg0JNKOo=cO*72CwByNs0~$E{LHE_oJ<;?Q=TAh%=<`<8xs;n4>!64Z4~wiJ?bb ze4ZfQC?j`I6^OEF^Uf8fgBn&2(OMX^Mn&X`4Eb*Eei12%K2+zSfL~Zual9xL3$fYMd|?N`S6%`%ZB&@5mC$TJ z7r^nox?F-&IFdOQ_4ov-4`sVw@HgfgJ7i3^+=Vr2Q%xLzXTeT=mPSGdx}%`Q4ZUik z%9BnZ+fS-1mjZZMiVYQ{4d;h@_f*C%#4PgP*2t}eflx2B67933HoiB>=Hs4@6fhUHv^G80J6L;LE>Sf z0@Y&Zgd_3PZ|1h|3&tXEzwzbe&BJif*oZC#2bms@#Ggr{hz zS1QR{+YlaG6k@?9$%lnEa#4sw$s;@#)Scdjj9f(1i1G+^eJay$cHN;o zzZQGPf%X@WUEakm2*HA^XIlF<1ijOpEl?P2=zqB;kdBV$K{D)ip|wWiV6(@yB2Z01 zJRUO_GdNW`a^z#iDc6vgfz+&eSVPV-R5*Byz?yg<8)E5p!NZdgQh`)WN5i?4CMCyM z`44JCac}2!d#{S0XE`_5L)Z6=*DdBD9D@(<#7GJXQc?%Mz}sbYM3;scp=we*kA^Y~ zIhyu|VkX4>Vd^}|_@N3z_~Kr28q|*ruO~?3_kIUlF2j8&nZ)s9r`2Abk{7qIQ_~}0 z0x8=B*Xh?c0OxBxxh0#1F*r!~5|U!DzzDBYUA8H$2WP_7J=|Agk|<7x`ytgUAA&%7y!YwR=g-H2J^mPB!5FzrT63rf(r%F#&i8~lNqm5S| zSP2B9#;EvJmznmq-d+vTI?eGly}1EIPD&z7c(_MNOttU>eXY=N6T#5=vjN6*Y0I59 z(+U$r;{dB&;#?Af?rexvKwnW|2y{MXS9q;#D&#U(;XvQ?0W!z_>Uy2Y{Re+3K_eBG zcz2pV_3v>+nLy?Ya)aQo94yWZ_k}~(Y|2{*VkFsZZm$}*XQL4vdGa-#5w=-qmWQyL zjo7-{mCQT+ANee58AAYA+P7qS^(BFsVLgwbT;^l|AkM@^5vO6>mathQ{S=a{t=K4n zIbljIFAFhOSi-8SJ-5z#Ycx*K>HKcahOGKc#dYa0q!zHu@VU+81`rVVYcT7M6xa37 zZ3&n;7oPctZ2@c`wf3w^WOx@vavO{p{Ft1+Z| zc<-{J7C-h(xY@f;Z2xx0;;yyk6@22FzxISPH zyHtPMqsGie9r_+t*yHI(ZVb`*-p&mycrGN~mcT4_ruXK1z>3&eI`KNNa;HA)5$C1R z(XJS_g(YG$F(mfK?6F=RX)7Cj8&4!f2s>w1(@un(*cx|;nyuz6aYZF(az549r+Mkm z+`UIAxhr$IrbfEUdff!-ERONp)J=#3;oJ!cXDndQ;^MB_M*_>gYIjka z;)A3WtTyU_)atpWgwX!NDgB}KV1uAKuX~i&!a#sG$3IwnYL1?`R4RFOJrg@+`7?0ir_N~T0>N9`43+@T zUk4!Z>%nCjK2g=GblZI5>in?1_!~f>&*6n0K2eK$^*(-^jR1Z^~r9tL+Dhgw^y8pGx#Ltc)S%_4OmHhYdO#AB{h7;pbj zuU>6~tkl2*0hzlhj=TX3mvL^5IK(6-GGF+|sPlshC+IHx==cNolz+03GK?l?1Kh&VF}qD<#lGc~t|8ZAWZH7k;$k><7?=&oysvzq;=^H!V{8hbkFu4~{JiAqkD>uU#!mQ$ymR*^oI& zxAGwApUl8oGqNu2xK-osHrsssvP)@fK_gFz-2D(7nM!hKm-7zUS8mQS_F#fYADGj!7^>Pe0 zY$BoGrwm7@J>fmJ^A_}iu zVANRSAmxAc#l3-Ozfo-Ts`YyH< za2lCN6+&7yOAQCzL4tHWR{%Z&^fm6ASUkn-t~;nO8PAvP9?2LyTWE+lz>xmU-FJU@ zb>bU2u*wE_>LzYx44A>d*{sO)~$V=mdtJ=(%6om4(Q{3O9tf2c3pcF2@bv z2eDMR6Dcd>{{2)Sb13Q-my!fj-qTN6NPk0EuLu9?e->q*A+tfoCJ9a4SEqxrJT2g+ zi||nAQjze(DT#_!k{Ep>e$_U-|Kw>DJ>}e0so&Xu3gy;9ko4Q{0Myx_1ToHg^03)7 zaYyJcvA$V#X+oa}I*}TYC=i#XAPYbf!a|j}rfJU;DT|3;PPMGoC#%r=)OsIMmJmDf zl*XSsjR$_stmbUVqQbMRI2lv%s``DnLo@bnirzfRWKu~TcRK~Cael2VSs9sVvpFEK zZ%N=~4sX8*g~Z;joK1|RBmqkGfRqLaT$h@V91zVTiKTleJkQp20W0+0!OMlQw13{q z{k3+801}i<=6X~Fr8-boJ7{_aAlPpJEgOrrE2DB#SemTb+$_ zzJhv)F@wVgs#i)ZlE7#vG<7=QpNy%$Q+FYR5a18%4OJK+3^H_@`&(zM&)zjx@7T}U zF(W6zO%WM|9!S%x)@qJ#x#n9CbHt(<4@QaAYr^#vbga?;w{>dFUWIEMIy`BQs3hp4}imxKN?r}otvBsAw|D?L+|=lRzcwX!*>bX}yO zQZ1c-6sgf7GaTP_81gZKRDwcCJ!Ibt5y^jsm~ePiJ;!}fky2P_;uv=wC@V}S4PGX} zrc>mc8ci2_pAQ}*-l08i03HaKh$-h8vFl}c`~(3P@Eez$(PrpoWHl#eEFEN_UgPy4 z)x>ki5y!gu7xt!%a**hmnK)y334raR3Mq@Bg&BzFDw867)LNT!luBiKPUzXQ*cXY8 zT`yB)jJ7U$xZ{0T)p{5-hA&UY}UIx%{w>5(}tR5-}ZVEGRu5+#;K43PXL=XXX|&u-qc2r=00&3FTdp3x#dm+w9tBC1P5Wf zEaAXe1}s1H&dU42?l$6APO|_Xu41aPut*~UjBJ_D+@w60mJo#UuusVN2C&^Ycs(U> z16U0fHiQ)5^`akQ+WxW4Hmmidwg|G~%Ng}SN4~fi3UWVWu0HdN1*}w0u_N|zl7kwsnme~3vs$Pl1N){#t9_j%7N%QD9_9?fsYz| zQJri&EzUsbQ$`1x>q?R!0kMy-Pr9KAl2U&FkOcnQxUUDzBQ_dP+cfRk0RBGuzhgB{ zgwKqmk;d{(S0QhCmBtEFmyAY}PhId60-WQtb?$(^o23Cd09*WIhX%unVS4z=x&! zehEJ_$DBDquN@IG1bO9EbXQgbSqsfh`<#4a>kQTu zv4`L2$@DNSP|Ic~QtAiu_XH5`hlzFs{_HTlg8PT?zj(W0jFmw|Jz`zCke?Z4?K#LYh~jSzzM4fxmH`>O*IJ!wIbCUC7RgJj8Hc8 z{c7k};$kgO@R(@DPAu64JrJXe*FwYuH^b*Q_vI*)kI>3CFNxWlTZi+*8-a6lR$3iR zrln`{EERS`8QdWC(_LykxsiUuTyTs&89SoElX$lLaE$)J&lMeRpE-c}JH3|;l>ZtD zol;w2YmDX#u)B#>H-KUWK@x0X2%eD+)o7{5TqO`sF-<^RYpL;hNo>G5Qh(0Zh}h6iGu`|h5!*XB5ezk zG<+xNB9hv<9t?EuX^moreDJ`V5i6MV7+e=A09~`IQibsN^6WEkK`l9gibtVTrii0f_*&? z(j_6$qT^3XopK^Qv$nF8ennK?NoJ;UWZsqZbwDw-5ju)JVPsGnIJvulUYF;2{Y9q# zw~1Xrb=e!|{1w02EjR2PLij@P&WsBep)5T$_5L%o3-BnFzqKg-1qhal{8C0mH12sNZ&3RevqBKd* zwg1uX?R#puc-NffUIuQDRg_&BTC0s`vr}{4PC3E8a5(imu{s&i()+3QUb86=^4XO1 zdaK57uR!yz<}X(oSj^5qaEf6%iC7B9Fd06@wKQ9dAJca*{j^bGIe4==*1UAIqSZI} z3}HzNF3xMdKg=~|=M76*7zBg}i8}&GMfR@=ZJF>@Co`R{RNo124owQus@(>l)Vyy; z0As0#`y|bUP6q#!mXchyx~IqF(cA`0zJ)#dbJm-(#z8Fw&6m-;g@d*p00sDGEu)#!3HZ#u|U$q(=Kj; zw_2N9**5?@bsvpoQkuDTZt89tY&5Sv!6^$Ji|x@s8Ult~gmk*$E?cK9+)e>%$U$J2 zh!__todDBCNrJxns-fYnVkB(ZlM9E+4QLJIWt_ zZ+tqynEe#<^nRpT-8umer$h6yAWPbBI!*!ghjh8k=n`U~_^KqS{h2=`@ZB9tu>&e` z4>5#do5uHDYvK_n4K_63Z+dUU>40+4+Ix+Kpq}72hy>Lr|G{}tLkdmyrsLgU_ahYZ zsjKo&U^&%vWkK_ozJn%%;>{1x<|yaI60HOoXN*utK=Yw7q&TR~{fkeM6nWwpOI}s6 zM|0@RUVR_&>;fxlmT{oZ?aku!=(cE<1?v1K1R_!90BvPKKzWkd% z+aAu^6=^d^H{*`sxBUPcML|ayfp|QZzasSoe7y(RoDyW9WyqD)Ol-$wr_1MEUfkcH zOCCof38vQzOfK;fpvFP@3SXXFwecTE6<&kg5=1AeFDzV)@Zl$_e|^j!tbQ6z{#J`q zab_3$s`-O7qsK7jFCA!y7PV+o4h_sQF`=6xUqRGgZv+ar&F-+>Gp zA|5k&9X1i8CH>BqJTL8)Or!W2A^`=jo+~HpX47X1oT%OdEVBa)96xw^zPwWk&YWQd zf02O>q>y`_E@avYi2r%xaxdpegsjoKXg75pz?9ys3t;&%Kz0bjU>@Czqw~xFt~xrX zUX(O~xlqAZUoB>WdKlPg28lI!JYb91t7uC-kA{?26U1l+3;Fc3lZuWmm*Tcs(nEk( zWQO?Vm}Pd1#1Df=X8i4YO>P$QAq_-nw6$^PK6);Oka)I$13D@Hi3<9HS?3Gxa(X5Z zr^qnRtPXa7$5Nf1?DgW@7J;bsQ^M&BJ}KN|IK7slZok*JElffw1!Lu$5~lt5Hhy2J z=i|60URg5RBUDZYIILyLA{!TpgMQo1$fKgDOq_5TT6y&xQo{A*M34t*p0=65gH)P! zELw??5-OAA;&12f!bz{ZW7A&)G{$VR=e{%HRLp6i2_Tux=BPfdO$-{CN31}#eGlNz z#}aWtKK+Oy-v&QGV{9?PWnnF%9qdN&;gP@ETJj}XHf*99RZ#zxtV?TJnq@6dlbYG) z_LLIyl(7$~p})y<*;7av^1!j6J06P2Z3xP^Nj{r}O!gvT&Ck71o!jd)!g zJ<*tWt_z8Ybe}fyH*nDeRu3`TH}C~psdq80;iw~nM-$j-o5?6N^kR1;kR{bGzbCj4kx#5A z*m1Xwcz359%w;BKkT~RG?hCOlHR`C1*0)`*)4^>mT10*(_439W)+RP%g_sCnYw_M#%% zdf_LgTIn*dwcWU1^CH^)2!cGk40?Ygh`@b?T92yva~PXG19sy$`I_z_cXnB4uSgB1!afS6hZSd#P~;s&4`0^@y#+s@yS(Mjz+U7pmm z(Gju(v_^B}Q6Qd+!eJ24sx|*vsfPVu4iZwV*@YT2WfCSRN4|n?XdjMf^+_CGV128< zSUB~+Akz+Gt}15QjMnP>9bo$F9lZ9OHJs$dR5R`)HYCAKJk$xu=f5IKJ*epi;6A`P z;=3;mjQF7`#+J|>tpkxq-Uq}d|S@32~mfiWbur$TU2SrNV$eJSfrTRZUBaZCrGk+`8MR$@eN>h zgrzZhCa80Rj|D09wDe=R+W zdRo1?I!on4gdqhuiW)d?c0?Z*FZtl!eb3<-{jCaGh*cC*t-Jw1BIL%OAs-(Aj&m#L{Kn17yp~na4Fj{}Vk!=J7HLvF zBxBg6$_0|4-s$&?dJ}CVf5i)38y&{YGKFI=9{VU^8pDYNsi>w2mM>L)$p!kfcS=L^ zQg$y}RI#Oco#Gn1Uyv)#@8)XCd3CEyzs`DGRJuvHzFzcmn`P3QNN^i*oOMWZ@P&KB z59MOk@zgF#zl-?-F`^=yV2wB*jv_O^Ia@cU607*PJJf?pmir6;g1`wV31nHN6o{sKKif{X;pXFe2S>Kl9C+>aJuj+|@J;F17UAmHR^GANzC=scsIxDAn) zzQ7p4TaS5V9V#TV6!Uvvb#d>)gP_%hi$iW-EqG^9WF%a*2 z#G(!tKVX@uHgE0YGsh2d)wZRcyLYD_9sAK|!fpULw3UKl#eh-`?#Oi(d@PsXon>Nd z+@xyNki}(Lz}AG6fFqQA9IdZ<3~_l_lqSGqzC7XeS!>}j!@Eq+>j%U4sN`Ud!%P}5 z*-|yZIExFqdlw~mZ+DEIcA1?r@_Ik)`a-DxX_>g-U;gZoMg({1F|4fAXh7w0g9DCn zFE#8a`iW?<_=#9$Y#W!BoN6WK2M9}?QR+7Mov`_dGQrxKf4-0yIEhL?oRa+UE3xBp z!X?0fGfAT)LfHoF2uspnUn`HWM=v*-29le08N;LPQJYBz8@yH>{LG~qn)KlzT0p~V zEmdk!=&z^m&#d0rR*ls8c$O`}t5;9ee+^~=N3YW2nV*A^eNwf5kg8*Y9`O}!$qhd` znaQfsy^R!LNs$Ke!B80MM8ST11bNfg-=aKxnxOJ zssSf^6Rft$yn0ETbX7(^=ktEhrPL)dBj)$RnWKYtj|kc!bv3iw=#{3Oy>+6D;q4Ggub8(eCWa7iqLpZ{`D{dAKCUmtr+0O+* z8#-S8$rQS(d;`p{!l{0;DS@zO*Iajxb!Gjax_>s9`6wblVh$ac{x?9HJ!3$R z=a0gS=s>S&wF~~nZp$@!+!ngvwuZt=9NYcH+5c>KLhJ;ca2`S4U{u*{fM4BCXWWe-EXS#}eRQVOz4ISC(TW5b>G6pvxxn|XSMF4FP65g*weNwCC^ zM8sjaq-PfG5aXzfLll~AaE||4=Q7)wH15gi|7yWNZlH^i=h@x$<(>0Lcy(wdKa4Yb z|Iq12r~LlV0%HcGABXuxUWpEuH-H3cJw{&uCKRXSq!!E;-YSQ%R`|j&LFIqPR1S?$ zk?+f+&sEvVp?zXCO#k-kg|wFcU!jpSIW3jP^g^50^(>znL7-+(u8L>aY~WKg&HGCs z%_y2{7!QiQbG=r2)A<~1zFquInm0efpwA~Kph88uceL-3Nmn||0if8@fK=hgL# z2Vr3a><9tHsfm=~q>0yy10@UDYu<5De!q9ho$=P4@izF?Rd-j&;!GWveB$W|@~Ufr zY}IvUG`U*%0czYB3f$R@f7hta6WyiZg|5SR$pTYSlrkTIyXnCT!7op@d63X`Dx`To zB2Q*~`AlE&c#OE~vaojf(O@46x z$XQ?4Uw;3v{o4Gf(3s5&X%&cLPvJ)#EPHi(-%6C3R!f}crTh4r+{8`S#&G6{c^^*D!M=4rwQUlA8u*=py2ZV^p@Ls_3A>2@0eOu4vVZ?DMlx(+X0J?g z$U(+*mJyq#)dJsGgNvc9%Bpa%6z#q=FE+`Fk|)PVf$E2dvvu%9Sz+AFl*>mJ#RzGF zSgKcGKl%gM*9kiEGCI(As|*dR3XOfMaEkw`6@|hqfz^>^0tl`MnhQVn$)_tRFRG@h z((NONjsx&K4Eb)eNR!_-`EN6E#YbCEK9~4SK#}`TuWCj4d%3BD$l8R_zH3&3EIx%j!QU$zy`~*>Z z`J*u!NyVeDqWyJGt}5F2WAM?rTt4|bu_UvGPUi9`Y77O0>S^3EfrSx`|9Tpu!<}kH zv3(Zg27iARIS(?GwGn@(lo9;CoqMTI)I8HoGl-t5%mab4;hg7hdR?Rr(rLPAlYPJ zysM}rQ+nDpfMU5`1I&nEm?we*Ar9a$hSu=aIEHv6)`Fm3p;GkdQK zxU1nQ4nO@hP7l&W#MhaAg*`LiWfmv4r*AJOpA4}O=XF#E-!8*XT698%7SER883ECu zUd4M6G{Q2_(x>U`tk=J7NWvTW<}nGw`t?I%pR_VW=fPfc3>G35_ftU%O5z1NFrd7Y`0&B+wLjm`?>N>)Up==e3M;Eus+@Y~ zOy*+Z&CGxh$L}E-!tP3%I?DFQ^^usXG~!op9Wv07WRj!91|tKuCgozON=dQ?ooO*c zmfvT6uyCVoZYU)m9u6rR{nHfJNS2^UDcm5_S<|&76N-q%!)xN}0nqsXS(iaV@+WKI zkHTlJ$rwr1L%t%s2lL#o1mxt8^k>8J3a(1Of|HCG1Y&@B3ew0^F=i?`7&C*_>tIcf zL&Tk=GY5L~pVR37Tl2Z+OAjLT=^g+Yc_R3mc8Ym7K7Y=wE)mrP<}=7w*kM0*{#QuI zpn>hkqd_KPo`+OI=6dR3$^Hhdza>QyLjTc|-ePiES0qU1d82(nBPyoU#XKCqmU~Fm z$t_y36OEXn{4^#$dwfvK2s@ao%t#F}KJ|6kow!>UL<_xk3?W;Jy{2Xe^U-%wtz=hB zYwL~KFB9jwY)&=bKv!`4E#Br++{%Mw4%PoBR3akPBm|keCn1#`nK2r3KZ+%`{UBXN z^RE!5OylLn8Fw?U;qh5^<}HR`k;S__7~q=)!~70y`j=4Soh+(a^mIrYdK&N3JyPi#KqQr@7k&auyK}f7-9vy^k~CmZ zzc(Jq$ztpTveARjq)2zRtb0Km=UCfAZz&Hkae@z2@;D z*f)Br+Jw_`K^<{FDhcxc;+#-Z-&@s2H6a6oXpaVPm7*fkLi$b;X50P9O@z@H51b!@ zL>BIuCB#g$NshXve?kBMm+vX5b3=g}k|5e?vF|NVEkZ0jVE0xoqOMSC%&qW$rv^TD za!}*{Q&w6+IE~lix81;2ZOa^?MsP6PC+amhe{BhTfyX#V0YM?Y-eN>b3^hFA3r30? zC4Cp4ZTIb78U5dM$t3PVT!il|PY^q#g^(2&!MCx1AXpZ)%6k7+LLYt8(G4I($bIo! z8y6NcIIGuXgux)yN4CQ9u1nI(q=v}4eo^M%Ms1)%kp*0{Im;vI5w!QaA|DtnmKr@x zXP_s@`gQ~GA;D&!oU12Kl|8uLiNpvAzuEZCNqLr{)zR>y`Xe6O>Flv7&-5Q6!iN;G z-X{>!wP=0PoD)w9RB7Nsv%ZWc;2|w+CNzUc+~%HS{vix2DK9L(=Jx-A#OOd zVc$yi_};<6hcGeys-QqR|NY)8r=m=C~N-^f~e|I8^F?S+(B17lDmvan)Qc(6T*uk!h@Ng>euOLn=BR8*Q2jl|cBM~R!yvv9 z#X@Y9uQL7v)qh#7&$+QH*|0v(eOtP|Nk)-ICo=a|b!f2}p?I)Jk6Te5Mo}B@KtKe6 zI3-?&d;Q2^k6EY5tz5RKegn{0qM&B;ELk50BCdvmzwi*KKu!?ddH z(nM;(8b4}rBEQ`?NnBz!g;XYwofyHA7I^m)Qyab7BC=9~ko{NUB)iwtb$C;wVAnQW zU}LMnr7}@EJafi(IHHXgDt>f9PG1sM^Z;Y zO=+@{eeLyrl7tu0kjBri8UtBHUA{8M5WHd~?m#Y>#s@oMWOK>ydNOZWwN;4JU1oGAYkFN7fKVYr{ZSB zV>|OXc5xlgLT11P6-yS49p&~iNHS9+qNFgvFKreehJGaw*E)qILB}ojVOX*6aR_x$ zyzkiv0xpGnY@(qNWc2jW6%}QHhK-EjnE_i9=r!|qZf3M(Tpskyz|m+R(m=yg92FiXsVSL)t~^W~2Et6vpc z$8du7enC?L-G_Wm425v$p;DS z5@#I8$jL-NtbIN6WlmwEs8wElb)gq?Lf#rUMsQRUB%UQx@hGuc0J7qtuq6IHtkwGB zrZxjpT!e{xb$`lbu7TPZL0T^Doivq}90V!JR~jFp&QlzxuE|(c z&R#g``PlB4a(=nwk}~QuMU$Bj8M%%8%Tr7F!F;cENgn_)1R`cVhWF;0s%>i=KzBT- z5sDZ(Sfv^`qw!PSo85MK{Fm?4A3=3F3STuSI#Nw=hrAz9SJRq@z_o#qo;e5;lCShJ z5ejS>@u`_06aLcu0i`0+Y!`0eXf}dHW>^aNozr(vI;=>l2=8r!QQu>N^n5o%Qd*_Yr`hLD^WmH(_gDWeG5xFoAAO;;Y(1jVt#B zL+4K&YC$UOJN3n64@x|{BQsVaWme#%ui7EnOljBLG<*a+rj~f(hV=&2U;yQz)>9r{ zWb%O~IS}td7Swx4}R^^x6>jjU^_tt9sR0%e1 zx2YVWUx{O^ry?mLZ3`#x+H-2rJf_;%PNRDF3~nR~{8JM&o^e5`XR`XE8TI1EU=oD5 zS~8lyoMA|2$w0!(Y8hXGX!2=AIL>iCOK2A-j{L9>qIycq!psd4h2Fu*VHIaHU?mkk z{KUMCr==o<0-cXq(sc4DLgv;qgx*e#i z38J=cD@QdcK|@FDGokVmjh5Z$Tv(_<%#+vn+! zw=+RvFWS(|VOy`V->LC<^%vpuR_?QS9>(5rUr1{&g)O72_0_AB@$OAYhXH(v0Lp-B zfvCv0kzLw5n~HNzroWOj1w7wVi^JDAGxfWFx!hxb^vPV_eKX8jl6{K@_K)`U=}b7e zXP7Fh{m9jMIBxv`#CbCT5TuUSi(pP4+Jq{s;XLs(NyNALU-9B$m>|&&5ix??3*_+a zc8`?BH(&nzn~kiYLDO2Kv)47{P~Qo9C(bkwDY)jnYO=}ldX}uqtQw-rt%(OxyPwfK zu@71H`^frfyivM;)A;F6?Wq51BfdrY)ws{R@>jdIKJ01oc@H&dye-eD%D&dBWX#Y2c*<0+1ooK(v-2N|! z(@EsLb*X#-Db~MCvz0J(T;*L!t{=kff~WQ$L+}MJUN5lLa?!GRAvKUF-<6~fo^M&( z5(HltBxN}4M=%5C93yDDog>N_M9zlRzup#|7+UZ@Myr>LHdhTornjfieJ`e*U=wz7m#+F zx`l>*4PH7Zj&vWSF0>G8;ud}}oL>%mKK$@GjxG+5NqRppphQ+Q(hXF_1k{;XbI9>w zc#1r5UsFh8#EummUC5cHxP#Dj`G$lyDvN7vjK189k3S@~qD`N;ir zgEmQ*fO65Gd-NJ4%Q5<{jaEP6t2%m;jw{aU3`H&?}fOQqDUJ*;6%RPtsu>+y&Xa7rQ7+;FRu zRD)!MfZCseBg&gS{wXd2#chd?ek`$%u_w+Cg{W6FETkiG&in?u$_Cf$R~_=VvyN(Q ztBS=)7F3gOh#<)2;2L#(+qeE`QsJo>o* zxK1iY_49wwD_Nwj#TP#S+ES@+=2G2^)TAZQw(g|OO zwZ+?U&6pS(R-CW9nB|l=-}`6GUko{T?C!M8dHaidP1W(U5Xj}~1y&RGv1@j)yLu(N zKA_FC*BOlM#Q3aIk}^?hW8Yrx`hbrPyTYGORyUcIGz3o*v0wsu>*F>iz;D7v%fl?X zCx;D9NlxpZY_H^I-foiGLiM#PVf3V@K2g37Q7*0C&9mTKM)lb*>7_C;QVt1x&Bpv3 zai(WSiRzU-zoxAS&Z<+5^-bkq=XC389joM->0Hq<>kBfWcpukBK%0>E=PC(u+XvLY zAAX5(Zb6|}(2Hih;&sA%yyL17!qOUGJEu*eix@N%ouQ6J!RCqTlV`;S{RtA<$=;(IF9tj>VZJ+lG;x^9E;tc* zV5G&gGz070H1NP`OxHn-KZ|!*FKO5$rS|BTmoc;_oq_i)j5dlsTUPEh8Xf!2R@b+? zs}^$fM6)Gvj1w*)@Oj1|CcN_-`1 zov4&*iBwF1Vo%%0FScKHvtC5e#F?&;PO=Qx!rz2@YZW$N@g!O*3DqBeTL`!g(aJyS zbF*}3_C7*ZYZRfYw7cxOq#2Ye2S2aL)>ej;t5i@@JPMu%$*km)YvadS^x?C~FE&sY zQwK!W5tR0!c5#Qi$L?a$L6b4H9dYCzk^*D-o}&;ThoySo2+ITcD@cd|ue5r-dZPk_ zmi=?esFd<1w;T|jWGc@f{i*cGpDBN>_aSGB>FD2x`TbWAtN{3+Q8#NO*xe_k<=p(@ z5rn;u)erI$4srT9A-(woQ%ZIM7_sn&VE(nz1@dTKBOM7Z@q;o*0}?$eZc1%NWaUu^ zrGe^vbz&Hso=3BRN1 z@H4YrgCMHv_v3UD3QvY9^!{o4YH0Df3&!Um@>!=MW*8&iTX?YSyCgsexM@^JF2Img z*LK*$@}j^GHcV3o8lk#ofo~we){Xyx<&8LFe&he9-#mbAd&_}r_~ocsygt;|9x4&# z3Ev@l*c#ktI~i!Gy2RKYe=kmj=u8A)Jd6kMKR$)W*?k}#*x0i&>##NVtdPx>DLi*I zF|uQ_xy{hH{i$NEA%peL=}J6tIyb|06y@OnG$kOgd4{Wf*ooY)Dy;;Aech7sJyBpm zDck+nNZ}X#+@Cw|kDU5ZemWlyE>KsA;Z1n#|6Ht_<*_QN$rJ9na&^vp?>> zVu9)MEu%udM4w^be4t)|O6=x{&awjmub< z^J}EVx<7X@U1?;^V*!tN7jcGzI>*$Qk~DYhGrOp#tKZUo^~W?M#W`Ll$=O=z*RN;- zO*J8(lI{oHJ3GgWOQ%X5j+MT9EwRY5xAj2{aqWx)WNO~?UV*kB|CQpo>-F<#?W;1& zriwcHj$<798lSY>uU6ULb@GZn?iJU3m80nK6%y4D^)ANI_0eJ@<^^A^(tf)yI1*S( z5^Mt?@h5aH>`Pyb!XT69%rdhNp`9C{=n=H8G^!l&O#@+cJ-6ZQH4m=x$%0DutM;&3 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 literal 0 HcmV?d00001 diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with slight differences No Metadata - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with slight differences No Metadata - Manga.json new file mode 100644 index 000000000..b8e275a05 --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with slight differences No Metadata - Manga.json @@ -0,0 +1,8 @@ +[ + "Toradora/Toradora v01.cbz", + "Toradora/Toradora v02.cbz", + "Toradora!/Toradora! v01.cbz", + "Toradora!/Toradora! v02.cbz", + "Toradora! Light Novel/Toradora! Light Novel v01.cbz", + "Toradora! Light Novel/Toradora! Light Novel v02.cbz" +] diff --git a/API/Controllers/AnnotationController.cs b/API/Controllers/AnnotationController.cs index 6601087a0..187259572 100644 --- a/API/Controllers/AnnotationController.cs +++ b/API/Controllers/AnnotationController.cs @@ -21,23 +21,14 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; -public class AnnotationController : BaseApiController +public class AnnotationController( + IUnitOfWork unitOfWork, + ILogger logger, + ILocalizationService localizationService, + IEventHub eventHub, + IAnnotationService annotationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly ILocalizationService _localizationService; - private readonly IEventHub _eventHub; - private readonly IAnnotationService _annotationService; - - public AnnotationController(IUnitOfWork unitOfWork, ILogger logger, - ILocalizationService localizationService, IEventHub eventHub, IAnnotationService annotationService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _localizationService = localizationService; - _eventHub = eventHub; - _annotationService = annotationService; - } ///

/// Returns a list of annotations for browsing @@ -50,7 +41,7 @@ public class AnnotationController : BaseApiController { userParams ??= UserParams.Default; - var list = await _unitOfWork.AnnotationRepository.GetAnnotationDtos(User.GetUserId(), filter, userParams); + var list = await unitOfWork.AnnotationRepository.GetAnnotationDtos(User.GetUserId(), filter, userParams); Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); return Ok(list); @@ -64,7 +55,7 @@ public class AnnotationController : BaseApiController [HttpGet("all")] public async Task>> GetAnnotations(int chapterId) { - return Ok(await _unitOfWork.UserRepository.GetAnnotations(User.GetUserId(), chapterId)); + return Ok(await unitOfWork.UserRepository.GetAnnotations(User.GetUserId(), chapterId)); } /// @@ -75,7 +66,7 @@ public class AnnotationController : BaseApiController [HttpGet("all-for-series")] public async Task> GetAnnotationsBySeries(int seriesId) { - return Ok(await _unitOfWork.UserRepository.GetAnnotationDtosBySeries(User.GetUserId(), seriesId)); + return Ok(await unitOfWork.UserRepository.GetAnnotationDtosBySeries(User.GetUserId(), seriesId)); } /// @@ -86,7 +77,7 @@ public class AnnotationController : BaseApiController [HttpGet("{annotationId}")] public async Task> GetAnnotation(int annotationId) { - return Ok(await _unitOfWork.UserRepository.GetAnnotationDtoById(User.GetUserId(), annotationId)); + return Ok(await unitOfWork.UserRepository.GetAnnotationDtoById(User.GetUserId(), annotationId)); } /// @@ -99,11 +90,11 @@ public class AnnotationController : BaseApiController { try { - return Ok(await _annotationService.CreateAnnotation(User.GetUserId(), dto)); + return Ok(await annotationService.CreateAnnotation(User.GetUserId(), dto)); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); + return BadRequest(await localizationService.Translate(User.GetUserId(), ex.Message)); } } @@ -117,14 +108,76 @@ public class AnnotationController : BaseApiController { try { - return Ok(await _annotationService.UpdateAnnotation(User.GetUserId(), dto)); + return Ok(await annotationService.UpdateAnnotation(User.GetUserId(), dto)); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); + return BadRequest(await localizationService.Translate(User.GetUserId(), ex.Message)); } } + /// + /// Adds a like for the currently authenticated user if not already from the annotations with given ids + /// + /// + /// + [HttpPost("like")] + public async Task LikeAnnotations(IList ids) + { + var userId = User.GetUserId(); + + var annotations = await unitOfWork.AnnotationRepository.GetAnnotations(userId, ids); + if (annotations.Count != ids.Count) + { + return BadRequest(); + } + + foreach (var annotation in annotations.Where(a => !a.Likes.Contains(userId) && a.AppUserId != userId)) + { + annotation.Likes.Add(userId); + unitOfWork.AnnotationRepository.Update(annotation); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + + + return Ok(); + } + + /// + /// Removes likes for the currently authenticated user if present from the annotations with given ids + /// + /// + /// + [HttpPost("unlike")] + public async Task UnLikeAnnotations(IList ids) + { + var userId = User.GetUserId(); + + var annotations = await unitOfWork.AnnotationRepository.GetAnnotations(userId, ids); + if (annotations.Count != ids.Count) + { + return BadRequest(); + } + + foreach (var annotation in annotations.Where(a => a.Likes.Contains(userId))) + { + annotation.Likes.Remove(userId); + unitOfWork.AnnotationRepository.Update(annotation); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + + + return Ok(); + } + /// /// Delete the annotation for the user /// @@ -133,11 +186,11 @@ public class AnnotationController : BaseApiController [HttpDelete] public async Task DeleteAnnotation(int annotationId) { - var annotation = await _unitOfWork.AnnotationRepository.GetAnnotation(annotationId); - if (annotation == null || annotation.AppUserId != User.GetUserId()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "annotation-delete")); + var annotation = await unitOfWork.AnnotationRepository.GetAnnotation(annotationId); + if (annotation == null || annotation.AppUserId != User.GetUserId()) return BadRequest(await localizationService.Translate(User.GetUserId(), "annotation-delete")); - _unitOfWork.AnnotationRepository.Remove(annotation); - await _unitOfWork.CommitAsync(); + unitOfWork.AnnotationRepository.Remove(annotation); + await unitOfWork.CommitAsync(); return Ok(); } @@ -152,14 +205,14 @@ public class AnnotationController : BaseApiController { var userId = User.GetUserId(); - var annotations = await _unitOfWork.AnnotationRepository.GetAnnotations(annotationIds); + var annotations = await unitOfWork.AnnotationRepository.GetAnnotations(userId, annotationIds); if (annotations.Any(a => a.AppUserId != userId)) { return BadRequest(); } - _unitOfWork.AnnotationRepository.Remove(annotations); - await _unitOfWork.CommitAsync(); + unitOfWork.AnnotationRepository.Remove(annotations); + await unitOfWork.CommitAsync(); return Ok(); } @@ -173,10 +226,10 @@ public class AnnotationController : BaseApiController { userParams ??= UserParams.Default; - var list = await _unitOfWork.AnnotationRepository.GetAnnotationDtos(User.GetUserId(), filter, userParams); + var list = await unitOfWork.AnnotationRepository.GetAnnotationDtos(User.GetUserId(), filter, userParams); var annotations = list.Select(a => a.Id).ToList(); - var json = await _annotationService.ExportAnnotations(User.GetUserId(), annotations); + var json = await annotationService.ExportAnnotations(User.GetUserId(), annotations); if (string.IsNullOrEmpty(json)) return BadRequest(); var bytes = Encoding.UTF8.GetBytes(json); @@ -192,7 +245,7 @@ public class AnnotationController : BaseApiController [HttpPost("export")] public async Task ExportAnnotations(IList? annotations = null) { - var json = await _annotationService.ExportAnnotations(User.GetUserId(), annotations); + var json = await annotationService.ExportAnnotations(User.GetUserId(), annotations); if (string.IsNullOrEmpty(json)) return BadRequest(); var bytes = Encoding.UTF8.GetBytes(json); diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index c5a7b8fe4..057a9bf49 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -24,6 +24,7 @@ using Hangfire; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using TaskScheduler = API.Services.TaskScheduler; @@ -557,6 +558,15 @@ public class LibraryController : BaseApiController await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false); + + var userPreferences = await _unitOfWork.DataContext.AppUserPreferences.ToListAsync(); + foreach (var userPreference in userPreferences) + { + userPreference.SocialPreferences.SocialLibraries = userPreference.SocialPreferences.SocialLibraries + .Where(l => l != libraryId).ToList(); + } + + await _unitOfWork.CommitAsync(); return true; } catch (Exception ex) diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 267dbb7af..01f78a370 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -87,11 +87,11 @@ public class OpdsController : BaseApiController private async Task> GetPrefix() { var baseUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl)).Value; - var prefix = "/api/opds/"; + var prefix = OpdsService.DefaultApiPrefix; if (!Configuration.DefaultBaseUrl.Equals(baseUrl, StringComparison.InvariantCultureIgnoreCase)) { // We need to update the Prefix to account for baseUrl - prefix = baseUrl + "api/opds/"; + prefix = baseUrl + OpdsService.DefaultApiPrefix; } return new Tuple(baseUrl, prefix); @@ -103,7 +103,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/smart-filters/{filterId}")] [Produces("application/xml")] - public async Task GetSmartFilter(string apiKey, int filterId, [FromQuery] int pageNumber = 1) + public async Task GetSmartFilter(string apiKey, int filterId, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { var userId = GetUserIdFromContext(); var (baseUrl, prefix) = await GetPrefix(); @@ -130,7 +130,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/smart-filters")] [Produces("application/xml")] - public async Task GetSmartFilters(string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetSmartFilters(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -162,7 +162,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/libraries")] [Produces("application/xml")] - public async Task GetLibraries(string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetLibraries(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -193,7 +193,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/want-to-read")] [Produces("application/xml")] - public async Task GetWantToRead(string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetWantToRead(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -224,7 +224,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/collections")] [Produces("application/xml")] - public async Task GetCollections(string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetCollections(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -256,7 +256,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/collections/{collectionId}")] [Produces("application/xml")] - public async Task GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -288,7 +288,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/reading-list")] [Produces("application/xml")] - public async Task GetReadingLists(string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetReadingLists(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -320,7 +320,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/reading-list/{readingListId}")] [Produces("application/xml")] - public async Task GetReadingListItems(int readingListId, string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetReadingListItems(int readingListId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -354,7 +354,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/libraries/{libraryId}")] [Produces("application/xml")] - public async Task GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -386,7 +386,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/recently-added")] [Produces("application/xml")] - public async Task GetRecentlyAdded(string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetRecentlyAdded(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -417,7 +417,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/more-in-genre")] [Produces("application/xml")] - public async Task GetMoreInGenre(string apiKey, [FromQuery] int genreId, [FromQuery] int pageNumber = 1) + public async Task GetMoreInGenre(string apiKey, [FromQuery] int genreId, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -448,7 +448,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/recently-updated")] [Produces("application/xml")] - public async Task GetRecentlyUpdated(string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetRecentlyUpdated(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { @@ -478,7 +478,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/on-deck")] [Produces("application/xml")] - public async Task GetOnDeck(string apiKey, [FromQuery] int pageNumber = 1) + public async Task GetOnDeck(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index e33ecebe1..4172983d9 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -665,4 +665,16 @@ public class SeriesController : BaseApiController return Ok(); } + /// + /// Returns all Series that a user has access to + /// + /// + [HttpGet("series-with-annotations")] + public async Task>> GetSeriesWithAnnotations() + { + var data = await _unitOfWork.AnnotationRepository.GetSeriesWithAnnotations(User.GetUserId()); + return Ok(data); + } + + } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index f67b90a43..ccdce18b7 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -42,6 +42,16 @@ public class UsersController : BaseApiController public async Task DeleteUser(string username) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username); + if (user == null) return BadRequest(); + + // Remove all likes for the user, so like counts are correct + var annotations = await _unitOfWork.AnnotationRepository.GetAllAnnotations(); + foreach (var annotation in annotations.Where(a => a.Likes.Contains(user.Id))) + { + annotation.Likes.Remove(user.Id); + _unitOfWork.AnnotationRepository.Update(annotation); + } + _unitOfWork.UserRepository.Delete(user); //(TODO: After updating a role or removing a user, delete their token) @@ -108,10 +118,16 @@ public class UsersController : BaseApiController existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize; existingPreferences.NoTransitions = preferencesDto.NoTransitions; existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships; - existingPreferences.ShareReviews = preferencesDto.ShareReviews; existingPreferences.ColorScapeEnabled = preferencesDto.ColorScapeEnabled; existingPreferences.BookReaderHighlightSlots = preferencesDto.BookReaderHighlightSlots; + var allLibs = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)) + .Select(l => l.Id).ToList(); + + preferencesDto.SocialPreferences.SocialLibraries = preferencesDto.SocialPreferences.SocialLibraries + .Where(l => allLibs.Contains(l)).ToList(); + existingPreferences.SocialPreferences = preferencesDto.SocialPreferences; + if (await _licenseService.HasActiveLicense()) { existingPreferences.AniListScrobblingEnabled = preferencesDto.AniListScrobblingEnabled; diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/API/DTOs/Filtering/v2/FilterField.cs index c00c15708..51f618a8e 100644 --- a/API/DTOs/Filtering/v2/FilterField.cs +++ b/API/DTOs/Filtering/v2/FilterField.cs @@ -87,4 +87,5 @@ public enum AnnotationFilterField /// This is the text the user wrote /// Comment = 6, + Series = 7 } diff --git a/API/DTOs/Reader/AnnotationDto.cs b/API/DTOs/Reader/AnnotationDto.cs index 729f3ccda..13dd6fe39 100644 --- a/API/DTOs/Reader/AnnotationDto.cs +++ b/API/DTOs/Reader/AnnotationDto.cs @@ -53,6 +53,12 @@ public sealed record AnnotationDto /// public int SelectedSlotIndex { get; set; } + /// + public ISet Likes { get; set; } + + public string SeriesName { get; set; } + public string LibraryName { get; set; } + public required int ChapterId { get; set; } public required int VolumeId { get; set; } diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index f4c72bfe8..9a1b1a70b 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -31,9 +31,6 @@ public sealed record UserPreferencesDto /// [Required] public bool CollapseSeriesRelationships { get; set; } = false; - /// - [Required] - public bool ShareReviews { get; set; } = false; /// [Required] public string Locale { get; set; } @@ -48,4 +45,11 @@ public sealed record UserPreferencesDto /// [Required] public List BookReaderHighlightSlots { get; set; } + + #region Social + + /// + public AppUserSocialPreferences SocialPreferences { get; set; } = new(); + + #endregion } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index dacf18920..7f9113781 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -13,6 +13,7 @@ using API.Entities.Metadata; using API.Entities.MetadataMatching; using API.Entities.Person; using API.Entities.Scrobble; +using API.Extensions; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; @@ -226,30 +227,18 @@ public sealed class DataContext : IdentityDbContext() .Property(x => x.AgeRatingMappings) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new Dictionary() - ); + .HasJsonConversion([]); // Ensure blacklist is stored as a JSON array builder.Entity() .Property(x => x.Blacklist) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List() - ); + .HasJsonConversion([]); builder.Entity() .Property(x => x.Whitelist) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List() - ); + .HasJsonConversion([]); builder.Entity() .Property(x => x.Overrides) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List() - ); + .HasJsonConversion([]); // Configure one-to-many relationship builder.Entity() @@ -280,44 +269,45 @@ public sealed class DataContext : IdentityDbContext() .Property(rp => rp.LibraryIds) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) + .HasJsonConversion([]) .HasColumnType("TEXT"); builder.Entity() .Property(rp => rp.SeriesIds) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) + .HasJsonConversion([]) .HasColumnType("TEXT"); builder.Entity() .Property(sm => sm.KPlusOverrides) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? - new List()) + .HasJsonConversion([]) .HasColumnType("TEXT") .HasDefaultValue(new List()); builder.Entity() .Property(sm => sm.KPlusOverrides) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) + .HasJsonConversion([]) .HasColumnType("TEXT") .HasDefaultValue(new List()); builder.Entity() .Property(a => a.BookReaderHighlightSlots) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) + .HasJsonConversion([]) .HasColumnType("TEXT") .HasDefaultValue(new List()); builder.Entity() .Property(user => user.IdentityProvider) .HasDefaultValue(IdentityProvider.Kavita); + + builder.Entity() + .Property(a => a.SocialPreferences) + .HasJsonConversion(new AppUserSocialPreferences()) + .HasColumnType("TEXT") + .HasDefaultValue(new AppUserSocialPreferences()); + + builder.Entity() + .Property(a => a.Likes) + .HasJsonConversion(new HashSet()) + .HasColumnType("TEXT") + .HasDefaultValue(new HashSet()); } #nullable enable diff --git a/API/Data/Migrations/20251003110154_SocialAnnotations.Designer.cs b/API/Data/Migrations/20251003110154_SocialAnnotations.Designer.cs new file mode 100644 index 000000000..687585e5c --- /dev/null +++ b/API/Data/Migrations/20251003110154_SocialAnnotations.Designer.cs @@ -0,0 +1,3925 @@ +// +using System; +using System.Collections.Generic; +using API.Data; +using API.Entities.MetadataMatching; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20251003110154_SocialAnnotations")] + partial class SocialAnnotations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.7"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("HasRunScrobbleEventGeneration") + .HasColumnType("INTEGER"); + + b.Property("IdentityProvider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("MalAccessToken") + .HasColumnType("TEXT"); + + b.Property("MalUserName") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OidcId") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventGenerationRan") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserAnnotation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("CommentHtml") + .HasColumnType("TEXT"); + + b.Property("CommentPlainText") + .HasColumnType("TEXT"); + + b.Property("ContainsSpoiler") + .HasColumnType("INTEGER"); + + b.Property("Context") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingXPath") + .HasColumnType("TEXT"); + + b.Property("HighlightCount") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Likes") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SelectedSlotIndex") + .HasColumnType("INTEGER"); + + b.Property("SelectedText") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("XPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserAnnotation"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("ImageOffset") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("XPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserChapterRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("MissingSeriesFromSource") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("SourceUrl") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TotalSourceCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserCollection"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(4); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserDashboardStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Host") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserExternalSource"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AniListScrobblingEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderHighlightSlots") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + b.Property("ColorScapeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("en"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SocialPreferences") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("{\"ShareReviews\":false,\"ShareAnnotations\":false,\"ViewOtherAnnotations\":false,\"SocialLibraries\":[],\"SocialMaxAgeRating\":-1,\"SocialIncludeUnknowns\":true}"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.Property("WantToReadSync") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("DisableWidthOverride") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("LibraryIds") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("SeriesIds") + .HasColumnType("TEXT"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("WidthOverride") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserReadingProfiles"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SelectedText") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AverageExternalRating") + .HasColumnType("REAL"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("ISBNLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("KPlusOverrides") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ReleaseDateLocked") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("REAL"); + + b.Property("SortOrderLocked") + .HasColumnType("INTEGER"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TitleNameLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DeliveryStatus") + .HasColumnType("TEXT"); + + b.Property("EmailTemplate") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SendDate") + .HasColumnType("TEXT"); + + b.Property("Sent") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); + + b.ToTable("EmailHistory"); + }); + + modelBuilder.Entity("API.Entities.EpubFont", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("EpubFont"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("RanAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ManualMigrationHistory"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowMetadataMatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EnableMetadata") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("RemovePrefixForSortName") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Pattern") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryExcludePattern"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FileTypeGroup") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryFileTypeGroup"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("KoreaderHash") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ProviderUrl") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalRating"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("ExternalRecommendation"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalReview"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("INTEGER"); + + b.Property("CbrId") + .HasColumnType("INTEGER"); + + b.Property("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("ValidUntilUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastChecked") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBlacklist"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("KPlusOverrides") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DestinationType") + .HasColumnType("INTEGER"); + + b.Property("DestinationValue") + .HasColumnType("TEXT"); + + b.Property("ExcludeFromSource") + .HasColumnType("INTEGER"); + + b.Property("MetadataSettingsId") + .HasColumnType("INTEGER"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.Property("SourceValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MetadataSettingsId"); + + b.ToTable("MetadataFieldMapping"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRatingMappings") + .HasColumnType("TEXT"); + + b.Property("Blacklist") + .HasColumnType("TEXT"); + + b.Property("EnableChapterCoverImage") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterPublisher") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterReleaseDate") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterTitle") + .HasColumnType("INTEGER"); + + b.Property("EnableCoverImage") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("EnableExtendedMetadataProcessing") + .HasColumnType("INTEGER"); + + b.Property("EnableGenres") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalizedName") + .HasColumnType("INTEGER"); + + b.Property("EnablePeople") + .HasColumnType("INTEGER"); + + b.Property("EnablePublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("EnableRelationships") + .HasColumnType("INTEGER"); + + b.Property("EnableStartDate") + .HasColumnType("INTEGER"); + + b.Property("EnableSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableTags") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FirstLastPeopleNaming") + .HasColumnType("INTEGER"); + + b.Property("Overrides") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("PersonRoles") + .HasColumnType("TEXT"); + + b.Property("Whitelist") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("ChapterPeople"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("Asin") + .HasColumnType("TEXT"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("HardcoverId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Alias") + .HasColumnType("TEXT"); + + b.Property("NormalizedAlias") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PersonId"); + + b.ToTable("PersonAlias"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.Property("SeriesMetadataId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.HasKey("SeriesMetadataId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsErrored") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DontMatch") + .HasColumnType("INTEGER"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsBlacklisted") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("LowestFolderPath") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("CompatibleVersion") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("GitHubPath") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PreviewUrls") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ShaHash") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LookupName") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.Property("CollectionsId") + .HasColumnType("INTEGER"); + + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionsId", "ItemsId"); + + b.HasIndex("ItemsId"); + + b.ToTable("AppUserCollectionSeries"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.Property("ExternalRatingsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRatingExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.Property("ExternalRecommendationsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRecommendationExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalReviewExternalSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserAnnotation", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Annotations") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ChapterRatings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Ratings") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Collections") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingProfiles") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalRatings") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalReviews") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") + .WithMany("FieldMappings") + .HasForeignKey("MetadataSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("People") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("ChapterPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("Aliases") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("SeriesMetadataPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") + .WithMany("People") + .HasForeignKey("SeriesMetadataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.HasOne("API.Entities.AppUserCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Annotations"); + + b.Navigation("Bookmarks"); + + b.Navigation("ChapterRatings"); + + b.Navigation("Collections"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ReadingProfiles"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences") + .IsRequired(); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("ExternalRatings"); + + b.Navigation("ExternalReviews"); + + b.Navigation("Files"); + + b.Navigation("People"); + + b.Navigation("Ratings"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Navigation("FieldMappings"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Navigation("Aliases"); + + b.Navigation("ChapterPeople"); + + b.Navigation("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("ExternalSeriesMetadata"); + + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20251003110154_SocialAnnotations.cs b/API/Data/Migrations/20251003110154_SocialAnnotations.cs new file mode 100644 index 000000000..ecd522211 --- /dev/null +++ b/API/Data/Migrations/20251003110154_SocialAnnotations.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class SocialAnnotations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SocialPreferences", + table: "AppUserPreferences", + type: "TEXT", + nullable: true, + defaultValue: "{\"ShareReviews\":false,\"ShareAnnotations\":false,\"ViewOtherAnnotations\":false,\"SocialLibraries\":[],\"SocialMaxAgeRating\":-1,\"SocialIncludeUnknowns\":true}"); + + migrationBuilder.AddColumn( + name: "Likes", + table: "AppUserAnnotation", + type: "TEXT", + nullable: true, + defaultValue: "[]"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserAnnotation_LibraryId", + table: "AppUserAnnotation", + column: "LibraryId"); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserAnnotation_Library_LibraryId", + table: "AppUserAnnotation", + column: "LibraryId", + principalTable: "Library", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppUserAnnotation_Library_LibraryId", + table: "AppUserAnnotation"); + + migrationBuilder.DropIndex( + name: "IX_AppUserAnnotation_LibraryId", + table: "AppUserAnnotation"); + + migrationBuilder.DropColumn( + name: "SocialPreferences", + table: "AppUserPreferences"); + + migrationBuilder.DropColumn( + name: "Likes", + table: "AppUserAnnotation"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index eae94e377..850ad5130 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -213,6 +213,11 @@ namespace API.Data.Migrations b.Property("LibraryId") .HasColumnType("INTEGER"); + b.Property("Likes") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + b.Property("PageNumber") .HasColumnType("INTEGER"); @@ -237,6 +242,8 @@ namespace API.Data.Migrations b.HasIndex("ChapterId"); + b.HasIndex("LibraryId"); + b.HasIndex("SeriesId"); b.ToTable("AppUserAnnotation"); @@ -614,6 +621,11 @@ namespace API.Data.Migrations b.Property("ShowScreenHints") .HasColumnType("INTEGER"); + b.Property("SocialPreferences") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("{\"ShareReviews\":false,\"ShareAnnotations\":false,\"ViewOtherAnnotations\":false,\"SocialLibraries\":[],\"SocialMaxAgeRating\":-1,\"SocialIncludeUnknowns\":true}"); + b.Property("SwipeToPaginate") .HasColumnType("INTEGER"); @@ -2988,6 +3000,12 @@ namespace API.Data.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.HasOne("API.Entities.Series", "Series") .WithMany() .HasForeignKey("SeriesId") @@ -2998,6 +3016,8 @@ namespace API.Data.Migrations b.Navigation("Chapter"); + b.Navigation("Library"); + b.Navigation("Series"); }); diff --git a/API/Data/Repositories/AnnotationRepository.cs b/API/Data/Repositories/AnnotationRepository.cs index 1918023e9..e0bcc802f 100644 --- a/API/Data/Repositories/AnnotationRepository.cs +++ b/API/Data/Repositories/AnnotationRepository.cs @@ -2,11 +2,14 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.Data.Misc; +using API.DTOs; using API.DTOs.Filtering.v2; using API.DTOs.Metadata.Browse.Requests; using API.DTOs.Annotations; using API.DTOs.Reader; using API.Entities; +using API.Entities.Enums; using API.Extensions.QueryExtensions; using API.Extensions.QueryExtensions.Filtering; using API.Helpers; @@ -26,10 +29,12 @@ public interface IAnnotationRepository void Remove(IEnumerable annotations); Task GetAnnotationDto(int id); Task GetAnnotation(int id); - Task> GetAnnotations(IList ids); + Task> GetAllAnnotations(); + Task> GetAnnotations(int userId, IList ids); Task> GetFullAnnotationsByUserIdAsync(int userId); Task> GetFullAnnotations(int userId, IList annotationIds); Task> GetAnnotationDtos(int userId, BrowseAnnotationFilterDto filter, UserParams userParams); + Task> GetSeriesWithAnnotations(int userId); } public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnotationRepository @@ -67,10 +72,18 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota .FirstOrDefaultAsync(a => a.Id == id); } - public async Task> GetAnnotations(IList ids) + public async Task> GetAllAnnotations() { + return await context.AppUserAnnotation.ToListAsync(); + } + + public async Task> GetAnnotations(int userId, IList ids) + { + var userPreferences = await context.AppUserPreferences.ToListAsync(); + return await context.AppUserAnnotation .Where(a => ids.Contains(a.Id)) + .RestrictBySocialPreferences(userId, userPreferences) .ToListAsync(); } @@ -80,26 +93,44 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota return await PagedList.CreateAsync(query, userParams); } + public async Task> GetSeriesWithAnnotations(int userId) + { + var userPreferences = await context.AppUserPreferences.ToListAsync(); + + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + + var seriesIdsWithAnnotations = await context.AppUserAnnotation + .RestrictBySocialPreferences(userId, userPreferences) + .Select(a => a.SeriesId) + .ToListAsync(); + + return await context.Series + .Where(s => libraryIds.Contains(s.LibraryId) && seriesIdsWithAnnotations.Contains(s.Id)) + .RestrictAgainstAgeRestriction(userRating) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(); + + } + private async Task> CreatedFilteredAnnotationQueryable(int userId, BrowseAnnotationFilterDto filter) { var allLibrariesCount = await context.Library.CountAsync(); var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(); + var seriesIds = await context.Series + .Where(s => userLibs.Contains(s.LibraryId)) + .Select(s => s.Id) + .ToListAsync(); - var seriesIds = await context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(); var query = context.AppUserAnnotation.AsNoTracking(); query = BuildAnnotationFilterQuery(userId, filter, query); - var validUsers = await context.AppUserPreferences - .Where(a => a.AppUserId == userId) // TODO: Remove when the below is done - .Where(p => true) // TODO: Filter on sharing annotations preference - .Select(p => p.AppUserId) - .ToListAsync(); - - query = query.Where(a => validUsers.Contains(a.AppUserId)) - .WhereIf(allLibrariesCount != userLibs.Count, - a => seriesIds.Contains(a.SeriesId)); + query = query + .WhereIf(allLibrariesCount != userLibs.Count, a => seriesIds.Contains(a.SeriesId)) + .RestrictBySocialPreferences(userId, userPreferences); var sortedQuery = query.SortBy(filter.SortOptions); var limitedQuery = filter.LimitTo <= 0 ? sortedQuery : sortedQuery.Take(filter.LimitTo); @@ -140,6 +171,7 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota { AnnotationFilterField.Owner => query.IsOwnedBy(true, statement.Comparison, (IList) value), AnnotationFilterField.Library => query.IsInLibrary(true, statement.Comparison, (IList) value), + AnnotationFilterField.Series => query.HasSeries(true, statement.Comparison, (IList) value), AnnotationFilterField.HighlightSlot => query.IsUsingHighlights(true, statement.Comparison, (IList) value), AnnotationFilterField.Spoiler => query.Where(a => !(bool) value || !a.ContainsSpoiler), AnnotationFilterField.Comment => query.HasCommented(true, statement.Comparison, (string) value), @@ -150,12 +182,14 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota public async Task> GetFullAnnotations(int userId, IList annotationIds) { + var userPreferences = await context.AppUserPreferences.ToListAsync(); + return await context.AppUserAnnotation .AsNoTracking() .Where(a => annotationIds.Contains(a.Id)) - .Where(a => a.AppUserId == userId) - //.Where(a => a.AppUserId == userId || a.AppUser.UserPreferences.ShareAnnotations) TODO: Filter out annotations for users who don't share them - .SelectFullAnnotation() + .RestrictBySocialPreferences(userId, userPreferences) + .ProjectTo(mapper.ConfigurationProvider) + .OrderFullAnnotation() .ToListAsync(); } @@ -166,9 +200,12 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota /// public async Task> GetFullAnnotationsByUserIdAsync(int userId) { + var userPreferences = await context.AppUserPreferences.ToListAsync(); + return await context.AppUserAnnotation - .Where(a => a.AppUserId == userId) - .SelectFullAnnotation() + .RestrictBySocialPreferences(userId, userPreferences) + .ProjectTo(mapper.ConfigurationProvider) + .OrderFullAnnotation() .ToListAsync(); } } diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 94a2198e4..721dec4d7 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -436,7 +436,7 @@ public class ReadingListRepository : IReadingListRepository if (userParams != null) { query = query - .Skip(userParams.PageNumber * userParams.PageSize) + .Skip((userParams.PageNumber - 1) * userParams.PageSize) // NOTE: PageNumber starts at 1 with PagedList, so copy logic here .Take(userParams.PageSize); } diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index b1c219a93..59ff6b550 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -966,7 +966,7 @@ public class SeriesRepository : ISeriesRepository var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckProgressDays); var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckUpdateDays); - var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); @@ -1555,7 +1555,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind) { - var libraryIds = GetLibraryIdsForUser(userId); + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); var usersSeriesIds = _context.Series @@ -1582,7 +1582,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams) { - var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); @@ -1610,7 +1610,7 @@ public class SeriesRepository : ISeriesRepository /// public async Task> GetRediscover(int userId, int libraryId, UserParams userParams) { - var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); var distinctSeriesIdsWithProgress = _context.AppUserProgresses @@ -1630,7 +1630,7 @@ public class SeriesRepository : ISeriesRepository public async Task GetSeriesForMangaFile(int mangaFileId, int userId) { - var libraryIds = GetLibraryIdsForUser(userId, 0, QueryContext.Search); + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, 0, QueryContext.Search); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.MangaFile @@ -1647,7 +1647,7 @@ public class SeriesRepository : ISeriesRepository public async Task GetSeriesForChapter(int chapterId, int userId) { - var libraryIds = GetLibraryIdsForUser(userId); + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.Chapter .Where(m => m.Id == chapterId) @@ -1792,7 +1792,7 @@ public class SeriesRepository : ISeriesRepository public async Task GetSeriesByAnyName(string seriesName, string localizedName, IList formats, int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None) { - var libraryIds = GetLibraryIdsForUser(userId); + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); var normalizedSeries = seriesName.ToNormalized(); var normalizedLocalized = localizedName.ToNormalized(); @@ -1826,7 +1826,7 @@ public class SeriesRepository : ISeriesRepository public async Task GetSeriesByAnyName(IList names, IList formats, int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None) { - var libraryIds = GetLibraryIdsForUser(userId); + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); names = names.Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList(); var normalizedNames = names.Select(s => s.ToNormalized()).ToList(); @@ -1923,7 +1923,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetHighlyRated(int userId, int libraryId, UserParams userParams) { - var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); var distinctSeriesIdsWithHighRating = _context.AppUserRating @@ -1945,7 +1945,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetQuickReads(int userId, int libraryId, UserParams userParams) { - var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); var distinctSeriesIdsWithProgress = _context.AppUserProgresses @@ -1972,7 +1972,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams) { - var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); var distinctSeriesIdsWithProgress = _context.AppUserProgresses @@ -2343,36 +2343,8 @@ public class SeriesRepository : ISeriesRepository .Select(s => s.Metadata.AgeRating) .OrderBy(s => s) .LastOrDefaultAsync(); + if (ret == null) return AgeRating.Unknown; return ret; } - - /// - /// Returns all library ids for a user - /// - /// - /// 0 for no library filter - /// Defaults to None - The context behind this query, so appropriate restrictions can be placed - /// - private IQueryable GetLibraryIdsForUser(int userId, int libraryId = 0, QueryContext queryContext = QueryContext.None) - { - var user = _context.AppUser - .AsSplitQuery() - .AsNoTracking() - .Where(u => u.Id == userId) - .AsSingleQuery(); - - if (libraryId == 0) - { - return user.SelectMany(l => l.Libraries) - .IsRestricted(queryContext) - .Select(lib => lib.Id); - } - - return user.SelectMany(l => l.Libraries) - .Where(lib => lib.Id == libraryId) - .IsRestricted(queryContext) - .Select(lib => lib.Id); - } - } diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 7732c5edc..a50d0a208 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -587,9 +587,11 @@ public class UserRepository : IUserRepository /// public async Task> GetAnnotations(int userId, int chapterId) { - // TODO: Check settings if I should include other user's annotations + var userPreferences = await _context.AppUserPreferences.ToListAsync(); + return await _context.AppUserAnnotation - .Where(a => a.AppUserId == userId && a.ChapterId == chapterId) + .Where(a => a.ChapterId == chapterId) + .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(a => a.PageNumber) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -597,9 +599,11 @@ public class UserRepository : IUserRepository public async Task> GetAnnotationsByPage(int userId, int chapterId, int pageNum) { - // TODO: Check settings if I should include other user's annotations + var userPreferences = await _context.AppUserPreferences.ToListAsync(); + return await _context.AppUserAnnotation - .Where(a => a.AppUserId == userId && a.ChapterId == chapterId && a.PageNumber == pageNum) + .Where(a => a.ChapterId == chapterId && a.PageNumber == pageNum) + .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(a => a.PageNumber) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -617,16 +621,22 @@ public class UserRepository : IUserRepository public async Task GetAnnotationDtoById(int userId, int annotationId) { + var userPreferences = await _context.AppUserPreferences.ToListAsync(); + return await _context.AppUserAnnotation - .Where(a => a.AppUserId == userId && a.Id == annotationId) + .Where(a => a.Id == annotationId) + .RestrictBySocialPreferences(userId, userPreferences) .ProjectTo(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(); } public async Task> GetAnnotationDtosBySeries(int userId, int seriesId) { + var userPreferences = await _context.AppUserPreferences.ToListAsync(); + return await _context.AppUserAnnotation - .Where(a => a.AppUserId == userId && a.SeriesId == seriesId) + .Where(a => a.SeriesId == seriesId) + .RestrictBySocialPreferences(userId, userPreferences) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } @@ -686,10 +696,12 @@ public class UserRepository : IUserRepository public async Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId) { + var userPreferences = await _context.AppUserPreferences.ToListAsync(); + return await _context.AppUserRating .Include(r => r.AppUser) .Where(r => r.SeriesId == seriesId) - .Where(r => r.AppUser.UserPreferences.ShareReviews || r.AppUserId == userId) + .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(r => r.AppUserId == userId) .ThenBy(r => r.Rating) .AsSplitQuery() @@ -699,10 +711,12 @@ public class UserRepository : IUserRepository public async Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId) { + var userPreferences = await _context.AppUserPreferences.ToListAsync(); + return await _context.AppUserChapterRating .Include(r => r.AppUser) .Where(r => r.ChapterId == chapterId) - .Where(r => r.AppUser.UserPreferences.ShareReviews || r.AppUserId == userId) + .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(r => r.AppUserId == userId) .ThenBy(r => r.Rating) .AsSplitQuery() @@ -881,6 +895,7 @@ public class UserRepository : IUserRepository }, Libraries = u.Libraries.Select(l => new LibraryDto { + Id = l.Id, Name = l.Name, Type = l.Type, LastScanned = l.LastScanned, diff --git a/API/Entities/AppUserAnnotation.cs b/API/Entities/AppUserAnnotation.cs index 960c884ec..e3b7be90b 100644 --- a/API/Entities/AppUserAnnotation.cs +++ b/API/Entities/AppUserAnnotation.cs @@ -52,7 +52,10 @@ public class AppUserAnnotation : IEntityDate public bool ContainsSpoiler { get; set; } - // TODO: Figure out a simple mechansim to track upvotes (hashmap of userids?) + /// + /// A set container userIds of all users who have liked this annotations + /// + public ISet Likes { get; set; } = new HashSet(); /// /// Title of the TOC Chapter within Epub (not Chapter Entity) @@ -60,6 +63,7 @@ public class AppUserAnnotation : IEntityDate public string? ChapterTitle { get; set; } public required int LibraryId { get; set; } + public Library Library { get; set; } public required int SeriesId { get; set; } public Series Series { get; set; } public required int VolumeId { get; set; } diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index e125e14e9..25b4f61a7 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using API.Data; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; @@ -163,10 +164,6 @@ public class AppUserPreferences /// public bool CollapseSeriesRelationships { get; set; } = false; /// - /// UI Site Global Setting: Should series reviews be shared with all users in the server - /// - public bool ShareReviews { get; set; } = false; - /// /// UI Site Global Setting: The language locale that should be used for the user /// public string Locale { get; set; } @@ -185,8 +182,60 @@ public class AppUserPreferences /// Should this account have Want to Read Sync enabled /// public bool WantToReadSync { get; set; } + #endregion + + #region Social + + /// + /// UI Site Global Setting: Should series reviews be shared with all users in the server + /// + [Obsolete("Use SocialPreferences.ShareReviews")] + public bool ShareReviews { get; set; } = false; + + /// + /// The social preferences of the AppUser + /// + /// Saved as a JSON obj in the DB + public AppUserSocialPreferences SocialPreferences { get; set; } = new(); + + + #endregion public AppUser AppUser { get; set; } = null!; public int AppUserId { get; set; } } + +public class AppUserSocialPreferences +{ + /// + /// UI Site Global Setting: Should series reviews be shared with all users in the server + /// + public bool ShareReviews { get; set; } = false; + + /// + /// UI Site Global Setting: Share your annotations with other users + /// + public bool ShareAnnotations { get; set; } = false; + + /// + /// UI Site Global Setting: See other users' annotations while reading + /// + public bool ViewOtherAnnotations { get; set; } = false; + + /// + /// UI Site Global Setting: For which libraries should social features be enabled + /// + /// Empty array means all, disable specific social features to opt out everywhere + public IList SocialLibraries { get; set; } = []; + + /// + /// UI Site Global Setting: Highest age rating for which social features are enabled + /// + public AgeRating SocialMaxAgeRating { get; set; } = AgeRating.NotApplicable; + + /// + /// UI Site Global Setting: Enable social features for unknown age ratings + /// + public bool SocialIncludeUnknowns { get; set; } = true; +} diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index fe3646943..86ce32b04 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -22,7 +22,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP /// Smallest number of the Range. Can be a partial like Chapter 4.5 /// [Obsolete("Use MinNumber and MaxNumber instead")] - public required string Number { get; set; } + public string Number { get; set; } /// /// Minimum Chapter Number. /// diff --git a/API/Extensions/DataContextExtensions.cs b/API/Extensions/DataContextExtensions.cs new file mode 100644 index 000000000..abf076861 --- /dev/null +++ b/API/Extensions/DataContextExtensions.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace API.Extensions; + +public static class DataContextExtensions +{ + + public static PropertyBuilder HasJsonConversion(this PropertyBuilder builder, TProperty def = default) + { + return builder.HasConversion( + v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), + v => JsonSerializer.Deserialize(v, JsonSerializerOptions.Default) ?? def + ); + } + +} diff --git a/API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs b/API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs index 443757fac..0f9265e2f 100644 --- a/API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs @@ -35,10 +35,25 @@ public static class AnnotationFilter return comparison switch { - FilterComparison.Equal => queryable.Where(a => a.Series.LibraryId == libraryIds[0]), - FilterComparison.Contains => queryable.Where(a => libraryIds.Contains(a.Series.LibraryId)), - FilterComparison.NotContains => queryable.Where(a => !libraryIds.Contains(a.Series.LibraryId)), - FilterComparison.NotEqual => queryable.Where(a => a.Series.LibraryId != libraryIds[0]), + FilterComparison.Equal => queryable.Where(a => a.LibraryId == libraryIds[0]), + FilterComparison.Contains => queryable.Where(a => libraryIds.Contains(a.LibraryId)), + FilterComparison.NotContains => queryable.Where(a => !libraryIds.Contains(a.LibraryId)), + FilterComparison.NotEqual => queryable.Where(a => a.LibraryId != libraryIds[0]), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public static IQueryable HasSeries(this IQueryable queryable, bool condition, + FilterComparison comparison, IList seriesIds) + { + if (seriesIds.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.SeriesId == seriesIds[0]), + FilterComparison.Contains => queryable.Where(a => seriesIds.Contains(a.SeriesId)), + FilterComparison.NotContains => queryable.Where(a => !seriesIds.Contains(a.SeriesId)), + FilterComparison.NotEqual => queryable.Where(a => a.SeriesId != seriesIds[0]), _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), }; } @@ -50,7 +65,7 @@ public static class AnnotationFilter return comparison switch { - FilterComparison.Equal => queryable.Where(a => a.SelectedSlotIndex== highlightSlotIdxs[0]), + FilterComparison.Equal => queryable.Where(a => a.SelectedSlotIndex == highlightSlotIdxs[0]), FilterComparison.Contains => queryable.Where(a => highlightSlotIdxs.Contains(a.SelectedSlotIndex)), FilterComparison.NotContains => queryable.Where(a => !highlightSlotIdxs.Contains(a.SelectedSlotIndex)), FilterComparison.NotEqual => queryable.Where(a => a.SelectedSlotIndex != highlightSlotIdxs[0]), diff --git a/API/Extensions/QueryExtensions/QueryableExtensions.cs b/API/Extensions/QueryExtensions/QueryableExtensions.cs index 7990056ec..df9663607 100644 --- a/API/Extensions/QueryExtensions/QueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/QueryableExtensions.cs @@ -14,6 +14,7 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Person; using API.Entities.Scrobble; +using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions; @@ -90,6 +91,34 @@ public static class QueryableExtensions .Select(lib => lib.Id); } + /// + /// Returns all library ids for a user + /// + /// + /// 0 for no library filter + /// Defaults to None - The context behind this query, so appropriate restrictions can be placed + /// + public static IQueryable GetLibraryIdsForUser(this DbSet query, int userId, int libraryId = 0, QueryContext queryContext = QueryContext.None) + { + var user = query + .AsSplitQuery() + .AsNoTracking() + .Where(u => u.Id == userId) + .AsSingleQuery(); + + if (libraryId == 0) + { + return user.SelectMany(l => l.Libraries) + .IsRestricted(queryContext) + .Select(lib => lib.Id); + } + + return user.SelectMany(l => l.Libraries) + .Where(lib => lib.Id == libraryId) + .IsRestricted(queryContext) + .Select(lib => lib.Id); + } + /// /// Returns all libraries for a given user and library type /// @@ -346,31 +375,9 @@ public static class QueryableExtensions }; } - public static IQueryable SelectFullAnnotation(this IQueryable query) + public static IQueryable OrderFullAnnotation(this IQueryable query) { - return query.Select(a => new FullAnnotationDto - { - Id = a.Id, - UserId = a.AppUserId, - SelectedText = a.SelectedText, - Comment = a.Comment, - CommentHtml = a.CommentHtml, - CommentPlainText = a.CommentPlainText, - Context = a.Context, - ChapterTitle = a.ChapterTitle, - PageNumber = a.PageNumber, - SelectedSlotIndex = a.SelectedSlotIndex, - ContainsSpoiler = a.ContainsSpoiler, - CreatedUtc = a.CreatedUtc, - LastModifiedUtc = a.LastModifiedUtc, - LibraryId = a.LibraryId, - LibraryName = a.Chapter.Volume.Series.Library.Name, - SeriesId = a.SeriesId, - SeriesName = a.Chapter.Volume.Series.Name, - VolumeId = a.VolumeId, - VolumeName = a.Chapter.Volume.Name, - ChapterId = a.ChapterId, - }) + return query .OrderBy(a => a.SeriesId) .ThenBy(a => a.VolumeId) .ThenBy(a => a.ChapterId) diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index e0738bdf3..ad609703c 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using API.Data.Misc; using API.Entities; @@ -151,4 +152,199 @@ public static class RestrictByAgeExtensions return q; } + + private static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction, int userId) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; + var q = queryable.Where(r => r.Series.Metadata.AgeRating <= restriction.AgeRating || r.AppUserId == userId); + + if (!restriction.IncludeUnknowns) + { + return q.Where(a => a.Series.Metadata.AgeRating != AgeRating.Unknown || a.AppUserId == userId); + } + + return q; + } + + private static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction, int userId) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; + var q = queryable.Where(r => r.Series.Metadata.AgeRating <= restriction.AgeRating || r.AppUserId == userId); + + if (!restriction.IncludeUnknowns) + { + return q.Where(a => a.Series.Metadata.AgeRating != AgeRating.Unknown || a.AppUserId == userId); + } + + return q; + } + + private static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction, int userId) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; + var q = queryable.Where(a => a.Series.Metadata.AgeRating <= restriction.AgeRating || a.AppUserId == userId); + + if (!restriction.IncludeUnknowns) + { + return q.Where(a => a.Series.Metadata.AgeRating != AgeRating.Unknown || a.AppUserId == userId); + } + + return q; + } + + // TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here + /// + /// Filter annotations by social preferences of users + /// + /// + /// + /// List of user preferences for every user on the server + /// + public static IQueryable RestrictBySocialPreferences(this IQueryable queryable, int userId, IList userPreferences) + { + var preferencesById = userPreferences.ToDictionary(p => p.AppUserId, p => p.SocialPreferences); + var socialPreferences = preferencesById[userId]; + + if (socialPreferences.ViewOtherAnnotations) + { + // We are unable to do dictionary lookups in Sqlite; This means we need to translate them to X IN Y. + var sharingUserIds = userPreferences + .Where(p => p.SocialPreferences.ShareAnnotations) + .Select(p => p.AppUserId) + .ToHashSet(); + + // Only include the users' annotations, or those of users that are sharing + queryable = queryable.Where(a => a.AppUserId == userId || sharingUserIds.Contains(a.AppUserId)); + + // For other users' annotation + foreach (var sharingUserId in sharingUserIds.Where(id => id != userId)) + { + // Filter out libs if enabled + var libs = preferencesById[sharingUserId].SocialLibraries; + if (libs.Count > 0) + { + queryable = queryable.Where(a => a.AppUserId != sharingUserId || libs.Contains(a.LibraryId)); + } + + // Filter on age rating + var ageRating = preferencesById[sharingUserId].SocialMaxAgeRating; + var includeUnknowns = preferencesById[sharingUserId].SocialIncludeUnknowns; + if (ageRating != AgeRating.NotApplicable) + { + queryable = queryable.Where(a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating <= ageRating) + .WhereIf(!includeUnknowns, + a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating != AgeRating.Unknown); + } + } + } + else + { + queryable = queryable.Where(a => a.AppUserId == userId); + } + + return queryable + .WhereIf(socialPreferences.SocialLibraries.Count > 0, + a => a.AppUserId == userId || socialPreferences.SocialLibraries.Contains(a.LibraryId)) + .RestrictAgainstAgeRestriction(new AgeRestriction + { + AgeRating = socialPreferences.SocialMaxAgeRating, + IncludeUnknowns = socialPreferences.SocialIncludeUnknowns, + }, userId); + } + + // TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here + /// + /// Filter user reviews social preferences of users + /// + /// + /// + /// List of user preferences for every user on the server + /// + public static IQueryable RestrictBySocialPreferences(this IQueryable queryable, int userId, IList userPreferences) + { + var preferencesById = userPreferences.ToDictionary(p => p.AppUserId, p => p.SocialPreferences); + var socialPreferences = preferencesById[userId]; + + var sharingUserIds = userPreferences + .Where(p => p.SocialPreferences.ShareReviews) + .Select(p => p.AppUserId) + .ToHashSet(); + + queryable = queryable.Where(r => r.AppUserId == userId || sharingUserIds.Contains(r.AppUserId)); + + foreach (var sharingUserId in sharingUserIds.Where(id => id != userId)) + { + var libs = preferencesById[sharingUserId].SocialLibraries; + if (libs.Count > 0) + { + queryable = queryable.Where(r => r.AppUserId != sharingUserId || libs.Contains(r.Series.LibraryId)); + } + + var ageRating = preferencesById[sharingUserId].SocialMaxAgeRating; + var includeUnknowns = preferencesById[sharingUserId].SocialIncludeUnknowns; + if (ageRating != AgeRating.NotApplicable) + { + queryable = queryable.Where(r => r.AppUserId != sharingUserId || r.Series.Metadata.AgeRating <= ageRating) + .WhereIf(!includeUnknowns, + r => r.AppUserId != sharingUserId || r.Series.Metadata.AgeRating != AgeRating.Unknown); + } + } + + return queryable + .WhereIf(socialPreferences.SocialLibraries.Count > 0, + r => r.AppUserId == userId || socialPreferences.SocialLibraries.Contains(r.Series.LibraryId)) + .RestrictAgainstAgeRestriction(new AgeRestriction + { + AgeRating = socialPreferences.SocialMaxAgeRating, + IncludeUnknowns = socialPreferences.SocialIncludeUnknowns, + }, userId); + } + + // TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here + /// + /// Filter user chapter reviews social preferences of users + /// + /// + /// + /// List of user preferences for every user on the server + /// + public static IQueryable RestrictBySocialPreferences(this IQueryable queryable, int userId, IList userPreferences) + { + var preferencesById = userPreferences.ToDictionary(p => p.AppUserId, p => p.SocialPreferences); + var socialPreferences = preferencesById[userId]; + + var sharingUserIds = userPreferences + .Where(p => p.SocialPreferences.ShareReviews) + .Select(p => p.AppUserId) + .ToHashSet(); + + queryable = queryable.Where(r => r.AppUserId == userId || sharingUserIds.Contains(r.AppUserId)); + + foreach (var sharingUserId in sharingUserIds.Where(id => id != userId)) + { + var libs = preferencesById[sharingUserId].SocialLibraries; + if (libs.Count > 0) + { + queryable = queryable.Where(r => r.AppUserId != sharingUserId || libs.Contains(r.Series.LibraryId)); + } + + var ageRating = preferencesById[sharingUserId].SocialMaxAgeRating; + var includeUnknowns = preferencesById[sharingUserId].SocialIncludeUnknowns; + if (ageRating != AgeRating.NotApplicable) + { + queryable = queryable.Where(r => r.AppUserId != sharingUserId || r.Series.Metadata.AgeRating <= ageRating) + .WhereIf(!includeUnknowns, + r => r.AppUserId != sharingUserId || r.Series.Metadata.AgeRating != AgeRating.Unknown); + } + } + + return queryable + .WhereIf(socialPreferences.SocialLibraries.Count > 0, + r => r.AppUserId == userId || socialPreferences.SocialLibraries.Contains(r.Series.LibraryId)) + .RestrictAgainstAgeRestriction(new AgeRestriction + { + AgeRating = socialPreferences.SocialMaxAgeRating, + IncludeUnknowns = socialPreferences.SocialIncludeUnknowns, + }, userId); + } } diff --git a/API/Extensions/StringExtensions.cs b/API/Extensions/StringExtensions.cs index f2f69d7f3..2fdaf52a1 100644 --- a/API/Extensions/StringExtensions.cs +++ b/API/Extensions/StringExtensions.cs @@ -101,8 +101,7 @@ public static partial class StringExtensions return []; } - return value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) + return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(int.Parse) .ToList(); } @@ -115,11 +114,17 @@ public static partial class StringExtensions public static long ParseHumanReadableBytes(this string input) { if (string.IsNullOrWhiteSpace(input)) + { throw new ArgumentException("Input cannot be null or empty.", nameof(input)); + } + var match = HumanReadableBytesRegex().Match(input); if (!match.Success) + { throw new FormatException($"Invalid format: '{input}'"); + } + var value = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); var unit = match.Groups[2].Value.ToUpperInvariant(); diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 6c5807aad..ecfcd5c97 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -4,6 +4,7 @@ using System.Linq; using API.Data.Migrations; using API.DTOs; using API.DTOs.Account; +using API.DTOs.Annotations; using API.DTOs.Collection; using API.DTOs.CollectionTags; using API.DTOs.Dashboard; @@ -391,7 +392,14 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.OwnerUsername, opt => opt.MapFrom(src => src.AppUser.UserName)) - .ForMember(dest => dest.OwnerUserId, opt => opt.MapFrom(src => src.AppUserId)) ; + .ForMember(dest => dest.OwnerUserId, opt => opt.MapFrom(src => src.AppUserId)) + .ForMember(dest => dest.SeriesName, opt => opt.MapFrom(src => src.Series.Name)) + .ForMember(dest => dest.LibraryName, opt => opt.MapFrom(src => src.Library.Name)); + + CreateMap() + .ForMember(dest => dest.SeriesName, opt => opt.MapFrom(src => src.Series.Name)) + .ForMember(dest => dest.VolumeName, opt => opt.MapFrom(src => src.Chapter.Volume.Name)) + .ForMember(dest => dest.LibraryName, opt => opt.MapFrom(src => src.Library.Name)); CreateMap(); } diff --git a/API/Helpers/Builders/AppUserBuilder.cs b/API/Helpers/Builders/AppUserBuilder.cs index 4fdea81c4..82469ebfe 100644 --- a/API/Helpers/Builders/AppUserBuilder.cs +++ b/API/Helpers/Builders/AppUserBuilder.cs @@ -23,16 +23,17 @@ public class AppUserBuilder : IEntityBuilder UserPreferences = new AppUserPreferences { Theme = theme ?? Seed.DefaultThemes.First(), + Locale = "en" }, - ReadingLists = new List(), - Bookmarks = new List(), - Libraries = new List(), - Ratings = new List(), - Progresses = new List(), - Devices = new List(), + ReadingLists = [], + Bookmarks = [], + Libraries = [], + Ratings = [], + Progresses = [], + Devices = [], Id = 0, - DashboardStreams = new List(), - SideNavStreams = new List(), + DashboardStreams = [], + SideNavStreams = [], ReadingProfiles = [], }; } @@ -65,7 +66,7 @@ public class AppUserBuilder : IEntityBuilder public AppUserBuilder WithRole(string role) { - _appUser.UserRoles ??= new List(); + _appUser.UserRoles ??= []; _appUser.UserRoles.Add(new AppUserRole() {Role = new AppRole() {Name = role}}); return this; } diff --git a/API/Helpers/Converters/AnnotationFilterFieldValueConverter.cs b/API/Helpers/Converters/AnnotationFilterFieldValueConverter.cs index d31e27389..23b0b6f07 100644 --- a/API/Helpers/Converters/AnnotationFilterFieldValueConverter.cs +++ b/API/Helpers/Converters/AnnotationFilterFieldValueConverter.cs @@ -14,7 +14,8 @@ public static class AnnotationFilterFieldValueConverter { AnnotationFilterField.Owner or AnnotationFilterField.HighlightSlot or - AnnotationFilterField.Library => value.ParseIntArray(), + AnnotationFilterField.Library or + AnnotationFilterField.Series => value.ParseIntArray(), AnnotationFilterField.Spoiler => bool.Parse(value), AnnotationFilterField.Selection => value, AnnotationFilterField.Comment => value, diff --git a/API/Services/AnnotationService.cs b/API/Services/AnnotationService.cs index 75d2bc3e2..11f4e861e 100644 --- a/API/Services/AnnotationService.cs +++ b/API/Services/AnnotationService.cs @@ -33,23 +33,20 @@ public interface IAnnotationService Task ExportAnnotations(int userId, IList? annotationIds = null); } -public class AnnotationService : IAnnotationService +public class AnnotationService( + ILogger logger, + IUnitOfWork unitOfWork, + IBookService bookService, + IEventHub eventHub) + : IAnnotationService { - private readonly IUnitOfWork _unitOfWork; - private readonly IBookService _bookService; - private readonly IDirectoryService _directoryService; - private readonly IEventHub _eventHub; - private readonly ILogger _logger; - public AnnotationService(IUnitOfWork unitOfWork, IBookService bookService, - IDirectoryService directoryService, IEventHub eventHub, ILogger logger) + private static readonly JsonSerializerOptions ExportJsonSerializerOptions = new() { - _unitOfWork = unitOfWork; - _bookService = bookService; - _directoryService = directoryService; - _eventHub = eventHub; - _logger = logger; - } + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; /// /// Create a new Annotation for the user against a Chapter @@ -67,12 +64,12 @@ public class AnnotationService : IAnnotationService throw new KavitaException("invalid-payload"); } - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId) ?? throw new KavitaException("chapter-doesnt-exist"); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId) ?? throw new KavitaException("chapter-doesnt-exist"); var chapterTitle = string.Empty; try { - var toc = await _bookService.GenerateTableOfContents(chapter); + var toc = await bookService.GenerateTableOfContents(chapter); var pageTocs = BookChapterItemHelper.GetTocForPage(toc, dto.PageNumber); if (pageTocs.Count > 0) { @@ -105,14 +102,14 @@ public class AnnotationService : IAnnotationService ChapterTitle = chapterTitle }; - _unitOfWork.AnnotationRepository.Attach(annotation); - await _unitOfWork.CommitAsync(); + unitOfWork.AnnotationRepository.Attach(annotation); + await unitOfWork.CommitAsync(); - return await _unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id); + return (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id))!; } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when creating an annotation on {ChapterId} - Page {Page}", dto.ChapterId, dto.PageNumber); + logger.LogError(ex, "There was an exception when creating an annotation on {ChapterId} - Page {Page}", dto.ChapterId, dto.PageNumber); throw new KavitaException("annotation-failed-create"); } } @@ -128,7 +125,7 @@ public class AnnotationService : IAnnotationService { try { - var annotation = await _unitOfWork.AnnotationRepository.GetAnnotation(dto.Id); + var annotation = await unitOfWork.AnnotationRepository.GetAnnotation(dto.Id); if (annotation == null || annotation.AppUserId != userId) throw new KavitaException("denied"); annotation.ContainsSpoiler = dto.ContainsSpoiler; @@ -137,17 +134,19 @@ public class AnnotationService : IAnnotationService annotation.CommentHtml = dto.CommentHtml; annotation.CommentPlainText = StripHtml(dto.CommentHtml); - _unitOfWork.AnnotationRepository.Update(annotation); + unitOfWork.AnnotationRepository.Update(annotation); - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync()) { - await _eventHub.SendMessageToAsync(MessageFactory.AnnotationUpdate, + dto = (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id))!; + + await eventHub.SendMessageToAsync(MessageFactory.AnnotationUpdate, MessageFactory.AnnotationUpdateEvent(dto), userId); return dto; } } catch (Exception ex) { - _logger.LogError(ex, "There was an exception updating Annotation for Chapter {ChapterId} - Page {PageNumber}", dto.ChapterId, dto.PageNumber); + logger.LogError(ex, "There was an exception updating Annotation for Chapter {ChapterId} - Page {PageNumber}", dto.ChapterId, dto.PageNumber); } throw new KavitaException("generic-error"); @@ -158,7 +157,7 @@ public class AnnotationService : IAnnotationService try { // Get users with preferences for highlight colors - var users = (await _unitOfWork.UserRepository + var users = (await unitOfWork.UserRepository .GetAllUsersAsync(AppUserIncludes.UserPreferences)) .ToDictionary(u => u.Id, u => u); @@ -166,15 +165,15 @@ public class AnnotationService : IAnnotationService IList annotations; if (annotationIds == null) { - annotations = await _unitOfWork.AnnotationRepository.GetFullAnnotationsByUserIdAsync(userId); + annotations = await unitOfWork.AnnotationRepository.GetFullAnnotationsByUserIdAsync(userId); } else { - annotations = await _unitOfWork.AnnotationRepository.GetFullAnnotations(userId, annotationIds); + annotations = await unitOfWork.AnnotationRepository.GetFullAnnotations(userId, annotationIds); } // Get settings for hostname - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var hostname = !string.IsNullOrWhiteSpace(settings.HostName) ? settings.HostName : "http://localhost:5000"; // Group annotations by series, then by volume @@ -235,22 +234,15 @@ public class AnnotationService : IAnnotationService }).ToArray(); // Serialize to JSON - var options = new JsonSerializerOptions - { - WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; + var json = JsonSerializer.Serialize(exportData, ExportJsonSerializerOptions); - var json = JsonSerializer.Serialize(exportData, options); - - _logger.LogInformation("Successfully exported {AnnotationCount} annotations for user {UserId}", annotations.Count, userId); + logger.LogInformation("Successfully exported {AnnotationCount} annotations for user {UserId}", annotations.Count, userId); return json; } catch (Exception ex) { - _logger.LogError(ex, "Failed to export annotations for user {UserId}", userId); + logger.LogError(ex, "Failed to export annotations for user {UserId}", userId); throw new KavitaException("annotation-export-failed"); } } @@ -271,7 +263,7 @@ public class AnnotationService : IAnnotationService } catch (Exception exception) { - _logger.LogError(exception, "Invalid html, cannot parse plain text"); + logger.LogError(exception, "Invalid html, cannot parse plain text"); return string.Empty; } } diff --git a/API/Services/OpdsService.cs b/API/Services/OpdsService.cs index f65d9dc41..acaf4e1a0 100644 --- a/API/Services/OpdsService.cs +++ b/API/Services/OpdsService.cs @@ -62,7 +62,16 @@ public class OpdsService : IOpdsService private readonly XmlSerializer _xmlSerializer; - private const int PageSize = 20; + public const int PageSize = 20; + public const int FirstPageNumber = 1; + public const string DefaultApiPrefix = "/api/opds/"; + + public const string NoReadingProgressIcon = "⭘"; + public const string QuarterReadingProgressIcon = "◔"; + public const string HalfReadingProgressIcon = "◑"; + public const string AboveHalfReadingProgressIcon = "◕"; + public const string FullReadingProgressIcon = "⬤"; + private readonly FilterV2Dto _filterV2Dto = new(); private readonly FilterDto _filterDto = new() { @@ -582,14 +591,14 @@ public class OpdsService : IOpdsService var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{apiKey}/reading-list/{readingListId}", apiKey, prefix); SetFeedId(feed, $"reading-list-{readingListId}"); - var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId, GetUserParams(request.PageNumber))).ToList(); var totalItems = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId)).Count(); // Check if there is reading progress or not, if so, inject a "continue-reading" item - var firstReadReadingListItem = items.FirstOrDefault(i => i.PagesRead > 0); - if (firstReadReadingListItem != null && request.PageNumber == 0) + var firstReadReadingListItem = items.FirstOrDefault(i => i.PagesRead > 0 && i.PagesRead != i.PagesTotal) ?? + items.FirstOrDefault(i => i.PagesRead == 0 && i.PagesRead != i.PagesTotal); + if (firstReadReadingListItem != null && request.PageNumber == FirstPageNumber) { await AddContinueReadingPoint(firstReadReadingListItem, feed, request); } @@ -724,8 +733,9 @@ public class OpdsService : IOpdsService var chapterDtos = await _unitOfWork.ChapterRepository.GetChapterDtoByIdsAsync(volume.Chapters.Select(c => c.Id), userId); // Check if there is reading progress or not, if so, inject a "continue-reading" item - var firstChapterWithProgress = chapterDtos.FirstOrDefault(c => c.PagesRead > 0); - if (firstChapterWithProgress != null) + var firstChapterWithProgress = chapterDtos.FirstOrDefault(i => i.PagesRead > 0 && i.PagesRead != i.Pages) ?? + chapterDtos.FirstOrDefault(i => i.PagesRead == 0 && i.PagesRead != i.Pages); + if (firstChapterWithProgress != null && request.PageNumber == FirstPageNumber) { var chapterDto = await _readerService.GetContinuePoint(seriesId, userId); await AddContinueReadingPoint(seriesId, chapterDto, feed, request); @@ -1221,19 +1231,22 @@ public class OpdsService : IOpdsService private static string GetReadingProgressIcon(int pagesRead, int totalPages) { - if (pagesRead == 0) return "⭘"; + if (pagesRead == 0) + { + return NoReadingProgressIcon; + } var percentageRead = (double)pagesRead / totalPages; return percentageRead switch { // 100% - >= 1.0 => "⬤", + >= 1.0 => FullReadingProgressIcon, // > 50% and < 100% - > 0.5 => "◕", + > 0.5 => AboveHalfReadingProgressIcon, // > 25% and <= 50% - > 0.25 => "◑", - _ => "◔" + > 0.25 => HalfReadingProgressIcon, + _ => QuarterReadingProgressIcon }; } diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 3343c090b..0223c7692 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -209,6 +209,7 @@ public class ReaderService : IReaderService if (user.Progresses == null) { + //throw new ArgumentException("AppUser must have Progress on it"); // TODO: Figure out the impact of switching to a more dev experience exception throw new KavitaException("progress-must-exist"); } diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index da5294f5c..e9a3315d5 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -355,7 +355,7 @@ public class StatsService : IStatsService userDto.DevicePlatforms = user.Devices.Select(d => d.Platform).ToList(); userDto.SeriesBookmarksCreatedCount = user.Bookmarks.Count; userDto.SmartFilterCreatedCount = user.SmartFilters.Count; - userDto.IsSharingReviews = user.UserPreferences.ShareReviews; + userDto.IsSharingReviews = user.UserPreferences.SocialPreferences.ShareReviews; userDto.WantToReadSeriesCount = user.WantToRead.Count; userDto.IdentityProvider = user.IdentityProvider; diff --git a/API/redo-migration.sh b/API/redo-migration.sh new file mode 100755 index 000000000..76ef8fc7f --- /dev/null +++ b/API/redo-migration.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +migrations=($(ls -1 Data/Migrations/*.cs 2>/dev/null | grep -v "Designer.cs" | grep -v "Snapshot.cs" | sort -r)) + +if [ ${#migrations[@]} -lt 2 ]; then + echo "Error: Need at least 2 migrations to redo" + exit 1 +fi + +second_last=$(basename "${migrations[1]}" .cs) + +last=$(basename "${migrations[0]}" .cs) +last_name=$(echo "$last" | sed 's/^[0-9]*_//') + +echo "Rolling back to: $second_last" +echo "Removing and re-adding: $last_name" +read -p "Continue? (y/N) " -n 1 -r +echo "" + +if [[ $REPLY =~ ^[Yy]$ ]]; then + dotnet ef database update "$second_last" && \ + dotnet ef migrations remove && \ + dotnet ef migrations add "$last_name" +else + echo "Cancelled" + exit 0 +fi diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index a86ee6586..4b53f2fd6 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -48,6 +48,7 @@ "ngx-stars": "^1.6.5", "ngx-toastr": "^19.1.0", "nosleep.js": "^0.12.0", + "quill": "^2.0.3", "rxjs": "^7.8.2", "screenfull": "^6.0.2", "swiper": "^11.2.10", @@ -1077,6 +1078,7 @@ "version": "20.3.2", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.2.tgz", "integrity": "sha512-rLox2THiALVQqYGUaxZ6YD8qUoXIOGTw3s0tim9/U65GuXGRtYgG0ZQWYp3yjEBes0Ksx2/15eFPp1Ol4FdEKQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "7.28.3", @@ -1109,6 +1111,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1121,6 +1124,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1133,6 +1137,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^7.2.0", @@ -1147,12 +1152,14 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, "license": "MIT" }, "node_modules/@angular/compiler-cli/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -1170,6 +1177,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -1185,6 +1193,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -1202,6 +1211,7 @@ "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^9.0.1", @@ -1219,6 +1229,7 @@ "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, "license": "ISC", "engines": { "node": "^20.19.0 || ^22.12.0 || >=23" @@ -6408,6 +6419,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -6580,7 +6592,8 @@ "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "node_modules/cookie": { "version": "0.7.2", @@ -7096,6 +7109,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -7666,8 +7680,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/fast-glob": { "version": "3.3.3", @@ -8191,7 +8204,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -8786,23 +8799,20 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -10001,8 +10011,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/parent-module": { "version": "1.0.1", @@ -10365,7 +10374,6 @@ "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "eventemitter3": "^5.0.1", "lodash-es": "^4.17.21", @@ -10381,7 +10389,6 @@ "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", "license": "MIT", - "peer": true, "dependencies": { "fast-diff": "^1.3.0", "lodash.clonedeep": "^4.5.0", @@ -10421,6 +10428,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -10433,7 +10441,8 @@ "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true }, "node_modules/require-from-string": { "version": "2.0.2", @@ -10642,7 +10651,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true + "dev": true }, "node_modules/sass": { "version": "1.90.0", @@ -10680,6 +10689,7 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11486,7 +11496,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/UI/Web/package.json b/UI/Web/package.json index 5987429a3..21eb882f4 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -59,6 +59,7 @@ "ngx-stars": "^1.6.5", "ngx-toastr": "^19.1.0", "nosleep.js": "^0.12.0", + "quill": "^2.0.3", "rxjs": "^7.8.2", "screenfull": "^6.0.2", "swiper": "^11.2.10", diff --git a/UI/Web/src/app/_helpers/browser.ts b/UI/Web/src/app/_helpers/browser.ts index 4d92e207c..d9200e19f 100644 --- a/UI/Web/src/app/_helpers/browser.ts +++ b/UI/Web/src/app/_helpers/browser.ts @@ -9,6 +9,68 @@ export const isSafari = [ // iPad on iOS 13 detection || (navigator.userAgent.includes("Mac") && "ontouchend" in document); +/** + * Detects if the browser is Chromium-based (Chrome, Edge, Opera, etc.) + */ +export const isChromiumBased = (): boolean => { + const userAgent = navigator.userAgent.toLowerCase(); + // Check for Chrome/Chromium indicators + return (userAgent.includes('chrome') || + userAgent.includes('crios') || // Chrome on iOS + userAgent.includes('chromium') || + userAgent.includes('edg/') || // Edge Chromium + userAgent.includes('opr/') || // Opera + userAgent.includes('samsungbrowser')) && + !userAgent.includes('firefox') && + !userAgent.includes('safari') || + (userAgent.includes('safari') && userAgent.includes('chrome')); // Chrome includes Safari in UA +}; + +/** + * Detects if the device is mobile or tablet + */ +export const isMobileDevice = (): boolean => { + // Check for touch capability and screen size + const hasTouchScreen = 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + (window.matchMedia && window.matchMedia('(pointer: coarse)').matches); + + // Additional mobile UA checks + const mobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test( + navigator.userAgent.toLowerCase() + ); + + // Screen width check for tablets + const isSmallScreen = window.innerWidth <= 1024; + + return hasTouchScreen && (mobileUA || isSmallScreen); +}; + +/** + * Detects if running on a Chromium-based mobile browser + */ +export const isMobileChromium = (): boolean => { + return isChromiumBased() && isMobileDevice(); +}; + +/** + * Gets the Chrome/Chromium version + */ +export const getChromiumVersion = (): Version | null => { + const userAgent = navigator.userAgent; + const matches = userAgent.match(/(?:Chrome|CriOS|Edg|OPR)\/(\d+)\.(\d+)\.(\d+)/); + + if (matches) { + return new Version( + parseInt(matches[1], 10), + parseInt(matches[2], 10), + parseInt(matches[3], 10) + ); + } + + return null; +}; + /** * Represents a Version for a browser */ @@ -48,7 +110,6 @@ export class Version { } } - export const getIosVersion = () => { const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/); if (match) { diff --git a/UI/Web/src/app/_models/metadata/v2/annotations-filter.ts b/UI/Web/src/app/_models/metadata/v2/annotations-filter.ts index dd2dab31b..a436f1def 100644 --- a/UI/Web/src/app/_models/metadata/v2/annotations-filter.ts +++ b/UI/Web/src/app/_models/metadata/v2/annotations-filter.ts @@ -8,6 +8,7 @@ export enum AnnotationsFilterField { HighlightSlots = 4, Selection = 5, Comment = 6, + Series = 7 } export const allAnnotationsFilterFields = Object.keys(AnnotationsFilterField) diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index 8a98a2c92..be26da1b1 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -1,6 +1,7 @@ import {PageLayoutMode} from '../page-layout-mode'; import {SiteTheme} from './site-theme'; import {HighlightSlot} from "../../book-reader/_models/annotations/highlight-slot"; +import {AgeRating} from "../metadata/age-rating"; export interface Preferences { @@ -11,7 +12,6 @@ export interface Preferences { promptForDownloadSize: boolean; noTransitions: boolean; collapseSeriesRelationships: boolean; - shareReviews: boolean; locale: string; bookReaderHighlightSlots: HighlightSlot[]; colorScapeEnabled: boolean; @@ -19,5 +19,17 @@ export interface Preferences { // Kavita+ aniListScrobblingEnabled: boolean; wantToReadSync: boolean; + + // Social + socialPreferences: SocialPreferences; +} + +export interface SocialPreferences { + shareReviews: boolean; + shareAnnotations: boolean; + viewOtherAnnotations: boolean; + socialLibraries: number[]; + socialMaxAgeRating: AgeRating; + socialIncludeUnknowns: boolean; } diff --git a/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts b/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts index 691d546a1..fd9262099 100644 --- a/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts +++ b/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts @@ -36,6 +36,8 @@ export class GenericFilterFieldPipe implements PipeTransform { return translate('filter-field-pipe.libraries'); case AnnotationsFilterField.Spoiler: return translate('generic-filter-field-pipe.annotation-spoiler'); + case AnnotationsFilterField.Series: + return translate('generic-filter-field-pipe.series'); } } diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index 499fb81ed..9f6ac5b3f 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -1,5 +1,5 @@ import {HttpClient} from '@angular/common/http'; -import {DestroyRef, inject, Injectable} from '@angular/core'; +import {computed, DestroyRef, inject, Injectable} from '@angular/core'; import {Observable, of, ReplaySubject, shareReplay} from 'rxjs'; import {filter, map, switchMap, tap} from 'rxjs/operators'; import {environment} from 'src/environments/environment'; @@ -64,8 +64,10 @@ export class AccountService { if (!u) return false; return this.hasAdminRole(u); }), shareReplay({bufferSize: 1, refCount: true})); + public readonly isAdmin = toSignal(this.isAdmin$); - public readonly currentUserSignal = toSignal(this.currentUserSource); + public readonly currentUserSignal = toSignal(this.currentUser$); + public readonly userId = computed(() => this.currentUserSignal()?.id); /** * SetTimeout handler for keeping track of refresh token call @@ -189,7 +191,7 @@ export class AccountService { } getRoles() { - return this.httpClient.get(this.baseUrl + 'account/roles'); + return this.httpClient.get(this.baseUrl + 'account/roles'); } @@ -201,8 +203,7 @@ export class AccountService { if (user) { this.setCurrentUser(user); } - }), - takeUntilDestroyed(this.destroyRef) + }) ); } diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index bd2d733d2..7b1d6aa9d 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -132,6 +132,8 @@ export enum Action { */ ClearReadingProfile = 31, Export = 32, + Like = 33, + UnLike = 34, } /** @@ -1126,7 +1128,25 @@ export class ActionFactoryService { shouldRender: this.dummyShouldRender, requiredRoles: [], children: [], - } + }, + { + action: Action.Like, + title: 'like', + description: '', + callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, + requiredRoles: [], + children: [], + }, + { + action: Action.UnLike, + title: 'unlike', + description: '', + callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, + requiredRoles: [], + children: [], + }, ]; diff --git a/UI/Web/src/app/_services/annotation.service.ts b/UI/Web/src/app/_services/annotation.service.ts index 8cf857e2b..2ffdba385 100644 --- a/UI/Web/src/app/_services/annotation.service.ts +++ b/UI/Web/src/app/_services/annotation.service.ts @@ -87,7 +87,7 @@ export class AnnotationService { return this.httpClient.post[]>(this.baseUrl + 'annotation/all-filtered', filter, {observe: 'response', params}).pipe( map((res: any) => { - return this.utilityService.createPaginatedResult(res as PaginatedResult[]); + return this.utilityService.createPaginatedResult(res); }), ); } @@ -114,7 +114,6 @@ export class AnnotationService { return this.httpClient.post(this.baseUrl + 'annotation/update', data).pipe( switchMap(newAnnotation => this.getAllAnnotations(data.chapterId)), tap(_ => { - console.log('emitting edit event'); this._events.set({ pageNumber: data.pageNumber, type: 'edit', @@ -201,4 +200,26 @@ export class AnnotationService { }) ); } + + /** + * Does not emit an event + * @param ids + */ + likeAnnotations(ids: number[]) { + const userId = this.accountService.currentUserSignal()?.id; + if (!userId) return of(); + + return this.httpClient.post(this.baseUrl + 'annotation/like', ids); + } + + /** + * Does not emit an event + * @param ids + */ + unLikeAnnotations(ids: number[]) { + const userId = this.accountService.currentUserSignal()?.id; + if (!userId) return of(); + + return this.httpClient.post(this.baseUrl + 'annotation/unlike', ids); + } } diff --git a/UI/Web/src/app/_services/epub-reader-settings.service.ts b/UI/Web/src/app/_services/epub-reader-settings.service.ts index 2e25d520e..dec0077a8 100644 --- a/UI/Web/src/app/_services/epub-reader-settings.service.ts +++ b/UI/Web/src/app/_services/epub-reader-settings.service.ts @@ -277,12 +277,12 @@ export class EpubReaderSettingsService { this._immersiveMode.set(profile.bookReaderImmersiveMode); // Set up page styles - this.setPageStyles( + this._pageStyles.set(this.buildPageStyles( profile.bookReaderFontFamily, profile.bookReaderFontSize + '%', profile.bookReaderMargin + 'vw', profile.bookReaderLineSpacing + '%' - ); + )); } /** @@ -597,12 +597,19 @@ export class EpubReaderSettingsService { */ resetSettings() { const defaultStyles = this.getDefaultPageStyles(); - this.setPageStyles( + + const styles = this.buildPageStyles( defaultStyles["font-family"], defaultStyles["font-size"], defaultStyles['margin-left'], defaultStyles['line-height'], ); + + // Update form to ensure RP is updated, forgive me for the replace... + this.settingsForm.get('bookReaderFontFamily')!.setValue(styles["font-family"]); + this.settingsForm.get('bookReaderFontSize')!.setValue(parseInt(styles["font-size"].replace("%", ""))); + this.settingsForm.get('bookReaderMargin')!.setValue(parseInt(styles["margin-left"].replace("vw", ""))); + this.settingsForm.get('bookReaderLineSpacing')!.setValue(parseInt(styles["line-height"].replace("%", ""))); } @@ -653,7 +660,7 @@ export class EpubReaderSettingsService { return data; } - private setPageStyles(fontFamily?: string, fontSize?: string, margin?: string, lineHeight?: string): void { + private buildPageStyles(fontFamily?: string, fontSize?: string, margin?: string, lineHeight?: string) { const windowWidth = window.innerWidth || this.document.documentElement.clientWidth || this.document.body.clientWidth; const mobileBreakpointMarginOverride = 700; @@ -671,7 +678,7 @@ export class EpubReaderSettingsService { 'line-height': lineHeight || currentStyles['line-height'] || '100%' }; - this._pageStyles.set(newStyles); + return newStyles } public getDefaultPageStyles(): PageStyle { diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index 598cb4138..dd3f48d8b 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -37,6 +37,7 @@ import {AnnotationsFilterField} from "../_models/metadata/v2/annotations-filter" import {AccountService} from "./account.service"; import {MemberService} from "./member.service"; import {RgbaColor} from "../book-reader/_models/annotations/highlight-slot"; +import {SeriesService} from "./series.service"; @Injectable({ providedIn: 'root' @@ -51,6 +52,7 @@ export class MetadataService { private readonly utilityService = inject(UtilityService); private readonly accountService = inject(AccountService); private readonly memberService = inject(MemberService) + private readonly seriesService = inject(SeriesService) private readonly highlightSlots = computed(() => { return this.accountService.currentUserSignal()?.preferences?.bookReaderHighlightSlots ?? []; @@ -280,6 +282,10 @@ export class MetadataService { return of(this.highlightSlots().map((slot, idx) => { return {value: slot.slotNumber, label: translate('highlight-bar.slot-label', {slot: slot.slotNumber + 1}), color: slot.color}; // Slots start at 0 })); + case AnnotationsFilterField.Series: + return this.seriesService.getSeriesWithAnnotations().pipe(map(series => series.map(s => { + return {value: s.id, label: s.name}; + }))); } return of([]); diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index b672f78ad..3ed4efb69 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -1,5 +1,5 @@ import {HttpClient, HttpParams} from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import {inject, Injectable} from '@angular/core'; import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {environment} from 'src/environments/environment'; @@ -224,4 +224,8 @@ export class SeriesService { updateDontMatch(seriesId: number, dontMatch: boolean) { return this.httpClient.post(this.baseUrl + `series/dont-match?seriesId=${seriesId}&dontMatch=${dontMatch}`, {}, TextResonse); } + + getSeriesWithAnnotations() { + return this.httpClient.get(this.baseUrl + 'series/series-with-annotations'); + } } 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 4da6289c8..870662465 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 @@ -1,54 +1,18 @@
- @if (readingTime) { + @if (accountService.isAdmin() && filePaths && filePaths.length > 0) {
-

{{t('read-time-title')}}

-
- {{readingTime | readTime}} +

{{t('file-path-title')}}

+
+ @for (fp of filePaths; track $index) { + {{fp}} + }
} - @if (releaseYear) { -
-

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

-
- {{releaseYear}} -
-
- } - - @if (language) { -
-

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

-
- {{language | languageName | async}} -
-
- } - - @if (ageRating) { -
-

{{t('age-rating-title')}}

-
- -
-
- } - - @if (format) { -
-

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

-
- {{format | mangaFormat }} -
-
- } - - @if (!suppressEmptyGenres || genres.length > 0) { -

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

diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts b/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts index 096826964..c6f4ebc38 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts @@ -22,6 +22,7 @@ import {AsyncPipe} from "@angular/common"; import {SafeUrlPipe} from "../../_pipes/safe-url.pipe"; import {AgeRating} from "../../_models/metadata/age-rating"; import {AgeRatingImageComponent} from "../age-rating-image/age-rating-image.component"; +import {AccountService} from "../../_services/account.service"; @Component({ selector: 'app-details-tab', @@ -31,13 +32,7 @@ import {AgeRatingImageComponent} from "../age-rating-image/age-rating-image.comp TranslocoDirective, ImageComponent, BadgeExpanderComponent, - ReadTimePipe, - SeriesFormatComponent, - MangaFormatPipe, - LanguageNamePipe, - AsyncPipe, SafeUrlPipe, - AgeRatingImageComponent ], templateUrl: './details-tab.component.html', styleUrl: './details-tab.component.scss', @@ -47,22 +42,19 @@ export class DetailsTabComponent { protected readonly imageService = inject(ImageService); private readonly filterUtilityService = inject(FilterUtilitiesService); + protected readonly accountService = inject(AccountService); protected readonly PersonRole = PersonRole; protected readonly FilterField = FilterField; protected readonly MangaFormat = MangaFormat; @Input({required: true}) metadata!: IHasCast; - @Input() readingTime: IHasReadingTime | undefined; - @Input() ageRating: AgeRating | undefined; - @Input() language: string | undefined; - @Input() format: MangaFormat | undefined; - @Input() releaseYear: number | undefined; @Input() genres: Array = []; @Input() tags: Array = []; @Input() webLinks: Array = []; @Input() suppressEmptyGenres: boolean = false; @Input() suppressEmptyTags: boolean = false; + @Input() filePaths: string[] | undefined; openGeneric(queryParamName: FilterField, filter: string | number) { diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.html b/UI/Web/src/app/admin/edit-user/edit-user.component.html index dc959fd32..7c4583f06 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.html +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.html @@ -93,11 +93,19 @@
- +
- +
diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.ts b/UI/Web/src/app/admin/edit-user/edit-user.component.ts index b93a08092..9a698de0b 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.ts +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.ts @@ -6,18 +6,16 @@ import { DestroyRef, inject, model, - OnInit + OnInit, signal } from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; import {AgeRestriction} from 'src/app/_models/metadata/age-restriction'; import {Library} from 'src/app/_models/library/library'; import {Member} from 'src/app/_models/auth/member'; -import {AccountService} from 'src/app/_services/account.service'; +import {AccountService, allRoles, Role} from 'src/app/_services/account.service'; import {SentenceCasePipe} from '../../_pipes/sentence-case.pipe'; import {RestrictionSelectorComponent} from '../../user-settings/restriction-selector/restriction-selector.component'; -import {LibrarySelectorComponent} from '../library-selector/library-selector.component'; -import {RoleSelectorComponent} from '../role-selector/role-selector.component'; import {AsyncPipe} from '@angular/common'; import {TranslocoDirective} from "@jsverse/transloco"; import {debounceTime, distinctUntilChanged, Observable, startWith, tap} from "rxjs"; @@ -26,6 +24,11 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {ServerSettings} from "../_models/server-settings"; import {IdentityProvider, IdentityProviders} from "../../_models/user"; import {IdentityProviderPipePipe} from "../../_pipes/identity-provider.pipe"; +import { + MultiCheckBoxItem, + SettingMultiCheckBox +} from "../../settings/_components/setting-multi-check-box/setting-multi-check-box.component"; +import {LibraryService} from "../../_services/library.service"; const AllowedUsernameCharacters = /^[a-zA-Z0-9\-._@+/]*$/; const EmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -34,7 +37,7 @@ const EmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; selector: 'app-edit-user', templateUrl: './edit-user.component.html', styleUrls: ['./edit-user.component.scss'], - imports: [ReactiveFormsModule, RoleSelectorComponent, LibrarySelectorComponent, RestrictionSelectorComponent, SentenceCasePipe, TranslocoDirective, AsyncPipe, IdentityProviderPipePipe], + imports: [ReactiveFormsModule, RestrictionSelectorComponent, SentenceCasePipe, TranslocoDirective, AsyncPipe, IdentityProviderPipePipe, SettingMultiCheckBox], changeDetection: ChangeDetectionStrategy.OnPush }) export class EditUserComponent implements OnInit { @@ -43,6 +46,7 @@ export class EditUserComponent implements OnInit { private readonly cdRef = inject(ChangeDetectorRef); private readonly destroyRef = inject(DestroyRef); protected readonly modal = inject(NgbActiveModal); + private readonly libraryService = inject(LibraryService); member = model.required(); settings = model.required(); @@ -53,8 +57,16 @@ export class EditUserComponent implements OnInit { return setting.oidcConfig.syncUserSettings && member.identityProvider === IdentityProvider.OpenIdConnect; }); - selectedRoles: Array = []; - selectedLibraries: Array = []; + libraries = signal([]); + libraryOptions = computed[]>(() => this.libraries().map(l => { + return { label: l.name, value: l.id }; + })); + roleOptions: MultiCheckBoxItem[] = allRoles.map(r => { + return { label: r, value: r, disableFunc: (r: Role, selected: Role[]) => { + return r !== Role.Admin && selected.includes(Role.Admin); + }} + }); + selectedRestriction!: AgeRestriction; isSaving: boolean = false; @@ -66,14 +78,18 @@ export class EditUserComponent implements OnInit { public get email() { return this.userForm.get('email'); } public get username() { return this.userForm.get('username'); } public get password() { return this.userForm.get('password'); } - public get hasAdminRoleSelected() { return this.selectedRoles.includes('Admin'); }; + get hasAdminRoleSelected() { return this.userForm.get('roles')!.value.includes(Role.Admin); }; ngOnInit(): void { + this.libraryService.getLibraries().subscribe(libraries => this.libraries.set(libraries)); + this.userForm.addControl('email', new FormControl(this.member().email, [Validators.required])); this.userForm.addControl('username', new FormControl(this.member().username, [Validators.required, Validators.pattern(AllowedUsernameCharacters)])); this.userForm.addControl('identityProvider', new FormControl(this.member().identityProvider, [Validators.required])); + this.userForm.addControl('roles', new FormControl(this.member().roles)); + this.userForm.addControl('libraries', new FormControl(this.member().libraries.map(l => l.id))); this.userForm.get('identityProvider')!.valueChanges.pipe( tap(value => { @@ -97,21 +113,11 @@ export class EditUserComponent implements OnInit { this.cdRef.markForCheck(); } - updateRoleSelection(roles: Array) { - this.selectedRoles = roles; - this.cdRef.markForCheck(); - } - updateRestrictionSelection(restriction: AgeRestriction) { this.selectedRestriction = restriction; this.cdRef.markForCheck(); } - updateLibrarySelection(libraries: Array) { - this.selectedLibraries = libraries.map(l => l.id); - this.cdRef.markForCheck(); - } - close() { this.modal.close(false); } @@ -119,8 +125,6 @@ export class EditUserComponent implements OnInit { save() { const model = this.userForm.getRawValue(); model.userId = this.member().id; - model.roles = this.selectedRoles; - model.libraries = this.selectedLibraries; model.ageRestriction = this.selectedRestriction; model.identityProvider = parseInt(model.identityProvider, 10) as IdentityProvider; diff --git a/UI/Web/src/app/admin/invite-user/invite-user.component.html b/UI/Web/src/app/admin/invite-user/invite-user.component.html index d6a494595..b48438513 100644 --- a/UI/Web/src/app/admin/invite-user/invite-user.component.html +++ b/UI/Web/src/app/admin/invite-user/invite-user.component.html @@ -30,11 +30,19 @@
- +
- +
diff --git a/UI/Web/src/app/admin/invite-user/invite-user.component.ts b/UI/Web/src/app/admin/invite-user/invite-user.component.ts index d11742cb8..cb4cd47ad 100644 --- a/UI/Web/src/app/admin/invite-user/invite-user.component.ts +++ b/UI/Web/src/app/admin/invite-user/invite-user.component.ts @@ -1,25 +1,28 @@ -import {ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; -import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; +import {ChangeDetectorRef, Component, computed, inject, OnInit, signal} from '@angular/core'; +import {FormControl, FormGroup, NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; import {ToastrService} from 'ngx-toastr'; import {AgeRestriction} from 'src/app/_models/metadata/age-restriction'; import {InviteUserResponse} from 'src/app/_models/auth/invite-user-response'; import {Library} from 'src/app/_models/library/library'; import {AgeRating} from 'src/app/_models/metadata/age-rating'; -import {AccountService} from 'src/app/_services/account.service'; +import {AccountService, allRoles, Role} from 'src/app/_services/account.service'; import {ApiKeyComponent} from '../../user-settings/api-key/api-key.component'; import {RestrictionSelectorComponent} from '../../user-settings/restriction-selector/restriction-selector.component'; -import {LibrarySelectorComponent} from '../library-selector/library-selector.component'; -import {RoleSelectorComponent} from '../role-selector/role-selector.component'; import {translate, TranslocoDirective} from "@jsverse/transloco"; import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; +import {LibraryService} from "../../_services/library.service"; +import { + MultiCheckBoxItem, + SettingMultiCheckBox +} from "../../settings/_components/setting-multi-check-box/setting-multi-check-box.component"; @Component({ selector: 'app-invite-user', templateUrl: './invite-user.component.html', styleUrls: ['./invite-user.component.scss'], - imports: [ReactiveFormsModule, RoleSelectorComponent, LibrarySelectorComponent, RestrictionSelectorComponent, - ApiKeyComponent, TranslocoDirective, SafeHtmlPipe] + imports: [ReactiveFormsModule,RestrictionSelectorComponent, + ApiKeyComponent, TranslocoDirective, SafeHtmlPipe, SettingMultiCheckBox] }) export class InviteUserComponent implements OnInit { @@ -27,29 +30,46 @@ export class InviteUserComponent implements OnInit { private readonly accountService = inject(AccountService); private readonly toastr = inject(ToastrService); protected readonly modal = inject(NgbActiveModal); + private readonly libraryService = inject(LibraryService); /** * Maintains if the backend is sending an email */ isSending: boolean = false; - inviteForm: FormGroup = new FormGroup({}); - selectedRoles: Array = []; - selectedLibraries: Array = []; + inviteForm: FormGroup<{ + email: FormControl, + libraries: FormControl, + roles: FormControl, + }> = new FormGroup({ + email: new FormControl(''), + libraries: new FormControl([]), + roles: new FormControl([Role.Login]), + }) as any; selectedRestriction: AgeRestriction = {ageRating: AgeRating.NotApplicable, includeUnknowns: false}; emailLink: string = ''; invited: boolean = false; inviteError: boolean = false; + libraries = signal([]); + libraryOptions = computed[]>(() => this.libraries().map(l => { + return { label: l.name, value: l.id }; + })); + roleOptions: MultiCheckBoxItem[] = allRoles.map(r => { + return { label: r, value: r, disableFunc: (r: Role, selected: Role[]) => { + return r !== Role.Admin && selected.includes(Role.Admin); + }} + }); + makeLink: (val: string) => string = (_: string) => {return this.emailLink}; - get hasAdminRoleSelected() { return this.selectedRoles.includes('Admin'); }; + get hasAdminRoleSelected() { return this.inviteForm.get('roles')!.value.includes(Role.Admin); }; get email() { return this.inviteForm.get('email'); } ngOnInit(): void { - this.inviteForm.addControl('email', new FormControl('', [Validators.required])); + this.libraryService.getLibraries().subscribe(libraries => this.libraries.set(libraries)); } close() { @@ -58,11 +78,11 @@ export class InviteUserComponent implements OnInit { invite() { this.isSending = true; - const email = this.inviteForm.get('email')?.value.trim(); + + const email = this.inviteForm.get('email')!.value; + this.accountService.inviteUser({ - email, - libraries: this.selectedLibraries, - roles: this.selectedRoles, + ...this.inviteForm.getRawValue(), ageRestriction: this.selectedRestriction }).subscribe((data: InviteUserResponse) => { this.emailLink = data.emailLink; @@ -89,16 +109,6 @@ export class InviteUserComponent implements OnInit { }); } - updateRoleSelection(roles: Array) { - this.selectedRoles = roles; - this.cdRef.markForCheck(); - } - - updateLibrarySelection(libraries: Array) { - this.selectedLibraries = libraries.map(l => l.id); - this.cdRef.markForCheck(); - } - updateRestrictionSelection(restriction: AgeRestriction) { this.selectedRestriction = restriction; this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/admin/library-selector/library-selector.component.html b/UI/Web/src/app/admin/library-selector/library-selector.component.html deleted file mode 100644 index a005a8fd7..000000000 --- a/UI/Web/src/app/admin/library-selector/library-selector.component.html +++ /dev/null @@ -1,42 +0,0 @@ - - -
-
-

{{t('title')}}

-
-
- @if(!isLoading && allLibraries.length > 0) { - - - - - } -
-
- - @if (isLoading) { - - } @else { -
-
    - @for (library of allLibraries; track library.name; let i = $index) { -
  • -
    - - -
    -
  • - } @empty { -
  • - {{t('no-data')}} -
  • - } -
-
- } - - - -
diff --git a/UI/Web/src/app/admin/library-selector/library-selector.component.scss b/UI/Web/src/app/admin/library-selector/library-selector.component.scss deleted file mode 100644 index 3f2adc8d1..000000000 --- a/UI/Web/src/app/admin/library-selector/library-selector.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.list-group-item { - border: none; -} \ No newline at end of file diff --git a/UI/Web/src/app/admin/library-selector/library-selector.component.ts b/UI/Web/src/app/admin/library-selector/library-selector.component.ts deleted file mode 100644 index 3ece1a4ba..000000000 --- a/UI/Web/src/app/admin/library-selector/library-selector.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - inject, input, - Input, - OnInit, - Output -} from '@angular/core'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {Library} from 'src/app/_models/library/library'; -import {Member} from 'src/app/_models/auth/member'; -import {LibraryService} from 'src/app/_services/library.service'; -import {TranslocoDirective} from "@jsverse/transloco"; -import {LoadingComponent} from "../../shared/loading/loading.component"; -import {SelectionModel} from "../../typeahead/_models/selection-model"; - -@Component({ - selector: 'app-library-selector', - templateUrl: './library-selector.component.html', - styleUrls: ['./library-selector.component.scss'], - imports: [ReactiveFormsModule, FormsModule, TranslocoDirective, LoadingComponent], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class LibrarySelectorComponent implements OnInit { - - private readonly libraryService = inject(LibraryService); - private readonly cdRef = inject(ChangeDetectorRef); - - @Input() member: Member | undefined; - preSelectedLibraries = input([]); - - @Output() selected: EventEmitter> = new EventEmitter>(); - - allLibraries: Library[] = []; - selectedLibraries: Array<{selected: boolean, data: Library}> = []; - selections!: SelectionModel; - selectAll: boolean = false; - isLoading: boolean = false; - - get hasSomeSelected() { - return this.selections != null && this.selections.hasSomeSelected(); - } - - - ngOnInit(): void { - this.libraryService.getLibraries().subscribe(libs => { - this.allLibraries = libs; - this.setupSelections(); - }); - } - - - setupSelections() { - this.selections = new SelectionModel(false, this.allLibraries); - this.isLoading = false; - - // If a member is passed in, then auto-select their libraries - if (this.member !== undefined) { - this.member.libraries.forEach(lib => { - this.selections.toggle(lib, true, (a, b) => a.name === b.name); - }); - this.selectAll = this.selections.selected().length === this.allLibraries.length; - this.selected.emit(this.selections.selected()); - } else if (this.preSelectedLibraries().length > 0) { - this.preSelectedLibraries().forEach((id) => { - const foundLib = this.allLibraries.find(lib => lib.id === id); - if (foundLib) { - this.selections.toggle(foundLib, true, (a, b) => a.name === b.name); - } - }); - this.selectAll = this.selections.selected().length === this.allLibraries.length; - } - this.cdRef.markForCheck(); - } - - toggleAll() { - this.selectAll = !this.selectAll; - this.allLibraries.forEach(s => this.selections.toggle(s, this.selectAll)); - this.selected.emit(this.selections.selected()); - this.cdRef.markForCheck(); - } - - handleSelection(item: Library) { - this.selections.toggle(item); - const numberOfSelected = this.selections.selected().length; - if (numberOfSelected == 0) { - this.selectAll = false; - } else if (numberOfSelected == this.selectedLibraries.length) { - this.selectAll = true; - } - - this.cdRef.markForCheck(); - this.selected.emit(this.selections.selected()); - } - -} diff --git a/UI/Web/src/app/admin/manage-metadata-mappings/manage-metadata-mappings.component.html b/UI/Web/src/app/admin/manage-metadata-mappings/manage-metadata-mappings.component.html index e52b33434..18931c370 100644 --- a/UI/Web/src/app/admin/manage-metadata-mappings/manage-metadata-mappings.component.html +++ b/UI/Web/src/app/admin/manage-metadata-mappings/manage-metadata-mappings.component.html @@ -2,47 +2,21 @@
- @if(settingsForm().get('blacklist'); as formControl) { - - - - @let val = breakTags(formControl.value); - @for(opt of val; track opt) { - {{opt.trim()}} - } @empty { - {{null | defaultValue}} - } - - - - - - - - } +
- @if(settingsForm().get('whitelist'); as formControl) { - - - @let val = breakTags(formControl.value); - - @for(opt of val; track opt) { - {{opt.trim()}} - } @empty { - {{null | defaultValue}} - } - s - - - - - } +
diff --git a/UI/Web/src/app/admin/manage-metadata-mappings/manage-metadata-mappings.component.ts b/UI/Web/src/app/admin/manage-metadata-mappings/manage-metadata-mappings.component.ts index 20a428357..bd43c6cd1 100644 --- a/UI/Web/src/app/admin/manage-metadata-mappings/manage-metadata-mappings.component.ts +++ b/UI/Web/src/app/admin/manage-metadata-mappings/manage-metadata-mappings.component.ts @@ -1,15 +1,15 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, input, OnInit, signal} from '@angular/core'; import {AgeRatingPipe} from "../../_pipes/age-rating.pipe"; -import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; -import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; -import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component"; import {MetadataFieldMapping, MetadataFieldType, MetadataSettings} from "../_models/metadata-settings"; import {AgeRatingDto} from "../../_models/metadata/age-rating-dto"; import {MetadataService} from "../../_services/metadata.service"; import {translate, TranslocoDirective} from "@jsverse/transloco"; import {AgeRating} from "../../_models/metadata/age-rating"; import {DownloadService} from "../../shared/_services/download.service"; +import { + SettingMultiTextFieldComponent +} from "../../settings/_components/setting-multi-text-field/setting-multi-text-field.component"; export type MetadataMappingsExport = { ageRatingMappings: Record, @@ -22,12 +22,10 @@ export type MetadataMappingsExport = { selector: 'app-manage-metadata-mappings', imports: [ AgeRatingPipe, - DefaultValuePipe, FormsModule, ReactiveFormsModule, - SettingItemComponent, - TagBadgeComponent, TranslocoDirective, + SettingMultiTextFieldComponent, ], templateUrl: './manage-metadata-mappings.component.html', styleUrl: './manage-metadata-mappings.component.scss', @@ -74,8 +72,8 @@ export class ManageMetadataMappingsComponent implements OnInit { const settings = this.settings(); const settingsForm = this.settingsForm(); - settingsForm.addControl('blacklist', new FormControl((settings.blacklist || '').join(','), [])); - settingsForm.addControl('whitelist', new FormControl((settings.whitelist || '').join(','), [])); + settingsForm.addControl('blacklist', new FormControl(settings.blacklist, [])); + settingsForm.addControl('whitelist', new FormControl(settings.whitelist, [])); settingsForm.addControl('ageRatingMappings', this.ageRatingMappings); settingsForm.addControl('fieldMappings', this.fieldMappings); @@ -94,14 +92,6 @@ export class ManageMetadataMappingsComponent implements OnInit { this.cdRef.markForCheck(); } - breakTags(csString: string) { - if (csString) { - return csString.split(','); - } - - return []; - } - public packData(): MetadataMappingsExport { const ageRatingMappings = this.ageRatingMappings.controls.reduce((acc: Record, control) => { const { str, rating } = control.value; @@ -114,15 +104,11 @@ export class ManageMetadataMappingsComponent implements OnInit { const fieldMappings = this.fieldMappings.controls .map((control) => control.value as MetadataFieldMapping) .filter(m => m.sourceValue.length > 0 && m.destinationValue.length > 0); - - const blacklist = (this.settingsForm().get('blacklist')?.value || '').split(',').map((item: string) => item.trim()).filter((tag: string) => tag.length > 0); - const whitelist = (this.settingsForm().get('whitelist')?.value || '').split(',').map((item: string) => item.trim()).filter((tag: string) => tag.length > 0); - return { ageRatingMappings: ageRatingMappings, fieldMappings: fieldMappings, - blacklist: blacklist, - whitelist: whitelist, + blacklist: this.settingsForm().get('blacklist')?.value || [], + whitelist: this.settingsForm().get('whitelist')?.value || [], } } diff --git a/UI/Web/src/app/admin/manage-open-idconnect/manage-open-idconnect.component.html b/UI/Web/src/app/admin/manage-open-idconnect/manage-open-idconnect.component.html index 6a7d16c42..335dd8a13 100644 --- a/UI/Web/src/app/admin/manage-open-idconnect/manage-open-idconnect.component.html +++ b/UI/Web/src/app/admin/manage-open-idconnect/manage-open-idconnect.component.html @@ -4,275 +4,274 @@
-
- - -

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

-
- -
- @if (settingsForm.get('authority'); as formControl) { - - - {{formControl.value}} - - - - - @if (settingsForm.dirty || !settingsForm.untouched) { -
- @if (formControl.errors?.invalidUri) { -
{{t('invalid-uri')}}
- } -
- } -
-
- } + @if (!loading()) { + + -
- @if (settingsForm.get('clientId'); as formControl) { - - - {{formControl.value}} - - - +

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

+
+ +
+ @if (settingsForm.get('authority'); as formControl) { + + + {{formControl.value}} + + + - @if (settingsForm.dirty || !settingsForm.untouched) { -
- @if (formControl.errors && formControl.errors.requiredIf) { -
{{t('other-field-required', {name: 'clientId', other: formControl.errors.requiredIf.other})}}
- } -
- } - -
-
- } -
- -
- @if (settingsForm.get('secret'); as formControl) { - - - {{formControl.value | defaultValue}} - - - - - @if (settingsForm.dirty || !settingsForm.untouched) { -
- @if (formControl.errors && formControl.errors.requiredIf) { -
{{t('other-field-required', {name: 'secret', other: formControl.errors.requiredIf.other})}}
- } -
- } - -
-
- } -
- -
- -
-

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

- -
- @if (settingsForm.get('providerName'); as formControl) { - - - {{formControl.value}} - - - - - - } -
- -
- @if(settingsForm.get('provisionAccounts'); as formControl) { - - -
- -
-
-
- } -
- -
- @if(settingsForm.get('requireVerifiedEmail'); as formControl) { - - -
- -
-
-
- } -
- -
- @if(settingsForm.get('syncUserSettings'); as formControl) { - - -
- -
-
-
- } -
- -
- @if(settingsForm.get('autoLogin'); as formControl) { - - -
- -
-
-
- } -
- -
- @if(settingsForm.get('disablePasswordAuthentication'); as formControl) { - - -
- -
-
-
- } -
- -
- -
-

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

-
{{t('defaults-requirement')}}
- - -
- @if(settingsForm.get('defaultAgeRestriction'); as formControl) { - - -
{{formControl.value | ageRating}}
-
- - - -
- } -
- -
- @if(settingsForm.get('defaultIncludeUnknowns'); as formControl) { - - -
- -
-
-
- } -
- - @if (oidcSettings()) { -
-
- -
- -
- -
+ + + }
- } -
+
+ @if (settingsForm.get('clientId'); as formControl) { + + + {{formControl.value}} + + + -
-

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

-
{{t('advanced-tooltip')}}
+ @if (settingsForm.dirty || !settingsForm.untouched) { +
+ @if (formControl.errors && formControl.errors.requiredIf) { +
{{t('other-field-required', {name: 'clientId', other: formControl.errors.requiredIf.other})}}
+ } +
+ } - -
- @if (settingsForm.get('rolesPrefix'); as formControl) { - - - {{formControl.value}} - - - - - + + + } +
+ +
+ @if (settingsForm.get('secret'); as formControl) { + + + {{formControl.value | defaultValue}} + + + + + @if (settingsForm.dirty || !settingsForm.untouched) { +
+ @if (formControl.errors && formControl.errors.requiredIf) { +
{{t('other-field-required', {name: 'secret', other: formControl.errors.requiredIf.other})}}
+ } +
+ } + +
+
+ } +
+ +
+ +
+

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

+ +
+ @if (settingsForm.get('providerName'); as formControl) { + + + {{formControl.value}} + + + + + + } +
+ +
+ @if(settingsForm.get('provisionAccounts'); as formControl) { + + +
+ +
+
+
+ } +
+ +
+ @if(settingsForm.get('requireVerifiedEmail'); as formControl) { + + +
+ +
+
+
+ } +
+ +
+ @if(settingsForm.get('syncUserSettings'); as formControl) { + + +
+ +
+
+
+ } +
+ +
+ @if(settingsForm.get('autoLogin'); as formControl) { + + +
+ +
+
+
+ } +
+ +
+ @if(settingsForm.get('disablePasswordAuthentication'); as formControl) { + + +
+ +
+
+
+ } +
+ +
+ +
+

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

+
{{t('defaults-requirement')}}
+ + +
+ @if(settingsForm.get('defaultAgeRestriction'); as formControl) { + + +
{{formControl.value | ageRating}}
+
+ + + +
+ } +
+ +
+ @if(settingsForm.get('defaultIncludeUnknowns'); as formControl) { + + +
+ +
+
+
+ } +
+ + @if (oidcSettings()) { +
+
+ +
+ +
+ +
+
} -
-
- @if (settingsForm.get('rolesClaim'); as formControl) { - - - {{formControl.value}} - - - - - - } -
+ -
- @if (settingsForm.get('customScopes'); as formControl) { - - - @let val = breakString(formControl.value); +
+

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

+
{{t('advanced-tooltip')}}
- @for(opt of val; track opt) { - {{opt.trim()}} - } @empty { - {{null | defaultValue}} - } -
+ +
+ @if (settingsForm.get('rolesPrefix'); as formControl) { + + + {{formControl.value | defaultValue}} + + + + + + } +
- - - +
+ @if (settingsForm.get('rolesClaim'); as formControl) { + + + {{formControl.value}} + + + + + + } +
-
+
+ +
- } -
+ - - - + + } diff --git a/UI/Web/src/app/admin/manage-open-idconnect/manage-open-idconnect.component.ts b/UI/Web/src/app/admin/manage-open-idconnect/manage-open-idconnect.component.ts index d404839f3..d50d40455 100644 --- a/UI/Web/src/app/admin/manage-open-idconnect/manage-open-idconnect.component.ts +++ b/UI/Web/src/app/admin/manage-open-idconnect/manage-open-idconnect.component.ts @@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + computed, DestroyRef, - effect, inject, OnInit, signal @@ -15,6 +15,7 @@ import { AsyncValidatorFn, FormControl, FormGroup, + NonNullableFormBuilder, ReactiveFormsModule, ValidationErrors, ValidatorFn @@ -23,22 +24,44 @@ import {SettingsService} from "../settings.service"; import {OidcConfig} from "../_models/oidc-config"; import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component"; -import {debounceTime, distinctUntilChanged, filter, map, of, switchMap, tap} from "rxjs"; +import {debounceTime, distinctUntilChanged, filter, forkJoin, map, of, tap} from "rxjs"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {RestrictionSelectorComponent} from "../../user-settings/restriction-selector/restriction-selector.component"; import {AgeRatingPipe} from "../../_pipes/age-rating.pipe"; import {MetadataService} from "../../_services/metadata.service"; import {AgeRating} from "../../_models/metadata/age-rating"; import {AgeRatingDto} from "../../_models/metadata/age-rating-dto"; -import {allRoles, Role} from "../../_services/account.service"; +import {AccountService, allRoles, Role} from "../../_services/account.service"; import {Library} from "../../_models/library/library"; import {LibraryService} from "../../_services/library.service"; -import {LibrarySelectorComponent} from "../library-selector/library-selector.component"; -import {RoleSelectorComponent} from "../role-selector/role-selector.component"; import {ToastrService} from "ngx-toastr"; import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; -import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component"; +import { + MultiCheckBoxItem, + SettingMultiCheckBox +} from "../../settings/_components/setting-multi-check-box/setting-multi-check-box.component"; +import { + SettingMultiTextFieldComponent +} from "../../settings/_components/setting-multi-text-field/setting-multi-text-field.component"; + +type OidcFormGroup = FormGroup<{ + autoLogin: FormControl; + disablePasswordAuthentication: FormControl; + providerName: FormControl; + authority: FormControl; + clientId: FormControl; + secret: FormControl; + provisionAccounts: FormControl; + requireVerifiedEmail: FormControl; + syncUserSettings: FormControl; + rolesPrefix: FormControl; + rolesClaim: FormControl; + customScopes: FormControl; + defaultRoles: FormControl; + defaultLibraries: FormControl; + defaultAgeRestriction: FormControl; + defaultIncludeUnknowns: FormControl; +}>; @Component({ selector: 'app-manage-open-idconnect', @@ -48,11 +71,10 @@ import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component"; SettingItemComponent, SettingSwitchComponent, AgeRatingPipe, - LibrarySelectorComponent, - RoleSelectorComponent, SafeHtmlPipe, DefaultValuePipe, - TagBadgeComponent + SettingMultiCheckBox, + SettingMultiTextFieldComponent ], templateUrl: './manage-open-idconnect.component.html', styleUrl: './manage-open-idconnect.component.scss', @@ -65,81 +87,88 @@ export class ManageOpenIDConnectComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); private readonly metadataService = inject(MetadataService); private readonly toastr = inject(ToastrService); + private readonly fb = inject(NonNullableFormBuilder); + private readonly accountService = inject(AccountService); + private readonly libraryService = inject(LibraryService); serverSettings!: ServerSettings; - settingsForm: FormGroup = new FormGroup({}); + settingsForm!: OidcFormGroup; + loading = signal(true); oidcSettings = signal(undefined); ageRatings = signal([]); - selectedLibraries = signal([]); - selectedRoles = signal([]); + libraries = signal([]); + libraryOptions = computed(() => this.libraries().map(l => { + return { label: l.name, value: l.id }; + })); + roles = signal(allRoles); + roleOptions: MultiCheckBoxItem[] = allRoles.map(r => { + return { label: r, value: r, disableFunc: (r, selected) => { + return r !== Role.Admin && selected.includes(Role.Admin); + }} + }); ngOnInit(): void { - this.metadataService.getAllAgeRatings().subscribe(ratings => { - this.ageRatings.set(ratings); - }); + forkJoin([ + this.metadataService.getAllAgeRatings(), + this.settingsService.getServerSettings(), + this.libraryService.getLibraries(), + ]).subscribe(([ageRatings, settings, libraries]) => { + this.ageRatings.set(ageRatings); + this.libraries.set(libraries); - this.settingsService.getServerSettings().subscribe({ - next: data => { - this.serverSettings = data; - this.oidcSettings.set(this.serverSettings.oidcConfig); - this.selectedRoles.set(this.serverSettings.oidcConfig.defaultRoles); - this.selectedLibraries.set(this.serverSettings.oidcConfig.defaultLibraries); + this.serverSettings = settings; + this.oidcSettings.set(this.serverSettings.oidcConfig); - this.settingsForm.addControl('authority', new FormControl(this.serverSettings.oidcConfig.authority, [], [this.authorityValidator()])); - this.settingsForm.addControl('clientId', new FormControl(this.serverSettings.oidcConfig.clientId, [this.requiredIf('authority')])); - this.settingsForm.addControl('secret', new FormControl(this.serverSettings.oidcConfig.secret, [this.requiredIf('authority')])); - this.settingsForm.addControl('provisionAccounts', new FormControl(this.serverSettings.oidcConfig.provisionAccounts, [])); - this.settingsForm.addControl('requireVerifiedEmail', new FormControl(this.serverSettings.oidcConfig.requireVerifiedEmail, [])); - this.settingsForm.addControl('syncUserSettings', new FormControl(this.serverSettings.oidcConfig.syncUserSettings, [])); - this.settingsForm.addControl('rolesPrefix', new FormControl(this.serverSettings.oidcConfig.rolesPrefix, [])); - this.settingsForm.addControl('rolesClaim', new FormControl(this.serverSettings.oidcConfig.rolesClaim, [])); - this.settingsForm.addControl('autoLogin', new FormControl(this.serverSettings.oidcConfig.autoLogin, [])); - this.settingsForm.addControl('disablePasswordAuthentication', new FormControl(this.serverSettings.oidcConfig.disablePasswordAuthentication, [])); - this.settingsForm.addControl('providerName', new FormControl(this.serverSettings.oidcConfig.providerName, [])); - this.settingsForm.addControl("defaultAgeRestriction", new FormControl(this.serverSettings.oidcConfig.defaultAgeRestriction, [])); - this.settingsForm.addControl('defaultIncludeUnknowns', new FormControl(this.serverSettings.oidcConfig.defaultIncludeUnknowns, [])); - this.settingsForm.addControl('customScopes', new FormControl(this.serverSettings.oidcConfig.customScopes.join(","), [])) - this.cdRef.markForCheck(); + this.settingsForm = this.fb.group({ + authority: this.fb.control(this.serverSettings.oidcConfig.authority, { asyncValidators: [this.authorityValidator()] }), + clientId: this.fb.control(this.serverSettings.oidcConfig.clientId, { validators: [this.requiredIf('authority')] }), + secret: this.fb.control(this.serverSettings.oidcConfig.secret, { validators: [this.requiredIf('authority')] }), + provisionAccounts: this.fb.control(this.serverSettings.oidcConfig.provisionAccounts), + requireVerifiedEmail: this.fb.control(this.serverSettings.oidcConfig.requireVerifiedEmail), + syncUserSettings: this.fb.control(this.serverSettings.oidcConfig.syncUserSettings), + rolesPrefix: this.fb.control(this.serverSettings.oidcConfig.rolesPrefix), + rolesClaim: this.fb.control(this.serverSettings.oidcConfig.rolesClaim), + autoLogin: this.fb.control(this.serverSettings.oidcConfig.autoLogin), + disablePasswordAuthentication: this.fb.control(this.serverSettings.oidcConfig.disablePasswordAuthentication), + providerName: this.fb.control(this.serverSettings.oidcConfig.providerName), + defaultLibraries: this.fb.control(this.serverSettings.oidcConfig.defaultLibraries), + defaultRoles: this.fb.control(this.serverSettings.oidcConfig.defaultRoles), + defaultAgeRestriction: this.fb.control(this.serverSettings.oidcConfig.defaultAgeRestriction), + defaultIncludeUnknowns: this.fb.control(this.serverSettings.oidcConfig.defaultIncludeUnknowns), + customScopes: this.fb.control(this.serverSettings.oidcConfig.customScopes) + }); - this.settingsForm.valueChanges.pipe( - debounceTime(300), - distinctUntilChanged(), - takeUntilDestroyed(this.destroyRef), - filter(() => { - // Do not auto save when provider settings have changed - const settings: OidcConfig = this.settingsForm.getRawValue(); - return settings.authority == this.oidcSettings()?.authority && settings.clientId == this.oidcSettings()?.clientId; - }), - tap(() => this.save()) - ).subscribe(); - } - }); + this.loading.set(false); + this.cdRef.markForCheck(); + + this.settingsForm.valueChanges.pipe( + debounceTime(300), + distinctUntilChanged(), + takeUntilDestroyed(this.destroyRef), + filter(() => { + // Do not auto save when provider settings have changed + const settings: OidcConfig = this.packData().oidcConfig; + return settings.authority == this.oidcSettings()?.authority && settings.clientId == this.oidcSettings()?.clientId; + }), + tap(() => this.save()) + ).subscribe(); + }) } - updateRoles(roles: string[]) { - this.selectedRoles.set(roles); - this.save(); - } - - updateLibraries(libraries: Library[]) { - this.selectedLibraries.set(libraries.map(l => l.id)); - this.save(); + private packData(): ServerSettings { + const newSettings = Object.assign({}, this.serverSettings); + newSettings.oidcConfig = { + ...this.settingsForm.getRawValue(), + enabled: false, + }; + return newSettings; } save(showConfirmation: boolean = false) { - if (!this.settingsForm.valid || !this.serverSettings || !this.oidcSettings) return; - - const data = this.settingsForm.getRawValue(); - const newSettings = Object.assign({}, this.serverSettings); - newSettings.oidcConfig = data as OidcConfig; - newSettings.oidcConfig.defaultAgeRestriction = parseInt(newSettings.oidcConfig.defaultAgeRestriction + '', 10) as AgeRating; - newSettings.oidcConfig.defaultRoles = this.selectedRoles(); - newSettings.oidcConfig.defaultLibraries = this.selectedLibraries(); - newSettings.oidcConfig.customScopes = (data.customScopes as string) - .split(',').map((item: string) => item.trim()) - .filter((scope: string) => scope.length > 0); + if (!this.settingsForm.valid || !this.serverSettings || !this.oidcSettings()) return; + const newSettings = this.packData(); this.settingsService.updateServerSettings(newSettings).subscribe({ next: data => { this.serverSettings = data; @@ -157,14 +186,6 @@ export class ManageOpenIDConnectComponent implements OnInit { }) } - breakString(s: string) { - if (s) { - return s.split(','); - } - - return []; - } - authorityValidator(): AsyncValidatorFn { return (control: AbstractControl) => { let uri: string = control.value; @@ -188,6 +209,8 @@ export class ManageOpenIDConnectComponent implements OnInit { requiredIf(other: string): ValidatorFn { return (control): ValidationErrors | null => { + if (!this.settingsForm) return null; + const otherControl = this.settingsForm.get(other); if (!otherControl) return null; diff --git a/UI/Web/src/app/admin/role-selector/role-selector.component.html b/UI/Web/src/app/admin/role-selector/role-selector.component.html deleted file mode 100644 index 7d3eca0c2..000000000 --- a/UI/Web/src/app/admin/role-selector/role-selector.component.html +++ /dev/null @@ -1,28 +0,0 @@ - -
-
-

{{t('title')}}

-
-
- @if(selectedRoles.length > 0) { - - - - - } -
-
- -
    - @for(role of selectedRoles; track role; let i = $index) { -
  • -
    - - -
    -
  • - } -
-
diff --git a/UI/Web/src/app/admin/role-selector/role-selector.component.scss b/UI/Web/src/app/admin/role-selector/role-selector.component.scss deleted file mode 100644 index 3f2adc8d1..000000000 --- a/UI/Web/src/app/admin/role-selector/role-selector.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.list-group-item { - border: none; -} \ No newline at end of file diff --git a/UI/Web/src/app/admin/role-selector/role-selector.component.ts b/UI/Web/src/app/admin/role-selector/role-selector.component.ts deleted file mode 100644 index ab9bbb6f5..000000000 --- a/UI/Web/src/app/admin/role-selector/role-selector.component.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - inject, input, - Input, - OnInit, - Output -} from '@angular/core'; -import {Member} from 'src/app/_models/auth/member'; -import {User} from 'src/app/_models/user'; -import {AccountService} from 'src/app/_services/account.service'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {TranslocoDirective,} from "@jsverse/transloco"; -import {SelectionModel} from "../../typeahead/_models/selection-model"; -import {RoleLocalizedPipe} from "../../_pipes/role-localized.pipe"; - -@Component({ - selector: 'app-role-selector', - templateUrl: './role-selector.component.html', - styleUrls: ['./role-selector.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ReactiveFormsModule, FormsModule, TranslocoDirective, RoleLocalizedPipe] -}) -export class RoleSelectorComponent implements OnInit { - - private readonly accountService = inject(AccountService); - private readonly cdRef = inject(ChangeDetectorRef); - - - /** - * This must have roles - */ - @Input() member: Member | undefined | User; - preSelectedRoles = input([]); - /** - * Allows the selection of Admin role - */ - @Input() allowAdmin: boolean = false; - @Output() selected: EventEmitter = new EventEmitter(); - - allRoles: string[] = []; - selectedRoles: Array<{selected: boolean, disabled: boolean, data: string}> = []; - selections!: SelectionModel; - selectAll: boolean = false; - - get hasSomeSelected() { - return this.selections != null && this.selections.hasSomeSelected(); - } - - ngOnInit(): void { - this.accountService.getRoles().subscribe(roles => { - const bannedRoles = ['Pleb']; - if (!this.allowAdmin) { - bannedRoles.push('Admin'); - } - roles = roles.filter(item => !bannedRoles.includes(item)); - this.allRoles = roles; - this.selections = new SelectionModel(false, this.allRoles); - - this.selectedRoles = roles.map(item => { - return {selected: false, disabled: false, data: item}; - }); - - this.cdRef.markForCheck(); - this.preselect(); - - this.selected.emit(this.selectedRoles.filter(item => item.selected).map(item => item.data)); - }); - } - - preselect() { - if (this.member !== undefined) { - this.member.roles.forEach(role => { - const foundRole = this.selectedRoles.filter(item => item.data === role); - if (foundRole.length > 0) { - foundRole[0].selected = true; - } - }); - } else if (this.preSelectedRoles().length > 0) { - this.preSelectedRoles().forEach((role) => { - const foundRole = this.selectedRoles.filter(item => item.data === role); - if (foundRole.length > 0) { - foundRole[0].selected = true; - } - }); - } else { - // For new users, preselect LoginRole - this.selectedRoles.forEach(role => { - if (role.data == 'Login') { - role.selected = true; - } - }); - } - this.syncSelections(); - this.cdRef.markForCheck(); - } - - handleModelUpdate() { - const roles = this.selectedRoles.filter(item => item.selected).map(item => item.data); - if (roles.filter(r => r === 'Admin').length > 0) { - // Disable all other items as Admin is selected - this.selectedRoles.filter(item => item.data !== 'Admin').forEach(e => { - e.disabled = true; - }); - } else { - // Re-enable everything - this.selectedRoles.forEach(e => { - e.disabled = false; - }); - } - this.syncSelections(); - this.cdRef.markForCheck(); - this.selected.emit(roles); - } - - syncSelections() { - this.selectedRoles.forEach(s => this.selections.toggle(s.data, s.selected)); - this.cdRef.markForCheck(); - } - - toggleAll() { - this.selectAll = !this.selectAll; - - // Update selectedRoles considering disabled state - this.selectedRoles.filter(r => !r.disabled).forEach(r => r.selected = this.selectAll); - - // Sync selections with updated selectedRoles - this.syncSelections(); - - this.selected.emit(this.selections.selected()); - this.cdRef.markForCheck(); - } - -} diff --git a/UI/Web/src/app/all-annotations/all-annotations.component.html b/UI/Web/src/app/all-annotations/all-annotations.component.html index b2ffc4b20..7f405c360 100644 --- a/UI/Web/src/app/all-annotations/all-annotations.component.html +++ b/UI/Web/src/app/all-annotations/all-annotations.component.html @@ -41,6 +41,8 @@ [showPageLink]="false" [openInIncognitoMode]="true" [showSelectionBox]="true" + [showLocationInformation]="true" + [listedToUpdates]="true" [selected]="bulkSelectionService.isCardSelected('annotations', position)" (selection)="bulkSelectionService.handleCardSelection('annotations', position, annotations().length, $event)" /> diff --git a/UI/Web/src/app/all-annotations/all-annotations.component.ts b/UI/Web/src/app/all-annotations/all-annotations.component.ts index b06bda5e3..83204ec3e 100644 --- a/UI/Web/src/app/all-annotations/all-annotations.component.ts +++ b/UI/Web/src/app/all-annotations/all-annotations.component.ts @@ -37,6 +37,7 @@ import {Action, ActionFactoryService, ActionItem} from "../_services/action-fact import {BulkOperationsComponent} from "../cards/bulk-operations/bulk-operations.component"; import {BulkSelectionService} from "../cards/bulk-selection.service"; import {User} from "../_models/user"; +import {AccountService} from "../_services/account.service"; @Component({ selector: 'app-all-annotations', @@ -62,6 +63,7 @@ export class AllAnnotationsComponent implements OnInit { private readonly metadataService = inject(MetadataService); private readonly actionFactoryService = inject(ActionFactoryService); public readonly bulkSelectionService = inject(BulkSelectionService); + private readonly accountService = inject(AccountService); isLoading = signal(true); annotations = signal([]); @@ -118,6 +120,7 @@ export class AllAnnotationsComponent implements OnInit { } handleAction = async (action: ActionItem, entity: Annotation) => { + const userId = this.accountService.currentUserSignal()!.id; const selectedIndices = this.bulkSelectionService.getSelectedCardsForSource('annotations'); const selectedAnnotations = this.annotations().filter((_, idx) => selectedIndices.includes(idx+'')); const ids = selectedAnnotations.map(a => a.id); @@ -142,9 +145,36 @@ export class AllAnnotationsComponent implements OnInit { case Action.Export: this.annotationsService.exportAnnotations(ids).subscribe(); break + case Action.Like: + this.annotationsService.likeAnnotations(ids).pipe( + tap(() => this.updateLikes(ids, userId, true)), + ).subscribe(); + break; + case Action.UnLike: + this.annotationsService.unLikeAnnotations(ids).pipe( + tap(() => this.updateLikes(ids, userId, false)), + ).subscribe(); } } + private updateLikes(ids: number[], userId: number, like: boolean): void { + this.annotations.update(annotations => + annotations.map(annotation => { + if (!ids.includes(annotation.id)) return annotation; + + let likes; + if (like) { + likes = annotation.likes.includes(userId) ? annotation.likes : [...annotation.likes, userId]; + } else { + likes = annotation.likes.filter(id => id !== userId); + } + + return { ...annotation, likes }; + }) + ); + } + + exportFilter() { const filter = this.filter(); if (!filter) return; diff --git a/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.html b/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.html index 180dc0921..2b74ccb4c 100644 --- a/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.html +++ b/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.html @@ -1,7 +1,11 @@
+
+ @if (showLocationInformation()) { + + } {{ annotation().ownerUsername }}
{{ annotation().createdUtc | utcToLocaleDate | date: 'shortDate' }}
@@ -53,18 +57,28 @@ }
- @if(annotation().containsSpoiler) { -
+
+ @if(annotation().containsSpoiler) { +
{{t('contains-spoilers-label')}} -
- } +
+ } + + + + @if (showSelectionBox()) { + + } + +
- @if (showSelectionBox()) { - - }
diff --git a/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.ts b/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.ts index c27d8a69a..7ebe216a0 100644 --- a/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.ts +++ b/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.ts @@ -2,6 +2,8 @@ import { ChangeDetectionStrategy, Component, computed, + DestroyRef, + effect, EventEmitter, inject, input, @@ -21,6 +23,11 @@ import {DefaultValuePipe} from "../../../../_pipes/default-value.pipe"; import {SlotColorPipe} from "../../../../_pipes/slot-color.pipe"; import {ColorscapeService} from "../../../../_services/colorscape.service"; import {ActivatedRoute, Router, RouterLink} from "@angular/router"; +import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; +import {AccountService} from "../../../../_services/account.service"; +import {EVENTS, MessageHubService} from "../../../../_services/message-hub.service"; +import {AnnotationUpdateEvent} from "../../../../_models/events/annotation-update-event"; +import {AnnotationLikesComponent} from "../annotation-likes/annotation-likes.component"; @Component({ selector: 'app-annotation-card', @@ -32,7 +39,9 @@ import {ActivatedRoute, Router, RouterLink} from "@angular/router"; DefaultValuePipe, NgStyle, RouterLink, - NgClass + NgClass, + NgbTooltip, + AnnotationLikesComponent ], templateUrl: './annotation-card.component.html', styleUrl: './annotation-card.component.scss', @@ -47,6 +56,9 @@ export class AnnotationCardComponent { private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); private readonly highlightSlotPipe = new SlotColorPipe(); + protected readonly accountService = inject(AccountService); + private readonly messageHub = inject(MessageHubService); + private readonly destroyRef = inject(DestroyRef); annotation = model.required(); allowEdit = input(true); @@ -59,13 +71,31 @@ export class AnnotationCardComponent { * Redirects to the reader with annotation in view */ showInReaderLink = input(false); + /** + * Disable a selection checkbox. Fires selection when called + */ showSelectionBox = input(false); + /** + * Displays series and library name + */ + showLocationInformation = input(false); + /** + * Disable a like button + */ + showLikes = input(true); openInIncognitoMode = input(false); isInReader = input(true); + /** + * If enabled, listens to annotation updates + */ + listedToUpdates = input(false); selected = input(false); @Output() delete = new EventEmitter(); @Output() navigate = new EventEmitter(); + /** + * Fire when the checkbox is pressed, with the last known state (inverse of checked state) + */ @Output() selection = new EventEmitter(); titleColor: Signal; @@ -73,16 +103,16 @@ export class AnnotationCardComponent { constructor() { - // TODO: Validate if I want this -- aka update content on a detail page when receiving update from backend - // this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => { - // if (message.payload !== EVENTS.AnnotationUpdate) return; - // const updatedAnnotation = message.payload as AnnotationUpdateEvent; - // if (this.annotation()?.id !== updatedAnnotation.annotation.id) return; - // - // console.log('Refreshing annotation from backend: ', updatedAnnotation.annotation); - // this.annotation.set(updatedAnnotation.annotation); - // }); + effect(() => { + const enabled = this.listedToUpdates(); + const event = this.messageHub.messageSignal(); + if (!enabled || event?.event !== EVENTS.AnnotationUpdate) return; + const newAnnotation = (event.payload as AnnotationUpdateEvent).annotation; + if (this.annotation().id != newAnnotation.id) return; + + this.annotation.set(newAnnotation); + }); this.titleColor = computed(() => { const annotation = this.annotation(); @@ -134,6 +164,5 @@ export class AnnotationCardComponent { this.annotationService.delete(annotation.id).subscribe(_ => { this.delete.emit(); }); - } } diff --git a/UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.html b/UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.html new file mode 100644 index 000000000..d7253e4bc --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.html @@ -0,0 +1,13 @@ + + @if (visible()) { + + } + diff --git a/UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.scss b/UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.ts b/UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.ts new file mode 100644 index 000000000..e25d556e3 --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/_annotations/annotation-likes/annotation-likes.component.ts @@ -0,0 +1,60 @@ +import {ChangeDetectionStrategy, Component, computed, inject, input, model} from '@angular/core'; +import {Annotation} from "../../../_models/annotations/annotation"; +import {AnnotationService} from "../../../../_services/annotation.service"; +import {AccountService} from "../../../../_services/account.service"; +import {tap} from "rxjs/operators"; +import {TranslocoDirective} from "@jsverse/transloco"; + +@Component({ + selector: 'app-annotation-likes', + imports: [ + TranslocoDirective + ], + templateUrl: './annotation-likes.component.html', + styleUrl: './annotation-likes.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AnnotationLikesComponent { + + private readonly annotationService = inject(AnnotationService); + protected readonly accountService = inject(AccountService) + + /** + * If the element should be shown + */ + visible = input.required(); + + /** + * The annotation for which the likes are shown. Will emit a annotationChange when the likes update + */ + annotation = model.required(); + + liked = computed(() => this.annotation().likes.includes(this.accountService.userId() ?? 0)); + + handleLikeChange() { + const userId = this.accountService.userId(); + if (!userId) return; + + if (this.annotation().ownerUserId ===userId) return; + + const sub$ = this.liked() + ? this.annotationService.unLikeAnnotations([this.annotation().id]) + : this.annotationService.likeAnnotations([this.annotation().id]); + + sub$.pipe( + tap(() => { + this.annotation.update(x => { + const newLikes = this.liked() + ? x.likes.filter(id => id !== userId) + : [...x.likes, userId]; + + return { + ...x, + likes: newLikes, + } + }) + }) + ).subscribe(); + } + +} diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.html b/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.html index a2d72d967..3a05cfbd5 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.html +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.html @@ -30,7 +30,13 @@ } @for(annotation of annotations() | filter: filterList; track annotation.comment + annotation.highlightColor + annotation.containsSpolier) { - + } @empty {

{{t('no-data')}}

diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.ts b/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.ts index 35cb540d8..396f190c4 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.ts +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.ts @@ -10,6 +10,7 @@ import { OffCanvasResizeComponent, ResizeMode } from "../../../../shared/_components/off-canvas-resize/off-canvas-resize.component"; +import {AccountService} from "../../../../_services/account.service"; @Component({ selector: 'app-view-annotations-drawer', @@ -28,6 +29,7 @@ export class ViewAnnotationsDrawerComponent { private readonly activeOffcanvas = inject(NgbActiveOffcanvas); private readonly annotationService = inject(AnnotationService); + protected readonly accountService = inject(AccountService); @Output() loadAnnotation: EventEmitter = new EventEmitter(); diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.html b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.html index 090abb454..d064d9505 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.html +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.html @@ -55,6 +55,13 @@ @case (AnnotationMode.View) { @let an = annotation(); @if (an) { + + +
{{ an.ownerUsername }} diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.scss b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.scss index 4573511c7..eb96a89e7 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.scss +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.scss @@ -45,9 +45,4 @@ $green-color: rgba(34, 197, 94); background-color: $green-color; } -::ng-deep .ql-editor { - height: calc(var(--drawer-height) - 278px); - overflow: auto; -} - diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.ts b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.ts index 39e563f62..0e3c5af0b 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.ts +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.ts @@ -17,7 +17,7 @@ import {FormControl, FormGroup, NonNullableFormBuilder, ReactiveFormsModule} fro import {Annotation} from "../../../_models/annotations/annotation"; import {translate, TranslocoDirective} from "@jsverse/transloco"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {debounceTime, switchMap} from "rxjs/operators"; +import {debounceTime, switchMap, tap} from "rxjs/operators"; import {of} from "rxjs"; import {HighlightBarComponent} from "../../_annotations/highlight-bar/highlight-bar.component"; import {SlotColorPipe} from "../../../../_pipes/slot-color.pipe"; @@ -37,6 +37,7 @@ import { ResizeMode } from "../../../../shared/_components/off-canvas-resize/off-canvas-resize.component"; import {ConfirmService} from "../../../../shared/confirm.service"; +import {AnnotationLikesComponent} from "../../_annotations/annotation-likes/annotation-likes.component"; export enum AnnotationMode { View = 0, @@ -59,7 +60,8 @@ const INIT_HIGHLIGHT_DELAY = 200; QuillViewComponent, DatePipe, UtcToLocaleDatePipe, - OffCanvasResizeComponent + OffCanvasResizeComponent, + AnnotationLikesComponent ], templateUrl: './view-edit-annotation-drawer.component.html', styleUrl: './view-edit-annotation-drawer.component.scss', @@ -91,7 +93,6 @@ export class ViewEditAnnotationDrawerComponent implements OnInit { titleColor: Signal; totalText!: Signal; - formGroup!: FormGroup<{ note: FormControl, hasSpoiler: FormControl, @@ -269,12 +270,8 @@ export class ViewEditAnnotationDrawerComponent implements OnInit { const annotation = this.annotation(); if (annotation) { - console.log('view-edit drawer, slot index changed: ', slotIndex, 'comment: ', this.annotation()?.comment, 'form comment: ', this.formGroup.get('note')?.value); this.annotation.set({...annotation, selectedSlotIndex: slotIndex}); this.formGroup.get('selectedSlotIndex')?.setValue(slotIndex); - - // Patch back in any text in the quill editor - console.log('(2) view-edit drawer, slot index changed: ', slotIndex, 'comment: ', this.annotation()?.comment, 'form comment: ', this.formGroup.get('note')?.value); } } diff --git a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts index 92522c087..4eb02a73e 100644 --- a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts @@ -14,7 +14,6 @@ import { } from '@angular/core'; import {fromEvent, merge, of} from "rxjs"; import {catchError, debounceTime, tap} from "rxjs/operators"; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; import {ReaderService} from "../../../_services/reader.service"; import {ToastrService} from "ngx-toastr"; @@ -22,6 +21,8 @@ import {translate, TranslocoDirective} from "@jsverse/transloco"; import {KEY_CODES} from "../../../shared/_services/utility.service"; import {EpubReaderMenuService} from "../../../_services/epub-reader-menu.service"; import {Annotation} from "../../_models/annotations/annotation"; +import {isMobileChromium} from "../../../_helpers/browser"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; enum BookLineOverlayMode { None = 0, @@ -78,73 +79,134 @@ export class BookLineOverlayComponent implements OnInit { ngOnInit() { - if (this.parent) { + // Check for Pointer Events API support + const hasPointerEvents = 'PointerEvent' in window; - const mouseUp$ = fromEvent(this.parent.nativeElement, 'mouseup'); - const touchEnd$ = fromEvent(this.parent.nativeElement, 'touchend'); - - merge(mouseUp$, touchEnd$) - .pipe( - takeUntilDestroyed(this.destroyRef), - debounceTime(20), // Need extra time for this extension to inject DOM https://github.com/Kareadita/Kavita/issues/3521 - tap((event: MouseEvent | TouchEvent) => this.handleEvent(event)) - ).subscribe(); + // Some mobile Chromium browsers do not send touchend events reliably: https://github.com/Kareadita/Kavita/issues/4072 + if (hasPointerEvents && !isMobileChromium()) { + // Use pointer events for modern browsers (except problematic mobile Chromium) + this.setupPointerEventListener(); + } else { + // Fallback to mouse/touch events + this.setupLegacyEventListeners(); } } - handleEvent(event: MouseEvent | TouchEvent) { - const selection = window.getSelection(); + private setupPointerEventListener(): void { + if (!this.parent) return; + + fromEvent(this.parent.nativeElement, 'pointerup') + .pipe( + takeUntilDestroyed(this.destroyRef), + debounceTime(20), + tap((event: PointerEvent) => this.handlePointerEvent(event)) + ).subscribe(); + } + + private setupLegacyEventListeners(): void { + if (!this.parent) return; + + const mouseUp$ = fromEvent(this.parent.nativeElement, 'mouseup'); + const touchEnd$ = fromEvent(this.parent.nativeElement, 'touchend'); + + // Additional events for mobile Chromium workaround + const additionalEvents$ = isMobileChromium() ? [ + fromEvent(this.parent.nativeElement, 'touchcancel'), + fromEvent(this.parent.nativeElement, 'pointerup') + ] : []; + + merge(mouseUp$, touchEnd$, ...additionalEvents$) + .pipe( + takeUntilDestroyed(this.destroyRef), + debounceTime(20), + tap((event: MouseEvent | TouchEvent | PointerEvent) => this.handleLegacyEvent(event)) + ).subscribe(); + } + + private handlePointerEvent(event: PointerEvent): void { + // Filter out pen/stylus events if you don't want to handle them + if (event.pointerType === 'pen') { + return; + } + + // Check for right-click + const isRightClick = event.button === 2; + + this.processSelectionEvent(event, isRightClick); + } + + private handleLegacyEvent(event: MouseEvent | TouchEvent | PointerEvent): void { + // Determine if it's a right-click (only applicable to mouse events) + const isRightClick = event instanceof MouseEvent && event.button === 2; + + this.processSelectionEvent(event, isRightClick); + } + + private processSelectionEvent(event: Event, isRightClick: boolean): void { if (!event.target) return; + const selection = window.getSelection(); - // NOTE: This doesn't account for a partial occlusion with an annotation + // Check if target has annotation class this.hasSelectedAnnotation.set((event.target as HTMLElement).classList.contains('epub-highlight')); - if ((selection === null || selection === undefined || selection.toString().trim() === '' - || selection.toString().trim() === this.selectedText) || this.hasSelectedAnnotation()) { + if (this.shouldSkipSelection(selection, isRightClick)) { if (this.selectedText !== '') { event.preventDefault(); event.stopPropagation(); } - const isRightClick = (event instanceof MouseEvent && event.button === 2); if (!isRightClick) { this.reset(); } - return; } + // Process valid selection this.selectedText = selection ? selection.toString().trim() : ''; - - if (this.selectedText.length > 0 && this.mode === BookLineOverlayMode.None) { - - // Get the range from the selection - const range = selection.getRangeAt(0); - - // Get start and end containers - const startContainer = this.getElementContainer(range.startContainer); - const endContainer = this.getElementContainer(range.endContainer); - - // Generate XPaths for both start and end - this.startXPath = this.readerService.getXPathTo(startContainer); - this.endXPath = this.readerService.getXPathTo(endContainer); - - // Protect from DOM Shift by removing the UI part and making this scoped to true epub html - this.startXPath = this.readerService.descopeBookReaderXpath(this.startXPath); - this.endXPath = this.readerService.descopeBookReaderXpath(this.endXPath); - - // Get the context window for generating a blurb in annotation flow - this.allTextFromSelection = (event.target as Element).textContent || ''; + if (this.selectedText.length > 0 && this.mode === BookLineOverlayMode.None && selection !== null) { + this.captureSelectionContext(selection, event.target as Element); this.isOpen.emit(true); event.preventDefault(); event.stopPropagation(); } + this.cdRef.markForCheck(); } + private shouldSkipSelection(selection: Selection | null, isRightClick: boolean): boolean { + return (selection === null || + selection === undefined || + selection.toString().trim() === '' || + selection.toString().trim() === this.selectedText) || + this.hasSelectedAnnotation(); + } + + /** + * Captures XPath and context for the current selection + */ + private captureSelectionContext(selection: Selection, targetElement: Element): void { + const range = selection.getRangeAt(0); + + // Get start and end containers + const startContainer = this.getElementContainer(range.startContainer); + const endContainer = this.getElementContainer(range.endContainer); + + // Generate XPaths for both start and end + this.startXPath = this.readerService.getXPathTo(startContainer); + this.endXPath = this.readerService.getXPathTo(endContainer); + + // Protect from DOM Shift by removing the UI part and making this scoped to true epub html + this.startXPath = this.readerService.descopeBookReaderXpath(this.startXPath); + this.endXPath = this.readerService.descopeBookReaderXpath(this.endXPath); + + // Get the context window for generating a blurb in annotation flow + this.allTextFromSelection = targetElement.textContent || ''; + } + + switchMode(mode: BookLineOverlayMode) { this.mode = mode; this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/book-reader/_models/annotations/annotation.ts b/UI/Web/src/app/book-reader/_models/annotations/annotation.ts index 2e8bcda87..939d8695b 100644 --- a/UI/Web/src/app/book-reader/_models/annotations/annotation.ts +++ b/UI/Web/src/app/book-reader/_models/annotations/annotation.ts @@ -12,6 +12,7 @@ export interface Annotation { selectedSlotIndex: number; chapterTitle: string | null; highlightCount: number; + likes: number[]; ownerUserId: number; ownerUsername: string; createdUtc: string; @@ -25,4 +26,7 @@ export interface Annotation { volumeId: number; seriesId: number; + seriesName: string; + libraryName: string; + } diff --git a/UI/Web/src/app/cards/bulk-selection.service.ts b/UI/Web/src/app/cards/bulk-selection.service.ts index 6d1c1ecbe..0520bff8d 100644 --- a/UI/Web/src/app/cards/bulk-selection.service.ts +++ b/UI/Web/src/app/cards/bulk-selection.service.ts @@ -74,6 +74,7 @@ export class BulkSelectionService { } this.prevIndex = index; this.prevDataSource = dataSource; + this.debugLog("Setting max for " + dataSource + " to " + maxIndex); this.dataSourceMax[dataSource] = maxIndex; this.actionsSource.next(this.getActions(() => {})); } diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index 3767cf96a..d8ffa55b1 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -265,7 +265,9 @@ export class CardDetailLayoutComponent + [mangaFormat]="series.format" + [totalBytes]="size" + /> @@ -166,9 +168,7 @@ [genres]="chapter.genres" [tags]="chapter.tags" [webLinks]="weblinks" - [readingTime]="chapter" - [language]="chapter.language" - [format]="series.format" /> + /> } diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.ts b/UI/Web/src/app/chapter-detail/chapter-detail.component.ts index c79d05cc6..ea9926a81 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.ts +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.ts @@ -177,6 +177,7 @@ export class ChapterDetailComponent implements OnInit { rating: number = 0; ratings: Array = []; hasBeenRated: boolean = false; + size: number = 0; annotations = model([]); weblinks: Array = []; @@ -263,6 +264,7 @@ export class ChapterDetailComponent implements OnInit { this.series = results.series; this.chapter = results.chapter; + this.size = this.chapter.files.reduce((sum, f) => sum + f.bytes, 0); this.weblinks = this.chapter.webLinks.split(','); this.libraryType = results.libraryType; this.userReviews = results.chapterDetail.reviews.filter(r => !r.isExternal); diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts index 8a43a99a8..f9c85a198 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts @@ -15,18 +15,7 @@ import { } from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {FilterStatement} from '../../../_models/metadata/v2/filter-statement'; -import { - BehaviorSubject, - distinctUntilChanged, - filter, - map, - Observable, - of, - pipe, - startWith, - switchMap, - tap -} from 'rxjs'; +import {BehaviorSubject, distinctUntilChanged, filter, map, Observable, of, startWith, switchMap, tap} from 'rxjs'; import {MetadataService} from 'src/app/_services/metadata.service'; import {FilterComparison} from 'src/app/_models/metadata/v2/filter-comparison'; import {FilterField} from 'src/app/_models/metadata/v2/filter-field'; @@ -35,7 +24,7 @@ import {FilterComparisonPipe} from "../../../_pipes/filter-comparison.pipe"; import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop"; import {Select2, Select2Option} from "ng-select2-component"; import {NgbDate, NgbDateParserFormatter, NgbInputDatepicker, NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; -import {TranslocoDirective, TranslocoService} from "@jsverse/transloco"; +import {TranslocoDirective} from "@jsverse/transloco"; import {ValidFilterEntity} from "../../filter-settings"; import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service"; import {AnnotationsFilterField} from "../../../_models/metadata/v2/annotations-filter"; @@ -132,7 +121,6 @@ export class MetadataFilterRowComponent @defer (when activeTabId === TabID.Details; prefetch on idle) { + /> } diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html index b19702592..6bdad5ecb 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html +++ b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html @@ -17,21 +17,36 @@ @if ((libraryType === LibraryType.Book || libraryType === LibraryType.LightNovel) && mangaFormat !== MangaFormat.PDF) { - {{t('words-count', {num: readingTimeEntity.wordCount | compactNumber})}} + {{t('words-count', {num: readingTimeEntity.wordCount | compactNumber})}} } @else { - {{t('pages-count', {num: readingTimeEntity.pages | compactNumber})}} + {{t('pages-count', {num: readingTimeEntity.pages | compactNumber})}} } @if (hasReadingProgress && readingTimeLeft && readingTimeLeft.avgHours !== 0) { - + {{readingTimeLeft | readTimeLeft }} } @else { - + {{readingTimeEntity | readTime }} } + + @if (releaseYear) { + + + {{releaseYear}} + + } + + @if (totalBytes) { + + + {{totalBytes | bytes}} + + } + diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.scss b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.scss index 42eb06de9..443dfe745 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.scss +++ b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.scss @@ -11,10 +11,6 @@ } } -.time-left{ - font-size: 0.8rem; -} - -.word-count { - font-size: 0.8rem; +.small-text { + font-size: 0.8rem; } diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts index 13e24c3c8..6987f77ce 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts +++ b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts @@ -18,20 +18,22 @@ import {FilterField} from "../../../_models/metadata/v2/filter-field"; import {MangaFormat} from "../../../_models/manga-format"; import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component"; import {PublisherFlipperComponent} from "../../../_single-module/publisher-flipper/publisher-flipper.component"; +import {BytesPipe} from "../../../_pipes/bytes.pipe"; @Component({ selector: 'app-metadata-detail-row', - imports: [ - AgeRatingImageComponent, - CompactNumberPipe, - ReadTimeLeftPipe, - ReadTimePipe, - NgbTooltip, - TranslocoDirective, - ImageComponent, - SeriesFormatComponent, - PublisherFlipperComponent - ], + imports: [ + AgeRatingImageComponent, + CompactNumberPipe, + ReadTimeLeftPipe, + ReadTimePipe, + NgbTooltip, + TranslocoDirective, + ImageComponent, + SeriesFormatComponent, + PublisherFlipperComponent, + BytesPipe + ], templateUrl: './metadata-detail-row.component.html', styleUrl: './metadata-detail-row.component.scss', changeDetection: ChangeDetectionStrategy.OnPush @@ -51,6 +53,8 @@ export class MetadataDetailRowComponent { @Input({required: true}) ageRating: AgeRating = AgeRating.Unknown; @Input({required: true}) libraryType!: LibraryType; @Input({required: true}) mangaFormat!: MangaFormat; + @Input() releaseYear: number | undefined; + @Input() totalBytes: number | undefined; openGeneric(queryParamName: FilterField, filter: string | number) { if (queryParamName === FilterField.None) return; 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 745215a18..ebbc2819c 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 @@ -15,7 +15,7 @@ @if(isLoadingExtra || isLoading) {
- loading... + {{t('loading')}}
}
@@ -33,7 +33,10 @@ [hasReadingProgress]="hasReadingProgress" [readingTimeEntity]="series" [libraryType]="libraryType" - [mangaFormat]="series.format" /> + [mangaFormat]="series.format" + [releaseYear]="seriesMetadata.releaseYear" + [totalBytes]="totalSize()" + />
@for(item of scroll.viewPortItems; let idx = $index; track item) { @if (item.isChapter) { - + } @else { - + } } @@ -254,7 +257,7 @@
@for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + '_specials') { - + }
@@ -340,10 +343,8 @@ [genres]="seriesMetadata.genres" [tags]="seriesMetadata.tags" [webLinks]="WebLinks" - [readingTime]="series" - [releaseYear]="seriesMetadata.releaseYear" - [language]="seriesMetadata.language" - [format]="series.format" /> + [filePaths]="[series.folderPath]" + /> } diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 141ce3131..93cfab260 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -9,7 +9,7 @@ import { ElementRef, inject, model, - OnInit, + OnInit, signal, ViewChild } from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; @@ -240,6 +240,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { isWantToRead: boolean = false; unreadCount: number = 0; totalCount: number = 0; + totalSize = signal(undefined); readingTimeLeft: HourEstimateRange | null = null; /** * Poster image for the Series @@ -865,6 +866,10 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { this.volumes = detail.volumes; this.storyChapters = detail.storylineChapters; + this.totalSize.set(detail.volumes.reduce((sum, v) => sum + v.chapters.reduce((volumeSum, c) => { + return volumeSum + c.files.reduce((chapterSum, f) => chapterSum + f.bytes , 0) + }, 0), 0)); + this.storylineItems = []; const v = this.volumes.map(v => { return {volume: v, chapter: undefined, isChapter: false} as StoryLineItem; diff --git a/UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.html b/UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.html new file mode 100644 index 000000000..59dfc1fdb --- /dev/null +++ b/UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.html @@ -0,0 +1,46 @@ + +
+
+

{{title()}}

+ {{tooltip()}} +
+ + @if (!isLoading() && options().length > 0) { + + + + + } + +
+ + + +
+
    + @for (opt of options(); track opt.value; let index = $index) { +
  • +
    + + + @if (opt.colour) { + @let c = opt.colour; + + } +
    +
  • + } @empty { +
  • + {{t('no-data')}} +
  • + } +
+
+ +
diff --git a/UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.scss b/UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.scss new file mode 100644 index 000000000..5bc630155 --- /dev/null +++ b/UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.scss @@ -0,0 +1,8 @@ +.list-group-item { + border: none; +} + +.text-muted { + font-size: 14px; +} + diff --git a/UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.ts b/UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.ts new file mode 100644 index 000000000..34adf5ac1 --- /dev/null +++ b/UI/Web/src/app/settings/_components/setting-multi-check-box/setting-multi-check-box.component.ts @@ -0,0 +1,159 @@ +import { + ChangeDetectionStrategy, + Component, + computed, effect, + forwardRef, + input, model, + signal +} from '@angular/core'; +import {RgbaColor} from "../../../book-reader/_models/annotations/highlight-slot"; +import {ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule} from "@angular/forms"; +import {TranslocoDirective} from "@jsverse/transloco"; +import {LoadingComponent} from "../../../shared/loading/loading.component"; +import {NgStyle} from "@angular/common"; + +/** + * An item to display in the SettingMultiCheckBox + */ +export interface MultiCheckBoxItem { + /** + * Label to display in the list + */ + label: string, + /** + * Value passed to the FormControl + */ + value: T, + /** + * Appends a dot after the label + */ + colour?: RgbaColor, + /** + * If the items checkbox should be disabled. Does not overwrite global disable + * @param value + * @param selected + */ + disableFunc?: (value: T, selected: T[]) => boolean, +} + +/** + * The SettingMultiCheckBox should be used when wanting to display all options, of which any may be selected at once. + * The component should have a formControlName bound to it of type FormControl. + * + * An example can be found in ManageUserPreferencesComponent + */ +@Component({ + selector: 'app-setting-multi-check-box', + imports: [ + TranslocoDirective, + LoadingComponent, + ReactiveFormsModule, + NgStyle + ], + standalone: true, + templateUrl: './setting-multi-check-box.component.html', + styleUrl: './setting-multi-check-box.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SettingMultiCheckBox), + multi: true, + } + ] +}) +export class SettingMultiCheckBox implements ControlValueAccessor { + + /** + * Title to display above the checkboxes + */ + title = input.required(); + /** + * Tooltip to display muted underneath the title + * @optional + */ + tooltip = input(''); + /** + * Loading indicator for the checkbox list + * @optional + */ + loading = input(undefined); + /** + * All possible options + */ + options = input.required[]>(); + /** + * Disable all checkboxes + */ + disabled = model(false); + + isLoading = computed(() => { + const loading = this.loading(); + return loading !== undefined && loading; + }); + allSelected = computed(() => this.options().length === this.selectedValues().length); + + selectedValues = signal([]); + + private _onChange: (value: T[]) => void = () => {}; + private _onTouched: () => void = () => {}; + + constructor() { + // Auto propagate changes to the FormGroup + effect(() => { + const selectedValues = this.selectedValues(); + this._onChange(selectedValues); + this._onTouched(); + }); + } + + writeValue(obj: T[]): void { + this.selectedValues.set(obj || []); + } + + registerOnChange(fn: (_: T[]) => void): void { + this._onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this._onTouched = fn; + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled.set(isDisabled); + } + + isChecked(item: MultiCheckBoxItem) { + return this.selectedValues().includes(item.value); + } + + isDisabled(item: MultiCheckBoxItem) { + const disabled = this.disabled(); + const selected = this.selectedValues(); + + if (disabled) { + return true; + } + + return item.disableFunc && item.disableFunc(item.value, selected); + } + + onCheckboxChange(item: MultiCheckBoxItem, event: Event) { + const checked = (event.target as HTMLInputElement).checked; + + if (checked) { + this.selectedValues.update(x => [...x, item.value]); + } else { + this.selectedValues.update(x => x.filter(t => t !== item.value)); + } + } + + toggleAll() { + if (this.allSelected()) { + this.selectedValues.set([]); + } else { + this.selectedValues.set(this.options().map(opt => opt.value)); + } + } + +} diff --git a/UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.html b/UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.html new file mode 100644 index 000000000..076e24215 --- /dev/null +++ b/UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.html @@ -0,0 +1,19 @@ + + + + @for(opt of selectedValues(); track opt) { + {{opt}} + } @empty { + {{null | defaultValue}} + } + + + + + + + + diff --git a/UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.scss b/UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.ts b/UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.ts new file mode 100644 index 000000000..efb93f1a8 --- /dev/null +++ b/UI/Web/src/app/settings/_components/setting-multi-text-field/setting-multi-text-field.component.ts @@ -0,0 +1,117 @@ +import {ChangeDetectionStrategy, Component, computed, effect, forwardRef, input, signal} from '@angular/core'; +import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule} from "@angular/forms"; +import {DefaultValuePipe} from "../../../_pipes/default-value.pipe"; +import {SettingItemComponent} from "../setting-item/setting-item.component"; +import {TagBadgeComponent} from "../../../shared/tag-badge/tag-badge.component"; + +/** + * SettingMultiTextFieldComponent should be used when using a text area to input several comma seperated values. + * The component should have a formControlName bound to it of type FormControl. + * By default, T is assumed to be a string + * + * An example can be found in ManageOpenIDConnectComponent + */ +@Component({ + selector: 'app-setting-multi-text-field', + imports: [ + DefaultValuePipe, + FormsModule, + ReactiveFormsModule, + SettingItemComponent, + TagBadgeComponent + ], + templateUrl: './setting-multi-text-field.component.html', + styleUrl: './setting-multi-text-field.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SettingMultiTextFieldComponent), + multi: true, + } + ] +}) +export class SettingMultiTextFieldComponent implements ControlValueAccessor { + /** + * Convertor, required if your type is not a string + * @default trimmed string value + */ + valueConvertor = input<(s: string) => T>((t: string) => t.trim() as T); + /** + * String to value convertor, required if your type is not a string + * @default the value as string + */ + stringConvertor = input<(t: T) => string>((t: T) => (t as string)); + /** + * Filter, required if your type is not a string + * @default non empty strings + */ + valueFilter = input<(t: T) => boolean>((t: T) => (t as string).length > 0); + /** + * Title to display + */ + title = input.required(); + /** + * Tooltip to display + * @optional + */ + tooltip = input(''); + /** + * Loading indicator for the checkbox list + * @optional + */ + loading = input(undefined); + /** + * id for the textarea input + * @optional + */ + id = input(''); + + isLoading = computed(() => { + const loading = this.loading(); + return loading !== undefined && loading; + }); + textFieldValue = computed(() => this.selectedValues().map(this.stringConvertor()).join(',')) + selectedValues = signal([]); + disabled = signal(false); + + textFieldValueTracker = ''; + + private _onChange: (value: T[]) => void = () => {}; + private _onTouched: () => void = () => {}; + + constructor() { + // Auto propagate changes to the FormGroup + effect(() => { + const selectedValues = this.selectedValues(); + this._onChange(selectedValues); + this._onTouched(); + }); + } + + writeValue(obj: T[]): void { + this.selectedValues.set(obj || []); + this.textFieldValueTracker = obj.map(this.stringConvertor()).join(','); + } + + registerOnChange(fn: (_: T[]) => void): void { + this._onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this._onTouched = fn; + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled.set(isDisabled); + } + + onTextFieldChange(event: Event) { + const input = (event.target as HTMLTextAreaElement).value; + this.selectedValues.set(input + .split(',') + .map(this.valueConvertor()) + .filter(this.valueFilter()) + ); + } +} diff --git a/UI/Web/src/app/shared/_services/filter-utilities.service.ts b/UI/Web/src/app/shared/_services/filter-utilities.service.ts index b736c3ab8..8041e365e 100644 --- a/UI/Web/src/app/shared/_services/filter-utilities.service.ts +++ b/UI/Web/src/app/shared/_services/filter-utilities.service.ts @@ -181,7 +181,7 @@ export class FilterUtilitiesService { case "annotation": return [ AnnotationsFilterField.Owner, AnnotationsFilterField.Library, - AnnotationsFilterField.HighlightSlots, + AnnotationsFilterField.HighlightSlots, AnnotationsFilterField.Series ] as T[]; case 'series': return [ diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index 538d986c5..48545931e 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -242,9 +242,9 @@ export class UtilityService { return params; } - createPaginatedResult(response: any, paginatedVariable: PaginatedResult | undefined = undefined) { + createPaginatedResult(response: any, paginatedVariable: PaginatedResult | undefined = undefined) { if (paginatedVariable === undefined) { - paginatedVariable = new PaginatedResult(); + paginatedVariable = new PaginatedResult(); } if (response.body === null) { paginatedVariable.result = []; diff --git a/UI/Web/src/app/user-settings/manga-user-preferences/manage-user-preferences.component.html b/UI/Web/src/app/user-settings/manga-user-preferences/manage-user-preferences.component.html index 295e0462e..57c3b7581 100644 --- a/UI/Web/src/app/user-settings/manga-user-preferences/manage-user-preferences.component.html +++ b/UI/Web/src/app/user-settings/manga-user-preferences/manage-user-preferences.component.html @@ -4,7 +4,7 @@

@if (user) { -
+

{{t('global-settings-title')}}

@@ -104,18 +104,90 @@

{{t('social-settings-title')}}

-
- - -
- -
-
-
-
- + +
+ + +
+ +
+
+
+
+ +
+ + +
+ +
+
+
+
+ +
+ + +
+ +
+
+
+
+ +
+ @if (settingsForm.get('socialPreferences')!.get('socialLibraries'); as control) { + + + + + {{lib.name}} + + + {{lib.name}} + + + + + } +
+ +
+ @if (settingsForm.get('socialPreferences')!.get('socialMaxAgeRating'); as control) { + + + {{control.value | ageRating}} + + + + + + } +
+ +
+ + +
+ +
+
+
+
+
diff --git a/UI/Web/src/app/user-settings/manga-user-preferences/manage-user-preferences.component.ts b/UI/Web/src/app/user-settings/manga-user-preferences/manage-user-preferences.component.ts index 90e10425f..446e78d23 100644 --- a/UI/Web/src/app/user-settings/manga-user-preferences/manage-user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/manga-user-preferences/manage-user-preferences.component.ts @@ -1,22 +1,65 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + computed, + DestroyRef, effect, + inject, + OnInit, + signal +} from '@angular/core'; import {TranslocoDirective} from "@jsverse/transloco"; import {Preferences} from "../../_models/preferences/preferences"; import {AccountService} from "../../_services/account.service"; -import {BookService} from "../../book-reader/_services/book.service"; import {Title} from "@angular/platform-browser"; import {Router} from "@angular/router"; import {LocalizationService} from "../../_services/localization.service"; -import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; +import {FormArray, FormControl, FormGroup, NonNullableFormBuilder, ReactiveFormsModule} from "@angular/forms"; import {User} from "../../_models/user"; import {KavitaLocale} from "../../_models/metadata/language"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {debounceTime, distinctUntilChanged, filter, forkJoin, switchMap, tap} from "rxjs"; +import {debounceTime, distinctUntilChanged, filter, forkJoin, of, switchMap, tap} from "rxjs"; import {take} from "rxjs/operators"; import {AsyncPipe, DecimalPipe, TitleCasePipe} from "@angular/common"; import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component"; import {LicenseService} from "../../_services/license.service"; import {HighlightBarComponent} from "../../book-reader/_components/_annotations/highlight-bar/highlight-bar.component"; +import {SiteTheme} from "../../_models/preferences/site-theme"; +import {PageLayoutMode} from "../../_models/page-layout-mode"; +import {HighlightSlot} from "../../book-reader/_models/annotations/highlight-slot"; +import {AgeRating} from "../../_models/metadata/age-rating"; +import {LibraryService} from "../../_services/library.service"; +import {Library} from "../../_models/library/library"; +import {MetadataService} from "../../_services/metadata.service"; +import {AgeRatingDto} from "../../_models/metadata/age-rating-dto"; +import {AgeRatingPipe} from "../../_pipes/age-rating.pipe"; +import {TypeaheadComponent} from "../../typeahead/_components/typeahead.component"; +import {TypeaheadSettings} from "../../typeahead/_models/typeahead-settings"; + +type UserPreferencesForm = FormGroup<{ + theme: FormControl, + globalPageLayoutMode: FormControl, + blurUnreadSummaries: FormControl, + promptForDownloadSize: FormControl, + noTransitions: FormControl, + collapseSeriesRelationships: FormControl, + locale: FormControl, + bookReaderHighlightSlots: FormArray>, + colorScapeEnabled: FormControl, + + aniListScrobblingEnabled: FormControl, + wantToReadSync: FormControl, + + socialPreferences: FormGroup<{ + shareReviews: FormControl, + shareAnnotations: FormControl, + viewOtherAnnotations: FormControl, + socialLibraries: FormControl, + socialMaxAgeRating: FormControl, + socialIncludeUnknowns: FormControl, + }>, +}> @Component({ selector: 'app-manga-user-preferences', @@ -28,7 +71,9 @@ import {HighlightBarComponent} from "../../book-reader/_components/_annotations/ SettingSwitchComponent, AsyncPipe, DecimalPipe, - HighlightBarComponent + HighlightBarComponent, + AgeRatingPipe, + TypeaheadComponent, ], templateUrl: './manage-user-preferences.component.html', styleUrl: './manage-user-preferences.component.scss', @@ -38,18 +83,28 @@ export class ManageUserPreferencesComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); private readonly accountService = inject(AccountService); - private readonly bookService = inject(BookService); private readonly titleService = inject(Title); private readonly router = inject(Router); private readonly cdRef = inject(ChangeDetectorRef); private readonly localizationService = inject(LocalizationService); protected readonly licenseService = inject(LicenseService); + private readonly libraryService = inject(LibraryService); + private readonly fb = inject(NonNullableFormBuilder); + private readonly metadataService = inject(MetadataService); + loading = signal(true); + ageRatings = signal([]); + libraries = signal([]); + libraryOptions = computed(() => this.libraries().map(l => { + return { label: l.name, value: l.id }; + })); + locales: Array = []; - settingsForm: FormGroup = new FormGroup({}); + settingsForm!: UserPreferencesForm; user: User | undefined = undefined; + libraryTypeAheadSettings = signal(new TypeaheadSettings()); get Locale() { if (!this.settingsForm.get('locale')) return 'English'; @@ -75,33 +130,54 @@ export class ManageUserPreferencesComponent implements OnInit { ngOnInit(): void { this.titleService.setTitle('Kavita - User Preferences'); + this.cdRef.markForCheck(); forkJoin({ user: this.accountService.currentUser$.pipe(take(1)), - pref: this.accountService.getPreferences() - }).subscribe(results => { - if (results.user === undefined) { + pref: this.accountService.getPreferences(), + libraries: this.libraryService.getLibraries(), + ageRatings: this.metadataService.getAllAgeRatings(), + }).subscribe(({user, pref, libraries, ageRatings}) => { + if (user === undefined) { this.router.navigateByUrl('/login'); return; } - this.user = results.user; - this.user.preferences = results.pref; + this.user = user; + this.user.preferences = pref; - this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, [])); - this.settingsForm.addControl('globalPageLayoutMode', new FormControl(this.user.preferences.globalPageLayoutMode, [])); - this.settingsForm.addControl('blurUnreadSummaries', new FormControl(this.user.preferences.blurUnreadSummaries, [])); - this.settingsForm.addControl('promptForDownloadSize', new FormControl(this.user.preferences.promptForDownloadSize, [])); - this.settingsForm.addControl('noTransitions', new FormControl(this.user.preferences.noTransitions, [])); - this.settingsForm.addControl('collapseSeriesRelationships', new FormControl(this.user.preferences.collapseSeriesRelationships, [])); - this.settingsForm.addControl('shareReviews', new FormControl(this.user.preferences.shareReviews, [])); - this.settingsForm.addControl('locale', new FormControl(this.user.preferences.locale || 'en', [])); - this.settingsForm.addControl('colorScapeEnabled', new FormControl(this.user.preferences.colorScapeEnabled ?? true, [])); + this.loading.set(false); + this.libraries.set(libraries); + this.ageRatings.set([{ + value: AgeRating.NotApplicable, + title: '', + }, ...ageRatings]); - this.settingsForm.addControl('aniListScrobblingEnabled', new FormControl(this.user.preferences.aniListScrobblingEnabled || false, [])); - this.settingsForm.addControl('wantToReadSync', new FormControl(this.user.preferences.wantToReadSync || false, [])); - this.settingsForm.addControl('bookReaderHighlightSlots', new FormControl(this.user.preferences.bookReaderHighlightSlots, [])); + this.setupLibraryTypeAheadSettings(); + this.settingsForm = this.fb.group({ + theme: this.fb.control(pref.theme), + globalPageLayoutMode: this.fb.control(pref.globalPageLayoutMode), + blurUnreadSummaries: this.fb.control(pref.blurUnreadSummaries), + promptForDownloadSize: this.fb.control(pref.promptForDownloadSize), + noTransitions: this.fb.control(pref.noTransitions), + collapseSeriesRelationships: this.fb.control(pref.collapseSeriesRelationships), + locale: this.fb.control(pref.locale || 'en'), + bookReaderHighlightSlots: this.fb.array(pref.bookReaderHighlightSlots.map(s => this.fb.control(s))), + colorScapeEnabled: this.fb.control(pref.colorScapeEnabled), + + aniListScrobblingEnabled: this.fb.control(pref.aniListScrobblingEnabled), + wantToReadSync: this.fb.control(pref.wantToReadSync), + + socialPreferences: this.fb.group({ + shareReviews: this.fb.control(pref.socialPreferences.shareReviews), + shareAnnotations: this.fb.control(pref.socialPreferences.shareAnnotations), + viewOtherAnnotations: this.fb.control(pref.socialPreferences.viewOtherAnnotations), + socialLibraries: this.fb.control(pref.socialPreferences.socialLibraries), + socialMaxAgeRating: this.fb.control(pref.socialPreferences.socialMaxAgeRating), + socialIncludeUnknowns: this.fb.control(pref.socialPreferences.socialIncludeUnknowns), + }), + }); // Automatically save settings as we edit them this.settingsForm.valueChanges.pipe( @@ -123,50 +199,31 @@ export class ManageUserPreferencesComponent implements OnInit { this.cdRef.markForCheck(); }); - - this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(mode => { - if (mode) { - this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true); - this.cdRef.markForCheck(); - } - }); - this.cdRef.markForCheck(); } - reset() { - if (!this.user) return; + private setupLibraryTypeAheadSettings() { + const libs = this.libraries(); + const selectedLibs = this.user!.preferences.socialPreferences.socialLibraries; - this.settingsForm.get('theme')?.setValue(this.user.preferences.theme, {onlySelf: true, emitEvent: false}); - this.settingsForm.get('globalPageLayoutMode')?.setValue(this.user.preferences.globalPageLayoutMode, {onlySelf: true, emitEvent: false}); - this.settingsForm.get('blurUnreadSummaries')?.setValue(this.user.preferences.blurUnreadSummaries, {onlySelf: true, emitEvent: false}); - this.settingsForm.get('promptForDownloadSize')?.setValue(this.user.preferences.promptForDownloadSize, {onlySelf: true, emitEvent: false}); - this.settingsForm.get('noTransitions')?.setValue(this.user.preferences.noTransitions, {onlySelf: true, emitEvent: false}); - this.settingsForm.get('collapseSeriesRelationships')?.setValue(this.user.preferences.collapseSeriesRelationships, {onlySelf: true, emitEvent: false}); - this.settingsForm.get('shareReviews')?.setValue(this.user.preferences.shareReviews, {onlySelf: true, emitEvent: false}); - this.settingsForm.get('locale')?.setValue(this.user.preferences.locale || 'en', {onlySelf: true, emitEvent: false}); - this.settingsForm.get('colorScapeEnabled')?.setValue(this.user.preferences.colorScapeEnabled ?? true, {onlySelf: true, emitEvent: false}); + const settings = new TypeaheadSettings(); + settings.multiple = true; + settings.minCharacters = 0; + settings.savedData = libs.filter(l => selectedLibs.includes(l.id)); + settings.compareFn = (libs, filter) => libs.filter(l => l.name.toLowerCase().includes(filter.toLowerCase())); + settings.trackByIdentityFn = (idx, l) => `${l.id}`; + settings.fetchFn = (filter) => of(settings.compareFn(libs, filter)); - this.settingsForm.get('aniListScrobblingEnabled')?.setValue(this.user.preferences.aniListScrobblingEnabled || false, {onlySelf: true, emitEvent: false}); - this.settingsForm.get('wantToReadSync')?.setValue(this.user.preferences.wantToReadSync || false, {onlySelf: true, emitEvent: false}); - this.settingsForm.get('bookReaderHighlightSlots')?.setValue(this.user.preferences.bookReaderHighlightSlots, {onlySelf: true, emitEvent: false}); + this.libraryTypeAheadSettings.set(settings); + } + + syncFormWithTypeahead(libs: Library[] | Library) { + this.settingsForm + .get('socialPreferences')! + .get('socialLibraries')! + .setValue((libs as Library[]).map(l => l.id)); } packSettings(): Preferences { - const modelSettings = this.settingsForm.value; - - return { - theme: modelSettings.theme, - globalPageLayoutMode: parseInt(modelSettings.globalPageLayoutMode, 10), - blurUnreadSummaries: modelSettings.blurUnreadSummaries, - promptForDownloadSize: modelSettings.promptForDownloadSize, - noTransitions: modelSettings.noTransitions, - collapseSeriesRelationships: modelSettings.collapseSeriesRelationships, - shareReviews: modelSettings.shareReviews, - locale: modelSettings.locale || 'en', - aniListScrobblingEnabled: modelSettings.aniListScrobblingEnabled, - wantToReadSync: modelSettings.wantToReadSync, - bookReaderHighlightSlots: modelSettings.bookReaderHighlightSlots, - colorScapeEnabled: modelSettings.colorScapeEnabled, - }; + return this.settingsForm.getRawValue(); } } diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.html b/UI/Web/src/app/volume-detail/volume-detail.component.html index fa82a1f92..e1232313a 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.html +++ b/UI/Web/src/app/volume-detail/volume-detail.component.html @@ -26,7 +26,9 @@ [hasReadingProgress]="volume.pagesRead > 0" [readingTimeEntity]="volume" [libraryType]="libraryType" - [mangaFormat]="series.format" /> + [mangaFormat]="series.format" + [totalBytes]="size" + /> @if (libraryType !== null && series && volume.chapters.length === 1) {
@@ -226,9 +228,7 @@ + /> } diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.ts b/UI/Web/src/app/volume-detail/volume-detail.component.ts index a932e008c..69f54ec71 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.ts +++ b/UI/Web/src/app/volume-detail/volume-detail.component.ts @@ -216,6 +216,7 @@ export class VolumeDetailComponent implements OnInit { plusReviews: Array = []; rating: number = 0; hasBeenRated: boolean = false; + size: number = 0; annotations = model([]); mobileSeriesImgBackground: string | undefined; @@ -407,6 +408,8 @@ export class VolumeDetailComponent implements OnInit { this.series = results.series; this.volume = results.volume; + this.size = this.volume.chapters.reduce((sum, c) => + sum + c.files.reduce((fileSum, f) => fileSum + f.bytes, 0), 0); this.libraryType = results.libraryType; if (this.volume.chapters.length === 1) { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 4b85bffd1..317c48587 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -54,6 +54,8 @@ "default-age-restriction-label": "Age rating", "default-age-restriction-tooltip": "Maximum age rating shown to new users", "no-restriction": "{{restriction-selector.no-restriction}}", + "default-roles-label": "{{common.roles}}", + "default-libraries-label": "{{common.libraries}}", "advanced-title": "Advanced settings", "advanced-tooltip": "Only modify these options if you understand their impact.", @@ -95,7 +97,9 @@ "out-of-sync": "This user was created via OIDC. If the SyncUsers setting is enabled, any changes made to this account may be overwritten.", "oidc-managed": "This user is managed via OIDC. Please contact your OIDC administrator to request any changes.", "identity-provider": "Identity provider", - "identity-provider-tooltip": "Kavita users are never synced with OIDC accounts." + "identity-provider-tooltip": "Kavita users are never synced with OIDC accounts.", + "libraries-label": "{{common.libraries}}", + "roles-label": "{{common.roles}}" }, "user-scrobble-history": { @@ -202,8 +206,6 @@ "disable-animations-tooltip": "Turns off animations in the site. Useful for e-ink readers.", "collapse-series-relationships-label": "Collapse Series Relationships", "collapse-series-relationships-tooltip": "Should Kavita show Series that have no relationships or is the parent/prequel", - "share-series-reviews-label": "Share Series Reviews", - "share-series-reviews-tooltip": "Allow your reviews to be visible to other users on this server", "highlight-bar-label": "Annotations highlight colors", "highlight-bar-tooltip": "These colors are shared between all books", "colorscape-label": "Use ColorScape", @@ -215,6 +217,19 @@ "want-to-read-sync-label": "Want To Read Sync", "want-to-read-sync-tooltip": "Allow Kavita to add items to your Want to Read list based on AniList and MAL series in Pending read list", + "share-series-reviews-label": "Share Series Reviews", + "share-series-reviews-tooltip": "Allow your reviews to be visible to other users on this server", + "share-annotations-label": "Share Annotations", + "share-annotations-tooltip": "Allow your annotations to be visible to other users on this server", + "view-other-annotations-label": "View Shared Annotations", + "view-other-annotations-tooltip": "View other users' annotations while reading", + "social-libraries-label": "Social Libraries", + "social-libraries-tooltip": "Libraries on which social features should be enabled. No selection enables social features everywhere. Disable specific features if you wish to use them nowhere.", + "social-max-age-rating-label": "Max age rating", + "social-max-age-rating-tooltip": "Max age rating on which social features will work", + "social-include-unknowns-label": "Include unknowns", + "social-include-unknowns-tooltip": "Enable social features for series and chapters with an unknown age rating", + "clients-opds-alert": "OPDS is not enabled on this server. This will not affect Mihon users.", "clients-opds-description": "All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.", "clients-api-key-tooltip": "The API key is like a password. Resetting it will invalidate any existing clients.", @@ -226,6 +241,11 @@ "save": "{{common.save}}" }, + "multi-check-box-form": { + "select-all": "Select all", + "deselect-all": "Deselect all" + }, + "user-holds": { "title": "Scrobble Holds", "description": "This is a user-managed list of Series that will not be scrobbled to upstream providers. You can remove a series at any time and the next scrobble-able event (reading progress, rating, want to read status) will trigger scrobbling.", @@ -471,7 +491,7 @@ }, "role-selector": { - "title": "Roles", + "title": "{{common.roles}}", "deselect-all": "{{common.deselect-all}}", "select-all": "{{common.select-all}}" }, @@ -748,11 +768,13 @@ "inviting": "Inviting…", "cancel": "{{common.cancel}}", "email-not-sent": "{{toasts.email-not-sent}}", - "notice": "{{manage-settings.notice}}" + "notice": "{{manage-settings.notice}}", + "libraries-label": "{{common.libraries}}", + "roles-label": "{{common.roles}}" }, "library-selector": { - "title": "Libraries", + "title": "{{common.libraries}}", "select-all": "{{common.select-all}}", "deselect-all": "{{common.deselect-all}}", "no-data": "There are no libraries setup yet." @@ -907,7 +929,7 @@ "field-mapping-description": "Setup rules for certain strings found in Genre/Tag field and map it to a new string in Genre/Tag and optionally remove it from the Source list. Matching is normalized and case-insensitive, the first matching value is used. Only applicable when Genre/Tag are enabled to be written.", "first-last-name-label": "First Last Naming", "first-last-name-tooltip": "Ensure People's names are written First then Last", - "person-roles-label": "Roles", + "person-roles-label": "{{common.roles}}", "overrides-label": "Overrides", "overrides-description": "Allow Kavita to write over locked fields.", "chapter-header": "Chapter Fields", @@ -931,7 +953,10 @@ "annotation-card": { "page-num": "{{book-reader.page-num-label}}", "contains-spoilers-label": "Spoilers", - "view-in-reader-label": "View in Reader" + "view-in-reader-label": "View in Reader", + "location-tooltip": "{{library}} - {{series}}", + "liked": "Liked by {{amount}} people, including you", + "not-liked": "Liked by {{amount}} people, you did not like it" }, "annotations-tab": { @@ -1143,7 +1168,8 @@ "publication-status-title": "Publication", "publication-status-tooltip": "Publication Status", "on": "{{reader-settings.on}}", - "off": "{{reader-settings.off}}" + "off": "{{reader-settings.off}}", + "loading": "{{common.loading}}" }, "match-series-modal": { @@ -1252,7 +1278,7 @@ "cover-image-description": "{{edit-series-modal.cover-image-description}}", "issue-count": "{{common.issue-count}}", "series-count": "{{common.series-count}}", - "roles-label": "Roles", + "roles-label": "{{common.roles}}", "sort-label": "Sort", "name-label": "Name", "issue-count-label": "Issue Count", @@ -1289,7 +1315,7 @@ "individual-role-title": "As a {{role}}", "browse-person-title": "All Works of {{name}}", "browse-person-by-role-title": "All Works of {{name}} as a {{role}}", - "all-roles": "Roles", + "all-roles": "{{common.roles}}", "anilist-url": "{{edit-person-modal.anilist-tooltip}}", "no-info": "No information about this Person" }, @@ -1462,7 +1488,8 @@ "release-title": "{{sort-field-pipe.release-year}}", "format-title": "{{metadata-filter.format-label}}", "length-title": "{{edit-chapter-modal.words-label}}", - "age-rating-title": "{{metadata-fields.age-rating-title}}" + "age-rating-title": "{{metadata-fields.age-rating-title}}", + "file-path-title": "Folder path" }, "related-tab": { @@ -1598,7 +1625,7 @@ }, "manage-library": { - "title": "Libraries", + "title": "{{common.libraries}}", "add-library": "Add Library", "no-data": "There are no libraries. Try creating one.", "loading": "{{common.loading}}", @@ -1836,7 +1863,7 @@ "resend": "Resend", "setup": "Setup", "last-active-header": "Last Active", - "roles-header": "Roles", + "roles-header": "{{common.roles}}", "name-header": "Name", "none": "None", "never": "Never", @@ -1908,7 +1935,7 @@ "admin-general": "General", "admin-oidc": "OpenID Connect", "admin-users": "Users", - "admin-libraries": "Libraries", + "admin-libraries": "{{common.libraries}}", "admin-media": "Media", "admin-media-issues": "Media Issues", "admin-logs": "Logs", @@ -2052,7 +2079,7 @@ "annotations": "{{tabs.annotations-tab}}", "genres": "{{metadata-fields.genres-title}}", "bookmarks": "{{side-nav.bookmarks}}", - "libraries": "Libraries", + "libraries": "{{common.libraries}}", "reading-lists": "Reading Lists", "collections": "Collections", "close": "{{common.close}}", @@ -2298,7 +2325,7 @@ "limit-label": "Limit", "format-label": "Format", - "libraries-label": "Libraries", + "libraries-label": "{{common.libraries}}", "collections-label": "Collections", "genres-label": "{{metadata-fields.genres-title}}", "tags-label": "{{metadata-fields.tags-title}}", @@ -2813,7 +2840,7 @@ "team": "Team", "location": "Location", "languages": "Languages", - "libraries": "Libraries", + "libraries": "{{common.libraries}}", "letterer": "Letterer", "publication-status": "Publication Status", "penciller": "Penciller", @@ -2909,7 +2936,8 @@ "annotation-spoiler": "Spoiler", "annotation-highlights": "Highlight color", "annotation-comment": "Annotation Text", - "annotation-selection": "Highlight Text" + "annotation-selection": "Highlight Text", + "series": "{{tabs.series-tab}}" }, @@ -3155,7 +3183,9 @@ "rename": "Rename", "rename-tooltip": "Rename the Smart Filter", "merge": "Merge", - "export": "Export" + "export": "Export", + "like": "Like", + "unlike": "Remove likes" }, "preferences": { @@ -3373,7 +3403,9 @@ "book-nums": "Books", "issue-nums": "Issues", "chapter-nums": "Chapters", - "volume-nums": "Volumes" + "volume-nums": "Volumes", + "roles": "Roles", + "libraries": "Libraries" } }