PDF Reader Settings, New Reading Modes, and lots of fixes (#2828)

Co-authored-by: Elry <144011449+ElryWeeb@users.noreply.github.com>
Co-authored-by: AlienHack <the4got10@windowslive.com>
Co-authored-by: William Brockhus <pickeringw@gmail.com>
Co-authored-by: Shivam Amin <xShivam.Amin@gmail.com>
This commit is contained in:
Joe Milazzo 2024-03-30 15:07:03 -05:00 committed by GitHub
parent f22f30b5a9
commit 2bde0ac82a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 4410 additions and 439 deletions

15
.sonarcloud.properties Normal file
View File

@ -0,0 +1,15 @@
# Path to sources
sonar.sources=.
sonar.exclusions=API.Benchmark
#sonar.inclusions=
# Path to tests
sonar.tests=API.Tests
#sonar.test.exclusions=
#sonar.test.inclusions=
# Source encoding
sonar.sourceEncoding=UTF-8
# Exclusions for copy-paste detection
#sonar.cpd.exclusions=

View File

@ -9,8 +9,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.28" /> <PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.0.2" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.28" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.0.2" />
<PackageReference Include="xunit" Version="2.7.0" /> <PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7"> <PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -78,6 +78,8 @@ public class ComicParsingTests
[InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")] [InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")]
[InlineData("Kebab Том 1 Глава 1", "Kebab")] [InlineData("Kebab Том 1 Глава 1", "Kebab")]
[InlineData("Манга Глава 1", "Манга")] [InlineData("Манга Глава 1", "Манга")]
[InlineData("ReZero รีเซทชีวิต ฝ่าวิกฤตต่างโลก เล่ม 1", "ReZero รีเซทชีวิต ฝ่าวิกฤตต่างโลก")]
[InlineData("SKY WORLD สกายเวิลด์ เล่มที่ 1", "SKY WORLD สกายเวิลด์")]
public void ParseComicSeriesTest(string filename, string expected) public void ParseComicSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename));
@ -129,6 +131,9 @@ public class ComicParsingTests
// Russian Tests // Russian Tests
[InlineData("Kebab Том 1 Глава 3", "1")] [InlineData("Kebab Том 1 Глава 3", "1")]
[InlineData("Манга Глава 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Манга Глава 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
[InlineData("ย้อนเวลากลับมาร้าย เล่ม 1", "1")]
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "1")]
[InlineData("วิวาห์รัก เดิมพันชีวิต ตอนที่ 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
public void ParseComicVolumeTest(string filename, string expected) public void ParseComicVolumeTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(filename));
@ -178,6 +183,9 @@ public class ComicParsingTests
[InlineData("Манга Глава 2", "2")] [InlineData("Манга Глава 2", "2")]
[InlineData("Манга 2 Глава", "2")] [InlineData("Манга 2 Глава", "2")]
[InlineData("Манга Том 1 2 Глава", "2")] [InlineData("Манга Том 1 2 Глава", "2")]
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")]
[InlineData("Max Level Returner ตอนที่ 5", "5")]
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
public void ParseComicChapterTest(string filename, string expected) public void ParseComicChapterTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(filename));

View File

@ -207,6 +207,9 @@ public class MangaParsingTests
[InlineData("test 2 years 1화", "test 2 years")] [InlineData("test 2 years 1화", "test 2 years")]
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake", "Nagasarete Airantou")] [InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake", "Nagasarete Airantou")]
[InlineData("Cynthia The Mission - c000 - c006 (v06)", "Cynthia The Mission")] [InlineData("Cynthia The Mission - c000 - c006 (v06)", "Cynthia The Mission")]
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1", "เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท")]
[InlineData("Max Level Returner เล่มที่ 5", "Max Level Returner")]
[InlineData("หนึ่งความคิด นิจนิรันดร์ เล่ม 2", "หนึ่งความคิด นิจนิรันดร์")]
public void ParseSeriesTest(string filename, string expected) public void ParseSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
@ -296,6 +299,9 @@ public class MangaParsingTests
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")] [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")]
[InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")] [InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")]
[InlineData("Adabana c00-02", "0-2")] [InlineData("Adabana c00-02", "0-2")]
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")]
[InlineData("Max Level Returner ตอนที่ 5", "5")]
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
public void ParseChaptersTest(string filename, string expected) public void ParseChaptersTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));

View File

@ -69,7 +69,7 @@
<PackageReference Include="Hangfire.InMemory" Version="0.8.1" /> <PackageReference Include="Hangfire.InMemory" Version="0.8.1" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" /> <PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.1" /> <PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.59" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.60" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" /> <PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.11" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.8.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
@ -81,8 +81,8 @@
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" /> <PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" /> <PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" /> <PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
<PackageReference Include="NetVips" Version="2.4.0" /> <PackageReference Include="NetVips" Version="2.4.1" />
<PackageReference Include="NetVips.Native" Version="8.15.1" /> <PackageReference Include="NetVips.Native" Version="8.15.2" />
<PackageReference Include="NReco.Logging.File" Version="1.2.0" /> <PackageReference Include="NReco.Logging.File" Version="1.2.0" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
@ -95,14 +95,14 @@
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" /> <PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.36.0" /> <PackageReference Include="SharpCompress" Version="0.36.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.21.0.86780"> <PackageReference Include="SonarAnalyzer.CSharp" Version="9.23.0.88079">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.5.0" />
<PackageReference Include="System.IO.Abstractions" Version="20.0.28" /> <PackageReference Include="System.IO.Abstractions" Version="21.0.2" />
<PackageReference Include="System.Drawing.Common" Version="8.0.3" /> <PackageReference Include="System.Drawing.Common" Version="8.0.3" />
<PackageReference Include="VersOne.Epub" Version="3.3.1" /> <PackageReference Include="VersOne.Epub" Version="3.3.1" />
</ItemGroup> </ItemGroup>

View File

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using API.Constants; using API.Constants;
using API.Data; using API.Data;
using API.DTOs.Uploads; using API.DTOs.Uploads;
using API.Entities.Enums;
using API.Extensions; using API.Extensions;
using API.Services; using API.Services;
using API.SignalR; using API.SignalR;
@ -98,6 +99,7 @@ public class UploadController : BaseApiController
try try
{ {
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist")); if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}"); var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
@ -225,17 +227,14 @@ public class UploadController : BaseApiController
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-reading-list-save")); return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-reading-list-save"));
} }
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename, int thumbnailSize = 0) private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename)
{ {
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (thumbnailSize > 0) var encodeFormat = settings.EncodeMediaAs;
{ var coverImageSize = settings.CoverImageSize;
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
filename, encodeFormat, thumbnailSize);
}
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
filename, encodeFormat); filename, encodeFormat, coverImageSize.GetDimensions().Width);
} }
/// <summary> /// <summary>
@ -326,8 +325,7 @@ public class UploadController : BaseApiController
try try
{ {
var filePath = await CreateThumbnail(uploadFileDto, var filePath = await CreateThumbnail(uploadFileDto,
$"{ImageService.GetLibraryFormat(uploadFileDto.Id)}", $"{ImageService.GetLibraryFormat(uploadFileDto.Id)}");
ImageService.LibraryThumbnailWidth);
if (!string.IsNullOrEmpty(filePath)) if (!string.IsNullOrEmpty(filePath))
{ {

View File

@ -118,6 +118,12 @@ public class UsersController : BaseApiController
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate; 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.PdfLayoutMode = preferencesDto.PdfLayoutMode;
existingPreferences.PdfScrollMode = preferencesDto.PdfScrollMode;
existingPreferences.PdfSpreadMode = preferencesDto.PdfSpreadMode;
if (_localizationService.GetLocales().Contains(preferencesDto.Locale)) if (_localizationService.GetLocales().Contains(preferencesDto.Locale))
{ {
existingPreferences.Locale = preferencesDto.Locale; existingPreferences.Locale = preferencesDto.Locale;

View File

@ -152,4 +152,25 @@ public class UserPreferencesDto
/// </summary> /// </summary>
[Required] [Required]
public string Locale { get; set; } public string Locale { get; set; }
/// <summary>
/// PDF Reader: Theme of the Reader
/// </summary>
[Required]
public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark;
/// <summary>
/// PDF Reader: Scroll mode of the reader
/// </summary>
[Required]
public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical;
/// <summary>
/// PDF Reader: Layout Mode of the reader
/// </summary>
[Required]
public PdfLayoutMode PdfLayoutMode { get; set; } = PdfLayoutMode.Multiple;
/// <summary>
/// PDF Reader: Spread Mode of the reader
/// </summary>
[Required]
public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None;
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class PdfSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PdfLayoutMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "PdfScrollMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "PdfSpreadMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "PdfTheme",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PdfLayoutMode",
table: "AppUserPreferences");
migrationBuilder.DropColumn(
name: "PdfScrollMode",
table: "AppUserPreferences");
migrationBuilder.DropColumn(
name: "PdfSpreadMode",
table: "AppUserPreferences");
migrationBuilder.DropColumn(
name: "PdfTheme",
table: "AppUserPreferences");
}
}
}

View File

@ -355,6 +355,18 @@ namespace API.Data.Migrations
b.Property<int>("PageSplitOption") b.Property<int>("PageSplitOption")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("PdfLayoutMode")
.HasColumnType("INTEGER");
b.Property<int>("PdfScrollMode")
.HasColumnType("INTEGER");
b.Property<int>("PdfSpreadMode")
.HasColumnType("INTEGER");
b.Property<int>("PdfTheme")
.HasColumnType("INTEGER");
b.Property<bool>("PromptForDownloadSize") b.Property<bool>("PromptForDownloadSize")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View File

@ -7,6 +7,9 @@ namespace API.Entities;
public class AppUserPreferences public class AppUserPreferences
{ {
public int Id { get; set; } public int Id { get; set; }
#region MangaReader
/// <summary> /// <summary>
/// Manga Reader Option: What direction should the next/prev page buttons go /// Manga Reader Option: What direction should the next/prev page buttons go
/// </summary> /// </summary>
@ -51,6 +54,11 @@ public class AppUserPreferences
/// Manga Reader Option: Should swiping trigger pagination /// Manga Reader Option: Should swiping trigger pagination
/// </summary> /// </summary>
public bool SwipeToPaginate { get; set; } public bool SwipeToPaginate { get; set; }
#endregion
#region EpubReader
/// <summary> /// <summary>
/// Book Reader Option: Override extra Margin /// Book Reader Option: Override extra Margin
/// </summary> /// </summary>
@ -75,17 +83,11 @@ public class AppUserPreferences
/// Book Reader Option: What direction should the next/prev page buttons go /// Book Reader Option: What direction should the next/prev page buttons go
/// </summary> /// </summary>
public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight; public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight;
/// <summary> /// <summary>
/// Book Reader Option: Defines the writing styles vertical/horizontal /// Book Reader Option: Defines the writing styles vertical/horizontal
/// </summary> /// </summary>
public WritingStyle BookReaderWritingStyle { get; set; } = WritingStyle.Horizontal; public WritingStyle BookReaderWritingStyle { get; set; } = WritingStyle.Horizontal;
/// <summary> /// <summary>
/// UI Site Global Setting: The UI theme the user should use.
/// </summary>
/// <remarks>Should default to Dark</remarks>
public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0];
/// <summary>
/// Book Reader Option: The color theme to decorate the book contents /// Book Reader Option: The color theme to decorate the book contents
/// </summary> /// </summary>
/// <remarks>Should default to Dark</remarks> /// <remarks>Should default to Dark</remarks>
@ -101,6 +103,37 @@ public class AppUserPreferences
/// </summary> /// </summary>
/// <remarks>Defaults to false</remarks> /// <remarks>Defaults to false</remarks>
public bool BookReaderImmersiveMode { get; set; } = false; public bool BookReaderImmersiveMode { get; set; } = false;
#endregion
#region PdfReader
/// <summary>
/// PDF Reader: Theme of the Reader
/// </summary>
public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark;
/// <summary>
/// PDF Reader: Scroll mode of the reader
/// </summary>
public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical;
/// <summary>
/// PDF Reader: Layout Mode of the reader
/// </summary>
public PdfLayoutMode PdfLayoutMode { get; set; } = PdfLayoutMode.Multiple;
/// <summary>
/// PDF Reader: Spread Mode of the reader
/// </summary>
public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None;
#endregion
#region Global
/// <summary>
/// UI Site Global Setting: The UI theme the user should use.
/// </summary>
/// <remarks>Should default to Dark</remarks>
public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0];
/// <summary> /// <summary>
/// Global Site Option: If the UI should layout items as Cards or List items /// Global Site Option: If the UI should layout items as Cards or List items
/// </summary> /// </summary>
@ -132,6 +165,8 @@ public class AppUserPreferences
/// </summary> /// </summary>
public string Locale { get; set; } public string Locale { get; set; }
#endregion
public AppUser AppUser { get; set; } = null!; public AppUser AppUser { get; set; } = null!;
public int AppUserId { get; set; } public int AppUserId { get; set; }
} }

View File

@ -0,0 +1,21 @@
using System.ComponentModel;
namespace API.Entities.Enums.UserPreferences;
public enum PdfLayoutMode
{
/// <summary>
/// Multiple pages render stacked (normal pdf experience)
/// </summary>
[Description("Multiple")]
Multiple = 0,
// [Description("Single")]
// Single = 1,
/// <summary>
/// A book mode where page turns are animated and layout is side-by-side
/// </summary>
[Description("Book")]
Book = 2,
// [Description("Infinite Scroll")]
// InfiniteScroll = 3
}

View File

@ -0,0 +1,21 @@
using System.ComponentModel;
namespace API.Entities.Enums.UserPreferences;
/// <summary>
/// Enum values match PdfViewer's enums
/// </summary>
public enum PdfScrollMode
{
[Description("Vertical")]
Vertical = 0,
[Description("Horizontal")]
Horizontal = 1,
// [Description("Wrapped")]
// Wrapped = 2,
/// <summary>
/// Single page view (tap to pagninate)
/// </summary>
[Description("Page")]
Page = 3
}

View File

@ -0,0 +1,13 @@
using System.ComponentModel;
namespace API.Entities.Enums.UserPreferences;
public enum PdfSpreadMode
{
[Description("None")]
None = 0,
[Description("Odd")]
Odd = 1,
[Description("Even")]
Even = 2
}

