First iteration of the UI

- Migrate current preferences over
- Set defaults in db
This commit is contained in:
Amelia 2025-05-18 01:25:24 +02:00
parent 5741a92bb2
commit 5656fb2148
26 changed files with 1246 additions and 728 deletions

View file

@ -7,6 +7,7 @@ using API.Helpers.Builders;
using API.Services; using API.Services;
using Kavita.Common; using Kavita.Common;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NSubstitute;
using Xunit; using Xunit;
namespace API.Tests.Services; namespace API.Tests.Services;
@ -29,12 +30,52 @@ public class ReadingProfileServiceTest: AbstractDbTest
user.Libraries.Add(library); user.Libraries.Add(library);
await UnitOfWork.CommitAsync(); await UnitOfWork.CommitAsync();
var rps = new ReadingProfileService(UnitOfWork); var rps = new ReadingProfileService(UnitOfWork, Substitute.For<ILocalizationService>());
user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.UserPreferences); user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.UserPreferences);
return (rps, user, library, series); return (rps, user, library, series);
} }
[Fact]
public async Task ImplicitProfileFirst()
{
await ResetDb();
var (rps, user, library, series) = await Setup();
var profile = new AppUserReadingProfileBuilder(user.Id)
.WithImplicit(true)
.WithSeries(series)
.WithName("Implicit Profile")
.Build();
var profile2 = new AppUserReadingProfileBuilder(user.Id)
.WithSeries(series)
.WithName("Non-implicit Profile")
.Build();
user.UserPreferences.ReadingProfiles.Add(profile);
user.UserPreferences.ReadingProfiles.Add(profile2);
await UnitOfWork.CommitAsync();
var seriesProfile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(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.DeleteImplicitForSeries(user.Id, series.Id);
seriesProfile = await UnitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(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] [Fact]
public async Task DeleteImplicitSeriesReadingProfile() public async Task DeleteImplicitSeriesReadingProfile()
{ {

View file

@ -1,9 +1,13 @@
#nullable enable #nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Data; using API.Data;
using API.Data.Repositories;
using API.DTOs; using API.DTOs;
using API.Extensions; using API.Extensions;
using API.Services; using API.Services;
using Kavita.Common;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -13,6 +17,16 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
IReadingProfileService readingProfileService): BaseApiController IReadingProfileService readingProfileService): BaseApiController
{ {
/// <summary>
/// Gets all non-implicit reading profiles for a user
/// </summary>
/// <returns></returns>
[HttpGet("all")]
public async Task<ActionResult<IList<UserReadingProfileDto>>> GetAllReadingProfiles()
{
return Ok(await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(User.GetUserId(), true));
}
/// <summary> /// <summary>
/// Returns the ReadingProfile that should be applied to the given series, walks up the tree. /// Returns the ReadingProfile that should be applied to the given series, walks up the tree.
/// Series -> Library -> Default /// Series -> Library -> Default
@ -20,13 +34,13 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("{seriesId}")] [HttpGet("{seriesId}")]
public async Task<ActionResult<UserReadingProfileDto?>> GetProfileForSeries(int seriesId) public async Task<ActionResult<UserReadingProfileDto>> GetProfileForSeries(int seriesId)
{ {
return Ok(await readingProfileService.GetReadingProfileForSeries(User.GetUserId(), seriesId)); return Ok(await readingProfileService.GetReadingProfileForSeries(User.GetUserId(), seriesId));
} }
/// <summary> /// <summary>
/// Update, or create the given profile /// Updates the given reading profile, must belong to the current user
/// </summary> /// </summary>
/// <param name="dto"></param> /// <param name="dto"></param>
/// <param name="seriesCtx"> /// <param name="seriesCtx">
@ -42,13 +56,23 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
await readingProfileService.DeleteImplicitForSeries(User.GetUserId(), seriesCtx.Value); await readingProfileService.DeleteImplicitForSeries(User.GetUserId(), seriesCtx.Value);
} }
var success = await readingProfileService.UpdateReadingProfile(User.GetUserId(), dto); var success = await readingProfileService.UpdateReadingProfile(User.GetUserId(), dto);
if (!success) return BadRequest(); if (!success) return BadRequest();
return Ok(); return Ok();
} }
/// <summary>
/// Creates a new reading profile for the current user
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("create")]
public async Task<ActionResult<UserReadingProfileDto>> CreateReadingProfile([FromBody] UserReadingProfileDto dto)
{
return Ok(await readingProfileService.CreateReadingProfile(User.GetUserId(), dto));
}
/// <summary> /// <summary>
/// Update the implicit reading profile for a series, creates one if none exists /// Update the implicit reading profile for a series, creates one if none exists
/// </summary> /// </summary>
@ -64,4 +88,32 @@ public class ReadingProfileController(ILogger<ReadingProfileController> logger,
return Ok(); return Ok();
} }
/// <summary>
/// Sets the given profile as the global default
/// </summary>
/// <param name="profileId"></param>
/// <returns></returns>
/// <exception cref="KavitaException"></exception>
/// <exception cref="UnauthorizedAccessException"></exception>
[HttpPost("set-default")]
public async Task<IActionResult> SetDefault([FromQuery] int profileId)
{
await readingProfileService.SetDefaultReadingProfile(User.GetUserId(), profileId);
return Ok();
}
/// <summary>
/// Deletes the given profile, requires the profile to belong to the logged-in user
/// </summary>
/// <param name="profileId"></param>
/// <returns></returns>
/// <exception cref="KavitaException"></exception>
/// <exception cref="UnauthorizedAccessException"></exception>
[HttpDelete]
public async Task<IActionResult> DeleteReadingProfile([FromQuery] int profileId)
{
await readingProfileService.DeleteReadingProfile(User.GetUserId(), profileId);
return Ok();
}
} }

View file

