diff --git a/API.Tests/Services/ReadingProfileServiceTest.cs b/API.Tests/Services/ReadingProfileServiceTest.cs index bf88aa2fb..7fa998eb0 100644 --- a/API.Tests/Services/ReadingProfileServiceTest.cs +++ b/API.Tests/Services/ReadingProfileServiceTest.cs @@ -4,7 +4,6 @@ using API.Data.Repositories; using API.DTOs; using API.Entities; using API.Entities.Enums; -using API.Extensions.QueryExtensions; using API.Helpers.Builders; using API.Services; using API.Tests.Helpers; @@ -18,7 +17,11 @@ namespace API.Tests.Services; public class ReadingProfileServiceTest: AbstractDbTest { - public async Task<(IReadingProfileService, AppUser, Library, Series)> Setup() + /// + /// Does not add a default reading profile + /// + /// + public async Task<(ReadingProfileService, AppUser, Library, Series)> Setup() { var user = new AppUserBuilder("amelia", "amelia@localhost").Build(); Context.AppUser.Add(user); @@ -33,7 +36,7 @@ public class ReadingProfileServiceTest: AbstractDbTest user.Libraries.Add(library); await UnitOfWork.CommitAsync(); - var rps = new ReadingProfileService(UnitOfWork, Substitute.For()); + var rps = new ReadingProfileService(UnitOfWork, Substitute.For(), Mapper); user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.UserPreferences); return (rps, user, library, series); @@ -46,7 +49,7 @@ public class ReadingProfileServiceTest: AbstractDbTest var (rps, user, library, series) = await Setup(); var profile = new AppUserReadingProfileBuilder(user.Id) - .WithImplicit(true) + .WithKind(ReadingProfileKind.Implicit) .WithSeries(series) .WithName("Implicit Profile") .Build(); @@ -56,31 +59,23 @@ public class ReadingProfileServiceTest: AbstractDbTest .WithName("Non-implicit Profile") .Build(); - user.UserPreferences.ReadingProfiles.Add(profile); - user.UserPreferences.ReadingProfiles.Add(profile2); + user.ReadingProfiles.Add(profile); + user.ReadingProfiles.Add(profile2); await UnitOfWork.CommitAsync(); - var seriesProfile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(user.Id, series.Id); + var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); Assert.NotNull(seriesProfile); Assert.Equal("Implicit Profile", seriesProfile.Name); - var seriesProfileDto = await UnitOfWork.AppUserReadingProfileRepository.GetProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(seriesProfileDto); - Assert.Equal("Implicit Profile", seriesProfileDto.Name); - await rps.UpdateReadingProfile(user.Id, new UserReadingProfileDto { Id = profile2.Id, WidthOverride = 23, }); - seriesProfile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(user.Id, series.Id); + seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); Assert.NotNull(seriesProfile); Assert.Equal("Non-implicit Profile", seriesProfile.Name); - - seriesProfileDto = await UnitOfWork.AppUserReadingProfileRepository.GetProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(seriesProfileDto); - Assert.Equal("Non-implicit Profile", seriesProfileDto.Name); } [Fact] @@ -89,11 +84,10 @@ public class ReadingProfileServiceTest: AbstractDbTest await ResetDb(); var (rps, user, _, _) = await Setup(); - var profile = new AppUserReadingProfileBuilder(user.Id).Build(); - Context.AppUserReadingProfile.Add(profile); - await UnitOfWork.CommitAsync(); - - user.UserPreferences.DefaultReadingProfileId = profile.Id; + var profile = new AppUserReadingProfileBuilder(user.Id) + .WithKind(ReadingProfileKind.Default) + .Build(); + Context.AppUserReadingProfiles.Add(profile); await UnitOfWork.CommitAsync(); await Assert.ThrowsAsync(async () => @@ -102,13 +96,13 @@ public class ReadingProfileServiceTest: AbstractDbTest }); var profile2 = new AppUserReadingProfileBuilder(user.Id).Build(); - Context.AppUserReadingProfile.Add(profile2); + Context.AppUserReadingProfiles.Add(profile2); await UnitOfWork.CommitAsync(); await rps.DeleteReadingProfile(user.Id, profile2.Id); await UnitOfWork.CommitAsync(); - var allProfiles = await Context.AppUserReadingProfile.ToListAsync(); + var allProfiles = await Context.AppUserReadingProfiles.ToListAsync(); Assert.Single(allProfiles); } @@ -127,14 +121,14 @@ public class ReadingProfileServiceTest: AbstractDbTest await rps.UpdateImplicitReadingProfile(user.Id, series.Id, dto); - var profile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(user.Id, series.Id); + var profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); Assert.NotNull(profile); - Assert.Contains(profile.Series, s => s.SeriesId == series.Id); - Assert.True(profile.Implicit); + Assert.Contains(profile.SeriesIds, s => s == series.Id); + Assert.Equal(ReadingProfileKind.Implicit, profile.Kind); } [Fact] - public async Task UpdateImplicitReadingProfile_DoesnotCreateNew() + public async Task UpdateImplicitReadingProfile_DoesNotCreateNew() { await ResetDb(); var (rps, user, _, series) = await Setup(); @@ -148,10 +142,10 @@ public class ReadingProfileServiceTest: AbstractDbTest await rps.UpdateImplicitReadingProfile(user.Id, series.Id, dto); - var profile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(user.Id, series.Id); + var profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); Assert.NotNull(profile); - Assert.Contains(profile.Series, s => s.SeriesId == series.Id); - Assert.True(profile.Implicit); + Assert.Contains(profile.SeriesIds, s => s == series.Id); + Assert.Equal(ReadingProfileKind.Implicit, profile.Kind); dto = new UserReadingProfileDto { @@ -159,14 +153,15 @@ public class ReadingProfileServiceTest: AbstractDbTest }; await rps.UpdateImplicitReadingProfile(user.Id, series.Id, dto); - profile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(user.Id, series.Id); + profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); Assert.NotNull(profile); - Assert.Contains(profile.Series, s => s.SeriesId == series.Id); - Assert.True(profile.Implicit); + Assert.Contains(profile.SeriesIds, s => s == series.Id); + Assert.Equal(ReadingProfileKind.Implicit, profile.Kind); Assert.Equal(ReaderMode.LeftRight, profile.ReaderMode); - var implicitCount = await Context.AppUserReadingProfile - .Where(p => p.Implicit).CountAsync(); + var implicitCount = await Context.AppUserReadingProfiles + .Where(p => p.Kind == ReadingProfileKind.Implicit) + .CountAsync(); Assert.Equal(1, implicitCount); } @@ -185,11 +180,12 @@ public class ReadingProfileServiceTest: AbstractDbTest .WithName("Library Specific") .Build(); var profile3 = new AppUserReadingProfileBuilder(user.Id) + .WithKind(ReadingProfileKind.Default) .WithName("Global") .Build(); - Context.AppUserReadingProfile.Add(profile); - Context.AppUserReadingProfile.Add(profile2); - Context.AppUserReadingProfile.Add(profile3); + Context.AppUserReadingProfiles.Add(profile); + Context.AppUserReadingProfiles.Add(profile2); + Context.AppUserReadingProfiles.Add(profile3); var series2 = new SeriesBuilder("Rainbows After Storms").Build(); lib.Series.Add(series2); @@ -201,18 +197,15 @@ public class ReadingProfileServiceTest: AbstractDbTest user.Libraries.Add(lib2); await UnitOfWork.CommitAsync(); - user.UserPreferences.DefaultReadingProfileId = profile3.Id; - await UnitOfWork.CommitAsync(); - - var p = await rps.GetReadingProfileForSeries(user.Id, series.Id); + var p = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); Assert.NotNull(p); Assert.Equal("Series Specific", p.Name); - p = await rps.GetReadingProfileForSeries(user.Id, series2.Id); + p = await rps.GetReadingProfileDtoForSeries(user.Id, series2.Id); Assert.NotNull(p); Assert.Equal("Library Specific", p.Name); - p = await rps.GetReadingProfileForSeries(user.Id, series3.Id); + p = await rps.GetReadingProfileDtoForSeries(user.Id, series3.Id); Assert.NotNull(p); Assert.Equal("Global", p.Name); } @@ -232,16 +225,16 @@ public class ReadingProfileServiceTest: AbstractDbTest .WithName("Profile 2") .Build(); - Context.AppUserReadingProfile.Add(profile1); - Context.AppUserReadingProfile.Add(profile2); + Context.AppUserReadingProfiles.Add(profile1); + Context.AppUserReadingProfiles.Add(profile2); await UnitOfWork.CommitAsync(); - var profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); + var profile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); Assert.NotNull(profile); Assert.Equal("Profile 1", profile.Name); await rps.AddProfileToSeries(user.Id, profile2.Id, series.Id); - profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); + profile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); Assert.NotNull(profile); Assert.Equal("Profile 2", profile.Name); } @@ -257,12 +250,12 @@ public class ReadingProfileServiceTest: AbstractDbTest .WithName("Profile 1") .Build(); - Context.AppUserReadingProfile.Add(profile1); + Context.AppUserReadingProfiles.Add(profile1); await UnitOfWork.CommitAsync(); await rps.ClearSeriesProfile(user.Id, series.Id); - var profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); - Assert.Null(profile); + var profiles = await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id); + Assert.DoesNotContain(profiles, rp => rp.SeriesIds.Contains(series.Id)); } @@ -282,13 +275,13 @@ public class ReadingProfileServiceTest: AbstractDbTest .WithSeries(series) .WithName("Profile") .Build(); - Context.AppUserReadingProfile.Add(profile); + Context.AppUserReadingProfiles.Add(profile); var profile2 = new AppUserReadingProfileBuilder(user.Id) .WithSeries(series) .WithName("Profile2") .Build(); - Context.AppUserReadingProfile.Add(profile2); + Context.AppUserReadingProfiles.Add(profile2); await UnitOfWork.CommitAsync(); @@ -297,7 +290,7 @@ public class ReadingProfileServiceTest: AbstractDbTest foreach (var id in someSeriesIds) { - var foundProfile = await rps.GetReadingProfileForSeries(user.Id, id); + var foundProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); Assert.NotNull(foundProfile); Assert.Equal(profile.Id, foundProfile.Id); } @@ -307,7 +300,7 @@ public class ReadingProfileServiceTest: AbstractDbTest foreach (var id in allIds) { - var foundProfile = await rps.GetReadingProfileForSeries(user.Id, id); + var foundProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); Assert.NotNull(foundProfile); Assert.Equal(profile2.Id, foundProfile.Id); } @@ -327,26 +320,27 @@ public class ReadingProfileServiceTest: AbstractDbTest var profile = new AppUserReadingProfileBuilder(user.Id) .WithName("Profile 1") .Build(); - Context.AppUserReadingProfile.Add(profile); + Context.AppUserReadingProfiles.Add(profile); await UnitOfWork.CommitAsync(); await rps.AddProfileToSeries(user.Id, profile.Id, series.Id); await rps.UpdateImplicitReadingProfile(user.Id, series.Id, implicitProfile); - var seriesProfile = await rps.GetReadingProfileForSeries(user.Id, series.Id); + var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); Assert.NotNull(seriesProfile); - Assert.True(seriesProfile.Implicit); + Assert.Equal(ReadingProfileKind.Implicit, seriesProfile.Kind); var profileDto = Mapper.Map(profile); await rps.UpdateReadingProfile(user.Id, profileDto); - seriesProfile = await rps.GetReadingProfileForSeries(user.Id, series.Id); + seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); Assert.NotNull(seriesProfile); - Assert.False(seriesProfile.Implicit); + Assert.Equal(ReadingProfileKind.User, seriesProfile.Kind); - var implicitCount = await Context.AppUserReadingProfile - .Where(p => p.Implicit).CountAsync(); + var implicitCount = await Context.AppUserReadingProfiles + .Where(p => p.Kind == ReadingProfileKind.Implicit) + .CountAsync(); Assert.Equal(0, implicitCount); } @@ -362,7 +356,7 @@ public class ReadingProfileServiceTest: AbstractDbTest var profile = new AppUserReadingProfileBuilder(user.Id) .WithName("Profile 1") .Build(); - Context.AppUserReadingProfile.Add(profile); + Context.AppUserReadingProfiles.Add(profile); for (var i = 0; i < 10; i++) { @@ -376,22 +370,23 @@ public class ReadingProfileServiceTest: AbstractDbTest foreach (var id in ids) { await rps.UpdateImplicitReadingProfile(user.Id, id, implicitProfile); - var seriesProfile = await rps.GetReadingProfileForSeries(user.Id, id); + var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); Assert.NotNull(seriesProfile); - Assert.True(seriesProfile.Implicit); + Assert.Equal(ReadingProfileKind.Implicit, seriesProfile.Kind); } await rps.BulkAddProfileToSeries(user.Id, profile.Id, ids); foreach (var id in ids) { - var seriesProfile = await rps.GetReadingProfileForSeries(user.Id, id); + var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); Assert.NotNull(seriesProfile); - Assert.False(seriesProfile.Implicit); + Assert.Equal(ReadingProfileKind.User, seriesProfile.Kind); } - var implicitCount = await Context.AppUserReadingProfile - .Where(p => p.Implicit).CountAsync(); + var implicitCount = await Context.AppUserReadingProfiles + .Where(p => p.Kind == ReadingProfileKind.Implicit) + .CountAsync(); Assert.Equal(0, implicitCount); } @@ -402,55 +397,33 @@ public class ReadingProfileServiceTest: AbstractDbTest var (rps, user, lib, series) = await Setup(); var implicitProfile = Mapper.Map(new AppUserReadingProfileBuilder(user.Id) + .WithKind(ReadingProfileKind.Implicit) .Build()); var profile = new AppUserReadingProfileBuilder(user.Id) .WithName("Profile 1") .Build(); - Context.AppUserReadingProfile.Add(profile); + Context.AppUserReadingProfiles.Add(profile); await UnitOfWork.CommitAsync(); await rps.UpdateImplicitReadingProfile(user.Id, series.Id, implicitProfile); - var seriesProfile = await rps.GetReadingProfileForSeries(user.Id, series.Id); + var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); Assert.NotNull(seriesProfile); - Assert.True(seriesProfile.Implicit); + Assert.Equal(ReadingProfileKind.Implicit, seriesProfile.Kind); await rps.AddProfileToSeries(user.Id, profile.Id, series.Id); - seriesProfile = await rps.GetReadingProfileForSeries(user.Id, series.Id); + seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); Assert.NotNull(seriesProfile); - Assert.False(seriesProfile.Implicit); + Assert.Equal(ReadingProfileKind.User, seriesProfile.Kind); - var implicitCount = await Context.AppUserReadingProfile - .Where(p => p.Implicit).CountAsync(); + var implicitCount = await Context.AppUserReadingProfiles + .Where(p => p.Kind == ReadingProfileKind.Implicit) + .CountAsync(); Assert.Equal(0, implicitCount); } - [Fact] - public async Task SetDefault() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Profile 1") - .Build(); - - Context.AppUserReadingProfile.Add(profile); - await UnitOfWork.CommitAsync(); - - await rps.SetDefaultReadingProfile(user.Id, profile.Id); - - var newSeries = new SeriesBuilder("New Series").Build(); - lib.Series.Add(newSeries); - await UnitOfWork.CommitAsync(); - - var seriesProfile = await rps.GetReadingProfileForSeries(user.Id, series.Id); - Assert.NotNull(seriesProfile); - Assert.Equal(profile.Id, seriesProfile.Id); - } - [Fact] public async Task CreateReadingProfile() { @@ -487,7 +460,7 @@ public class ReadingProfileServiceTest: AbstractDbTest await rps.CreateReadingProfile(user.Id, dto3); }); - var allProfiles = Context.AppUserReadingProfile.ToList(); + var allProfiles = Context.AppUserReadingProfiles.ToList(); Assert.Equal(2, allProfiles.Count); } @@ -499,32 +472,31 @@ public class ReadingProfileServiceTest: AbstractDbTest var implicitProfile = new AppUserReadingProfileBuilder(user.Id) .WithSeries(series) - .WithImplicit(true) + .WithKind(ReadingProfileKind.Implicit) .WithName("Implicit Profile") .Build(); var explicitProfile = new AppUserReadingProfileBuilder(user.Id) .WithSeries(series) - .WithImplicit(false) .WithName("Explicit Profile") .Build(); - Context.AppUserReadingProfile.Add(implicitProfile); - Context.AppUserReadingProfile.Add(explicitProfile); + Context.AppUserReadingProfiles.Add(implicitProfile); + Context.AppUserReadingProfiles.Add(explicitProfile); await UnitOfWork.CommitAsync(); - var allBefore = await UnitOfWork.AppUserReadingProfileRepository.GetAllProfilesForSeries(user.Id, series.Id, ReadingProfileIncludes.Series); - Assert.Equal(2, allBefore.Count); + var allBefore = await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id); + Assert.Equal(2, allBefore.Count(rp => rp.SeriesIds.Contains(series.Id))); await rps.ClearSeriesProfile(user.Id, series.Id); - var remainingProfiles = await Context.AppUserReadingProfile.Includes(ReadingProfileIncludes.Series).ToListAsync(); + var remainingProfiles = await Context.AppUserReadingProfiles.ToListAsync(); Assert.Single(remainingProfiles); Assert.Equal("Explicit Profile", remainingProfiles[0].Name); - Assert.Empty(remainingProfiles[0].Series); + Assert.Empty(remainingProfiles[0].SeriesIds); - var profilesForSeries = await UnitOfWork.AppUserReadingProfileRepository.GetAllProfilesForSeries(user.Id, series.Id, ReadingProfileIncludes.Series); - Assert.Empty(profilesForSeries); + var profilesForSeries = await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id); + Assert.DoesNotContain(profilesForSeries, rp => rp.SeriesIds.Contains(series.Id)); } [Fact] @@ -536,26 +508,28 @@ public class ReadingProfileServiceTest: AbstractDbTest var profile = new AppUserReadingProfileBuilder(user.Id) .WithName("Library Profile") .Build(); - Context.AppUserReadingProfile.Add(profile); + Context.AppUserReadingProfiles.Add(profile); await UnitOfWork.CommitAsync(); await rps.AddProfileToLibrary(user.Id, profile.Id, lib.Id); await UnitOfWork.CommitAsync(); - var linkedProfile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForLibrary(user.Id, lib.Id, ReadingProfileIncludes.Library); + var linkedProfile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) + .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); Assert.NotNull(linkedProfile); Assert.Equal(profile.Id, linkedProfile.Id); var newProfile = new AppUserReadingProfileBuilder(user.Id) .WithName("New Profile") .Build(); - Context.AppUserReadingProfile.Add(newProfile); + Context.AppUserReadingProfiles.Add(newProfile); await UnitOfWork.CommitAsync(); await rps.AddProfileToLibrary(user.Id, newProfile.Id, lib.Id); await UnitOfWork.CommitAsync(); - linkedProfile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForLibrary(user.Id, lib.Id, ReadingProfileIncludes.Library); + linkedProfile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) + .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); Assert.NotNull(linkedProfile); Assert.Equal(newProfile.Id, linkedProfile.Id); } @@ -567,27 +541,29 @@ public class ReadingProfileServiceTest: AbstractDbTest var (rps, user, lib, _) = await Setup(); var implicitProfile = new AppUserReadingProfileBuilder(user.Id) - .WithImplicit(true) + .WithKind(ReadingProfileKind.Implicit) .WithLibrary(lib) .Build(); - Context.AppUserReadingProfile.Add(implicitProfile); + Context.AppUserReadingProfiles.Add(implicitProfile); await UnitOfWork.CommitAsync(); await rps.ClearLibraryProfile(user.Id, lib.Id); - var profile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForLibrary(user.Id, lib.Id, ReadingProfileIncludes.Library); + var profile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) + .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); Assert.Null(profile); var explicitProfile = new AppUserReadingProfileBuilder(user.Id) .WithLibrary(lib) .Build(); - Context.AppUserReadingProfile.Add(explicitProfile); + Context.AppUserReadingProfiles.Add(explicitProfile); await UnitOfWork.CommitAsync(); await rps.ClearLibraryProfile(user.Id, lib.Id); - profile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForLibrary(user.Id, lib.Id, ReadingProfileIncludes.Library); + profile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) + .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); Assert.Null(profile); - var stillExists = await Context.AppUserReadingProfile.FindAsync(explicitProfile.Id); + var stillExists = await Context.AppUserReadingProfiles.FindAsync(explicitProfile.Id); Assert.NotNull(stillExists); } @@ -609,14 +585,14 @@ public class ReadingProfileServiceTest: AbstractDbTest var newDto = Mapper.Map(profile); Assert.True(RandfHelper.AreSimpleFieldsEqual(dto, newDto, - ["k__BackingField", "k__BackingField", "k__BackingField"])); + ["k__BackingField", "k__BackingField", "k__BackingField"])); } protected override async Task ResetDb() { - Context.AppUserReadingProfile.RemoveRange(Context.AppUserReadingProfile); + Context.AppUserReadingProfiles.RemoveRange(Context.AppUserReadingProfiles); await UnitOfWork.CommitAsync(); } } diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 2828bf9a1..d8b9164af 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -612,7 +612,7 @@ public class AccountController : BaseApiController } /// - /// Requests the Invite Url for the UserId. Will return error if user is already validated. + /// Requests the Invite Url for the AppUserId. Will return error if user is already validated. /// /// /// Include the "https://ip:port/" in the generated link @@ -789,12 +789,10 @@ public class AccountController : BaseApiController { var profile = new AppUserReadingProfileBuilder(user.Id) .WithName("Default Profile") + .WithKind(ReadingProfileKind.Default) .Build(); _unitOfWork.AppUserReadingProfileRepository.Add(profile); await _unitOfWork.CommitAsync(); - - user.UserPreferences.ReadingProfiles.Add(profile); - user.UserPreferences.DefaultReadingProfileId = profile.Id; } /// diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs index c7f48cf54..f39462bbf 100644 --- a/API/Controllers/PluginController.cs +++ b/API/Controllers/PluginController.cs @@ -45,7 +45,7 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService throw new KavitaUnauthenticatedUserException(); } var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); - logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId); + logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({AppUserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId); return new UserDto { diff --git a/API/Controllers/ReadingProfileController.cs b/API/Controllers/ReadingProfileController.cs index 07cacd2b3..da6232836 100644 --- a/API/Controllers/ReadingProfileController.cs +++ b/API/Controllers/ReadingProfileController.cs @@ -26,10 +26,7 @@ public class ReadingProfileController(ILogger logger, [HttpGet("all")] public async Task>> GetAllReadingProfiles() { - var profiles = await unitOfWork.AppUserReadingProfileRepository - .GetProfilesDtoForUser(User.GetUserId(), true, - ReadingProfileIncludes.Series | ReadingProfileIncludes.Library); - + var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesDtoForUser(User.GetUserId()); return Ok(profiles); } @@ -42,7 +39,7 @@ public class ReadingProfileController(ILogger logger, [HttpGet("{seriesId}")] public async Task> GetProfileForSeries(int seriesId) { - return Ok(await readingProfileService.GetReadingProfileForSeries(User.GetUserId(), seriesId)); + return Ok(await readingProfileService.GetReadingProfileDtoForSeries(User.GetUserId(), seriesId)); } /// @@ -85,20 +82,6 @@ public class ReadingProfileController(ILogger logger, return Ok(); } - /// - /// Sets the given profile as the global default - /// - /// - /// - /// - /// - [HttpPost("set-default")] - public async Task SetDefault([FromQuery] int profileId) - { - await readingProfileService.SetDefaultReadingProfile(User.GetUserId(), profileId); - return Ok(); - } - /// /// Deletes the given profile, requires the profile to belong to the logged-in user /// diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index dd66353fc..46f42306e 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -9,8 +9,6 @@ namespace API.DTOs; public sealed record UserPreferencesDto { - /// - public int DefaultReadingProfileId { get; init; } /// /// UI Site Global Setting: The UI theme the user should use. diff --git a/API/DTOs/UserReadingProfileDto.cs b/API/DTOs/UserReadingProfileDto.cs index c9745ea72..23f67ce4d 100644 --- a/API/DTOs/UserReadingProfileDto.cs +++ b/API/DTOs/UserReadingProfileDto.cs @@ -10,13 +10,10 @@ public sealed record UserReadingProfileDto { public int Id { get; set; } - public int UserId { get; init; } public string Name { get; init; } - - /// - public bool Implicit { get; set; } = false; + public ReadingProfileKind Kind { get; init; } #region MangaReader @@ -129,12 +126,4 @@ public sealed record UserReadingProfileDto #endregion - #region Relations - - public IList SeriesIds { get; set; } = []; - - public IList LibraryIds { get; set; } = []; - - #endregion - } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 5065c6116..3bbf45e23 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -81,9 +81,7 @@ public sealed class DataContext : IdentityDbContext MetadataSettings { get; set; } = null!; public DbSet MetadataFieldMapping { get; set; } = null!; public DbSet AppUserChapterRating { get; set; } = null!; - public DbSet AppUserReadingProfile { get; set; } = null!; - public DbSet SeriesReadingProfile { get; set; } = null!; - public DbSet LibraryReadingProfile { get; set; } = null!; + public DbSet AppUserReadingProfiles { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) { @@ -272,6 +270,19 @@ public sealed class DataContext : IdentityDbContext() .Property(b => b.AllowAutomaticWebtoonReaderDetection) .HasDefaultValue(true); + + builder.Entity() + .Property(rp => rp.LibraryIds) + .HasConversion( + v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), + v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) + .HasColumnType("TEXT"); + builder.Entity() + .Property(rp => rp.SeriesIds) + .HasConversion( + v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), + v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) + .HasColumnType("TEXT"); } #nullable enable diff --git a/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs b/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs index 37eed970a..b2afde98a 100644 --- a/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs +++ b/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using API.Entities; +using API.Entities.Enums; using API.Entities.History; using API.Extensions; using API.Helpers.Builders; @@ -23,7 +24,7 @@ public static class ManualMigrateReadingProfiles var users = await context.AppUser .Include(u => u.UserPreferences) - .Include(u => u.UserPreferences.ReadingProfiles) + .Include(u => u.ReadingProfiles) .ToListAsync(); foreach (var user in users) @@ -32,16 +33,19 @@ public static class ManualMigrateReadingProfiles { Name = "Default", NormalizedName = "Default".ToNormalized(), + Kind = ReadingProfileKind.Default, + LibraryIds = [], + SeriesIds = [], BackgroundColor = user.UserPreferences.BackgroundColor, EmulateBook = user.UserPreferences.EmulateBook, - User = user, + AppUser = user, PdfTheme = user.UserPreferences.PdfTheme, ReaderMode = user.UserPreferences.ReaderMode, ReadingDirection = user.UserPreferences.ReadingDirection, ScalingOption = user.UserPreferences.ScalingOption, LayoutMode = user.UserPreferences.LayoutMode, WidthOverride = null, - UserId = user.Id, + AppUserId = user.Id, AutoCloseMenu = user.UserPreferences.AutoCloseMenu, BookReaderMargin = user.UserPreferences.BookReaderMargin, PageSplitOption = user.UserPreferences.PageSplitOption, @@ -60,16 +64,10 @@ public static class ManualMigrateReadingProfiles BookReaderTapToPaginate = user.UserPreferences.BookReaderTapToPaginate, ShowScreenHints = user.UserPreferences.ShowScreenHints, }; - user.UserPreferences.ReadingProfiles.Add(readingProfile); + user.ReadingProfiles.Add(readingProfile); } await context.SaveChangesAsync(); - foreach (var user in users) - { - user.UserPreferences.DefaultReadingProfileId = - (await context.AppUserReadingProfile - .FirstAsync(rp => rp.UserId == user.Id)).Id; - } context.ManualMigrationHistory.Add(new ManualMigrationHistory { diff --git a/API/Data/Migrations/20250519113715_AppUserReadingProfiles.cs b/API/Data/Migrations/20250519113715_AppUserReadingProfiles.cs deleted file mode 100644 index a553d3ac4..000000000 --- a/API/Data/Migrations/20250519113715_AppUserReadingProfiles.cs +++ /dev/null @@ -1,198 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class AppUserReadingProfiles : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "DefaultReadingProfileId", - table: "AppUserPreferences", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.CreateTable( - name: "AppUserReadingProfile", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", nullable: true), - NormalizedName = table.Column(type: "TEXT", nullable: true), - UserId = table.Column(type: "INTEGER", nullable: false), - ReadingDirection = table.Column(type: "INTEGER", nullable: false), - ScalingOption = table.Column(type: "INTEGER", nullable: false), - PageSplitOption = table.Column(type: "INTEGER", nullable: false), - ReaderMode = table.Column(type: "INTEGER", nullable: false), - AutoCloseMenu = table.Column(type: "INTEGER", nullable: false), - ShowScreenHints = table.Column(type: "INTEGER", nullable: false), - EmulateBook = table.Column(type: "INTEGER", nullable: false), - LayoutMode = table.Column(type: "INTEGER", nullable: false), - BackgroundColor = table.Column(type: "TEXT", nullable: true, defaultValue: "#000000"), - SwipeToPaginate = table.Column(type: "INTEGER", nullable: false), - AllowAutomaticWebtoonReaderDetection = table.Column(type: "INTEGER", nullable: false, defaultValue: true), - WidthOverride = table.Column(type: "INTEGER", nullable: true), - BookReaderMargin = table.Column(type: "INTEGER", nullable: false), - BookReaderLineSpacing = table.Column(type: "INTEGER", nullable: false), - BookReaderFontSize = table.Column(type: "INTEGER", nullable: false), - BookReaderFontFamily = table.Column(type: "TEXT", nullable: true), - BookReaderTapToPaginate = table.Column(type: "INTEGER", nullable: false), - BookReaderReadingDirection = table.Column(type: "INTEGER", nullable: false), - BookReaderWritingStyle = table.Column(type: "INTEGER", nullable: false, defaultValue: 0), - BookThemeName = table.Column(type: "TEXT", nullable: true, defaultValue: "Dark"), - BookReaderLayoutMode = table.Column(type: "INTEGER", nullable: false), - BookReaderImmersiveMode = table.Column(type: "INTEGER", nullable: false), - PdfTheme = table.Column(type: "INTEGER", nullable: false), - PdfScrollMode = table.Column(type: "INTEGER", nullable: false), - PdfSpreadMode = table.Column(type: "INTEGER", nullable: false), - Implicit = table.Column(type: "INTEGER", nullable: false), - AppUserPreferencesId = table.Column(type: "INTEGER", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AppUserReadingProfile", x => x.Id); - table.ForeignKey( - name: "FK_AppUserReadingProfile_AppUserPreferences_AppUserPreferencesId", - column: x => x.AppUserPreferencesId, - principalTable: "AppUserPreferences", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_AppUserReadingProfile_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "LibraryReadingProfile", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - AppUserId = table.Column(type: "INTEGER", nullable: false), - LibraryId = table.Column(type: "INTEGER", nullable: false), - ReadingProfileId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_LibraryReadingProfile", x => x.Id); - table.ForeignKey( - name: "FK_LibraryReadingProfile_AppUserReadingProfile_ReadingProfileId", - column: x => x.ReadingProfileId, - principalTable: "AppUserReadingProfile", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_LibraryReadingProfile_AspNetUsers_AppUserId", - column: x => x.AppUserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_LibraryReadingProfile_Library_LibraryId", - column: x => x.LibraryId, - principalTable: "Library", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "SeriesReadingProfile", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - AppUserId = table.Column(type: "INTEGER", nullable: false), - SeriesId = table.Column(type: "INTEGER", nullable: false), - ReadingProfileId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SeriesReadingProfile", x => x.Id); - table.ForeignKey( - name: "FK_SeriesReadingProfile_AppUserReadingProfile_ReadingProfileId", - column: x => x.ReadingProfileId, - principalTable: "AppUserReadingProfile", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_SeriesReadingProfile_AspNetUsers_AppUserId", - column: x => x.AppUserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_SeriesReadingProfile_Series_SeriesId", - column: x => x.SeriesId, - principalTable: "Series", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AppUserReadingProfile_AppUserPreferencesId", - table: "AppUserReadingProfile", - column: "AppUserPreferencesId"); - - migrationBuilder.CreateIndex( - name: "IX_AppUserReadingProfile_UserId", - table: "AppUserReadingProfile", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_LibraryReadingProfile_AppUserId", - table: "LibraryReadingProfile", - column: "AppUserId"); - - migrationBuilder.CreateIndex( - name: "IX_LibraryReadingProfile_LibraryId_AppUserId", - table: "LibraryReadingProfile", - columns: new[] { "LibraryId", "AppUserId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_LibraryReadingProfile_ReadingProfileId", - table: "LibraryReadingProfile", - column: "ReadingProfileId"); - - migrationBuilder.CreateIndex( - name: "IX_SeriesReadingProfile_AppUserId", - table: "SeriesReadingProfile", - column: "AppUserId"); - - migrationBuilder.CreateIndex( - name: "IX_SeriesReadingProfile_ReadingProfileId", - table: "SeriesReadingProfile", - column: "ReadingProfileId"); - - migrationBuilder.CreateIndex( - name: "IX_SeriesReadingProfile_SeriesId", - table: "SeriesReadingProfile", - column: "SeriesId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "LibraryReadingProfile"); - - migrationBuilder.DropTable( - name: "SeriesReadingProfile"); - - migrationBuilder.DropTable( - name: "AppUserReadingProfile"); - - migrationBuilder.DropColumn( - name: "DefaultReadingProfileId", - table: "AppUserPreferences"); - } - } -} diff --git a/API/Data/Migrations/20250519113715_AppUserReadingProfiles.Designer.cs b/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs similarity index 96% rename from API/Data/Migrations/20250519113715_AppUserReadingProfiles.Designer.cs rename to API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs index 3104b4ce0..762eae142 100644 --- a/API/Data/Migrations/20250519113715_AppUserReadingProfiles.Designer.cs +++ b/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs @@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace API.Data.Migrations { [DbContext(typeof(DataContext))] - [Migration("20250519113715_AppUserReadingProfiles")] - partial class AppUserReadingProfiles + [Migration("20250601200056_ReadingProfiles")] + partial class ReadingProfiles { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -458,9 +458,6 @@ namespace API.Data.Migrations b.Property("CollapseSeriesRelationships") .HasColumnType("INTEGER"); - b.Property("DefaultReadingProfileId") - .HasColumnType("INTEGER"); - b.Property("EmulateBook") .HasColumnType("INTEGER"); @@ -626,7 +623,7 @@ namespace API.Data.Migrations .HasColumnType("INTEGER") .HasDefaultValue(true); - b.Property("AppUserPreferencesId") + b.Property("AppUserId") .HasColumnType("INTEGER"); b.Property("AutoCloseMenu") @@ -674,12 +671,15 @@ namespace API.Data.Migrations b.Property("EmulateBook") .HasColumnType("INTEGER"); - b.Property("Implicit") + b.Property("Kind") .HasColumnType("INTEGER"); b.Property("LayoutMode") .HasColumnType("INTEGER"); + b.Property("LibraryIds") + .HasColumnType("TEXT"); + b.Property("Name") .HasColumnType("TEXT"); @@ -707,25 +707,23 @@ namespace API.Data.Migrations b.Property("ScalingOption") .HasColumnType("INTEGER"); + b.PrimitiveCollection("SeriesIds") + .HasColumnType("TEXT"); + b.Property("ShowScreenHints") .HasColumnType("INTEGER"); b.Property("SwipeToPaginate") .HasColumnType("INTEGER"); - b.Property("UserId") - .HasColumnType("INTEGER"); - b.Property("WidthOverride") .HasColumnType("INTEGER"); b.HasKey("Id"); - b.HasIndex("AppUserPreferencesId"); + b.HasIndex("AppUserId"); - b.HasIndex("UserId"); - - b.ToTable("AppUserReadingProfile"); + b.ToTable("AppUserReadingProfiles"); }); modelBuilder.Entity("API.Entities.AppUserRole", b => @@ -1380,33 +1378,6 @@ namespace API.Data.Migrations b.ToTable("LibraryFileTypeGroup"); }); - modelBuilder.Entity("API.Entities.LibraryReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ReadingProfileId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ReadingProfileId"); - - b.HasIndex("LibraryId", "AppUserId") - .IsUnique(); - - b.ToTable("LibraryReadingProfile"); - }); - modelBuilder.Entity("API.Entities.MangaFile", b => { b.Property("Id") @@ -2394,32 +2365,6 @@ namespace API.Data.Migrations b.ToTable("Series"); }); - modelBuilder.Entity("API.Entities.SeriesReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ReadingProfileId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ReadingProfileId"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesReadingProfile"); - }); - modelBuilder.Entity("API.Entities.ServerSetting", b => { b.Property("Key") @@ -3012,17 +2957,13 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => { - b.HasOne("API.Entities.AppUserPreferences", null) + b.HasOne("API.Entities.AppUser", "AppUser") .WithMany("ReadingProfiles") - .HasForeignKey("AppUserPreferencesId"); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany() - .HasForeignKey("UserId") + .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("User"); + b.Navigation("AppUser"); }); modelBuilder.Entity("API.Entities.AppUserRole", b => @@ -3184,33 +3125,6 @@ namespace API.Data.Migrations b.Navigation("Library"); }); - modelBuilder.Entity("API.Entities.LibraryReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany("ReadingProfiles") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserReadingProfile", "ReadingProfile") - .WithMany("Libraries") - .HasForeignKey("ReadingProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("ReadingProfile"); - }); - modelBuilder.Entity("API.Entities.MangaFile", b => { b.HasOne("API.Entities.Chapter", "Chapter") @@ -3468,33 +3382,6 @@ namespace API.Data.Migrations b.Navigation("Library"); }); - modelBuilder.Entity("API.Entities.SeriesReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserReadingProfile", "ReadingProfile") - .WithMany("Series") - .HasForeignKey("ReadingProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("ReadingProfiles") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("ReadingProfile"); - - b.Navigation("Series"); - }); - modelBuilder.Entity("API.Entities.Volume", b => { b.HasOne("API.Entities.Series", "Series") @@ -3717,6 +3604,8 @@ namespace API.Data.Migrations b.Navigation("ReadingLists"); + b.Navigation("ReadingProfiles"); + b.Navigation("ScrobbleHolds"); b.Navigation("SideNavStreams"); @@ -3732,18 +3621,6 @@ namespace API.Data.Migrations b.Navigation("WantToRead"); }); - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Navigation("ReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Navigation("Libraries"); - - b.Navigation("Series"); - }); - modelBuilder.Entity("API.Entities.Chapter", b => { b.Navigation("ExternalRatings"); @@ -3767,8 +3644,6 @@ namespace API.Data.Migrations b.Navigation("LibraryFileTypes"); - b.Navigation("ReadingProfiles"); - b.Navigation("Series"); }); @@ -3806,8 +3681,6 @@ namespace API.Data.Migrations b.Navigation("Ratings"); - b.Navigation("ReadingProfiles"); - b.Navigation("RelationOf"); b.Navigation("Relations"); diff --git a/API/Data/Migrations/20250601200056_ReadingProfiles.cs b/API/Data/Migrations/20250601200056_ReadingProfiles.cs new file mode 100644 index 000000000..66b9e53e5 --- /dev/null +++ b/API/Data/Migrations/20250601200056_ReadingProfiles.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class ReadingProfiles : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AppUserReadingProfiles", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: true), + NormalizedName = table.Column(type: "TEXT", nullable: true), + AppUserId = table.Column(type: "INTEGER", nullable: false), + Kind = table.Column(type: "INTEGER", nullable: false), + LibraryIds = table.Column(type: "TEXT", nullable: true), + SeriesIds = table.Column(type: "TEXT", nullable: true), + ReadingDirection = table.Column(type: "INTEGER", nullable: false), + ScalingOption = table.Column(type: "INTEGER", nullable: false), + PageSplitOption = table.Column(type: "INTEGER", nullable: false), + ReaderMode = table.Column(type: "INTEGER", nullable: false), + AutoCloseMenu = table.Column(type: "INTEGER", nullable: false), + ShowScreenHints = table.Column(type: "INTEGER", nullable: false), + EmulateBook = table.Column(type: "INTEGER", nullable: false), + LayoutMode = table.Column(type: "INTEGER", nullable: false), + BackgroundColor = table.Column(type: "TEXT", nullable: true, defaultValue: "#000000"), + SwipeToPaginate = table.Column(type: "INTEGER", nullable: false), + AllowAutomaticWebtoonReaderDetection = table.Column(type: "INTEGER", nullable: false, defaultValue: true), + WidthOverride = table.Column(type: "INTEGER", nullable: true), + BookReaderMargin = table.Column(type: "INTEGER", nullable: false), + BookReaderLineSpacing = table.Column(type: "INTEGER", nullable: false), + BookReaderFontSize = table.Column(type: "INTEGER", nullable: false), + BookReaderFontFamily = table.Column(type: "TEXT", nullable: true), + BookReaderTapToPaginate = table.Column(type: "INTEGER", nullable: false), + BookReaderReadingDirection = table.Column(type: "INTEGER", nullable: false), + BookReaderWritingStyle = table.Column(type: "INTEGER", nullable: false, defaultValue: 0), + BookThemeName = table.Column(type: "TEXT", nullable: true, defaultValue: "Dark"), + BookReaderLayoutMode = table.Column(type: "INTEGER", nullable: false), + BookReaderImmersiveMode = table.Column(type: "INTEGER", nullable: false), + PdfTheme = table.Column(type: "INTEGER", nullable: false), + PdfScrollMode = table.Column(type: "INTEGER", nullable: false), + PdfSpreadMode = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AppUserReadingProfiles", x => x.Id); + table.ForeignKey( + name: "FK_AppUserReadingProfiles_AspNetUsers_AppUserId", + column: x => x.AppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppUserReadingProfiles_AppUserId", + table: "AppUserReadingProfiles", + column: "AppUserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppUserReadingProfiles"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 517a390a8..25db64e2a 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -455,9 +455,6 @@ namespace API.Data.Migrations b.Property("CollapseSeriesRelationships") .HasColumnType("INTEGER"); - b.Property("DefaultReadingProfileId") - .HasColumnType("INTEGER"); - b.Property("EmulateBook") .HasColumnType("INTEGER"); @@ -623,7 +620,7 @@ namespace API.Data.Migrations .HasColumnType("INTEGER") .HasDefaultValue(true); - b.Property("AppUserPreferencesId") + b.Property("AppUserId") .HasColumnType("INTEGER"); b.Property("AutoCloseMenu") @@ -671,12 +668,15 @@ namespace API.Data.Migrations b.Property("EmulateBook") .HasColumnType("INTEGER"); - b.Property("Implicit") + b.Property("Kind") .HasColumnType("INTEGER"); b.Property("LayoutMode") .HasColumnType("INTEGER"); + b.Property("LibraryIds") + .HasColumnType("TEXT"); + b.Property("Name") .HasColumnType("TEXT"); @@ -704,25 +704,23 @@ namespace API.Data.Migrations b.Property("ScalingOption") .HasColumnType("INTEGER"); + b.PrimitiveCollection("SeriesIds") + .HasColumnType("TEXT"); + b.Property("ShowScreenHints") .HasColumnType("INTEGER"); b.Property("SwipeToPaginate") .HasColumnType("INTEGER"); - b.Property("UserId") - .HasColumnType("INTEGER"); - b.Property("WidthOverride") .HasColumnType("INTEGER"); b.HasKey("Id"); - b.HasIndex("AppUserPreferencesId"); + b.HasIndex("AppUserId"); - b.HasIndex("UserId"); - - b.ToTable("AppUserReadingProfile"); + b.ToTable("AppUserReadingProfiles"); }); modelBuilder.Entity("API.Entities.AppUserRole", b => @@ -1377,33 +1375,6 @@ namespace API.Data.Migrations b.ToTable("LibraryFileTypeGroup"); }); - modelBuilder.Entity("API.Entities.LibraryReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ReadingProfileId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ReadingProfileId"); - - b.HasIndex("LibraryId", "AppUserId") - .IsUnique(); - - b.ToTable("LibraryReadingProfile"); - }); - modelBuilder.Entity("API.Entities.MangaFile", b => { b.Property("Id") @@ -2391,32 +2362,6 @@ namespace API.Data.Migrations b.ToTable("Series"); }); - modelBuilder.Entity("API.Entities.SeriesReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ReadingProfileId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ReadingProfileId"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesReadingProfile"); - }); - modelBuilder.Entity("API.Entities.ServerSetting", b => { b.Property("Key") @@ -3009,17 +2954,13 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => { - b.HasOne("API.Entities.AppUserPreferences", null) + b.HasOne("API.Entities.AppUser", "AppUser") .WithMany("ReadingProfiles") - .HasForeignKey("AppUserPreferencesId"); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany() - .HasForeignKey("UserId") + .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("User"); + b.Navigation("AppUser"); }); modelBuilder.Entity("API.Entities.AppUserRole", b => @@ -3181,33 +3122,6 @@ namespace API.Data.Migrations b.Navigation("Library"); }); - modelBuilder.Entity("API.Entities.LibraryReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany("ReadingProfiles") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserReadingProfile", "ReadingProfile") - .WithMany("Libraries") - .HasForeignKey("ReadingProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("ReadingProfile"); - }); - modelBuilder.Entity("API.Entities.MangaFile", b => { b.HasOne("API.Entities.Chapter", "Chapter") @@ -3465,33 +3379,6 @@ namespace API.Data.Migrations b.Navigation("Library"); }); - modelBuilder.Entity("API.Entities.SeriesReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserReadingProfile", "ReadingProfile") - .WithMany("Series") - .HasForeignKey("ReadingProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("ReadingProfiles") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("ReadingProfile"); - - b.Navigation("Series"); - }); - modelBuilder.Entity("API.Entities.Volume", b => { b.HasOne("API.Entities.Series", "Series") @@ -3714,6 +3601,8 @@ namespace API.Data.Migrations b.Navigation("ReadingLists"); + b.Navigation("ReadingProfiles"); + b.Navigation("ScrobbleHolds"); b.Navigation("SideNavStreams"); @@ -3729,18 +3618,6 @@ namespace API.Data.Migrations b.Navigation("WantToRead"); }); - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Navigation("ReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Navigation("Libraries"); - - b.Navigation("Series"); - }); - modelBuilder.Entity("API.Entities.Chapter", b => { b.Navigation("ExternalRatings"); @@ -3764,8 +3641,6 @@ namespace API.Data.Migrations b.Navigation("LibraryFileTypes"); - b.Navigation("ReadingProfiles"); - b.Navigation("Series"); }); @@ -3803,8 +3678,6 @@ namespace API.Data.Migrations b.Navigation("Ratings"); - b.Navigation("ReadingProfiles"); - b.Navigation("RelationOf"); b.Navigation("Relations"); diff --git a/API/Data/Repositories/AppUserReadingProfileRepository.cs b/API/Data/Repositories/AppUserReadingProfileRepository.cs index 07aec3c95..46ec49949 100644 --- a/API/Data/Repositories/AppUserReadingProfileRepository.cs +++ b/API/Data/Repositories/AppUserReadingProfileRepository.cs @@ -1,10 +1,10 @@ #nullable enable -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.Entities; +using API.Entities.Enums; using API.Extensions; using API.Extensions.QueryExtensions; using AutoMapper; @@ -13,210 +13,94 @@ using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -[Flags] -public enum ReadingProfileIncludes -{ - None = 0, - Series = 1 << 1, - Library = 1 << 2 -} public interface IAppUserReadingProfileRepository { - Task> GetProfilesForUser(int userId, bool nonImplicitOnly, ReadingProfileIncludes includes = ReadingProfileIncludes.None); - Task> GetProfilesDtoForUser(int userId, bool nonImplicitOnly, ReadingProfileIncludes includes = ReadingProfileIncludes.None); - Task GetProfileForSeries(int userId, int seriesId, ReadingProfileIncludes includes = ReadingProfileIncludes.None); + /// - /// Returns both implicit and "real" reading profiles + /// Return the given profile if it belongs the user /// /// - /// - /// + /// + /// + Task GetUserProfile(int userId, int profileId); + /// + /// Returns all reading profiles for the user + /// + /// + /// + Task> GetProfilesForUser(int userId); + /// + /// Returns all non-implicit reading profiles for the user + /// + /// + /// + Task> GetProfilesDtoForUser(int userId); + /// + /// Find a profile by name, belonging to a specific user + /// + /// + /// /// - Task> GetAllProfilesForSeries(int userId, int seriesId, ReadingProfileIncludes includes = ReadingProfileIncludes.None); - Task> GetProfilesForSeries(int userId, IList seriesIds, bool implicitOnly, ReadingProfileIncludes includes = ReadingProfileIncludes.None); - Task GetProfileDtoForSeries(int userId, int seriesId); - Task GetProfileForLibrary(int userId, int libraryId, ReadingProfileIncludes includes = ReadingProfileIncludes.None); - Task GetProfileDtoForLibrary(int userId, int libraryId); - Task GetProfile(int profileId, ReadingProfileIncludes includes = ReadingProfileIncludes.None); - Task GetProfileDto(int profileId); Task GetProfileByName(int userId, string name); - Task GetSeriesProfile(int userId, int seriesId); - Task> GetSeriesProfilesForSeries(int userId, IList seriesIds); - Task GetLibraryProfile(int userId, int libraryId); void Add(AppUserReadingProfile readingProfile); - void Add(SeriesReadingProfile readingProfile); - void Add(LibraryReadingProfile readingProfile); - void Attach(AppUserReadingProfile readingProfile); void Update(AppUserReadingProfile readingProfile); - void Update(SeriesReadingProfile readingProfile); void Remove(AppUserReadingProfile readingProfile); - void Remove(SeriesReadingProfile readingProfile); void RemoveRange(IEnumerable readingProfiles); } public class AppUserReadingProfileRepository(DataContext context, IMapper mapper): IAppUserReadingProfileRepository { - - public async Task> GetProfilesForUser(int userId, bool nonImplicitOnly, ReadingProfileIncludes includes = ReadingProfileIncludes.None) + public async Task GetUserProfile(int userId, int profileId) { - return await context.AppUserReadingProfile - .Where(rp => rp.UserId == userId && !(nonImplicitOnly && rp.Implicit)) - .Includes(includes) + return await context.AppUserReadingProfiles + .Where(rp => rp.AppUserId == userId && rp.Id == profileId) + .FirstOrDefaultAsync(); + } + + public async Task> GetProfilesForUser(int userId) + { + return await context.AppUserReadingProfiles + .Where(rp => rp.AppUserId == userId) .ToListAsync(); } - public async Task> GetProfilesDtoForUser(int userId, bool nonImplicitOnly, - ReadingProfileIncludes includes = ReadingProfileIncludes.None) + public async Task> GetProfilesDtoForUser(int userId) { - return await context.AppUserReadingProfile - .Where(rp => rp.UserId == userId && !(nonImplicitOnly && rp.Implicit)) - .Includes(includes) + return await context.AppUserReadingProfiles + .Where(rp => rp.AppUserId == userId) + .Where(rp => rp.Kind !=ReadingProfileKind.Implicit) .ProjectTo(mapper.ConfigurationProvider) .ToListAsync(); } - public async Task GetProfileForSeries(int userId, int seriesId, ReadingProfileIncludes includes = ReadingProfileIncludes.None) - { - return await context.AppUserReadingProfile - .Where(rp => rp.UserId == userId && rp.Series.Any(s => s.SeriesId == seriesId)) - .Includes(includes) - .OrderByDescending(rp => rp.Implicit) // Get implicit profiles first - .FirstOrDefaultAsync(); - } - - public async Task> GetAllProfilesForSeries(int userId, int seriesId, ReadingProfileIncludes includes = ReadingProfileIncludes.None) - { - return await context.AppUserReadingProfile - .Where(rp => rp.UserId == userId && rp.Series.Any(s => s.SeriesId == seriesId)) - .Includes(includes) - .ToListAsync(); - } - - public async Task> GetProfilesForSeries(int userId, IList seriesIds, bool implicitOnly, ReadingProfileIncludes includes = ReadingProfileIncludes.None) - { - return await context.AppUserReadingProfile - .Where(rp - => rp.UserId == userId - && rp.Series.Any(s => seriesIds.Contains(s.SeriesId)) - && (!implicitOnly || rp.Implicit)) - .Includes(includes) - .ToListAsync(); - } - - public async Task GetProfileDtoForSeries(int userId, int seriesId) - { - return await context.AppUserReadingProfile - .Where(rp => rp.UserId == userId && rp.Series.Any(s => s.SeriesId == seriesId)) - .OrderByDescending(rp => rp.Implicit) // Get implicit profiles first - .ProjectTo(mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public async Task GetProfileForLibrary(int userId, int libraryId, ReadingProfileIncludes includes = ReadingProfileIncludes.None) - { - return await context.AppUserReadingProfile - .Where(rp => rp.UserId == userId && rp.Libraries.Any(s => s.LibraryId == libraryId)) - .Includes(includes) - .FirstOrDefaultAsync(); - } - - public async Task GetProfileDtoForLibrary(int userId, int libraryId) - { - return await context.AppUserReadingProfile - .Where(rp => rp.UserId == userId && rp.Libraries.Any(s => s.LibraryId == libraryId)) - .ProjectTo(mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public async Task GetProfile(int profileId, ReadingProfileIncludes includes = ReadingProfileIncludes.None) - { - return await context.AppUserReadingProfile - .Where(rp => rp.Id == profileId) - .Includes(includes) - .FirstOrDefaultAsync(); - } - public async Task GetProfileDto(int profileId) - { - return await context.AppUserReadingProfile - .Where(rp => rp.Id == profileId) - .ProjectTo(mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - public async Task GetProfileByName(int userId, string name) { var normalizedName = name.ToNormalized(); - return await context.AppUserReadingProfile - .Where(rp => rp.NormalizedName == normalizedName && rp.UserId == userId) - .FirstOrDefaultAsync(); - } - - public async Task GetSeriesProfile(int userId, int seriesId) - { - return await context.SeriesReadingProfile - .Where(rp => rp.SeriesId == seriesId && rp.AppUserId == userId) - .FirstOrDefaultAsync(); - } - - public async Task> GetSeriesProfilesForSeries(int userId, IList seriesIds) - { - return await context.SeriesReadingProfile - .Where(rp => seriesIds.Contains(rp.SeriesId) && rp.AppUserId == userId) - .ToListAsync(); - } - - public async Task GetLibraryProfile(int userId, int libraryId) - { - return await context.LibraryReadingProfile - .Where(rp => rp.LibraryId == libraryId && rp.AppUserId == userId) + return await context.AppUserReadingProfiles + .Where(rp => rp.NormalizedName == normalizedName && rp.AppUserId == userId) .FirstOrDefaultAsync(); } public void Add(AppUserReadingProfile readingProfile) { - context.AppUserReadingProfile.Add(readingProfile); - } - - public void Add(SeriesReadingProfile readingProfile) - { - context.SeriesReadingProfile.Add(readingProfile); - } - - public void Add(LibraryReadingProfile readingProfile) - { - context.LibraryReadingProfile.Add(readingProfile); - } - - public void Attach(AppUserReadingProfile readingProfile) - { - context.AppUserReadingProfile.Attach(readingProfile); + context.AppUserReadingProfiles.Add(readingProfile); } public void Update(AppUserReadingProfile readingProfile) { - context.AppUserReadingProfile.Update(readingProfile).State = EntityState.Modified; - } - - public void Update(SeriesReadingProfile readingProfile) - { - context.SeriesReadingProfile.Update(readingProfile).State = EntityState.Modified; + context.AppUserReadingProfiles.Update(readingProfile).State = EntityState.Modified; } public void Remove(AppUserReadingProfile readingProfile) { - context.AppUserReadingProfile.Remove(readingProfile); - } - - public void Remove(SeriesReadingProfile readingProfile) - { - context.SeriesReadingProfile.Remove(readingProfile); + context.AppUserReadingProfiles.Remove(readingProfile); } public void RemoveRange(IEnumerable readingProfiles) { - context.AppUserReadingProfile.RemoveRange(readingProfiles); + context.AppUserReadingProfiles.RemoveRange(readingProfiles); } } diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index ef9dfa7ec..d9bc20c99 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -111,7 +111,7 @@ public class GenreRepository : IGenreRepository /// /// Returns a set of Genre tags for a set of library Ids. - /// UserId will restrict returned Genres based on user's age restriction and library access. + /// AppUserId will restrict returned Genres based on user's age restriction and library access. /// /// /// diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index e55338c8b..6437cfcfe 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -757,7 +757,7 @@ public class UserRepository : IUserRepository /// - /// Fetches the UserId by API Key. This does not include any extra information + /// Fetches the AppUserId by API Key. This does not include any extra information /// /// /// diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 50f795041..848636209 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -21,6 +21,7 @@ public class AppUser : IdentityUser, IHasConcurrencyToken public ICollection Ratings { get; set; } = null!; public ICollection ChapterRatings { get; set; } = null!; public AppUserPreferences UserPreferences { get; set; } = null!; + public ICollection ReadingProfiles { get; set; } = null!; /// /// Bookmarks associated with this User /// diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index 9545f190b..b0f21bcba 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -9,14 +9,6 @@ public class AppUserPreferences { public int Id { get; set; } - #region ReadingProfiles - - public int DefaultReadingProfileId { get; set; } - - public ICollection ReadingProfiles { get; set; } = null!; - - #endregion - #region MangaReader /// diff --git a/API/Entities/ReadingProfile/AppUserReadingProfile.cs b/API/Entities/AppUserReadingProfile.cs similarity index 91% rename from API/Entities/ReadingProfile/AppUserReadingProfile.cs rename to API/Entities/AppUserReadingProfile.cs index c18d25e9e..ad2548661 100644 --- a/API/Entities/ReadingProfile/AppUserReadingProfile.cs +++ b/API/Entities/AppUserReadingProfile.cs @@ -11,11 +11,12 @@ public class AppUserReadingProfile public string Name { get; set; } public string NormalizedName { get; set; } - public int UserId { get; set; } - public AppUser User { get; set; } + public int AppUserId { get; set; } + public AppUser AppUser { get; set; } - public ICollection Series { get; set; } - public ICollection Libraries { get; set; } + public ReadingProfileKind Kind { get; set; } + public List LibraryIds { get; set; } + public List SeriesIds { get; set; } #region MangaReader @@ -139,10 +140,4 @@ public class AppUserReadingProfile #endregion - - /// - /// If the profile has been created in the background after a user modified a series settings - /// - /// A profile can be made non-implicit by a user, but not implicit - public bool Implicit { get; set; } = false; } diff --git a/API/Entities/Enums/ReadingProfileKind.cs b/API/Entities/Enums/ReadingProfileKind.cs new file mode 100644 index 000000000..e33db6874 --- /dev/null +++ b/API/Entities/Enums/ReadingProfileKind.cs @@ -0,0 +1,17 @@ +namespace API.Entities.Enums; + +public enum ReadingProfileKind +{ + /// + /// Generate by Kavita when registering a user, this is your default profile + /// + Default, + /// + /// Created by the user in the UI or via the API + /// + User, + /// + /// Automatically generated by Kavita to tracked changes made in the readers + /// + Implicit +} diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index cc2d04d9c..abab81378 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -65,7 +65,6 @@ public class Library : IEntityDate, IHasCoverImage public ICollection Series { get; set; } = null!; public ICollection LibraryFileTypes { get; set; } = new List(); public ICollection LibraryExcludePatterns { get; set; } = new List(); - public ICollection ReadingProfiles { get; set; } = null!; public void UpdateLastModified() { diff --git a/API/Entities/ReadingProfile/LibraryReadingProfile.cs b/API/Entities/ReadingProfile/LibraryReadingProfile.cs deleted file mode 100644 index de131e2b1..000000000 --- a/API/Entities/ReadingProfile/LibraryReadingProfile.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace API.Entities; - -[Index(nameof(LibraryId), nameof(AppUserId), IsUnique = true)] -public class LibraryReadingProfile -{ - - public int Id { get; set; } - - public int AppUserId { get; set; } - public AppUser AppUser { get; set; } - - public int LibraryId { get; set; } - public Library Library { get; set; } - - public int ReadingProfileId { get; set; } - public AppUserReadingProfile ReadingProfile { get; set; } - -} diff --git a/API/Entities/ReadingProfile/SeriesReadingProfile.cs b/API/Entities/ReadingProfile/SeriesReadingProfile.cs deleted file mode 100644 index b8c82b338..000000000 --- a/API/Entities/ReadingProfile/SeriesReadingProfile.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace API.Entities; - -public class SeriesReadingProfile -{ - - public int Id { get; set; } - - public int AppUserId { get; set; } - public AppUser AppUser { get; set; } - - public int SeriesId { get; set; } - public Series Series { get; set; } - - public int ReadingProfileId { get; set; } - public AppUserReadingProfile ReadingProfile { get; set; } - -} diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index 34f0f85b8..4f06ab0fc 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -135,7 +135,6 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage public List Volumes { get; set; } = null!; public Library Library { get; set; } = null!; public int LibraryId { get; set; } - public ICollection ReadingProfiles { get; set; } = null!; public void UpdateLastFolderScanned() diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/API/Extensions/QueryExtensions/IncludesExtensions.cs index 4de5ff81f..bfc585455 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/API/Extensions/QueryExtensions/IncludesExtensions.cs @@ -220,8 +220,7 @@ public static class IncludesExtensions { query = query .Include(u => u.UserPreferences) - .ThenInclude(p => p.Theme) - .Include(u => u.UserPreferences.ReadingProfiles); + .ThenInclude(p => p.Theme); } if (includeFlags.HasFlag(AppUserIncludes.WantToRead)) @@ -343,20 +342,4 @@ public static class IncludesExtensions return queryable; } - - public static IQueryable Includes(this IQueryable queryable, ReadingProfileIncludes includeFlags) - { - - if (includeFlags.HasFlag(ReadingProfileIncludes.Series)) - { - queryable = queryable.Include(r => r.Series); - } - - if (includeFlags.HasFlag(ReadingProfileIncludes.Library)) - { - queryable = queryable.Include(r => r.Libraries); - } - - return queryable; - } } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 7370ee2a3..d25444a51 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -280,16 +280,7 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.BookReaderThemeName, opt => - opt.MapFrom(src => src.BookThemeName)) - .ForMember(dest => dest.BookReaderLayoutMode, - opt => - opt.MapFrom(src => src.BookReaderLayoutMode)) - .ForMember(dest => dest.SeriesIds, - opt => - opt.MapFrom(src => src.Series.Select(s => s.SeriesId).ToList())) - .ForMember(dest => dest.LibraryIds, - opt => - opt.MapFrom(src => src.Libraries.Select(s => s.LibraryId).ToList())); + opt.MapFrom(src => src.BookThemeName)); CreateMap(); diff --git a/API/Helpers/Builders/AppUserBuilder.cs b/API/Helpers/Builders/AppUserBuilder.cs index 382e4b35b..7ffac355e 100644 --- a/API/Helpers/Builders/AppUserBuilder.cs +++ b/API/Helpers/Builders/AppUserBuilder.cs @@ -22,7 +22,6 @@ public class AppUserBuilder : IEntityBuilder UserPreferences = new AppUserPreferences { Theme = theme ?? Seed.DefaultThemes.First(), - ReadingProfiles = [], }, ReadingLists = new List(), Bookmarks = new List(), @@ -33,6 +32,7 @@ public class AppUserBuilder : IEntityBuilder Id = 0, DashboardStreams = new List(), SideNavStreams = new List(), + ReadingProfiles = [], }; } diff --git a/API/Helpers/Builders/AppUserReadingProfileBuilder.cs b/API/Helpers/Builders/AppUserReadingProfileBuilder.cs index d22e13fd7..26da5fd86 100644 --- a/API/Helpers/Builders/AppUserReadingProfileBuilder.cs +++ b/API/Helpers/Builders/AppUserReadingProfileBuilder.cs @@ -1,4 +1,5 @@ using API.Entities; +using API.Entities.Enums; using API.Extensions; namespace API.Helpers.Builders; @@ -9,39 +10,36 @@ public class AppUserReadingProfileBuilder public AppUserReadingProfile Build() => _profile; + /// + /// The profile's kind will be unless overwritten with + /// + /// public AppUserReadingProfileBuilder(int userId) { _profile = new AppUserReadingProfile { - UserId = userId, - Series = [], - Libraries = [], + AppUserId = userId, + Kind = ReadingProfileKind.User, + SeriesIds = [], + LibraryIds = [] }; } public AppUserReadingProfileBuilder WithSeries(Series series) { - _profile.Series.Add(new SeriesReadingProfile - { - Series = series, - AppUserId = _profile.UserId, - }); + _profile.SeriesIds.Add(series.Id); return this; } public AppUserReadingProfileBuilder WithLibrary(Library library) { - _profile.Libraries.Add(new LibraryReadingProfile - { - Library = library, - AppUserId = _profile.UserId, - }); + _profile.LibraryIds.Add(library.Id); return this; } - public AppUserReadingProfileBuilder WithImplicit(bool b) + public AppUserReadingProfileBuilder WithKind(ReadingProfileKind kind) { - _profile.Implicit = b; + _profile.Kind = kind; return this; } diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index ef22736d2..85814dcd9 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -261,7 +261,7 @@ public class ScrobblingService : IScrobblingService var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - _logger.LogInformation("Processing Scrobbling review event for {UserId} on {SeriesName}", userId, series.Name); + _logger.LogInformation("Processing Scrobbling review event for {AppUserId} on {SeriesName}", userId, series.Name); if (await CheckIfCannotScrobble(userId, seriesId, series)) return; if (IsAniListReviewValid(reviewTitle, reviewBody)) @@ -297,7 +297,7 @@ public class ScrobblingService : IScrobblingService }; _unitOfWork.ScrobbleRepository.Attach(evt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling Review update on {SeriesName} with Userid {UserId} ", series.Name, userId); + _logger.LogDebug("Added Scrobbling Review update on {SeriesName} with Userid {AppUserId} ", series.Name, userId); } private static bool IsAniListReviewValid(string reviewTitle, string reviewBody) @@ -317,7 +317,7 @@ public class ScrobblingService : IScrobblingService var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; - _logger.LogInformation("Processing Scrobbling rating event for {UserId} on {SeriesName}", userId, series.Name); + _logger.LogInformation("Processing Scrobbling rating event for {AppUserId} on {SeriesName}", userId, series.Name); if (await CheckIfCannotScrobble(userId, seriesId, series)) return; var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, @@ -346,7 +346,7 @@ public class ScrobblingService : IScrobblingService }; _unitOfWork.ScrobbleRepository.Attach(evt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling Rating update on {SeriesName} with Userid {UserId}", series.Name, userId); + _logger.LogDebug("Added Scrobbling Rating update on {SeriesName} with Userid {AppUserId}", series.Name, userId); } public static long? GetMalId(Series series) @@ -371,7 +371,7 @@ public class ScrobblingService : IScrobblingService var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; - _logger.LogInformation("Processing Scrobbling reading event for {UserId} on {SeriesName}", userId, series.Name); + _logger.LogInformation("Processing Scrobbling reading event for {AppUserId} on {SeriesName}", userId, series.Name); if (await CheckIfCannotScrobble(userId, seriesId, series)) return; var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, @@ -418,7 +418,7 @@ public class ScrobblingService : IScrobblingService _unitOfWork.ScrobbleRepository.Attach(evt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling Read update on {SeriesName} - Volume: {VolumeNumber} Chapter: {ChapterNumber} for User: {UserId}", series.Name, evt.VolumeNumber, evt.ChapterNumber, userId); + _logger.LogDebug("Added Scrobbling Read update on {SeriesName} - Volume: {VolumeNumber} Chapter: {ChapterNumber} for User: {AppUserId}", series.Name, evt.VolumeNumber, evt.ChapterNumber, userId); } catch (Exception ex) { @@ -437,7 +437,7 @@ public class ScrobblingService : IScrobblingService if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; if (await CheckIfCannotScrobble(userId, seriesId, series)) return; - _logger.LogInformation("Processing Scrobbling want-to-read event for {UserId} on {SeriesName}", userId, series.Name); + _logger.LogInformation("Processing Scrobbling want-to-read event for {AppUserId} on {SeriesName}", userId, series.Name); // Get existing events for this series/user var existingEvents = (await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId)) @@ -463,7 +463,7 @@ public class ScrobblingService : IScrobblingService _unitOfWork.ScrobbleRepository.Attach(evt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {UserId} ", series.Name, userId); + _logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {AppUserId} ", series.Name, userId); } private async Task CheckIfCannotScrobble(int userId, int seriesId, Series series) @@ -471,7 +471,7 @@ public class ScrobblingService : IScrobblingService if (series.DontMatch) return true; if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId)) { - _logger.LogInformation("Series {SeriesName} is on UserId {UserId}'s hold list. Not scrobbling", series.Name, + _logger.LogInformation("Series {SeriesName} is on AppUserId {AppUserId}'s hold list. Not scrobbling", series.Name, userId); return true; } @@ -750,7 +750,7 @@ public class ScrobblingService : IScrobblingService /// public async Task ClearEventsForSeries(int userId, int seriesId) { - _logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {UserId} as Series is now on hold list", seriesId, userId); + _logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {AppUserId} as Series is now on hold list", seriesId, userId); var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId); foreach (var scrobble in events) { @@ -1109,7 +1109,7 @@ public class ScrobblingService : IScrobblingService { if (ex.Message.Contains("Access token is invalid")) { - _logger.LogCritical(ex, "Access Token for UserId: {UserId} needs to be regenerated/renewed to continue scrobbling", evt.AppUser.Id); + _logger.LogCritical(ex, "Access Token for AppUserId: {AppUserId} needs to be regenerated/renewed to continue scrobbling", evt.AppUser.Id); evt.IsErrored = true; evt.ErrorDetails = AccessTokenErrorMessage; _unitOfWork.ScrobbleRepository.Update(evt); diff --git a/API/Services/ReadingProfileService.cs b/API/Services/ReadingProfileService.cs index 2d0169fd3..4d28ac1e1 100644 --- a/API/Services/ReadingProfileService.cs +++ b/API/Services/ReadingProfileService.cs @@ -6,8 +6,10 @@ using API.Data; using API.Data.Repositories; using API.DTOs; using API.Entities; +using API.Entities.Enums; using API.Extensions; using API.Helpers.Builders; +using AutoMapper; using Kavita.Common; namespace API.Services; @@ -16,12 +18,12 @@ public interface IReadingProfileService { /// /// Returns the ReadingProfile that should be applied to the given series, walks up the tree. - /// Series (implicit) -> Series (Assigned) -> Library -> Default + /// Series (Implicit) -> Series (User) -> Library (User) -> Default /// /// /// /// - Task GetReadingProfileForSeries(int userId, int seriesId); + Task GetReadingProfileDtoForSeries(int userId, int seriesId); /// /// Updates a given reading profile for a user, and deletes all implicit profiles @@ -60,59 +62,87 @@ public interface IReadingProfileService Task DeleteReadingProfile(int userId, int profileId); /// - /// Sets the given profile as global default + /// Assigns the reading profile to the series, and remove the implicit RP from the series if it exists /// /// /// + /// /// - Task SetDefaultReadingProfile(int userId, int profileId); - Task AddProfileToSeries(int userId, int profileId, int seriesId); + /// + /// Assigns the reading profile to many series, and remove the implicit RP from the series if it exists + /// + /// + /// + /// + /// Task BulkAddProfileToSeries(int userId, int profileId, IList seriesIds); + /// + /// Remove all reading profiles from the series + /// + /// + /// + /// Task ClearSeriesProfile(int userId, int seriesId); + /// + /// Assign the reading profile to the library + /// + /// + /// + /// + /// Task AddProfileToLibrary(int userId, int profileId, int libraryId); + /// + /// Remove the reading profile from the library, if it exists + /// + /// + /// + /// Task ClearLibraryProfile(int userId, int libraryId); } -public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService localizationService): IReadingProfileService +public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper): IReadingProfileService { - public async Task GetReadingProfileForSeries(int userId, int seriesId) + public async Task GetReadingProfileDtoForSeries(int userId, int seriesId) { - var seriesProfile = await unitOfWork.AppUserReadingProfileRepository.GetProfileDtoForSeries(userId, seriesId); + return mapper.Map(await GetReadingProfileForSeries(userId, seriesId)); + } + + public async Task GetReadingProfileForSeries(int userId, int seriesId) + { + var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); + + var implicitSeriesProfile = profiles + .FirstOrDefault(p => p.SeriesIds.Contains(seriesId) && p.Kind == ReadingProfileKind.Implicit); + if (implicitSeriesProfile != null) return implicitSeriesProfile; + + var seriesProfile = profiles + .FirstOrDefault(p => p.SeriesIds.Contains(seriesId) && p.Kind != ReadingProfileKind.Implicit); if (seriesProfile != null) return seriesProfile; + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); if (series == null) throw new KavitaException(await localizationService.Translate(userId, "series-doesnt-exist")); - var libraryProfile = await unitOfWork.AppUserReadingProfileRepository.GetProfileDtoForLibrary(userId, series.LibraryId); + var libraryProfile = profiles + .FirstOrDefault(p => p.LibraryIds.Contains(series.LibraryId) && p.Kind != ReadingProfileKind.Implicit); if (libraryProfile != null) return libraryProfile; - var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null) throw new UnauthorizedAccessException(); - - return await unitOfWork.AppUserReadingProfileRepository.GetProfileDto(user.UserPreferences.DefaultReadingProfileId); + return profiles.First(p => p.Kind == ReadingProfileKind.Default); } public async Task UpdateReadingProfile(int userId, UserReadingProfileDto dto) { - var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null) throw new UnauthorizedAccessException(); - - var profile = await unitOfWork.AppUserReadingProfileRepository.GetProfile(dto.Id, ReadingProfileIncludes.Series); + var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, dto.Id); if (profile == null) throw new KavitaException("profile-does-not-exist"); - if (profile.UserId != userId) throw new UnauthorizedAccessException(); - UpdateReaderProfileFields(profile, dto); unitOfWork.AppUserReadingProfileRepository.Update(profile); - // Remove all implicit profiles for series using this profile - var allLinkedSeries = profile.Series.Select(sp => sp.SeriesId).ToList(); - var implicitProfiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForSeries(userId, allLinkedSeries, true); - unitOfWork.AppUserReadingProfileRepository.RemoveRange(implicitProfiles); + await DeleteImplicateReadingProfilesForSeries(userId, profile.SeriesIds); await unitOfWork.CommitAsync(); } @@ -128,11 +158,11 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService var newProfile = new AppUserReadingProfileBuilder(user.Id).Build(); UpdateReaderProfileFields(newProfile, dto); unitOfWork.AppUserReadingProfileRepository.Add(newProfile); - user.UserPreferences.ReadingProfiles.Add(newProfile); + user.ReadingProfiles.Add(newProfile); await unitOfWork.CommitAsync(); - return await unitOfWork.AppUserReadingProfileRepository.GetProfileDto(newProfile.Id); + return mapper.Map(newProfile); } public async Task UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto) @@ -140,10 +170,11 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); if (user == null) throw new UnauthorizedAccessException(); - var existingProfile = await unitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(userId, seriesId); + var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); + var existingProfile = profiles.FirstOrDefault(rp => rp.Kind == ReadingProfileKind.Implicit && rp.SeriesIds.Contains(seriesId)); // Series already had an implicit profile, update it - if (existingProfile is {Implicit: true}) + if (existingProfile is {Kind: ReadingProfileKind.Implicit}) { UpdateReaderProfileFields(existingProfile, dto, false); unitOfWork.AppUserReadingProfileRepository.Update(existingProfile); @@ -154,7 +185,7 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId) ?? throw new KeyNotFoundException(); var newProfile = new AppUserReadingProfileBuilder(userId) .WithSeries(series) - .WithImplicit(true) + .WithKind(ReadingProfileKind.Implicit) .Build(); // Set name to something fitting for debugging if needed @@ -162,165 +193,111 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService newProfile.Name = $"Implicit Profile for {seriesId}"; newProfile.NormalizedName = newProfile.Name.ToNormalized(); - user.UserPreferences.ReadingProfiles.Add(newProfile); + user.ReadingProfiles.Add(newProfile); await unitOfWork.CommitAsync(); } public async Task DeleteReadingProfile(int userId, int profileId) { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetProfile(profileId); + var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); if (profile == null) throw new KavitaException("profile-doesnt-exist"); - var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null || profile.UserId != userId) throw new UnauthorizedAccessException(); - - if (user.UserPreferences.DefaultReadingProfileId == profileId) throw new KavitaException("cant-delete-default-profile"); + if (profile.Kind == ReadingProfileKind.Default) throw new KavitaException("cant-delete-default-profile"); unitOfWork.AppUserReadingProfileRepository.Remove(profile); await unitOfWork.CommitAsync(); } - public async Task SetDefaultReadingProfile(int userId, int profileId) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetProfile(profileId); - if (profile == null) throw new KavitaException("profile-not-found"); - - if (profile.UserId != userId) throw new UnauthorizedAccessException(); - - var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null) throw new UnauthorizedAccessException(); - - user.UserPreferences.DefaultReadingProfileId = profile.Id; - await unitOfWork.CommitAsync(); - } - public async Task AddProfileToSeries(int userId, int profileId, int seriesId) { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetProfile(profileId); - if (profile == null) throw new KavitaException("profile-not-found"); + var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); + if (profile == null) throw new KavitaException("profile-doesnt-exist"); - if (profile.UserId != userId) throw new UnauthorizedAccessException(); + await DeleteImplicitAndRemoveFromUserProfiles(userId, [seriesId], []); - // Remove all implicit profiles - var implicitProfiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForSeries(userId, [seriesId], true); - unitOfWork.AppUserReadingProfileRepository.RemoveRange(implicitProfiles); + profile.SeriesIds.Add(seriesId); + unitOfWork.AppUserReadingProfileRepository.Update(profile); - var seriesProfile = await unitOfWork.AppUserReadingProfileRepository.GetSeriesProfile(userId, seriesId); - if (seriesProfile != null) - { - seriesProfile.ReadingProfile = profile; - await unitOfWork.CommitAsync(); - return; - } - - seriesProfile = new SeriesReadingProfile - { - AppUserId = userId, - SeriesId = seriesId, - ReadingProfileId = profile.Id - }; - - unitOfWork.AppUserReadingProfileRepository.Add(seriesProfile); await unitOfWork.CommitAsync(); } public async Task BulkAddProfileToSeries(int userId, int profileId, IList seriesIds) { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetProfile(profileId, ReadingProfileIncludes.Series); - if (profile == null) throw new KavitaException("profile-not-found"); + var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); + if (profile == null) throw new KavitaException("profile-doesnt-exist"); - if (profile.UserId != userId) throw new UnauthorizedAccessException(); + await DeleteImplicitAndRemoveFromUserProfiles(userId, seriesIds, []); - var seriesProfiles = await unitOfWork.AppUserReadingProfileRepository.GetSeriesProfilesForSeries(userId, seriesIds); - var newSeriesIds = seriesIds.Except(seriesProfiles.Select(p => p.SeriesId)).ToList(); - - // Update existing - foreach (var seriesProfile in seriesProfiles) - { - seriesProfile.ReadingProfile = profile; - } - - // Create new ones - foreach (var seriesId in newSeriesIds) - { - var seriesProfile = new SeriesReadingProfile - { - AppUserId = userId, - SeriesId = seriesId, - ReadingProfile = profile, - }; - unitOfWork.AppUserReadingProfileRepository.Add(seriesProfile); - } - - // Remove all implicit profiles - var implicitProfiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForSeries(userId, seriesIds, true); - unitOfWork.AppUserReadingProfileRepository.RemoveRange(implicitProfiles); + profile.SeriesIds.AddRange(seriesIds.Except(profile.SeriesIds)); + unitOfWork.AppUserReadingProfileRepository.Update(profile); await unitOfWork.CommitAsync(); } public async Task ClearSeriesProfile(int userId, int seriesId) { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetAllProfilesForSeries(userId, seriesId, ReadingProfileIncludes.Series); - if (!profiles.Any()) return; - - foreach (var profile in profiles) - { - if (profile.Implicit) - { - unitOfWork.AppUserReadingProfileRepository.Remove(profile); - } - else - { - profile.Series = profile.Series.Where(s => !(s.SeriesId == seriesId && s.AppUserId == userId)).ToList(); - } - } - + await DeleteImplicitAndRemoveFromUserProfiles(userId, [seriesId], []); await unitOfWork.CommitAsync(); } public async Task AddProfileToLibrary(int userId, int profileId, int libraryId) { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetProfile(profileId); - if (profile == null) throw new KavitaException("profile-not-found"); + var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); + if (profile == null) throw new KavitaException("profile-doesnt-exist"); - if (profile.UserId != userId) throw new UnauthorizedAccessException(); + await DeleteImplicitAndRemoveFromUserProfiles(userId, [], [libraryId]); - var libraryProfile = await unitOfWork.AppUserReadingProfileRepository.GetLibraryProfile(userId, libraryId); - if (libraryProfile != null) - { - libraryProfile.ReadingProfile = profile; - await unitOfWork.CommitAsync(); - return; - } - - libraryProfile = new LibraryReadingProfile - { - AppUserId = userId, - LibraryId = libraryId, - ReadingProfile = profile, - }; - - unitOfWork.AppUserReadingProfileRepository.Add(libraryProfile); + profile.LibraryIds.Add(libraryId); + unitOfWork.AppUserReadingProfileRepository.Update(profile); await unitOfWork.CommitAsync(); } public async Task ClearLibraryProfile(int userId, int libraryId) { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetProfileForLibrary(userId, libraryId, ReadingProfileIncludes.Library); - if (profile == null) return; - - if (profile.Implicit) + var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); + var libraryProfile = profiles.FirstOrDefault(p => p.LibraryIds.Contains(libraryId)); + if (libraryProfile != null) { - unitOfWork.AppUserReadingProfileRepository.Remove(profile); - await unitOfWork.CommitAsync(); - return; + libraryProfile.LibraryIds.Remove(libraryId); + unitOfWork.AppUserReadingProfileRepository.Update(libraryProfile); } - profile.Libraries = profile.Libraries - .Where(s => !(s.LibraryId == libraryId && s.AppUserId == userId)) + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + } + + private async Task DeleteImplicitAndRemoveFromUserProfiles(int userId, IList seriesIds, IList libraryIds) + { + var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); + var implicitProfiles = profiles + .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any()) + .Where(rp => rp.Kind == ReadingProfileKind.Implicit) .ToList(); - await unitOfWork.CommitAsync(); + unitOfWork.AppUserReadingProfileRepository.RemoveRange(implicitProfiles); + + var nonImplicitProfiles = profiles + .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any() || rp.LibraryIds.Intersect(libraryIds).Any()) + .Where(rp => rp.Kind != ReadingProfileKind.Implicit); + + foreach (var profile in nonImplicitProfiles) + { + profile.SeriesIds.RemoveAll(seriesIds.Contains); + profile.LibraryIds.RemoveAll(libraryIds.Contains); + unitOfWork.AppUserReadingProfileRepository.Update(profile); + } + } + + private async Task DeleteImplicateReadingProfilesForSeries(int userId, IList seriesIds) + { + var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); + var implicitProfiles = profiles + .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any()) + .Where(rp => rp.Kind == ReadingProfileKind.Implicit) + .ToList(); + unitOfWork.AppUserReadingProfileRepository.RemoveRange(implicitProfiles); } public static void UpdateReaderProfileFields(AppUserReadingProfile existingProfile, UserReadingProfileDto dto, bool updateName = true) diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index ed2095649..886c570e2 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -2,7 +2,6 @@ import {PageLayoutMode} from '../page-layout-mode'; import {SiteTheme} from './site-theme'; export interface Preferences { - defaultReadingProfileId: number; // Global theme: SiteTheme; diff --git a/UI/Web/src/app/_models/preferences/reading-profiles.ts b/UI/Web/src/app/_models/preferences/reading-profiles.ts index 45a8650a7..d81b8cc88 100644 --- a/UI/Web/src/app/_models/preferences/reading-profiles.ts +++ b/UI/Web/src/app/_models/preferences/reading-profiles.ts @@ -13,11 +13,18 @@ import {PdfSpreadMode} from "./pdf-spread-mode"; import {Series} from "../series"; import {Library} from "../library/library"; +export enum ReadingProfileKind { + Default = 0, + User = 1, + Implicit = 2, +} + export interface ReadingProfile { id: number; name: string; normalizedName: string; + kind: ReadingProfileKind; // Manga Reader readingDirection: ReadingDirection; diff --git a/UI/Web/src/app/_services/reading-profile.service.ts b/UI/Web/src/app/_services/reading-profile.service.ts index fa0976f97..49ef94817 100644 --- a/UI/Web/src/app/_services/reading-profile.service.ts +++ b/UI/Web/src/app/_services/reading-profile.service.ts @@ -36,10 +36,6 @@ export class ReadingProfileService { return this.httpClient.delete(this.baseUrl + "ReadingProfile?profileId="+id); } - setDefault(id: number) { - return this.httpClient.post(this.baseUrl + "ReadingProfile/set-default?profileId=" + id, {}); - } - addToSeries(id: number, seriesId: number) { return this.httpClient.post(this.baseUrl + `ReadingProfile/series/${seriesId}?profileId=${id}`, {}); } diff --git a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.html b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.html index 63c351a5d..31c769bbd 100644 --- a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.html +++ b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.html @@ -55,10 +55,7 @@ @if (selectedProfile.id !== 0) {
- -
@@ -501,7 +498,7 @@ >
{{profile.name | sentenceCase}}
- @if (profile.id === user.preferences.defaultReadingProfileId) { + @if (profile.kind === ReadingProfileKind.Default) { {{t('default-profile')}} } diff --git a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts index 2ed8d94ad..36438b532 100644 --- a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts +++ b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts @@ -1,11 +1,18 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, OnInit} from '@angular/core'; import {ReadingProfileService} from "../../_services/reading-profile.service"; import { - bookLayoutModes, bookWritingStyles, layoutModes, - pageSplitOptions, pdfScrollModes, - pdfSpreadModes, pdfThemes, - readingDirections, readingModes, - ReadingProfile, scalingOptions + bookLayoutModes, + bookWritingStyles, + layoutModes, + pageSplitOptions, + pdfScrollModes, + pdfSpreadModes, + pdfThemes, + readingDirections, + readingModes, + ReadingProfile, + ReadingProfileKind, + scalingOptions } from "../../_models/preferences/reading-profiles"; import {translate, TranslocoDirective} from "@jsverse/transloco"; import {NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common"; @@ -34,13 +41,7 @@ import {SettingItemComponent} from "../../settings/_components/setting-item/sett import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component"; import {WritingStylePipe} from "../../_pipes/writing-style.pipe"; import {ColorPickerDirective} from "ngx-color-picker"; -import { - NgbNav, - NgbNavItem, - NgbNavLinkBase, - NgbNavContent, - NgbNavOutlet -} from "@ng-bootstrap/ng-bootstrap"; +import {NgbNav, NgbNavContent, NgbNavItem, NgbNavLinkBase, NgbNavOutlet} from "@ng-bootstrap/ng-bootstrap"; import {filter} from "rxjs"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {LoadingComponent} from "../../shared/loading/loading.component"; @@ -141,13 +142,6 @@ export class ManageReadingProfilesComponent implements OnInit { }); } - setDefault(id: number) { - this.readingProfileService.setDefault(id).subscribe(() => { - this.user.preferences.defaultReadingProfileId = id; - this.cdRef.markForCheck(); - }) - } - get widthOverwriteLabel() { const rawVal = this.readingProfileForm?.get('widthOverride')!.value; if (!rawVal) { @@ -286,7 +280,7 @@ export class ManageReadingProfilesComponent implements OnInit { } addNew() { - const defaultProfile = this.readingProfiles.find(f => f.id === this.user.preferences.defaultReadingProfileId); + const defaultProfile = this.readingProfiles.find(f => f.kind === ReadingProfileKind.Default); this.selectedProfile = {...defaultProfile!}; this.selectedProfile.id = 0; this.selectedProfile.name = "New Profile #" + (this.readingProfiles.length + 1); @@ -306,4 +300,5 @@ export class ManageReadingProfilesComponent implements OnInit { protected readonly pdfScrollModes = pdfScrollModes; protected readonly TabId = TabId; + protected readonly ReadingProfileKind = ReadingProfileKind; } 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 c95d776cb..a9c250691 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 @@ -243,7 +243,6 @@ export class ManageUserPreferencesComponent implements OnInit { //pdfSpreadMode: parseInt(modelSettings.pdfSpreadMode, 10), aniListScrobblingEnabled: modelSettings.aniListScrobblingEnabled, wantToReadSync: modelSettings.wantToReadSync, - defaultReadingProfileId: this.user!.preferences.defaultReadingProfileId, }; } }