View File

@ -0,0 +1,11 @@
using System.ComponentModel;
namespace API.Entities.Enums.UserPreferences;
public enum PdfTheme
{
[Description("Dark")]
Dark = 0,
[Description("Light")]
Light = 1
}

View File

@ -3,6 +3,7 @@ using System.IO;
using System.Linq; using System.Linq;
using API.Entities; using API.Entities;
using API.Helpers; using API.Helpers;
using API.Helpers.Builders;
using API.Services.Tasks.Scanner.Parser; using API.Services.Tasks.Scanner.Parser;
namespace API.Extensions; namespace API.Extensions;
@ -24,6 +25,7 @@ public static class ChapterListExtensions
/// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info /// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info
/// is <see cref="ParserInfo.IsSpecial"/> then, the filename is used to search against Range or if filename exists within Files of said Chapter. /// is <see cref="ParserInfo.IsSpecial"/> then, the filename is used to search against Range or if filename exists within Files of said Chapter.
/// </summary> /// </summary>
/// <remarks>This uses GetNumberTitle() to calculate the Range to compare against the info.Chapters</remarks>
/// <param name="chapters"></param> /// <param name="chapters"></param>
/// <param name="info"></param> /// <param name="info"></param>
/// <returns></returns> /// <returns></returns>
@ -31,9 +33,12 @@ public static class ChapterListExtensions
{ {
var normalizedPath = Parser.NormalizePath(info.FullFilePath); var normalizedPath = Parser.NormalizePath(info.FullFilePath);
var specialTreatment = info.IsSpecialInfo(); var specialTreatment = info.IsSpecialInfo();
// NOTE: This can fail to find the chapter when Range is "1.0" as the chapter will store it as "1" hence why we need to emulate a Chapter
var fakeChapter = new ChapterBuilder(info.Chapters, info.Chapters).Build();
fakeChapter.UpdateFrom(info);
return specialTreatment return specialTreatment
? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath)) ? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath))
: chapters.FirstOrDefault(c => c.Range == info.Chapters); : chapters.FirstOrDefault(c => c.Range == fakeChapter.GetNumberTitle());
} }
/// <summary> /// <summary>

View File

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

View File

@ -36,7 +36,7 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
var specialTitle = specialTreatment ? Parser.RemoveExtensionIfSupported(info.Filename) : info.Chapters; var specialTitle = specialTreatment ? Parser.RemoveExtensionIfSupported(info.Filename) : info.Chapters;
var builder = new ChapterBuilder(Parser.DefaultChapter); var builder = new ChapterBuilder(Parser.DefaultChapter);
return builder.WithNumber(Parser.RemoveExtensionIfSupported(info.Chapters)) return builder.WithNumber(Parser.RemoveExtensionIfSupported(info.Chapters)!)
.WithRange(specialTreatment ? info.Filename : info.Chapters) .WithRange(specialTreatment ? info.Filename : info.Chapters)
.WithTitle((specialTreatment && info.Format == MangaFormat.Epub) .WithTitle((specialTreatment && info.Format == MangaFormat.Epub)
? info.Title ? info.Title

View File

@ -382,7 +382,7 @@ public class BookService : IBookService
} }
} }
var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link"); var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link[@href]");
if (styleNodes != null) if (styleNodes != null)
{ {
foreach (var styleLinks in styleNodes) foreach (var styleLinks in styleNodes)

View File

@ -148,14 +148,14 @@ public class LibraryWatcher : ILibraryWatcher
private void OnChanged(object sender, FileSystemEventArgs e) private void OnChanged(object sender, FileSystemEventArgs e)
{ {
_logger.LogDebug("[LibraryWatcher] Changed: {FullPath}, {Name}, {ChangeType}", e.FullPath, e.Name, e.ChangeType); _logger.LogTrace("[LibraryWatcher] Changed: {FullPath}, {Name}, {ChangeType}", e.FullPath, e.Name, e.ChangeType);
if (e.ChangeType != WatcherChangeTypes.Changed) return; if (e.ChangeType != WatcherChangeTypes.Changed) return;
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name)))); BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name))));
} }
private void OnCreated(object sender, FileSystemEventArgs e) private void OnCreated(object sender, FileSystemEventArgs e)
{ {
_logger.LogDebug("[LibraryWatcher] Created: {FullPath}, {Name}", e.FullPath, e.Name); _logger.LogTrace("[LibraryWatcher] Created: {FullPath}, {Name}", e.FullPath, e.Name);
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, !_directoryService.FileSystem.File.Exists(e.Name))); BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, !_directoryService.FileSystem.File.Exists(e.Name)));
} }
@ -167,7 +167,7 @@ public class LibraryWatcher : ILibraryWatcher
private void OnDeleted(object sender, FileSystemEventArgs e) { private void OnDeleted(object sender, FileSystemEventArgs e) {
var isDirectory = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name)); var isDirectory = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name));
if (!isDirectory) return; if (!isDirectory) return;
_logger.LogDebug("[LibraryWatcher] Deleted: {FullPath}, {Name}", e.FullPath, e.Name); _logger.LogTrace("[LibraryWatcher] Deleted: {FullPath}, {Name}", e.FullPath, e.Name);
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, true)); BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, true));
} }
@ -285,10 +285,10 @@ public class LibraryWatcher : ILibraryWatcher
var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList(); var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList();
_logger.LogTrace("[LibraryWatcher] Root Folders: {RootFolders}", rootFolder); _logger.LogTrace("[LibraryWatcher] Root Folders: {RootFolders}", rootFolder);
if (!rootFolder.Any()) return string.Empty; if (rootFolder.Count == 0) return string.Empty;
// Select the first folder and join with library folder, this should give us the folder to scan. // Select the first folder and join with library folder, this should give us the folder to scan.
return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[rootFolder.Count - 1])); return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[rootFolder.Count - 1]));
} }

View File

@ -115,13 +115,21 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
{ {
info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim(); info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim();
} }
if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format))
{
info.IsSpecial = true;
info.Chapters = Parser.DefaultChapter;
info.Volumes = Parser.SpecialVolume;
}
if (!string.IsNullOrEmpty(info.ComicInfo.Number)) if (!string.IsNullOrEmpty(info.ComicInfo.Number))
{ {
info.Chapters = info.ComicInfo.Number; info.Chapters = info.ComicInfo.Number;
if (info.IsSpecial && Parser.DefaultChapter != info.Chapters) if (info.IsSpecial && Parser.DefaultChapter != info.Chapters)
{ {
info.IsSpecial = false; info.IsSpecial = false;
info.Volumes = $"{Parser.SpecialVolumeNumber}"; info.Volumes = Parser.SpecialVolume;
} }
} }
@ -130,6 +138,7 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
{ {
info.SeriesSort = info.ComicInfo.TitleSort.Trim(); info.SeriesSort = info.ComicInfo.TitleSort.Trim();
} }
} }
public abstract bool IsApplicable(string filePath, LibraryType type); public abstract bool IsApplicable(string filePath, LibraryType type);

View File

