From 29c29976f1fbcf36979d4395c58b4fd6b39396a3 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 7 Jun 2025 15:07:18 -0500 Subject: [PATCH] Reworked some of the logic to be less round trips to db. Fixed a localization "off" issue. Don't allow users to rename the Default Reading Profile. --- API/Controllers/ReadingProfileController.cs | 14 ++- .../AppUserReadingProfileRepository.cs | 26 +++-- API/Entities/Enums/ReadingProfileKind.cs | 2 +- API/Services/ReadingProfileService.cs | 101 +++++++++++------- .../app/_services/reading-profile.service.ts | 26 ++--- .../reader-settings.component.ts | 3 +- .../manage-reading-profiles.component.html | 11 +- .../manage-reading-profiles.component.ts | 2 +- UI/Web/src/assets/langs/en.json | 6 +- 9 files changed, 112 insertions(+), 79 deletions(-) diff --git a/API/Controllers/ReadingProfileController.cs b/API/Controllers/ReadingProfileController.cs index 214a267ea..73936985f 100644 --- a/API/Controllers/ReadingProfileController.cs +++ b/API/Controllers/ReadingProfileController.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +[Route("api/reading-profile")] public class ReadingProfileController(ILogger logger, IUnitOfWork unitOfWork, IReadingProfileService readingProfileService): BaseApiController { @@ -26,8 +27,7 @@ public class ReadingProfileController(ILogger logger, [HttpGet("all")] public async Task>> GetAllReadingProfiles() { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesDtoForUser(User.GetUserId()); - return Ok(profiles); + return Ok(await unitOfWork.AppUserReadingProfileRepository.GetProfilesDtoForUser(User.GetUserId(), true)); } /// @@ -95,16 +95,14 @@ public class ReadingProfileController(ILogger logger, /// Updates the given reading profile, must belong to the current user /// /// - /// + /// The updated reading profile /// - /// This does not update connected series, and libraries. - /// Deletes all implicit profiles for series linked to this profile + /// This does not update connected series and libraries. /// [HttpPost] - public async Task> UpdateReadingProfile([FromBody] UserReadingProfileDto dto) + public async Task> UpdateReadingProfile(UserReadingProfileDto dto) { - var newProfile = await readingProfileService.UpdateReadingProfile(User.GetUserId(), dto); - return Ok(newProfile); + return Ok(await readingProfileService.UpdateReadingProfile(User.GetUserId(), dto)); } /// diff --git a/API/Data/Repositories/AppUserReadingProfileRepository.cs b/API/Data/Repositories/AppUserReadingProfileRepository.cs index 46ec49949..11b97f21a 100644 --- a/API/Data/Repositories/AppUserReadingProfileRepository.cs +++ b/API/Data/Repositories/AppUserReadingProfileRepository.cs @@ -29,20 +29,20 @@ public interface IAppUserReadingProfileRepository /// /// /// - Task> GetProfilesForUser(int userId); + Task> GetProfilesForUser(int userId, bool skipImplicit = false); /// - /// Returns all non-implicit reading profiles for the user + /// Returns all reading profiles for the user /// /// /// - Task> GetProfilesDtoForUser(int userId); + Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false); /// - /// Find a profile by name, belonging to a specific user + /// Is there a user reading profile with this name (normalized) /// /// /// /// - Task GetProfileByName(int userId, string name); + Task IsProfileNameInUse(int userId, string name); void Add(AppUserReadingProfile readingProfile); void Update(AppUserReadingProfile readingProfile); @@ -59,29 +59,35 @@ public class AppUserReadingProfileRepository(DataContext context, IMapper mapper .FirstOrDefaultAsync(); } - public async Task> GetProfilesForUser(int userId) + public async Task> GetProfilesForUser(int userId, bool skipImplicit = false) { return await context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId) + .WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit) .ToListAsync(); } - public async Task> GetProfilesDtoForUser(int userId) + /// + /// Returns all Reading Profiles for the User + /// + /// + /// + public async Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false) { return await context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId) - .Where(rp => rp.Kind !=ReadingProfileKind.Implicit) + .WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit) .ProjectTo(mapper.ConfigurationProvider) .ToListAsync(); } - public async Task GetProfileByName(int userId, string name) + public async Task IsProfileNameInUse(int userId, string name) { var normalizedName = name.ToNormalized(); return await context.AppUserReadingProfiles .Where(rp => rp.NormalizedName == normalizedName && rp.AppUserId == userId) - .FirstOrDefaultAsync(); + .AnyAsync(); } public void Add(AppUserReadingProfile readingProfile) diff --git a/API/Entities/Enums/ReadingProfileKind.cs b/API/Entities/Enums/ReadingProfileKind.cs index e33db6874..0f9cfa20b 100644 --- a/API/Entities/Enums/ReadingProfileKind.cs +++ b/API/Entities/Enums/ReadingProfileKind.cs @@ -11,7 +11,7 @@ public enum ReadingProfileKind /// User, /// - /// Automatically generated by Kavita to tracked changes made in the readers + /// Automatically generated by Kavita to track changes made in the readers. Can be converted to a User Reading Profile. /// Implicit } diff --git a/API/Services/ReadingProfileService.cs b/API/Services/ReadingProfileService.cs index 38cc5ae15..aa2c6dabe 100644 --- a/API/Services/ReadingProfileService.cs +++ b/API/Services/ReadingProfileService.cs @@ -33,13 +33,6 @@ public interface IReadingProfileService /// /// Task CreateReadingProfile(int userId, UserReadingProfileDto dto); - - /// - /// Promotes the implicit profile to a user profile. Removes the series from other profiles - /// - /// - /// - /// Task PromoteImplicitProfile(int userId, int profileId); /// @@ -123,22 +116,18 @@ public interface IReadingProfileService public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper): IReadingProfileService { - - public async Task GetReadingProfileDtoForSeries(int userId, int seriesId, bool skipImplicit = false) - { - return mapper.Map(await GetReadingProfileForSeries(userId, seriesId, skipImplicit)); - } - + /// + /// Tries to resolve the Reading Profile for a given Series. Will first check (optionally) Implicit profiles, then check for a bound Series profile, then a bound + /// Library profile, then default to the default profile. + /// + /// + /// + /// + /// + /// public async Task GetReadingProfileForSeries(int userId, int seriesId, bool skipImplicit = false) { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - - if (!skipImplicit) - { - var implicitSeriesProfile = profiles - .FirstOrDefault(p => p.SeriesIds.Contains(seriesId) && p.Kind == ReadingProfileKind.Implicit); - if (implicitSeriesProfile != null) return implicitSeriesProfile; - } + var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId, skipImplicit); var seriesProfile = profiles .FirstOrDefault(p => p.SeriesIds.Contains(seriesId) && p.Kind != ReadingProfileKind.Implicit); @@ -155,6 +144,11 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService return profiles.First(p => p.Kind == ReadingProfileKind.Default); } + public async Task GetReadingProfileDtoForSeries(int userId, int seriesId, bool skipImplicit = false) + { + return mapper.Map(await GetReadingProfileForSeries(userId, seriesId, skipImplicit)); + } + public async Task UpdateParent(int userId, int seriesId, UserReadingProfileDto dto) { var parentProfile = await GetReadingProfileForSeries(userId, seriesId, true); @@ -176,8 +170,6 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService UpdateReaderProfileFields(profile, dto); unitOfWork.AppUserReadingProfileRepository.Update(profile); - // await DeleteImplicateReadingProfilesForSeries(userId, profile.SeriesIds); - await unitOfWork.CommitAsync(); return mapper.Map(profile); } @@ -187,11 +179,11 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); if (user == null) throw new UnauthorizedAccessException(); - var other = await unitOfWork.AppUserReadingProfileRepository.GetProfileByName(userId, dto.Name); - if (other != null) throw new KavitaException("name-already-in-use"); + if (await unitOfWork.AppUserReadingProfileRepository.IsProfileNameInUse(userId, dto.Name)) throw new KavitaException("name-already-in-use"); var newProfile = new AppUserReadingProfileBuilder(user.Id).Build(); UpdateReaderProfileFields(newProfile, dto); + unitOfWork.AppUserReadingProfileRepository.Add(newProfile); user.ReadingProfiles.Add(newProfile); @@ -200,27 +192,61 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService return mapper.Map(newProfile); } + /// + /// Promotes the implicit profile to a user profile. Removes the series from other profiles. + /// + /// + /// + /// public async Task PromoteImplicitProfile(int userId, int profileId) { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); - if (profile == null) throw new KavitaException("profile-does-not-exist"); - if (profile.Kind != ReadingProfileKind.Implicit) throw new KavitaException("cannot-promote-non-implicit-profile"); + // Get all the user's profiles including the implicit + var allUserProfiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId, false); + var profileToPromote = allUserProfiles.First(r => r.Id == profileId); + var seriesId = profileToPromote.SeriesIds[0]; // An Implicit series can only be bound to 1 Series - // Implicit profiles are only bound to one profile - var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(profile.SeriesIds[0]); + // Check if there are any reading profiles (Series) already bound to the series + var existingSeriesProfile = allUserProfiles.FirstOrDefault(r => r.SeriesIds.Contains(seriesId) && r.Kind == ReadingProfileKind.User); + if (existingSeriesProfile != null) + { + existingSeriesProfile.SeriesIds.Remove(seriesId); + unitOfWork.AppUserReadingProfileRepository.Update(existingSeriesProfile); + } + + // Convert the implicit profile into a proper Series + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); if (series == null) throw new KavitaException("series-doesnt-exist"); // Shouldn't happen - await RemoveSeriesFromUserProfiles(userId, [series.Id]); + profileToPromote.Kind = ReadingProfileKind.User; + profileToPromote.Name = await localizationService.Translate(userId, "generated-reading-profile-name", series.Name); + profileToPromote.NormalizedName = profileToPromote.Name.ToNormalized(); + unitOfWork.AppUserReadingProfileRepository.Update(profileToPromote); - profile.Kind = ReadingProfileKind.User; - profile.Name = await localizationService.Translate(userId, "generated-reading-profile-name", series.Name); - profile.NormalizedName = profile.Name.ToNormalized(); - - unitOfWork.AppUserReadingProfileRepository.Update(profile); await unitOfWork.CommitAsync(); - return mapper.Map(profile); + return mapper.Map(profileToPromote); + + + // var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); + // if (profile == null) throw new KavitaException("profile-does-not-exist"); + // + // if (profile.Kind != ReadingProfileKind.Implicit) throw new KavitaException("cannot-promote-non-implicit-profile"); + // + // // Implicit profiles are only bound to one profile + // var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(profile.SeriesIds[0]); + // if (series == null) throw new KavitaException("series-doesnt-exist"); // Shouldn't happen + // + // await RemoveSeriesFromUserProfiles(userId, [series.Id]); + // + // profile.Kind = ReadingProfileKind.User; + // profile.Name = await localizationService.Translate(userId, "generated-reading-profile-name", series.Name); + // profile.NormalizedName = profile.Name.ToNormalized(); + // + // unitOfWork.AppUserReadingProfileRepository.Update(profile); + // await unitOfWork.CommitAsync(); + // + // return mapper.Map(profile); } public async Task UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto) @@ -368,6 +394,7 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any()) .Where(rp => rp.Kind == ReadingProfileKind.User) .ToList(); + unitOfWork.AppUserReadingProfileRepository.RemoveRange(userProfiles); } diff --git a/UI/Web/src/app/_services/reading-profile.service.ts b/UI/Web/src/app/_services/reading-profile.service.ts index 341e44a91..41953e092 100644 --- a/UI/Web/src/app/_services/reading-profile.service.ts +++ b/UI/Web/src/app/_services/reading-profile.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import {HttpClient} from "@angular/common/http"; import {environment} from "../../environments/environment"; import {ReadingProfile} from "../_models/preferences/reading-profiles"; @@ -17,51 +17,51 @@ export class ReadingProfileService { } updateProfile(profile: ReadingProfile) { - return this.httpClient.post(this.baseUrl + "ReadingProfile", profile); + return this.httpClient.post(this.baseUrl + 'reading-profile', profile); } updateParentProfile(seriesId: number, profile: ReadingProfile) { - return this.httpClient.post(this.baseUrl + `ReadingProfile/update-parent?seriesId=${seriesId}`, profile); + return this.httpClient.post(this.baseUrl + `reading-profile/update-parent?seriesId=${seriesId}`, profile); } createProfile(profile: ReadingProfile) { - return this.httpClient.post(this.baseUrl + "ReadingProfile/create", profile); + return this.httpClient.post(this.baseUrl + 'reading-profile/create', profile); } promoteProfile(profileId: number) { - return this.httpClient.post(this.baseUrl + "ReadingProfile/promote?profileId=" + profileId, {}); + return this.httpClient.post(this.baseUrl + "reading-profile/promote?profileId=" + profileId, {}); } updateImplicit(profile: ReadingProfile, seriesId: number) { - return this.httpClient.post(this.baseUrl + "ReadingProfile/series?seriesId="+seriesId, profile); + return this.httpClient.post(this.baseUrl + "reading-profile/series?seriesId="+seriesId, profile); } all() { - return this.httpClient.get(this.baseUrl + "ReadingProfile/all"); + return this.httpClient.get(this.baseUrl + 'reading-profile/all'); } delete(id: number) { - return this.httpClient.delete(this.baseUrl + "ReadingProfile?profileId="+id); + return this.httpClient.delete(this.baseUrl + `reading-profile?profileId=${id}`); } addToSeries(id: number, seriesId: number) { - return this.httpClient.post(this.baseUrl + `ReadingProfile/series/${seriesId}?profileId=${id}`, {}); + return this.httpClient.post(this.baseUrl + `reading-profile/series/${seriesId}?profileId=${id}`, {}); } clearSeriesProfiles(seriesId: number) { - return this.httpClient.delete(this.baseUrl + `ReadingProfile/series/${seriesId}`, {}); + return this.httpClient.delete(this.baseUrl + `reading-profile/series/${seriesId}`, {}); } addToLibrary(id: number, libraryId: number) { - return this.httpClient.post(this.baseUrl + `ReadingProfile/library/${libraryId}?profileId=${id}`, {}); + return this.httpClient.post(this.baseUrl + `reading-profile/library/${libraryId}?profileId=${id}`, {}); } clearLibraryProfiles(libraryId: number) { - return this.httpClient.delete(this.baseUrl + `ReadingProfile/library/${libraryId}`, {}); + return this.httpClient.delete(this.baseUrl + `reading-profile/library/${libraryId}`, {}); } bulkAddToSeries(id: number, seriesIds: number[]) { - return this.httpClient.post(this.baseUrl + `ReadingProfile/bulk?profileId=${id}`, seriesIds); + return this.httpClient.post(this.baseUrl + `reading-profile/bulk?profileId=${id}`, seriesIds); } } diff --git a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts index 2d1e0a15f..8d3356bd0 100644 --- a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts +++ b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts @@ -432,8 +432,9 @@ export class ReaderSettingsComponent implements OnInit { this.readingProfileService.promoteProfile(this.readingProfile.id).subscribe(newProfile => { this.readingProfile = newProfile; this.parentReadingProfile = newProfile; // profile is no longer implicit - this.toastr.success(translate("manga-reader.reading-profile-promoted")); this.cdRef.markForCheck(); + + this.toastr.success(translate("manga-reader.reading-profile-promoted")); }); } 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 97bbde8fd..79ab7073b 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 @@ -44,19 +44,20 @@
- + {{readingProfileForm.get('name')!.value}} - + @if (selectedProfile.id !== 0) {
-
} @@ -233,12 +234,12 @@
- {{ widthOverrideLabel }} + {{ widthOverrideLabel | sentenceCase }} 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 689358425..8da31a79f 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 @@ -158,7 +158,7 @@ export class ManageReadingProfilesComponent implements OnInit { get widthOverrideLabel() { const rawVal = this.readingProfileForm?.get('widthOverride')!.value; if (!rawVal) { - return translate('off'); + return translate('reader-settings.off'); } const val = parseInt(rawVal); diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index fa61bf94d..7d10cae50 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1978,7 +1978,7 @@ "height": "Height", "width": "Width", "width-override-label": "Width Override", - "off": "Off", + "off": "{{reader-settings.off}}", "original": "Original", "auto-close-menu-label": "{{manage-reading-profiles.auto-close-menu-label}}", "swipe-enabled-label": "Swipe Enabled", @@ -2886,8 +2886,8 @@ "pdf-theme-tooltip": "Color theme of the reader", "reading-profile-series-settings-title": "Series", - - "reading-profile-library-settings-title": "Library" + "reading-profile-library-settings-title": "Library", + "delete": "{{common.delete}}" },