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.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.28" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.28" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.0.2" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.0.2" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<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("Kebab Том 1 Глава 1", "Kebab")]
[InlineData("Манга Глава 1", "Манга")]
[InlineData("ReZero รีเซทชีวิต ฝ่าวิกฤตต่างโลก เล่ม 1", "ReZero รีเซทชีวิต ฝ่าวิกฤตต่างโลก")]
[InlineData("SKY WORLD สกายเวิลด์ เล่มที่ 1", "SKY WORLD สกายเวิลด์")]
public void ParseComicSeriesTest(string filename, string expected)
{
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename));
@ -129,6 +131,9 @@ public class ComicParsingTests
// Russian Tests
[InlineData("Kebab Том 1 Глава 3", "1")]
[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)
{
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("Манга Том 1 2 Глава", "2")]
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")]
[InlineData("Max Level Returner ตอนที่ 5", "5")]
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
public void ParseComicChapterTest(string filename, string expected)
{
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("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake", "Nagasarete Airantou")]
[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)
{
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 c01-c04", "1-4")]
[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)
{
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.MaximumConcurrentExecutions" Version="1.1.0" />
<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="Hangfire.AspNetCore" Version="1.8.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
@ -81,8 +81,8 @@
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
<PackageReference Include="NetVips" Version="2.4.0" />
<PackageReference Include="NetVips.Native" Version="8.15.1" />
<PackageReference Include="NetVips" Version="2.4.1" />
<PackageReference Include="NetVips.Native" Version="8.15.2" />
<PackageReference Include="NReco.Logging.File" Version="1.2.0" />
<PackageReference Include="Serilog" Version="3.1.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="SharpCompress" Version="0.36.0" />
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.0" />
<PackageReference Include="System.IO.Abstractions" Version="20.0.28" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.5.0" />
<PackageReference Include="System.IO.Abstractions" Version="21.0.2" />
<PackageReference Include="System.Drawing.Common" Version="8.0.3" />
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
</ItemGroup>

View File

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

View File

@ -118,6 +118,12 @@ public class UsersController : BaseApiController
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
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))
{
existingPreferences.Locale = preferencesDto.Locale;

View File

@ -152,4 +152,25 @@ public class UserPreferencesDto
/// </summary>
[Required]
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")
.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")
.HasColumnType("INTEGER");

View File

@ -7,6 +7,9 @@ namespace API.Entities;
public class AppUserPreferences
{
public int Id { get; set; }
#region MangaReader
/// <summary>
/// Manga Reader Option: What direction should the next/prev page buttons go
/// </summary>
@ -51,6 +54,11 @@ public class AppUserPreferences
/// Manga Reader Option: Should swiping trigger pagination
/// </summary>
public bool SwipeToPaginate { get; set; }
#endregion
#region EpubReader
/// <summary>
/// Book Reader Option: Override extra Margin
/// </summary>
@ -75,17 +83,11 @@ public class AppUserPreferences
/// Book Reader Option: What direction should the next/prev page buttons go
/// </summary>
public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight;
/// <summary>
/// Book Reader Option: Defines the writing styles vertical/horizontal
/// </summary>
public WritingStyle BookReaderWritingStyle { get; set; } = WritingStyle.Horizontal;
/// <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
/// </summary>
/// <remarks>Should default to Dark</remarks>
@ -101,6 +103,37 @@ public class AppUserPreferences
/// </summary>
/// <remarks>Defaults to false</remarks>
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>
/// Global Site Option: If the UI should layout items as Cards or List items
/// </summary>
@ -132,6 +165,8 @@ public class AppUserPreferences
/// </summary>
public string Locale { get; set; }
#endregion
public AppUser AppUser { get; set; } = null!;
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 API.Entities;
using API.Helpers;
using API.Helpers.Builders;
using API.Services.Tasks.Scanner.Parser;
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
/// is <see cref="ParserInfo.IsSpecial"/> then, the filename is used to search against Range or if filename exists within Files of said Chapter.
/// </summary>
/// <remarks>This uses GetNumberTitle() to calculate the Range to compare against the info.Chapters</remarks>
/// <param name="chapters"></param>
/// <param name="info"></param>
/// <returns></returns>
@ -31,9 +33,12 @@ public static class ChapterListExtensions
{
var normalizedPath = Parser.NormalizePath(info.FullFilePath);
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
? 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>

View File

@ -164,7 +164,9 @@ public static class IncludesExtensions
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))

View File

@ -36,7 +36,7 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
var specialTitle = specialTreatment ? Parser.RemoveExtensionIfSupported(info.Filename) : info.Chapters;
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)
.WithTitle((specialTreatment && info.Format == MangaFormat.Epub)
? 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)
{
foreach (var styleLinks in styleNodes)

View File

@ -148,14 +148,14 @@ public class LibraryWatcher : ILibraryWatcher
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;
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name))));
}
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)));
}
@ -167,7 +167,7 @@ public class LibraryWatcher : ILibraryWatcher
private void OnDeleted(object sender, FileSystemEventArgs e) {
var isDirectory = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name));
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));
}
@ -285,10 +285,10 @@ public class LibraryWatcher : ILibraryWatcher
var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList();
_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.
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();
}
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))
{
info.Chapters = info.ComicInfo.Number;
if (info.IsSpecial && Parser.DefaultChapter != info.Chapters)
{
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();
}
}
public abstract bool IsApplicable(string filePath, LibraryType type);

