Update some flows

- Promote/Create new from implicit profile
- Fix save to parent updating implicit instead
- Remove skip in the manga reader as the resolver has made it redundant
- Add confirm to delete profile flow
This commit is contained in:
Amelia 2025-06-05 20:52:11 +02:00
parent 9109988514
commit 45a44480e1
13 changed files with 289 additions and 106 deletions

View file

@ -42,22 +42,6 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
return Ok(await readingProfileService.GetReadingProfileDtoForSeries(User.GetUserId(), seriesId));
}
/// <summary>
/// Updates the given reading profile, must belong to the current user
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
/// <remarks>
/// This does not update connected series, and libraries.
/// Deletes all implicit profiles for series linked to this profile
/// </remarks>
[HttpPost]
public async Task<ActionResult> UpdateReadingProfile([FromBody] UserReadingProfileDto dto)
{
await readingProfileService.UpdateReadingProfile(User.GetUserId(), dto);
return Ok();
}
/// <summary>
/// Creates a new reading profile for the current user
/// </summary>
@ -69,6 +53,17 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
return Ok(await readingProfileService.CreateReadingProfile(User.GetUserId(), dto));
}
/// <summary>
/// Promotes the implicit profile to a user profile. Removes the series from other profiles
/// </summary>
/// <param name="profileId"></param>
/// <returns></returns>
[HttpPost("promote")]
public async Task<ActionResult<UserReadingProfileDto>> PromoteImplicitReadingProfile([FromQuery] int profileId)
{
return Ok(await readingProfileService.PromoteImplicitProfile(User.GetUserId(), profileId));
}
/// <summary>
/// Update the implicit reading profile for a series, creates one if none exists
/// </summary>
@ -76,10 +71,39 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpPost("series")]
public async Task<ActionResult> UpdateReadingProfileForSeries([FromBody] UserReadingProfileDto dto, [FromQuery] int seriesId)
public async Task<ActionResult<UserReadingProfileDto>> UpdateReadingProfileForSeries([FromBody] UserReadingProfileDto dto, [FromQuery] int seriesId)
{
await readingProfileService.UpdateImplicitReadingProfile(User.GetUserId(), seriesId, dto);
return Ok();
var updatedProfile = await readingProfileService.UpdateImplicitReadingProfile(User.GetUserId(), seriesId, dto);
return Ok(updatedProfile);
}
/// <summary>
/// Updates the non-implicit reading profile for the given series, and removes implicit profiles
/// </summary>
/// <param name="dto"></param>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpPost("update-parent")]
public async Task<ActionResult<UserReadingProfileDto>> UpdateParentProfileForSeries([FromBody] UserReadingProfileDto dto, [FromQuery] int seriesId)
{
var newParentProfile = await readingProfileService.UpdateParent(User.GetUserId(), seriesId, dto);
return Ok(newParentProfile);
}
/// <summary>
/// Updates the given reading profile, must belong to the current user
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
/// <remarks>
/// This does not update connected series, and libraries.
/// Deletes all implicit profiles for series linked to this profile
/// </remarks>
[HttpPost]
public async Task<ActionResult<UserReadingProfileDto>> UpdateReadingProfile([FromBody] UserReadingProfileDto dto)
{
var newProfile = await readingProfileService.UpdateReadingProfile(User.GetUserId(), dto);
return Ok(newProfile);
}
/// <summary>

View file

@ -107,8 +107,6 @@ public class UsersController : BaseApiController
existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries;
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
existingPreferences.AllowAutomaticWebtoonReaderDetection = preferencesDto.AllowAutomaticWebtoonReaderDetection;
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
existingPreferences.ShareReviews = preferencesDto.ShareReviews;

View file

@ -230,6 +230,8 @@
"scan-libraries": "Scan Libraries",
"kavita+-data-refresh": "Kavita+ Data Refresh",
"backup": "Backup",
"update-yearly-stats": "Update Yearly Stats"
"update-yearly-stats": "Update Yearly Stats",
"generated-reading-profile-name": "Generated from {0}"
}

View file

