From bd19b282d57981f7621d787c9670ca27d17a7820 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Fri, 10 Mar 2023 19:09:38 -0600 Subject: [PATCH] Misc Fixes + Enhancements (#1875) * Moved Collapse Series with relationships into a user preference rather than library setting. * Fixed bookmarks not converting to webp after initial save * Fixed a bug where when merging we'd print out a duplicate series error when we shouldn't have * Fixed a bug where clicking on a genre or tag from server stats wouldn't load all-series page in a filtered state. * Implemented the ability to have Login role and thus disable accounts. * Ensure first time flow gets the Login role * Refactored user management screen so that pending users can be edited or deleted before the end user accepts the invite. A side effect is old legacy users that were here before email was required can now be deleted. * Show a progress bar under the main series image on larger viewports to show whole series progress. * Removed code no longer needed * Cleanup tags, people, collections without connections after editing series metadata. * Moved the Entity Builders to the main project --- API.Tests/Entities/ComicInfoTests.cs | 1 - .../Extensions/QueryableExtensionsTests.cs | 52 +- API.Tests/Extensions/SeriesExtensionsTests.cs | 2 +- API.Tests/Helpers/ParserInfoHelperTests.cs | 2 +- API.Tests/Repository/SeriesRepositoryTests.cs | 2 +- API.Tests/Services/ArchiveServiceTests.cs | 11 + API.Tests/Services/BookmarkServiceTests.cs | 2 +- API.Tests/Services/CleanupServiceTests.cs | 2 +- API.Tests/Services/ReaderServiceTests.cs | 2 +- API.Tests/Services/ReadingListServiceTests.cs | 2 +- API.Tests/Services/ScannerServiceTests.cs | 2 +- API.Tests/Services/SeriesServiceTests.cs | 2 +- API.Tests/Services/TachiyomiServiceTests.cs | 2 +- .../ArchiveService/ComicInfos/Umlaut.zip | Bin 0 -> 1754 bytes API/API.csproj | 15 + API/Constants/PolicyConstants.cs | 6 +- API/Controllers/AccountController.cs | 3 + API/Controllers/LibraryController.cs | 1 - API/Controllers/MetadataController.cs | 5 + API/Controllers/UsersController.cs | 17 +- API/DTOs/Account/AgeRestrictionDto.cs | 4 +- API/DTOs/Account/UpdateUserDto.cs | 2 +- API/DTOs/MemberDto.cs | 4 + API/DTOs/UpdateLibraryDto.cs | 3 - API/DTOs/UserPreferencesDto.cs | 7 +- API/Data/MigrateLoginRole.cs | 36 + ...0_MoveCollapseSeriesToUserPref.Designer.cs | 1858 +++++++++++++++++ ...0310142630_MoveCollapseSeriesToUserPref.cs | 29 + .../Migrations/DataContextModelSnapshot.cs | 12 +- API/Data/Repositories/SeriesRepository.cs | 6 +- API/Data/Repositories/UserRepository.cs | 48 +- API/Entities/AppUser.cs | 1 + API/Entities/AppUserPreferences.cs | 4 + API/Entities/Library.cs | 4 - API/Entities/Person.cs | 6 +- .../Helpers/Builders/ChapterBuilder.cs | 5 +- .../Helpers/Builders/EntityBuilder.cs | 2 +- API/Helpers/Builders/PersonBuilder.cs | 32 + .../Helpers/Builders/SeriesBuilder.cs | 2 +- .../Helpers/Builders/SeriesMetadataBuilder.cs | 5 +- .../Helpers/Builders/VolumeBuilder.cs | 2 +- API/Helpers/PersonHelper.cs | 5 +- API/Services/BookmarkService.cs | 3 +- API/Services/SeriesService.cs | 7 +- API/Services/TaskScheduler.cs | 6 + .../Tasks/Scanner/ParseScannedFiles.cs | 7 +- API/Services/Tasks/Scanner/ProcessSeries.cs | 2 +- API/Services/Tasks/ScannerService.cs | 4 +- API/Startup.cs | 4 + UI/Web/src/app/_models/auth/member.ts | 1 + .../app/_models/preferences/preferences.ts | 1 + UI/Web/src/app/_services/member.service.ts | 4 +- .../manage-users/manage-users.component.html | 47 +- .../manage-users/manage-users.component.ts | 2 +- .../role-selector/role-selector.component.ts | 9 +- .../series-detail.component.html | 3 + .../series-detail.component.scss | 4 + .../app/series-detail/series-detail.module.ts | 3 +- .../library-settings-modal.component.html | 16 +- .../server-stats/server-stats.component.ts | 4 +- .../user-preferences.component.html | 13 + .../user-preferences.component.ts | 5 +- openapi.json | 72 +- 63 files changed, 2186 insertions(+), 239 deletions(-) create mode 100644 API.Tests/Services/Test Data/ArchiveService/ComicInfos/Umlaut.zip create mode 100644 API/Data/MigrateLoginRole.cs create mode 100644 API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs create mode 100644 API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs rename {API.Tests => API}/Helpers/Builders/ChapterBuilder.cs (94%) rename {API.Tests => API}/Helpers/Builders/EntityBuilder.cs (61%) create mode 100644 API/Helpers/Builders/PersonBuilder.cs rename {API.Tests => API}/Helpers/Builders/SeriesBuilder.cs (97%) rename {API.Tests => API}/Helpers/Builders/SeriesMetadataBuilder.cs (90%) rename {API.Tests => API}/Helpers/Builders/VolumeBuilder.cs (96%) diff --git a/API.Tests/Entities/ComicInfoTests.cs b/API.Tests/Entities/ComicInfoTests.cs index ea8b0187d..783248a3b 100644 --- a/API.Tests/Entities/ComicInfoTests.cs +++ b/API.Tests/Entities/ComicInfoTests.cs @@ -36,7 +36,6 @@ public class ComicInfoTests } #endregion - #region CalculatedCount [Fact] diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/API.Tests/Extensions/QueryableExtensionsTests.cs index 76e8752ac..ded191396 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/API.Tests/Extensions/QueryableExtensionsTests.cs @@ -6,7 +6,7 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; -using API.Tests.Helpers.Builders; +using API.Helpers.Builders; using Xunit; namespace API.Tests.Extensions; @@ -226,40 +226,28 @@ public class QueryableExtensionsTests { var items = new List() { - new Person() - { - SeriesMetadatas = new List() + new PersonBuilder("Test", PersonRole.Character) + .WithSeriesMetadata(new SeriesMetadata() { - new SeriesMetadata() - { - AgeRating = AgeRating.Teen, - } - } - }, - new Person() - { - SeriesMetadatas = new List() + AgeRating = AgeRating.Teen, + }) + .Build(), + new PersonBuilder("Test", PersonRole.Character) + .WithSeriesMetadata(new SeriesMetadata() { - new SeriesMetadata() - { - AgeRating = AgeRating.Unknown, - }, - new SeriesMetadata() - { - AgeRating = AgeRating.Teen, - } - } - }, - new Person() - { - SeriesMetadatas = new List() + AgeRating = AgeRating.Unknown, + }) + .WithSeriesMetadata(new SeriesMetadata() { - new SeriesMetadata() - { - AgeRating = AgeRating.X18Plus, - } - } - }, + AgeRating = AgeRating.Teen, + }) + .Build(), + new PersonBuilder("Test", PersonRole.Character) + .WithSeriesMetadata(new SeriesMetadata() + { + AgeRating = AgeRating.X18Plus, + }) + .Build(), }; var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction() diff --git a/API.Tests/Extensions/SeriesExtensionsTests.cs b/API.Tests/Extensions/SeriesExtensionsTests.cs index 99d0825a0..b68d7c533 100644 --- a/API.Tests/Extensions/SeriesExtensionsTests.cs +++ b/API.Tests/Extensions/SeriesExtensionsTests.cs @@ -4,7 +4,7 @@ using API.Comparators; using API.Entities; using API.Entities.Enums; using API.Extensions; -using API.Tests.Helpers.Builders; +using API.Helpers.Builders; using Xunit; namespace API.Tests.Extensions; diff --git a/API.Tests/Helpers/ParserInfoHelperTests.cs b/API.Tests/Helpers/ParserInfoHelperTests.cs index 581b3392c..a6cff42e7 100644 --- a/API.Tests/Helpers/ParserInfoHelperTests.cs +++ b/API.Tests/Helpers/ParserInfoHelperTests.cs @@ -4,9 +4,9 @@ using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; using API.Helpers; +using API.Helpers.Builders; using API.Parser; using API.Services.Tasks.Scanner; -using API.Tests.Helpers.Builders; using Xunit; namespace API.Tests.Helpers; diff --git a/API.Tests/Repository/SeriesRepositoryTests.cs b/API.Tests/Repository/SeriesRepositoryTests.cs index 3d45d5ed7..5aab01128 100644 --- a/API.Tests/Repository/SeriesRepositoryTests.cs +++ b/API.Tests/Repository/SeriesRepositoryTests.cs @@ -8,8 +8,8 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers; +using API.Helpers.Builders; using API.Services; -using API.Tests.Helpers.Builders; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 9974c256c..75fa8f500 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -244,6 +244,17 @@ public class ArchiveServiceTests Assert.Equal(summaryInfo, comicInfo.Summary); } + [Fact] + public void ShouldHaveComicInfo_CanParseUmlaut() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var archive = Path.Join(testDirectory, "Umlaut.zip"); + + var comicInfo = _archiveService.GetComicInfo(archive); + Assert.NotNull(comicInfo); + Assert.Equal("Belladonna", comicInfo.Series); + } + [Fact] public void ShouldHaveComicInfo_WithAuthors() { diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs index 108d95d4b..f4a172eda 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -12,9 +12,9 @@ using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; using API.Helpers; +using API.Helpers.Builders; using API.Services; using API.SignalR; -using API.Tests.Helpers.Builders; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index 177376bd9..d174c6f44 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -12,11 +12,11 @@ using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; using API.Helpers; +using API.Helpers.Builders; using API.Services; using API.Services.Tasks; using API.SignalR; using API.Tests.Helpers; -using API.Tests.Helpers.Builders; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index a9a713209..19dc26d71 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -12,10 +12,10 @@ using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; using API.Helpers; +using API.Helpers.Builders; using API.Services; using API.SignalR; using API.Tests.Helpers; -using API.Tests.Helpers.Builders; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/API.Tests/Services/ReadingListServiceTests.cs index a999c7e20..d9f582719 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/API.Tests/Services/ReadingListServiceTests.cs @@ -13,10 +13,10 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers; +using API.Helpers.Builders; using API.Services; using API.SignalR; using API.Tests.Helpers; -using API.Tests.Helpers.Builders; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 9f2073985..12ce45843 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -4,11 +4,11 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; +using API.Helpers.Builders; using API.Parser; using API.Services.Tasks; using API.Services.Tasks.Scanner; using API.Tests.Helpers; -using API.Tests.Helpers.Builders; using Xunit; namespace API.Tests.Services; diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 20207a66f..36f949757 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -12,10 +12,10 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; +using API.Helpers.Builders; using API.Services; using API.SignalR; using API.Tests.Helpers; -using API.Tests.Helpers.Builders; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; diff --git a/API.Tests/Services/TachiyomiServiceTests.cs b/API.Tests/Services/TachiyomiServiceTests.cs index 77971b444..ffa784da6 100644 --- a/API.Tests/Services/TachiyomiServiceTests.cs +++ b/API.Tests/Services/TachiyomiServiceTests.cs @@ -1,5 +1,5 @@ using API.Extensions; -using API.Tests.Helpers.Builders; +using API.Helpers.Builders; namespace API.Tests.Services; using System.Collections.Generic; diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/Umlaut.zip b/API.Tests/Services/Test Data/ArchiveService/ComicInfos/Umlaut.zip new file mode 100644 index 0000000000000000000000000000000000000000..9c8a8c6fc40c5476c7a16046f8f5672e4e6326eb GIT binary patch literal 1754 zcmV<01||7WO9KQH000080Hu^_R+-jthdKrT0Er6#01W^D07GwWX=6!lW^XQdZES^B z%WfMt6x|!}Kd@fQ8d*t;z?dj-;xsj4Cq|vxX?K+7YK9&TIp9OGweml7RiOQVpr4UH z@t5>ma^%OVKmw1*%lkO@oJ*E(ZaOViO^c6zaBsFQmS#_Io#_m%NM zng#74o)Dt^qJpN`Thdxq))-k9c_r^&kW=KJZ4XPse(XBwZfEDxscSzH8(NVGicCd< zD6Yri)K;`s*RA+^ayb%M{sV5PwFb2V1ktIgBHc$5#7Y_=wI(qqEluqMqQYt<8lq)@ zKO_92k&Z-gHm*rj2!x+TIBP1~iH%w`V{vJ%!{@aTJrK9nXoM%gue7QgQk8JjB1_Sz zYhioNsf)fPr0qPX)I=j%*{eXxh%8n5Sq) z5Suy?NciC7a>?c%H9jO)OIN|~d)P*Va@d?xE#(rPPlD7+O5>3l4pb;pI^ngHHE?iZ z4hK>w^CSxdssObSfPx%v7>Yh&`mDe)DnbKC3C?S4YMhfDe%FLNm8sb6iK&!iDAwfK zjg%cnOebmrf*t-CYXf%!_{ktVTo^VEqNDIU;z%gZD`ZqnTkM{c)gnb7Y~Pa^i(e?P zc|!>eWl{kgc&SzJ;1$e+xWD@|aVIM8d(vVJKd_ynxeUE-*)JB1qTqo@*eG5f=(UPU zGlwGgDOjpOH*PDpWxTs{1b(vZhwtAUdL%wXeArRS$uF333e+@Jv_j@4^G@$6LLrJe zaRgA=d_Et@bi;vc}avq(N;y0ssSkQ=B_Zc{C+j#6G za&|14Ow~(rI{7}APF9eh*hG|QQolMzoav`}t!SANa%ibefI?)N!Zp)|6{Ji{nmcfY zVjlG_df75e8%kA5y^@|OSPLCdL@U73Bo4VivpJ=dt#nz=dM2s6pL zUfFcav|9o+)-J0gUL$MDgdK)J$McH}ieHgg5o+OypnNA2h%7JWCUq6JfN78!yHO_5 zFL$iwfiX&4gg4hF(sHiWsqg=qfOH|fnp|QA+LdJ;SY^x-Rtsm70&FbPPZ(Rn4Y>qv zck4q!2lQNoF#v5)d18JYgCB~i2(WfMk>(cVFewCBkX#WL33H7FKi=Ir<_YGc3G1Ys zG-dKMNnVl}D<|-5<9yDcJ&ejDjrj~%!H0^dLhDwnm6|7C9w5ic8@f%J@&Qe?_jiB! zB&jqZQBx8&rKWw6ZX=VFK9bs?+K+Lwuw(H~WA=Xk*2w_Mtyoh$Jm4VDqB$)wMJnP^ zS3!6!XK;V__aJu^CQSyH%>CUzDHN2Mn6+8MiDF6l+pd=ZK3mFlD#LLWwwo(MEQ8Jx zW+}k%4B*PfDpN8@r8BpfjT!r_3|`3aCj!)zFC-q#mnb7uz+&Q_^aN7klCv?e+tg@l zL81jljFaq-y8GYwxVKd^2JB;WO`?Qb34w;Nf$71{^$cVSDWHSKfaQKrNpeK87qKrlCi( zW&>Z#;&CaPd|*b*jw*f)Mu)P0fm7tPkh(1L=8zj6dQsMNa`AaarYxTR&K5sghp95V zqIr59m&LG~p000O8rIcz`nbvTJ zItBm$i3 + + + + + + @@ -131,6 +137,12 @@ + + + + + + @@ -146,6 +158,8 @@ + + @@ -169,6 +183,7 @@ Always + diff --git a/API/Constants/PolicyConstants.cs b/API/Constants/PolicyConstants.cs index 546ad4158..69de1821b 100644 --- a/API/Constants/PolicyConstants.cs +++ b/API/Constants/PolicyConstants.cs @@ -31,7 +31,11 @@ public static class PolicyConstants /// Used to give a user ability to Change Restrictions on their account /// public const string ChangeRestrictionRole = "Change Restriction"; + /// + /// Used to give a user ability to Login to their account + /// + public const string LoginRole = "Login"; public static readonly ImmutableArray ValidRoles = - ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole); + ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole); } diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 7779a5f3d..65ceaa5a8 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -144,6 +144,7 @@ public class AccountController : BaseApiController var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole); if (!roleResult.Succeeded) return BadRequest(result.Errors); + await _userManager.AddToRoleAsync(user, PolicyConstants.LoginRole); return new UserDto { @@ -182,6 +183,8 @@ public class AccountController : BaseApiController .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper()); if (user == null) return Unauthorized("Your credentials are not correct"); + var roles = await _userManager.GetRolesAsync(user); + if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized("Your account is disabled. Contact the server admin."); var result = await _signInManager .CheckPasswordSignInAsync(user, loginDto.Password, true); diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 9b9308b2c..ffc3ef788 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -338,7 +338,6 @@ public class LibraryController : BaseApiController library.IncludeInRecommended = dto.IncludeInRecommended; library.IncludeInSearch = dto.IncludeInSearch; library.ManageCollections = dto.ManageCollections; - library.CollapseSeriesRelationships = dto.CollapseSeriesRelationships; _unitOfWork.LibraryRepository.Update(library); diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index 9b3c5876a..3891b788d 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -31,6 +31,7 @@ public class MetadataController : BaseApiController /// String separated libraryIds or null for all genres /// [HttpGet("genres")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] public async Task>> GetAllGenres(string? libraryIds) { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); @@ -51,6 +52,7 @@ public class MetadataController : BaseApiController /// String separated libraryIds or null for all people /// [HttpGet("people")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] public async Task>> GetAllPeople(string? libraryIds) { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); @@ -68,6 +70,7 @@ public class MetadataController : BaseApiController /// String separated libraryIds or null for all tags /// [HttpGet("tags")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] public async Task>> GetAllTags(string? libraryIds) { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); @@ -132,6 +135,7 @@ public class MetadataController : BaseApiController /// String separated libraryIds or null for all ratings /// [HttpGet("languages")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] public async Task>> GetAllLanguages(string? libraryIds) { var ids = libraryIds?.Split(",").Select(int.Parse).ToList(); @@ -145,6 +149,7 @@ public class MetadataController : BaseApiController } [HttpGet("all-languages")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)] public IEnumerable GetAllValidLanguages() { return CultureInfo.GetCultures(CultureTypes.AllCultures).Select(c => diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index b1f8d98b9..1a6373ba2 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -38,18 +38,16 @@ public class UsersController : BaseApiController return BadRequest("Could not delete the user."); } + /// + /// Returns all users of this server + /// + /// This will include pending members + /// [Authorize(Policy = "RequireAdminRole")] [HttpGet] - public async Task>> GetUsers() + public async Task>> GetUsers(bool includePending = false) { - return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync()); - } - - [Authorize(Policy = "RequireAdminRole")] - [HttpGet("pending")] - public async Task>> GetPendingUsers() - { - return Ok(await _unitOfWork.UserRepository.GetPendingMemberDtosAsync()); + return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync(!includePending)); } [HttpGet("myself")] @@ -110,6 +108,7 @@ public class UsersController : BaseApiController existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize; existingPreferences.NoTransitions = preferencesDto.NoTransitions; existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate; + existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships; _unitOfWork.UserRepository.Update(existingPreferences); diff --git a/API/DTOs/Account/AgeRestrictionDto.cs b/API/DTOs/Account/AgeRestrictionDto.cs index ad4534b35..0aaec9b97 100644 --- a/API/DTOs/Account/AgeRestrictionDto.cs +++ b/API/DTOs/Account/AgeRestrictionDto.cs @@ -7,10 +7,10 @@ public class AgeRestrictionDto /// /// The maximum age rating a user has access to. -1 if not applicable /// - public AgeRating AgeRating { get; set; } = AgeRating.NotApplicable; + public required AgeRating AgeRating { get; set; } = AgeRating.NotApplicable; /// /// Are Unknowns explicitly allowed against age rating /// /// Unknown is always lowest and default age rating. Setting this to false will ensure Teen age rating applies and unknowns are still filtered - public bool IncludeUnknowns { get; set; } = false; + public required bool IncludeUnknowns { get; set; } = false; } diff --git a/API/DTOs/Account/UpdateUserDto.cs b/API/DTOs/Account/UpdateUserDto.cs index 4941e8d8a..bda664bdb 100644 --- a/API/DTOs/Account/UpdateUserDto.cs +++ b/API/DTOs/Account/UpdateUserDto.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace API.DTOs.Account; @@ -17,5 +18,4 @@ public record UpdateUserDto /// An Age Rating which will limit the account to seeing everything equal to or below said rating. /// public AgeRestrictionDto AgeRestriction { get; init; } = default!; - } diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs index 23d87358a..31b5e62be 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -12,6 +12,10 @@ public class MemberDto public int Id { get; init; } public string? Username { get; init; } public string? Email { get; init; } + /// + /// If the member is still pending or not + /// + public bool IsPending { get; init; } public AgeRestrictionDto? AgeRestriction { get; init; } public DateTime Created { get; init; } public DateTime LastActive { get; init; } diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs index db25be886..0f3b2d642 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/API/DTOs/UpdateLibraryDto.cs @@ -24,7 +24,4 @@ public class UpdateLibraryDto public bool IncludeInSearch { get; init; } [Required] public bool ManageCollections { get; init; } - [Required] - public bool CollapseSeriesRelationships { get; init; } - } diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index ab09329bc..cb738aa42 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using API.Entities; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; @@ -137,4 +137,9 @@ public class UserPreferencesDto /// [Required] public bool NoTransitions { get; set; } = false; + /// + /// When showing series, only parent series or series with no relationships will be returned + /// + [Required] + public bool CollapseSeriesRelationships { get; set; } = false; } diff --git a/API/Data/MigrateLoginRole.cs b/API/Data/MigrateLoginRole.cs new file mode 100644 index 000000000..93f839589 --- /dev/null +++ b/API/Data/MigrateLoginRole.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using API.Constants; +using API.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; + +namespace API.Data; + +/// +/// Added in v0.7.1.18 +/// +public static class MigrateLoginRoles +{ + /// + /// Will not run if any users have the role already + /// + /// + /// + /// + public static async Task Migrate(IUnitOfWork unitOfWork, UserManager userManager, ILogger logger) + { + var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.LoginRole); + if (usersWithRole.Count != 0) return; + + logger.LogCritical("Running MigrateLoginRoles migration"); + + var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(); + foreach (var user in allUsers) + { + await userManager.RemoveFromRoleAsync(user, PolicyConstants.LoginRole); + await userManager.AddToRoleAsync(user, PolicyConstants.LoginRole); + } + + logger.LogInformation("MigrateLoginRoles migration complete"); + } +} diff --git a/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs b/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs new file mode 100644 index 000000000..2edee6323 --- /dev/null +++ b/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs @@ -0,0 +1,1858 @@ +// +using System; +using API.Data; +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("20230310142630_MoveCollapseSeriesToUserPref")] + partial class MoveCollapseSeriesToUserPref + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + 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("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("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .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("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.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .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("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + 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("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") + .HasColumnType("INTEGER"); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + 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("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + 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.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .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.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.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInDashboard") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInRecommended") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInSearch") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + 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("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + 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.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("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .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("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + 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.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + 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("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .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.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .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("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NameLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + 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("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .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("Provider") + .HasColumnType("INTEGER"); + + 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("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + 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("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + 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("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("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + 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.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + 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", null) + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .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.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.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .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.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.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.Series", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + 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("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("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .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("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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .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("Bookmarks"); + + b.Navigation("Devices"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + 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/20230310142630_MoveCollapseSeriesToUserPref.cs b/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs new file mode 100644 index 000000000..db5920d0a --- /dev/null +++ b/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class MoveCollapseSeriesToUserPref : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CollapseSeriesRelationships", + table: "AppUserPreferences", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CollapseSeriesRelationships", + table: "AppUserPreferences"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index bffad6524..0bd51c92a 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -221,10 +221,10 @@ namespace API.Data.Migrations b.Property("BookReaderReadingDirection") .HasColumnType("INTEGER"); - b.Property("BookReaderWritingStyle") + b.Property("BookReaderTapToPaginate") .HasColumnType("INTEGER"); - b.Property("BookReaderTapToPaginate") + b.Property("BookReaderWritingStyle") .HasColumnType("INTEGER"); b.Property("BookThemeName") @@ -232,6 +232,9 @@ namespace API.Data.Migrations .HasColumnType("TEXT") .HasDefaultValue("Dark"); + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + b.Property("EmulateBook") .HasColumnType("INTEGER"); @@ -601,9 +604,6 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - b.Property("CoverImage") .HasColumnType("TEXT"); diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index a2937665e..de7c5c39f 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -770,9 +770,9 @@ public class SeriesRepository : ISeriesRepository // NOTE: Why do we even have libraryId when the filter has the actual libraryIds? var userLibraries = await GetUserLibrariesForFilteredQuery(libraryId, userId, queryContext); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var onlyParentSeries = await _context.Library.AsNoTracking() - .Where(l => filter.Libraries.Contains(l.Id)) - .AllAsync(l => l.CollapseSeriesRelationships); + var onlyParentSeries = await _context.AppUserPreferences.Where(u => u.AppUserId == userId) + .Select(u => u.CollapseSeriesRelationships) + .SingleOrDefaultAsync(); var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries, out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter, diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 8cb13cf56..f50196c18 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -39,8 +39,7 @@ public interface IUserRepository void Add(AppUserBookmark bookmark); public void Delete(AppUser? user); void Delete(AppUserBookmark bookmark); - Task> GetEmailConfirmedMemberDtosAsync(); - Task> GetPendingMemberDtosAsync(); + Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true); Task> GetAdminUsersAsync(); Task IsUserAdminAsync(AppUser? user); Task GetUserRatingAsync(int seriesId, int userId); @@ -329,10 +328,10 @@ public class UserRepository : IUserRepository } - public async Task> GetEmailConfirmedMemberDtosAsync() + public async Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true) { return await _context.Users - .Where(u => u.EmailConfirmed) + .Where(u => (emailConfirmed && u.EmailConfirmed) || !emailConfirmed) .Include(x => x.Libraries) .Include(r => r.UserRoles) .ThenInclude(r => r.Role) @@ -344,45 +343,8 @@ public class UserRepository : IUserRepository Email = u.Email, Created = u.Created, LastActive = u.LastActive, - Roles = u.UserRoles.Select(r => r.Role.Name).ToList()!, - AgeRestriction = new AgeRestrictionDto() - { - AgeRating = u.AgeRestriction, - IncludeUnknowns = u.AgeRestrictionIncludeUnknowns - }, - Libraries = u.Libraries.Select(l => new LibraryDto - { - Name = l.Name, - Type = l.Type, - LastScanned = l.LastScanned, - Folders = l.Folders.Select(x => x.Path).ToList() - }).ToList() - }) - .AsSplitQuery() - .AsNoTracking() - .ToListAsync(); - } - - /// - /// Returns a list of users that are considered Pending by invite. This means email is unconfirmed and they have never logged in - /// - /// - public async Task> GetPendingMemberDtosAsync() - { - return await _context.Users - .Where(u => !u.EmailConfirmed && u.LastActive == DateTime.MinValue) - .Include(x => x.Libraries) - .Include(r => r.UserRoles) - .ThenInclude(r => r.Role) - .OrderBy(u => u.UserName) - .Select(u => new MemberDto - { - Id = u.Id, - Username = u.UserName, - Email = u.Email, - Created = u.Created, - LastActive = u.LastActive, - Roles = u.UserRoles.Select(r => r.Role.Name).ToList()!, + Roles = u.UserRoles.Select(r => r.Role.Name).ToList(), + IsPending = !u.EmailConfirmed, AgeRestriction = new AgeRestrictionDto() { AgeRating = u.AgeRestriction, diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 9a7405ebf..77fd2bd12 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -52,6 +52,7 @@ public class AppUser : IdentityUser, IHasConcurrencyToken /// public bool AgeRestrictionIncludeUnknowns { get; set; } = false; + /// [ConcurrencyCheck] public uint RowVersion { get; private set; } diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index 835ac2cda..0b53f3f24 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -119,6 +119,10 @@ public class AppUserPreferences /// UI Site Global Setting: Should Kavita disable CSS transitions /// public bool NoTransitions { get; set; } = false; + /// + /// UI Site Global Setting: When showing series, only parent series or series with no relationships will be returned + /// + public bool CollapseSeriesRelationships { get; set; } = false; public AppUser AppUser { get; set; } = null!; public int AppUserId { get; set; } diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 43535ca4c..a6c2e8e1a 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -31,10 +31,6 @@ public class Library : IEntityDate /// Should this library create and manage collections from Metadata /// public bool ManageCollections { get; set; } = true; - /// - /// When showing series, only parent series or series with no relationships will be returned - /// - public bool CollapseSeriesRelationships { get; set; } = false; public DateTime Created { get; set; } public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } diff --git a/API/Entities/Person.cs b/API/Entities/Person.cs index 05b3cfdba..eeb21d6b1 100644 --- a/API/Entities/Person.cs +++ b/API/Entities/Person.cs @@ -7,9 +7,9 @@ namespace API.Entities; public class Person { public int Id { get; set; } - public string? Name { get; set; } - public string? NormalizedName { get; set; } - public PersonRole Role { get; set; } + public required string Name { get; set; } + public required string NormalizedName { get; set; } + public required PersonRole Role { get; set; } // Relationships public ICollection SeriesMetadatas { get; set; } = null!; diff --git a/API.Tests/Helpers/Builders/ChapterBuilder.cs b/API/Helpers/Builders/ChapterBuilder.cs similarity index 94% rename from API.Tests/Helpers/Builders/ChapterBuilder.cs rename to API/Helpers/Builders/ChapterBuilder.cs index 00ad694a5..ae4253a60 100644 --- a/API.Tests/Helpers/Builders/ChapterBuilder.cs +++ b/API/Helpers/Builders/ChapterBuilder.cs @@ -1,13 +1,12 @@ using System.Collections.Generic; -using API.Data; using API.Entities; using API.Entities.Enums; -namespace API.Tests.Helpers.Builders; +namespace API.Helpers.Builders; public class ChapterBuilder : IEntityBuilder { - private Chapter _chapter; + private readonly Chapter _chapter; public Chapter Build() => _chapter; public ChapterBuilder(string number, string? range=null) diff --git a/API.Tests/Helpers/Builders/EntityBuilder.cs b/API/Helpers/Builders/EntityBuilder.cs similarity index 61% rename from API.Tests/Helpers/Builders/EntityBuilder.cs rename to API/Helpers/Builders/EntityBuilder.cs index 515e4b934..d45666e29 100644 --- a/API.Tests/Helpers/Builders/EntityBuilder.cs +++ b/API/Helpers/Builders/EntityBuilder.cs @@ -1,4 +1,4 @@ -namespace API.Tests.Helpers.Builders; +namespace API.Helpers.Builders; public interface IEntityBuilder { diff --git a/API/Helpers/Builders/PersonBuilder.cs b/API/Helpers/Builders/PersonBuilder.cs new file mode 100644 index 000000000..721a45ebd --- /dev/null +++ b/API/Helpers/Builders/PersonBuilder.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Metadata; +using API.Extensions; + +namespace API.Helpers.Builders; + +public class PersonBuilder : IEntityBuilder +{ + private readonly Person _person; + public Person Build() => _person; + + public PersonBuilder(string name, PersonRole role) + { + _person = new Person() + { + Name = name.Trim(), + NormalizedName = name.ToNormalized(), + Role = role, + ChapterMetadatas = new List(), + SeriesMetadatas = new List() + }; + } + + public PersonBuilder WithSeriesMetadata(SeriesMetadata metadata) + { + _person.SeriesMetadatas ??= new List(); + _person.SeriesMetadatas.Add(metadata); + return this; + } +} diff --git a/API.Tests/Helpers/Builders/SeriesBuilder.cs b/API/Helpers/Builders/SeriesBuilder.cs similarity index 97% rename from API.Tests/Helpers/Builders/SeriesBuilder.cs rename to API/Helpers/Builders/SeriesBuilder.cs index 253341403..765036230 100644 --- a/API.Tests/Helpers/Builders/SeriesBuilder.cs +++ b/API/Helpers/Builders/SeriesBuilder.cs @@ -5,7 +5,7 @@ using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; -namespace API.Tests.Helpers.Builders; +namespace API.Helpers.Builders; public class SeriesBuilder : IEntityBuilder { diff --git a/API.Tests/Helpers/Builders/SeriesMetadataBuilder.cs b/API/Helpers/Builders/SeriesMetadataBuilder.cs similarity index 90% rename from API.Tests/Helpers/Builders/SeriesMetadataBuilder.cs rename to API/Helpers/Builders/SeriesMetadataBuilder.cs index d84ad152b..2cf5f5d2e 100644 --- a/API.Tests/Helpers/Builders/SeriesMetadataBuilder.cs +++ b/API/Helpers/Builders/SeriesMetadataBuilder.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; -using API.Data; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; -namespace API.Tests.Helpers.Builders; +namespace API.Helpers.Builders; public class SeriesMetadataBuilder : IEntityBuilder { - private SeriesMetadata _seriesMetadata; + private readonly SeriesMetadata _seriesMetadata; public SeriesMetadata Build() => _seriesMetadata; public SeriesMetadataBuilder() diff --git a/API.Tests/Helpers/Builders/VolumeBuilder.cs b/API/Helpers/Builders/VolumeBuilder.cs similarity index 96% rename from API.Tests/Helpers/Builders/VolumeBuilder.cs rename to API/Helpers/Builders/VolumeBuilder.cs index eb074a9df..b5b4f31cd 100644 --- a/API.Tests/Helpers/Builders/VolumeBuilder.cs +++ b/API/Helpers/Builders/VolumeBuilder.cs @@ -3,7 +3,7 @@ using System.Linq; using API.Data; using API.Entities; -namespace API.Tests.Helpers.Builders; +namespace API.Helpers.Builders; public class VolumeBuilder : IEntityBuilder { diff --git a/API/Helpers/PersonHelper.cs b/API/Helpers/PersonHelper.cs index 89d6177f2..606b3b3db 100644 --- a/API/Helpers/PersonHelper.cs +++ b/API/Helpers/PersonHelper.cs @@ -85,7 +85,7 @@ public static class PersonHelper foreach (var person in existingPeople) { var existingPerson = removeAllExcept - .FirstOrDefault(p => person.NormalizedName != null && p.Role == person.Role && person.NormalizedName.Equals(p.NormalizedName)); + .FirstOrDefault(p => p.Role == person.Role && person.NormalizedName.Equals(p.NormalizedName)); if (existingPerson == null) { action?.Invoke(person); @@ -100,8 +100,9 @@ public static class PersonHelper /// public static void AddPersonIfNotExists(ICollection metadataPeople, Person person) { + if (string.IsNullOrEmpty(person.Name)) return; var existingPerson = metadataPeople.SingleOrDefault(p => - p.NormalizedName == person.Name?.ToNormalized() && p.Role == person.Role); + p.NormalizedName == person.Name.ToNormalized() && p.Role == person.Role); if (existingPerson == null) { metadataPeople.Add(person); diff --git a/API/Services/BookmarkService.cs b/API/Services/BookmarkService.cs index 7188663e4..9f2a0794e 100644 --- a/API/Services/BookmarkService.cs +++ b/API/Services/BookmarkService.cs @@ -348,8 +348,9 @@ public class BookmarkService : IBookmarkService /// The file to convert /// Full path to where files should be stored or any stem /// - private async Task SaveAsWebP(string imageDirectory, string filename, string targetFolder) + public async Task SaveAsWebP(string imageDirectory, string filename, string targetFolder) { + // This must be Public as it's used in via Hangfire as a background task var fullSourcePath = _directoryService.FileSystem.Path.Join(imageDirectory, filename); var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(filename).Name, string.Empty); diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 3399c2df0..51e25797a 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -182,7 +182,12 @@ public class SeriesService : ISeriesService return true; } - if (await _unitOfWork.CommitAsync() && updateSeriesMetadataDto.CollectionTags != null) + await _unitOfWork.CommitAsync(); + + // Trigger code to cleanup tags, collections, people, etc + await _taskScheduler.CleanupDbEntries(); + + if (updateSeriesMetadataDto.CollectionTags != null) { foreach (var tag in updateSeriesMetadataDto.CollectionTags) { diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index d098e195e..3cf6c9b43 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -31,6 +31,7 @@ public interface ITaskScheduler Task RunStatCollection(); void ScanSiteThemes(); Task CovertAllCoversToWebP(); + Task CleanupDbEntries(); } public class TaskScheduler : ITaskScheduler { @@ -230,6 +231,11 @@ public class TaskScheduler : ITaskScheduler #endregion + public async Task CleanupDbEntries() + { + await _cleanupService.CleanupDbEntries(); + } + public void ScanLibraries() { if (RunningAnyTasksByMethod(ScanTasks, ScanQueue)) diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 098246de1..63437f0e0 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -236,6 +236,11 @@ public class ParseScannedFiles p.Key.Format == info.Format) .Key; + if (existingName == null) + { + return info.Series; + } + if (!string.IsNullOrEmpty(existingName.Name)) { return existingName.Name; @@ -297,7 +302,7 @@ public class ParseScannedFiles _logger.LogDebug("[ScannerService] Found {Count} files for {Folder}", files.Count, folder); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.FileScanProgressEvent(folder, libraryName, ProgressEventType.Updated)); + MessageFactory.FileScanProgressEvent($"{files.Count} files in {folder}", libraryName, ProgressEventType.Updated)); if (files.Count == 0) { _logger.LogInformation("[ScannerService] {Folder} is empty or is no longer in this location", folder); diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 6b01f3f6e..184b1d06d 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -433,7 +433,7 @@ public class ProcessSeries : IProcessSeries var genres = chapters.SelectMany(c => c.Genres).ToList(); GenreHelper.KeepOnlySameGenreBetweenLists(series.Metadata.Genres.ToList(), genres, genre => { - if (series.Metadata.GenresLocked) return; + if (series.Metadata.GenresLocked) return; // NOTE: Doesn't it make sense to do the locked skip outside this loop? series.Metadata.Genres.Remove(genre); }); diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 20c65e92f..dc0243627 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -520,12 +520,14 @@ public class ScannerService : IScannerService var scanElapsedTime = await ScanFiles(library, libraryFolderPaths, shouldUseLibraryScan, TrackFiles, forceUpdate); + // NOTE: This runs sync after every file is scanned foreach (var task in processTasks) { await task(); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent(string.Empty, library.Name, ProgressEventType.Ended)); + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.FileScanProgressEvent(string.Empty, library.Name, ProgressEventType.Ended)); _logger.LogInformation("[ScannerService] Finished file scan in {ScanAndUpdateTime} milliseconds. Updating database", scanElapsedTime); diff --git a/API/Startup.cs b/API/Startup.cs index 60fd490ee..f2c96afe5 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -214,6 +214,7 @@ public class Startup logger.LogInformation("Running Migrations"); + // Only run this if we are upgrading await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager); await MigrateRemoveExtraThemes.Migrate(unitOfWork, themeService); @@ -235,6 +236,9 @@ public class Startup // v0.7 await MigrateBrokenGMT1Dates.Migrate(unitOfWork, dataContext, logger); + // v0.7.2 + await MigrateLoginRoles.Migrate(unitOfWork, userManager, logger); + // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); installVersion.Value = BuildInfo.Version.ToString(); diff --git a/UI/Web/src/app/_models/auth/member.ts b/UI/Web/src/app/_models/auth/member.ts index 76f05d44d..a9b50d110 100644 --- a/UI/Web/src/app/_models/auth/member.ts +++ b/UI/Web/src/app/_models/auth/member.ts @@ -10,4 +10,5 @@ export interface Member { roles: string[]; libraries: Library[]; ageRestriction: AgeRestriction; + isPending: boolean; } \ No newline at end of file diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index b80f946f4..96f2beb61 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -40,6 +40,7 @@ export interface Preferences { blurUnreadSummaries: boolean; promptForDownloadSize: boolean; noTransitions: boolean; + collapseSeriesRelationships: boolean; } export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}]; diff --git a/UI/Web/src/app/_services/member.service.ts b/UI/Web/src/app/_services/member.service.ts index b06ee3e38..e7179cf7f 100644 --- a/UI/Web/src/app/_services/member.service.ts +++ b/UI/Web/src/app/_services/member.service.ts @@ -12,8 +12,8 @@ export class MemberService { constructor(private httpClient: HttpClient) { } - getMembers() { - return this.httpClient.get(this.baseUrl + 'users'); + getMembers(includePending: boolean = false) { + return this.httpClient.get(this.baseUrl + 'users?includePending=' + includePending); } getMemberNames() { diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.html b/UI/Web/src/app/admin/manage-users/manage-users.component.html index 2c32c009d..50dedc99c 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.html +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.html @@ -1,40 +1,11 @@
- -
-

Pending Invites

-
-
-
    -
  • -
    -

    - {{invite.username | titlecase}} -
    - - - -
    -

    - -
    Invited: {{invite.created | date: 'short'}}
    -
    -
  • -
  • -
    - -
    -
  • -
  • - There are no invited Users -
  • -
-
- - - -

Active Users

+
+

Active Users

+
+
+
  • @@ -45,10 +16,14 @@ (You) + Pending
    - - + + + + +
    +
    + +
    Continue {{ContinuePointTitle}}
    diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss index 039d42757..ee0c847d7 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss @@ -53,3 +53,7 @@ .info-container { align-items: flex-start; } + +::ng-deep .progress { + border-radius: 0px; +} \ No newline at end of file diff --git a/UI/Web/src/app/series-detail/series-detail.module.ts b/UI/Web/src/app/series-detail/series-detail.module.ts index 2ecaf7344..df9c638dd 100644 --- a/UI/Web/src/app/series-detail/series-detail.module.ts +++ b/UI/Web/src/app/series-detail/series-detail.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SeriesDetailRoutingModule } from './series-detail-routing.module'; -import { NgbCollapseModule, NgbNavModule, NgbRatingModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbCollapseModule, NgbNavModule, NgbProgressbarModule, NgbRatingModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { SeriesMetadataDetailComponent } from './_components/series-metadata-detail/series-metadata-detail.component'; import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component'; import { SharedModule } from '../shared/shared.module'; @@ -26,6 +26,7 @@ import { SeriesDetailComponent } from './_components/series-detail/series-detail NgbNavModule, NgbRatingModule, NgbTooltipModule, // Series Detail, Extras Drawer + NgbProgressbarModule, TypeaheadModule, PipeModule, diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html index 49d2ca38f..ea1a54734 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html @@ -105,21 +105,7 @@

- -
-
-
-
- - -
-
-

- Experiemental: Should Kavita show Series that have no relationships or is the parent/prequel -

-
-
- +
diff --git a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts index f79a229b9..d88bff1a9 100644 --- a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts +++ b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts @@ -101,7 +101,7 @@ export class ServerStatsComponent implements OnDestroy { ref.componentInstance.title = 'Genres'; ref.componentInstance.clicked = (item: string) => { const params: any = {}; - params[FilterQueryParam.Genres] = item; + params[FilterQueryParam.Genres] = genres.filter(g => g.title === item)[0].id; params[FilterQueryParam.Page] = 1; this.router.navigate(['all-series'], {queryParams: params}); }; @@ -115,7 +115,7 @@ export class ServerStatsComponent implements OnDestroy { ref.componentInstance.title = 'Tags'; ref.componentInstance.clicked = (item: string) => { const params: any = {}; - params[FilterQueryParam.Tags] = item; + params[FilterQueryParam.Tags] = tags.filter(g => g.title === item)[0].id; params[FilterQueryParam.Page] = 1; this.router.navigate(['all-series'], {queryParams: params}); }; diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html index fe9a594d8..67521217d 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html @@ -77,6 +77,19 @@
+
+
+
+
+ + +
+
+ Experiemental: Should Kavita show Series that have no relationships or is the parent/prequel + Experiemental: Should Kavita show Series that have no relationships or is the parent/prequel +
+
+
diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index 07e2cfd95..1960e033b 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -156,6 +156,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { 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.cdRef.markForCheck(); }); @@ -202,6 +203,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.settingsForm.get('noTransitions')?.setValue(this.user.preferences.noTransitions); this.settingsForm.get('emulateBook')?.setValue(this.user.preferences.emulateBook); this.settingsForm.get('swipeToPaginate')?.setValue(this.user.preferences.swipeToPaginate); + this.settingsForm.get('collapseSeriesRelationships')?.setValue(this.user.preferences.collapseSeriesRelationships); this.cdRef.markForCheck(); this.settingsForm.markAsPristine(); } @@ -234,7 +236,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { promptForDownloadSize: modelSettings.promptForDownloadSize, noTransitions: modelSettings.noTransitions, emulateBook: modelSettings.emulateBook, - swipeToPaginate: modelSettings.swipeToPaginate + swipeToPaginate: modelSettings.swipeToPaginate, + collapseSeriesRelationships: modelSettings.collapseSeriesRelationships }; this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => { diff --git a/openapi.json b/openapi.json index 22143c1d8..a860d0e35 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.1.13" + "version": "0.7.1.16" }, "servers": [ { @@ -8926,43 +8926,17 @@ "tags": [ "Users" ], - "responses": { - "200": { - "description": "Success", - "content": { - "text/plain": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MemberDto" - } - } - }, - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MemberDto" - } - } - }, - "text/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MemberDto" - } - } - } + "summary": "Returns all users of this server", + "parameters": [ + { + "name": "includePending", + "in": "query", + "description": "This will include pending members", + "schema": { + "type": "boolean", + "default": false } } - } - } - }, - "/api/Users/pending": { - "get": { - "tags": [ - "Users" ], "responses": { "200": { @@ -9811,6 +9785,10 @@ "type": "boolean", "description": "UI Site Global Setting: Should Kavita disable CSS transitions" }, + "collapseSeriesRelationships": { + "type": "boolean", + "description": "UI Site Global Setting: When showing series, only parent series or series with no relationships will be returned" + }, "appUser": { "$ref": "#/components/schemas/AppUser" }, @@ -11729,10 +11707,6 @@ "type": "boolean", "description": "Should this library create and manage collections from Metadata" }, - "collapseSeriesRelationships": { - "type": "boolean", - "description": "When showing series, only parent series or series with no relationships will be returned" - }, "created": { "type": "string", "format": "date-time" @@ -12075,6 +12049,10 @@ "type": "string", "nullable": true }, + "isPending": { + "type": "boolean", + "description": "If the member is still pending or not" + }, "ageRestriction": { "$ref": "#/components/schemas/AgeRestrictionDto" }, @@ -14130,6 +14108,11 @@ "description": "Name of the Theme", "nullable": true }, + "normalizedName": { + "type": "string", + "description": "Normalized name for lookups", + "nullable": true + }, "fileName": { "type": "string", "description": "File path to the content. Stored under API.Services.DirectoryService.SiteThemeDirectory.\r\nMust be a .css file", @@ -14367,7 +14350,6 @@ }, "UpdateLibraryDto": { "required": [ - "collapseSeriesRelationships", "folders", "folderWatching", "id", @@ -14411,9 +14393,6 @@ }, "manageCollections": { "type": "boolean" - }, - "collapseSeriesRelationships": { - "type": "boolean" } }, "additionalProperties": false @@ -14964,6 +14943,7 @@ "bookReaderTapToPaginate", "bookReaderThemeName", "bookReaderWritingStyle", + "collapseSeriesRelationships", "emulateBook", "globalPageLayoutMode", "layoutMode", @@ -15073,6 +15053,10 @@ "noTransitions": { "type": "boolean", "description": "UI Site Global Setting: Should Kavita disable CSS transitions" + }, + "collapseSeriesRelationships": { + "type": "boolean", + "description": "When showing series, only parent series or series with no relationships will be returned" } }, "additionalProperties": false