View File

@ -121,6 +121,10 @@ public static class Parser
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
new Regex(
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)",
@ -194,6 +198,10 @@ public static class Parser
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
new Regex(
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
@ -368,6 +376,10 @@ public static class Parser
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
new Regex(
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
@ -456,6 +468,10 @@ public static class Parser
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)
new Regex(
@"^(?<Series>.+?)(?: |_)(t|v)(?<Volume>" + NumberRange + @")",
@ -492,6 +508,10 @@ public static class Parser
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)
new Regex(
@"(?<Series>.*(\d{4})?)( |_)(?:\((?<Chapter>\d+) of \d+)",
@ -557,6 +577,10 @@ public static class Parser
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
new Regex(
@"(\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))
{
_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);
}
else

View File

@ -21,15 +21,18 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit
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)
3. Install the required Node Packages
- cd Kavita/UI/Web
- `cd Kavita/UI/Web`
- `npm install`
- `npm install -g @angular/cli`
- `npm run cache-locale-prime` (only do this once to generate the locale file)
4. Start angular server `ng serve`
5. Build the project in Visual Studio/Rider, Setting startup project to `API`
6. Debug the project in Visual Studio/Rider
7. 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.
5. Start the frontend
- `npm run start`
6. Build the project in Visual Studio/Rider, Setting startup project to `API`
7. Debug the project in Visual Studio/Rider
8. Open http://localhost:4200
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 ###

View File

@ -15,7 +15,7 @@
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" 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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -4,7 +4,7 @@ This project was generated with [Angular CLI](https://github.com/angular/angular
## 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.
## 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.
## 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
and update environment.ts to your local ip.
Update `IP` constant in `src/environments/environment.ts` to your dev machine's ip instead of `localhost`.
Run `npm run start`
## Notes:
- injected services should be at the top of the file

View File

@ -14,6 +14,15 @@ function generateChecksum(str, algorithm, encoding) {
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
const cacheBustingFilePath = './i18n-cache-busting.json';
if (fs.existsSync(cacheBustingFilePath)) {

View File

@ -3,7 +3,7 @@
"version": "0.7.12.1",
"scripts": {
"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",
"minify-langs": "node minify-json.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 { SiteTheme } from './site-theme';
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 {
// Manga Reader
@ -34,6 +38,12 @@ export interface Preferences {
bookReaderLayoutMode: BookPageLayoutMode;
bookReaderImmersiveMode: boolean;
// PDF Reader
pdfTheme: PdfTheme;
pdfScrollMode: PdfScrollMode;
pdfLayoutMode: PdfLayoutMode;
pdfSpreadMode: PdfSpreadMode;
// Global
theme: SiteTheme;
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 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 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 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 isOnline: boolean = true;
constructor(private httpClient: HttpClient, private router: Router,
private messageHub: MessageHubService, private themeService: ThemeService) {
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
@ -59,6 +61,15 @@ export class AccountService {
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
switchMap(() => this.refreshAccount()))
.subscribe(() => {});
window.addEventListener("offline", (e) => {
this.isOnline = false;
});
window.addEventListener("online", (e) => {
this.isOnline = true;
this.refreshToken().subscribe();
});
}
hasAdminRole(user: User) {
@ -143,6 +154,7 @@ export class AccountService {
localStorage.setItem(this.userKey, JSON.stringify(user));
localStorage.setItem(AccountService.lastLoginKey, user.username);
if (user.preferences && user.preferences.theme) {
this.themeService.setTheme(user.preferences.theme.name);
} else {
@ -329,7 +341,7 @@ export class AccountService {
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',
{token: this.currentUser.token, refreshToken: this.currentUser.refreshToken}).pipe(map(user => {
if (this.currentUser) {

View File

@ -2,18 +2,25 @@
<div>
<div class="modal-header">
<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>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
</div>
<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>
</div>
<div class="modal-footer">
<a *ngIf="review.externalUrl" class="btn btn-icon" [href]="review.externalUrl | safeHtml" target="_blank" rel="noopener noreferrer" [title]="review.externalUrl">
{{t('go-to-review')}}
</a>
@if (review.siteUrl) {
<a class="btn btn-icon" [href]="review.siteUrl | safeHtml" target="_blank" rel="noopener noreferrer" [title]="review.siteUrl">
{{t('go-to-review')}}
</a>
}
<button type="submit" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
</div>
</div>

View File

@ -3,10 +3,12 @@
<div class="row g-0">
<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>
<div *ngIf="isMyReview" class="my-review">
<i class="fa-solid fa-star" aria-hidden="true" [title]="t('your-review')"></i>
<span class="visually-hidden">{{t('your-review')}}</span>
</div>
@if (isMyReview) {
<div class="my-review">
<i class="fa-solid fa-star" aria-hidden="true" [title]="t('your-review')"></i>
<span class="visually-hidden">{{t('your-review')}}</span>
</div>
}
</div>
<div class="col-md-10">
<div class="card-body">
@ -21,17 +23,19 @@
<div class="card-footer bg-transparent text-muted">
<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>
<img class="me-1" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
{{review.username}}
</ng-container>
<ng-template #normalReview>
} @else {
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
</ng-template>
}
{{(isMyReview ? '' : review.username | defaultValue:'')}}
</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>

View File

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

View File

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

View File

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

View File

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

View File

@ -33,25 +33,54 @@
[backgroundColor]="backgroundColor"
[customToolbar]="multiToolbar"
[language]="user.preferences.locale"
[(scrollMode)]="scrollMode"
[pageViewMode]="pageLayoutMode"
[spread]="spreadMode"
(pageChange)="saveProgress()"
(pdfLoadingStarts)="updateLoading(true)"
(pdfLoaded)="updateLoading(false)"
(progress)="updateLoadProgress($event)"
(zoomChange)="calcScrollbarNeeded()"
>
</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>
<div style="min-height: 36px" id="toolbarViewer" [ngStyle]="{'background-color': backgroundColor, 'color': fontColor}">
<div id="toolbarViewerLeft">
<pdf-toggle-sidebar></pdf-toggle-sidebar>
<pdf-find-button></pdf-find-button>
<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>
<pdf-zoom-toolbar ></pdf-zoom-toolbar>
@if (utilityService.getActiveBreakpoint() > Breakpoint.Tablet) {
<pdf-zoom-toolbar ></pdf-zoom-toolbar>
}
<div id="toolbarViewerRight">
<pdf-hand-tool></pdf-hand-tool>
@ -59,42 +88,66 @@
<pdf-presentation-mode></pdf-presentation-mode>
<!-- This is not yet supported by the underlying library
<button (click)="toggleBookPageMode()" class="btn btn-icon toolbarButton">
<i class="toolbar-icon fa-solid {{this.bookMode !== '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>
</button> -->
@if (utilityService.getActiveBreakpoint() > Breakpoint.Mobile) {
<button (click)="toggleBookPageMode()" class="btn-icon toolbarButton" [ngbTooltip]="pageLayoutMode | pdfLayoutMode" [disabled]="scrollMode === ScrollModeType.page">
<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.pageLayoutMode | pdfLayoutMode}}</span>
</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>
<span class="visually-hidden">{{bookTitle}}</span>
<!-- scroll mode should be disabled when book mode is used -->
<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 *ngIf="incognitoMode" (click)="turnOffIncognito()" class="btn 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 (click)="toggleSpreadMode()" class="btn-icon toolbarButton" [ngbTooltip]="spreadMode | pdfSpreadMode" [disabled]="this.pageLayoutMode === 'book'">
@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>
<!-- 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>
<span class="visually-hidden">{{this.theme === 'light' ? t('light-theme-alt') : t('dark-theme-alt')}}</span>
</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>
<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>

View File

@ -2,6 +2,9 @@
font-size: 19px;
}
.btn-icon {
border: none;
}
.book-title {
margin: 8px 0 4px !important;
@ -24,3 +27,76 @@
// NOTE: We have to override due to theme variables not being available
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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, ElementRef,
HostListener,
inject, OnDestroy,
OnInit, ViewChild
Component,
ElementRef,
HostListener, inject, Inject,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { NgxExtendedPdfViewerService, PageViewModeType, ScrollModeType, ProgressBarEvent, NgxExtendedPdfViewerModule } from 'ngx-extended-pdf-viewer';
import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs';
import { BookService } from 'src/app/book-reader/_services/book.service';
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { Chapter } from 'src/app/_models/chapter';
import { User } from 'src/app/_models/user';
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 { NgIf, NgStyle, AsyncPipe } from '@angular/common';
import {ActivatedRoute, Router} from '@angular/router';
import {
NgxExtendedPdfViewerModule,
NgxExtendedPdfViewerService,
PageViewModeType,
ProgressBarEvent,
ScrollModeType
} from 'ngx-extended-pdf-viewer';
import {ToastrService} from 'ngx-toastr';
import {take} from 'rxjs';
import {BookService} from 'src/app/book-reader/_services/book.service';
import {Breakpoint, KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
import {Chapter} from 'src/app/_models/chapter';
import {User} from 'src/app/_models/user';
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 {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({
selector: 'app-pdf-reader',
@ -29,10 +45,26 @@ import {translate, TranslocoDirective} from "@ngneat/transloco";
styleUrls: ['./pdf-reader.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
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 {
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;
libraryId!: number;
@ -82,20 +114,13 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
* How much of the current document is loaded
*/
loadPercent: number = 0;
scrollbarNeeded = false;
/**
* This can't be updated dynamically:
* https://github.com/stephanrauh/ngx-extended-pdf-viewer/issues/1415
*/
bookMode: PageViewModeType = 'multiple';
pageLayoutMode: PageViewModeType = 'multiple';
scrollMode: ScrollModeType = ScrollModeType.vertical;
spreadMode: SpreadType = 'off';
constructor(private route: ActivatedRoute, private router: Router, public accountService: AccountService,
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) {
constructor(@Inject(DOCUMENT) private document: Document) {
this.navService.hideNavBar();
this.themeService.clearThemes();
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 {
this.themeService.currentTheme$.pipe(take(1)).subscribe(theme => {
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() {
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.volumeId = info.volumeId;
this.bookTitle = info.bookTitle;
@ -171,7 +267,7 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
}
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();
}
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() {
if (this.bookMode === 'book') {
this.bookMode = 'multiple';
if (this.pageLayoutMode === 'book') {
this.pageLayoutMode = 'multiple';
} 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();
}
@ -225,4 +343,16 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
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 { NgIf, NgFor, AsyncPipe } from '@angular/common';
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import {tap} from "rxjs/operators";
@Component({
selector: 'app-theme-manager',
@ -52,19 +53,11 @@ export class ThemeManagerComponent {
}
applyTheme(theme: SiteTheme) {
if (!this.user) return;
if (this.user) {
const pref = Object.assign({}, this.user.preferences);
pref.theme = theme;
this.accountService.updatePreferences(pref).subscribe(updatedPref => {
if (this.user) {
this.user.preferences = updatedPref;
}
this.themeService.setTheme(theme.name);
this.cdRef.markForCheck();
});
}
const pref = Object.assign({}, this.user.preferences);
pref.theme = theme;
this.accountService.updatePreferences(pref).subscribe();
}
updateDefault(theme: SiteTheme) {

View File

@ -1,152 +1,36 @@
<ng-container *transloco="let t; read:'user-preferences'">
<app-side-nav-companion-bar>
<h2 title>
{{t('title')}}
</h2>
<h2 title>
{{t('title')}}
</h2>
</app-side-nav-companion-bar>
<div class="container-fluid">
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ t(tab.title) }}</a>
<ng-template ngbNavContent>
@defer (when tab.fragment === FragmentID.Account; prefetch on idle) {
<app-change-email></app-change-email>
<app-change-password></app-change-password>
<app-change-age-restriction></app-change-age-restriction>
<app-manage-scrobbling-providers></app-manage-scrobbling-providers>
}
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ t(tab.title) }}</a>
<ng-template ngbNavContent>
@defer (when tab.fragment === FragmentID.Account; prefetch on idle) {
<app-change-email></app-change-email>
<app-change-password></app-change-password>
<app-change-age-restriction></app-change-age-restriction>
<app-manage-scrobbling-providers></app-manage-scrobbling-providers>
}
@defer (when tab.fragment === FragmentID.Preferences; prefetch on idle) {
<ng-container *ngIf="tab.fragment === FragmentID.Preferences">
<p>
{{t('pref-description')}}
</p>
@defer (when tab.fragment === FragmentID.Preferences; prefetch on idle) {
<ng-container *ngIf="tab.fragment === FragmentID.Preferences">
<p>
{{t('pref-description')}}
</p>
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
<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>
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
<div ngbAccordion [closeOthers]="true" #acc="ngbAccordion">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label>
<i class="fa fa-info-circle ms-1"
aria-hidden="true" placement="right" [ngbTooltip]="localeTooltip" role="button" tabindex="0"></i>
<ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-locale-help">
<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')}}
<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>
@ -154,67 +38,241 @@
<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>
<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">
<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="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
[ngbTooltip]="localeTooltip" role="button" tabindex="0"></i>
<ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-locale-help">
<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">
<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>
<span class="visually-hidden" id="settings-pagesplit-option-help">
<ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
<select class="form-select" aria-describedby="manga-header"
formControlName="pageSplitOption" id="settings-pagesplit-option">
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select>
</div>
<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>
<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>
<label for="settings-readingmode-option"
class="form-label">{{t('reading-mode-label')}}</label>
<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>
</div>
</div>
<div class="row g-0">
<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>
<span class="visually-hidden" id="settings-layoutmode-option-help">
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode"
id="settings-layoutmode-option">
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select>
</div>
<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>
<input [value]="user!.preferences!.backgroundColor"
class="form-control"
id="settings-background-color-option"
(colorPickerChange)="handleBackgroundColorChange()"
[style.background]="user!.preferences!.backgroundColor"
[cpAlphaChannel]="'disabled'"
[(colorPicker)]="user!.preferences!.backgroundColor"/>
<label for="settings-background-color-option"
class="form-label">{{t('background-color-label')}}</label>
<input [value]="user!.preferences!.backgroundColor" class="form-control"
id="settings-background-color-option" (colorPickerChange)="handleBackgroundColorChange()"
[style.background]="user!.preferences!.backgroundColor" [cpAlphaChannel]="'disabled'"
[(colorPicker)]="user!.preferences!.backgroundColor" />
</div>
</div>
@ -222,7 +280,8 @@
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<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>
</div>
</div>
@ -230,8 +289,11 @@
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<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">
<label class="form-check-label" for="show-screen-hints">{{t('show-screen-hints-label')}}</label>
<input type="checkbox" id="show-screen-hints" role="switch"
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>
@ -241,24 +303,36 @@
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook" 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>
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook"
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 class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="swipe-to-paginate" role="switch" 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>
<input type="checkbox" id="swipe-to-paginate" role="switch"
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 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>
<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>
@ -267,20 +341,26 @@
<div ngbAccordionItem [id]="AccordionPanelID.BookReader">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
{{t('book-reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="taptopaginate-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<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>
<button class="accordion-button" ngbAccordionButton type="button"
[attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
{{t('book-reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="taptopaginate-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<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>
<span class="visually-hidden" id="settings-taptopaginate-option-help">
<ng-container [ngTemplateOutlet]="tapToPaginateOptionTooltip"></ng-container>
@ -292,8 +372,13 @@
<label id="immersivemode-label" class="form-label"></label>
<div class="mb-3">
<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">
<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>
<input type="checkbox" role="switch" id="immersivemode"
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>
<span class="visually-hidden" id="settings-immersivemode-option-help">
<ng-container [ngTemplateOutlet]="immersivemodeOptionTooltip"></ng-container>
@ -305,24 +390,35 @@
<div class="row g-0">
<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>
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}</ng-template>
<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>
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}
</ng-template>
<span class="visually-hidden" id="settings-book-reading-direction-book-help">
<ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container>
</span>
<select id="settings-book-reading-direction" class="form-select" aria-describedby="settings-book-reading-direction-help" formControlName="bookReaderReadingDirection">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
<select id="settings-book-reading-direction" class="form-select"
aria-describedby="settings-book-reading-direction-help"
formControlName="bookReaderReadingDirection">
<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-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>
<span class="visually-hidden" id="settings-fontfamily-option-help">
<ng-container [ngTemplateOutlet]="fontFamilyOptionTooltip"></ng-container>
</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>
</select>
</div>
@ -330,24 +426,35 @@
<div class="row g-0">
<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>
<span class="visually-hidden" id="settings-book-writing-style-help">
<ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-writing-style-help" formControlName="bookReaderWritingStyle" id="settings-book-writing-style" >
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
<select class="form-select" aria-describedby="settings-book-writing-style-help"
formControlName="bookReaderWritingStyle" id="settings-book-writing-style">
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select>
</div>
<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>
<span class="visually-hidden" id="settings-book-layout-mode-help">
<ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
<select class="form-select" aria-describedby="settings-book-layout-mode-help"
formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">
{{opt.text | titlecase}}</option>
</select>
</div>
</div>
@ -355,13 +462,18 @@
<div class="row g-0">
<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>
<span class="visually-hidden" id="settings-color-theme-option-help">
<ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">{{opt.name | titlecase}}</option>
<select class="form-select" aria-describedby="settings-color-theme-option-help"
formControlName="bookReaderThemeName" id="settings-color-theme-option">
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">
{{opt.name | titlecase}}</option>
</select>
</div>
</div>
@ -369,79 +481,181 @@
<div class="row g-0">
<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>
<input type="range" class="form-range" id="fontsize"
min="50" max="300" step="10" formControlName="bookReaderFontSize">
<input type="range" class="form-range" id="fontsize" min="50" max="300" step="10"
formControlName="bookReaderFontSize">
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<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>
<span class="visually-hidden" id="settings-booklineheight-option-help">
<ng-container [ngTemplateOutlet]="bookLineHeightOptionTooltip"></ng-container>
</span>
</div>
<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>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<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>
<span class="visually-hidden" id="settings-bookmargin-option-help">
<ng-container [ngTemplateOutlet]="bookReaderMarginOptionTooltip"></ng-container>
</span>
</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>
</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>
<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.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>
</ng-template>
</div>
</div>
</div>
</div>
</form>
</ng-container>
}
</form>
</ng-container>
}
@defer (when tab.fragment === FragmentID.Clients; prefetch on idle) {
<div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">{{t('clients-opds-alert')}}</div>
<p>{{t('clients-opds-description')}}</p>
<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>
}
@defer (when tab.fragment === FragmentID.Theme; prefetch on idle) {
<app-theme-manager></app-theme-manager>
}
@defer (when tab.fragment === FragmentID.Clients; prefetch on idle) {
<div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">{{t('clients-opds-alert')}}</div>
<p>{{t('clients-opds-description')}}</p>
<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>
}
@defer (when tab.fragment === FragmentID.Theme; prefetch on idle) {
<app-theme-manager></app-theme-manager>
}
@defer (when tab.fragment === FragmentID.Devices; prefetch on idle) {
<app-manage-devices></app-manage-devices>
}
@defer (when tab.fragment === FragmentID.Devices; prefetch on idle) {
<app-manage-devices></app-manage-devices>
}
@defer (when tab.fragment === FragmentID.Stats; prefetch on idle) {
<app-user-stats></app-user-stats>
}
@defer (when tab.fragment === FragmentID.Stats; prefetch on idle) {
<app-user-stats></app-user-stats>
}
@defer (when tab.fragment === FragmentID.Scrobbling; prefetch on idle) {
@if(hasActiveLicense) {
<app-user-scrobble-history></app-user-scrobble-history>
<app-user-holds></app-user-holds>
}
}
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="mt-3"></div>
@defer (when tab.fragment === FragmentID.Scrobbling; prefetch on idle) {
@if(hasActiveLicense) {
<app-user-scrobble-history></app-user-scrobble-history>
<app-user-holds></app-user-holds>
}
}
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="mt-3"></div>
</div>
</ng-container>

View File

@ -7,54 +7,82 @@ import {
OnDestroy,
OnInit
} from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {ToastrService} from 'ngx-toastr';
import {take} from 'rxjs/operators';
import { Title } from '@angular/platform-browser';
import {Title} from '@angular/platform-browser';
import {
readingDirections,
scalingOptions,
pageSplitOptions,
readingModes,
Preferences,
bookLayoutModes,
bookWritingStyles,
layoutModes,
pageLayoutModes,
bookWritingStyles
pageSplitOptions,
pdfLayoutModes,
pdfScrollModes,
pdfSpreadModes,
pdfThemes,
Preferences,
readingDirections,
readingModes,
scalingOptions
} from 'src/app/_models/preferences/preferences';
import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { SettingsService } from 'src/app/admin/settings.service';
import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode';
import {User} from 'src/app/_models/user';
import {AccountService} from 'src/app/_services/account.service';
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
import {SettingsService} from 'src/app/admin/settings.service';
import {BookPageLayoutMode} from 'src/app/_models/readers/book-page-layout-mode';
import {forkJoin} from 'rxjs';
import { bookColorThemes } from 'src/app/book-reader/_components/reader-settings/reader-settings.component';
import { BookService } from 'src/app/book-reader/_services/book.service';
import {bookColorThemes} from 'src/app/book-reader/_components/reader-settings/reader-settings.component';
import {BookService} from 'src/app/book-reader/_services/book.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe';
import { UserHoldsComponent } from '../user-holds/user-holds.component';
import { UserScrobbleHistoryComponent } from '../../_single-module/user-scrobble-history/user-scrobble-history.component';
import { UserStatsComponent } from '../../statistics/_components/user-stats/user-stats.component';
import { ManageDevicesComponent } from '../manage-devices/manage-devices.component';
import { ThemeManagerComponent } from '../theme-manager/theme-manager.component';
import { ApiKeyComponent } from '../api-key/api-key.component';
import { ColorPickerModule } from 'ngx-color-picker';
import { ChangeAgeRestrictionComponent } from '../change-age-restriction/change-age-restriction.component';
import { ChangePasswordComponent } from '../change-password/change-password.component';
import { ChangeEmailComponent } from '../change-email/change-email.component';
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 { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {SentenceCasePipe} from '../../_pipes/sentence-case.pipe';
import {UserHoldsComponent} from '../user-holds/user-holds.component';
import {UserScrobbleHistoryComponent} from '../../_single-module/user-scrobble-history/user-scrobble-history.component';
import {UserStatsComponent} from '../../statistics/_components/user-stats/user-stats.component';
import {ManageDevicesComponent} from '../manage-devices/manage-devices.component';
import {ThemeManagerComponent} from '../theme-manager/theme-manager.component';
import {ApiKeyComponent} from '../api-key/api-key.component';
import {ColorPickerModule} from 'ngx-color-picker';
import {ChangeAgeRestrictionComponent} from '../change-age-restriction/change-age-restriction.component';
import {ChangePasswordComponent} from '../change-password/change-password.component';
import {ChangeEmailComponent} from '../change-email/change-email.component';
import {NgFor, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
import {
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 {Language} from "../../_models/metadata/language";
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {LoadingComponent} from "../../shared/loading/loading.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 {
ImageReader = 'image-reader',
BookReader = 'book-reader',
GlobalSettings = 'global-settings'
GlobalSettings = 'global-settings',
PdfReader = 'pdf-reader'
}
enum FragmentID {
@ -77,7 +105,7 @@ enum FragmentID {
ChangePasswordComponent, ChangeAgeRestrictionComponent, ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader,
NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, ColorPickerModule, ApiKeyComponent,
ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe,
TranslocoDirective, LoadingComponent, ManageScrobblingProvidersComponent],
TranslocoDirective, LoadingComponent, ManageScrobblingProvidersComponent, PdfLayoutModePipe],
})
export class UserPreferencesComponent implements OnInit, OnDestroy {
@ -108,6 +136,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
pageLayoutModesTranslated = pageLayoutModes.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({});
user: User | undefined = undefined;
@ -129,7 +162,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
opdsUrl: string = '';
makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; };
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('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, []));
this.settingsForm.addControl('bookReaderWritingStyle', new FormControl(this.user.preferences.bookReaderWritingStyle, []))
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(!!this.user.preferences.bookReaderTapToPaginate, []));
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.user.preferences.bookReaderTapToPaginate, []));
this.settingsForm.addControl('bookReaderLayoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
this.settingsForm.addControl('bookReaderThemeName', new FormControl(this.user?.preferences.bookReaderThemeName || bookColorThemes[0].name, []));
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user?.preferences.bookReaderImmersiveMode, []));
this.settingsForm.addControl('pdfTheme', new FormControl(this.user?.preferences.pdfTheme || PdfTheme.Dark, []));
this.settingsForm.addControl('pdfScrollMode', new FormControl(this.user?.preferences.pdfScrollMode || PdfScrollMode.Vertical, []));
this.settingsForm.addControl('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('globalPageLayoutMode', new FormControl(this.user.preferences.globalPageLayoutMode, []));
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('shareReviews')?.setValue(this.user.preferences.shareReviews);
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.settingsForm.markAsPristine();
}
@ -313,7 +356,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
swipeToPaginate: modelSettings.swipeToPaginate,
collapseSeriesRelationships: modelSettings.collapseSeriesRelationships,
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) => {

View File

@ -159,6 +159,15 @@
"margin-book-label": "Margin",
"margin-book-tooltip": "How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.",
"pdf-reader-settings-title": "PDF Reader",
"pdf-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-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.",
@ -1600,7 +1609,28 @@
"incognito-mode": "Incognito Mode",
"light-theme-alt": "Light 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": {
@ -2189,7 +2219,17 @@
"2-column": "2 Column",
"cards": "Cards",
"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`.
// The list of file replacements can be found in `angular.json`.
const IP = 'localhost';
export const environment = {
production: false,
apiUrl: 'http://localhost:5000/api/',
hubUrl: 'http://localhost:5000/hubs/',
apiUrl: 'http://' + IP + ':5000/api/',
hubUrl: 'http://'+ IP + ':5000/hubs/',
buyLink: 'https://buy.stripe.com/test_9AQ5mi058h1PcIo3cf?prefilled_promo_code=FREETRIAL',
manageLink: 'https://billing.stripe.com/p/login/test_14kfZocuh6Tz5ag7ss'
};

View File

@ -10,6 +10,8 @@ export class HttpLoader implements TranslocoLoader {
getTranslation(langPath: string) {
const tokens = langPath.split('/');
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 {LazyLoadImageModule} from "ng-lazyload-image";
import {getSaver, SAVER} from "./app/_providers/saver.provider";
import {distinctUntilChanged} from "rxjs/operators";
const disableAnimations = !('animate' in document.documentElement);
export function preloadUser(userService: AccountService, transloco: TranslocoService) {
return function() {
return userService.currentUser$.pipe(switchMap((user) => {
return userService.currentUser$.pipe(distinctUntilChanged(), switchMap((user) => {
if (user && user.preferences.locale) {
transloco.setActiveLang(user.preferences.locale);
return transloco.load(user.preferences.locale)

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.14.9"
"version": "0.7.14.10"
},
"servers": [
{
@ -13300,9 +13300,6 @@
"description": "Book Reader Option: Defines the writing styles vertical/horizontal",
"format": "int32"
},
"theme": {
"$ref": "#/components/schemas/SiteTheme"
},
"bookThemeName": {
"type": "string",
"description": "Book Reader Option: The color theme to decorate the book contents",
@ -13322,6 +13319,47 @@
"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."
},
"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": {
"enum": [
0,
@ -20626,6 +20664,10 @@
"locale",
"noTransitions",
"pageSplitOption",
"pdfLayoutMode",
"pdfScrollMode",
"pdfSpreadMode",
"pdfTheme",
"promptForDownloadSize",
"readerMode",
"readingDirection",
@ -20804,6 +20846,44 @@
"minLength": 1,
"type": "string",
"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