@ -25,15 +25,6 @@ public interface IReadingProfileService
/// <returns></returns>
Task<UserReadingProfileDto> GetReadingProfileDtoForSeries(int userId, int seriesId);
/// <summary>
/// Updates a given reading profile for a user, and deletes all implicit profiles
/// </summary>
/// <param name="userId"></param>
/// <param name="dto"></param>
/// <returns></returns>
/// <remarks>Does not update connected series and libraries</remarks>
Task UpdateReadingProfile(int userId, UserReadingProfileDto dto);
/// <summary>
/// Creates a new reading profile for a user. Name must be unique per user
/// </summary>
@ -42,6 +33,14 @@ public interface IReadingProfileService
/// <returns></returns>
Task<UserReadingProfileDto> CreateReadingProfile(int userId, UserReadingProfileDto dto);
/// <summary>
/// Promotes the implicit profile to a user profile. Removes the series from other profiles
/// </summary>
/// <param name="userId"></param>
/// <param name="profileId"></param>
/// <returns></returns>
Task<UserReadingProfileDto> PromoteImplicitProfile(int userId, int profileId);
/// <summary>
/// Updates the implicit reading profile for a series, creates one if none exists
/// </summary>
@ -49,7 +48,25 @@ public interface IReadingProfileService
/// <param name="seriesId"></param>
/// <param name="dto"></param>
/// <returns></returns>
Task UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto);
Task<UserReadingProfileDto> UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto);
/// <summary>
/// Updates the non-implicit reading profile for the given series, and removes implicit profiles
/// </summary>
/// <param name="userId"></param>
/// <param name="seriesId"></param>
/// <param name="dto"></param>
/// <returns></returns>
Task<UserReadingProfileDto> UpdateParent(int userId, int seriesId, UserReadingProfileDto dto);
/// <summary>
/// Updates a given reading profile for a user, and deletes all implicit profiles
/// </summary>
/// <param name="userId"></param>
/// <param name="dto"></param>
/// <returns></returns>
/// <remarks>Does not update connected series and libraries</remarks>
Task<UserReadingProfileDto> UpdateReadingProfile(int userId, UserReadingProfileDto dto);
/// <summary>
/// Deletes a given profile for a user
@ -111,13 +128,16 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
return mapper.Map<UserReadingProfileDto>(await GetReadingProfileForSeries(userId, seriesId));
}
public async Task<AppUserReadingProfile> GetReadingProfileForSeries(int userId, int seriesId)
public async Task<AppUserReadingProfile> GetReadingProfileForSeries(int userId, int seriesId, bool skipImplicit = false)
{
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;
if (!skipImplicit)
{
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);
@ -134,7 +154,20 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
return profiles.First(p => p.Kind == ReadingProfileKind.Default);
}
public async Task UpdateReadingProfile(int userId, UserReadingProfileDto dto)
public async Task<UserReadingProfileDto> UpdateParent(int userId, int seriesId, UserReadingProfileDto dto)
{
var parentProfile = await GetReadingProfileForSeries(userId, seriesId, true);
UpdateReaderProfileFields(parentProfile, dto, false);
unitOfWork.AppUserReadingProfileRepository.Update(parentProfile);
await DeleteImplicateReadingProfilesForSeries(userId, [seriesId]);
await unitOfWork.CommitAsync();
return mapper.Map<UserReadingProfileDto>(parentProfile);
}
public async Task<UserReadingProfileDto> UpdateReadingProfile(int userId, UserReadingProfileDto dto)
{
var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, dto.Id);
if (profile == null) throw new KavitaException("profile-does-not-exist");
@ -145,6 +178,7 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
await DeleteImplicateReadingProfilesForSeries(userId, profile.SeriesIds);
await unitOfWork.CommitAsync();
return mapper.Map<UserReadingProfileDto>(profile);
}
public async Task<UserReadingProfileDto> CreateReadingProfile(int userId, UserReadingProfileDto dto)
@ -165,7 +199,30 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
return mapper.Map<UserReadingProfileDto>(newProfile);
}
public async Task UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto)
public async Task<UserReadingProfileDto> 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");
// 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<UserReadingProfileDto>(profile);
}
public async Task<UserReadingProfileDto> UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto)
{
var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences);
if (user == null) throw new UnauthorizedAccessException();
@ -179,7 +236,8 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
UpdateReaderProfileFields(existingProfile, dto, false);
unitOfWork.AppUserReadingProfileRepository.Update(existingProfile);
await unitOfWork.CommitAsync();
return;
return mapper.Map<UserReadingProfileDto>(existingProfile);
}
var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId) ?? throw new KeyNotFoundException();
@ -195,6 +253,8 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
user.ReadingProfiles.Add(newProfile);
await unitOfWork.CommitAsync();
return mapper.Map<UserReadingProfileDto>(newProfile);
}
public async Task DeleteReadingProfile(int userId, int profileId)
@ -300,6 +360,16 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
unitOfWork.AppUserReadingProfileRepository.RemoveRange(implicitProfiles);
}
private async Task RemoveSeriesFromUserProfiles(int userId, IList<int> seriesIds)
{
var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId);
var userProfiles = profiles
.Where(rp => rp.SeriesIds.Intersect(seriesIds).Any())
.Where(rp => rp.Kind == ReadingProfileKind.User)
.ToList();
unitOfWork.AppUserReadingProfileRepository.RemoveRange(userProfiles);
}
public static void UpdateReaderProfileFields(AppUserReadingProfile existingProfile, UserReadingProfileDto dto, bool updateName = true)
{
if (updateName && !string.IsNullOrEmpty(dto.Name) && existingProfile.NormalizedName != dto.Name.ToNormalized())

View file

@ -17,15 +17,23 @@ export class ReadingProfileService {
}
updateProfile(profile: ReadingProfile) {
return this.httpClient.post(this.baseUrl + "ReadingProfile", profile);
return this.httpClient.post<ReadingProfile>(this.baseUrl + "ReadingProfile", profile);
}
updateParentProfile(seriesId: number, profile: ReadingProfile) {
return this.httpClient.post<ReadingProfile>(this.baseUrl + `ReadingProfile/update-parent?seriesId=${seriesId}`, profile);
}
createProfile(profile: ReadingProfile) {
return this.httpClient.post<ReadingProfile>(this.baseUrl + "ReadingProfile/create", profile);
}
promoteProfile(profileId: number) {
return this.httpClient.post<ReadingProfile>(this.baseUrl + "ReadingProfile/promote?profileId=" + profileId, {});
}
updateImplicit(profile: ReadingProfile, seriesId: number) {
return this.httpClient.post(this.baseUrl + "ReadingProfile/series?seriesId="+seriesId, profile);
return this.httpClient.post<ReadingProfile>(this.baseUrl + "ReadingProfile/series?seriesId="+seriesId, profile);
}
all() {

View file

@ -618,7 +618,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.router.navigateByUrl('/home');
return;
}
//this.setupReaderSettings(); // TODO: Implement this Amelia
//this.setupReaderSettings(); // TODO: Implement this Amelia, it works without am I missing something?
this.cdRef.markForCheck();
});