@ -121,6 +121,10 @@ public static class Parser
private static readonly Regex[] MangaVolumeRegex = new[] private static readonly Regex[] MangaVolumeRegex = new[]
{ {
// Thai Volume: เล่ม n -> Volume n
new Regex(
@"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Dance in the Vampire Bund v16-17 // Dance in the Vampire Bund v16-17
new Regex( new Regex(
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)", @"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)",
@ -194,6 +198,10 @@ public static class Parser
private static readonly Regex[] MangaSeriesRegex = new[] private static readonly Regex[] MangaSeriesRegex = new[]
{ {
// Thai Volume: เล่ม n -> Volume n
new Regex(
@"(?<Series>.+?)(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: Том n -> Volume n, Тома n -> Volume // Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex( new Regex(
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)", @"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
@ -368,6 +376,10 @@ public static class Parser
private static readonly Regex[] ComicSeriesRegex = new[] private static readonly Regex[] ComicSeriesRegex = new[]
{ {
// Thai Volume: เล่ม n -> Volume n
new Regex(
@"(?<Series>.+?)(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: Том n -> Volume n, Тома n -> Volume // Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex( new Regex(
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)", @"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
@ -456,6 +468,10 @@ public static class Parser
private static readonly Regex[] ComicVolumeRegex = new[] private static readonly Regex[] ComicVolumeRegex = new[]
{ {
// Thai Volume: เล่ม n -> Volume n
new Regex(
@"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) // Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
new Regex( new Regex(
@"^(?<Series>.+?)(?: |_)(t|v)(?<Volume>" + NumberRange + @")", @"^(?<Series>.+?)(?: |_)(t|v)(?<Volume>" + NumberRange + @")",
@ -492,6 +508,10 @@ public static class Parser
private static readonly Regex[] ComicChapterRegex = new[] private static readonly Regex[] ComicChapterRegex = new[]
{ {
// Thai Volume: บทที่ n -> Chapter n, ตอนที่ n -> Chapter n
new Regex(
@"(บทที่|ตอนที่)(\s)?(\.?)(\s|_)?(?<Chapter>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Batman & Wildcat (1 of 3) // Batman & Wildcat (1 of 3)
new Regex( new Regex(
@"(?<Series>.*(\d{4})?)( |_)(?:\((?<Chapter>\d+) of \d+)", @"(?<Series>.*(\d{4})?)( |_)(?:\((?<Chapter>\d+) of \d+)",
@ -557,6 +577,10 @@ public static class Parser
private static readonly Regex[] MangaChapterRegex = new[] private static readonly Regex[] MangaChapterRegex = new[]
{ {
// Thai Chapter: บทที่ n -> Chapter n, ตอนที่ n -> Chapter n, เล่ม n -> Volume n, เล่มที่ n -> Volume n
new Regex(
@"(?<Volume>((เล่ม|เล่มที่))?(\s|_)?\.?\d+)(\s|_)(บทที่|ตอนที่)\.?(\s|_)?(?<Chapter>\d+)",
MatchOptions, RegexTimeout),
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5 // Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5
new Regex( new Regex(
@"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)(-c?\d+(\.\d)?)?)", @"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)(-c?\d+(\.\d)?)?)",

View File

@ -701,7 +701,8 @@ public class ProcessSeries : IProcessSeries
{ {
if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter)) if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter))
{ {
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.Range, volume.Name, parsedInfos[0].Series); _logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}",
existingChapter.Range, volume.Name, parsedInfos[0].Series);
volume.Chapters.Remove(existingChapter); volume.Chapters.Remove(existingChapter);
} }
else else

View File

@ -21,15 +21,18 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit
1. Fork Kavita 1. Fork Kavita
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github) 2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
3. Install the required Node Packages 3. Install the required Node Packages
- cd Kavita/UI/Web - `cd Kavita/UI/Web`
- `npm install` - `npm install`
- `npm install -g @angular/cli` - `npm install -g @angular/cli`
- `npm run cache-locale-prime` (only do this once to generate the locale file) 5. Start the frontend
4. Start angular server `ng serve` - `npm run start`
5. Build the project in Visual Studio/Rider, Setting startup project to `API` 6. Build the project in Visual Studio/Rider, Setting startup project to `API`
6. Debug the project in Visual Studio/Rider 7. Debug the project in Visual Studio/Rider
7. Open http://localhost:4200 8. Open http://localhost:4200
8. (Deployment only) Run build.sh and pass the Runtime Identifier for your OS or just build.sh for all supported RIDs. 9. (Deployment only) Run build.sh and pass the Runtime Identifier for your OS or just build.sh for all supported RIDs.
### Debugging on Device ###
- Update `IP` constant in `Web/UI/src/environments/environment.ts` to your dev machine's ip instead of `localhost`.
### Contributing Code ### ### Contributing Code ###

View File

@ -15,7 +15,7 @@
<PackageReference Include="Flurl.Http" Version="3.2.4" /> <PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.21.0.86780"> <PackageReference Include="SonarAnalyzer.CSharp" Version="9.23.0.88079">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -4,7 +4,7 @@ This project was generated with [Angular CLI](https://github.com/angular/angular
## Development server ## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. Run `npm run start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
Your backend must be served on port 5000. Your backend must be served on port 5000.
## Code scaffolding ## Code scaffolding
@ -25,10 +25,11 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
Run `npx playwright test --reporter=line` or `npx playwright test` to run e2e tests. Run `npx playwright test --reporter=line` or `npx playwright test` to run e2e tests.
## Connecting to your dev server via your phone ## Connecting to your dev server via your phone or any other compatible client on local network
ng serve --host 0.0.0.0 Update `IP` constant in `src/environments/environment.ts` to your dev machine's ip instead of `localhost`.
and update environment.ts to your local ip.
Run `npm run start`
## Notes: ## Notes:
- injected services should be at the top of the file - injected services should be at the top of the file

View File

@ -14,6 +14,15 @@ function generateChecksum(str, algorithm, encoding) {
const result = {}; const result = {};
// Generate directory if it doesn't exist
const distFolderPath = './dist/';
const browserFolderPath = './dist/browser/';
if (!fs.existsSync(browserFolderPath)) {
console.log('Creating ./dist/browser folder');
fs.mkdirSync(distFolderPath, 0o744);
fs.mkdirSync(browserFolderPath, 0o744);
}
// Remove file if it exists // Remove file if it exists
const cacheBustingFilePath = './i18n-cache-busting.json'; const cacheBustingFilePath = './i18n-cache-busting.json';
if (fs.existsSync(cacheBustingFilePath)) { if (fs.existsSync(cacheBustingFilePath)) {

View File

@ -3,7 +3,7 @@
"version": "0.7.12.1", "version": "0.7.12.1",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "npm run cache-locale && ng serve", "start": "npm run cache-locale && ng serve --host 0.0.0.0",
"build": "npm run cache-locale && ng build", "build": "npm run cache-locale && ng build",
"minify-langs": "node minify-json.js", "minify-langs": "node minify-json.js",
"cache-locale": "node hash-localization.js", "cache-locale": "node hash-localization.js",

View File

@ -0,0 +1,6 @@
export enum PdfLayoutMode {
Multiple = 0,
Single = 1,
Book = 2,
InfiniteScroll = 3
}

View File

@ -0,0 +1,6 @@
export enum PdfScrollMode {
Vertical = 0,
Horizontal = 1,
Wrapped = 2,
Page = 3
}

View File

@ -0,0 +1,5 @@
export enum PdfSpreadMode {
None = 0,
Odd = 1,
Even = 2
}

View File

@ -0,0 +1,4 @@
export enum PdfTheme{
Dark = 0,
Light = 1
}

View File

@ -8,6 +8,10 @@ import { ReadingDirection } from './reading-direction';
import { ScalingOption } from './scaling-option'; import { ScalingOption } from './scaling-option';
import { SiteTheme } from './site-theme'; import { SiteTheme } from './site-theme';
import {WritingStyle} from "./writing-style"; import {WritingStyle} from "./writing-style";
import {PdfTheme} from "./pdf-theme";
import {PdfScrollMode} from "./pdf-scroll-mode";
import {PdfLayoutMode} from "./pdf-layout-mode";
import {PdfSpreadMode} from "./pdf-spread-mode";
export interface Preferences { export interface Preferences {
// Manga Reader // Manga Reader
@ -34,6 +38,12 @@ export interface Preferences {
bookReaderLayoutMode: BookPageLayoutMode; bookReaderLayoutMode: BookPageLayoutMode;
bookReaderImmersiveMode: boolean; bookReaderImmersiveMode: boolean;
// PDF Reader
pdfTheme: PdfTheme;
pdfScrollMode: PdfScrollMode;
pdfLayoutMode: PdfLayoutMode;
pdfSpreadMode: PdfSpreadMode;
// Global // Global
theme: SiteTheme; theme: SiteTheme;
globalPageLayoutMode: PageLayoutMode; globalPageLayoutMode: PageLayoutMode;
@ -50,6 +60,10 @@ export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horiz
export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}]; export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}];
export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}]; export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}];
export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}]; export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}];
export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // , {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover} export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // TODO: Build this, {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover}
export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}]; export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}];
export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}]; export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}];
export const pdfLayoutModes = [{text: 'pdf-multiple', value: PdfLayoutMode.Multiple}, {text: 'pdf-book', value: PdfLayoutMode.Book}];
export const pdfScrollModes = [{text: 'pdf-vertical', value: PdfScrollMode.Vertical}, {text: 'pdf-horizontal', value: PdfScrollMode.Horizontal}, {text: 'pdf-page', value: PdfScrollMode.Page}];
export const pdfSpreadModes = [{text: 'pdf-none', value: PdfSpreadMode.None}, {text: 'pdf-odd', value: PdfSpreadMode.Odd}, {text: 'pdf-even', value: PdfSpreadMode.Even}];
export const pdfThemes = [{text: 'pdf-light', value: PdfTheme.Light}, {text: 'pdf-dark', value: PdfTheme.Dark}];

View File

@ -52,6 +52,8 @@ export class AccountService {
*/ */
private refreshTokenTimeout: ReturnType<typeof setTimeout> | undefined; private refreshTokenTimeout: ReturnType<typeof setTimeout> | undefined;
private isOnline: boolean = true;
constructor(private httpClient: HttpClient, private router: Router, constructor(private httpClient: HttpClient, private router: Router,
private messageHub: MessageHubService, private themeService: ThemeService) { private messageHub: MessageHubService, private themeService: ThemeService) {
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate), messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
@ -59,6 +61,15 @@ export class AccountService {
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username), filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
switchMap(() => this.refreshAccount())) switchMap(() => this.refreshAccount()))
.subscribe(() => {}); .subscribe(() => {});
window.addEventListener("offline", (e) => {
this.isOnline = false;
});
window.addEventListener("online", (e) => {
this.isOnline = true;
this.refreshToken().subscribe();
});
} }
hasAdminRole(user: User) { hasAdminRole(user: User) {
@ -143,6 +154,7 @@ export class AccountService {
localStorage.setItem(this.userKey, JSON.stringify(user)); localStorage.setItem(this.userKey, JSON.stringify(user));
localStorage.setItem(AccountService.lastLoginKey, user.username); localStorage.setItem(AccountService.lastLoginKey, user.username);
if (user.preferences && user.preferences.theme) { if (user.preferences && user.preferences.theme) {
this.themeService.setTheme(user.preferences.theme.name); this.themeService.setTheme(user.preferences.theme.name);
} else { } else {
@ -329,7 +341,7 @@ export class AccountService {
private refreshToken() { private refreshToken() {
if (this.currentUser === null || this.currentUser === undefined) return of(); if (this.currentUser === null || this.currentUser === undefined || !this.isOnline) return of();
return this.httpClient.post<{token: string, refreshToken: string}>(this.baseUrl + 'account/refresh-token', return this.httpClient.post<{token: string, refreshToken: string}>(this.baseUrl + 'account/refresh-token',
{token: this.currentUser.token, refreshToken: this.currentUser.refreshToken}).pipe(map(user => { {token: this.currentUser.token, refreshToken: this.currentUser.refreshToken}).pipe(map(user => {
if (this.currentUser) { if (this.currentUser) {

View File

@ -2,18 +2,25 @@
<div> <div>
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title"> <h4 class="modal-title" id="modal-basic-title">
{{t('user-review', {username: review.username})}} @if(review.isExternal) {<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">} {{t('user-review', {username: review.username})}}
@if(review.isExternal) {
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
}
</h4> </h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button> <button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
</div> </div>
<div class="modal-body scrollable-modal"> <div class="modal-body scrollable-modal">
<p *ngIf="review.tagline" [innerHTML]="review.tagline | safeHtml"></p> @if (review.tagline) {
<p [innerHTML]="review.tagline | safeHtml"></p>
}
<p #container class="img-max-width" [innerHTML]="review.body | safeHtml"></p> <p #container class="img-max-width" [innerHTML]="review.body | safeHtml"></p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a *ngIf="review.externalUrl" class="btn btn-icon" [href]="review.externalUrl | safeHtml" target="_blank" rel="noopener noreferrer" [title]="review.externalUrl"> @if (review.siteUrl) {
{{t('go-to-review')}} <a class="btn btn-icon" [href]="review.siteUrl | safeHtml" target="_blank" rel="noopener noreferrer" [title]="review.siteUrl">
</a> {{t('go-to-review')}}
</a>
}
<button type="submit" class="btn btn-primary" (click)="close()">{{t('close')}}</button> <button type="submit" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
</div> </div>
</div> </div>

View File

@ -3,10 +3,12 @@
<div class="row g-0"> <div class="row g-0">
<div class="col-md-2 d-none d-md-block"> <div class="col-md-2 d-none d-md-block">
<i class="img-fluid rounded-start fa-solid fa-circle-user profile-image" aria-hidden="true"></i> <i class="img-fluid rounded-start fa-solid fa-circle-user profile-image" aria-hidden="true"></i>
<div *ngIf="isMyReview" class="my-review"> @if (isMyReview) {
<i class="fa-solid fa-star" aria-hidden="true" [title]="t('your-review')"></i> <div class="my-review">
<span class="visually-hidden">{{t('your-review')}}</span> <i class="fa-solid fa-star" aria-hidden="true" [title]="t('your-review')"></i>
</div> <span class="visually-hidden">{{t('your-review')}}</span>
</div>
}
</div> </div>
<div class="col-md-10"> <div class="col-md-10">
<div class="card-body"> <div class="card-body">
@ -21,17 +23,19 @@
<div class="card-footer bg-transparent text-muted"> <div class="card-footer bg-transparent text-muted">
<div> <div>
<ng-container *ngIf="isMyReview; else normalReview"> @if (isMyReview) {
<i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i> <i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i>
<img class="me-1" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt=""> <img class="me-1" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
{{review.username}} {{review.username}}
</ng-container> } @else {
<ng-template #normalReview>
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt=""> <img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
</ng-template> }
{{(isMyReview ? '' : review.username | defaultValue:'')}} {{(isMyReview ? '' : review.username | defaultValue:'')}}
</div> </div>
<span class="review-score" *ngIf="review.isExternal">{{t('rating-percentage', {r: review.score})}}</span> @if (review.isExternal){
<span class="review-score">{{t('rating-percentage', {r: review.score})}}</span>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -28,7 +28,7 @@ import {ScrobbleProvider} from "../../_services/scrobbling.service";
@Component({ @Component({
selector: 'app-review-card', selector: 'app-review-card',
standalone: true, standalone: true,
imports: [CommonModule, ReadMoreComponent, DefaultValuePipe, ImageComponent, NgOptimizedImage, ProviderImagePipe, TranslocoDirective], imports: [ReadMoreComponent, DefaultValuePipe, ImageComponent, NgOptimizedImage, ProviderImagePipe, TranslocoDirective],
templateUrl: './review-card.component.html', templateUrl: './review-card.component.html',
styleUrls: ['./review-card.component.scss'], styleUrls: ['./review-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush

View File

@ -9,6 +9,6 @@ export interface UserReview {
tagline?: string; tagline?: string;
isExternal: boolean; isExternal: boolean;
bodyJustText?: string; bodyJustText?: string;
externalUrl?: string; siteUrl?: string;
provider: ScrobbleProvider; provider: ScrobbleProvider;
} }

View File

@ -13,7 +13,7 @@
@for(rowForm of items.controls; track rowForm; let idx = $index) { @for(rowForm of items.controls; track rowForm; let idx = $index) {
<tr > <tr >
<td id="progress-event--{{idx}}"> <td id="progress-event--{{idx}}">
{{progressEvents[idx].userName}} {{progressEvents[idx].userName | sentenceCase}}
</td> </td>
<td> <td>
@if(editMode[idx]) { @if(editMode[idx]) {

View File

@ -7,6 +7,7 @@ import {FullProgress} from "../../_models/readers/full-progress";
import {ReaderService} from "../../_services/reader.service"; import {ReaderService} from "../../_services/reader.service";
import {TranslocoDirective} from "@ngneat/transloco"; import {TranslocoDirective} from "@ngneat/transloco";
import {FormArray, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; import {FormArray, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
@Component({ @Component({
selector: 'app-edit-chapter-progress', selector: 'app-edit-chapter-progress',
@ -18,7 +19,8 @@ import {FormArray, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from
TitleCasePipe, TitleCasePipe,
UtcToLocalTimePipe, UtcToLocalTimePipe,
TranslocoDirective, TranslocoDirective,
ReactiveFormsModule ReactiveFormsModule,
SentenceCasePipe
], ],
templateUrl: './edit-chapter-progress.component.html', templateUrl: './edit-chapter-progress.component.html',
styleUrl: './edit-chapter-progress.component.scss', styleUrl: './edit-chapter-progress.component.scss',

View File

@ -33,25 +33,54 @@
[backgroundColor]="backgroundColor" [backgroundColor]="backgroundColor"
[customToolbar]="multiToolbar" [customToolbar]="multiToolbar"
[language]="user.preferences.locale" [language]="user.preferences.locale"
[(scrollMode)]="scrollMode" [(scrollMode)]="scrollMode"
[pageViewMode]="pageLayoutMode"
[spread]="spreadMode"
(pageChange)="saveProgress()" (pageChange)="saveProgress()"
(pdfLoadingStarts)="updateLoading(true)" (pdfLoadingStarts)="updateLoading(true)"
(pdfLoaded)="updateLoading(false)" (pdfLoaded)="updateLoading(false)"
(progress)="updateLoadProgress($event)" (progress)="updateLoadProgress($event)"
(zoomChange)="calcScrollbarNeeded()"
> >
</ngx-extended-pdf-viewer> </ngx-extended-pdf-viewer>
@if (scrollMode === ScrollModeType.page) {
<div class="left" (click)="prevPage()"></div>
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}}" (click)="nextPage()"></div>
}
<ng-template #multiToolbar> <ng-template #multiToolbar>
<div style="min-height: 36px" id="toolbarViewer" [ngStyle]="{'background-color': backgroundColor, 'color': fontColor}"> <div style="min-height: 36px" id="toolbarViewer" [ngStyle]="{'background-color': backgroundColor, 'color': fontColor}">
<div id="toolbarViewerLeft"> <div id="toolbarViewerLeft">
<pdf-toggle-sidebar></pdf-toggle-sidebar> <pdf-toggle-sidebar></pdf-toggle-sidebar>
<pdf-find-button></pdf-find-button> <pdf-find-button></pdf-find-button>
<pdf-paging-area></pdf-paging-area> <pdf-paging-area></pdf-paging-area>
@if (utilityService.getActiveBreakpoint() > Breakpoint.Mobile) {
<button class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton" [ngbTooltip]="bookTitle">
<i class="toolbar-icon fa-solid fa-info" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
<span class="visually-hidden">{{bookTitle}}</span>
</button>
}
<button *ngIf="incognitoMode" [ngbTooltip]="t('toggle-incognito')" (click)="turnOffIncognito()" class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton">
<i class="toolbar-icon fa fa-glasses" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
<span class="visually-hidden">{{t('incognito-mode')}}</span>
</button>
<button class="btn-icon col-2 col-xs-1 mt-0 mb-0 pt-1 pb-0 toolbarButton" (click)="closeReader()" [ngbTooltip]="t('close-reader-alt')">
<i class="toolbar-icon fa fa-times-circle" aria-hidden="true" [ngStyle]="{color: fontColor}"></i>
<span class="visually-hidden">{{t('close-reader-alt')}}</span>
</button>
</div> </div>
<pdf-zoom-toolbar ></pdf-zoom-toolbar> @if (utilityService.getActiveBreakpoint() > Breakpoint.Tablet) {
<pdf-zoom-toolbar ></pdf-zoom-toolbar>
}
<div id="toolbarViewerRight"> <div id="toolbarViewerRight">
<pdf-hand-tool></pdf-hand-tool> <pdf-hand-tool></pdf-hand-tool>
@ -59,42 +88,66 @@
<pdf-presentation-mode></pdf-presentation-mode> <pdf-presentation-mode></pdf-presentation-mode>
<!-- This is not yet supported by the underlying library @if (utilityService.getActiveBreakpoint() > Breakpoint.Mobile) {
<button (click)="toggleBookPageMode()" class="btn btn-icon toolbarButton"> <button (click)="toggleBookPageMode()" class="btn-icon toolbarButton" [ngbTooltip]="pageLayoutMode | pdfLayoutMode" [disabled]="scrollMode === ScrollModeType.page">
<i class="toolbar-icon fa-solid {{this.bookMode !== 'book' ? 'fa-book' : 'fa-book-open'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i> <i class="toolbar-icon fa-solid {{this.pageLayoutMode !== 'book' ? 'fa-book' : 'fa-book-open'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
<span class="visually-hidden">{{this.bookMode !== 'book' ? 'Book Mode' : 'Normal Mode'}}</span> <span class="visually-hidden">{{this.pageLayoutMode | pdfLayoutMode}}</span>
</button> --> </button>
}
<button class="btn btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton" [ngbTooltip]="bookTitle">
<i class="toolbar-icon fa-solid fa-info" [ngStyle]="{color: fontColor}" aria-hidden="true"></i> <!-- scroll mode should be disabled when book mode is used -->
<span class="visually-hidden">{{bookTitle}}</span> <button (click)="toggleScrollMode()" class="btn-icon toolbarButton" [ngbTooltip]="scrollMode | pdfScrollMode" [disabled]="this.pageLayoutMode === 'book'">
@switch (scrollMode) {
@case (ScrollModeType.vertical) {
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM11 0v.5c0 1-.5 1.5-1.5 1.5h-3C5.5 2 5 1.5 5 .5V0h6zM11 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6z"></path></svg>
}
@case (ScrollModeType.page) {
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,7V9H12V17H14V7H10Z"></path></svg>
}
@case (ScrollModeType.horizontal) {
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px"> <path fill="currentColor" d="M0 4h1.5c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5H0zM9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM16 4h-1.5c-1 0-1.5.5-1.5 1.5v5c0 1 .5 1.5 1.5 1.5H16z"></path> </svg>
}
@case (ScrollModeType.wrapped) {
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M5.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C1 4.5 1.5 4 2.5 4zM7 0v.5C7 1.5 6.5 2 5.5 2h-3C1.5 2 1 1.5 1 .5V0h6zM7 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6zM13.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5c0-1 .5-1.5 1.5-1.5zM15 0v.5c0 1-.5 1.5-1.5 1.5h-3C9.5 2 9 1.5 9 .5V0h6zM15 16v-.507c0-1-.5-1.5-1.5-1.5h-3C9.5 14 9 14.5 9 15.5v.5h6z"></path></svg>
}
}
<span class="visually-hidden">{{scrollMode | pdfScrollMode}}</span>
</button> </button>
<button *ngIf="incognitoMode" (click)="turnOffIncognito()" class="btn btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton"> <button (click)="toggleSpreadMode()" class="btn-icon toolbarButton" [ngbTooltip]="spreadMode | pdfSpreadMode" [disabled]="this.pageLayoutMode === 'book'">
<i class="toolbar-icon fa fa-glasses" [ngStyle]="{color: fontColor}" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-mode')}}</span>
@switch (spreadMode) {
@case ('off') {
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M6 3c-1 0-1.5.5-1.5 1.5v7c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5v-7c0-1-.5-1.5-1.5-1.5z"></path></svg>
}
@case ('odd') {
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M10.56 3.5C9.56 3.5 9 4 9 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.93 1.2c.8 0 1.4.2 1.8.64.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.44-.2.3-.6.6-1 .93l-.6.4c-.4.3-.6.4-.7.55-.1.1-.2.2-.3.4h3.2v1.27h-5c0-.5.1-1 .3-1.43.2-.49.7-1 1.5-1.54.7-.5 1.1-.8 1.3-1.02.3-.3.4-.7.4-1.05 0-.3-.1-.6-.3-.77-.2-.2-.4-.3-.7-.3-.4 0-.7.2-.9.5-.1.2-.1.5-.2.9h-1.4c0-.6.2-1.1.3-1.5.4-.7 1.1-1.1 2-1.1zM1.54 3.5C.54 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.54 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.8 1.125H4.5V12H3V6.9H1.3v-1c.5 0 .8 0 .97-.03.33-.07.53-.17.73-.37.1-.2.2-.3.25-.5.05-.2.05-.3.05-.3z"></path></svg>
}
@case ('even') {
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px"><path fill="currentColor" d="M1.5 3.5C.5 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm2 1.2c.8 0 1.4.2 1.8.6.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.4-.2.3-.5.7-1 1l-.6.4c-.4.3-.6.4-.75.56-.15.14-.25.24-.35.44H6v1.3H1c0-.6.1-1.1.3-1.5.3-.6.7-1 1.5-1.6.7-.4 1.1-.8 1.28-1 .32-.3.42-.6.42-1 0-.3-.1-.6-.23-.8-.17-.2-.37-.3-.77-.3s-.7.1-.9.5c-.04.2-.1.5-.1.9H1.1c0-.6.1-1.1.3-1.5.4-.7 1.1-1.1 2.1-1.1zM10.54 3.54C9.5 3.54 9 4 9 5v6.5c0 1 .5 1.5 1.54 1.5h4c.96 0 1.46-.5 1.46-1.5V5c0-1-.5-1.46-1.5-1.46zm1.9.95c.7 0 1.3.2 1.7.5.4.4.6.8.6 1.4 0 .4-.1.8-.4 1.1-.2.2-.3.3-.5.4.1 0 .3.1.6.3.4.3.5.8.5 1.4 0 .6-.2 1.2-.6 1.6-.4.5-1.1.7-1.9.7-1 0-1.8-.3-2.2-1-.14-.29-.24-.69-.24-1.29h1.4c0 .3 0 .5.1.7.2.4.5.5 1 .5.3 0 .5-.1.7-.3.2-.2.3-.5.3-.8 0-.5-.2-.8-.6-.95-.2-.05-.5-.15-1-.15v-1c.5 0 .8-.1 1-.14.3-.1.5-.4.5-.9 0-.3-.1-.5-.2-.7-.2-.2-.4-.3-.7-.3-.3 0-.6.1-.75.3-.2.2-.2.5-.2.86h-1.34c0-.4.1-.7.19-1.1 0-.12.2-.32.4-.62.2-.2.4-.3.7-.4.3-.1.6-.1 1-.1z"></path></svg>
}
}
<span class="visually-hidden">{{spreadMode | pdfSpreadMode}}</span>
</button> </button>
<!-- This is pretty experimental, so it might not work perfectly --> <!-- This is pretty experimental, so it might not work perfectly -->
<button (click)="toggleTheme()" class="btn btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton"> <button (click)="toggleTheme()" class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton toolbar-btn-fix">
<i class="toolbar-icon fa-solid {{this.theme === 'light' ? 'fa-sun' : 'fa-moon'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i> <i class="toolbar-icon fa-solid {{this.theme === 'light' ? 'fa-sun' : 'fa-moon'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
<span class="visually-hidden">{{this.theme === 'light' ? t('light-theme-alt') : t('dark-theme-alt')}}</span> <span class="visually-hidden">{{this.theme === 'light' ? t('light-theme-alt') : t('dark-theme-alt')}}</span>
</button> </button>
<button class="btn btn-icon col-2 col-xs-1 mt-0 mb-0 pt-1 pb-0 toolbarButton" (click)="closeReader()">
<i class="toolbar-icon fa fa-times-circle" aria-hidden="true" [ngStyle]="{color: fontColor}"></i>
<span class="visually-hidden">{{t('close-reader-alt')}}</span>
</button>
<div class="verticalToolbarSeparator hiddenSmallView"></div> <div class="verticalToolbarSeparator hiddenSmallView"></div>
<pdf-toggle-secondary-toolbar></pdf-toggle-secondary-toolbar> <pdf-toggle-secondary-toolbar></pdf-toggle-secondary-toolbar>
<pdf-single-page-mode [show]="true" [scrollMode]="scrollMode"></pdf-single-page-mode>
<pdf-vertical-scroll-mode [show]="true" [scrollMode]="scrollMode"></pdf-vertical-scroll-mode>
<pdf-horizontal-scroll [show]="true" [scrollMode]="scrollMode"></pdf-horizontal-scroll>
<pdf-wrapped-scroll-mode [show]="true" [scrollMode]="scrollMode"></pdf-wrapped-scroll-mode>
<pdf-no-spread [show]=true [scrollMode]="scrollMode"></pdf-no-spread>
<pdf-odd-spread [show]=true [scrollMode]="scrollMode"></pdf-odd-spread>
<pdf-even-spread [show]="true" [scrollMode]="scrollMode"></pdf-even-spread>
</div> </div>
</div> </div>

View File

@ -2,6 +2,9 @@
font-size: 19px; font-size: 19px;
} }
.btn-icon {
border: none;
}
.book-title { .book-title {
margin: 8px 0 4px !important; margin: 8px 0 4px !important;
@ -24,3 +27,76 @@
// NOTE: We have to override due to theme variables not being available // NOTE: We have to override due to theme variables not being available
background-color: #3B9E76; background-color: #3B9E76;
} }
$pagination-color: transparent;
$pagination-opacity: 0;
//$pagination-color: red;
//$pagination-opacity: 0.7;
$action-bar-height: 36px;
// Tap to Paginate
.right {
position: absolute;
right: 0px;
top: $action-bar-height;
width: 20vw;
z-index: 3;
background: $pagination-color;
border-color: transparent;
border: none !important;
opacity: $pagination-opacity;
outline: none;
height: 100%;
&.immersive {
top: 0px;
}
&.no-pointer-events {
pointer-events: none;
}
}
// This class pushes the click area to the left a bit to let users click the scrollbar
.right-with-scrollbar {
position: absolute;
right: 17px;
top: $action-bar-height;
width: 18vw;
z-index: 3;
background: $pagination-color;
opacity: $pagination-opacity;
border-color: transparent;
border: none !important;
outline: none;
height: 100%;
cursor: pointer;
&.immersive {
top: 0px;
}
}
.left {
position: absolute;
left: 0px;
top: $action-bar-height;
width: 20vw;
background: $pagination-color;
opacity: $pagination-opacity;
border-color: transparent;
border: none !important;
z-index: 3;
outline: none;
height: 100%;
cursor: pointer;
&.immersive {
top: 0px;
}
}

View File

@ -1,27 +1,43 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, ElementRef, Component,
HostListener, ElementRef,
inject, OnDestroy, HostListener, inject, Inject,
OnInit, ViewChild OnDestroy,
OnInit,
ViewChild
} from '@angular/core'; } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import { NgxExtendedPdfViewerService, PageViewModeType, ScrollModeType, ProgressBarEvent, NgxExtendedPdfViewerModule } from 'ngx-extended-pdf-viewer'; import {
import { ToastrService } from 'ngx-toastr'; NgxExtendedPdfViewerModule,
import { take } from 'rxjs'; NgxExtendedPdfViewerService,
import { BookService } from 'src/app/book-reader/_services/book.service'; PageViewModeType,
import { KEY_CODES } from 'src/app/shared/_services/utility.service'; ProgressBarEvent,
import { Chapter } from 'src/app/_models/chapter'; ScrollModeType
import { User } from 'src/app/_models/user'; } from 'ngx-extended-pdf-viewer';
import { AccountService } from 'src/app/_services/account.service'; import {ToastrService} from 'ngx-toastr';
import { NavService } from 'src/app/_services/nav.service'; import {take} from 'rxjs';
import { CHAPTER_ID_DOESNT_EXIST, ReaderService } from 'src/app/_services/reader.service'; import {BookService} from 'src/app/book-reader/_services/book.service';
import { SeriesService } from 'src/app/_services/series.service'; import {Breakpoint, KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
import { ThemeService } from 'src/app/_services/theme.service'; import {Chapter} from 'src/app/_models/chapter';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import {User} from 'src/app/_models/user';
import { NgIf, NgStyle, AsyncPipe } from '@angular/common'; import {AccountService} from 'src/app/_services/account.service';
import {NavService} from 'src/app/_services/nav.service';
import {CHAPTER_ID_DOESNT_EXIST, ReaderService} from 'src/app/_services/reader.service';
import {SeriesService} from 'src/app/_services/series.service';
import {ThemeService} from 'src/app/_services/theme.service';
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {AsyncPipe, DOCUMENT, NgIf, NgStyle} from '@angular/common';
import {translate, TranslocoDirective} from "@ngneat/transloco"; import {translate, TranslocoDirective} from "@ngneat/transloco";
import {PdfLayoutMode} from "../../../_models/preferences/pdf-layout-mode";
import {PdfScrollMode} from "../../../_models/preferences/pdf-scroll-mode";
import {PdfTheme} from "../../../_models/preferences/pdf-theme";
import {PdfSpreadMode} from "../../../_models/preferences/pdf-spread-mode";
import {SpreadType} from "ngx-extended-pdf-viewer/lib/options/spread-type";
import {PdfLayoutModePipe} from "../../_pipe/pdf-layout-mode.pipe";
import {PdfScrollModePipe} from "../../_pipe/pdf-scroll-mode.pipe";
import {PdfSpreadModePipe} from "../../_pipe/pdf-spread-mode.pipe";
@Component({ @Component({
selector: 'app-pdf-reader', selector: 'app-pdf-reader',
@ -29,10 +45,26 @@ import {translate, TranslocoDirective} from "@ngneat/transloco";
styleUrls: ['./pdf-reader.component.scss'], styleUrls: ['./pdf-reader.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [NgIf, NgStyle, NgxExtendedPdfViewerModule, NgbTooltip, AsyncPipe, TranslocoDirective] imports: [NgIf, NgStyle, NgxExtendedPdfViewerModule, NgbTooltip, AsyncPipe, TranslocoDirective,
PdfLayoutModePipe, PdfScrollModePipe, PdfSpreadModePipe]
}) })
export class PdfReaderComponent implements OnInit, OnDestroy { export class PdfReaderComponent implements OnInit, OnDestroy {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly seriesService = inject(SeriesService);
private readonly navService = inject(NavService);
private readonly toastr = inject(ToastrService);
private readonly bookService = inject(BookService);
private readonly themeService = inject(ThemeService);
private readonly cdRef = inject(ChangeDetectorRef);
public readonly accountService = inject(AccountService);
public readonly readerService = inject(ReaderService);
public readonly utilityService = inject(UtilityService);
protected readonly ScrollModeType = ScrollModeType;
protected readonly Breakpoint = Breakpoint;
@ViewChild('container') container!: ElementRef; @ViewChild('container') container!: ElementRef;
libraryId!: number; libraryId!: number;
@ -82,20 +114,13 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
* How much of the current document is loaded * How much of the current document is loaded
*/ */
loadPercent: number = 0; loadPercent: number = 0;
scrollbarNeeded = false;
/** pageLayoutMode: PageViewModeType = 'multiple';
* This can't be updated dynamically:
* https://github.com/stephanrauh/ngx-extended-pdf-viewer/issues/1415
*/
bookMode: PageViewModeType = 'multiple';
scrollMode: ScrollModeType = ScrollModeType.vertical; scrollMode: ScrollModeType = ScrollModeType.vertical;
spreadMode: SpreadType = 'off';
constructor(private route: ActivatedRoute, private router: Router, public accountService: AccountService, constructor(@Inject(DOCUMENT) private document: Document) {
private seriesService: SeriesService, public readerService: ReaderService,
private navService: NavService, private toastr: ToastrService,
private bookService: BookService, private themeService: ThemeService,
private readonly cdRef: ChangeDetectorRef, private pdfViewerService: NgxExtendedPdfViewerService) {
this.navService.hideNavBar(); this.navService.hideNavBar();
this.themeService.clearThemes(); this.themeService.clearThemes();
this.navService.hideSideNav(); this.navService.hideSideNav();
@ -108,6 +133,13 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
} }
} }
@HostListener('window:resize', ['$event'])
@HostListener('window:orientationchange', ['$event'])
onResize(){
// Update the window Height
this.calcScrollbarNeeded();
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.themeService.currentTheme$.pipe(take(1)).subscribe(theme => { this.themeService.currentTheme$.pipe(take(1)).subscribe(theme => {
this.themeService.setTheme(theme.name); this.themeService.setTheme(theme.name);
@ -150,7 +182,71 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
}); });
} }
calcScrollbarNeeded() {
const viewContainer = this.document.querySelector('#viewerContainer');
if (viewContainer == null) return;
this.scrollbarNeeded = viewContainer.scrollHeight > this.container?.nativeElement?.clientHeight;
this.cdRef.markForCheck();
}
convertPdfLayoutMode(mode: PdfLayoutMode) {
switch (mode) {
case PdfLayoutMode.Multiple:
return 'multiple';
case PdfLayoutMode.Single:
return 'single';
case PdfLayoutMode.Book:
return 'book';
case PdfLayoutMode.InfiniteScroll:
return 'infinite-scroll';
}
}
convertPdfScrollMode(mode: PdfScrollMode) {
switch (mode) {
case PdfScrollMode.Vertical:
return ScrollModeType.vertical;
case PdfScrollMode.Horizontal:
return ScrollModeType.horizontal;
case PdfScrollMode.Wrapped:
return ScrollModeType.wrapped;
case PdfScrollMode.Page:
return ScrollModeType.page;
}
}
convertPdfSpreadMode(mode: PdfSpreadMode): SpreadType {
switch (mode) {
case PdfSpreadMode.None:
return 'off' as SpreadType;
case PdfSpreadMode.Odd:
return 'odd' as SpreadType;
case PdfSpreadMode.Even:
return 'even' as SpreadType;
}
}
convertPdfTheme(theme: PdfTheme) {
switch (theme) {
case PdfTheme.Dark:
return 'dark';
case PdfTheme.Light:
return 'light';
}
}
init() { init() {
this.pageLayoutMode = this.convertPdfLayoutMode(this.user.preferences.pdfLayoutMode || PdfLayoutMode.Multiple);
this.scrollMode = this.convertPdfScrollMode(this.user.preferences.pdfScrollMode || PdfScrollMode.Vertical);
this.spreadMode = this.convertPdfSpreadMode(this.user.preferences.pdfSpreadMode || PdfSpreadMode.None);
this.theme = this.convertPdfTheme(this.user.preferences.pdfTheme || PdfTheme.Dark);
this.backgroundColor = this.themeMap[this.theme].background;
this.fontColor = this.themeMap[this.theme].font; // TODO: Move this to an observable or something
this.calcScrollbarNeeded();
this.bookService.getBookInfo(this.chapterId).subscribe(info => { this.bookService.getBookInfo(this.chapterId).subscribe(info => {
this.volumeId = info.volumeId; this.volumeId = info.volumeId;
this.bookTitle = info.bookTitle; this.bookTitle = info.bookTitle;
@ -171,7 +267,7 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
} }
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.readerService.enableWakeLock(this.container.nativeElement); setTimeout(() => this.readerService.enableWakeLock(this.container.nativeElement), 1000); // TODO: This needs to be in afterviewinit i think
} }
/** /**
@ -197,11 +293,33 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
toggleScrollMode() {
const options: Array<ScrollModeType> = [ScrollModeType.vertical, ScrollModeType.horizontal, ScrollModeType.page];
let index = options.indexOf(this.scrollMode) + 1;
if (index >= options.length) index = 0;
this.scrollMode = options[index];
this.calcScrollbarNeeded();
this.cdRef.markForCheck();
}
toggleSpreadMode() {
const options: Array<SpreadType> = ['off', 'odd', 'even'];
let index = options.indexOf(this.spreadMode) + 1;
if (index >= options.length) index = 0;
this.spreadMode = options[index];
this.cdRef.markForCheck();
}
toggleBookPageMode() { toggleBookPageMode() {
if (this.bookMode === 'book') { if (this.pageLayoutMode === 'book') {
this.bookMode = 'multiple'; this.pageLayoutMode = 'multiple';
} else { } else {
this.bookMode = 'book'; this.pageLayoutMode = 'book';
// If the fit is automatic, let's adjust to 100% to ensure it renders correctly (can't do this, but it doesn't always happen)
} }
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
@ -225,4 +343,16 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
prevPage() {
this.currentPage--;
if (this.currentPage < 0) this.currentPage = 0;
this.cdRef.markForCheck();
}
nextPage() {
this.currentPage++;
if (this.currentPage > this.maxPages) this.currentPage = this.maxPages;
this.cdRef.markForCheck();
}
} }

View File

@ -0,0 +1,26 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import {PageViewModeType} from "ngx-extended-pdf-viewer";
import {TranslocoService} from "@ngneat/transloco";
@Pipe({
name: 'pdfLayoutMode',
standalone: true
})
export class PdfLayoutModePipe implements PipeTransform {
translocoService = inject(TranslocoService);
transform(value: PageViewModeType): string {
switch (value) {
case "single":
return this.translocoService.translate('pdf-layout-mode-pipe.single');
case "book":
return this.translocoService.translate('pdf-layout-mode-pipe.book');
case "multiple":
return this.translocoService.translate('pdf-layout-mode-pipe.multiple');
case "infinite-scroll":
return this.translocoService.translate('pdf-layout-mode-pipe.infinite-scroll');
}
}
}

View File

@ -0,0 +1,24 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import {TranslocoService} from "@ngneat/transloco";
import {ScrollModeType} from "ngx-extended-pdf-viewer";
@Pipe({
name: 'pdfScrollMode',
standalone: true
})
export class PdfScrollModePipe implements PipeTransform {
translocoService = inject(TranslocoService);
transform(value: ScrollModeType): string {
switch (value) {
case ScrollModeType.vertical:
return this.translocoService.translate('pdf-scroll-mode-pipe.vertical');
case ScrollModeType.horizontal:
return this.translocoService.translate('pdf-scroll-mode-pipe.horizontal');
case ScrollModeType.wrapped:
return this.translocoService.translate('pdf-scroll-mode-pipe.wrapped');
case ScrollModeType.page:
return this.translocoService.translate('pdf-scroll-mode-pipe.page');
}
}
}

View File

@ -0,0 +1,24 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import {TranslocoService} from "@ngneat/transloco";
import {SpreadType} from "ngx-extended-pdf-viewer/lib/options/spread-type";
@Pipe({
name: 'pdfSpreadMode',
standalone: true
})
export class PdfSpreadModePipe implements PipeTransform {
translocoService = inject(TranslocoService);
transform(value: SpreadType): string {
switch (value) {
case 'off' as SpreadType:
return this.translocoService.translate('pdf-spread-mode-pipe.off');
case "even":
return this.translocoService.translate('pdf-spread-mode-pipe.even');
case "odd":
return this.translocoService.translate('pdf-spread-mode-pipe.odd');
}
return this.translocoService.translate('pdf-spread-mode-pipe.off');
}
}

View File

@ -16,6 +16,7 @@ import { SiteThemeProviderPipe } from '../../_pipes/site-theme-provider.pipe';
import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe'; import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe';
import { NgIf, NgFor, AsyncPipe } from '@angular/common'; import { NgIf, NgFor, AsyncPipe } from '@angular/common';
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco"; import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import {tap} from "rxjs/operators";
@Component({ @Component({
selector: 'app-theme-manager', selector: 'app-theme-manager',
@ -52,19 +53,11 @@ export class ThemeManagerComponent {
} }
applyTheme(theme: SiteTheme) { applyTheme(theme: SiteTheme) {
if (!this.user) return;
if (this.user) { const pref = Object.assign({}, this.user.preferences);
const pref = Object.assign({}, this.user.preferences); pref.theme = theme;
pref.theme = theme; this.accountService.updatePreferences(pref).subscribe();
this.accountService.updatePreferences(pref).subscribe(updatedPref => {
if (this.user) {
this.user.preferences = updatedPref;
}
this.themeService.setTheme(theme.name);
this.cdRef.markForCheck();
});
}
} }
updateDefault(theme: SiteTheme) { updateDefault(theme: SiteTheme) {

View File

@ -1,152 +1,36 @@
<ng-container *transloco="let t; read:'user-preferences'"> <ng-container *transloco="let t; read:'user-preferences'">
<app-side-nav-companion-bar> <app-side-nav-companion-bar>
<h2 title> <h2 title>
{{t('title')}} {{t('title')}}
</h2> </h2>
</app-side-nav-companion-bar> </app-side-nav-companion-bar>
<div class="container-fluid"> <div class="container-fluid">
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs"> <ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
<li *ngFor="let tab of tabs" [ngbNavItem]="tab"> <li *ngFor="let tab of tabs" [ngbNavItem]="tab">
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ t(tab.title) }}</a> <a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ t(tab.title) }}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when tab.fragment === FragmentID.Account; prefetch on idle) { @defer (when tab.fragment === FragmentID.Account; prefetch on idle) {
<app-change-email></app-change-email> <app-change-email></app-change-email>
<app-change-password></app-change-password> <app-change-password></app-change-password>
<app-change-age-restriction></app-change-age-restriction> <app-change-age-restriction></app-change-age-restriction>
<app-manage-scrobbling-providers></app-manage-scrobbling-providers> <app-manage-scrobbling-providers></app-manage-scrobbling-providers>
} }
@defer (when tab.fragment === FragmentID.Preferences; prefetch on idle) { @defer (when tab.fragment === FragmentID.Preferences; prefetch on idle) {
<ng-container *ngIf="tab.fragment === FragmentID.Preferences"> <ng-container *ngIf="tab.fragment === FragmentID.Preferences">
<p> <p>
{{t('pref-description')}} {{t('pref-description')}}
</p> </p>
<form [formGroup]="settingsForm" *ngIf="user !== undefined"> <form [formGroup]="settingsForm" *ngIf="user !== undefined">
<div ngbAccordion [closeOthers]="true" #acc="ngbAccordion"> <div ngbAccordion [closeOthers]="true" #acc="ngbAccordion">
<div ngbAccordionItem [id]="AccordionPanelID.GlobalSettings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)" aria-controls="collapseOne">
{{t('global-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-global-layoutmode" class="form-label">{{t('page-layout-mode-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #layoutModeTooltip>{{t('page-layout-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-layoutmode-help">
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="globalPageLayoutMode" id="settings-global-layoutmode">
<option *ngFor="let opt of pageLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div ngbAccordionItem [id]="AccordionPanelID.GlobalSettings" [collapsed]="false">
<label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label> <h2 class="accordion-header" ngbAccordionHeader>
<i class="fa fa-info-circle ms-1" <button class="accordion-button" ngbAccordionButton type="button"
aria-hidden="true" placement="right" [ngbTooltip]="localeTooltip" role="button" tabindex="0"></i> [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)"
<ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template> aria-controls="collapseOne">
<span class="visually-hidden" id="settings-global-locale-help"> {{t('global-settings-title')}}
<ng-container [ngTemplateOutlet]="localeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="locale" id="settings-global-locale">
<option *ngFor="let opt of locales" [value]="opt.isoCode">{{opt.title | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="blur-unread-summaries" role="switch" formControlName="blurUnreadSummaries" class="form-check-input" aria-describedby="settings-global-blurUnreadSummaries-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="blur-unread-summaries">{{t('blur-unread-summaries-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="blurUnreadSummariesTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #blurUnreadSummariesTooltip>{{t('blur-unread-summaries-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-blurUnreadSummaries-help">
<ng-container [ngTemplateOutlet]="blurUnreadSummariesTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="prompt-download" role="switch" formControlName="promptForDownloadSize" class="form-check-input" aria-describedby="settings-global-promptForDownloadSize-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="prompt-download">{{t('prompt-on-download-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="promptForDownloadSizeTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #promptForDownloadSizeTooltip>{{t('prompt-on-download-tooltip', {size: '100'})}}</ng-template>
<span class="visually-hidden" id="settings-global-promptForDownloadSize-help">
<ng-container [ngTemplateOutlet]="promptForDownloadSizeTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="no-transitions" role="switch" formControlName="noTransitions" class="form-check-input"
aria-describedby="settings-global-noTransitions-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="no-transitions">{{t('disable-animations-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="noTransitionsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #noTransitionsTooltip>{{t('disable-animations-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-noTransitions-help">
<ng-container [ngTemplateOutlet]="noTransitionsTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="collapse-relationships" role="switch" formControlName="collapseSeriesRelationships"
aria-describedby="settings-collapse-relationships-help" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="collapse-relationships">{{t('collapse-series-relationships-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="collapseSeriesRelationshipsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #collapseSeriesRelationshipsTooltip>{{t('collapse-series-relationships-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-collapse-relationships-help">
<ng-container [ngTemplateOutlet]="collapseSeriesRelationshipsTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="share-reviews" role="switch" formControlName="shareReviews"
aria-describedby="settings-share-reviews-help" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="share-reviews">{{t('share-series-reviews-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="shareReviewsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #shareReviewsTooltip>{{t('share-series-reviews-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-share-reviews-help">
<ng-container [ngTemplateOutlet]="shareReviewsTooltip"></ng-container>
</span>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem [id]="AccordionPanelID.ImageReader">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
{{t('image-reader-settings-title')}}
</button> </button>
</h2> </h2>
<div ngbAccordionCollapse> <div ngbAccordionCollapse>
@ -154,67 +38,241 @@
<ng-template> <ng-template>
<div class="row g-0"> <div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i> <label for="settings-global-layoutmode"
<ng-template #readingDirectionTooltip>{{t('reading-direction-tooltip')}}</ng-template> class="form-label">{{t('page-layout-mode-label')}}</label>
<span class="visually-hidden" id="settings-reading-direction-help"> <i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
<ng-container [ngTemplateOutlet]="readingDirectionTooltip"></ng-container> [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
</span> <ng-template #layoutModeTooltip>{{t('page-layout-mode-tooltip')}}</ng-template>
<select class="form-select" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction"> <span class="visually-hidden" id="settings-global-layoutmode-help">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option> <ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</select> </span>
</div> <select class="form-select" aria-describedby="manga-header"
formControlName="globalPageLayoutMode" id="settings-global-layoutmode">
<option *ngFor="let opt of pageLayoutModesTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-scaling-option" class="form-label">{{t('scaling-option-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="scalingOptionTooltip" role="button" tabindex="0"></i> <label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label>
<ng-template #scalingOptionTooltip>{{t('scaling-option-tooltip')}}</ng-template> <i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
<span class="visually-hidden" id="settings-scaling-option-help"> [ngbTooltip]="localeTooltip" role="button" tabindex="0"></i>
<ng-container [ngTemplateOutlet]="scalingOptionTooltip"></ng-container> <ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template>
</span> <span class="visually-hidden" id="settings-global-locale-help">
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option"> <ng-container [ngTemplateOutlet]="localeTooltip"></ng-container>
<option *ngFor="let opt of scalingOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option> </span>
</select> <select class="form-select" aria-describedby="manga-header" formControlName="locale"
</div> id="settings-global-locale">
</div> <option *ngFor="let opt of locales" [value]="opt.isoCode">{{opt.title | titlecase}}
</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="blur-unread-summaries" role="switch"
formControlName="blurUnreadSummaries" class="form-check-input"
aria-describedby="settings-global-blurUnreadSummaries-help" [value]="true"
aria-labelledby="auto-close-label">
<label class="form-check-label"
for="blur-unread-summaries">{{t('blur-unread-summaries-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="blurUnreadSummariesTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #blurUnreadSummariesTooltip>{{t('blur-unread-summaries-tooltip')}}
</ng-template>
<span class="visually-hidden" id="settings-global-blurUnreadSummaries-help">
<ng-container [ngTemplateOutlet]="blurUnreadSummariesTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0"> <div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-pagesplit-option" class="form-label">{{t('page-splitting-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i> <div class="form-check form-switch">
<input type="checkbox" id="prompt-download" role="switch"
formControlName="promptForDownloadSize" class="form-check-input"
aria-describedby="settings-global-promptForDownloadSize-help" [value]="true"
aria-labelledby="auto-close-label">
<label class="form-check-label"
for="prompt-download">{{t('prompt-on-download-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="promptForDownloadSizeTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #promptForDownloadSizeTooltip>
{{t('prompt-on-download-tooltip', {size: '100'})}}</ng-template>
<span class="visually-hidden" id="settings-global-promptForDownloadSize-help">
<ng-container [ngTemplateOutlet]="promptForDownloadSizeTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="no-transitions" role="switch" formControlName="noTransitions"
class="form-check-input" aria-describedby="settings-global-noTransitions-help"
[value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label"
for="no-transitions">{{t('disable-animations-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="noTransitionsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #noTransitionsTooltip>{{t('disable-animations-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-noTransitions-help">
<ng-container [ngTemplateOutlet]="noTransitionsTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="collapse-relationships" role="switch"
formControlName="collapseSeriesRelationships"
aria-describedby="settings-collapse-relationships-help" class="form-check-input"
aria-labelledby="auto-close-label">
<label class="form-check-label"
for="collapse-relationships">{{t('collapse-series-relationships-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="collapseSeriesRelationshipsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #collapseSeriesRelationshipsTooltip>
{{t('collapse-series-relationships-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-collapse-relationships-help">
<ng-container [ngTemplateOutlet]="collapseSeriesRelationshipsTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="share-reviews" role="switch" formControlName="shareReviews"
aria-describedby="settings-share-reviews-help" class="form-check-input"
aria-labelledby="auto-close-label">
<label class="form-check-label"
for="share-reviews">{{t('share-series-reviews-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="shareReviewsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #shareReviewsTooltip>{{t('share-series-reviews-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-share-reviews-help">
<ng-container [ngTemplateOutlet]="shareReviewsTooltip"></ng-container>
</span>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()"
aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()"
aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem [id]="AccordionPanelID.ImageReader">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button"
[attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
{{t('image-reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-reading-direction"
class="form-label">{{t('reading-direction-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #readingDirectionTooltip>{{t('reading-direction-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-reading-direction-help">
<ng-container [ngTemplateOutlet]="readingDirectionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header"
formControlName="readingDirection" id="settings-reading-direction">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-scaling-option"
class="form-label">{{t('scaling-option-label')}}</label><i class="fa fa-info-circle ms-1"
aria-hidden="true" placement="right" [ngbTooltip]="scalingOptionTooltip" role="button"
tabindex="0"></i>
<ng-template #scalingOptionTooltip>{{t('scaling-option-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-scaling-option-help">
<ng-container [ngTemplateOutlet]="scalingOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption"
id="settings-scaling-option">
<option *ngFor="let opt of scalingOptionsTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-pagesplit-option"
class="form-label">{{t('page-splitting-label')}}</label><i class="fa fa-info-circle ms-1"
aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button"
tabindex="0"></i>
<ng-template #pageSplitOptionTooltip>{{t('page-splitting-tooltip')}}</ng-template> <ng-template #pageSplitOptionTooltip>{{t('page-splitting-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-pagesplit-option-help"> <span class="visually-hidden" id="settings-pagesplit-option-help">
<ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container> <ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container>
</span> </span>
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option"> <select class="form-select" aria-describedby="manga-header"
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option> formControlName="pageSplitOption" id="settings-pagesplit-option">
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select> </select>
</div> </div>
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-readingmode-option" class="form-label">{{t('reading-mode-label')}}</label> <label for="settings-readingmode-option"
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option"> class="form-label">{{t('reading-mode-label')}}</label>
<option *ngFor="let opt of readingModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option> <select class="form-select" aria-describedby="manga-header" formControlName="readerMode"
id="settings-readingmode-option">
<option *ngFor="let opt of readingModesTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select> </select>
</div> </div>
</div> </div>
<div class="row g-0"> <div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="true"> <div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="true">
<label for="settings-layoutmode-option" class="form-label">{{t('layout-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i> <label for="settings-layoutmode-option"
class="form-label">{{t('layout-mode-label')}}</label><i class="fa fa-info-circle ms-1"
aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button"
tabindex="0"></i>
<ng-template #layoutModeTooltip>{{t('layout-mode-tooltip')}}</ng-template> <ng-template #layoutModeTooltip>{{t('layout-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-layoutmode-option-help"> <span class="visually-hidden" id="settings-layoutmode-option-help">
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container> <ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span> </span>
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option"> <select class="form-select" aria-describedby="manga-header" formControlName="layoutMode"
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option> id="settings-layoutmode-option">
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select> </select>
</div> </div>
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-background-color-option" class="form-label">{{t('background-color-label')}}</label> <label for="settings-background-color-option"
<input [value]="user!.preferences!.backgroundColor" class="form-label">{{t('background-color-label')}}</label>
class="form-control" <input [value]="user!.preferences!.backgroundColor" class="form-control"
id="settings-background-color-option" id="settings-background-color-option" (colorPickerChange)="handleBackgroundColorChange()"
(colorPickerChange)="handleBackgroundColorChange()" [style.background]="user!.preferences!.backgroundColor" [cpAlphaChannel]="'disabled'"
[style.background]="user!.preferences!.backgroundColor" [(colorPicker)]="user!.preferences!.backgroundColor" />
[cpAlphaChannel]="'disabled'"
[(colorPicker)]="user!.preferences!.backgroundColor"/>
</div> </div>
</div> </div>
@ -222,7 +280,8 @@
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1"> <div class="mb-3 mt-1">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label"> <input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu"
class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="auto-close">{{t('auto-close-menu-label')}}</label> <label class="form-check-label" for="auto-close">{{t('auto-close-menu-label')}}</label>
</div> </div>
</div> </div>
@ -230,8 +289,11 @@
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1"> <div class="mb-3 mt-1">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label"> <input type="checkbox" id="show-screen-hints" role="switch"
<label class="form-check-label" for="show-screen-hints">{{t('show-screen-hints-label')}}</label> formControlName="showScreenHints" class="form-check-input" [value]="true"
aria-labelledby="auto-close-label">
<label class="form-check-label"
for="show-screen-hints">{{t('show-screen-hints-label')}}</label>
</div> </div>
</div> </div>
</div> </div>
@ -241,24 +303,36 @@
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1"> <div class="mb-3 mt-1">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook" class="form-check-input" [value]="true"> <input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook"
<label class="form-check-label me-1" for="emulate-book">{{t('emulate-comic-book-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button" tabindex="0"></i> class="form-check-input" [value]="true">
<label class="form-check-label me-1"
for="emulate-book">{{t('emulate-comic-book-label')}}</label><i
class="fa fa-info-circle" aria-hidden="true" placement="top"
ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button"
tabindex="0"></i>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1"> <div class="mb-3 mt-1">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" id="swipe-to-paginate" role="switch" formControlName="swipeToPaginate" class="form-check-input" [value]="true"> <input type="checkbox" id="swipe-to-paginate" role="switch"
<label class="form-check-label me-1" for="swipe-to-paginate">{{t('swipe-to-paginate-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered" role="button" tabindex="0"></i> formControlName="swipeToPaginate" class="form-check-input" [value]="true">
<label class="form-check-label me-1"
for="swipe-to-paginate">{{t('swipe-to-paginate-label')}}</label><i
class="fa fa-info-circle" aria-hidden="true" placement="top"
ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered"
role="button" tabindex="0"></i>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3"> <div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button> <button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()"
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button> aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()"
aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div> </div>
</ng-template> </ng-template>
</div> </div>
@ -267,20 +341,26 @@
<div ngbAccordionItem [id]="AccordionPanelID.BookReader"> <div ngbAccordionItem [id]="AccordionPanelID.BookReader">
<h2 class="accordion-header" ngbAccordionHeader> <h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne"> <button class="accordion-button" ngbAccordionButton type="button"
{{t('book-reader-settings-title')}} [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
</button> {{t('book-reader-settings-title')}}
</h2> </button>
<div ngbAccordionCollapse> </h2>
<div ngbAccordionBody> <div ngbAccordionCollapse>
<ng-template> <div ngbAccordionBody>
<div class="row g-0"> <ng-template>
<div class="col-md-4 col-sm-12 pe-2 mb-3"> <div class="row g-0">
<label id="taptopaginate-label" class="form-label"></label> <div class="col-md-4 col-sm-12 pe-2 mb-3">
<div class="mb-3"> <label id="taptopaginate-label" class="form-label"></label>
<div class="form-check form-switch"> <div class="mb-3">
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label"> <div class="form-check form-switch">
<label for="taptopaginate" class="form-check-label">{{t('tap-to-paginate-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i> <input type="checkbox" role="switch" id="taptopaginate"
formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true"
aria-labelledby="taptopaginate-label">
<label for="taptopaginate"
class="form-check-label">{{t('tap-to-paginate-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
<ng-template #tapToPaginateOptionTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template> <ng-template #tapToPaginateOptionTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-taptopaginate-option-help"> <span class="visually-hidden" id="settings-taptopaginate-option-help">
<ng-container [ngTemplateOutlet]="tapToPaginateOptionTooltip"></ng-container> <ng-container [ngTemplateOutlet]="tapToPaginateOptionTooltip"></ng-container>
@ -292,8 +372,13 @@
<label id="immersivemode-label" class="form-label"></label> <label id="immersivemode-label" class="form-label"></label>
<div class="mb-3"> <div class="mb-3">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label"> <input type="checkbox" role="switch" id="immersivemode"
<label for="immersivemode" class="form-check-label">{{t('immersive-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i> formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true"
aria-labelledby="immersivemode-label">
<label for="immersivemode"
class="form-check-label">{{t('immersive-mode-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
<ng-template #immersivemodeOptionTooltip>{{t('immersive-mode-label')}}</ng-template> <ng-template #immersivemodeOptionTooltip>{{t('immersive-mode-label')}}</ng-template>
<span class="visually-hidden" id="settings-immersivemode-option-help"> <span class="visually-hidden" id="settings-immersivemode-option-help">
<ng-container [ngTemplateOutlet]="immersivemodeOptionTooltip"></ng-container> <ng-container [ngTemplateOutlet]="immersivemodeOptionTooltip"></ng-container>
@ -305,24 +390,35 @@
<div class="row g-0"> <div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3"> <div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i> <label for="settings-book-reading-direction"
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}</ng-template> class="form-label">{{t('reading-direction-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}
</ng-template>
<span class="visually-hidden" id="settings-book-reading-direction-book-help"> <span class="visually-hidden" id="settings-book-reading-direction-book-help">
<ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container> <ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container>
</span> </span>
<select id="settings-book-reading-direction" class="form-select" aria-describedby="settings-book-reading-direction-help" formControlName="bookReaderReadingDirection"> <select id="settings-book-reading-direction" class="form-select"
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option> aria-describedby="settings-book-reading-direction-help"
formControlName="bookReaderReadingDirection">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select> </select>
</div> </div>
<div class="col-md-6 col-sm-12 pe-2 mb-3"> <div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-fontfamily-option" class="form-label">{{t('font-family-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i> <label for="settings-fontfamily-option"
class="form-label">{{t('font-family-label')}}</label><i class="fa fa-info-circle ms-1"
aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button"
tabindex="0"></i>
<ng-template #fontFamilyOptionTooltip>{{t('font-family-tooltip')}}</ng-template> <ng-template #fontFamilyOptionTooltip>{{t('font-family-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-fontfamily-option-help"> <span class="visually-hidden" id="settings-fontfamily-option-help">
<ng-container [ngTemplateOutlet]="fontFamilyOptionTooltip"></ng-container> <ng-container [ngTemplateOutlet]="fontFamilyOptionTooltip"></ng-container>
</span> </span>
<select id="settings-fontfamily-option" class="form-select" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily"> <select id="settings-fontfamily-option" class="form-select"
aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option> <option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
</select> </select>
</div> </div>
@ -330,24 +426,35 @@
<div class="row g-0"> <div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3"> <div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-writing-style" class="form-label me-1">{{t('writing-style-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" aria-describedby="settings-book-writing-style-help" placement="right" [ngbTooltip]="bookWritingStyleToolTip" role="button" tabindex="0"></i> <label for="settings-book-writing-style"
class="form-label me-1">{{t('writing-style-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true"
aria-describedby="settings-book-writing-style-help" placement="right"
[ngbTooltip]="bookWritingStyleToolTip" role="button" tabindex="0"></i>
<ng-template #bookWritingStyleToolTip>{{t('writing-style-tooltip')}}</ng-template> <ng-template #bookWritingStyleToolTip>{{t('writing-style-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-writing-style-help"> <span class="visually-hidden" id="settings-book-writing-style-help">
<ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container> <ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container>
</span> </span>
<select class="form-select" aria-describedby="settings-book-writing-style-help" formControlName="bookReaderWritingStyle" id="settings-book-writing-style" > <select class="form-select" aria-describedby="settings-book-writing-style-help"
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option> formControlName="bookReaderWritingStyle" id="settings-book-writing-style">
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select> </select>
</div> </div>
<div class="col-md-6 col-sm-12 pe-2 mb-3"> <div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-layout-mode" class="form-label">{{t('layout-mode-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i> <label for="settings-book-layout-mode"
class="form-label">{{t('layout-mode-book-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #bookLayoutModeTooltip>{{t('layout-mode-book-tooltip')}}</ng-template> <ng-template #bookLayoutModeTooltip>{{t('layout-mode-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-layout-mode-help"> <span class="visually-hidden" id="settings-book-layout-mode-help">
<ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container> <ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container>
</span> </span>
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode"> <select class="form-select" aria-describedby="settings-book-layout-mode-help"
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option> formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select> </select>
</div> </div>
</div> </div>
@ -355,13 +462,18 @@
<div class="row g-0"> <div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3"> <div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-color-theme-option" class="form-label">{{t('color-theme-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i> <label for="settings-color-theme-option"
class="form-label">{{t('color-theme-book-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
<ng-template #bookColorThemeTooltip>{{t('color-theme-book-tooltip')}}</ng-template> <ng-template #bookColorThemeTooltip>{{t('color-theme-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-color-theme-option-help"> <span class="visually-hidden" id="settings-color-theme-option-help">
<ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container> <ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container>
</span> </span>
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option"> <select class="form-select" aria-describedby="settings-color-theme-option-help"
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">{{opt.name | titlecase}}</option> formControlName="bookReaderThemeName" id="settings-color-theme-option">
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">
{{opt.name | titlecase}}</option>
</select> </select>
</div> </div>
</div> </div>
@ -369,79 +481,181 @@
<div class="row g-0"> <div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3"> <div class="col-md-4 col-sm-12 pe-2 mb-3">
<label for="fontsize" class="form-label range-label">{{t('font-size-book-label')}}</label> <label for="fontsize" class="form-label range-label">{{t('font-size-book-label')}}</label>
<input type="range" class="form-range" id="fontsize" <input type="range" class="form-range" id="fontsize" min="50" max="300" step="10"
min="50" max="300" step="10" formControlName="bookReaderFontSize"> formControlName="bookReaderFontSize">
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span> <span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
</div> </div>
<div class="col-md-4 col-sm-12 pe-2 mb-3"> <div class="col-md-4 col-sm-12 pe-2 mb-3">
<div class="range-label"> <div class="range-label">
<label class="form-label" for="linespacing">{{t('line-height-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i> <label class="form-label" for="linespacing">{{t('line-height-book-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookLineHeightOptionTooltip>{{t('line-height-book-tooltip')}}</ng-template> <ng-template #bookLineHeightOptionTooltip>{{t('line-height-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-booklineheight-option-help"> <span class="visually-hidden" id="settings-booklineheight-option-help">
<ng-container [ngTemplateOutlet]="bookLineHeightOptionTooltip"></ng-container> <ng-container [ngTemplateOutlet]="bookLineHeightOptionTooltip"></ng-container>
</span> </span>
</div> </div>
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10" <input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help"> formControlName="bookReaderLineSpacing"
aria-describedby="settings-booklineheight-option-help">
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span> <span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</div> </div>
<div class="col-md-4 col-sm-12 pe-2 mb-3"> <div class="col-md-4 col-sm-12 pe-2 mb-3">
<div class="range-label"> <div class="range-label">
<label class="form-label">{{t('margin-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i> <label class="form-label">{{t('margin-book-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReaderMarginOptionTooltip>{{t('margin-book-tooltip')}}</ng-template> <ng-template #bookReaderMarginOptionTooltip>{{t('margin-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-bookmargin-option-help"> <span class="visually-hidden" id="settings-bookmargin-option-help">
<ng-container [ngTemplateOutlet]="bookReaderMarginOptionTooltip"></ng-container> <ng-container [ngTemplateOutlet]="bookReaderMarginOptionTooltip"></ng-container>
</span> </span>
</div> </div>
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin"> <input type="range" class="form-range" id="margin" min="0" max="30" step="5"
formControlName="bookReaderMargin" aria-describedby="bookmargin">
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span> <span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
</div> </div>
</div> </div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3"> <div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button> <button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()"
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button> aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()"
aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem [id]="AccordionPanelID.PdfReader">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button"
[attr.aria-expanded]="acc.isExpanded(AccordionPanelID.PdfReader)" aria-controls="collapseOne">
{{t('pdf-reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-pdf-layout-mode" class="form-label">{{t('pdf-layout-mode-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="pdfLayoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #pdfLayoutModeTooltip>{{t('pdf-layout-mode-tooltip')}}
</ng-template>
<span class="visually-hidden" id="settings-pdf-layout-mode-help">
<ng-container [ngTemplateOutlet]="pdfLayoutModeTooltip"></ng-container>
</span>
<select id="settings-pdf-layout-mode" class="form-select"
aria-describedby="settings-pdf-layout-mode-help"
formControlName="pdfLayoutMode">
<option *ngFor="let opt of pdfLayoutModesTranslated" [value]="opt.value">
{{opt.text | titlecase}}
</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-pdf-scroll-mode" class="form-label">{{t('pdf-scroll-mode-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="pdfScrollModeTooltip" role="button" tabindex="0"></i>
<ng-template #pdfScrollModeTooltip>{{t('pdf-scroll-mode-tooltip')}}
</ng-template>
<span class="visually-hidden" id="settings-pdf-scroll-mode-help">
<ng-container [ngTemplateOutlet]="pdfScrollModeTooltip"></ng-container>
</span>
<select id="settings-pdf-scroll-mode" class="form-select"
aria-describedby="settings-pdf-scroll-mode-help"
formControlName="pdfScrollMode">
<option *ngFor="let opt of pdfScrollModesTranslated" [value]="opt.value">
{{opt.text | titlecase}}
</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-pdf-spread-mode" class="form-label">{{t('pdf-spread-mode-label')}}</label><i
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="pdfSpreadModeTooltip" role="button" tabindex="0"></i>
<ng-template #pdfSpreadModeTooltip>{{t('pdf-spread-mode-tooltip')}}
</ng-template>
<span class="visually-hidden" id="settings-pdf-spread-mode-help">
<ng-container [ngTemplateOutlet]="pdfSpreadModeTooltip"></ng-container>
</span>
<select id="settings-pdf-spread-mode" class="form-select"
aria-describedby="settings-pdf-spread-mode-help"
formControlName="pdfSpreadMode">
<option *ngFor="let opt of pdfSpreadModesTranslated" [value]="opt.value">
{{opt.text | titlecase}}
</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-pdf-theme" class="form-label">{{t('pdf-theme-label')}}</label>
<select id="settings-pdf-theme" class="form-select"
formControlName="pdfTheme">
<option *ngFor="let opt of pdfThemesTranslated" [value]="opt.value">
{{opt.text | titlecase}}
</option>
</select>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()"
aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()"
aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div> </div>
</ng-template> </ng-template>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
</ng-container> </ng-container>
} }
@defer (when tab.fragment === FragmentID.Clients; prefetch on idle) { @defer (when tab.fragment === FragmentID.Clients; prefetch on idle) {
<div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">{{t('clients-opds-alert')}}</div> <div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">{{t('clients-opds-alert')}}</div>
<p>{{t('clients-opds-description')}}</p> <p>{{t('clients-opds-description')}}</p>
<app-api-key [tooltipText]="t('clients-api-key-tooltip')" [hideData]="true"></app-api-key> <app-api-key [tooltipText]="t('clients-api-key-tooltip')" [hideData]="true"></app-api-key>
<app-api-key [title]="t('clients-opds-url-tooltip')" [hideData]="true" [showRefresh]="false" [transform]="makeUrl"></app-api-key> <app-api-key [title]="t('clients-opds-url-tooltip')" [hideData]="true" [showRefresh]="false"
} [transform]="makeUrl"></app-api-key>
@defer (when tab.fragment === FragmentID.Theme; prefetch on idle) { }
<app-theme-manager></app-theme-manager> @defer (when tab.fragment === FragmentID.Theme; prefetch on idle) {
} <app-theme-manager></app-theme-manager>
}
@defer (when tab.fragment === FragmentID.Devices; prefetch on idle) { @defer (when tab.fragment === FragmentID.Devices; prefetch on idle) {
<app-manage-devices></app-manage-devices> <app-manage-devices></app-manage-devices>
} }
@defer (when tab.fragment === FragmentID.Stats; prefetch on idle) { @defer (when tab.fragment === FragmentID.Stats; prefetch on idle) {
<app-user-stats></app-user-stats> <app-user-stats></app-user-stats>
} }
@defer (when tab.fragment === FragmentID.Scrobbling; prefetch on idle) { @defer (when tab.fragment === FragmentID.Scrobbling; prefetch on idle) {
@if(hasActiveLicense) { @if(hasActiveLicense) {
<app-user-scrobble-history></app-user-scrobble-history> <app-user-scrobble-history></app-user-scrobble-history>
<app-user-holds></app-user-holds> <app-user-holds></app-user-holds>
} }
} }
</ng-template> </ng-template>
</li> </li>
</ul> </ul>
<div [ngbNavOutlet]="nav" class="mt-3"></div> <div [ngbNavOutlet]="nav" class="mt-3"></div>
</div> </div>
</ng-container> </ng-container>

View File

@ -7,54 +7,82 @@ import {
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import { ToastrService } from 'ngx-toastr'; import {ToastrService} from 'ngx-toastr';
import {take} from 'rxjs/operators'; import {take} from 'rxjs/operators';
import { Title } from '@angular/platform-browser'; import {Title} from '@angular/platform-browser';
import { import {
readingDirections,
scalingOptions,
pageSplitOptions,
readingModes,
Preferences,
bookLayoutModes, bookLayoutModes,
bookWritingStyles,
layoutModes, layoutModes,
pageLayoutModes, pageLayoutModes,
bookWritingStyles pageSplitOptions,
pdfLayoutModes,
pdfScrollModes,
pdfSpreadModes,
pdfThemes,
Preferences,
readingDirections,
readingModes,
scalingOptions
} from 'src/app/_models/preferences/preferences'; } from 'src/app/_models/preferences/preferences';
import { User } from 'src/app/_models/user'; import {User} from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service'; import {AccountService} from 'src/app/_services/account.service';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import {ActivatedRoute, Router, RouterLink} from '@angular/router';
import { SettingsService } from 'src/app/admin/settings.service'; import {SettingsService} from 'src/app/admin/settings.service';
import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode'; import {BookPageLayoutMode} from 'src/app/_models/readers/book-page-layout-mode';
import {forkJoin} from 'rxjs'; import {forkJoin} from 'rxjs';
import { bookColorThemes } from 'src/app/book-reader/_components/reader-settings/reader-settings.component'; import {bookColorThemes} from 'src/app/book-reader/_components/reader-settings/reader-settings.component';
import { BookService } from 'src/app/book-reader/_services/book.service'; import {BookService} from 'src/app/book-reader/_services/book.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe'; import {SentenceCasePipe} from '../../_pipes/sentence-case.pipe';
import { UserHoldsComponent } from '../user-holds/user-holds.component'; import {UserHoldsComponent} from '../user-holds/user-holds.component';
import { UserScrobbleHistoryComponent } from '../../_single-module/user-scrobble-history/user-scrobble-history.component'; import {UserScrobbleHistoryComponent} from '../../_single-module/user-scrobble-history/user-scrobble-history.component';
import { UserStatsComponent } from '../../statistics/_components/user-stats/user-stats.component'; import {UserStatsComponent} from '../../statistics/_components/user-stats/user-stats.component';
import { ManageDevicesComponent } from '../manage-devices/manage-devices.component'; import {ManageDevicesComponent} from '../manage-devices/manage-devices.component';
import { ThemeManagerComponent } from '../theme-manager/theme-manager.component'; import {ThemeManagerComponent} from '../theme-manager/theme-manager.component';
import { ApiKeyComponent } from '../api-key/api-key.component'; import {ApiKeyComponent} from '../api-key/api-key.component';
import { ColorPickerModule } from 'ngx-color-picker'; import {ColorPickerModule} from 'ngx-color-picker';
import { ChangeAgeRestrictionComponent } from '../change-age-restriction/change-age-restriction.component'; import {ChangeAgeRestrictionComponent} from '../change-age-restriction/change-age-restriction.component';
import { ChangePasswordComponent } from '../change-password/change-password.component'; import {ChangePasswordComponent} from '../change-password/change-password.component';
import { ChangeEmailComponent } from '../change-email/change-email.component'; import {ChangeEmailComponent} from '../change-email/change-email.component';
import { NgFor, NgIf, NgTemplateOutlet, TitleCasePipe } from '@angular/common'; import {NgFor, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
import { NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgbNavOutlet } from '@ng-bootstrap/ng-bootstrap'; import {
import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; NgbAccordionBody,
NgbAccordionButton,
NgbAccordionCollapse,
NgbAccordionDirective,
NgbAccordionHeader,
NgbAccordionItem,
NgbAccordionToggle,
NgbCollapse,
NgbNav,
NgbNavContent,
NgbNavItem,
NgbNavItemRole,
NgbNavLink,
NgbNavOutlet,
NgbTooltip
} from '@ng-bootstrap/ng-bootstrap';
import {
SideNavCompanionBarComponent
} from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {LocalizationService} from "../../_services/localization.service"; import {LocalizationService} from "../../_services/localization.service";
import {Language} from "../../_models/metadata/language"; import {Language} from "../../_models/metadata/language";
import {translate, TranslocoDirective} from "@ngneat/transloco"; import {translate, TranslocoDirective} from "@ngneat/transloco";
import {LoadingComponent} from "../../shared/loading/loading.component"; import {LoadingComponent} from "../../shared/loading/loading.component";
import {ManageScrobblingProvidersComponent} from "../manage-scrobbling-providers/manage-scrobbling-providers.component"; import {ManageScrobblingProvidersComponent} from "../manage-scrobbling-providers/manage-scrobbling-providers.component";
import {PdfLayoutModePipe} from "../../pdf-reader/_pipe/pdf-layout-mode.pipe";
import {PdfTheme} from "../../_models/preferences/pdf-theme";
import {PdfScrollMode} from "../../_models/preferences/pdf-scroll-mode";
import {PdfLayoutMode} from "../../_models/preferences/pdf-layout-mode";
import {PdfSpreadMode} from "../../_models/preferences/pdf-spread-mode";
enum AccordionPanelID { enum AccordionPanelID {
ImageReader = 'image-reader', ImageReader = 'image-reader',
BookReader = 'book-reader', BookReader = 'book-reader',
GlobalSettings = 'global-settings' GlobalSettings = 'global-settings',
PdfReader = 'pdf-reader'
} }
enum FragmentID { enum FragmentID {
@ -77,7 +105,7 @@ enum FragmentID {
ChangePasswordComponent, ChangeAgeRestrictionComponent, ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, ChangePasswordComponent, ChangeAgeRestrictionComponent, ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader,
NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, ColorPickerModule, ApiKeyComponent, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, ColorPickerModule, ApiKeyComponent,
ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe, ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe,
TranslocoDirective, LoadingComponent, ManageScrobblingProvidersComponent], TranslocoDirective, LoadingComponent, ManageScrobblingProvidersComponent, PdfLayoutModePipe],
}) })
export class UserPreferencesComponent implements OnInit, OnDestroy { export class UserPreferencesComponent implements OnInit, OnDestroy {
@ -108,6 +136,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
pageLayoutModesTranslated = pageLayoutModes.map(this.translatePrefOptions); pageLayoutModesTranslated = pageLayoutModes.map(this.translatePrefOptions);
bookWritingStylesTranslated = bookWritingStyles.map(this.translatePrefOptions); bookWritingStylesTranslated = bookWritingStyles.map(this.translatePrefOptions);
pdfLayoutModesTranslated = pdfLayoutModes.map(this.translatePrefOptions);
pdfScrollModesTranslated = pdfScrollModes.map(this.translatePrefOptions);
pdfSpreadModesTranslated = pdfSpreadModes.map(this.translatePrefOptions);
pdfThemesTranslated = pdfThemes.map(this.translatePrefOptions);
settingsForm: FormGroup = new FormGroup({}); settingsForm: FormGroup = new FormGroup({});
user: User | undefined = undefined; user: User | undefined = undefined;
@ -129,7 +162,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
opdsUrl: string = ''; opdsUrl: string = '';
makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; }; makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; };
hasActiveLicense = false; hasActiveLicense = false;
canEdit = true;
@ -214,11 +246,16 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, [])); this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, [])); this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, []));
this.settingsForm.addControl('bookReaderWritingStyle', new FormControl(this.user.preferences.bookReaderWritingStyle, [])) this.settingsForm.addControl('bookReaderWritingStyle', new FormControl(this.user.preferences.bookReaderWritingStyle, []))
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(!!this.user.preferences.bookReaderTapToPaginate, [])); 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('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('bookReaderThemeName', new FormControl(this.user?.preferences.bookReaderThemeName || bookColorThemes[0].name, []));
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user?.preferences.bookReaderImmersiveMode, [])); 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('pdfLayoutMode', new FormControl(this.user?.preferences.pdfLayoutMode || PdfLayoutMode.Multiple, []));
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, []));
@ -278,6 +315,12 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.settingsForm.get('collapseSeriesRelationships')?.setValue(this.user.preferences.collapseSeriesRelationships); this.settingsForm.get('collapseSeriesRelationships')?.setValue(this.user.preferences.collapseSeriesRelationships);
this.settingsForm.get('shareReviews')?.setValue(this.user.preferences.shareReviews); this.settingsForm.get('shareReviews')?.setValue(this.user.preferences.shareReviews);
this.settingsForm.get('locale')?.setValue(this.user.preferences.locale); this.settingsForm.get('locale')?.setValue(this.user.preferences.locale);
this.settingsForm.get('pdfTheme')?.setValue(this.user.preferences.pdfTheme);
this.settingsForm.get('pdfScrollMode')?.setValue(this.user.preferences.pdfScrollMode);
this.settingsForm.get('pdfLayoutMode')?.setValue(this.user.preferences.pdfLayoutMode);
this.settingsForm.get('pdfSpreadMode')?.setValue(this.user.preferences.pdfSpreadMode);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.settingsForm.markAsPristine(); this.settingsForm.markAsPristine();
} }
@ -313,7 +356,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
swipeToPaginate: modelSettings.swipeToPaginate, swipeToPaginate: modelSettings.swipeToPaginate,
collapseSeriesRelationships: modelSettings.collapseSeriesRelationships, collapseSeriesRelationships: modelSettings.collapseSeriesRelationships,
shareReviews: modelSettings.shareReviews, shareReviews: modelSettings.shareReviews,
locale: modelSettings.locale locale: modelSettings.locale,
pdfTheme: parseInt(modelSettings.pdfTheme, 10),
pdfScrollMode: parseInt(modelSettings.pdfScrollMode, 10),
pdfLayoutMode: parseInt(modelSettings.pdfLayoutMode, 10),
pdfSpreadMode: parseInt(modelSettings.pdfSpreadMode, 10),
}; };
this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => { this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {

View File

@ -159,6 +159,15 @@
"margin-book-label": "Margin", "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.", "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-layout-mode-label": "Layout Mode",
"pdf-layout-mode-tooltip": "How the reader lays the pdf out. Default is pages stacked with scrolling and Book emulates a physical book",
"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",
"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. Keep it secret, Keep it safe.", "clients-api-key-tooltip": "The API key is like a password. Keep it secret, Keep it safe.",
@ -1600,7 +1609,28 @@
"incognito-mode": "Incognito Mode", "incognito-mode": "Incognito Mode",
"light-theme-alt": "Light Theme", "light-theme-alt": "Light Theme",
"dark-theme-alt": "Dark Theme", "dark-theme-alt": "Dark Theme",
"close-reader-alt": "Close Reader" "close-reader-alt": "Close Reader",
"toggle-incognito": "Turn off Incognito Mode"
},
"pdf-layout-mode-pipe": {
"single": "Single Page",
"book": "Book mode",
"multiple": "Default",
"infinite-scroll": "Infinite Scroll"
},
"pdf-scroll-mode-pipe": {
"vertical": "Vertical",
"horizontal": "Horizontal",
"wrapped": "Wrapped",
"page": "Tap to Paginate"
},
"pdf-spread-mode-pipe": {
"off": "No Spreads",
"odd": "Odd Spreads",
"even": "Even Spreads"
}, },
"infinite-scroller": { "infinite-scroller": {
@ -2189,7 +2219,17 @@
"2-column": "2 Column", "2-column": "2 Column",
"cards": "Cards", "cards": "Cards",
"list": "List", "list": "List",
"up-to-down": "Up to Down" "up-to-down": "Up to Down",
"pdf-multiple": "Default",
"pdf-book": "Book",
"pdf-vertical": "Scroll Vertical",
"pdf-horizontal": "Scroll Horizontal",
"pdf-page": "Tap to Paginate",
"pdf-none": "None",
"pdf-odd": "Odd",
"pdf-even": "Even",
"pdf-light": "Light",
"pdf-dark": "Dark"
}, },

View File

@ -2,10 +2,12 @@
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`. // The list of file replacements can be found in `angular.json`.
const IP = 'localhost';
export const environment = { export const environment = {
production: false, production: false,
apiUrl: 'http://localhost:5000/api/', apiUrl: 'http://' + IP + ':5000/api/',
hubUrl: 'http://localhost:5000/hubs/', hubUrl: 'http://'+ IP + ':5000/hubs/',
buyLink: 'https://buy.stripe.com/test_9AQ5mi058h1PcIo3cf?prefilled_promo_code=FREETRIAL', buyLink: 'https://buy.stripe.com/test_9AQ5mi058h1PcIo3cf?prefilled_promo_code=FREETRIAL',
manageLink: 'https://billing.stripe.com/p/login/test_14kfZocuh6Tz5ag7ss' manageLink: 'https://billing.stripe.com/p/login/test_14kfZocuh6Tz5ag7ss'
}; };

View File

@ -10,6 +10,8 @@ export class HttpLoader implements TranslocoLoader {
getTranslation(langPath: string) { getTranslation(langPath: string) {
const tokens = langPath.split('/'); const tokens = langPath.split('/');
const langCode = tokens[tokens.length - 1]; const langCode = tokens[tokens.length - 1];
return this.http.get<Translation>(`assets/langs/${langCode}.json?v=${(cacheBusting as { [key: string]: string })[langCode]}`); const url = `assets/langs/${langCode}.json?v=${(cacheBusting as { [key: string]: string })[langCode]}`;
console.log('loading locale: ', url);
return this.http.get<Translation>(url);
} }
} }

View File

@ -28,12 +28,13 @@ import {provideTranslocoLocale} from "@ngneat/transloco-locale";
import {provideTranslocoPersistTranslations} from "@ngneat/transloco-persist-translations"; import {provideTranslocoPersistTranslations} from "@ngneat/transloco-persist-translations";
import {LazyLoadImageModule} from "ng-lazyload-image"; import {LazyLoadImageModule} from "ng-lazyload-image";
import {getSaver, SAVER} from "./app/_providers/saver.provider"; import {getSaver, SAVER} from "./app/_providers/saver.provider";
import {distinctUntilChanged} from "rxjs/operators";
const disableAnimations = !('animate' in document.documentElement); const disableAnimations = !('animate' in document.documentElement);
export function preloadUser(userService: AccountService, transloco: TranslocoService) { export function preloadUser(userService: AccountService, transloco: TranslocoService) {
return function() { return function() {
return userService.currentUser$.pipe(switchMap((user) => { return userService.currentUser$.pipe(distinctUntilChanged(), switchMap((user) => {
if (user && user.preferences.locale) { if (user && user.preferences.locale) {
transloco.setActiveLang(user.preferences.locale); transloco.setActiveLang(user.preferences.locale);
return transloco.load(user.preferences.locale) return transloco.load(user.preferences.locale)

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.14.9" "version": "0.7.14.10"
}, },
"servers": [ "servers": [
{ {
@ -13300,9 +13300,6 @@
"description": "Book Reader Option: Defines the writing styles vertical/horizontal", "description": "Book Reader Option: Defines the writing styles vertical/horizontal",
"format": "int32" "format": "int32"
}, },
"theme": {
"$ref": "#/components/schemas/SiteTheme"
},
"bookThemeName": { "bookThemeName": {
"type": "string", "type": "string",
"description": "Book Reader Option: The color theme to decorate the book contents", "description": "Book Reader Option: The color theme to decorate the book contents",
@ -13322,6 +13319,47 @@
"type": "boolean", "type": "boolean",
"description": "Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this." "description": "Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this."
}, },
"pdfTheme": {
"enum": [
0,
1
],
"type": "integer",
"description": "PDF Reader: Theme of the Reader",
"format": "int32"
},
"pdfScrollMode": {
"enum": [
0,
1,
3
],
"type": "integer",
"description": "PDF Reader: Scroll mode of the reader",
"format": "int32"
},
"pdfLayoutMode": {
"enum": [
0,
2
],
"type": "integer",
"description": "PDF Reader: Layout Mode of the reader",
"format": "int32"
},
"pdfSpreadMode": {
"enum": [
0,
1,
2
],
"type": "integer",
"description": "PDF Reader: Spread Mode of the reader",
"format": "int32"
},
"theme": {
"$ref": "#/components/schemas/SiteTheme"
},
"globalPageLayoutMode": { "globalPageLayoutMode": {
"enum": [ "enum": [
0, 0,
@ -20626,6 +20664,10 @@
"locale", "locale",
"noTransitions", "noTransitions",
"pageSplitOption", "pageSplitOption",
"pdfLayoutMode",
"pdfScrollMode",
"pdfSpreadMode",
"pdfTheme",
"promptForDownloadSize", "promptForDownloadSize",
"readerMode", "readerMode",
"readingDirection", "readingDirection",
@ -20804,6 +20846,44 @@
"minLength": 1, "minLength": 1,
"type": "string", "type": "string",
"description": "UI Site Global Setting: The language locale that should be used for the user" "description": "UI Site Global Setting: The language locale that should be used for the user"
},
"pdfTheme": {
"enum": [
0,
1
],
"type": "integer",
"description": "PDF Reader: Theme of the Reader",
"format": "int32"
},
"pdfScrollMode": {
"enum": [
0,
1,
3
],
"type": "integer",
"description": "PDF Reader: Scroll mode of the reader",
"format": "int32"
},
"pdfLayoutMode": {
"enum": [
0,
2
],
"type": "integer",
"description": "PDF Reader: Layout Mode of the reader",
"format": "int32"
},
"pdfSpreadMode": {
"enum": [
0,
1,
2
],
"type": "integer",
"description": "PDF Reader: Spread Mode of the reader",
"format": "int32"
} }
}, },
"additionalProperties": false "additionalProperties": false