@ -103,38 +103,13 @@ public class UsersController : BaseApiController
var existingPreferences = user!.UserPreferences; var existingPreferences = user!.UserPreferences;
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu;
existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints;
existingPreferences.EmulateBook = preferencesDto.EmulateBook;
existingPreferences.ReaderMode = preferencesDto.ReaderMode;
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor;
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
existingPreferences.BookReaderWritingStyle = preferencesDto.BookReaderWritingStyle;
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode; existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode;
existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries; existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries;
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize; existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
existingPreferences.NoTransitions = preferencesDto.NoTransitions; existingPreferences.NoTransitions = preferencesDto.NoTransitions;
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships; existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
existingPreferences.ShareReviews = preferencesDto.ShareReviews; existingPreferences.ShareReviews = preferencesDto.ShareReviews;
existingPreferences.PdfTheme = preferencesDto.PdfTheme;
existingPreferences.PdfScrollMode = preferencesDto.PdfScrollMode;
existingPreferences.PdfSpreadMode = preferencesDto.PdfSpreadMode;
if (await _licenseService.HasActiveLicense()) if (await _licenseService.HasActiveLicense())
{ {
existingPreferences.AniListScrobblingEnabled = preferencesDto.AniListScrobblingEnabled; existingPreferences.AniListScrobblingEnabled = preferencesDto.AniListScrobblingEnabled;

View file

@ -9,61 +9,8 @@ namespace API.DTOs;
public sealed record UserPreferencesDto public sealed record UserPreferencesDto
{ {
/// <inheritdoc cref="API.Entities.AppUserPreferences.ReadingDirection"/> /// <inheritdoc cref="AppUserPreferences.DefaultReadingProfileId"/>
[Required] public int DefaultReadingProfileId { get; init; }
public ReadingDirection ReadingDirection { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.ScalingOption"/>
[Required]
public ScalingOption ScalingOption { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.PageSplitOption"/>
[Required]
public PageSplitOption PageSplitOption { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.ReaderMode"/>
[Required]
public ReaderMode ReaderMode { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.LayoutMode"/>
[Required]
public LayoutMode LayoutMode { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.EmulateBook"/>
[Required]
public bool EmulateBook { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.BackgroundColor"/>
[Required]
public string BackgroundColor { get; set; } = "#000000";
/// <inheritdoc cref="API.Entities.AppUserPreferences.SwipeToPaginate"/>
[Required]
public bool SwipeToPaginate { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.AutoCloseMenu"/>
[Required]
public bool AutoCloseMenu { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.ShowScreenHints"/>
[Required]
public bool ShowScreenHints { get; set; } = true;
/// <inheritdoc cref="API.Entities.AppUserPreferences.AllowAutomaticWebtoonReaderDetection"/>
[Required]
public bool AllowAutomaticWebtoonReaderDetection { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.BookReaderMargin"/>
[Required]
public int BookReaderMargin { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.BookReaderLineSpacing"/>
[Required]
public int BookReaderLineSpacing { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.BookReaderFontSize"/>
[Required]
public int BookReaderFontSize { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.BookReaderFontFamily"/>
[Required]
public string BookReaderFontFamily { get; set; } = null!;
/// <inheritdoc cref="API.Entities.AppUserPreferences.BookReaderTapToPaginate"/>
[Required]
public bool BookReaderTapToPaginate { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.BookReaderReadingDirection"/>
[Required]
public ReadingDirection BookReaderReadingDirection { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.BookReaderWritingStyle"/>
[Required]
public WritingStyle BookReaderWritingStyle { get; set; }
/// <summary> /// <summary>
/// UI Site Global Setting: The UI theme the user should use. /// UI Site Global Setting: The UI theme the user should use.
@ -72,15 +19,6 @@ public sealed record UserPreferencesDto
[Required] [Required]
public SiteThemeDto? Theme { get; set; } public SiteThemeDto? Theme { get; set; }
[Required] public string BookReaderThemeName { get; set; } = null!;
/// <inheritdoc cref="API.Entities.AppUserPreferences.BookReaderLayoutMode"/>
[Required]
public BookPageLayoutMode BookReaderLayoutMode { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.BookReaderImmersiveMode"/>
[Required]
public bool BookReaderImmersiveMode { get; set; } = false;
/// <inheritdoc cref="API.Entities.AppUserPreferences.GlobalPageLayoutMode"/>
[Required]
public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards; public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards;
/// <inheritdoc cref="API.Entities.AppUserPreferences.BlurUnreadSummaries"/> /// <inheritdoc cref="API.Entities.AppUserPreferences.BlurUnreadSummaries"/>
[Required] [Required]
@ -101,16 +39,6 @@ public sealed record UserPreferencesDto
[Required] [Required]
public string Locale { get; set; } public string Locale { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.PdfTheme"/>
[Required]
public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark;
/// <inheritdoc cref="API.Entities.AppUserPreferences.PdfScrollMode"/>
[Required]
public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical;
/// <inheritdoc cref="API.Entities.AppUserPreferences.PdfSpreadMode"/>
[Required]
public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None;
/// <inheritdoc cref="API.Entities.AppUserPreferences.AniListScrobblingEnabled"/> /// <inheritdoc cref="API.Entities.AppUserPreferences.AniListScrobblingEnabled"/>
public bool AniListScrobblingEnabled { get; set; } public bool AniListScrobblingEnabled { get; set; }
/// <inheritdoc cref="API.Entities.AppUserPreferences.WantToReadSync"/> /// <inheritdoc cref="API.Entities.AppUserPreferences.WantToReadSync"/>

View file

@ -257,6 +257,19 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
builder.Entity<MetadataSettings>() builder.Entity<MetadataSettings>()
.Property(b => b.EnableCoverImage) .Property(b => b.EnableCoverImage)
.HasDefaultValue(true); .HasDefaultValue(true);
builder.Entity<AppUserReadingProfile>()
.Property(b => b.BookThemeName)
.HasDefaultValue("Dark");
builder.Entity<AppUserReadingProfile>()
.Property(b => b.BackgroundColor)
.HasDefaultValue("#000000");
builder.Entity<AppUserReadingProfile>()
.Property(b => b.BookReaderWritingStyle)
.HasDefaultValue(WritingStyle.Horizontal);
builder.Entity<AppUserReadingProfile>()
.Property(b => b.AllowAutomaticWebtoonReaderDetection)
.HasDefaultValue(true);
} }
#nullable enable #nullable enable

View file

@ -1,13 +1,16 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Entities;
using API.Entities.History; using API.Entities.History;
using API.Extensions;
using API.Helpers.Builders;
using Kavita.Common.EnvironmentInfo; using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations.v0._8._7; namespace API.Data.ManualMigrations;
public class ManualMigrateReadingProfiles public static class ManualMigrateReadingProfiles
{ {
public static async Task Migrate(DataContext context, ILogger<Program> logger) public static async Task Migrate(DataContext context, ILogger<Program> logger)
{ {
@ -18,62 +21,55 @@ public class ManualMigrateReadingProfiles
logger.LogCritical("Running ManualMigrateReadingProfiles migration - Please be patient, this may take some time. This is not an error"); logger.LogCritical("Running ManualMigrateReadingProfiles migration - Please be patient, this may take some time. This is not an error");
await context.Database.ExecuteSqlRawAsync(@" var users = await context.AppUser
INSERT INTO AppUserReadingProfiles ( .Include(u => u.UserPreferences)
AppUserId, .Include(u => u.UserPreferences.ReadingProfiles)
ReadingDirection, .ToListAsync();
ScalingOption,
PageSplitOption, foreach (var user in users)
ReaderMode, {
AutoCloseMenu, var readingProfile = new AppUserReadingProfile
ShowScreenHints, {
EmulateBook, Name = "Default",
LayoutMode, NormalizedName = "Default".ToNormalized(),
BackgroundColor, BackgroundColor = user.UserPreferences.BackgroundColor,
SwipeToPaginate, EmulateBook = user.UserPreferences.EmulateBook,
AllowAutomaticWebtoonReaderDetection, User = user,
BookReaderMargin, PdfTheme = user.UserPreferences.PdfTheme,
BookReaderLineSpacing, ReaderMode = user.UserPreferences.ReaderMode,
BookReaderFontSize, ReadingDirection = user.UserPreferences.ReadingDirection,
BookReaderFontFamily, ScalingOption = user.UserPreferences.ScalingOption,
BookReaderTapToPaginate, LayoutMode = user.UserPreferences.LayoutMode,
BookReaderReadingDirection, WidthOverride = null,
BookReaderWritingStyle, UserId = user.Id,
BookThemeName, AutoCloseMenu = user.UserPreferences.AutoCloseMenu,
BookReaderLayoutMode, BookReaderMargin = user.UserPreferences.BookReaderMargin,
BookReaderImmersiveMode, PageSplitOption = user.UserPreferences.PageSplitOption,
PdfTheme, BookThemeName = user.UserPreferences.BookThemeName,
PdfScrollMode, PdfSpreadMode = user.UserPreferences.PdfSpreadMode,
PdfSpreadMode PdfScrollMode = user.UserPreferences.PdfScrollMode,
) SwipeToPaginate = user.UserPreferences.SwipeToPaginate,
SELECT BookReaderFontFamily = user.UserPreferences.BookReaderFontFamily,
AppUserId, BookReaderFontSize = user.UserPreferences.BookReaderFontSize,
ReadingDirection, BookReaderImmersiveMode = user.UserPreferences.BookReaderImmersiveMode,
ScalingOption, BookReaderLayoutMode = user.UserPreferences.BookReaderLayoutMode,
PageSplitOption, BookReaderLineSpacing = user.UserPreferences.BookReaderLineSpacing,
ReaderMode, BookReaderReadingDirection = user.UserPreferences.BookReaderReadingDirection,
AutoCloseMenu, BookReaderWritingStyle = user.UserPreferences.BookReaderWritingStyle,
ShowScreenHints, AllowAutomaticWebtoonReaderDetection = user.UserPreferences.AllowAutomaticWebtoonReaderDetection,
EmulateBook, BookReaderTapToPaginate = user.UserPreferences.BookReaderTapToPaginate,
LayoutMode, ShowScreenHints = user.UserPreferences.ShowScreenHints,
BackgroundColor, };
SwipeToPaginate, user.UserPreferences.ReadingProfiles.Add(readingProfile);
AllowAutomaticWebtoonReaderDetection, }
BookReaderMargin,
BookReaderLineSpacing, await context.SaveChangesAsync();
BookReaderFontSize, foreach (var user in users)
BookReaderFontFamily, {
BookReaderTapToPaginate, user.UserPreferences.DefaultReadingProfileId =
BookReaderReadingDirection, (await context.AppUserReadingProfile
BookReaderWritingStyle, .FirstAsync(rp => rp.UserId == user.Id)).Id;
BookThemeName, }
BookReaderLayoutMode,
BookReaderImmersiveMode,
PdfTheme,
PdfScrollMode,
PdfSpreadMode
FROM AppUserPreferences
");
context.ManualMigrationHistory.Add(new ManualMigrationHistory context.ManualMigrationHistory.Add(new ManualMigrationHistory
{ {

View file

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace API.Data.Migrations namespace API.Data.Migrations
{ {
[DbContext(typeof(DataContext))] [DbContext(typeof(DataContext))]
[Migration("20250515215234_AppUserReadingProfiles")] [Migration("20250517195000_AppUserReadingProfiles")]
partial class AppUserReadingProfiles partial class AppUserReadingProfiles
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -622,7 +622,9 @@ namespace API.Data.Migrations
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<bool>("AllowAutomaticWebtoonReaderDetection") b.Property<bool>("AllowAutomaticWebtoonReaderDetection")
.HasColumnType("INTEGER"); .ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<int?>("AppUserPreferencesId") b.Property<int?>("AppUserPreferencesId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -631,7 +633,9 @@ namespace API.Data.Migrations
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("BackgroundColor") b.Property<string>("BackgroundColor")
.HasColumnType("TEXT"); .ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("#000000");
b.Property<string>("BookReaderFontFamily") b.Property<string>("BookReaderFontFamily")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -658,10 +662,14 @@ namespace API.Data.Migrations
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("BookReaderWritingStyle") b.Property<int>("BookReaderWritingStyle")
.HasColumnType("INTEGER"); .ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0);
b.Property<string>("BookThemeName") b.Property<string>("BookThemeName")
.HasColumnType("TEXT"); .ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("Dark");
b.Property<bool>("EmulateBook") b.Property<bool>("EmulateBook")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View file

@ -34,9 +34,9 @@ namespace API.Data.Migrations
ShowScreenHints = table.Column<bool>(type: "INTEGER", nullable: false), ShowScreenHints = table.Column<bool>(type: "INTEGER", nullable: false),
EmulateBook = table.Column<bool>(type: "INTEGER", nullable: false), EmulateBook = table.Column<bool>(type: "INTEGER", nullable: false),
LayoutMode = table.Column<int>(type: "INTEGER", nullable: false), LayoutMode = table.Column<int>(type: "INTEGER", nullable: false),
BackgroundColor = table.Column<string>(type: "TEXT", nullable: true), BackgroundColor = table.Column<string>(type: "TEXT", nullable: true, defaultValue: "#000000"),
SwipeToPaginate = table.Column<bool>(type: "INTEGER", nullable: false), SwipeToPaginate = table.Column<bool>(type: "INTEGER", nullable: false),
AllowAutomaticWebtoonReaderDetection = table.Column<bool>(type: "INTEGER", nullable: false), AllowAutomaticWebtoonReaderDetection = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true),
WidthOverride = table.Column<int>(type: "INTEGER", nullable: true), WidthOverride = table.Column<int>(type: "INTEGER", nullable: true),
BookReaderMargin = table.Column<int>(type: "INTEGER", nullable: false), BookReaderMargin = table.Column<int>(type: "INTEGER", nullable: false),
BookReaderLineSpacing = table.Column<int>(type: "INTEGER", nullable: false), BookReaderLineSpacing = table.Column<int>(type: "INTEGER", nullable: false),
@ -44,8 +44,8 @@ namespace API.Data.Migrations
BookReaderFontFamily = table.Column<string>(type: "TEXT", nullable: true), BookReaderFontFamily = table.Column<string>(type: "TEXT", nullable: true),
BookReaderTapToPaginate = table.Column<bool>(type: "INTEGER", nullable: false), BookReaderTapToPaginate = table.Column<bool>(type: "INTEGER", nullable: false),
BookReaderReadingDirection = table.Column<int>(type: "INTEGER", nullable: false), BookReaderReadingDirection = table.Column<int>(type: "INTEGER", nullable: false),
BookReaderWritingStyle = table.Column<int>(type: "INTEGER", nullable: false), BookReaderWritingStyle = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
BookThemeName = table.Column<string>(type: "TEXT", nullable: true), BookThemeName = table.Column<string>(type: "TEXT", nullable: true, defaultValue: "Dark"),
BookReaderLayoutMode = table.Column<int>(type: "INTEGER", nullable: false), BookReaderLayoutMode = table.Column<int>(type: "INTEGER", nullable: false),
BookReaderImmersiveMode = table.Column<bool>(type: "INTEGER", nullable: false), BookReaderImmersiveMode = table.Column<bool>(type: "INTEGER", nullable: false),
PdfTheme = table.Column<int>(type: "INTEGER", nullable: false), PdfTheme = table.Column<int>(type: "INTEGER", nullable: false),

View file

@ -619,7 +619,9 @@ namespace API.Data.Migrations
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<bool>("AllowAutomaticWebtoonReaderDetection") b.Property<bool>("AllowAutomaticWebtoonReaderDetection")
.HasColumnType("INTEGER"); .ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<int?>("AppUserPreferencesId") b.Property<int?>("AppUserPreferencesId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -628,7 +630,9 @@ namespace API.Data.Migrations
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("BackgroundColor") b.Property<string>("BackgroundColor")
.HasColumnType("TEXT"); .ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("#000000");
b.Property<string>("BookReaderFontFamily") b.Property<string>("BookReaderFontFamily")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -655,10 +659,14 @@ namespace API.Data.Migrations
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("BookReaderWritingStyle") b.Property<int>("BookReaderWritingStyle")
.HasColumnType("INTEGER"); .ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0);
b.Property<string>("BookThemeName") b.Property<string>("BookThemeName")
.HasColumnType("TEXT"); .ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("Dark");
b.Property<bool>("EmulateBook") b.Property<bool>("EmulateBook")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs; using API.DTOs;
using API.Entities; using API.Entities;
using API.Extensions;
using API.Extensions.QueryExtensions; using API.Extensions.QueryExtensions;
using AutoMapper; using AutoMapper;
using AutoMapper.QueryableExtensions; using AutoMapper.QueryableExtensions;
@ -22,13 +23,14 @@ public enum ReadingProfileIncludes
public interface IAppUserReadingProfileRepository public interface IAppUserReadingProfileRepository
{ {
Task<IList<AppUserReadingProfile>> GetProfilesForUser(int userId); Task<IList<AppUserReadingProfile>> GetProfilesForUser(int userId, bool nonImplicitOnly, ReadingProfileIncludes includes = ReadingProfileIncludes.None);
Task<AppUserReadingProfile?> GetProfileForSeries(int userId, int seriesId); Task<AppUserReadingProfile?> GetProfileForSeries(int userId, int seriesId, ReadingProfileIncludes includes = ReadingProfileIncludes.None);
Task<UserReadingProfileDto?> GetProfileDtoForSeries(int userId, int seriesId); Task<UserReadingProfileDto?> GetProfileDtoForSeries(int userId, int seriesId);
Task<AppUserReadingProfile?> GetProfileForLibrary(int userId, int libraryId); Task<AppUserReadingProfile?> GetProfileForLibrary(int userId, int libraryId, ReadingProfileIncludes includes = ReadingProfileIncludes.None);
Task<UserReadingProfileDto?> GetProfileDtoForLibrary(int userId, int libraryId); Task<UserReadingProfileDto?> GetProfileDtoForLibrary(int userId, int libraryId);
Task<AppUserReadingProfile?> GetProfile(int profileId, ReadingProfileIncludes includes = ReadingProfileIncludes.None); Task<AppUserReadingProfile?> GetProfile(int profileId, ReadingProfileIncludes includes = ReadingProfileIncludes.None);
Task<UserReadingProfileDto?> GetProfileDto(int profileId); Task<UserReadingProfileDto?> GetProfileDto(int profileId);
Task<AppUserReadingProfile?> GetProfileByName(int userId, string name);
void Add(AppUserReadingProfile readingProfile); void Add(AppUserReadingProfile readingProfile);
void Update(AppUserReadingProfile readingProfile); void Update(AppUserReadingProfile readingProfile);
@ -38,17 +40,20 @@ public interface IAppUserReadingProfileRepository
public class AppUserReadingProfileRepository(DataContext context, IMapper mapper): IAppUserReadingProfileRepository public class AppUserReadingProfileRepository(DataContext context, IMapper mapper): IAppUserReadingProfileRepository
{ {
public async Task<IList<AppUserReadingProfile>> GetProfilesForUser(int userId) public async Task<IList<AppUserReadingProfile>> GetProfilesForUser(int userId, bool nonImplicitOnly, ReadingProfileIncludes includes = ReadingProfileIncludes.None)
{ {
return await context.AppUserReadingProfile return await context.AppUserReadingProfile
.Where(rp => rp.UserId == userId) .Where(rp => rp.UserId == userId && !(nonImplicitOnly && rp.Implicit))
.Includes(includes)
.ToListAsync(); .ToListAsync();
} }
public async Task<AppUserReadingProfile?> GetProfileForSeries(int userId, int seriesId) public async Task<AppUserReadingProfile?> GetProfileForSeries(int userId, int seriesId, ReadingProfileIncludes includes = ReadingProfileIncludes.None)
{ {
return await context.AppUserReadingProfile return await context.AppUserReadingProfile
.Where(rp => rp.UserId == userId && rp.Series.Any(s => s.Id == seriesId)) .Where(rp => rp.UserId == userId && rp.Series.Any(s => s.Id == seriesId))
.Includes(includes)
.OrderByDescending(rp => rp.Implicit) // Get implicit profiles first
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
@ -56,14 +61,16 @@ public class AppUserReadingProfileRepository(DataContext context, IMapper mapper
{ {
return await context.AppUserReadingProfile return await context.AppUserReadingProfile
.Where(rp => rp.UserId == userId && rp.Series.Any(s => s.Id == seriesId)) .Where(rp => rp.UserId == userId && rp.Series.Any(s => s.Id == seriesId))
.OrderByDescending(rp => rp.Implicit) // Get implicit profiles first
.ProjectTo<UserReadingProfileDto>(mapper.ConfigurationProvider) .ProjectTo<UserReadingProfileDto>(mapper.ConfigurationProvider)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
public async Task<AppUserReadingProfile?> GetProfileForLibrary(int userId, int libraryId) public async Task<AppUserReadingProfile?> GetProfileForLibrary(int userId, int libraryId, ReadingProfileIncludes includes = ReadingProfileIncludes.None)
{ {
return await context.AppUserReadingProfile return await context.AppUserReadingProfile
.Where(rp => rp.UserId == userId && rp.Libraries.Any(s => s.Id == libraryId)) .Where(rp => rp.UserId == userId && rp.Libraries.Any(s => s.Id == libraryId))
.Includes(includes)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
@ -90,6 +97,15 @@ public class AppUserReadingProfileRepository(DataContext context, IMapper mapper
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
public async Task<AppUserReadingProfile?> GetProfileByName(int userId, string name)
{
var normalizedName = name.ToNormalized();
return await context.AppUserReadingProfile
.Where(rp => rp.NormalizedName == normalizedName && rp.UserId == userId)
.FirstOrDefaultAsync();
}
public void Add(AppUserReadingProfile readingProfile) public void Add(AppUserReadingProfile readingProfile)
{ {
context.AppUserReadingProfile.Add(readingProfile); context.AppUserReadingProfile.Add(readingProfile);

View file

@ -54,6 +54,7 @@ public static class ApplicationServiceExtensions
services.AddScoped<IStreamService, StreamService>(); services.AddScoped<IStreamService, StreamService>();
services.AddScoped<IRatingService, RatingService>(); services.AddScoped<IRatingService, RatingService>();
services.AddScoped<IPersonService, PersonService>(); services.AddScoped<IPersonService, PersonService>();
services.AddScoped<IReadingProfileService, ReadingProfileService>();
services.AddScoped<IScannerService, ScannerService>(); services.AddScoped<IScannerService, ScannerService>();
services.AddScoped<IProcessSeries, ProcessSeries>(); services.AddScoped<IProcessSeries, ProcessSeries>();

View file

@ -220,7 +220,8 @@ public static class IncludesExtensions
{ {
query = query query = query
.Include(u => u.UserPreferences) .Include(u => u.UserPreferences)
.ThenInclude(p => p.Theme); .ThenInclude(p => p.Theme)
.Include(u => u.UserPreferences.ReadingProfiles);
} }
if (includeFlags.HasFlag(AppUserIncludes.WantToRead)) if (includeFlags.HasFlag(AppUserIncludes.WantToRead))

View file

@ -275,13 +275,7 @@ public class AutoMapperProfiles : Profile
CreateMap<AppUserPreferences, UserPreferencesDto>() CreateMap<AppUserPreferences, UserPreferencesDto>()
.ForMember(dest => dest.Theme, .ForMember(dest => dest.Theme,
opt => opt =>
opt.MapFrom(src => src.Theme)) opt.MapFrom(src => src.Theme));
.ForMember(dest => dest.BookReaderThemeName,
opt =>
opt.MapFrom(src => src.BookThemeName))
.ForMember(dest => dest.BookReaderLayoutMode,
opt =>
opt.MapFrom(src => src.BookReaderLayoutMode));
CreateMap<AppUserReadingProfile, UserReadingProfileDto>() CreateMap<AppUserReadingProfile, UserReadingProfileDto>()
.ForMember(dest => dest.BookReaderThemeName, .ForMember(dest => dest.BookReaderThemeName,

View file

@ -24,13 +24,21 @@ public interface IReadingProfileService
Task<UserReadingProfileDto> GetReadingProfileForSeries(int userId, int seriesId); Task<UserReadingProfileDto> GetReadingProfileForSeries(int userId, int seriesId);
/// <summary> /// <summary>
/// Updates, or adds a specific reading profile for a user /// Updates a given reading profile for a user
/// </summary> /// </summary>
/// <param name="userId"></param> /// <param name="userId"></param>
/// <param name="dto"></param> /// <param name="dto"></param>
/// <returns></returns> /// <returns></returns>
Task<bool> UpdateReadingProfile(int userId, UserReadingProfileDto dto); Task<bool> UpdateReadingProfile(int userId, UserReadingProfileDto dto);
/// <summary>
/// Creates a new reading profile for a user. Name must be unique per user
/// </summary>
/// <param name="userId"></param>
/// <param name="dto"></param>
/// <returns></returns>
Task<UserReadingProfileDto> CreateReadingProfile(int userId, UserReadingProfileDto dto);
/// <summary> /// <summary>
/// Updates the implicit reading profile for a series, creates one if none exists /// Updates the implicit reading profile for a series, creates one if none exists
/// </summary> /// </summary>
@ -58,6 +66,14 @@ public interface IReadingProfileService
/// <exception cref="KavitaException">The default profile for the user cannot be deleted</exception> /// <exception cref="KavitaException">The default profile for the user cannot be deleted</exception>
Task DeleteReadingProfile(int userId, int profileId); Task DeleteReadingProfile(int userId, int profileId);
/// <summary>
/// Sets the given profile as global default
/// </summary>
/// <param name="userId"></param>
/// <param name="profileId"></param>
/// <returns></returns>
Task SetDefaultReadingProfile(int userId, int profileId);
} }
public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService localizationService): IReadingProfileService public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService localizationService): IReadingProfileService
@ -74,7 +90,7 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
var libraryProfile = await unitOfWork.AppUserReadingProfileRepository.GetProfileDtoForLibrary(userId, series.LibraryId); var libraryProfile = await unitOfWork.AppUserReadingProfileRepository.GetProfileDtoForLibrary(userId, series.LibraryId);
if (libraryProfile != null) return libraryProfile; if (libraryProfile != null) return libraryProfile;
var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences);
if (user == null) throw new UnauthorizedAccessException(); if (user == null) throw new UnauthorizedAccessException();
return await unitOfWork.AppUserReadingProfileRepository.GetProfileDto(user.UserPreferences.DefaultReadingProfileId); return await unitOfWork.AppUserReadingProfileRepository.GetProfileDto(user.UserPreferences.DefaultReadingProfileId);
@ -82,27 +98,47 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
public async Task<bool> UpdateReadingProfile(int userId, UserReadingProfileDto dto) public async Task<bool> UpdateReadingProfile(int userId, UserReadingProfileDto dto)
{ {
var existingProfile = await unitOfWork.AppUserReadingProfileRepository.GetProfile(dto.Id); var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences);
if (existingProfile == null) if (user == null) throw new UnauthorizedAccessException();
{
existingProfile = new AppUserReadingProfileBuilder(userId).Build();
}
if (existingProfile.UserId != userId) return false; var existingProfile = await unitOfWork.AppUserReadingProfileRepository.GetProfile(dto.Id);
if (existingProfile == null) throw new KavitaException("profile-does-not-exist");
if (existingProfile.UserId != userId) throw new UnauthorizedAccessException();
UpdateReaderProfileFields(existingProfile, dto); UpdateReaderProfileFields(existingProfile, dto);
unitOfWork.AppUserReadingProfileRepository.Update(existingProfile); unitOfWork.AppUserReadingProfileRepository.Update(existingProfile);
return await unitOfWork.CommitAsync(); return await unitOfWork.CommitAsync();
} }
public async Task<UserReadingProfileDto> CreateReadingProfile(int userId, UserReadingProfileDto dto)
{
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");
var newProfile = new AppUserReadingProfileBuilder(user.Id).Build();
UpdateReaderProfileFields(newProfile, dto);
unitOfWork.AppUserReadingProfileRepository.Add(newProfile);
await unitOfWork.CommitAsync();
return await unitOfWork.AppUserReadingProfileRepository.GetProfileDto(newProfile.Id);
}
public async Task<bool> UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto) public async Task<bool> UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto)
{ {
var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences);
if (user == null) throw new UnauthorizedAccessException();
var existingProfile = await unitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(userId, seriesId); var existingProfile = await unitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(userId, seriesId);
// Series already had an implicit profile, update it // Series already had an implicit profile, update it
if (existingProfile is {Implicit: true}) if (existingProfile is {Implicit: true})
{ {
UpdateReaderProfileFields(existingProfile, dto); UpdateReaderProfileFields(existingProfile, dto, false);
unitOfWork.AppUserReadingProfileRepository.Update(existingProfile); unitOfWork.AppUserReadingProfileRepository.Update(existingProfile);
return await unitOfWork.CommitAsync(); return await unitOfWork.CommitAsync();
} }
@ -113,14 +149,18 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
.WithImplicit(true) .WithImplicit(true)
.Build(); .Build();
unitOfWork.AppUserReadingProfileRepository.Add(existingProfile); UpdateReaderProfileFields(existingProfile, dto, false);
existingProfile.Name = $"Implicit Profile for {seriesId}";
existingProfile.NormalizedName = existingProfile.Name.ToNormalized();
user.UserPreferences.ReadingProfiles.Add(existingProfile);
return await unitOfWork.CommitAsync(); return await unitOfWork.CommitAsync();
} }
public async Task DeleteImplicitForSeries(int userId, int seriesId) public async Task DeleteImplicitForSeries(int userId, int seriesId)
{ {
var profile = await unitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(userId, seriesId); var profile = await unitOfWork.AppUserReadingProfileRepository.GetProfileForSeries(userId, seriesId, ReadingProfileIncludes.Series);
if (profile == null) throw new KavitaException(await localizationService.Translate(userId, "profile-doesnt-exist")); if (profile == null) return;
if (!profile.Implicit) return; if (!profile.Implicit) return;
@ -143,9 +183,23 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
await unitOfWork.CommitAsync(); await unitOfWork.CommitAsync();
} }
private static void UpdateReaderProfileFields(AppUserReadingProfile existingProfile, UserReadingProfileDto dto) public async Task SetDefaultReadingProfile(int userId, int profileId)
{ {
if (!string.IsNullOrEmpty(dto.Name) && existingProfile.NormalizedName != dto.Name.ToNormalized()) 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();
}
private static void UpdateReaderProfileFields(AppUserReadingProfile existingProfile, UserReadingProfileDto dto, bool updateName = true)
{
if (updateName && !string.IsNullOrEmpty(dto.Name) && existingProfile.NormalizedName != dto.Name.ToNormalized())
{ {
existingProfile.Name = dto.Name; existingProfile.Name = dto.Name;
existingProfile.NormalizedName = dto.Name.ToNormalized(); existingProfile.NormalizedName = dto.Name.ToNormalized();

View file

@ -293,6 +293,9 @@ public class Startup
await ManualMigrateScrobbleSpecials.Migrate(dataContext, logger); await ManualMigrateScrobbleSpecials.Migrate(dataContext, logger);
await ManualMigrateScrobbleEventGen.Migrate(dataContext, logger); await ManualMigrateScrobbleEventGen.Migrate(dataContext, logger);
// v0.8.7
await ManualMigrateReadingProfiles.Migrate(dataContext, logger);
#endregion #endregion
// Update the version in the DB after all migrations are run // Update the version in the DB after all migrations are run

View file

@ -2,6 +2,8 @@ import {PageLayoutMode} from '../page-layout-mode';
import {SiteTheme} from './site-theme'; import {SiteTheme} from './site-theme';
export interface Preferences { export interface Preferences {
defaultReadingProfileId: number;
// Global // Global
theme: SiteTheme; theme: SiteTheme;
globalPageLayoutMode: PageLayoutMode; globalPageLayoutMode: PageLayoutMode;

View file

@ -23,8 +23,24 @@ export class ReadingProfileService {
return this.httpClient.post(this.baseUrl + "ReadingProfile", profile); return this.httpClient.post(this.baseUrl + "ReadingProfile", profile);
} }
createProfile(profile: ReadingProfile) {
return this.httpClient.post<ReadingProfile>(this.baseUrl + "ReadingProfile/create", profile);
}
updateImplicit(profile: ReadingProfile, seriesId: number) { updateImplicit(profile: ReadingProfile, seriesId: number) {
return this.httpClient.post(this.baseUrl + "ReadingProfile/series?seriesId="+seriesId, profile); return this.httpClient.post(this.baseUrl + "ReadingProfile/series?seriesId="+seriesId, profile);
} }
all() {
return this.httpClient.get<ReadingProfile[]>(this.baseUrl + "ReadingProfile/all");
}
delete(id: number) {
return this.httpClient.delete(this.baseUrl + "ReadingProfile?profileId="+id);
}
setDefault(id: number) {
return this.httpClient.post(this.baseUrl + "ReadingProfile/set-default?profileId=" + id, {});
}
} }

View file

@ -217,6 +217,15 @@
</div> </div>
} }
} }
@defer (when fragment === SettingsTabId.ReadingProfiles; prefetch on idle) {
@if (fragment === SettingsTabId.ReadingProfiles) {
<div class="scale col-md-12">
<app-manage-reading-profiles></app-manage-reading-profiles>
</div>
}
}
} }
</div> </div>
</ng-container> </ng-container>

View file

@ -52,43 +52,47 @@ import {ScrobblingHoldsComponent} from "../../../user-settings/user-holds/scrobb
import { import {
ManageMetadataSettingsComponent ManageMetadataSettingsComponent
} from "../../../admin/manage-metadata-settings/manage-metadata-settings.component"; } from "../../../admin/manage-metadata-settings/manage-metadata-settings.component";
import {
ManageReadingProfilesComponent
} from "../../../user-settings/manage-reading-profiles/manage-reading-profiles.component";
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
imports: [ imports: [
ChangeAgeRestrictionComponent, ChangeAgeRestrictionComponent,
ChangeEmailComponent, ChangeEmailComponent,
ChangePasswordComponent, ChangePasswordComponent,
ManageDevicesComponent, ManageDevicesComponent,
ManageOpdsComponent, ManageOpdsComponent,
ManageScrobblingProvidersComponent, ManageScrobblingProvidersComponent,
ManageUserPreferencesComponent, ManageUserPreferencesComponent,
SideNavCompanionBarComponent, SideNavCompanionBarComponent,
ThemeManagerComponent, ThemeManagerComponent,
TranslocoDirective, TranslocoDirective,
UserStatsComponent, UserStatsComponent,
AsyncPipe, AsyncPipe,
LicenseComponent, LicenseComponent,
ManageEmailSettingsComponent, ManageEmailSettingsComponent,
ManageLibraryComponent, ManageLibraryComponent,
ManageMediaSettingsComponent, ManageMediaSettingsComponent,
ManageSettingsComponent, ManageSettingsComponent,
ManageSystemComponent, ManageSystemComponent,
ManageTasksSettingsComponent, ManageTasksSettingsComponent,
ManageUsersComponent, ManageUsersComponent,
ServerStatsComponent, ServerStatsComponent,
SettingFragmentPipe, SettingFragmentPipe,
ManageScrobblingComponent, ManageScrobblingComponent,
ManageMediaIssuesComponent, ManageMediaIssuesComponent,
ManageCustomizationComponent, ManageCustomizationComponent,
ImportMalCollectionComponent, ImportMalCollectionComponent,
ImportCblComponent, ImportCblComponent,
ManageMatchedMetadataComponent, ManageMatchedMetadataComponent,
ManageUserTokensComponent, ManageUserTokensComponent,
EmailHistoryComponent, EmailHistoryComponent,
ScrobblingHoldsComponent, ScrobblingHoldsComponent,
ManageMetadataSettingsComponent ManageMetadataSettingsComponent,
], ManageReadingProfilesComponent
],
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
styleUrl: './settings.component.scss', styleUrl: './settings.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush

View file

@ -41,6 +41,7 @@ export enum SettingsTabId {
// Non-Admin // Non-Admin
Account = 'account', Account = 'account',
Preferences = 'preferences', Preferences = 'preferences',
ReadingProfiles = 'reading-profiles',
Clients = 'clients', Clients = 'clients',
Theme = 'theme', Theme = 'theme',
Devices = 'devices', Devices = 'devices',
@ -111,6 +112,7 @@ export class PreferenceNavComponent implements AfterViewInit {
children: [ children: [
new SideNavItem(SettingsTabId.Account, []), new SideNavItem(SettingsTabId.Account, []),
new SideNavItem(SettingsTabId.Preferences), new SideNavItem(SettingsTabId.Preferences),
new SideNavItem(SettingsTabId.ReadingProfiles),
new SideNavItem(SettingsTabId.Customize, [], undefined, [Role.ReadOnly]), new SideNavItem(SettingsTabId.Customize, [], undefined, [Role.ReadOnly]),
new SideNavItem(SettingsTabId.Clients), new SideNavItem(SettingsTabId.Clients),
new SideNavItem(SettingsTabId.Theme), new SideNavItem(SettingsTabId.Theme),

View file

@ -0,0 +1,499 @@
<ng-container *transloco="let t;prefix:'manage-reading-profiles'">
<app-loading [loading]="loading"></app-loading>
@if (!loading) {
<div class="position-relative">
<button class="btn btn-outline-primary position-absolute custom-position" (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>
<p class="ps-2">{{t('description')}}</p>
<p class="ps-2 text-muted">{{t('extra-tip')}}</p>
<div class="row g-0 ">
<div class="col-lg-3 col-md-5 col-sm-7 col-xs-7 scroller">
<div class="pe-2">
@if (readingProfiles.length < virtualScrollerBreakPoint) {
@for (readingProfile of readingProfiles; track readingProfile.id) {
<ng-container [ngTemplateOutlet]="readingProfileOption" [ngTemplateOutletContext]="{$implicit: readingProfile}"></ng-container>
}
} @else {
<virtual-scroller #scroll [items]="readingProfiles">
@for (readingProfile of scroll.viewPortItems; track readingProfile.id) {
<ng-container [ngTemplateOutlet]="readingProfileOption" [ngTemplateOutletContext]="{$implicit: readingProfile}"></ng-container>
}
</virtual-scroller>
}
</div>
</div>
<div class="col-lg-9 col-md-7 col-sm-4 col-xs-4 ps-3">
<div class="card p-3">
@if (selectedProfile === null) {
<p class="ps-2">{{t('no-selected')}}</p>
<p class="ps-2 text-muted">{{t('selection-tip')}}</p>
}
@if (readingProfileForm !== null && selectedProfile !== null) {
<form [formGroup]="readingProfileForm">
<div class="mb-2 d-flex justify-content-between align-items-center">
<app-setting-item [title]="''" [showEdit]="false">
<ng-template #view>
{{readingProfileForm.get('name')!.value}}
</ng-template>
<ng-template #edit>
<input class="form-control" type="text" formControlName="name">
</ng-template>
</app-setting-item>
@if (this.selectedProfile?.id !== 0) {
<div class="d-flex justify-content-between">
<button class="me-2 btn btn-primary" (click)="setDefault(this.selectedProfile!.id)">
<span>{{t('make-default')}}</span>
</button>
<button class="btn btn-danger" (click)="delete(this.selectedProfile!.id)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
}
</div>
<div class="carousel-tabs-container mb-2">
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs">
<li [ngbNavItem]="TabId.ImageReader">
<a ngbNavLink (click)="activeTabId = TabId.ImageReader">{{t('image-reader-settings-title')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabId.ImageReader; prefetch on idle) {
<div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-direction-label')" [subtitle]="t('reading-direction-tooltip')">
<ng-template #view>
{{readingProfileForm.get('readingDirection')!.value | readingDirection}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="readingDirection">
@for (opt of readingDirections; track opt) {
<option [value]="opt.value">{{opt.value | readingDirection}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('scaling-option-label')" [subtitle]="t('scaling-option-tooltip')">
<ng-template #view>
{{readingProfileForm.get('scalingOption')!.value | scalingOption}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="scalingOption">
@for (opt of scalingOptions; track opt) {
<option [value]="opt.value">{{opt.value | scalingOption}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('page-splitting-label')" [subtitle]="t('page-splitting-tooltip')">
<ng-template #view>
{{readingProfileForm.get('pageSplitOption')!.value | pageSplitOption}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="pageSplitOption">
@for (opt of pageSplitOptions; track opt) {
<option [value]="opt.value">{{opt.value | pageSplitOption}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-mode-label')" [subtitle]="t('reading-mode-tooltip')">
<ng-template #view>
{{readingProfileForm.get('readerMode')!.value | readerMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="readerMode">
@for (opt of readerModes; track opt) {
<option [value]="opt.value">{{opt.value | readerMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('layout-mode-label')" [subtitle]="t('layout-mode-tooltip')">
<ng-template #view>
{{readingProfileForm.get('layoutMode')!.value | layoutMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="layoutMode">
@for (opt of layoutModes; track opt) {
<option [value]="opt.value">{{opt.value | layoutMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('background-color-label')" [subtitle]="t('background-color-tooltip')">
<ng-template #view>
<div class="color-box-container">
<div class="color-box" [ngStyle]="{'background-color': selectedProfile!.backgroundColor}"></div>
<span class="hex-code">{{ selectedProfile!.backgroundColor.toUpperCase() }}</span>
</div>
</ng-template>
<ng-template #edit>
<input [value]="selectedProfile!.backgroundColor" class="form-control"
(colorPickerChange)="handleBackgroundColorChange($event)"
[style.background]="selectedProfile!.backgroundColor" [cpAlphaChannel]="'disabled'"
[(colorPicker)]="selectedProfile!.backgroundColor" />
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('auto-close-menu-label')" [subtitle]="t('auto-close-menu-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="autoCloseMenu" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('show-screen-hints-label')" [subtitle]="t('show-screen-hints-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="showScreenHints" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('emulate-comic-book-label')" [subtitle]="t('emulate-comic-book-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="emulate-comic-book"
formControlName="emulateBook" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('swipe-to-paginate-label')" [subtitle]="t('swipe-to-paginate-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="swipe-to-paginate"
formControlName="swipeToPaginate" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('allow-auto-webtoon-reader-label')" [subtitle]="t('allow-auto-webtoon-reader-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="allow-auto-webtoon-reader"
formControlName="allowAutomaticWebtoonReaderDetection" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
</div>
}
</ng-template>
</li>
<li [ngbNavItem]="TabId.BookReader">
<a ngbNavLink (click)="activeTabId = TabId.BookReader">{{t('book-reader-settings-title')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabId.BookReader; prefetch on idle) {
<div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('tap-to-paginate-label')" [subtitle]="t('tap-to-paginate-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="tap-to-paginate"
formControlName="bookReaderTapToPaginate" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('immersive-mode-label')" [subtitle]="t('immersive-mode-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="bookReaderImmersiveMode" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-direction-label')" [subtitle]="t('reading-direction-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderReadingDirection')!.value | readingDirection}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderReadingDirection">
@for (opt of readingDirections; track opt) {
<option [value]="opt.value">{{opt.value | readingDirection}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('font-family-label')" [subtitle]="t('font-family-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderFontFamily')!.value | titlecase}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderFontFamily">
@for (opt of fontFamilies; track opt) {
<option [value]="opt">{{opt | titlecase}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('writing-style-label')" [subtitle]="t('writing-style-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderWritingStyle')!.value | writingStyle}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderWritingStyle">
@for (opt of bookWritingStyles; track opt) {
<option [value]="opt.value">{{opt.value | writingStyle}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('layout-mode-book-label')" [subtitle]="t('layout-mode-book-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderLayoutMode')!.value | bookPageLayoutMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderLayoutMode">
@for (opt of bookLayoutModes; track opt) {
<option [value]="opt.value">{{opt.value | bookPageLayoutMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('color-theme-book-label')" [subtitle]="t('color-theme-book-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderThemeName')!.value}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderThemeName">
@for (opt of bookColorThemesTranslated; track opt) {
<option [value]="opt.name">{{opt.name | titlecase}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('font-size-book-label')" [subtitle]="t('font-size-book-tooltip')">
<ng-template #view>
<span class="range-text">{{readingProfileForm.get('bookReaderFontSize')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="fontsize" min="50" max="300" step="10"
formControlName="bookReaderFontSize">
</div>
<span class="ps-2 col-2 align-middle">{{readingProfileForm.get('bookReaderFontSize')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('line-height-book-label')" [subtitle]="t('line-height-book-tooltip')">
<ng-template #view>
<span class="range-text">{{readingProfileForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
formControlName="bookReaderLineSpacing">
</div>
<span class="ps-2 col-2 align-middle">{{readingProfileForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('margin-book-label')" [subtitle]="t('margin-book-tooltip')">
<ng-template #view>
<span class="range-text">{{readingProfileForm.get('bookReaderMargin')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="margin" min="0" max="30" step="5"
formControlName="bookReaderMargin">
</div>
<span class="ps-2 col-2 align-middle">{{readingProfileForm!.get('bookReaderMargin')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
</div>
}
</ng-template>
</li>
<li [ngbNavItem]="TabId.PdfReader">
<a ngbNavLink (click)="activeTabId = TabId.PdfReader">{{t('pdf-reader-settings-title')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabId.PdfReader; prefetch on idle) {
<div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-spread-mode-label')" [subtitle]="t('pdf-spread-mode-tooltip')">
<ng-template #view>
{{readingProfileForm!.get('pdfSpreadMode')!.value | pdfSpreadMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfSpreadMode">
@for (opt of pdfSpreadModes; track opt) {
<option [value]="opt.value">{{opt.value | pdfSpreadMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-theme-label')" [subtitle]="t('pdf-theme-tooltip')">
<ng-template #view>
{{readingProfileForm!.get('pdfTheme')!.value | pdfTheme}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfTheme">
@for (opt of pdfThemes; track opt) {
<option [value]="opt.value">{{opt.value | pdfTheme}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-scroll-mode-label')" [subtitle]="t('pdf-scroll-mode-tooltip')">
<ng-template #view>
{{readingProfileForm!.get('pdfScrollMode')!.value | pdfScrollMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfScrollMode">
@for (opt of pdfScrollModes; track opt) {
<option [value]="opt.value">{{opt.value | pdfScrollMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
</div>
}
</ng-template>
</li>
<li [ngbNavItem]="TabId.Series">
<a ngbNavLink (click)="activeTabId = TabId.Series">{{t('reading-profile-series-settings-title')}}</a>
<ng-template ngbNavContent></ng-template>
</li>
<li [ngbNavItem]="TabId.Libraries">
<a ngbNavLink (click)="activeTabId = TabId.Libraries">{{t('reading-profile-library-settings-title')}}</a>
<ng-template ngbNavContent></ng-template>
</li>
</ul>
</div>
</form>
<div [ngbNavOutlet]="nav"></div>
}
</div>
</div>
</div>
<ng-template #readingProfileOption let-profile>
<div class="p-2 group-item d-flex justify-content-between align-items-start {{selectedProfile && profile.id === selectedProfile.id ? 'active' : ''}}"
(click)="selectProfile(profile)"
>
<div class="fw-bold">{{profile.name | sentenceCase}}</div>
@if (profile.id === user.preferences.defaultReadingProfileId) {
<span class="pill p-1 ms-1">{{t('default-profile')}}</span>
}
</div>
</ng-template>
}
</ng-container>

View file

@ -0,0 +1,38 @@
@use '../../../series-detail-common';
.group-item {
background-color: transparent;
&:hover {
background-color: var(--card-bg-color);
border-radius: 5px;
cursor: pointer;
}
&:active, &.active {
background-color: var(--card-bg-color);
border-radius: 5px;
}
}
.pill {
font-size: .8rem;
background-color: var(--card-bg-color);
border-radius: 0.375rem;
color: var(--badge-text-color);
&.active {
background-color : var(--primary-color);
}
}
.custom-position {
right: 15px;
top: -42px;
}
a:hover {
cursor: pointer;
}

View file

@ -0,0 +1,277 @@
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
} from "../../_models/preferences/reading-profiles";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {Location, NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
import {User} from "../../_models/user";
import {AccountService} from "../../_services/account.service";
import {debounceTime, distinctUntilChanged, take, tap} from "rxjs/operators";
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
import {BookService} from "../../book-reader/_services/book.service";
import {BookPageLayoutMode} from "../../_models/readers/book-page-layout-mode";
import {PdfTheme} from "../../_models/preferences/pdf-theme";
import {PdfScrollMode} from "../../_models/preferences/pdf-scroll-mode";
import {PdfSpreadMode} from "../../_models/preferences/pdf-spread-mode";
import {bookColorThemes} from "../../book-reader/_components/reader-settings/reader-settings.component";
import {BookPageLayoutModePipe} from "../../_pipes/book-page-layout-mode.pipe";
import {LayoutModePipe} from "../../_pipes/layout-mode.pipe";
import {PageSplitOptionPipe} from "../../_pipes/page-split-option.pipe";
import {PdfScrollModePipe} from "../../_pipes/pdf-scroll-mode.pipe";
import {PdfSpreadModePipe} from "../../_pipes/pdf-spread-mode.pipe";
import {PdfThemePipe} from "../../_pipes/pdf-theme.pipe";
import {ReaderModePipe} from "../../_pipes/reading-mode.pipe";
import {ReadingDirectionPipe} from "../../_pipes/reading-direction.pipe";
import {ScalingOptionPipe} from "../../_pipes/scaling-option.pipe";
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
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 {filter} from "rxjs";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {LoadingComponent} from "../../shared/loading/loading.component";
enum TabId {
ImageReader = "image-reader",
BookReader = "book-reader",
PdfReader = "pdf-reader",
Series = "series",
Libraries = "libraries",
}
@Component({
selector: 'app-manage-reading-profiles',
imports: [
TranslocoDirective,
NgTemplateOutlet,
VirtualScrollerModule,
SentenceCasePipe,
BookPageLayoutModePipe,
FormsModule,
LayoutModePipe,
PageSplitOptionPipe,
PdfScrollModePipe,
PdfSpreadModePipe,
PdfThemePipe,
ReactiveFormsModule,
ReaderModePipe,
ReadingDirectionPipe,
ScalingOptionPipe,
SettingItemComponent,
SettingSwitchComponent,
TitleCasePipe,
WritingStylePipe,
NgStyle,
ColorPickerDirective,
NgbNav,
NgbNavItem,
NgbNavLinkBase,
NgbNavContent,
NgbNavOutlet,
LoadingComponent
],
templateUrl: './manage-reading-profiles.component.html',
styleUrl: './manage-reading-profiles.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ManageReadingProfilesComponent implements OnInit {
virtualScrollerBreakPoint = 20;
fontFamilies: Array<string> = [];
readingProfiles: ReadingProfile[] = [];
user!: User;
activeTabId = TabId.ImageReader;
loading = true;
selectedProfile: ReadingProfile | null = null;
readingProfileForm: FormGroup | null = null;
bookColorThemesTranslated = bookColorThemes.map(o => {
const d = {...o};
d.name = translate('theme.' + d.translationKey);
return d;
});
constructor(
private readingProfileService: ReadingProfileService,
private cdRef: ChangeDetectorRef,
private accountService: AccountService,
private bookService: BookService,
private destroyRef: DestroyRef,
) {
this.fontFamilies = this.bookService.getFontFamilies().map(f => f.title);
this.cdRef.markForCheck();
}
ngOnInit(): void {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.user = user;
console.log(this.user.preferences.defaultReadingProfileId);
}
});
this.readingProfileService.all().subscribe(profiles => {
this.readingProfiles = profiles;
this.loading = false;
this.setupForm();
this.cdRef.markForCheck();
});
}
delete(id: number) {
this.readingProfileService.delete(id).subscribe(() => {
this.selectProfile(undefined);
this.readingProfiles = this.readingProfiles.filter(o => o.id !== id);
this.cdRef.markForCheck();
});
}
setDefault(id: number) {
this.readingProfileService.setDefault(id).subscribe(() => {
this.user.preferences.defaultReadingProfileId = id;
this.cdRef.markForCheck();
})
}
setupForm() {
if (this.selectedProfile == null) {
return;
}
this.readingProfileForm = new FormGroup({})
if (this.fontFamilies.indexOf(this.selectedProfile.bookReaderFontFamily) < 0) {
this.selectedProfile.bookReaderFontFamily = 'default';
}
this.readingProfileForm.addControl('name', new FormControl(this.selectedProfile.name, Validators.required));
this.readingProfileForm.addControl('readingDirection', new FormControl(this.selectedProfile.readingDirection, []));
this.readingProfileForm.addControl('scalingOption', new FormControl(this.selectedProfile.scalingOption, []));
this.readingProfileForm.addControl('pageSplitOption', new FormControl(this.selectedProfile.pageSplitOption, []));
this.readingProfileForm.addControl('autoCloseMenu', new FormControl(this.selectedProfile.autoCloseMenu, []));
this.readingProfileForm.addControl('showScreenHints', new FormControl(this.selectedProfile.showScreenHints, []));
this.readingProfileForm.addControl('readerMode', new FormControl(this.selectedProfile.readerMode, []));
this.readingProfileForm.addControl('layoutMode', new FormControl(this.selectedProfile.layoutMode, []));
this.readingProfileForm.addControl('emulateBook', new FormControl(this.selectedProfile.emulateBook, []));
this.readingProfileForm.addControl('swipeToPaginate', new FormControl(this.selectedProfile.swipeToPaginate, []));
this.readingProfileForm.addControl('backgroundColor', new FormControl(this.selectedProfile.backgroundColor, []));
this.readingProfileForm.addControl('allowAutomaticWebtoonReaderDetection', new FormControl(this.selectedProfile.allowAutomaticWebtoonReaderDetection, []));
this.readingProfileForm.addControl('bookReaderFontFamily', new FormControl(this.selectedProfile.bookReaderFontFamily, []));
this.readingProfileForm.addControl('bookReaderFontSize', new FormControl(this.selectedProfile.bookReaderFontSize, []));
this.readingProfileForm.addControl('bookReaderLineSpacing', new FormControl(this.selectedProfile.bookReaderLineSpacing, []));
this.readingProfileForm.addControl('bookReaderMargin', new FormControl(this.selectedProfile.bookReaderMargin, []));
this.readingProfileForm.addControl('bookReaderReadingDirection', new FormControl(this.selectedProfile.bookReaderReadingDirection, []));
this.readingProfileForm.addControl('bookReaderWritingStyle', new FormControl(this.selectedProfile.bookReaderWritingStyle, []))
this.readingProfileForm.addControl('bookReaderTapToPaginate', new FormControl(this.selectedProfile.bookReaderTapToPaginate, []));
this.readingProfileForm.addControl('bookReaderLayoutMode', new FormControl(this.selectedProfile.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
this.readingProfileForm.addControl('bookReaderThemeName', new FormControl(this.selectedProfile.bookReaderThemeName || bookColorThemes[0].name, []));
this.readingProfileForm.addControl('bookReaderImmersiveMode', new FormControl(this.selectedProfile.bookReaderImmersiveMode, []));
this.readingProfileForm.addControl('pdfTheme', new FormControl(this.selectedProfile.pdfTheme || PdfTheme.Dark, []));
this.readingProfileForm.addControl('pdfScrollMode', new FormControl(this.selectedProfile.pdfScrollMode || PdfScrollMode.Vertical, []));
this.readingProfileForm.addControl('pdfSpreadMode', new FormControl(this.selectedProfile.pdfSpreadMode || PdfSpreadMode.None, []));
this.readingProfileForm.valueChanges.pipe(
debounceTime(500),
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);
}
})
} 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);
}
})
}
}),
).subscribe();
}
private packData(): ReadingProfile {
const data: ReadingProfile = this.readingProfileForm!.getRawValue();
data.id = this.selectedProfile!.id;
return data;
}
handleBackgroundColorChange(color: string) {
if (!this.readingProfileForm || !this.selectedProfile) return;
this.readingProfileForm.markAsDirty();
this.readingProfileForm.markAsTouched();
this.selectedProfile.backgroundColor = color;
this.readingProfileForm.get('backgroundColor')?.setValue(color);
this.cdRef.markForCheck();
}
selectProfile(profile: ReadingProfile | undefined | null) {
if (profile === undefined) {
this.selectedProfile = null;
this.cdRef.markForCheck();
return;
}
this.selectedProfile = profile;
this.setupForm();
this.cdRef.markForCheck();
}
addNew() {
const defaultProfile = this.readingProfiles.find(f => f.id === this.user.preferences.defaultReadingProfileId);
this.selectedProfile = {...defaultProfile!};
this.selectedProfile.id = 0;
this.selectedProfile.name = "New Profile #" + (this.readingProfiles.length + 1);
this.setupForm();
this.cdRef.markForCheck();
}
protected readonly readingDirections = readingDirections;
protected readonly pdfSpreadModes = pdfSpreadModes;
protected readonly pageSplitOptions = pageSplitOptions;
protected readonly bookLayoutModes = bookLayoutModes;
protected readonly pdfThemes = pdfThemes;
protected readonly scalingOptions = scalingOptions;
protected readonly layoutModes = layoutModes;
protected readonly readerModes = readingModes;
protected readonly bookWritingStyles = bookWritingStyles;
protected readonly pdfScrollModes = pdfScrollModes;
protected readonly TabId = TabId;
}

View file

@ -117,383 +117,6 @@
</div> </div>
<div class="setting-section-break"></div> <div class="setting-section-break"></div>
} }
<h4 id="image-reader-heading" class="mt-3">{{t('image-reader-settings-title')}}</h4>
<ng-container>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-direction-label')" [subtitle]="t('reading-direction-tooltip')">
<ng-template #view>
{{settingsForm.get('readingDirection')!.value | readingDirection}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="readingDirection">
@for (opt of readingDirections; track opt) {
<option [value]="opt.value">{{opt.value | readingDirection}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('scaling-option-label')" [subtitle]="t('scaling-option-tooltip')">
<ng-template #view>
{{settingsForm.get('scalingOption')!.value | scalingOption}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="scalingOption">
@for (opt of scalingOptions; track opt) {
<option [value]="opt.value">{{opt.value | scalingOption}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('page-splitting-label')" [subtitle]="t('page-splitting-tooltip')">
<ng-template #view>
{{settingsForm.get('pageSplitOption')!.value | pageSplitOption}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="pageSplitOption">
@for (opt of pageSplitOptions; track opt) {
<option [value]="opt.value">{{opt.value | pageSplitOption}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-mode-label')" [subtitle]="t('reading-mode-tooltip')">
<ng-template #view>
{{settingsForm.get('readerMode')!.value | readerMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="readerMode">
@for (opt of readerModes; track opt) {
<option [value]="opt.value">{{opt.value | readerMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('layout-mode-label')" [subtitle]="t('layout-mode-tooltip')">
<ng-template #view>
{{settingsForm.get('layoutMode')!.value | layoutMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="layoutMode">
@for (opt of layoutModes; track opt) {
<option [value]="opt.value">{{opt.value | layoutMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<!--
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('background-color-label')" [subtitle]="t('background-color-tooltip')">
<ng-template #view>
<div class="color-box-container">
<div class="color-box" [ngStyle]="{'background-color': user.preferences!.backgroundColor}"></div>
<span class="hex-code">{{ user.preferences!.backgroundColor.toUpperCase() }}</span>
</div>
</ng-template>
<ng-template #edit>
<input [value]="user!.preferences!.backgroundColor" class="form-control"
(colorPickerChange)="handleBackgroundColorChange($event)"
[style.background]="user!.preferences!.backgroundColor" [cpAlphaChannel]="'disabled'"
[(colorPicker)]="user!.preferences!.backgroundColor" />
</ng-template>
</app-setting-item>
</div>
-->
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('auto-close-menu-label')" [subtitle]="t('auto-close-menu-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="autoCloseMenu" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('show-screen-hints-label')" [subtitle]="t('show-screen-hints-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="showScreenHints" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('emulate-comic-book-label')" [subtitle]="t('emulate-comic-book-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="emulate-comic-book"
formControlName="emulateBook" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('swipe-to-paginate-label')" [subtitle]="t('swipe-to-paginate-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="swipe-to-paginate"
formControlName="swipeToPaginate" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('allow-auto-webtoon-reader-label')" [subtitle]="t('allow-auto-webtoon-reader-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="allow-auto-webtoon-reader"
formControlName="allowAutomaticWebtoonReaderDetection" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
</ng-container>
<div class="setting-section-break"></div>
<h4 id="book-reader-heading" class="mt-3">{{t('book-reader-settings-title')}}</h4>
<ng-container>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('tap-to-paginate-label')" [subtitle]="t('tap-to-paginate-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="tap-to-paginate"
formControlName="bookReaderTapToPaginate" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('immersive-mode-label')" [subtitle]="t('immersive-mode-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="bookReaderImmersiveMode" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-direction-label')" [subtitle]="t('reading-direction-tooltip')">
<ng-template #view>
{{settingsForm.get('bookReaderReadingDirection')!.value | readingDirection}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderReadingDirection">
@for (opt of readingDirections; track opt) {
<option [value]="opt.value">{{opt.value | readingDirection}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('font-family-label')" [subtitle]="t('font-family-tooltip')">
<ng-template #view>
{{settingsForm.get('bookReaderFontFamily')!.value | titlecase}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderFontFamily">
@for (opt of fontFamilies; track opt) {
<option [value]="opt">{{opt | titlecase}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('writing-style-label')" [subtitle]="t('writing-style-tooltip')">
<ng-template #view>
{{settingsForm.get('bookReaderWritingStyle')!.value | writingStyle}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderWritingStyle">
@for (opt of bookWritingStyles; track opt) {
<option [value]="opt.value">{{opt.value | writingStyle}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('layout-mode-book-label')" [subtitle]="t('layout-mode-book-tooltip')">
<ng-template #view>
{{settingsForm.get('bookReaderLayoutMode')!.value | bookPageLayoutMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderLayoutMode">
@for (opt of bookLayoutModes; track opt) {
<option [value]="opt.value">{{opt.value | bookPageLayoutMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('color-theme-book-label')" [subtitle]="t('color-theme-book-tooltip')">
<ng-template #view>
{{settingsForm.get('bookReaderThemeName')!.value}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderThemeName">
@for (opt of bookColorThemesTranslated; track opt) {
<option [value]="opt.name">{{opt.name | titlecase}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('font-size-book-label')" [subtitle]="t('font-size-book-tooltip')">
<ng-template #view>
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="fontsize" min="50" max="300" step="10"
formControlName="bookReaderFontSize">
</div>
<span class="ps-2 col-2 align-middle">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('line-height-book-label')" [subtitle]="t('line-height-book-tooltip')">
<ng-template #view>
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
formControlName="bookReaderLineSpacing">
</div>
<span class="ps-2 col-2 align-middle">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('margin-book-label')" [subtitle]="t('margin-book-tooltip')">
<ng-template #view>
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="margin" min="0" max="30" step="5"
formControlName="bookReaderMargin">
</div>
<span class="ps-2 col-2 align-middle">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
</ng-container>
<div class="setting-section-break"></div>
<h4 id="pdf-reader-heading" class="mt-3">{{t('pdf-reader-settings-title')}}</h4>
<ng-container>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-spread-mode-label')" [subtitle]="t('pdf-spread-mode-tooltip')">
<ng-template #view>
{{settingsForm.get('pdfSpreadMode')!.value | pdfSpreadMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfSpreadMode">
@for (opt of pdfSpreadModes; track opt) {
<option [value]="opt.value">{{opt.value | pdfSpreadMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-theme-label')" [subtitle]="t('pdf-theme-tooltip')">
<ng-template #view>
{{settingsForm.get('pdfTheme')!.value | pdfTheme}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfTheme">
@for (opt of pdfThemes; track opt) {
<option [value]="opt.value">{{opt.value | pdfTheme}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-scroll-mode-label')" [subtitle]="t('pdf-scroll-mode-tooltip')">
<ng-template #view>
{{settingsForm.get('pdfScrollMode')!.value | pdfScrollMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfScrollMode">
@for (opt of pdfScrollModes; track opt) {
<option [value]="opt.value">{{opt.value | pdfScrollMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
</ng-container>
</form> </form>
} }

View file

@ -80,23 +80,6 @@ export class ManageUserPreferencesComponent implements OnInit {
private readonly localizationService = inject(LocalizationService); private readonly localizationService = inject(LocalizationService);
protected readonly licenseService = inject(LicenseService); protected readonly licenseService = inject(LicenseService);
protected readonly readingDirections = readingDirections;
protected readonly scalingOptions = scalingOptions;
protected readonly pageSplitOptions = pageSplitOptions;
protected readonly readerModes = readingModes;
protected readonly layoutModes = layoutModes;
protected readonly bookWritingStyles = bookWritingStyles;
protected readonly bookLayoutModes = bookLayoutModes;
protected readonly pdfSpreadModes = pdfSpreadModes;
protected readonly pdfThemes = pdfThemes;
protected readonly pdfScrollModes = pdfScrollModes;
bookColorThemesTranslated = bookColorThemes.map(o => {
const d = {...o};
d.name = translate('theme.' + d.translationKey);
return d;
});
fontFamilies: Array<string> = []; fontFamilies: Array<string> = [];
locales: Array<KavitaLocale> = []; locales: Array<KavitaLocale> = [];
@ -137,37 +120,6 @@ export class ManageUserPreferencesComponent implements OnInit {
this.user = results.user; this.user = results.user;
this.user.preferences = results.pref; this.user.preferences = results.pref;
/*if (this.fontFamilies.indexOf(this.user.preferences.bookReaderFontFamily) < 0) {
this.user.preferences.bookReaderFontFamily = 'default';
}
this.settingsForm.addControl('readingDirection', new FormControl(this.user.preferences.readingDirection, []));
this.settingsForm.addControl('scalingOption', new FormControl(this.user.preferences.scalingOption, []));
this.settingsForm.addControl('pageSplitOption', new FormControl(this.user.preferences.pageSplitOption, []));
this.settingsForm.addControl('autoCloseMenu', new FormControl(this.user.preferences.autoCloseMenu, []));
this.settingsForm.addControl('showScreenHints', new FormControl(this.user.preferences.showScreenHints, []));
this.settingsForm.addControl('readerMode', new FormControl(this.user.preferences.readerMode, []));
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.layoutMode, []));
this.settingsForm.addControl('emulateBook', new FormControl(this.user.preferences.emulateBook, []));
this.settingsForm.addControl('swipeToPaginate', new FormControl(this.user.preferences.swipeToPaginate, []));
this.settingsForm.addControl('backgroundColor', new FormControl(this.user.preferences.backgroundColor, []));
this.settingsForm.addControl('allowAutomaticWebtoonReaderDetection', new FormControl(this.user.preferences.allowAutomaticWebtoonReaderDetection, []));
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, []));
this.settingsForm.addControl('bookReaderWritingStyle', new FormControl(this.user.preferences.bookReaderWritingStyle, []))
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.user.preferences.bookReaderTapToPaginate, []));
this.settingsForm.addControl('bookReaderLayoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
this.settingsForm.addControl('bookReaderThemeName', new FormControl(this.user?.preferences.bookReaderThemeName || bookColorThemes[0].name, []));
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user?.preferences.bookReaderImmersiveMode, []));
this.settingsForm.addControl('pdfTheme', new FormControl(this.user?.preferences.pdfTheme || PdfTheme.Dark, []));
this.settingsForm.addControl('pdfScrollMode', new FormControl(this.user?.preferences.pdfScrollMode || PdfScrollMode.Vertical, []));
this.settingsForm.addControl('pdfSpreadMode', new FormControl(this.user?.preferences.pdfSpreadMode || PdfSpreadMode.None, []));*/
this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, [])); this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, []));
this.settingsForm.addControl('globalPageLayoutMode', new FormControl(this.user.preferences.globalPageLayoutMode, [])); this.settingsForm.addControl('globalPageLayoutMode', new FormControl(this.user.preferences.globalPageLayoutMode, []));
this.settingsForm.addControl('blurUnreadSummaries', new FormControl(this.user.preferences.blurUnreadSummaries, [])); this.settingsForm.addControl('blurUnreadSummaries', new FormControl(this.user.preferences.blurUnreadSummaries, []));
@ -290,18 +242,8 @@ export class ManageUserPreferencesComponent implements OnInit {
//pdfScrollMode: parseInt(modelSettings.pdfScrollMode, 10), //pdfScrollMode: parseInt(modelSettings.pdfScrollMode, 10),
//pdfSpreadMode: parseInt(modelSettings.pdfSpreadMode, 10), //pdfSpreadMode: parseInt(modelSettings.pdfSpreadMode, 10),
aniListScrobblingEnabled: modelSettings.aniListScrobblingEnabled, aniListScrobblingEnabled: modelSettings.aniListScrobblingEnabled,
wantToReadSync: modelSettings.wantToReadSync wantToReadSync: modelSettings.wantToReadSync,
defaultReadingProfileId: this.user!.preferences.defaultReadingProfileId,
}; };
} }
handleBackgroundColorChange(color: string) {
this.settingsForm.markAsDirty();
this.settingsForm.markAsTouched();
if (this.user?.preferences) {
//this.user.preferences.backgroundColor = color;
}
this.settingsForm.get('backgroundColor')?.setValue(color);
this.cdRef.markForCheck();
}
} }

View file

@ -140,60 +140,6 @@
"want-to-read-sync-label": "Want To Read Sync", "want-to-read-sync-label": "Want To Read Sync",
"want-to-read-sync-tooltip": "Allow Kavita to add items to your Want to Read list based on AniList and MAL series in Pending readlist", "want-to-read-sync-tooltip": "Allow Kavita to add items to your Want to Read list based on AniList and MAL series in Pending readlist",
"image-reader-settings-title": "Image Reader",
"reading-direction-label": "Reading Direction",
"reading-direction-tooltip": "Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.",
"scaling-option-label": "Scaling Options",
"scaling-option-tooltip": "How to scale the image to your screen.",
"page-splitting-label": "Page Splitting",
"page-splitting-tooltip": "How to split a full width image (ie both left and right images are combined)",
"reading-mode-label": "Reading Mode",
"reading-mode-tooltip": "Change reader to paginate vertically, horizontally, or have an infinite scroll",
"layout-mode-label": "Layout Mode",
"layout-mode-tooltip": "Render a single image to the screen or two side-by-side images",
"background-color-label": "Background Color",
"background-color-tooltip": "Background Color of Image Reader",
"auto-close-menu-label": "Auto Close Menu",
"auto-close-menu-tooltip": "Should menu auto close",
"show-screen-hints-label": "Show Screen Hints",
"show-screen-hints-tooltip": "Show an overlay to help understand pagination area and direction",
"emulate-comic-book-label": "Emulate comic book",
"emulate-comic-book-tooltip": "Applies a shadow effect to emulate reading from a book",
"swipe-to-paginate-label": "Swipe to Paginate",
"swipe-to-paginate-tooltip": "Should swiping on the screen cause the next or previous page to be triggered",
"allow-auto-webtoon-reader-label": "Automatic Webtoon Reader Mode",
"allow-auto-webtoon-reader-tooltip": "Switch into Webtoon Reader mode if pages look like a webtoon. Some false positives may occur.",
"book-reader-settings-title": "Book Reader",
"tap-to-paginate-label": "Tap to Paginate",
"tap-to-paginate-tooltip": "Should the sides of the book reader screen allow tapping on it to move to prev/next page",
"immersive-mode-label": "Immersive Mode",
"immersive-mode-tooltip": "This will hide the menu behind a click on the reader document and turn tap to paginate on",
"reading-direction-book-label": "Reading Direction",
"reading-direction-book-tooltip": "Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.",
"font-family-label": "Font Family",
"font-family-tooltip": "Font family to load up. Default will load the book's default font",
"writing-style-label": "Writing Style",
"writing-style-tooltip": "Changes the direction of the text. Horizontal is left to right, vertical is top to bottom.",
"layout-mode-book-label": "Layout Mode",
"layout-mode-book-tooltip": "How content should be laid out. Scroll is as the book packs it. 1 or 2 Column fits to the height of the device and fits 1 or 2 columns of text per page",
"color-theme-book-label": "Color Theme",
"color-theme-book-tooltip": "What color theme to apply to the book reader content and menu",
"font-size-book-label": "Font Size",
"font-size-book-tooltip": "Percent of scaling to apply to font in the book",
"line-height-book-label": "Line Spacing",
"line-height-book-tooltip": "How much spacing between the lines of the book",
"margin-book-label": "Margin",
"margin-book-tooltip": "How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.",
"pdf-reader-settings-title": "PDF Reader",
"pdf-scroll-mode-label": "Scroll Mode",
"pdf-scroll-mode-tooltip": "How you scroll through pages. Vertical/Horizontal and Tap to Paginate (no scroll)",
"pdf-spread-mode-label": "Spread Mode",
"pdf-spread-mode-tooltip": "How pages should be laid out. Single or double (odd/even)",
"pdf-theme-label": "Theme",
"pdf-theme-tooltip": "Color theme of the reader",
"clients-opds-alert": "OPDS is not enabled on this server. This will not affect Tachiyomi users.", "clients-opds-alert": "OPDS is not enabled on this server. This will not affect Tachiyomi users.",
"clients-opds-description": "All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.", "clients-opds-description": "All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.",
"clients-api-key-tooltip": "The API key is like a password. Resetting it will invalidate any existing clients.", "clients-api-key-tooltip": "The API key is like a password. Resetting it will invalidate any existing clients.",
@ -1716,6 +1662,7 @@
"scrobble-holds": "Scrobble Holds", "scrobble-holds": "Scrobble Holds",
"account": "Account", "account": "Account",
"preferences": "Preferences", "preferences": "Preferences",
"reading-profiles": "Reading Profiles",
"clients": "API Key / OPDS", "clients": "API Key / OPDS",
"devices": "Devices", "devices": "Devices",
"user-stats": "Stats", "user-stats": "Stats",
@ -2838,6 +2785,75 @@
"pdf-dark": "Dark" "pdf-dark": "Dark"
}, },
"manage-reading-profiles": {
"description": "Not all your series may be read in the same way, set up distinct reading profiles per library or series to make getting back in your series as seamless as possible.",
"extra-tip": "When changing reading settings for a specific series, an implicit profile is created until you save it to one you created. To keep your list from cluttering with every little change you might need",
"profiles-title": "Your reading profiles",
"default-profile": "Default",
"add": "{{common.add}}",
"make-default": "Set as default",
"no-selected": "No profile selected",
"selection-tip": "Select a profile from the list, or create a new one at the top right",
"image-reader-settings-title": "Image Reader",
"reading-direction-label": "Reading Direction",
"reading-direction-tooltip": "Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.",
"scaling-option-label": "Scaling Options",
"scaling-option-tooltip": "How to scale the image to your screen.",
"page-splitting-label": "Page Splitting",
"page-splitting-tooltip": "How to split a full width image (ie both left and right images are combined)",
"reading-mode-label": "Reading Mode",
"reading-mode-tooltip": "Change reader to paginate vertically, horizontally, or have an infinite scroll",
"layout-mode-label": "Layout Mode",
"layout-mode-tooltip": "Render a single image to the screen or two side-by-side images",
"background-color-label": "Background Color",
"background-color-tooltip": "Background Color of Image Reader",
"auto-close-menu-label": "Auto Close Menu",
"auto-close-menu-tooltip": "Should menu auto close",
"show-screen-hints-label": "Show Screen Hints",
"show-screen-hints-tooltip": "Show an overlay to help understand pagination area and direction",
"emulate-comic-book-label": "Emulate comic book",
"emulate-comic-book-tooltip": "Applies a shadow effect to emulate reading from a book",
"swipe-to-paginate-label": "Swipe to Paginate",
"swipe-to-paginate-tooltip": "Should swiping on the screen cause the next or previous page to be triggered",
"allow-auto-webtoon-reader-label": "Automatic Webtoon Reader Mode",
"allow-auto-webtoon-reader-tooltip": "Switch into Webtoon Reader mode if pages look like a webtoon. Some false positives may occur.",
"book-reader-settings-title": "Book Reader",
"tap-to-paginate-label": "Tap to Paginate",
"tap-to-paginate-tooltip": "Should the sides of the book reader screen allow tapping on it to move to prev/next page",
"immersive-mode-label": "Immersive Mode",
"immersive-mode-tooltip": "This will hide the menu behind a click on the reader document and turn tap to paginate on",
"reading-direction-book-label": "Reading Direction",
"reading-direction-book-tooltip": "Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.",
"font-family-label": "Font Family",
"font-family-tooltip": "Font family to load up. Default will load the book's default font",
"writing-style-label": "Writing Style",
"writing-style-tooltip": "Changes the direction of the text. Horizontal is left to right, vertical is top to bottom.",
"layout-mode-book-label": "Layout Mode",
"layout-mode-book-tooltip": "How content should be laid out. Scroll is as the book packs it. 1 or 2 Column fits to the height of the device and fits 1 or 2 columns of text per page",
"color-theme-book-label": "Color Theme",
"color-theme-book-tooltip": "What color theme to apply to the book reader content and menu",
"font-size-book-label": "Font Size",
"font-size-book-tooltip": "Percent of scaling to apply to font in the book",
"line-height-book-label": "Line Spacing",
"line-height-book-tooltip": "How much spacing between the lines of the book",
"margin-book-label": "Margin",
"margin-book-tooltip": "How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.",
"pdf-reader-settings-title": "PDF Reader",
"pdf-scroll-mode-label": "Scroll Mode",
"pdf-scroll-mode-tooltip": "How you scroll through pages. Vertical/Horizontal and Tap to Paginate (no scroll)",
"pdf-spread-mode-label": "Spread Mode",
"pdf-spread-mode-tooltip": "How pages should be laid out. Single or double (odd/even)",
"pdf-theme-label": "Theme",
"pdf-theme-tooltip": "Color theme of the reader",
"reading-profile-series-settings-title": "Series",
"reading-profile-library-settings-title": "Library"
},
"validation": { "validation": {
"required-field": "This field is required", "required-field": "This field is required",