View file

@ -168,10 +168,21 @@
</div>
</div>
<div class="row g-0 justify-content-between mt-2">
<button (click)="savePref()" class="btn btn-primary col">{{t('save-global')}}</button>
<div class="d-flex gap-2 mt-2 flex-wrap mb-2">
<button class="btn btn-primary"
[disabled]="readingProfile.kind !== ReadingProfileKind.Implicit"
(click)="updateParentPref()">
{{ t('update-parent') }}
</button>
<button class="btn btn-primary"
[ngbTooltip]="t('create-new-tooltip')"
[disabled]="readingProfile.kind !== ReadingProfileKind.Implicit"
(click)="createNewProfileFromImplicit()">
{{ t('create-new') }}
</button>
</div>
</div>
</form>

View file

@ -36,10 +36,11 @@ import {
NgbAccordionItem,
NgbTooltip
} from '@ng-bootstrap/ng-bootstrap';
import {TranslocoDirective} from "@jsverse/transloco";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {ReadingProfileService} from "../../../_services/reading-profile.service";
import {ReadingProfile} from "../../../_models/preferences/reading-profiles";
import {ReadingProfile, ReadingProfileKind} from "../../../_models/preferences/reading-profiles";
import {debounceTime, distinctUntilChanged, tap} from "rxjs/operators";
import {ToastrService} from "ngx-toastr";
/**
* Used for book reader. Do not use for other components
@ -185,7 +186,8 @@ export class ReaderSettingsComponent implements OnInit {
constructor(private bookService: BookService, private accountService: AccountService,
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService,
private readonly cdRef: ChangeDetectorRef, private readingProfileService: ReadingProfileService) {}
private readonly cdRef: ChangeDetectorRef, private readingProfileService: ReadingProfileService,
private toastr: ToastrService) {}
ngOnInit(): void {
@ -226,14 +228,13 @@ export class ReaderSettingsComponent implements OnInit {
this.layoutModeUpdate.emit(this.readingProfile.bookReaderLayoutMode);
this.immersiveMode.emit(this.readingProfile.bookReaderImmersiveMode);
this.resetSettings();
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.user = user;
} else {
this.resetSettings();
}
// User needs to be loaded before we call this
this.resetSettings();
});
}
@ -393,8 +394,29 @@ export class ReaderSettingsComponent implements OnInit {
this.fullscreen.emit();
}
savePref() {
this.readingProfileService.updateProfile(this.packReadingProfile()).subscribe()
// menu only code
updateParentPref() {
if (this.readingProfile.kind !== ReadingProfileKind.Implicit) {
return;
}
this.readingProfileService.updateParentProfile(this.seriesId, this.packReadingProfile()).subscribe(newProfile => {
this.readingProfile = newProfile;
this.toastr.success(translate('manga-reader.reading-profile-updated'));
this.cdRef.markForCheck();
});
}
createNewProfileFromImplicit() {
if (this.readingProfile.kind !== ReadingProfileKind.Implicit) {
return;
}
this.readingProfileService.promoteProfile(this.readingProfile.id).subscribe(newProfile => {
this.readingProfile = newProfile;
this.toastr.success(translate("manga-reader.reading-profile-promoted"));
this.cdRef.markForCheck();
});
}
private packReadingProfile(): ReadingProfile {
@ -417,4 +439,5 @@ export class ReaderSettingsComponent implements OnInit {
return data;
}
protected readonly ReadingProfileKind = ReadingProfileKind;
}

View file

@ -310,7 +310,8 @@
<div class="col-md-6 col-sm-12">
<button class="btn btn-primary" (click)="savePref()">{{t('save-globally')}}</button>
<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>
</div>
</div>
</form>

View file

@ -25,7 +25,6 @@ import {
merge,
Observable,
ReplaySubject,
skip,
Subject,
take,
tap
@ -33,7 +32,7 @@ import {
import {ChangeContext, LabelType, NgxSliderModule, Options} from '@angular-slider/ngx-slider';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr';
import {ShortcutsModalComponent} from 'src/app/reader-shared/_modals/shortcuts-modal/shortcuts-modal.component';
import {Stack} from 'src/app/shared/data-structures/stack';
@ -70,7 +69,12 @@ import {LoadingComponent} from '../../../shared/loading/loading.component';
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {shareReplay} from "rxjs/operators";
import {DblClickDirective} from "../../../_directives/dbl-click.directive";
import {layoutModes, pageSplitOptions, ReadingProfile} from "../../../_models/preferences/reading-profiles";
import {
layoutModes,
pageSplitOptions,
ReadingProfile,
ReadingProfileKind
} from "../../../_models/preferences/reading-profiles";
import {ReadingProfileService} from "../../../_services/reading-profile.service";
import {ConfirmService} from "../../../shared/confirm.service";
@ -125,10 +129,10 @@ enum KeyDirection {
])
])
],
imports: [NgStyle, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent,
DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent,
NgxSliderModule, ReactiveFormsModule, FittingIconPipe, ReaderModeIconPipe,
FullscreenIconPipe, TranslocoDirective, PercentPipe, NgClass, AsyncPipe, DblClickDirective]
imports: [NgStyle, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent,
DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent,
NgxSliderModule, ReactiveFormsModule, FittingIconPipe, ReaderModeIconPipe,
FullscreenIconPipe, TranslocoDirective, PercentPipe, NgClass, AsyncPipe, DblClickDirective, NgbTooltip]
})
export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ -537,14 +541,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
skip(1), // Skip the initial creation of the form, we do not want an implicit profile of this snapshot
takeUntilDestroyed(this.destroyRef),
map(_ => this.packReadingProfile()),
distinctUntilChanged(),
tap(newProfile => {
this.readingProfileService.updateImplicit(newProfile, this.seriesId).subscribe({
next: () => {
this.readingProfile = newProfile;
next: updatedProfile => {
this.readingProfile = updatedProfile;
},
error: err => {
console.error(err);
@ -1783,10 +1786,28 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
// menu only code
savePref() {
this.readingProfileService.updateProfile(this.packReadingProfile()).subscribe(_ => {
updateParentPref() {
if (this.readingProfile.kind !== ReadingProfileKind.Implicit) {
return;
}
this.readingProfileService.updateParentProfile(this.seriesId, this.packReadingProfile()).subscribe(newProfile => {
this.readingProfile = newProfile;
this.toastr.success(translate('manga-reader.reading-profile-updated'));
})
this.cdRef.markForCheck();
});
}
createNewProfileFromImplicit() {
if (this.readingProfile.kind !== ReadingProfileKind.Implicit) {
return;
}
this.readingProfileService.promoteProfile(this.readingProfile.id).subscribe(newProfile => {
this.readingProfile = newProfile;
this.toastr.success(translate("manga-reader.reading-profile-promoted"));
this.cdRef.markForCheck();
});
}
translatePrefOptions(o: {text: string, value: any}) {
@ -1811,4 +1832,5 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return data;
}
protected readonly ReadingProfileKind = ReadingProfileKind;
}

View file

@ -4,7 +4,7 @@
@if (!loading) {
<div class="position-relative">
<button class="btn btn-outline-primary position-absolute custom-position" (click)="addNew()" [title]="t('add')">
<button class="btn btn-outline-primary position-absolute custom-position" [ngbTooltip]="t('add-tooltip')" (click)="addNew()" [title]="t('add')">
<i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden ms-1">{{t('add')}}</span>
</button>
</div>
@ -55,7 +55,7 @@
@if (selectedProfile.id !== 0) {
<div class="d-flex justify-content-between">
<button class="btn btn-danger" (click)="delete(selectedProfile!.id)" [disabled]="selectedProfile.kind === ReadingProfileKind.Default">
<button class="btn btn-danger" (click)="delete(selectedProfile!)" [disabled]="selectedProfile.kind === ReadingProfileKind.Default">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>

View file

@ -14,7 +14,7 @@ import {
ReadingProfileKind,
scalingOptions
} from "../../_models/preferences/reading-profiles";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
import {NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
import {User} from "../../_models/user";
@ -41,11 +41,12 @@ 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, NgbNavContent, NgbNavItem, NgbNavLinkBase, NgbNavOutlet} from "@ng-bootstrap/ng-bootstrap";
import {NgbNav, NgbNavContent, NgbNavItem, NgbNavLinkBase, NgbNavOutlet, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
import {filter} from "rxjs";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {LoadingComponent} from "../../shared/loading/loading.component";
import {ToastrService} from "ngx-toastr";
import {ConfirmService} from "../../shared/confirm.service";
enum TabId {
ImageReader = "image-reader",
@ -83,6 +84,7 @@ enum TabId {
NgbNavContent,
NgbNavOutlet,
LoadingComponent,
NgbTooltip,
],
templateUrl: './manage-reading-profiles.component.html',
styleUrl: './manage-reading-profiles.component.scss',
@ -113,6 +115,8 @@ export class ManageReadingProfilesComponent implements OnInit {
private bookService: BookService,
private destroyRef: DestroyRef,
private toastr: ToastrService,
private confirmService: ConfirmService,
private transLoco: TranslocoService,
) {
this.fontFamilies = this.bookService.getFontFamilies().map(f => f.title);
this.cdRef.markForCheck();
@ -129,15 +133,24 @@ export class ManageReadingProfilesComponent implements OnInit {
this.readingProfiles = profiles;
this.loading = false;
this.setupForm();
const defaultProfile = this.readingProfiles.find(rp => rp.kind === ReadingProfileKind.Default);
this.selectProfile(defaultProfile);
this.cdRef.markForCheck();
});
}
delete(id: number) {
this.readingProfileService.delete(id).subscribe(() => {
async delete(readingProfile: ReadingProfile) {
if (!await this.confirmService.confirm(this.transLoco.translate("manage-reading-profiles.confirm", {name: readingProfile.name}))) {
return;
}
this.readingProfileService.delete(readingProfile.id).subscribe(() => {
this.selectProfile(undefined);
this.readingProfiles = this.readingProfiles.filter(o => o.id !== id);
this.readingProfiles = this.readingProfiles.filter(o => o.id !== readingProfile.id);
this.cdRef.markForCheck();
});
}
@ -204,39 +217,41 @@ export class ManageReadingProfilesComponent implements OnInit {
distinctUntilChanged(),
filter(_ => this.readingProfileForm!.valid),
takeUntilDestroyed(this.destroyRef),
tap(_ => {
if (this.selectedProfile!.id == 0) {
this.readingProfileService.createProfile(this.packData()).subscribe({
next: createdProfile => {
this.selectedProfile = createdProfile;
this.readingProfiles.push(createdProfile);
this.cdRef.markForCheck();
},
error: err => {
console.log(err);
this.toastr.error(err.message);
}
})
} else {
const profile = this.packData();
this.readingProfileService.updateProfile(profile).subscribe({
next: _ => {
this.readingProfiles = this.readingProfiles.map(p => {
if (p.id !== profile.id) return p;
return profile;
});
this.cdRef.markForCheck();
},
error: err => {
console.log(err);
this.toastr.error(err.message);
}
})
}
}),
tap(_ => this.autoSave()),
).subscribe();
}
private autoSave() {
if (this.selectedProfile!.id == 0) {
this.readingProfileService.createProfile(this.packData()).subscribe({
next: createdProfile => {
this.selectedProfile = createdProfile;
this.readingProfiles.push(createdProfile);
this.cdRef.markForCheck();
},
error: err => {
console.log(err);
this.toastr.error(err.message);
}
})
} else {
const profile = this.packData();
this.readingProfileService.updateProfile(profile).subscribe({
next: _ => {
this.readingProfiles = this.readingProfiles.map(p => {
if (p.id !== profile.id) return p;
return profile;
});
this.cdRef.markForCheck();
},
error: err => {
console.log(err);
this.toastr.error(err.message);
}
})
}
}
private packData(): ReadingProfile {
const data: ReadingProfile = this.readingProfileForm!.getRawValue();
data.id = this.selectedProfile!.id;

View file

@ -1132,7 +1132,11 @@
"line-spacing-label": "{{manage-reading-profiles.line-height-book-label}}",
"margin-label": "{{manage-reading-profiles.margin-book-label}}",
"reset-to-defaults": "Reset to Defaults",
"save-global": "Save to your reading profile",
"update-parent": "Update parent profile",
"create-new": "Promote profile",
"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",
"reader-settings-title": "Reader Settings",
"reading-direction-label": "{{manage-reading-profiles.reading-direction-book-label}}",
"right-to-left": "Right to Left",
@ -1948,7 +1952,9 @@
"manga-reader": {
"back": "Back",
"save-globally": "Save Globally",
"update-parent": "Update parent profile",
"create-new": "Promote profile",
"create-new-tooltip": "Create a new manageable profile from your current implicit one",
"incognito-alt": "Incognito mode is on. Toggle to turn off.",
"incognito-title": "Incognito Mode:",
"shortcuts-menu-alt": "Keyboard Shortcuts Modal",
@ -1985,6 +1991,7 @@
"no-next-chapter": "No Next Chapter",
"no-prev-chapter": "No Previous Chapter",
"reading-profile-updated": "Reading profile updated",
"reading-profile-promoted": "Reading profile promoted",
"emulate-comic-book-label": "{{manage-reading-profiles.emulate-comic-book-label}}",
"series-progress": "Series Progress: {{percentage}}"
},
@ -2813,8 +2820,10 @@
"profiles-title": "Your reading profiles",
"default-profile": "Default",
"add": "{{common.add}}",
"add-tooltip": "Your new profile will be saved after making a change to it",
"make-default": "Set as default",
"no-selected": "No profile selected",
"confirm": "Are you sure you want to delete the reading profile {{name}}?",
"selection-tip": "Select a profile from the list, or create a new one at the top right",
"image-reader-settings-title": "Image Reader",