Make reading profile buttons more user-friendly

Updating a profile no longer deletes all implicit profiles
This commit is contained in:
Amelia 2025-06-06 13:24:25 +02:00
parent 45a44480e1
commit 82f557490a
9 changed files with 96 additions and 87 deletions

View file

@ -67,13 +67,8 @@ public class ReadingProfileServiceTest: AbstractDbTest
Assert.NotNull(seriesProfile);
Assert.Equal("Implicit Profile", seriesProfile.Name);
await rps.UpdateReadingProfile(user.Id, new UserReadingProfileDto
{
Id = profile2.Id,
WidthOverride = 23,
});
seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id);
// Find parent
seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id, true);
Assert.NotNull(seriesProfile);
Assert.Equal("Non-implicit Profile", seriesProfile.Name);
}
@ -260,7 +255,7 @@ public class ReadingProfileServiceTest: AbstractDbTest
}
[Fact]
public async Task BatchAddReadingProfiles()
public async Task BulkAddReadingProfiles()
{
await ResetDb();
var (rps, user, lib, series) = await Setup();
@ -309,43 +304,7 @@ public class ReadingProfileServiceTest: AbstractDbTest
}
[Fact]
public async Task UpdateDeletesImplicit()
{
await ResetDb();
var (rps, user, lib, series) = await Setup();
var implicitProfile = Mapper.Map<UserReadingProfileDto>(new AppUserReadingProfileBuilder(user.Id)
.Build());
var profile = new AppUserReadingProfileBuilder(user.Id)
.WithName("Profile 1")
.Build();
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.GetReadingProfileDtoForSeries(user.Id, series.Id);
Assert.NotNull(seriesProfile);
Assert.Equal(ReadingProfileKind.Implicit, seriesProfile.Kind);
var profileDto = Mapper.Map<UserReadingProfileDto>(profile);
await rps.UpdateReadingProfile(user.Id, profileDto);
seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id);
Assert.NotNull(seriesProfile);
Assert.Equal(ReadingProfileKind.User, seriesProfile.Kind);
var implicitCount = await Context.AppUserReadingProfiles
.Where(p => p.Kind == ReadingProfileKind.Implicit)
.CountAsync();
Assert.Equal(0, implicitCount);
}
[Fact]
public async Task BatchUpdateDeletesImplicit()
public async Task BulkAssignDeletesImplicit()
{
await ResetDb();
var (rps, user, lib, series) = await Setup();
@ -574,18 +533,22 @@ public class ReadingProfileServiceTest: AbstractDbTest
[Fact]
public void UpdateFields_UpdatesAll()
{
var profile = new AppUserReadingProfile();
var dto = new UserReadingProfileDto();
// Repeat to ensure booleans are flipped and actually tested
for (int i = 0; i < 10; i++)
{
var profile = new AppUserReadingProfile();
var dto = new UserReadingProfileDto();
RandfHelper.SetRandomValues(profile);
RandfHelper.SetRandomValues(dto);
RandfHelper.SetRandomValues(profile);
RandfHelper.SetRandomValues(dto);
ReadingProfileService.UpdateReaderProfileFields(profile, dto);
ReadingProfileService.UpdateReaderProfileFields(profile, dto);
var newDto = Mapper.Map<UserReadingProfileDto>(profile);
var newDto = Mapper.Map<UserReadingProfileDto>(profile);
Assert.True(RandfHelper.AreSimpleFieldsEqual(dto, newDto,
["<Id>k__BackingField", "<AppUserId>k__BackingField", "<Implicit>k__BackingField"]));
Assert.True(RandfHelper.AreSimpleFieldsEqual(dto, newDto,
["<Id>k__BackingField", "<UserId>k__BackingField"]));
}
}

View file

@ -35,11 +35,12 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
/// Series -> Library -> Default
/// </summary>
/// <param name="seriesId"></param>
/// <param name="skipImplicit"></param>
/// <returns></returns>
[HttpGet("{seriesId}")]
public async Task<ActionResult<UserReadingProfileDto>> GetProfileForSeries(int seriesId)
[HttpGet("{seriesId:int}")]
public async Task<ActionResult<UserReadingProfileDto>> GetProfileForSeries(int seriesId, [FromQuery] bool skipImplicit)
{
return Ok(await readingProfileService.GetReadingProfileDtoForSeries(User.GetUserId(), seriesId));
return Ok(await readingProfileService.GetReadingProfileDtoForSeries(User.GetUserId(), seriesId, skipImplicit));
}
/// <summary>
@ -126,7 +127,7 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
/// <param name="seriesId"></param>
/// <param name="profileId"></param>
/// <returns></returns>
[HttpPost("series/{seriesId}")]
[HttpPost("series/{seriesId:int}")]
public async Task<IActionResult> AddProfileToSeries(int seriesId, [FromQuery] int profileId)
{
await readingProfileService.AddProfileToSeries(User.GetUserId(), profileId, seriesId);
@ -138,7 +139,7 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpDelete("series/{seriesId}")]
[HttpDelete("series/{seriesId:int}")]
public async Task<IActionResult> ClearSeriesProfile(int seriesId)
{
await readingProfileService.ClearSeriesProfile(User.GetUserId(), seriesId);
@ -151,7 +152,7 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
/// <param name="libraryId"></param>
/// <param name="profileId"></param>
/// <returns></returns>
[HttpPost("library/{libraryId}")]
[HttpPost("library/{libraryId:int}")]
public async Task<IActionResult> AddProfileToLibrary(int libraryId, [FromQuery] int profileId)
{
await readingProfileService.AddProfileToLibrary(User.GetUserId(), profileId, libraryId);
@ -164,7 +165,7 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
/// <param name="libraryId"></param>
/// <param name="profileId"></param>
/// <returns></returns>
[HttpDelete("library/{libraryId}")]
[HttpDelete("library/{libraryId:int}")]
public async Task<IActionResult> ClearLibraryProfile(int libraryId)
{
await readingProfileService.ClearLibraryProfile(User.GetUserId(), libraryId);

View file

@ -22,8 +22,9 @@ public interface IReadingProfileService
/// </summary>
/// <param name="userId"></param>
/// <param name="seriesId"></param>
/// <param name="skipImplicit"></param>
/// <returns></returns>
Task<UserReadingProfileDto> GetReadingProfileDtoForSeries(int userId, int seriesId);
Task<UserReadingProfileDto> GetReadingProfileDtoForSeries(int userId, int seriesId, bool skipImplicit = false);
/// <summary>
/// Creates a new reading profile for a user. Name must be unique per user
@ -60,7 +61,7 @@ public interface IReadingProfileService
Task<UserReadingProfileDto> UpdateParent(int userId, int seriesId, UserReadingProfileDto dto);
/// <summary>
/// Updates a given reading profile for a user, and deletes all implicit profiles
/// Updates a given reading profile for a user
/// </summary>
/// <param name="userId"></param>
/// <param name="dto"></param>
@ -123,9 +124,9 @@ public interface IReadingProfileService
public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper): IReadingProfileService
{
public async Task<UserReadingProfileDto> GetReadingProfileDtoForSeries(int userId, int seriesId)
public async Task<UserReadingProfileDto> GetReadingProfileDtoForSeries(int userId, int seriesId, bool skipImplicit = false)
{
return mapper.Map<UserReadingProfileDto>(await GetReadingProfileForSeries(userId, seriesId));
return mapper.Map<UserReadingProfileDto>(await GetReadingProfileForSeries(userId, seriesId, skipImplicit));
}
public async Task<AppUserReadingProfile> GetReadingProfileForSeries(int userId, int seriesId, bool skipImplicit = false)
@ -175,7 +176,7 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
UpdateReaderProfileFields(profile, dto);
unitOfWork.AppUserReadingProfileRepository.Update(profile);
await DeleteImplicateReadingProfilesForSeries(userId, profile.SeriesIds);
// await DeleteImplicateReadingProfilesForSeries(userId, profile.SeriesIds);
await unitOfWork.CommitAsync();
return mapper.Map<UserReadingProfileDto>(profile);

View file

@ -12,8 +12,8 @@ export class ReadingProfileService {
constructor(private httpClient: HttpClient) { }
getForSeries(seriesId: number) {
return this.httpClient.get<ReadingProfile>(this.baseUrl + "ReadingProfile/"+seriesId);
getForSeries(seriesId: number, skipImplicit: boolean = false) {
return this.httpClient.get<ReadingProfile>(this.baseUrl + `ReadingProfile/${seriesId}?skipImplicit=${skipImplicit}`);
}
updateProfile(profile: ReadingProfile) {

View file

@ -170,9 +170,9 @@
<div class="d-flex gap-2 mt-2 flex-wrap mb-2">
<button class="btn btn-primary"
[disabled]="readingProfile.kind !== ReadingProfileKind.Implicit"
[disabled]="readingProfile.kind !== ReadingProfileKind.Implicit || !parentReadingProfile"
(click)="updateParentPref()">
{{ t('update-parent') }}
{{ t('update-parent', {name: parentReadingProfile?.name || t('loading')}) }}
</button>
<button class="btn btn-primary"
[ngbTooltip]="t('create-new-tooltip')"

View file

@ -165,6 +165,11 @@ export class ReaderSettingsComponent implements OnInit {
settingsForm: FormGroup = new FormGroup({});
/**
* The reading profile itself, unless readingProfile is implicit
*/
parentReadingProfile: ReadingProfile | null = null;
/**
* System provided themes
*/
@ -190,6 +195,14 @@ export class ReaderSettingsComponent implements OnInit {
private toastr: ToastrService) {}
ngOnInit(): void {
if (this.readingProfile.kind === ReadingProfileKind.Implicit) {
this.readingProfileService.getForSeries(this.seriesId, true).subscribe(parent => {
this.parentReadingProfile = parent;
this.cdRef.markForCheck();
})
} else {
this.parentReadingProfile = this.readingProfile;
}
this.fontFamilies = this.bookService.getFontFamilies();
this.fontOptions = this.fontFamilies.map(f => f.title);
@ -325,6 +338,10 @@ export class ReaderSettingsComponent implements OnInit {
updateImplicit() {
this.readingProfileService.updateImplicit(this.packReadingProfile(), this.seriesId).subscribe({
next: newProfile => {
this.readingProfile = newProfile;
this.cdRef.markForCheck();
},
error: err => {
console.error(err);
}
@ -414,6 +431,7 @@ 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();
});

View file

@ -310,8 +310,17 @@
<div class="col-md-6 col-sm-12">
<button class="btn btn-primary" [disabled]="readingProfile.kind !== ReadingProfileKind.Implicit" (click)="updateParentPref()">{{t('update-parent')}}</button>
<button class="btn btn-primary ms-2" [ngbTooltip]="t('create-new-tooltip')" [disabled]="readingProfile.kind !== ReadingProfileKind.Implicit" (click)="createNewProfileFromImplicit()">{{t('create-new')}}</button>
<button class="btn btn-primary"
[disabled]="readingProfile.kind !== ReadingProfileKind.Implicit || !parentReadingProfile"
(click)="updateParentPref()">
{{ t('update-parent', {name: parentReadingProfile?.name || t('loading')}) }}
</button>
<button class="btn btn-primary ms-2"
[ngbTooltip]="t('create-new-tooltip')"
[disabled]="readingProfile.kind !== ReadingProfileKind.Implicit"
(click)="createNewProfileFromImplicit()">
{{ t('create-new') }}
</button>
</div>
</div>
</form>

View file

@ -205,6 +205,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
totalSeriesPagesRead = 0;
user!: User;
readingProfile!: ReadingProfile;
/**
* The reading profile itself, unless readingProfile is implicit
*/
parentReadingProfile: ReadingProfile | null = null;
generalSettingsForm!: FormGroup;
readingDirection = ReadingDirection.LeftToRight;
@ -491,17 +495,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => {
this.readingProfile = data['readingProfile'];
if (this.readingProfile == null) {
this.router.navigateByUrl('/home');
return;
}
this.setupReaderSettings();
this.cdRef.markForCheck();
});
this.getPageFn = this.getPage.bind(this);
this.libraryId = parseInt(libraryId, 10);
@ -510,6 +503,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.incognitoMode = this.route.snapshot.queryParamMap.get('incognitoMode') === 'true';
this.bookmarkMode = this.route.snapshot.queryParamMap.get('bookmarkMode') === 'true';
this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => {
this.readingProfile = data['readingProfile'];
if (this.readingProfile == null) {
this.router.navigateByUrl('/home');
return;
}
// Requires seriesId to be set
this.setupReaderSettings();
this.cdRef.markForCheck();
});
const readingListId = this.route.snapshot.queryParamMap.get('readingListId');
if (readingListId != null) {
this.readingListMode = true;
@ -644,6 +648,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
setupReaderSettings() {
if (this.readingProfile.kind === ReadingProfileKind.Implicit) {
this.readingProfileService.getForSeries(this.seriesId, true).subscribe(parent => {
this.parentReadingProfile = parent;
this.cdRef.markForCheck();
})
} else {
this.parentReadingProfile = this.readingProfile;
}
this.readingDirection = this.readingProfile.readingDirection;
this.scalingOption = this.readingProfile.scalingOption;
this.pageSplitOption = this.readingProfile.pageSplitOption;
@ -1805,6 +1819,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
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();
});

View file

@ -1132,8 +1132,9 @@
"line-spacing-label": "{{manage-reading-profiles.line-height-book-label}}",
"margin-label": "{{manage-reading-profiles.margin-book-label}}",
"reset-to-defaults": "Reset to Defaults",
"update-parent": "Update parent profile",
"create-new": "Promote profile",
"update-parent": "Save to {{name}}",
"loading": "loading",
"create-new": "New profile from implicit",
"create-new-tooltip": "Create a new manageable profile from your current implicit one",
"reading-profile-updated": "Reading profile updated",
"reading-profile-promoted": "Reading profile promoted",
@ -1952,9 +1953,10 @@
"manga-reader": {
"back": "Back",
"update-parent": "Update parent profile",
"create-new": "Promote profile",
"create-new-tooltip": "Create a new manageable profile from your current implicit one",
"update-parent": "{{reader-settings.update-parent}}",
"loading": "{{reader-settings.loading}}",
"create-new": "{{reader-settings.create-new}}",
"create-new-tooltip": "{{reader-settings.create-new-tooltip}}",
"incognito-alt": "Incognito mode is on. Toggle to turn off.",
"incognito-title": "Incognito Mode:",
"shortcuts-menu-alt": "Keyboard Shortcuts Modal",