mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
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:
parent
f22f30b5a9
commit
2bde0ac82a
15
.sonarcloud.properties
Normal file
15
.sonarcloud.properties
Normal 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=
|
@ -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>
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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>
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
2916
API/Data/Migrations/20240328130057_PdfSettings.Designer.cs
generated
Normal file
2916
API/Data/Migrations/20240328130057_PdfSettings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
62
API/Data/Migrations/20240328130057_PdfSettings.cs
Normal file
62
API/Data/Migrations/20240328130057_PdfSettings.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
||||
|
@ -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; }
|
||||
}
|
||||
|
21
API/Entities/Enums/UserPreferences/PdfBookMode.cs
Normal file
21
API/Entities/Enums/UserPreferences/PdfBookMode.cs
Normal 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
|
||||
}
|
21
API/Entities/Enums/UserPreferences/PdfScrollMode.cs
Normal file
21
API/Entities/Enums/UserPreferences/PdfScrollMode.cs
Normal 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
|
||||
}
|
13
API/Entities/Enums/UserPreferences/PdfSpreadMode.cs
Normal file
13
API/Entities/Enums/UserPreferences/PdfSpreadMode.cs
Normal 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
|
||||
}
|
11
API/Entities/Enums/UserPreferences/PdfTheme.cs
Normal file
11
API/Entities/Enums/UserPreferences/PdfTheme.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace API.Entities.Enums.UserPreferences;
|
||||
|
||||
public enum PdfTheme
|
||||
{
|
||||
[Description("Dark")]
|
||||
Dark = 0,
|
||||
[Description("Light")]
|
||||
Light = 1
|
||||
}
|
@ -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>
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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]));
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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)?)?)",
|
||||
|
@ -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
|
||||
|
@ -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 ###
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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)) {
|
||||
|
@ -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",
|
||||
|
6
UI/Web/src/app/_models/preferences/pdf-layout-mode.ts
Normal file
6
UI/Web/src/app/_models/preferences/pdf-layout-mode.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum PdfLayoutMode {
|
||||
Multiple = 0,
|
||||
Single = 1,
|
||||
Book = 2,
|
||||
InfiniteScroll = 3
|
||||
}
|
6
UI/Web/src/app/_models/preferences/pdf-scroll-mode.ts
Normal file
6
UI/Web/src/app/_models/preferences/pdf-scroll-mode.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum PdfScrollMode {
|
||||
Vertical = 0,
|
||||
Horizontal = 1,
|
||||
Wrapped = 2,
|
||||
Page = 3
|
||||
}
|
5
UI/Web/src/app/_models/preferences/pdf-spread-mode.ts
Normal file
5
UI/Web/src/app/_models/preferences/pdf-spread-mode.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum PdfSpreadMode {
|
||||
None = 0,
|
||||
Odd = 1,
|
||||
Even = 2
|
||||
}
|
4
UI/Web/src/app/_models/preferences/pdf-theme.ts
Normal file
4
UI/Web/src/app/_models/preferences/pdf-theme.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum PdfTheme{
|
||||
Dark = 0,
|
||||
Light = 1
|
||||
}
|
@ -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}];
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -9,6 +9,6 @@ export interface UserReview {
|
||||
tagline?: string;
|
||||
isExternal: boolean;
|
||||
bodyJustText?: string;
|
||||
externalUrl?: string;
|
||||
siteUrl?: string;
|
||||
provider: ScrobbleProvider;
|
||||
}
|
||||
|
@ -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]) {
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
26
UI/Web/src/app/pdf-reader/_pipe/pdf-layout-mode.pipe.ts
Normal file
26
UI/Web/src/app/pdf-reader/_pipe/pdf-layout-mode.pipe.ts
Normal 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');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
24
UI/Web/src/app/pdf-reader/_pipe/pdf-scroll-mode.pipe.ts
Normal file
24
UI/Web/src/app/pdf-reader/_pipe/pdf-scroll-mode.pipe.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
24
UI/Web/src/app/pdf-reader/_pipe/pdf-spread-mode.pipe.ts
Normal file
24
UI/Web/src/app/pdf-reader/_pipe/pdf-spread-mode.pipe.ts
Normal 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');
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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) => {
|
||||
|
@ -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"
|
||||
},
|
||||
|
||||
|
||||
|
@ -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'
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
88
openapi.json
88
openapi.json
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user