mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-03 21:54:47 -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.EntityFrameworkCore.InMemory" Version="8.0.3" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.28" />
|
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.0.2" />
|
||||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.28" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.0.2" />
|
||||||
<PackageReference Include="xunit" Version="2.7.0" />
|
<PackageReference Include="xunit" Version="2.7.0" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
@ -78,6 +78,8 @@ public class ComicParsingTests
|
|||||||
[InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")]
|
[InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")]
|
||||||
[InlineData("Kebab Том 1 Глава 1", "Kebab")]
|
[InlineData("Kebab Том 1 Глава 1", "Kebab")]
|
||||||
[InlineData("Манга Глава 1", "Манга")]
|
[InlineData("Манга Глава 1", "Манга")]
|
||||||
|
[InlineData("ReZero รีเซทชีวิต ฝ่าวิกฤตต่างโลก เล่ม 1", "ReZero รีเซทชีวิต ฝ่าวิกฤตต่างโลก")]
|
||||||
|
[InlineData("SKY WORLD สกายเวิลด์ เล่มที่ 1", "SKY WORLD สกายเวิลด์")]
|
||||||
public void ParseComicSeriesTest(string filename, string expected)
|
public void ParseComicSeriesTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename));
|
||||||
@ -129,6 +131,9 @@ public class ComicParsingTests
|
|||||||
// Russian Tests
|
// Russian Tests
|
||||||
[InlineData("Kebab Том 1 Глава 3", "1")]
|
[InlineData("Kebab Том 1 Глава 3", "1")]
|
||||||
[InlineData("Манга Глава 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
|
[InlineData("Манга Глава 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
|
||||||
|
[InlineData("ย้อนเวลากลับมาร้าย เล่ม 1", "1")]
|
||||||
|
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "1")]
|
||||||
|
[InlineData("วิวาห์รัก เดิมพันชีวิต ตอนที่ 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
|
||||||
public void ParseComicVolumeTest(string filename, string expected)
|
public void ParseComicVolumeTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(filename));
|
||||||
@ -178,6 +183,9 @@ public class ComicParsingTests
|
|||||||
[InlineData("Манга Глава 2", "2")]
|
[InlineData("Манга Глава 2", "2")]
|
||||||
[InlineData("Манга 2 Глава", "2")]
|
[InlineData("Манга 2 Глава", "2")]
|
||||||
[InlineData("Манга Том 1 2 Глава", "2")]
|
[InlineData("Манга Том 1 2 Глава", "2")]
|
||||||
|
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")]
|
||||||
|
[InlineData("Max Level Returner ตอนที่ 5", "5")]
|
||||||
|
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
|
||||||
public void ParseComicChapterTest(string filename, string expected)
|
public void ParseComicChapterTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(filename));
|
||||||
|
@ -207,6 +207,9 @@ public class MangaParsingTests
|
|||||||
[InlineData("test 2 years 1화", "test 2 years")]
|
[InlineData("test 2 years 1화", "test 2 years")]
|
||||||
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake", "Nagasarete Airantou")]
|
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake", "Nagasarete Airantou")]
|
||||||
[InlineData("Cynthia The Mission - c000 - c006 (v06)", "Cynthia The Mission")]
|
[InlineData("Cynthia The Mission - c000 - c006 (v06)", "Cynthia The Mission")]
|
||||||
|
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1", "เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท")]
|
||||||
|
[InlineData("Max Level Returner เล่มที่ 5", "Max Level Returner")]
|
||||||
|
[InlineData("หนึ่งความคิด นิจนิรันดร์ เล่ม 2", "หนึ่งความคิด นิจนิรันดร์")]
|
||||||
public void ParseSeriesTest(string filename, string expected)
|
public void ParseSeriesTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
|
||||||
@ -296,6 +299,9 @@ public class MangaParsingTests
|
|||||||
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")]
|
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")]
|
||||||
[InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")]
|
[InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")]
|
||||||
[InlineData("Adabana c00-02", "0-2")]
|
[InlineData("Adabana c00-02", "0-2")]
|
||||||
|
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")]
|
||||||
|
[InlineData("Max Level Returner ตอนที่ 5", "5")]
|
||||||
|
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
|
||||||
public void ParseChaptersTest(string filename, string expected)
|
public void ParseChaptersTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
<PackageReference Include="Hangfire.InMemory" Version="0.8.1" />
|
<PackageReference Include="Hangfire.InMemory" Version="0.8.1" />
|
||||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.1" />
|
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.1" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.59" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.60" />
|
||||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.11" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
@ -81,8 +81,8 @@
|
|||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
||||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||||
<PackageReference Include="NetVips" Version="2.4.0" />
|
<PackageReference Include="NetVips" Version="2.4.1" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.15.1" />
|
<PackageReference Include="NetVips.Native" Version="8.15.2" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.2.0" />
|
<PackageReference Include="NReco.Logging.File" Version="1.2.0" />
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||||
@ -95,14 +95,14 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.36.0" />
|
<PackageReference Include="SharpCompress" Version="0.36.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.21.0.86780">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.23.0.88079">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.1" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.5.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="20.0.28" />
|
<PackageReference Include="System.IO.Abstractions" Version="21.0.2" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.3" />
|
<PackageReference Include="System.Drawing.Common" Version="8.0.3" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
|
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs.Uploads;
|
using API.DTOs.Uploads;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
@ -98,6 +99,7 @@ public class UploadController : BaseApiController
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
|
||||||
|
|
||||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
|
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
|
||||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
|
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
|
||||||
|
|
||||||
@ -225,17 +227,14 @@ public class UploadController : BaseApiController
|
|||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-reading-list-save"));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-reading-list-save"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename, int thumbnailSize = 0)
|
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename)
|
||||||
{
|
{
|
||||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
if (thumbnailSize > 0)
|
var encodeFormat = settings.EncodeMediaAs;
|
||||||
{
|
var coverImageSize = settings.CoverImageSize;
|
||||||
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
|
|
||||||
filename, encodeFormat, thumbnailSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
|
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
|
||||||
filename, encodeFormat);
|
filename, encodeFormat, coverImageSize.GetDimensions().Width);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -326,8 +325,7 @@ public class UploadController : BaseApiController
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var filePath = await CreateThumbnail(uploadFileDto,
|
var filePath = await CreateThumbnail(uploadFileDto,
|
||||||
$"{ImageService.GetLibraryFormat(uploadFileDto.Id)}",
|
$"{ImageService.GetLibraryFormat(uploadFileDto.Id)}");
|
||||||
ImageService.LibraryThumbnailWidth);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(filePath))
|
if (!string.IsNullOrEmpty(filePath))
|
||||||
{
|
{
|
||||||
|
@ -118,6 +118,12 @@ public class UsersController : BaseApiController
|
|||||||
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
||||||
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
|
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
|
||||||
existingPreferences.ShareReviews = preferencesDto.ShareReviews;
|
existingPreferences.ShareReviews = preferencesDto.ShareReviews;
|
||||||
|
|
||||||
|
existingPreferences.PdfTheme = preferencesDto.PdfTheme;
|
||||||
|
existingPreferences.PdfLayoutMode = preferencesDto.PdfLayoutMode;
|
||||||
|
existingPreferences.PdfScrollMode = preferencesDto.PdfScrollMode;
|
||||||
|
existingPreferences.PdfSpreadMode = preferencesDto.PdfSpreadMode;
|
||||||
|
|
||||||
if (_localizationService.GetLocales().Contains(preferencesDto.Locale))
|
if (_localizationService.GetLocales().Contains(preferencesDto.Locale))
|
||||||
{
|
{
|
||||||
existingPreferences.Locale = preferencesDto.Locale;
|
existingPreferences.Locale = preferencesDto.Locale;
|
||||||
|
@ -152,4 +152,25 @@ public class UserPreferencesDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public string Locale { get; set; }
|
public string Locale { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF Reader: Theme of the Reader
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark;
|
||||||
|
/// <summary>
|
||||||
|
/// PDF Reader: Scroll mode of the reader
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical;
|
||||||
|
/// <summary>
|
||||||
|
/// PDF Reader: Layout Mode of the reader
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public PdfLayoutMode PdfLayoutMode { get; set; } = PdfLayoutMode.Multiple;
|
||||||
|
/// <summary>
|
||||||
|
/// PDF Reader: Spread Mode of the reader
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None;
|
||||||
}
|
}
|
||||||
|
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")
|
b.Property<int>("PageSplitOption")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PdfLayoutMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PdfScrollMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PdfSpreadMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PdfTheme")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<bool>("PromptForDownloadSize")
|
b.Property<bool>("PromptForDownloadSize")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
@ -7,6 +7,9 @@ namespace API.Entities;
|
|||||||
public class AppUserPreferences
|
public class AppUserPreferences
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
#region MangaReader
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manga Reader Option: What direction should the next/prev page buttons go
|
/// Manga Reader Option: What direction should the next/prev page buttons go
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -51,6 +54,11 @@ public class AppUserPreferences
|
|||||||
/// Manga Reader Option: Should swiping trigger pagination
|
/// Manga Reader Option: Should swiping trigger pagination
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SwipeToPaginate { get; set; }
|
public bool SwipeToPaginate { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region EpubReader
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Book Reader Option: Override extra Margin
|
/// Book Reader Option: Override extra Margin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -75,17 +83,11 @@ public class AppUserPreferences
|
|||||||
/// Book Reader Option: What direction should the next/prev page buttons go
|
/// Book Reader Option: What direction should the next/prev page buttons go
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight;
|
public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Book Reader Option: Defines the writing styles vertical/horizontal
|
/// Book Reader Option: Defines the writing styles vertical/horizontal
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public WritingStyle BookReaderWritingStyle { get; set; } = WritingStyle.Horizontal;
|
public WritingStyle BookReaderWritingStyle { get; set; } = WritingStyle.Horizontal;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UI Site Global Setting: The UI theme the user should use.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>Should default to Dark</remarks>
|
|
||||||
public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0];
|
|
||||||
/// <summary>
|
|
||||||
/// Book Reader Option: The color theme to decorate the book contents
|
/// Book Reader Option: The color theme to decorate the book contents
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Should default to Dark</remarks>
|
/// <remarks>Should default to Dark</remarks>
|
||||||
@ -101,6 +103,37 @@ public class AppUserPreferences
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Defaults to false</remarks>
|
/// <remarks>Defaults to false</remarks>
|
||||||
public bool BookReaderImmersiveMode { get; set; } = false;
|
public bool BookReaderImmersiveMode { get; set; } = false;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region PdfReader
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF Reader: Theme of the Reader
|
||||||
|
/// </summary>
|
||||||
|
public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark;
|
||||||
|
/// <summary>
|
||||||
|
/// PDF Reader: Scroll mode of the reader
|
||||||
|
/// </summary>
|
||||||
|
public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical;
|
||||||
|
/// <summary>
|
||||||
|
/// PDF Reader: Layout Mode of the reader
|
||||||
|
/// </summary>
|
||||||
|
public PdfLayoutMode PdfLayoutMode { get; set; } = PdfLayoutMode.Multiple;
|
||||||
|
/// <summary>
|
||||||
|
/// PDF Reader: Spread Mode of the reader
|
||||||
|
/// </summary>
|
||||||
|
public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None;
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Global
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI Site Global Setting: The UI theme the user should use.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Should default to Dark</remarks>
|
||||||
|
public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0];
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Global Site Option: If the UI should layout items as Cards or List items
|
/// Global Site Option: If the UI should layout items as Cards or List items
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -132,6 +165,8 @@ public class AppUserPreferences
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Locale { get; set; }
|
public string Locale { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public AppUser AppUser { get; set; } = null!;
|
public AppUser AppUser { get; set; } = null!;
|
||||||
public int AppUserId { get; set; }
|
public int AppUserId { get; set; }
|
||||||
}
|
}
|
||||||
|
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 System.Linq;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
|
using API.Helpers.Builders;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
|
||||||
namespace API.Extensions;
|
namespace API.Extensions;
|
||||||
@ -24,6 +25,7 @@ public static class ChapterListExtensions
|
|||||||
/// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info
|
/// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info
|
||||||
/// is <see cref="ParserInfo.IsSpecial"/> then, the filename is used to search against Range or if filename exists within Files of said Chapter.
|
/// is <see cref="ParserInfo.IsSpecial"/> then, the filename is used to search against Range or if filename exists within Files of said Chapter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>This uses GetNumberTitle() to calculate the Range to compare against the info.Chapters</remarks>
|
||||||
/// <param name="chapters"></param>
|
/// <param name="chapters"></param>
|
||||||
/// <param name="info"></param>
|
/// <param name="info"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@ -31,9 +33,12 @@ public static class ChapterListExtensions
|
|||||||
{
|
{
|
||||||
var normalizedPath = Parser.NormalizePath(info.FullFilePath);
|
var normalizedPath = Parser.NormalizePath(info.FullFilePath);
|
||||||
var specialTreatment = info.IsSpecialInfo();
|
var specialTreatment = info.IsSpecialInfo();
|
||||||
|
// NOTE: This can fail to find the chapter when Range is "1.0" as the chapter will store it as "1" hence why we need to emulate a Chapter
|
||||||
|
var fakeChapter = new ChapterBuilder(info.Chapters, info.Chapters).Build();
|
||||||
|
fakeChapter.UpdateFrom(info);
|
||||||
return specialTreatment
|
return specialTreatment
|
||||||
? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath))
|
? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath))
|
||||||
: chapters.FirstOrDefault(c => c.Range == info.Chapters);
|
: chapters.FirstOrDefault(c => c.Range == fakeChapter.GetNumberTitle());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -164,7 +164,9 @@ public static class IncludesExtensions
|
|||||||
|
|
||||||
if (includeFlags.HasFlag(AppUserIncludes.UserPreferences))
|
if (includeFlags.HasFlag(AppUserIncludes.UserPreferences))
|
||||||
{
|
{
|
||||||
query = query.Include(u => u.UserPreferences);
|
query = query
|
||||||
|
.Include(u => u.UserPreferences)
|
||||||
|
.ThenInclude(p => p.Theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeFlags.HasFlag(AppUserIncludes.WantToRead))
|
if (includeFlags.HasFlag(AppUserIncludes.WantToRead))
|
||||||
|
@ -36,7 +36,7 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||||||
var specialTitle = specialTreatment ? Parser.RemoveExtensionIfSupported(info.Filename) : info.Chapters;
|
var specialTitle = specialTreatment ? Parser.RemoveExtensionIfSupported(info.Filename) : info.Chapters;
|
||||||
var builder = new ChapterBuilder(Parser.DefaultChapter);
|
var builder = new ChapterBuilder(Parser.DefaultChapter);
|
||||||
|
|
||||||
return builder.WithNumber(Parser.RemoveExtensionIfSupported(info.Chapters))
|
return builder.WithNumber(Parser.RemoveExtensionIfSupported(info.Chapters)!)
|
||||||
.WithRange(specialTreatment ? info.Filename : info.Chapters)
|
.WithRange(specialTreatment ? info.Filename : info.Chapters)
|
||||||
.WithTitle((specialTreatment && info.Format == MangaFormat.Epub)
|
.WithTitle((specialTreatment && info.Format == MangaFormat.Epub)
|
||||||
? info.Title
|
? info.Title
|
||||||
|
@ -382,7 +382,7 @@ public class BookService : IBookService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link");
|
var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link[@href]");
|
||||||
if (styleNodes != null)
|
if (styleNodes != null)
|
||||||
{
|
{
|
||||||
foreach (var styleLinks in styleNodes)
|
foreach (var styleLinks in styleNodes)
|
||||||
|
@ -148,14 +148,14 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
|
|
||||||
private void OnChanged(object sender, FileSystemEventArgs e)
|
private void OnChanged(object sender, FileSystemEventArgs e)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("[LibraryWatcher] Changed: {FullPath}, {Name}, {ChangeType}", e.FullPath, e.Name, e.ChangeType);
|
_logger.LogTrace("[LibraryWatcher] Changed: {FullPath}, {Name}, {ChangeType}", e.FullPath, e.Name, e.ChangeType);
|
||||||
if (e.ChangeType != WatcherChangeTypes.Changed) return;
|
if (e.ChangeType != WatcherChangeTypes.Changed) return;
|
||||||
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name))));
|
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCreated(object sender, FileSystemEventArgs e)
|
private void OnCreated(object sender, FileSystemEventArgs e)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("[LibraryWatcher] Created: {FullPath}, {Name}", e.FullPath, e.Name);
|
_logger.LogTrace("[LibraryWatcher] Created: {FullPath}, {Name}", e.FullPath, e.Name);
|
||||||
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, !_directoryService.FileSystem.File.Exists(e.Name)));
|
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, !_directoryService.FileSystem.File.Exists(e.Name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
private void OnDeleted(object sender, FileSystemEventArgs e) {
|
private void OnDeleted(object sender, FileSystemEventArgs e) {
|
||||||
var isDirectory = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name));
|
var isDirectory = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name));
|
||||||
if (!isDirectory) return;
|
if (!isDirectory) return;
|
||||||
_logger.LogDebug("[LibraryWatcher] Deleted: {FullPath}, {Name}", e.FullPath, e.Name);
|
_logger.LogTrace("[LibraryWatcher] Deleted: {FullPath}, {Name}", e.FullPath, e.Name);
|
||||||
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, true));
|
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,10 +285,10 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
|
|
||||||
var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList();
|
var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList();
|
||||||
_logger.LogTrace("[LibraryWatcher] Root Folders: {RootFolders}", rootFolder);
|
_logger.LogTrace("[LibraryWatcher] Root Folders: {RootFolders}", rootFolder);
|
||||||
if (!rootFolder.Any()) return string.Empty;
|
if (rootFolder.Count == 0) return string.Empty;
|
||||||
|
|
||||||
// Select the first folder and join with library folder, this should give us the folder to scan.
|
// Select the first folder and join with library folder, this should give us the folder to scan.
|
||||||
return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[rootFolder.Count - 1]));
|
return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[rootFolder.Count - 1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -115,13 +115,21 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
|
|||||||
{
|
{
|
||||||
info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim();
|
info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format))
|
||||||
|
{
|
||||||
|
info.IsSpecial = true;
|
||||||
|
info.Chapters = Parser.DefaultChapter;
|
||||||
|
info.Volumes = Parser.SpecialVolume;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(info.ComicInfo.Number))
|
if (!string.IsNullOrEmpty(info.ComicInfo.Number))
|
||||||
{
|
{
|
||||||
info.Chapters = info.ComicInfo.Number;
|
info.Chapters = info.ComicInfo.Number;
|
||||||
if (info.IsSpecial && Parser.DefaultChapter != info.Chapters)
|
if (info.IsSpecial && Parser.DefaultChapter != info.Chapters)
|
||||||
{
|
{
|
||||||
info.IsSpecial = false;
|
info.IsSpecial = false;
|
||||||
info.Volumes = $"{Parser.SpecialVolumeNumber}";
|
info.Volumes = Parser.SpecialVolume;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,6 +138,7 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
|
|||||||
{
|
{
|
||||||
info.SeriesSort = info.ComicInfo.TitleSort.Trim();
|
info.SeriesSort = info.ComicInfo.TitleSort.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract bool IsApplicable(string filePath, LibraryType type);
|
public abstract bool IsApplicable(string filePath, LibraryType type);
|
||||||
|
@ -121,6 +121,10 @@ public static class Parser
|
|||||||
|
|
||||||
private static readonly Regex[] MangaVolumeRegex = new[]
|
private static readonly Regex[] MangaVolumeRegex = new[]
|
||||||
{
|
{
|
||||||
|
// Thai Volume: เล่ม n -> Volume n
|
||||||
|
new Regex(
|
||||||
|
@"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
// Dance in the Vampire Bund v16-17
|
// Dance in the Vampire Bund v16-17
|
||||||
new Regex(
|
new Regex(
|
||||||
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)",
|
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)",
|
||||||
@ -194,6 +198,10 @@ public static class Parser
|
|||||||
|
|
||||||
private static readonly Regex[] MangaSeriesRegex = new[]
|
private static readonly Regex[] MangaSeriesRegex = new[]
|
||||||
{
|
{
|
||||||
|
// Thai Volume: เล่ม n -> Volume n
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.+?)(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
// Russian Volume: Том n -> Volume n, Тома n -> Volume
|
// Russian Volume: Том n -> Volume n, Тома n -> Volume
|
||||||
new Regex(
|
new Regex(
|
||||||
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
|
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
|
||||||
@ -368,6 +376,10 @@ public static class Parser
|
|||||||
|
|
||||||
private static readonly Regex[] ComicSeriesRegex = new[]
|
private static readonly Regex[] ComicSeriesRegex = new[]
|
||||||
{
|
{
|
||||||
|
// Thai Volume: เล่ม n -> Volume n
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.+?)(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
// Russian Volume: Том n -> Volume n, Тома n -> Volume
|
// Russian Volume: Том n -> Volume n, Тома n -> Volume
|
||||||
new Regex(
|
new Regex(
|
||||||
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
|
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
|
||||||
@ -456,6 +468,10 @@ public static class Parser
|
|||||||
|
|
||||||
private static readonly Regex[] ComicVolumeRegex = new[]
|
private static readonly Regex[] ComicVolumeRegex = new[]
|
||||||
{
|
{
|
||||||
|
// Thai Volume: เล่ม n -> Volume n
|
||||||
|
new Regex(
|
||||||
|
@"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
// Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
|
// Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
|
||||||
new Regex(
|
new Regex(
|
||||||
@"^(?<Series>.+?)(?: |_)(t|v)(?<Volume>" + NumberRange + @")",
|
@"^(?<Series>.+?)(?: |_)(t|v)(?<Volume>" + NumberRange + @")",
|
||||||
@ -492,6 +508,10 @@ public static class Parser
|
|||||||
|
|
||||||
private static readonly Regex[] ComicChapterRegex = new[]
|
private static readonly Regex[] ComicChapterRegex = new[]
|
||||||
{
|
{
|
||||||
|
// Thai Volume: บทที่ n -> Chapter n, ตอนที่ n -> Chapter n
|
||||||
|
new Regex(
|
||||||
|
@"(บทที่|ตอนที่)(\s)?(\.?)(\s|_)?(?<Chapter>\d+(\-\d+)?(\.\d+)?)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
// Batman & Wildcat (1 of 3)
|
// Batman & Wildcat (1 of 3)
|
||||||
new Regex(
|
new Regex(
|
||||||
@"(?<Series>.*(\d{4})?)( |_)(?:\((?<Chapter>\d+) of \d+)",
|
@"(?<Series>.*(\d{4})?)( |_)(?:\((?<Chapter>\d+) of \d+)",
|
||||||
@ -557,6 +577,10 @@ public static class Parser
|
|||||||
|
|
||||||
private static readonly Regex[] MangaChapterRegex = new[]
|
private static readonly Regex[] MangaChapterRegex = new[]
|
||||||
{
|
{
|
||||||
|
// Thai Chapter: บทที่ n -> Chapter n, ตอนที่ n -> Chapter n, เล่ม n -> Volume n, เล่มที่ n -> Volume n
|
||||||
|
new Regex(
|
||||||
|
@"(?<Volume>((เล่ม|เล่มที่))?(\s|_)?\.?\d+)(\s|_)(บทที่|ตอนที่)\.?(\s|_)?(?<Chapter>\d+)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5
|
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5
|
||||||
new Regex(
|
new Regex(
|
||||||
@"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)(-c?\d+(\.\d)?)?)",
|
@"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)(-c?\d+(\.\d)?)?)",
|
||||||
|
@ -701,7 +701,8 @@ public class ProcessSeries : IProcessSeries
|
|||||||
{
|
{
|
||||||
if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter))
|
if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.Range, volume.Name, parsedInfos[0].Series);
|
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}",
|
||||||
|
existingChapter.Range, volume.Name, parsedInfos[0].Series);
|
||||||
volume.Chapters.Remove(existingChapter);
|
volume.Chapters.Remove(existingChapter);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -21,15 +21,18 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit
|
|||||||
1. Fork Kavita
|
1. Fork Kavita
|
||||||
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
|
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
|
||||||
3. Install the required Node Packages
|
3. Install the required Node Packages
|
||||||
- cd Kavita/UI/Web
|
- `cd Kavita/UI/Web`
|
||||||
- `npm install`
|
- `npm install`
|
||||||
- `npm install -g @angular/cli`
|
- `npm install -g @angular/cli`
|
||||||
- `npm run cache-locale-prime` (only do this once to generate the locale file)
|
5. Start the frontend
|
||||||
4. Start angular server `ng serve`
|
- `npm run start`
|
||||||
5. Build the project in Visual Studio/Rider, Setting startup project to `API`
|
6. Build the project in Visual Studio/Rider, Setting startup project to `API`
|
||||||
6. Debug the project in Visual Studio/Rider
|
7. Debug the project in Visual Studio/Rider
|
||||||
7. Open http://localhost:4200
|
8. Open http://localhost:4200
|
||||||
8. (Deployment only) Run build.sh and pass the Runtime Identifier for your OS or just build.sh for all supported RIDs.
|
9. (Deployment only) Run build.sh and pass the Runtime Identifier for your OS or just build.sh for all supported RIDs.
|
||||||
|
|
||||||
|
### Debugging on Device ###
|
||||||
|
- Update `IP` constant in `Web/UI/src/environments/environment.ts` to your dev machine's ip instead of `localhost`.
|
||||||
|
|
||||||
|
|
||||||
### Contributing Code ###
|
### Contributing Code ###
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.21.0.86780">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.23.0.88079">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -4,7 +4,7 @@ This project was generated with [Angular CLI](https://github.com/angular/angular
|
|||||||
|
|
||||||
## Development server
|
## Development server
|
||||||
|
|
||||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
Run `npm run start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||||
Your backend must be served on port 5000.
|
Your backend must be served on port 5000.
|
||||||
|
|
||||||
## Code scaffolding
|
## Code scaffolding
|
||||||
@ -25,10 +25,11 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
|
|||||||
|
|
||||||
Run `npx playwright test --reporter=line` or `npx playwright test` to run e2e tests.
|
Run `npx playwright test --reporter=line` or `npx playwright test` to run e2e tests.
|
||||||
|
|
||||||
## Connecting to your dev server via your phone
|
## Connecting to your dev server via your phone or any other compatible client on local network
|
||||||
|
|
||||||
ng serve --host 0.0.0.0
|
Update `IP` constant in `src/environments/environment.ts` to your dev machine's ip instead of `localhost`.
|
||||||
and update environment.ts to your local ip.
|
|
||||||
|
Run `npm run start`
|
||||||
|
|
||||||
## Notes:
|
## Notes:
|
||||||
- injected services should be at the top of the file
|
- injected services should be at the top of the file
|
||||||
|
@ -14,6 +14,15 @@ function generateChecksum(str, algorithm, encoding) {
|
|||||||
|
|
||||||
const result = {};
|
const result = {};
|
||||||
|
|
||||||
|
// Generate directory if it doesn't exist
|
||||||
|
const distFolderPath = './dist/';
|
||||||
|
const browserFolderPath = './dist/browser/';
|
||||||
|
if (!fs.existsSync(browserFolderPath)) {
|
||||||
|
console.log('Creating ./dist/browser folder');
|
||||||
|
fs.mkdirSync(distFolderPath, 0o744);
|
||||||
|
fs.mkdirSync(browserFolderPath, 0o744);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove file if it exists
|
// Remove file if it exists
|
||||||
const cacheBustingFilePath = './i18n-cache-busting.json';
|
const cacheBustingFilePath = './i18n-cache-busting.json';
|
||||||
if (fs.existsSync(cacheBustingFilePath)) {
|
if (fs.existsSync(cacheBustingFilePath)) {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"version": "0.7.12.1",
|
"version": "0.7.12.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "npm run cache-locale && ng serve",
|
"start": "npm run cache-locale && ng serve --host 0.0.0.0",
|
||||||
"build": "npm run cache-locale && ng build",
|
"build": "npm run cache-locale && ng build",
|
||||||
"minify-langs": "node minify-json.js",
|
"minify-langs": "node minify-json.js",
|
||||||
"cache-locale": "node hash-localization.js",
|
"cache-locale": "node hash-localization.js",
|
||||||
|
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 { ScalingOption } from './scaling-option';
|
||||||
import { SiteTheme } from './site-theme';
|
import { SiteTheme } from './site-theme';
|
||||||
import {WritingStyle} from "./writing-style";
|
import {WritingStyle} from "./writing-style";
|
||||||
|
import {PdfTheme} from "./pdf-theme";
|
||||||
|
import {PdfScrollMode} from "./pdf-scroll-mode";
|
||||||
|
import {PdfLayoutMode} from "./pdf-layout-mode";
|
||||||
|
import {PdfSpreadMode} from "./pdf-spread-mode";
|
||||||
|
|
||||||
export interface Preferences {
|
export interface Preferences {
|
||||||
// Manga Reader
|
// Manga Reader
|
||||||
@ -34,6 +38,12 @@ export interface Preferences {
|
|||||||
bookReaderLayoutMode: BookPageLayoutMode;
|
bookReaderLayoutMode: BookPageLayoutMode;
|
||||||
bookReaderImmersiveMode: boolean;
|
bookReaderImmersiveMode: boolean;
|
||||||
|
|
||||||
|
// PDF Reader
|
||||||
|
pdfTheme: PdfTheme;
|
||||||
|
pdfScrollMode: PdfScrollMode;
|
||||||
|
pdfLayoutMode: PdfLayoutMode;
|
||||||
|
pdfSpreadMode: PdfSpreadMode;
|
||||||
|
|
||||||
// Global
|
// Global
|
||||||
theme: SiteTheme;
|
theme: SiteTheme;
|
||||||
globalPageLayoutMode: PageLayoutMode;
|
globalPageLayoutMode: PageLayoutMode;
|
||||||
@ -50,6 +60,10 @@ export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horiz
|
|||||||
export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}];
|
export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}];
|
||||||
export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}];
|
export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}];
|
||||||
export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}];
|
export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}];
|
||||||
export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // , {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover}
|
export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // TODO: Build this, {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover}
|
||||||
export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}];
|
export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}];
|
||||||
export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}];
|
export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}];
|
||||||
|
export const pdfLayoutModes = [{text: 'pdf-multiple', value: PdfLayoutMode.Multiple}, {text: 'pdf-book', value: PdfLayoutMode.Book}];
|
||||||
|
export const pdfScrollModes = [{text: 'pdf-vertical', value: PdfScrollMode.Vertical}, {text: 'pdf-horizontal', value: PdfScrollMode.Horizontal}, {text: 'pdf-page', value: PdfScrollMode.Page}];
|
||||||
|
export const pdfSpreadModes = [{text: 'pdf-none', value: PdfSpreadMode.None}, {text: 'pdf-odd', value: PdfSpreadMode.Odd}, {text: 'pdf-even', value: PdfSpreadMode.Even}];
|
||||||
|
export const pdfThemes = [{text: 'pdf-light', value: PdfTheme.Light}, {text: 'pdf-dark', value: PdfTheme.Dark}];
|
||||||
|
@ -52,6 +52,8 @@ export class AccountService {
|
|||||||
*/
|
*/
|
||||||
private refreshTokenTimeout: ReturnType<typeof setTimeout> | undefined;
|
private refreshTokenTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
private isOnline: boolean = true;
|
||||||
|
|
||||||
constructor(private httpClient: HttpClient, private router: Router,
|
constructor(private httpClient: HttpClient, private router: Router,
|
||||||
private messageHub: MessageHubService, private themeService: ThemeService) {
|
private messageHub: MessageHubService, private themeService: ThemeService) {
|
||||||
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
|
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
|
||||||
@ -59,6 +61,15 @@ export class AccountService {
|
|||||||
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
|
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
|
||||||
switchMap(() => this.refreshAccount()))
|
switchMap(() => this.refreshAccount()))
|
||||||
.subscribe(() => {});
|
.subscribe(() => {});
|
||||||
|
|
||||||
|
window.addEventListener("offline", (e) => {
|
||||||
|
this.isOnline = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("online", (e) => {
|
||||||
|
this.isOnline = true;
|
||||||
|
this.refreshToken().subscribe();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAdminRole(user: User) {
|
hasAdminRole(user: User) {
|
||||||
@ -143,6 +154,7 @@ export class AccountService {
|
|||||||
|
|
||||||
localStorage.setItem(this.userKey, JSON.stringify(user));
|
localStorage.setItem(this.userKey, JSON.stringify(user));
|
||||||
localStorage.setItem(AccountService.lastLoginKey, user.username);
|
localStorage.setItem(AccountService.lastLoginKey, user.username);
|
||||||
|
|
||||||
if (user.preferences && user.preferences.theme) {
|
if (user.preferences && user.preferences.theme) {
|
||||||
this.themeService.setTheme(user.preferences.theme.name);
|
this.themeService.setTheme(user.preferences.theme.name);
|
||||||
} else {
|
} else {
|
||||||
@ -329,7 +341,7 @@ export class AccountService {
|
|||||||
|
|
||||||
|
|
||||||
private refreshToken() {
|
private refreshToken() {
|
||||||
if (this.currentUser === null || this.currentUser === undefined) return of();
|
if (this.currentUser === null || this.currentUser === undefined || !this.isOnline) return of();
|
||||||
return this.httpClient.post<{token: string, refreshToken: string}>(this.baseUrl + 'account/refresh-token',
|
return this.httpClient.post<{token: string, refreshToken: string}>(this.baseUrl + 'account/refresh-token',
|
||||||
{token: this.currentUser.token, refreshToken: this.currentUser.refreshToken}).pipe(map(user => {
|
{token: this.currentUser.token, refreshToken: this.currentUser.refreshToken}).pipe(map(user => {
|
||||||
if (this.currentUser) {
|
if (this.currentUser) {
|
||||||
|
@ -2,18 +2,25 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">
|
<h4 class="modal-title" id="modal-basic-title">
|
||||||
{{t('user-review', {username: review.username})}} @if(review.isExternal) {<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">}
|
{{t('user-review', {username: review.username})}}
|
||||||
|
@if(review.isExternal) {
|
||||||
|
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
|
||||||
|
}
|
||||||
</h4>
|
</h4>
|
||||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body scrollable-modal">
|
<div class="modal-body scrollable-modal">
|
||||||
<p *ngIf="review.tagline" [innerHTML]="review.tagline | safeHtml"></p>
|
@if (review.tagline) {
|
||||||
|
<p [innerHTML]="review.tagline | safeHtml"></p>
|
||||||
|
}
|
||||||
<p #container class="img-max-width" [innerHTML]="review.body | safeHtml"></p>
|
<p #container class="img-max-width" [innerHTML]="review.body | safeHtml"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<a *ngIf="review.externalUrl" class="btn btn-icon" [href]="review.externalUrl | safeHtml" target="_blank" rel="noopener noreferrer" [title]="review.externalUrl">
|
@if (review.siteUrl) {
|
||||||
{{t('go-to-review')}}
|
<a class="btn btn-icon" [href]="review.siteUrl | safeHtml" target="_blank" rel="noopener noreferrer" [title]="review.siteUrl">
|
||||||
</a>
|
{{t('go-to-review')}}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
<button type="submit" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
|
<button type="submit" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-2 d-none d-md-block">
|
<div class="col-md-2 d-none d-md-block">
|
||||||
<i class="img-fluid rounded-start fa-solid fa-circle-user profile-image" aria-hidden="true"></i>
|
<i class="img-fluid rounded-start fa-solid fa-circle-user profile-image" aria-hidden="true"></i>
|
||||||
<div *ngIf="isMyReview" class="my-review">
|
@if (isMyReview) {
|
||||||
<i class="fa-solid fa-star" aria-hidden="true" [title]="t('your-review')"></i>
|
<div class="my-review">
|
||||||
<span class="visually-hidden">{{t('your-review')}}</span>
|
<i class="fa-solid fa-star" aria-hidden="true" [title]="t('your-review')"></i>
|
||||||
</div>
|
<span class="visually-hidden">{{t('your-review')}}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -21,17 +23,19 @@
|
|||||||
|
|
||||||
<div class="card-footer bg-transparent text-muted">
|
<div class="card-footer bg-transparent text-muted">
|
||||||
<div>
|
<div>
|
||||||
<ng-container *ngIf="isMyReview; else normalReview">
|
@if (isMyReview) {
|
||||||
<i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i>
|
<i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i>
|
||||||
<img class="me-1" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
|
<img class="me-1" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
|
||||||
{{review.username}}
|
{{review.username}}
|
||||||
</ng-container>
|
} @else {
|
||||||
<ng-template #normalReview>
|
|
||||||
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
|
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
|
||||||
</ng-template>
|
}
|
||||||
|
|
||||||
{{(isMyReview ? '' : review.username | defaultValue:'')}}
|
{{(isMyReview ? '' : review.username | defaultValue:'')}}
|
||||||
</div>
|
</div>
|
||||||
<span class="review-score" *ngIf="review.isExternal">{{t('rating-percentage', {r: review.score})}}</span>
|
@if (review.isExternal){
|
||||||
|
<span class="review-score">{{t('rating-percentage', {r: review.score})}}</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,7 +28,7 @@ import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-review-card',
|
selector: 'app-review-card',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ReadMoreComponent, DefaultValuePipe, ImageComponent, NgOptimizedImage, ProviderImagePipe, TranslocoDirective],
|
imports: [ReadMoreComponent, DefaultValuePipe, ImageComponent, NgOptimizedImage, ProviderImagePipe, TranslocoDirective],
|
||||||
templateUrl: './review-card.component.html',
|
templateUrl: './review-card.component.html',
|
||||||
styleUrls: ['./review-card.component.scss'],
|
styleUrls: ['./review-card.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
@ -9,6 +9,6 @@ export interface UserReview {
|
|||||||
tagline?: string;
|
tagline?: string;
|
||||||
isExternal: boolean;
|
isExternal: boolean;
|
||||||
bodyJustText?: string;
|
bodyJustText?: string;
|
||||||
externalUrl?: string;
|
siteUrl?: string;
|
||||||
provider: ScrobbleProvider;
|
provider: ScrobbleProvider;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
@for(rowForm of items.controls; track rowForm; let idx = $index) {
|
@for(rowForm of items.controls; track rowForm; let idx = $index) {
|
||||||
<tr >
|
<tr >
|
||||||
<td id="progress-event--{{idx}}">
|
<td id="progress-event--{{idx}}">
|
||||||
{{progressEvents[idx].userName}}
|
{{progressEvents[idx].userName | sentenceCase}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if(editMode[idx]) {
|
@if(editMode[idx]) {
|
||||||
|
@ -7,6 +7,7 @@ import {FullProgress} from "../../_models/readers/full-progress";
|
|||||||
import {ReaderService} from "../../_services/reader.service";
|
import {ReaderService} from "../../_services/reader.service";
|
||||||
import {TranslocoDirective} from "@ngneat/transloco";
|
import {TranslocoDirective} from "@ngneat/transloco";
|
||||||
import {FormArray, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
import {FormArray, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
|
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-edit-chapter-progress',
|
selector: 'app-edit-chapter-progress',
|
||||||
@ -18,7 +19,8 @@ import {FormArray, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from
|
|||||||
TitleCasePipe,
|
TitleCasePipe,
|
||||||
UtcToLocalTimePipe,
|
UtcToLocalTimePipe,
|
||||||
TranslocoDirective,
|
TranslocoDirective,
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule,
|
||||||
|
SentenceCasePipe
|
||||||
],
|
],
|
||||||
templateUrl: './edit-chapter-progress.component.html',
|
templateUrl: './edit-chapter-progress.component.html',
|
||||||
styleUrl: './edit-chapter-progress.component.scss',
|
styleUrl: './edit-chapter-progress.component.scss',
|
||||||
|
@ -33,25 +33,54 @@
|
|||||||
[backgroundColor]="backgroundColor"
|
[backgroundColor]="backgroundColor"
|
||||||
[customToolbar]="multiToolbar"
|
[customToolbar]="multiToolbar"
|
||||||
[language]="user.preferences.locale"
|
[language]="user.preferences.locale"
|
||||||
|
|
||||||
[(scrollMode)]="scrollMode"
|
[(scrollMode)]="scrollMode"
|
||||||
|
[pageViewMode]="pageLayoutMode"
|
||||||
|
[spread]="spreadMode"
|
||||||
|
|
||||||
(pageChange)="saveProgress()"
|
(pageChange)="saveProgress()"
|
||||||
(pdfLoadingStarts)="updateLoading(true)"
|
(pdfLoadingStarts)="updateLoading(true)"
|
||||||
(pdfLoaded)="updateLoading(false)"
|
(pdfLoaded)="updateLoading(false)"
|
||||||
(progress)="updateLoadProgress($event)"
|
(progress)="updateLoadProgress($event)"
|
||||||
|
(zoomChange)="calcScrollbarNeeded()"
|
||||||
>
|
>
|
||||||
|
|
||||||
</ngx-extended-pdf-viewer>
|
</ngx-extended-pdf-viewer>
|
||||||
|
|
||||||
|
@if (scrollMode === ScrollModeType.page) {
|
||||||
|
<div class="left" (click)="prevPage()"></div>
|
||||||
|
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}}" (click)="nextPage()"></div>
|
||||||
|
}
|
||||||
|
|
||||||
<ng-template #multiToolbar>
|
<ng-template #multiToolbar>
|
||||||
<div style="min-height: 36px" id="toolbarViewer" [ngStyle]="{'background-color': backgroundColor, 'color': fontColor}">
|
<div style="min-height: 36px" id="toolbarViewer" [ngStyle]="{'background-color': backgroundColor, 'color': fontColor}">
|
||||||
<div id="toolbarViewerLeft">
|
<div id="toolbarViewerLeft">
|
||||||
<pdf-toggle-sidebar></pdf-toggle-sidebar>
|
<pdf-toggle-sidebar></pdf-toggle-sidebar>
|
||||||
<pdf-find-button></pdf-find-button>
|
<pdf-find-button></pdf-find-button>
|
||||||
<pdf-paging-area></pdf-paging-area>
|
<pdf-paging-area></pdf-paging-area>
|
||||||
|
|
||||||
|
@if (utilityService.getActiveBreakpoint() > Breakpoint.Mobile) {
|
||||||
|
<button class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton" [ngbTooltip]="bookTitle">
|
||||||
|
<i class="toolbar-icon fa-solid fa-info" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
||||||
|
<span class="visually-hidden">{{bookTitle}}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button *ngIf="incognitoMode" [ngbTooltip]="t('toggle-incognito')" (click)="turnOffIncognito()" class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton">
|
||||||
|
<i class="toolbar-icon fa fa-glasses" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
||||||
|
<span class="visually-hidden">{{t('incognito-mode')}}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn-icon col-2 col-xs-1 mt-0 mb-0 pt-1 pb-0 toolbarButton" (click)="closeReader()" [ngbTooltip]="t('close-reader-alt')">
|
||||||
|
<i class="toolbar-icon fa fa-times-circle" aria-hidden="true" [ngStyle]="{color: fontColor}"></i>
|
||||||
|
<span class="visually-hidden">{{t('close-reader-alt')}}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pdf-zoom-toolbar ></pdf-zoom-toolbar>
|
@if (utilityService.getActiveBreakpoint() > Breakpoint.Tablet) {
|
||||||
|
<pdf-zoom-toolbar ></pdf-zoom-toolbar>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
<div id="toolbarViewerRight">
|
<div id="toolbarViewerRight">
|
||||||
<pdf-hand-tool></pdf-hand-tool>
|
<pdf-hand-tool></pdf-hand-tool>
|
||||||
@ -59,42 +88,66 @@
|
|||||||
<pdf-presentation-mode></pdf-presentation-mode>
|
<pdf-presentation-mode></pdf-presentation-mode>
|
||||||
|
|
||||||
|
|
||||||
<!-- This is not yet supported by the underlying library
|
@if (utilityService.getActiveBreakpoint() > Breakpoint.Mobile) {
|
||||||
<button (click)="toggleBookPageMode()" class="btn btn-icon toolbarButton">
|
<button (click)="toggleBookPageMode()" class="btn-icon toolbarButton" [ngbTooltip]="pageLayoutMode | pdfLayoutMode" [disabled]="scrollMode === ScrollModeType.page">
|
||||||
<i class="toolbar-icon fa-solid {{this.bookMode !== 'book' ? 'fa-book' : 'fa-book-open'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
<i class="toolbar-icon fa-solid {{this.pageLayoutMode !== 'book' ? 'fa-book' : 'fa-book-open'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
||||||
<span class="visually-hidden">{{this.bookMode !== 'book' ? 'Book Mode' : 'Normal Mode'}}</span>
|
<span class="visually-hidden">{{this.pageLayoutMode | pdfLayoutMode}}</span>
|
||||||
</button> -->
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
<button class="btn btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton" [ngbTooltip]="bookTitle">
|
|
||||||
<i class="toolbar-icon fa-solid fa-info" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
<!-- scroll mode should be disabled when book mode is used -->
|
||||||
<span class="visually-hidden">{{bookTitle}}</span>
|
<button (click)="toggleScrollMode()" class="btn-icon toolbarButton" [ngbTooltip]="scrollMode | pdfScrollMode" [disabled]="this.pageLayoutMode === 'book'">
|
||||||
|
@switch (scrollMode) {
|
||||||
|
@case (ScrollModeType.vertical) {
|
||||||
|
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM11 0v.5c0 1-.5 1.5-1.5 1.5h-3C5.5 2 5 1.5 5 .5V0h6zM11 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6z"></path></svg>
|
||||||
|
}
|
||||||
|
@case (ScrollModeType.page) {
|
||||||
|
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,7V9H12V17H14V7H10Z"></path></svg>
|
||||||
|
}
|
||||||
|
@case (ScrollModeType.horizontal) {
|
||||||
|
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px"> <path fill="currentColor" d="M0 4h1.5c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5H0zM9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM16 4h-1.5c-1 0-1.5.5-1.5 1.5v5c0 1 .5 1.5 1.5 1.5H16z"></path> </svg>
|
||||||
|
}
|
||||||
|
@case (ScrollModeType.wrapped) {
|
||||||
|
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M5.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C1 4.5 1.5 4 2.5 4zM7 0v.5C7 1.5 6.5 2 5.5 2h-3C1.5 2 1 1.5 1 .5V0h6zM7 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6zM13.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5c0-1 .5-1.5 1.5-1.5zM15 0v.5c0 1-.5 1.5-1.5 1.5h-3C9.5 2 9 1.5 9 .5V0h6zM15 16v-.507c0-1-.5-1.5-1.5-1.5h-3C9.5 14 9 14.5 9 15.5v.5h6z"></path></svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="visually-hidden">{{scrollMode | pdfScrollMode}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button *ngIf="incognitoMode" (click)="turnOffIncognito()" class="btn btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton">
|
<button (click)="toggleSpreadMode()" class="btn-icon toolbarButton" [ngbTooltip]="spreadMode | pdfSpreadMode" [disabled]="this.pageLayoutMode === 'book'">
|
||||||
<i class="toolbar-icon fa fa-glasses" [ngStyle]="{color: fontColor}" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-mode')}}</span>
|
|
||||||
|
@switch (spreadMode) {
|
||||||
|
@case ('off') {
|
||||||
|
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M6 3c-1 0-1.5.5-1.5 1.5v7c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5v-7c0-1-.5-1.5-1.5-1.5z"></path></svg>
|
||||||
|
}
|
||||||
|
@case ('odd') {
|
||||||
|
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M10.56 3.5C9.56 3.5 9 4 9 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.93 1.2c.8 0 1.4.2 1.8.64.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.44-.2.3-.6.6-1 .93l-.6.4c-.4.3-.6.4-.7.55-.1.1-.2.2-.3.4h3.2v1.27h-5c0-.5.1-1 .3-1.43.2-.49.7-1 1.5-1.54.7-.5 1.1-.8 1.3-1.02.3-.3.4-.7.4-1.05 0-.3-.1-.6-.3-.77-.2-.2-.4-.3-.7-.3-.4 0-.7.2-.9.5-.1.2-.1.5-.2.9h-1.4c0-.6.2-1.1.3-1.5.4-.7 1.1-1.1 2-1.1zM1.54 3.5C.54 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.54 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.8 1.125H4.5V12H3V6.9H1.3v-1c.5 0 .8 0 .97-.03.33-.07.53-.17.73-.37.1-.2.2-.3.25-.5.05-.2.05-.3.05-.3z"></path></svg>
|
||||||
|
}
|
||||||
|
@case ('even') {
|
||||||
|
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px"><path fill="currentColor" d="M1.5 3.5C.5 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm2 1.2c.8 0 1.4.2 1.8.6.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.4-.2.3-.5.7-1 1l-.6.4c-.4.3-.6.4-.75.56-.15.14-.25.24-.35.44H6v1.3H1c0-.6.1-1.1.3-1.5.3-.6.7-1 1.5-1.6.7-.4 1.1-.8 1.28-1 .32-.3.42-.6.42-1 0-.3-.1-.6-.23-.8-.17-.2-.37-.3-.77-.3s-.7.1-.9.5c-.04.2-.1.5-.1.9H1.1c0-.6.1-1.1.3-1.5.4-.7 1.1-1.1 2.1-1.1zM10.54 3.54C9.5 3.54 9 4 9 5v6.5c0 1 .5 1.5 1.54 1.5h4c.96 0 1.46-.5 1.46-1.5V5c0-1-.5-1.46-1.5-1.46zm1.9.95c.7 0 1.3.2 1.7.5.4.4.6.8.6 1.4 0 .4-.1.8-.4 1.1-.2.2-.3.3-.5.4.1 0 .3.1.6.3.4.3.5.8.5 1.4 0 .6-.2 1.2-.6 1.6-.4.5-1.1.7-1.9.7-1 0-1.8-.3-2.2-1-.14-.29-.24-.69-.24-1.29h1.4c0 .3 0 .5.1.7.2.4.5.5 1 .5.3 0 .5-.1.7-.3.2-.2.3-.5.3-.8 0-.5-.2-.8-.6-.95-.2-.05-.5-.15-1-.15v-1c.5 0 .8-.1 1-.14.3-.1.5-.4.5-.9 0-.3-.1-.5-.2-.7-.2-.2-.4-.3-.7-.3-.3 0-.6.1-.75.3-.2.2-.2.5-.2.86h-1.34c0-.4.1-.7.19-1.1 0-.12.2-.32.4-.62.2-.2.4-.3.7-.4.3-.1.6-.1 1-.1z"></path></svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="visually-hidden">{{spreadMode | pdfSpreadMode}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- This is pretty experimental, so it might not work perfectly -->
|
<!-- This is pretty experimental, so it might not work perfectly -->
|
||||||
<button (click)="toggleTheme()" class="btn btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton">
|
<button (click)="toggleTheme()" class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton toolbar-btn-fix">
|
||||||
<i class="toolbar-icon fa-solid {{this.theme === 'light' ? 'fa-sun' : 'fa-moon'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
<i class="toolbar-icon fa-solid {{this.theme === 'light' ? 'fa-sun' : 'fa-moon'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
||||||
<span class="visually-hidden">{{this.theme === 'light' ? t('light-theme-alt') : t('dark-theme-alt')}}</span>
|
<span class="visually-hidden">{{this.theme === 'light' ? t('light-theme-alt') : t('dark-theme-alt')}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-icon col-2 col-xs-1 mt-0 mb-0 pt-1 pb-0 toolbarButton" (click)="closeReader()">
|
|
||||||
<i class="toolbar-icon fa fa-times-circle" aria-hidden="true" [ngStyle]="{color: fontColor}"></i>
|
|
||||||
<span class="visually-hidden">{{t('close-reader-alt')}}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="verticalToolbarSeparator hiddenSmallView"></div>
|
<div class="verticalToolbarSeparator hiddenSmallView"></div>
|
||||||
<pdf-toggle-secondary-toolbar></pdf-toggle-secondary-toolbar>
|
<pdf-toggle-secondary-toolbar></pdf-toggle-secondary-toolbar>
|
||||||
|
|
||||||
<pdf-single-page-mode [show]="true" [scrollMode]="scrollMode"></pdf-single-page-mode>
|
|
||||||
<pdf-vertical-scroll-mode [show]="true" [scrollMode]="scrollMode"></pdf-vertical-scroll-mode>
|
|
||||||
<pdf-horizontal-scroll [show]="true" [scrollMode]="scrollMode"></pdf-horizontal-scroll>
|
|
||||||
<pdf-wrapped-scroll-mode [show]="true" [scrollMode]="scrollMode"></pdf-wrapped-scroll-mode>
|
|
||||||
<pdf-no-spread [show]=true [scrollMode]="scrollMode"></pdf-no-spread>
|
|
||||||
<pdf-odd-spread [show]=true [scrollMode]="scrollMode"></pdf-odd-spread>
|
|
||||||
<pdf-even-spread [show]="true" [scrollMode]="scrollMode"></pdf-even-spread>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.book-title {
|
.book-title {
|
||||||
margin: 8px 0 4px !important;
|
margin: 8px 0 4px !important;
|
||||||
@ -24,3 +27,76 @@
|
|||||||
// NOTE: We have to override due to theme variables not being available
|
// NOTE: We have to override due to theme variables not being available
|
||||||
background-color: #3B9E76;
|
background-color: #3B9E76;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pagination-color: transparent;
|
||||||
|
$pagination-opacity: 0;
|
||||||
|
|
||||||
|
//$pagination-color: red;
|
||||||
|
//$pagination-opacity: 0.7;
|
||||||
|
$action-bar-height: 36px;
|
||||||
|
|
||||||
|
// Tap to Paginate
|
||||||
|
.right {
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
top: $action-bar-height;
|
||||||
|
width: 20vw;
|
||||||
|
z-index: 3;
|
||||||
|
background: $pagination-color;
|
||||||
|
border-color: transparent;
|
||||||
|
border: none !important;
|
||||||
|
opacity: $pagination-opacity;
|
||||||
|
outline: none;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.immersive {
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.no-pointer-events {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// This class pushes the click area to the left a bit to let users click the scrollbar
|
||||||
|
.right-with-scrollbar {
|
||||||
|
position: absolute;
|
||||||
|
right: 17px;
|
||||||
|
top: $action-bar-height;
|
||||||
|
width: 18vw;
|
||||||
|
z-index: 3;
|
||||||
|
background: $pagination-color;
|
||||||
|
opacity: $pagination-opacity;
|
||||||
|
border-color: transparent;
|
||||||
|
border: none !important;
|
||||||
|
outline: none;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.immersive {
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: $action-bar-height;
|
||||||
|
width: 20vw;
|
||||||
|
background: $pagination-color;
|
||||||
|
opacity: $pagination-opacity;
|
||||||
|
border-color: transparent;
|
||||||
|
border: none !important;
|
||||||
|
z-index: 3;
|
||||||
|
outline: none;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.immersive {
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,27 +1,43 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component, ElementRef,
|
Component,
|
||||||
HostListener,
|
ElementRef,
|
||||||
inject, OnDestroy,
|
HostListener, inject, Inject,
|
||||||
OnInit, ViewChild
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
import { NgxExtendedPdfViewerService, PageViewModeType, ScrollModeType, ProgressBarEvent, NgxExtendedPdfViewerModule } from 'ngx-extended-pdf-viewer';
|
import {
|
||||||
import { ToastrService } from 'ngx-toastr';
|
NgxExtendedPdfViewerModule,
|
||||||
import { take } from 'rxjs';
|
NgxExtendedPdfViewerService,
|
||||||
import { BookService } from 'src/app/book-reader/_services/book.service';
|
PageViewModeType,
|
||||||
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
|
ProgressBarEvent,
|
||||||
import { Chapter } from 'src/app/_models/chapter';
|
ScrollModeType
|
||||||
import { User } from 'src/app/_models/user';
|
} from 'ngx-extended-pdf-viewer';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import {ToastrService} from 'ngx-toastr';
|
||||||
import { NavService } from 'src/app/_services/nav.service';
|
import {take} from 'rxjs';
|
||||||
import { CHAPTER_ID_DOESNT_EXIST, ReaderService } from 'src/app/_services/reader.service';
|
import {BookService} from 'src/app/book-reader/_services/book.service';
|
||||||
import { SeriesService } from 'src/app/_services/series.service';
|
import {Breakpoint, KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||||
import { ThemeService } from 'src/app/_services/theme.service';
|
import {Chapter} from 'src/app/_models/chapter';
|
||||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
import {User} from 'src/app/_models/user';
|
||||||
import { NgIf, NgStyle, AsyncPipe } from '@angular/common';
|
import {AccountService} from 'src/app/_services/account.service';
|
||||||
|
import {NavService} from 'src/app/_services/nav.service';
|
||||||
|
import {CHAPTER_ID_DOESNT_EXIST, ReaderService} from 'src/app/_services/reader.service';
|
||||||
|
import {SeriesService} from 'src/app/_services/series.service';
|
||||||
|
import {ThemeService} from 'src/app/_services/theme.service';
|
||||||
|
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {AsyncPipe, DOCUMENT, NgIf, NgStyle} from '@angular/common';
|
||||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||||
|
import {PdfLayoutMode} from "../../../_models/preferences/pdf-layout-mode";
|
||||||
|
import {PdfScrollMode} from "../../../_models/preferences/pdf-scroll-mode";
|
||||||
|
import {PdfTheme} from "../../../_models/preferences/pdf-theme";
|
||||||
|
import {PdfSpreadMode} from "../../../_models/preferences/pdf-spread-mode";
|
||||||
|
import {SpreadType} from "ngx-extended-pdf-viewer/lib/options/spread-type";
|
||||||
|
import {PdfLayoutModePipe} from "../../_pipe/pdf-layout-mode.pipe";
|
||||||
|
import {PdfScrollModePipe} from "../../_pipe/pdf-scroll-mode.pipe";
|
||||||
|
import {PdfSpreadModePipe} from "../../_pipe/pdf-spread-mode.pipe";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-pdf-reader',
|
selector: 'app-pdf-reader',
|
||||||
@ -29,10 +45,26 @@ import {translate, TranslocoDirective} from "@ngneat/transloco";
|
|||||||
styleUrls: ['./pdf-reader.component.scss'],
|
styleUrls: ['./pdf-reader.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, NgStyle, NgxExtendedPdfViewerModule, NgbTooltip, AsyncPipe, TranslocoDirective]
|
imports: [NgIf, NgStyle, NgxExtendedPdfViewerModule, NgbTooltip, AsyncPipe, TranslocoDirective,
|
||||||
|
PdfLayoutModePipe, PdfScrollModePipe, PdfSpreadModePipe]
|
||||||
})
|
})
|
||||||
export class PdfReaderComponent implements OnInit, OnDestroy {
|
export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
private readonly seriesService = inject(SeriesService);
|
||||||
|
private readonly navService = inject(NavService);
|
||||||
|
private readonly toastr = inject(ToastrService);
|
||||||
|
private readonly bookService = inject(BookService);
|
||||||
|
private readonly themeService = inject(ThemeService);
|
||||||
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
|
public readonly accountService = inject(AccountService);
|
||||||
|
public readonly readerService = inject(ReaderService);
|
||||||
|
public readonly utilityService = inject(UtilityService);
|
||||||
|
|
||||||
|
protected readonly ScrollModeType = ScrollModeType;
|
||||||
|
protected readonly Breakpoint = Breakpoint;
|
||||||
|
|
||||||
@ViewChild('container') container!: ElementRef;
|
@ViewChild('container') container!: ElementRef;
|
||||||
|
|
||||||
libraryId!: number;
|
libraryId!: number;
|
||||||
@ -82,20 +114,13 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
|||||||
* How much of the current document is loaded
|
* How much of the current document is loaded
|
||||||
*/
|
*/
|
||||||
loadPercent: number = 0;
|
loadPercent: number = 0;
|
||||||
|
scrollbarNeeded = false;
|
||||||
|
|
||||||
/**
|
pageLayoutMode: PageViewModeType = 'multiple';
|
||||||
* This can't be updated dynamically:
|
|
||||||
* https://github.com/stephanrauh/ngx-extended-pdf-viewer/issues/1415
|
|
||||||
*/
|
|
||||||
bookMode: PageViewModeType = 'multiple';
|
|
||||||
|
|
||||||
scrollMode: ScrollModeType = ScrollModeType.vertical;
|
scrollMode: ScrollModeType = ScrollModeType.vertical;
|
||||||
|
spreadMode: SpreadType = 'off';
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private router: Router, public accountService: AccountService,
|
constructor(@Inject(DOCUMENT) private document: Document) {
|
||||||
private seriesService: SeriesService, public readerService: ReaderService,
|
|
||||||
private navService: NavService, private toastr: ToastrService,
|
|
||||||
private bookService: BookService, private themeService: ThemeService,
|
|
||||||
private readonly cdRef: ChangeDetectorRef, private pdfViewerService: NgxExtendedPdfViewerService) {
|
|
||||||
this.navService.hideNavBar();
|
this.navService.hideNavBar();
|
||||||
this.themeService.clearThemes();
|
this.themeService.clearThemes();
|
||||||
this.navService.hideSideNav();
|
this.navService.hideSideNav();
|
||||||
@ -108,6 +133,13 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
@HostListener('window:orientationchange', ['$event'])
|
||||||
|
onResize(){
|
||||||
|
// Update the window Height
|
||||||
|
this.calcScrollbarNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.themeService.currentTheme$.pipe(take(1)).subscribe(theme => {
|
this.themeService.currentTheme$.pipe(take(1)).subscribe(theme => {
|
||||||
this.themeService.setTheme(theme.name);
|
this.themeService.setTheme(theme.name);
|
||||||
@ -150,7 +182,71 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calcScrollbarNeeded() {
|
||||||
|
const viewContainer = this.document.querySelector('#viewerContainer');
|
||||||
|
if (viewContainer == null) return;
|
||||||
|
this.scrollbarNeeded = viewContainer.scrollHeight > this.container?.nativeElement?.clientHeight;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
convertPdfLayoutMode(mode: PdfLayoutMode) {
|
||||||
|
switch (mode) {
|
||||||
|
case PdfLayoutMode.Multiple:
|
||||||
|
return 'multiple';
|
||||||
|
case PdfLayoutMode.Single:
|
||||||
|
return 'single';
|
||||||
|
case PdfLayoutMode.Book:
|
||||||
|
return 'book';
|
||||||
|
case PdfLayoutMode.InfiniteScroll:
|
||||||
|
return 'infinite-scroll';
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
convertPdfScrollMode(mode: PdfScrollMode) {
|
||||||
|
switch (mode) {
|
||||||
|
case PdfScrollMode.Vertical:
|
||||||
|
return ScrollModeType.vertical;
|
||||||
|
case PdfScrollMode.Horizontal:
|
||||||
|
return ScrollModeType.horizontal;
|
||||||
|
case PdfScrollMode.Wrapped:
|
||||||
|
return ScrollModeType.wrapped;
|
||||||
|
case PdfScrollMode.Page:
|
||||||
|
return ScrollModeType.page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
convertPdfSpreadMode(mode: PdfSpreadMode): SpreadType {
|
||||||
|
switch (mode) {
|
||||||
|
case PdfSpreadMode.None:
|
||||||
|
return 'off' as SpreadType;
|
||||||
|
case PdfSpreadMode.Odd:
|
||||||
|
return 'odd' as SpreadType;
|
||||||
|
case PdfSpreadMode.Even:
|
||||||
|
return 'even' as SpreadType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
convertPdfTheme(theme: PdfTheme) {
|
||||||
|
switch (theme) {
|
||||||
|
case PdfTheme.Dark:
|
||||||
|
return 'dark';
|
||||||
|
case PdfTheme.Light:
|
||||||
|
return 'light';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
||||||
|
this.pageLayoutMode = this.convertPdfLayoutMode(this.user.preferences.pdfLayoutMode || PdfLayoutMode.Multiple);
|
||||||
|
this.scrollMode = this.convertPdfScrollMode(this.user.preferences.pdfScrollMode || PdfScrollMode.Vertical);
|
||||||
|
this.spreadMode = this.convertPdfSpreadMode(this.user.preferences.pdfSpreadMode || PdfSpreadMode.None);
|
||||||
|
this.theme = this.convertPdfTheme(this.user.preferences.pdfTheme || PdfTheme.Dark);
|
||||||
|
this.backgroundColor = this.themeMap[this.theme].background;
|
||||||
|
this.fontColor = this.themeMap[this.theme].font; // TODO: Move this to an observable or something
|
||||||
|
|
||||||
|
this.calcScrollbarNeeded();
|
||||||
|
|
||||||
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
|
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
|
||||||
this.volumeId = info.volumeId;
|
this.volumeId = info.volumeId;
|
||||||
this.bookTitle = info.bookTitle;
|
this.bookTitle = info.bookTitle;
|
||||||
@ -171,7 +267,7 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
this.readerService.enableWakeLock(this.container.nativeElement);
|
setTimeout(() => this.readerService.enableWakeLock(this.container.nativeElement), 1000); // TODO: This needs to be in afterviewinit i think
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -197,11 +293,33 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
|||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleScrollMode() {
|
||||||
|
const options: Array<ScrollModeType> = [ScrollModeType.vertical, ScrollModeType.horizontal, ScrollModeType.page];
|
||||||
|
let index = options.indexOf(this.scrollMode) + 1;
|
||||||
|
if (index >= options.length) index = 0;
|
||||||
|
this.scrollMode = options[index];
|
||||||
|
|
||||||
|
this.calcScrollbarNeeded();
|
||||||
|
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSpreadMode() {
|
||||||
|
const options: Array<SpreadType> = ['off', 'odd', 'even'];
|
||||||
|
let index = options.indexOf(this.spreadMode) + 1;
|
||||||
|
if (index >= options.length) index = 0;
|
||||||
|
this.spreadMode = options[index];
|
||||||
|
|
||||||
|
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
toggleBookPageMode() {
|
toggleBookPageMode() {
|
||||||
if (this.bookMode === 'book') {
|
if (this.pageLayoutMode === 'book') {
|
||||||
this.bookMode = 'multiple';
|
this.pageLayoutMode = 'multiple';
|
||||||
} else {
|
} else {
|
||||||
this.bookMode = 'book';
|
this.pageLayoutMode = 'book';
|
||||||
|
// If the fit is automatic, let's adjust to 100% to ensure it renders correctly (can't do this, but it doesn't always happen)
|
||||||
}
|
}
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
@ -225,4 +343,16 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
|||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevPage() {
|
||||||
|
this.currentPage--;
|
||||||
|
if (this.currentPage < 0) this.currentPage = 0;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage() {
|
||||||
|
this.currentPage++;
|
||||||
|
if (this.currentPage > this.maxPages) this.currentPage = this.maxPages;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
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 { SentenceCasePipe } from '../../_pipes/sentence-case.pipe';
|
||||||
import { NgIf, NgFor, AsyncPipe } from '@angular/common';
|
import { NgIf, NgFor, AsyncPipe } from '@angular/common';
|
||||||
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
||||||
|
import {tap} from "rxjs/operators";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-theme-manager',
|
selector: 'app-theme-manager',
|
||||||
@ -52,19 +53,11 @@ export class ThemeManagerComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyTheme(theme: SiteTheme) {
|
applyTheme(theme: SiteTheme) {
|
||||||
|
if (!this.user) return;
|
||||||
|
|
||||||
if (this.user) {
|
const pref = Object.assign({}, this.user.preferences);
|
||||||
const pref = Object.assign({}, this.user.preferences);
|
pref.theme = theme;
|
||||||
pref.theme = theme;
|
this.accountService.updatePreferences(pref).subscribe();
|
||||||
this.accountService.updatePreferences(pref).subscribe(updatedPref => {
|
|
||||||
if (this.user) {
|
|
||||||
this.user.preferences = updatedPref;
|
|
||||||
}
|
|
||||||
this.themeService.setTheme(theme.name);
|
|
||||||
this.cdRef.markForCheck();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDefault(theme: SiteTheme) {
|
updateDefault(theme: SiteTheme) {
|
||||||
|
@ -1,152 +1,36 @@
|
|||||||
<ng-container *transloco="let t; read:'user-preferences'">
|
<ng-container *transloco="let t; read:'user-preferences'">
|
||||||
<app-side-nav-companion-bar>
|
<app-side-nav-companion-bar>
|
||||||
<h2 title>
|
<h2 title>
|
||||||
{{t('title')}}
|
{{t('title')}}
|
||||||
</h2>
|
</h2>
|
||||||
</app-side-nav-companion-bar>
|
</app-side-nav-companion-bar>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
|
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
|
||||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||||
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ t(tab.title) }}</a>
|
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ t(tab.title) }}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
@defer (when tab.fragment === FragmentID.Account; prefetch on idle) {
|
@defer (when tab.fragment === FragmentID.Account; prefetch on idle) {
|
||||||
<app-change-email></app-change-email>
|
<app-change-email></app-change-email>
|
||||||
<app-change-password></app-change-password>
|
<app-change-password></app-change-password>
|
||||||
<app-change-age-restriction></app-change-age-restriction>
|
<app-change-age-restriction></app-change-age-restriction>
|
||||||
<app-manage-scrobbling-providers></app-manage-scrobbling-providers>
|
<app-manage-scrobbling-providers></app-manage-scrobbling-providers>
|
||||||
}
|
}
|
||||||
|
|
||||||
@defer (when tab.fragment === FragmentID.Preferences; prefetch on idle) {
|
@defer (when tab.fragment === FragmentID.Preferences; prefetch on idle) {
|
||||||
<ng-container *ngIf="tab.fragment === FragmentID.Preferences">
|
<ng-container *ngIf="tab.fragment === FragmentID.Preferences">
|
||||||
<p>
|
<p>
|
||||||
{{t('pref-description')}}
|
{{t('pref-description')}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||||
<div ngbAccordion [closeOthers]="true" #acc="ngbAccordion">
|
<div ngbAccordion [closeOthers]="true" #acc="ngbAccordion">
|
||||||
<div ngbAccordionItem [id]="AccordionPanelID.GlobalSettings" [collapsed]="false">
|
|
||||||
<h2 class="accordion-header" ngbAccordionHeader>
|
|
||||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)" aria-controls="collapseOne">
|
|
||||||
{{t('global-settings-title')}}
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div ngbAccordionCollapse>
|
|
||||||
<div ngbAccordionBody>
|
|
||||||
<ng-template>
|
|
||||||
<div class="row g-0">
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
|
||||||
<label for="settings-global-layoutmode" class="form-label">{{t('page-layout-mode-label')}}</label>
|
|
||||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
|
||||||
<ng-template #layoutModeTooltip>{{t('page-layout-mode-tooltip')}}</ng-template>
|
|
||||||
<span class="visually-hidden" id="settings-global-layoutmode-help">
|
|
||||||
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
|
|
||||||
</span>
|
|
||||||
<select class="form-select" aria-describedby="manga-header" formControlName="globalPageLayoutMode" id="settings-global-layoutmode">
|
|
||||||
<option *ngFor="let opt of pageLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div ngbAccordionItem [id]="AccordionPanelID.GlobalSettings" [collapsed]="false">
|
||||||
<label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label>
|
<h2 class="accordion-header" ngbAccordionHeader>
|
||||||
<i class="fa fa-info-circle ms-1"
|
<button class="accordion-button" ngbAccordionButton type="button"
|
||||||
aria-hidden="true" placement="right" [ngbTooltip]="localeTooltip" role="button" tabindex="0"></i>
|
[attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)"
|
||||||
<ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template>
|
aria-controls="collapseOne">
|
||||||
<span class="visually-hidden" id="settings-global-locale-help">
|
{{t('global-settings-title')}}
|
||||||
<ng-container [ngTemplateOutlet]="localeTooltip"></ng-container>
|
|
||||||
</span>
|
|
||||||
<select class="form-select" aria-describedby="manga-header" formControlName="locale" id="settings-global-locale">
|
|
||||||
<option *ngFor="let opt of locales" [value]="opt.isoCode">{{opt.title | titlecase}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0">
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input type="checkbox" id="blur-unread-summaries" role="switch" formControlName="blurUnreadSummaries" class="form-check-input" aria-describedby="settings-global-blurUnreadSummaries-help" [value]="true" aria-labelledby="auto-close-label">
|
|
||||||
<label class="form-check-label" for="blur-unread-summaries">{{t('blur-unread-summaries-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="blurUnreadSummariesTooltip" role="button" tabindex="0"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #blurUnreadSummariesTooltip>{{t('blur-unread-summaries-tooltip')}}</ng-template>
|
|
||||||
<span class="visually-hidden" id="settings-global-blurUnreadSummaries-help">
|
|
||||||
<ng-container [ngTemplateOutlet]="blurUnreadSummariesTooltip"></ng-container>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0">
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input type="checkbox" id="prompt-download" role="switch" formControlName="promptForDownloadSize" class="form-check-input" aria-describedby="settings-global-promptForDownloadSize-help" [value]="true" aria-labelledby="auto-close-label">
|
|
||||||
<label class="form-check-label" for="prompt-download">{{t('prompt-on-download-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="promptForDownloadSizeTooltip" role="button" tabindex="0"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #promptForDownloadSizeTooltip>{{t('prompt-on-download-tooltip', {size: '100'})}}</ng-template>
|
|
||||||
<span class="visually-hidden" id="settings-global-promptForDownloadSize-help">
|
|
||||||
<ng-container [ngTemplateOutlet]="promptForDownloadSizeTooltip"></ng-container>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0">
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input type="checkbox" id="no-transitions" role="switch" formControlName="noTransitions" class="form-check-input"
|
|
||||||
aria-describedby="settings-global-noTransitions-help" [value]="true" aria-labelledby="auto-close-label">
|
|
||||||
<label class="form-check-label" for="no-transitions">{{t('disable-animations-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="noTransitionsTooltip" role="button" tabindex="0"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #noTransitionsTooltip>{{t('disable-animations-tooltip')}}</ng-template>
|
|
||||||
<span class="visually-hidden" id="settings-global-noTransitions-help">
|
|
||||||
<ng-container [ngTemplateOutlet]="noTransitionsTooltip"></ng-container>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0">
|
|
||||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input type="checkbox" id="collapse-relationships" role="switch" formControlName="collapseSeriesRelationships"
|
|
||||||
aria-describedby="settings-collapse-relationships-help" class="form-check-input" aria-labelledby="auto-close-label">
|
|
||||||
<label class="form-check-label" for="collapse-relationships">{{t('collapse-series-relationships-label')}}</label>
|
|
||||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="collapseSeriesRelationshipsTooltip" role="button" tabindex="0"></i>
|
|
||||||
</div>
|
|
||||||
<ng-template #collapseSeriesRelationshipsTooltip>{{t('collapse-series-relationships-tooltip')}}</ng-template>
|
|
||||||
<span class="visually-hidden" id="settings-collapse-relationships-help">
|
|
||||||
<ng-container [ngTemplateOutlet]="collapseSeriesRelationshipsTooltip"></ng-container>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0">
|
|
||||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input type="checkbox" id="share-reviews" role="switch" formControlName="shareReviews"
|
|
||||||
aria-describedby="settings-share-reviews-help" class="form-check-input" aria-labelledby="auto-close-label">
|
|
||||||
<label class="form-check-label" for="share-reviews">{{t('share-series-reviews-label')}}</label>
|
|
||||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="shareReviewsTooltip" role="button" tabindex="0"></i>
|
|
||||||
</div>
|
|
||||||
<ng-template #shareReviewsTooltip>{{t('share-series-reviews-tooltip')}}</ng-template>
|
|
||||||
<span class="visually-hidden" id="settings-share-reviews-help">
|
|
||||||
<ng-container [ngTemplateOutlet]="shareReviewsTooltip"></ng-container>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
|
||||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
|
|
||||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ngbAccordionItem [id]="AccordionPanelID.ImageReader">
|
|
||||||
<h2 class="accordion-header" ngbAccordionHeader>
|
|
||||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
|
|
||||||
{{t('image-reader-settings-title')}}
|
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div ngbAccordionCollapse>
|
<div ngbAccordionCollapse>
|
||||||
@ -154,67 +38,241 @@
|
|||||||
<ng-template>
|
<ng-template>
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<label for="settings-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
<label for="settings-global-layoutmode"
|
||||||
<ng-template #readingDirectionTooltip>{{t('reading-direction-tooltip')}}</ng-template>
|
class="form-label">{{t('page-layout-mode-label')}}</label>
|
||||||
<span class="visually-hidden" id="settings-reading-direction-help">
|
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
<ng-container [ngTemplateOutlet]="readingDirectionTooltip"></ng-container>
|
[ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
||||||
</span>
|
<ng-template #layoutModeTooltip>{{t('page-layout-mode-tooltip')}}</ng-template>
|
||||||
<select class="form-select" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
<span class="visually-hidden" id="settings-global-layoutmode-help">
|
||||||
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
|
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
|
||||||
</select>
|
</span>
|
||||||
</div>
|
<select class="form-select" aria-describedby="manga-header"
|
||||||
|
formControlName="globalPageLayoutMode" id="settings-global-layoutmode">
|
||||||
|
<option *ngFor="let opt of pageLayoutModesTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<label for="settings-scaling-option" class="form-label">{{t('scaling-option-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="scalingOptionTooltip" role="button" tabindex="0"></i>
|
<label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label>
|
||||||
<ng-template #scalingOptionTooltip>{{t('scaling-option-tooltip')}}</ng-template>
|
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
<span class="visually-hidden" id="settings-scaling-option-help">
|
[ngbTooltip]="localeTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-container [ngTemplateOutlet]="scalingOptionTooltip"></ng-container>
|
<ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template>
|
||||||
</span>
|
<span class="visually-hidden" id="settings-global-locale-help">
|
||||||
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
|
<ng-container [ngTemplateOutlet]="localeTooltip"></ng-container>
|
||||||
<option *ngFor="let opt of scalingOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
|
</span>
|
||||||
</select>
|
<select class="form-select" aria-describedby="manga-header" formControlName="locale"
|
||||||
</div>
|
id="settings-global-locale">
|
||||||
</div>
|
<option *ngFor="let opt of locales" [value]="opt.isoCode">{{opt.title | titlecase}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" id="blur-unread-summaries" role="switch"
|
||||||
|
formControlName="blurUnreadSummaries" class="form-check-input"
|
||||||
|
aria-describedby="settings-global-blurUnreadSummaries-help" [value]="true"
|
||||||
|
aria-labelledby="auto-close-label">
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="blur-unread-summaries">{{t('blur-unread-summaries-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="blurUnreadSummariesTooltip" role="button" tabindex="0"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #blurUnreadSummariesTooltip>{{t('blur-unread-summaries-tooltip')}}
|
||||||
|
</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-global-blurUnreadSummaries-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="blurUnreadSummariesTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<label for="settings-pagesplit-option" class="form-label">{{t('page-splitting-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" id="prompt-download" role="switch"
|
||||||
|
formControlName="promptForDownloadSize" class="form-check-input"
|
||||||
|
aria-describedby="settings-global-promptForDownloadSize-help" [value]="true"
|
||||||
|
aria-labelledby="auto-close-label">
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="prompt-download">{{t('prompt-on-download-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="promptForDownloadSizeTooltip" role="button" tabindex="0"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #promptForDownloadSizeTooltip>
|
||||||
|
{{t('prompt-on-download-tooltip', {size: '100'})}}</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-global-promptForDownloadSize-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="promptForDownloadSizeTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" id="no-transitions" role="switch" formControlName="noTransitions"
|
||||||
|
class="form-check-input" aria-describedby="settings-global-noTransitions-help"
|
||||||
|
[value]="true" aria-labelledby="auto-close-label">
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="no-transitions">{{t('disable-animations-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="noTransitionsTooltip" role="button" tabindex="0"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #noTransitionsTooltip>{{t('disable-animations-tooltip')}}</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-global-noTransitions-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="noTransitionsTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" id="collapse-relationships" role="switch"
|
||||||
|
formControlName="collapseSeriesRelationships"
|
||||||
|
aria-describedby="settings-collapse-relationships-help" class="form-check-input"
|
||||||
|
aria-labelledby="auto-close-label">
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="collapse-relationships">{{t('collapse-series-relationships-label')}}</label>
|
||||||
|
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="collapseSeriesRelationshipsTooltip" role="button" tabindex="0"></i>
|
||||||
|
</div>
|
||||||
|
<ng-template #collapseSeriesRelationshipsTooltip>
|
||||||
|
{{t('collapse-series-relationships-tooltip')}}</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-collapse-relationships-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="collapseSeriesRelationshipsTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" id="share-reviews" role="switch" formControlName="shareReviews"
|
||||||
|
aria-describedby="settings-share-reviews-help" class="form-check-input"
|
||||||
|
aria-labelledby="auto-close-label">
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="share-reviews">{{t('share-series-reviews-label')}}</label>
|
||||||
|
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="shareReviewsTooltip" role="button" tabindex="0"></i>
|
||||||
|
</div>
|
||||||
|
<ng-template #shareReviewsTooltip>{{t('share-series-reviews-tooltip')}}</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-share-reviews-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="shareReviewsTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||||
|
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()"
|
||||||
|
aria-describedby="reading-panel">{{t('reset')}}</button>
|
||||||
|
<button type="submit" class="flex-fill btn btn-primary" (click)="save()"
|
||||||
|
aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ngbAccordionItem [id]="AccordionPanelID.ImageReader">
|
||||||
|
<h2 class="accordion-header" ngbAccordionHeader>
|
||||||
|
<button class="accordion-button" ngbAccordionButton type="button"
|
||||||
|
[attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
|
||||||
|
{{t('image-reader-settings-title')}}
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div ngbAccordionCollapse>
|
||||||
|
<div ngbAccordionBody>
|
||||||
|
<ng-template>
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
|
<label for="settings-reading-direction"
|
||||||
|
class="form-label">{{t('reading-direction-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #readingDirectionTooltip>{{t('reading-direction-tooltip')}}</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-reading-direction-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="readingDirectionTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
<select class="form-select" aria-describedby="manga-header"
|
||||||
|
formControlName="readingDirection" id="settings-reading-direction">
|
||||||
|
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
|
<label for="settings-scaling-option"
|
||||||
|
class="form-label">{{t('scaling-option-label')}}</label><i class="fa fa-info-circle ms-1"
|
||||||
|
aria-hidden="true" placement="right" [ngbTooltip]="scalingOptionTooltip" role="button"
|
||||||
|
tabindex="0"></i>
|
||||||
|
<ng-template #scalingOptionTooltip>{{t('scaling-option-tooltip')}}</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-scaling-option-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="scalingOptionTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption"
|
||||||
|
id="settings-scaling-option">
|
||||||
|
<option *ngFor="let opt of scalingOptionsTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
|
<label for="settings-pagesplit-option"
|
||||||
|
class="form-label">{{t('page-splitting-label')}}</label><i class="fa fa-info-circle ms-1"
|
||||||
|
aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button"
|
||||||
|
tabindex="0"></i>
|
||||||
<ng-template #pageSplitOptionTooltip>{{t('page-splitting-tooltip')}}</ng-template>
|
<ng-template #pageSplitOptionTooltip>{{t('page-splitting-tooltip')}}</ng-template>
|
||||||
<span class="visually-hidden" id="settings-pagesplit-option-help">
|
<span class="visually-hidden" id="settings-pagesplit-option-help">
|
||||||
<ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
|
<select class="form-select" aria-describedby="manga-header"
|
||||||
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
|
formControlName="pageSplitOption" id="settings-pagesplit-option">
|
||||||
|
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<label for="settings-readingmode-option" class="form-label">{{t('reading-mode-label')}}</label>
|
<label for="settings-readingmode-option"
|
||||||
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
|
class="form-label">{{t('reading-mode-label')}}</label>
|
||||||
<option *ngFor="let opt of readingModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
|
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode"
|
||||||
|
id="settings-readingmode-option">
|
||||||
|
<option *ngFor="let opt of readingModesTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="true">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="true">
|
||||||
<label for="settings-layoutmode-option" class="form-label">{{t('layout-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
<label for="settings-layoutmode-option"
|
||||||
|
class="form-label">{{t('layout-mode-label')}}</label><i class="fa fa-info-circle ms-1"
|
||||||
|
aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button"
|
||||||
|
tabindex="0"></i>
|
||||||
<ng-template #layoutModeTooltip>{{t('layout-mode-tooltip')}}</ng-template>
|
<ng-template #layoutModeTooltip>{{t('layout-mode-tooltip')}}</ng-template>
|
||||||
<span class="visually-hidden" id="settings-layoutmode-option-help">
|
<span class="visually-hidden" id="settings-layoutmode-option-help">
|
||||||
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
|
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode"
|
||||||
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
|
id="settings-layoutmode-option">
|
||||||
|
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<label for="settings-background-color-option" class="form-label">{{t('background-color-label')}}</label>
|
<label for="settings-background-color-option"
|
||||||
<input [value]="user!.preferences!.backgroundColor"
|
class="form-label">{{t('background-color-label')}}</label>
|
||||||
class="form-control"
|
<input [value]="user!.preferences!.backgroundColor" class="form-control"
|
||||||
id="settings-background-color-option"
|
id="settings-background-color-option" (colorPickerChange)="handleBackgroundColorChange()"
|
||||||
(colorPickerChange)="handleBackgroundColorChange()"
|
[style.background]="user!.preferences!.backgroundColor" [cpAlphaChannel]="'disabled'"
|
||||||
[style.background]="user!.preferences!.backgroundColor"
|
[(colorPicker)]="user!.preferences!.backgroundColor" />
|
||||||
[cpAlphaChannel]="'disabled'"
|
|
||||||
[(colorPicker)]="user!.preferences!.backgroundColor"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -222,7 +280,8 @@
|
|||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<div class="mb-3 mt-1">
|
<div class="mb-3 mt-1">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu"
|
||||||
|
class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||||
<label class="form-check-label" for="auto-close">{{t('auto-close-menu-label')}}</label>
|
<label class="form-check-label" for="auto-close">{{t('auto-close-menu-label')}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -230,8 +289,11 @@
|
|||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<div class="mb-3 mt-1">
|
<div class="mb-3 mt-1">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
<input type="checkbox" id="show-screen-hints" role="switch"
|
||||||
<label class="form-check-label" for="show-screen-hints">{{t('show-screen-hints-label')}}</label>
|
formControlName="showScreenHints" class="form-check-input" [value]="true"
|
||||||
|
aria-labelledby="auto-close-label">
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="show-screen-hints">{{t('show-screen-hints-label')}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -241,24 +303,36 @@
|
|||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<div class="mb-3 mt-1">
|
<div class="mb-3 mt-1">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook" class="form-check-input" [value]="true">
|
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook"
|
||||||
<label class="form-check-label me-1" for="emulate-book">{{t('emulate-comic-book-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button" tabindex="0"></i>
|
class="form-check-input" [value]="true">
|
||||||
|
<label class="form-check-label me-1"
|
||||||
|
for="emulate-book">{{t('emulate-comic-book-label')}}</label><i
|
||||||
|
class="fa fa-info-circle" aria-hidden="true" placement="top"
|
||||||
|
ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button"
|
||||||
|
tabindex="0"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<div class="mb-3 mt-1">
|
<div class="mb-3 mt-1">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input type="checkbox" id="swipe-to-paginate" role="switch" formControlName="swipeToPaginate" class="form-check-input" [value]="true">
|
<input type="checkbox" id="swipe-to-paginate" role="switch"
|
||||||
<label class="form-check-label me-1" for="swipe-to-paginate">{{t('swipe-to-paginate-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered" role="button" tabindex="0"></i>
|
formControlName="swipeToPaginate" class="form-check-input" [value]="true">
|
||||||
|
<label class="form-check-label me-1"
|
||||||
|
for="swipe-to-paginate">{{t('swipe-to-paginate-label')}}</label><i
|
||||||
|
class="fa fa-info-circle" aria-hidden="true" placement="top"
|
||||||
|
ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered"
|
||||||
|
role="button" tabindex="0"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
|
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()"
|
||||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
aria-describedby="reading-panel">{{t('reset')}}</button>
|
||||||
|
<button type="submit" class="flex-fill btn btn-primary" (click)="save()"
|
||||||
|
aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
@ -267,20 +341,26 @@
|
|||||||
|
|
||||||
<div ngbAccordionItem [id]="AccordionPanelID.BookReader">
|
<div ngbAccordionItem [id]="AccordionPanelID.BookReader">
|
||||||
<h2 class="accordion-header" ngbAccordionHeader>
|
<h2 class="accordion-header" ngbAccordionHeader>
|
||||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
|
<button class="accordion-button" ngbAccordionButton type="button"
|
||||||
{{t('book-reader-settings-title')}}
|
[attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
|
||||||
</button>
|
{{t('book-reader-settings-title')}}
|
||||||
</h2>
|
</button>
|
||||||
<div ngbAccordionCollapse>
|
</h2>
|
||||||
<div ngbAccordionBody>
|
<div ngbAccordionCollapse>
|
||||||
<ng-template>
|
<div ngbAccordionBody>
|
||||||
<div class="row g-0">
|
<ng-template>
|
||||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
<div class="row g-0">
|
||||||
<label id="taptopaginate-label" class="form-label"></label>
|
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||||
<div class="mb-3">
|
<label id="taptopaginate-label" class="form-label"></label>
|
||||||
<div class="form-check form-switch">
|
<div class="mb-3">
|
||||||
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
|
<div class="form-check form-switch">
|
||||||
<label for="taptopaginate" class="form-check-label">{{t('tap-to-paginate-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
<input type="checkbox" role="switch" id="taptopaginate"
|
||||||
|
formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true"
|
||||||
|
aria-labelledby="taptopaginate-label">
|
||||||
|
<label for="taptopaginate"
|
||||||
|
class="form-check-label">{{t('tap-to-paginate-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #tapToPaginateOptionTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
|
<ng-template #tapToPaginateOptionTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
|
||||||
<span class="visually-hidden" id="settings-taptopaginate-option-help">
|
<span class="visually-hidden" id="settings-taptopaginate-option-help">
|
||||||
<ng-container [ngTemplateOutlet]="tapToPaginateOptionTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="tapToPaginateOptionTooltip"></ng-container>
|
||||||
@ -292,8 +372,13 @@
|
|||||||
<label id="immersivemode-label" class="form-label"></label>
|
<label id="immersivemode-label" class="form-label"></label>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label">
|
<input type="checkbox" role="switch" id="immersivemode"
|
||||||
<label for="immersivemode" class="form-check-label">{{t('immersive-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
|
formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true"
|
||||||
|
aria-labelledby="immersivemode-label">
|
||||||
|
<label for="immersivemode"
|
||||||
|
class="form-check-label">{{t('immersive-mode-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #immersivemodeOptionTooltip>{{t('immersive-mode-label')}}</ng-template>
|
<ng-template #immersivemodeOptionTooltip>{{t('immersive-mode-label')}}</ng-template>
|
||||||
<span class="visually-hidden" id="settings-immersivemode-option-help">
|
<span class="visually-hidden" id="settings-immersivemode-option-help">
|
||||||
<ng-container [ngTemplateOutlet]="immersivemodeOptionTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="immersivemodeOptionTooltip"></ng-container>
|
||||||
@ -305,24 +390,35 @@
|
|||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
<label for="settings-book-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
<label for="settings-book-reading-direction"
|
||||||
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}</ng-template>
|
class="form-label">{{t('reading-direction-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}
|
||||||
|
</ng-template>
|
||||||
<span class="visually-hidden" id="settings-book-reading-direction-book-help">
|
<span class="visually-hidden" id="settings-book-reading-direction-book-help">
|
||||||
<ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
<select id="settings-book-reading-direction" class="form-select" aria-describedby="settings-book-reading-direction-help" formControlName="bookReaderReadingDirection">
|
<select id="settings-book-reading-direction" class="form-select"
|
||||||
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
|
aria-describedby="settings-book-reading-direction-help"
|
||||||
|
formControlName="bookReaderReadingDirection">
|
||||||
|
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
<label for="settings-fontfamily-option" class="form-label">{{t('font-family-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
|
<label for="settings-fontfamily-option"
|
||||||
|
class="form-label">{{t('font-family-label')}}</label><i class="fa fa-info-circle ms-1"
|
||||||
|
aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button"
|
||||||
|
tabindex="0"></i>
|
||||||
<ng-template #fontFamilyOptionTooltip>{{t('font-family-tooltip')}}</ng-template>
|
<ng-template #fontFamilyOptionTooltip>{{t('font-family-tooltip')}}</ng-template>
|
||||||
<span class="visually-hidden" id="settings-fontfamily-option-help">
|
<span class="visually-hidden" id="settings-fontfamily-option-help">
|
||||||
<ng-container [ngTemplateOutlet]="fontFamilyOptionTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="fontFamilyOptionTooltip"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
<select id="settings-fontfamily-option" class="form-select" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
<select id="settings-fontfamily-option" class="form-select"
|
||||||
|
aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
||||||
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -330,24 +426,35 @@
|
|||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
<label for="settings-book-writing-style" class="form-label me-1">{{t('writing-style-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" aria-describedby="settings-book-writing-style-help" placement="right" [ngbTooltip]="bookWritingStyleToolTip" role="button" tabindex="0"></i>
|
<label for="settings-book-writing-style"
|
||||||
|
class="form-label me-1">{{t('writing-style-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true"
|
||||||
|
aria-describedby="settings-book-writing-style-help" placement="right"
|
||||||
|
[ngbTooltip]="bookWritingStyleToolTip" role="button" tabindex="0"></i>
|
||||||
<ng-template #bookWritingStyleToolTip>{{t('writing-style-tooltip')}}</ng-template>
|
<ng-template #bookWritingStyleToolTip>{{t('writing-style-tooltip')}}</ng-template>
|
||||||
<span class="visually-hidden" id="settings-book-writing-style-help">
|
<span class="visually-hidden" id="settings-book-writing-style-help">
|
||||||
<ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container>
|
<ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
<select class="form-select" aria-describedby="settings-book-writing-style-help" formControlName="bookReaderWritingStyle" id="settings-book-writing-style" >
|
<select class="form-select" aria-describedby="settings-book-writing-style-help"
|
||||||
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
|
formControlName="bookReaderWritingStyle" id="settings-book-writing-style">
|
||||||
|
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
<label for="settings-book-layout-mode" class="form-label">{{t('layout-mode-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
|
<label for="settings-book-layout-mode"
|
||||||
|
class="form-label">{{t('layout-mode-book-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #bookLayoutModeTooltip>{{t('layout-mode-book-tooltip')}}</ng-template>
|
<ng-template #bookLayoutModeTooltip>{{t('layout-mode-book-tooltip')}}</ng-template>
|
||||||
<span class="visually-hidden" id="settings-book-layout-mode-help">
|
<span class="visually-hidden" id="settings-book-layout-mode-help">
|
||||||
<ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
|
<select class="form-select" aria-describedby="settings-book-layout-mode-help"
|
||||||
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
|
formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
|
||||||
|
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -355,13 +462,18 @@
|
|||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
<label for="settings-color-theme-option" class="form-label">{{t('color-theme-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
|
<label for="settings-color-theme-option"
|
||||||
|
class="form-label">{{t('color-theme-book-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #bookColorThemeTooltip>{{t('color-theme-book-tooltip')}}</ng-template>
|
<ng-template #bookColorThemeTooltip>{{t('color-theme-book-tooltip')}}</ng-template>
|
||||||
<span class="visually-hidden" id="settings-color-theme-option-help">
|
<span class="visually-hidden" id="settings-color-theme-option-help">
|
||||||
<ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
|
<select class="form-select" aria-describedby="settings-color-theme-option-help"
|
||||||
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">{{opt.name | titlecase}}</option>
|
formControlName="bookReaderThemeName" id="settings-color-theme-option">
|
||||||
|
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">
|
||||||
|
{{opt.name | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -369,79 +481,181 @@
|
|||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||||
<label for="fontsize" class="form-label range-label">{{t('font-size-book-label')}}</label>
|
<label for="fontsize" class="form-label range-label">{{t('font-size-book-label')}}</label>
|
||||||
<input type="range" class="form-range" id="fontsize"
|
<input type="range" class="form-range" id="fontsize" min="50" max="300" step="10"
|
||||||
min="50" max="300" step="10" formControlName="bookReaderFontSize">
|
formControlName="bookReaderFontSize">
|
||||||
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
|
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||||
<div class="range-label">
|
<div class="range-label">
|
||||||
<label class="form-label" for="linespacing">{{t('line-height-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
<label class="form-label" for="linespacing">{{t('line-height-book-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #bookLineHeightOptionTooltip>{{t('line-height-book-tooltip')}}</ng-template>
|
<ng-template #bookLineHeightOptionTooltip>{{t('line-height-book-tooltip')}}</ng-template>
|
||||||
<span class="visually-hidden" id="settings-booklineheight-option-help">
|
<span class="visually-hidden" id="settings-booklineheight-option-help">
|
||||||
<ng-container [ngTemplateOutlet]="bookLineHeightOptionTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="bookLineHeightOptionTooltip"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
|
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
|
||||||
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help">
|
formControlName="bookReaderLineSpacing"
|
||||||
|
aria-describedby="settings-booklineheight-option-help">
|
||||||
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
|
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||||
<div class="range-label">
|
<div class="range-label">
|
||||||
<label class="form-label">{{t('margin-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
<label class="form-label">{{t('margin-book-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #bookReaderMarginOptionTooltip>{{t('margin-book-tooltip')}}</ng-template>
|
<ng-template #bookReaderMarginOptionTooltip>{{t('margin-book-tooltip')}}</ng-template>
|
||||||
<span class="visually-hidden" id="settings-bookmargin-option-help">
|
<span class="visually-hidden" id="settings-bookmargin-option-help">
|
||||||
<ng-container [ngTemplateOutlet]="bookReaderMarginOptionTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="bookReaderMarginOptionTooltip"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin">
|
<input type="range" class="form-range" id="margin" min="0" max="30" step="5"
|
||||||
|
formControlName="bookReaderMargin" aria-describedby="bookmargin">
|
||||||
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
|
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
|
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()"
|
||||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
aria-describedby="reading-panel">{{t('reset')}}</button>
|
||||||
|
<button type="submit" class="flex-fill btn btn-primary" (click)="save()"
|
||||||
|
aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ngbAccordionItem [id]="AccordionPanelID.PdfReader">
|
||||||
|
<h2 class="accordion-header" ngbAccordionHeader>
|
||||||
|
<button class="accordion-button" ngbAccordionButton type="button"
|
||||||
|
[attr.aria-expanded]="acc.isExpanded(AccordionPanelID.PdfReader)" aria-controls="collapseOne">
|
||||||
|
{{t('pdf-reader-settings-title')}}
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div ngbAccordionCollapse>
|
||||||
|
<div ngbAccordionBody>
|
||||||
|
<ng-template>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
|
<label for="settings-pdf-layout-mode" class="form-label">{{t('pdf-layout-mode-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="pdfLayoutModeTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #pdfLayoutModeTooltip>{{t('pdf-layout-mode-tooltip')}}
|
||||||
|
</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-pdf-layout-mode-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="pdfLayoutModeTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
<select id="settings-pdf-layout-mode" class="form-select"
|
||||||
|
aria-describedby="settings-pdf-layout-mode-help"
|
||||||
|
formControlName="pdfLayoutMode">
|
||||||
|
<option *ngFor="let opt of pdfLayoutModesTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
|
<label for="settings-pdf-scroll-mode" class="form-label">{{t('pdf-scroll-mode-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="pdfScrollModeTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #pdfScrollModeTooltip>{{t('pdf-scroll-mode-tooltip')}}
|
||||||
|
</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-pdf-scroll-mode-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="pdfScrollModeTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
<select id="settings-pdf-scroll-mode" class="form-select"
|
||||||
|
aria-describedby="settings-pdf-scroll-mode-help"
|
||||||
|
formControlName="pdfScrollMode">
|
||||||
|
<option *ngFor="let opt of pdfScrollModesTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
|
<label for="settings-pdf-spread-mode" class="form-label">{{t('pdf-spread-mode-label')}}</label><i
|
||||||
|
class="fa fa-info-circle ms-1" aria-hidden="true" placement="right"
|
||||||
|
[ngbTooltip]="pdfSpreadModeTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #pdfSpreadModeTooltip>{{t('pdf-spread-mode-tooltip')}}
|
||||||
|
</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-pdf-spread-mode-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="pdfSpreadModeTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
<select id="settings-pdf-spread-mode" class="form-select"
|
||||||
|
aria-describedby="settings-pdf-spread-mode-help"
|
||||||
|
formControlName="pdfSpreadMode">
|
||||||
|
<option *ngFor="let opt of pdfSpreadModesTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
|
<label for="settings-pdf-theme" class="form-label">{{t('pdf-theme-label')}}</label>
|
||||||
|
<select id="settings-pdf-theme" class="form-select"
|
||||||
|
formControlName="pdfTheme">
|
||||||
|
<option *ngFor="let opt of pdfThemesTranslated" [value]="opt.value">
|
||||||
|
{{opt.text | titlecase}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||||
|
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()"
|
||||||
|
aria-describedby="reading-panel">{{t('reset')}}</button>
|
||||||
|
<button type="submit" class="flex-fill btn btn-primary" (click)="save()"
|
||||||
|
aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
}
|
}
|
||||||
|
|
||||||
@defer (when tab.fragment === FragmentID.Clients; prefetch on idle) {
|
@defer (when tab.fragment === FragmentID.Clients; prefetch on idle) {
|
||||||
<div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">{{t('clients-opds-alert')}}</div>
|
<div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">{{t('clients-opds-alert')}}</div>
|
||||||
<p>{{t('clients-opds-description')}}</p>
|
<p>{{t('clients-opds-description')}}</p>
|
||||||
<app-api-key [tooltipText]="t('clients-api-key-tooltip')" [hideData]="true"></app-api-key>
|
<app-api-key [tooltipText]="t('clients-api-key-tooltip')" [hideData]="true"></app-api-key>
|
||||||
<app-api-key [title]="t('clients-opds-url-tooltip')" [hideData]="true" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
|
<app-api-key [title]="t('clients-opds-url-tooltip')" [hideData]="true" [showRefresh]="false"
|
||||||
}
|
[transform]="makeUrl"></app-api-key>
|
||||||
@defer (when tab.fragment === FragmentID.Theme; prefetch on idle) {
|
}
|
||||||
<app-theme-manager></app-theme-manager>
|
@defer (when tab.fragment === FragmentID.Theme; prefetch on idle) {
|
||||||
}
|
<app-theme-manager></app-theme-manager>
|
||||||
|
}
|
||||||
|
|
||||||
@defer (when tab.fragment === FragmentID.Devices; prefetch on idle) {
|
@defer (when tab.fragment === FragmentID.Devices; prefetch on idle) {
|
||||||
<app-manage-devices></app-manage-devices>
|
<app-manage-devices></app-manage-devices>
|
||||||
}
|
}
|
||||||
|
|
||||||
@defer (when tab.fragment === FragmentID.Stats; prefetch on idle) {
|
@defer (when tab.fragment === FragmentID.Stats; prefetch on idle) {
|
||||||
<app-user-stats></app-user-stats>
|
<app-user-stats></app-user-stats>
|
||||||
}
|
}
|
||||||
|
|
||||||
@defer (when tab.fragment === FragmentID.Scrobbling; prefetch on idle) {
|
@defer (when tab.fragment === FragmentID.Scrobbling; prefetch on idle) {
|
||||||
@if(hasActiveLicense) {
|
@if(hasActiveLicense) {
|
||||||
<app-user-scrobble-history></app-user-scrobble-history>
|
<app-user-scrobble-history></app-user-scrobble-history>
|
||||||
<app-user-holds></app-user-holds>
|
<app-user-holds></app-user-holds>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -7,54 +7,82 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import {ToastrService} from 'ngx-toastr';
|
||||||
import {take} from 'rxjs/operators';
|
import {take} from 'rxjs/operators';
|
||||||
import { Title } from '@angular/platform-browser';
|
import {Title} from '@angular/platform-browser';
|
||||||
import {
|
import {
|
||||||
readingDirections,
|
|
||||||
scalingOptions,
|
|
||||||
pageSplitOptions,
|
|
||||||
readingModes,
|
|
||||||
Preferences,
|
|
||||||
bookLayoutModes,
|
bookLayoutModes,
|
||||||
|
bookWritingStyles,
|
||||||
layoutModes,
|
layoutModes,
|
||||||
pageLayoutModes,
|
pageLayoutModes,
|
||||||
bookWritingStyles
|
pageSplitOptions,
|
||||||
|
pdfLayoutModes,
|
||||||
|
pdfScrollModes,
|
||||||
|
pdfSpreadModes,
|
||||||
|
pdfThemes,
|
||||||
|
Preferences,
|
||||||
|
readingDirections,
|
||||||
|
readingModes,
|
||||||
|
scalingOptions
|
||||||
} from 'src/app/_models/preferences/preferences';
|
} from 'src/app/_models/preferences/preferences';
|
||||||
import { User } from 'src/app/_models/user';
|
import {User} from 'src/app/_models/user';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import {AccountService} from 'src/app/_services/account.service';
|
||||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
|
||||||
import { SettingsService } from 'src/app/admin/settings.service';
|
import {SettingsService} from 'src/app/admin/settings.service';
|
||||||
import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode';
|
import {BookPageLayoutMode} from 'src/app/_models/readers/book-page-layout-mode';
|
||||||
import {forkJoin} from 'rxjs';
|
import {forkJoin} from 'rxjs';
|
||||||
import { bookColorThemes } from 'src/app/book-reader/_components/reader-settings/reader-settings.component';
|
import {bookColorThemes} from 'src/app/book-reader/_components/reader-settings/reader-settings.component';
|
||||||
import { BookService } from 'src/app/book-reader/_services/book.service';
|
import {BookService} from 'src/app/book-reader/_services/book.service';
|
||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe';
|
import {SentenceCasePipe} from '../../_pipes/sentence-case.pipe';
|
||||||
import { UserHoldsComponent } from '../user-holds/user-holds.component';
|
import {UserHoldsComponent} from '../user-holds/user-holds.component';
|
||||||
import { UserScrobbleHistoryComponent } from '../../_single-module/user-scrobble-history/user-scrobble-history.component';
|
import {UserScrobbleHistoryComponent} from '../../_single-module/user-scrobble-history/user-scrobble-history.component';
|
||||||
import { UserStatsComponent } from '../../statistics/_components/user-stats/user-stats.component';
|
import {UserStatsComponent} from '../../statistics/_components/user-stats/user-stats.component';
|
||||||
import { ManageDevicesComponent } from '../manage-devices/manage-devices.component';
|
import {ManageDevicesComponent} from '../manage-devices/manage-devices.component';
|
||||||
import { ThemeManagerComponent } from '../theme-manager/theme-manager.component';
|
import {ThemeManagerComponent} from '../theme-manager/theme-manager.component';
|
||||||
import { ApiKeyComponent } from '../api-key/api-key.component';
|
import {ApiKeyComponent} from '../api-key/api-key.component';
|
||||||
import { ColorPickerModule } from 'ngx-color-picker';
|
import {ColorPickerModule} from 'ngx-color-picker';
|
||||||
import { ChangeAgeRestrictionComponent } from '../change-age-restriction/change-age-restriction.component';
|
import {ChangeAgeRestrictionComponent} from '../change-age-restriction/change-age-restriction.component';
|
||||||
import { ChangePasswordComponent } from '../change-password/change-password.component';
|
import {ChangePasswordComponent} from '../change-password/change-password.component';
|
||||||
import { ChangeEmailComponent } from '../change-email/change-email.component';
|
import {ChangeEmailComponent} from '../change-email/change-email.component';
|
||||||
import { NgFor, NgIf, NgTemplateOutlet, TitleCasePipe } from '@angular/common';
|
import {NgFor, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
|
||||||
import { NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgbNavOutlet } from '@ng-bootstrap/ng-bootstrap';
|
import {
|
||||||
import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
NgbAccordionBody,
|
||||||
|
NgbAccordionButton,
|
||||||
|
NgbAccordionCollapse,
|
||||||
|
NgbAccordionDirective,
|
||||||
|
NgbAccordionHeader,
|
||||||
|
NgbAccordionItem,
|
||||||
|
NgbAccordionToggle,
|
||||||
|
NgbCollapse,
|
||||||
|
NgbNav,
|
||||||
|
NgbNavContent,
|
||||||
|
NgbNavItem,
|
||||||
|
NgbNavItemRole,
|
||||||
|
NgbNavLink,
|
||||||
|
NgbNavOutlet,
|
||||||
|
NgbTooltip
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {
|
||||||
|
SideNavCompanionBarComponent
|
||||||
|
} from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||||
import {LocalizationService} from "../../_services/localization.service";
|
import {LocalizationService} from "../../_services/localization.service";
|
||||||
import {Language} from "../../_models/metadata/language";
|
import {Language} from "../../_models/metadata/language";
|
||||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||||
import {LoadingComponent} from "../../shared/loading/loading.component";
|
import {LoadingComponent} from "../../shared/loading/loading.component";
|
||||||
import {ManageScrobblingProvidersComponent} from "../manage-scrobbling-providers/manage-scrobbling-providers.component";
|
import {ManageScrobblingProvidersComponent} from "../manage-scrobbling-providers/manage-scrobbling-providers.component";
|
||||||
|
import {PdfLayoutModePipe} from "../../pdf-reader/_pipe/pdf-layout-mode.pipe";
|
||||||
|
import {PdfTheme} from "../../_models/preferences/pdf-theme";
|
||||||
|
import {PdfScrollMode} from "../../_models/preferences/pdf-scroll-mode";
|
||||||
|
import {PdfLayoutMode} from "../../_models/preferences/pdf-layout-mode";
|
||||||
|
import {PdfSpreadMode} from "../../_models/preferences/pdf-spread-mode";
|
||||||
|
|
||||||
enum AccordionPanelID {
|
enum AccordionPanelID {
|
||||||
ImageReader = 'image-reader',
|
ImageReader = 'image-reader',
|
||||||
BookReader = 'book-reader',
|
BookReader = 'book-reader',
|
||||||
GlobalSettings = 'global-settings'
|
GlobalSettings = 'global-settings',
|
||||||
|
PdfReader = 'pdf-reader'
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FragmentID {
|
enum FragmentID {
|
||||||
@ -77,7 +105,7 @@ enum FragmentID {
|
|||||||
ChangePasswordComponent, ChangeAgeRestrictionComponent, ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader,
|
ChangePasswordComponent, ChangeAgeRestrictionComponent, ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader,
|
||||||
NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, ColorPickerModule, ApiKeyComponent,
|
NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, ColorPickerModule, ApiKeyComponent,
|
||||||
ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe,
|
ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe,
|
||||||
TranslocoDirective, LoadingComponent, ManageScrobblingProvidersComponent],
|
TranslocoDirective, LoadingComponent, ManageScrobblingProvidersComponent, PdfLayoutModePipe],
|
||||||
})
|
})
|
||||||
export class UserPreferencesComponent implements OnInit, OnDestroy {
|
export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@ -108,6 +136,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
pageLayoutModesTranslated = pageLayoutModes.map(this.translatePrefOptions);
|
pageLayoutModesTranslated = pageLayoutModes.map(this.translatePrefOptions);
|
||||||
bookWritingStylesTranslated = bookWritingStyles.map(this.translatePrefOptions);
|
bookWritingStylesTranslated = bookWritingStyles.map(this.translatePrefOptions);
|
||||||
|
pdfLayoutModesTranslated = pdfLayoutModes.map(this.translatePrefOptions);
|
||||||
|
pdfScrollModesTranslated = pdfScrollModes.map(this.translatePrefOptions);
|
||||||
|
pdfSpreadModesTranslated = pdfSpreadModes.map(this.translatePrefOptions);
|
||||||
|
pdfThemesTranslated = pdfThemes.map(this.translatePrefOptions);
|
||||||
|
|
||||||
|
|
||||||
settingsForm: FormGroup = new FormGroup({});
|
settingsForm: FormGroup = new FormGroup({});
|
||||||
user: User | undefined = undefined;
|
user: User | undefined = undefined;
|
||||||
@ -129,7 +162,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
opdsUrl: string = '';
|
opdsUrl: string = '';
|
||||||
makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; };
|
makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; };
|
||||||
hasActiveLicense = false;
|
hasActiveLicense = false;
|
||||||
canEdit = true;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -214,11 +246,16 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
|
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
|
||||||
this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, []));
|
this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, []));
|
||||||
this.settingsForm.addControl('bookReaderWritingStyle', new FormControl(this.user.preferences.bookReaderWritingStyle, []))
|
this.settingsForm.addControl('bookReaderWritingStyle', new FormControl(this.user.preferences.bookReaderWritingStyle, []))
|
||||||
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(!!this.user.preferences.bookReaderTapToPaginate, []));
|
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.user.preferences.bookReaderTapToPaginate, []));
|
||||||
this.settingsForm.addControl('bookReaderLayoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
|
this.settingsForm.addControl('bookReaderLayoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
|
||||||
this.settingsForm.addControl('bookReaderThemeName', new FormControl(this.user?.preferences.bookReaderThemeName || bookColorThemes[0].name, []));
|
this.settingsForm.addControl('bookReaderThemeName', new FormControl(this.user?.preferences.bookReaderThemeName || bookColorThemes[0].name, []));
|
||||||
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user?.preferences.bookReaderImmersiveMode, []));
|
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user?.preferences.bookReaderImmersiveMode, []));
|
||||||
|
|
||||||
|
this.settingsForm.addControl('pdfTheme', new FormControl(this.user?.preferences.pdfTheme || PdfTheme.Dark, []));
|
||||||
|
this.settingsForm.addControl('pdfScrollMode', new FormControl(this.user?.preferences.pdfScrollMode || PdfScrollMode.Vertical, []));
|
||||||
|
this.settingsForm.addControl('pdfLayoutMode', new FormControl(this.user?.preferences.pdfLayoutMode || PdfLayoutMode.Multiple, []));
|
||||||
|
this.settingsForm.addControl('pdfSpreadMode', new FormControl(this.user?.preferences.pdfSpreadMode || PdfSpreadMode.None, []));
|
||||||
|
|
||||||
this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, []));
|
this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, []));
|
||||||
this.settingsForm.addControl('globalPageLayoutMode', new FormControl(this.user.preferences.globalPageLayoutMode, []));
|
this.settingsForm.addControl('globalPageLayoutMode', new FormControl(this.user.preferences.globalPageLayoutMode, []));
|
||||||
this.settingsForm.addControl('blurUnreadSummaries', new FormControl(this.user.preferences.blurUnreadSummaries, []));
|
this.settingsForm.addControl('blurUnreadSummaries', new FormControl(this.user.preferences.blurUnreadSummaries, []));
|
||||||
@ -278,6 +315,12 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.settingsForm.get('collapseSeriesRelationships')?.setValue(this.user.preferences.collapseSeriesRelationships);
|
this.settingsForm.get('collapseSeriesRelationships')?.setValue(this.user.preferences.collapseSeriesRelationships);
|
||||||
this.settingsForm.get('shareReviews')?.setValue(this.user.preferences.shareReviews);
|
this.settingsForm.get('shareReviews')?.setValue(this.user.preferences.shareReviews);
|
||||||
this.settingsForm.get('locale')?.setValue(this.user.preferences.locale);
|
this.settingsForm.get('locale')?.setValue(this.user.preferences.locale);
|
||||||
|
|
||||||
|
this.settingsForm.get('pdfTheme')?.setValue(this.user.preferences.pdfTheme);
|
||||||
|
this.settingsForm.get('pdfScrollMode')?.setValue(this.user.preferences.pdfScrollMode);
|
||||||
|
this.settingsForm.get('pdfLayoutMode')?.setValue(this.user.preferences.pdfLayoutMode);
|
||||||
|
this.settingsForm.get('pdfSpreadMode')?.setValue(this.user.preferences.pdfSpreadMode);
|
||||||
|
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
this.settingsForm.markAsPristine();
|
this.settingsForm.markAsPristine();
|
||||||
}
|
}
|
||||||
@ -313,7 +356,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
swipeToPaginate: modelSettings.swipeToPaginate,
|
swipeToPaginate: modelSettings.swipeToPaginate,
|
||||||
collapseSeriesRelationships: modelSettings.collapseSeriesRelationships,
|
collapseSeriesRelationships: modelSettings.collapseSeriesRelationships,
|
||||||
shareReviews: modelSettings.shareReviews,
|
shareReviews: modelSettings.shareReviews,
|
||||||
locale: modelSettings.locale
|
locale: modelSettings.locale,
|
||||||
|
pdfTheme: parseInt(modelSettings.pdfTheme, 10),
|
||||||
|
pdfScrollMode: parseInt(modelSettings.pdfScrollMode, 10),
|
||||||
|
pdfLayoutMode: parseInt(modelSettings.pdfLayoutMode, 10),
|
||||||
|
pdfSpreadMode: parseInt(modelSettings.pdfSpreadMode, 10),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||||
|
@ -159,6 +159,15 @@
|
|||||||
"margin-book-label": "Margin",
|
"margin-book-label": "Margin",
|
||||||
"margin-book-tooltip": "How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.",
|
"margin-book-tooltip": "How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.",
|
||||||
|
|
||||||
|
"pdf-reader-settings-title": "PDF Reader",
|
||||||
|
"pdf-layout-mode-label": "Layout Mode",
|
||||||
|
"pdf-layout-mode-tooltip": "How the reader lays the pdf out. Default is pages stacked with scrolling and Book emulates a physical book",
|
||||||
|
"pdf-scroll-mode-label": "Scroll Mode",
|
||||||
|
"pdf-scroll-mode-tooltip": "How you scroll through pages. Vertical/Horizontal and Tap to Paginate (no scroll)",
|
||||||
|
"pdf-spread-mode-label": "Spread Mode",
|
||||||
|
"pdf-spread-mode-tooltip": "How pages should be laid out. Single or double (odd/even)",
|
||||||
|
"pdf-theme-label": "Theme",
|
||||||
|
|
||||||
"clients-opds-alert": "OPDS is not enabled on this server. This will not affect Tachiyomi users.",
|
"clients-opds-alert": "OPDS is not enabled on this server. This will not affect Tachiyomi users.",
|
||||||
"clients-opds-description": "All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.",
|
"clients-opds-description": "All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.",
|
||||||
"clients-api-key-tooltip": "The API key is like a password. Keep it secret, Keep it safe.",
|
"clients-api-key-tooltip": "The API key is like a password. Keep it secret, Keep it safe.",
|
||||||
@ -1600,7 +1609,28 @@
|
|||||||
"incognito-mode": "Incognito Mode",
|
"incognito-mode": "Incognito Mode",
|
||||||
"light-theme-alt": "Light Theme",
|
"light-theme-alt": "Light Theme",
|
||||||
"dark-theme-alt": "Dark Theme",
|
"dark-theme-alt": "Dark Theme",
|
||||||
"close-reader-alt": "Close Reader"
|
"close-reader-alt": "Close Reader",
|
||||||
|
"toggle-incognito": "Turn off Incognito Mode"
|
||||||
|
},
|
||||||
|
|
||||||
|
"pdf-layout-mode-pipe": {
|
||||||
|
"single": "Single Page",
|
||||||
|
"book": "Book mode",
|
||||||
|
"multiple": "Default",
|
||||||
|
"infinite-scroll": "Infinite Scroll"
|
||||||
|
},
|
||||||
|
|
||||||
|
"pdf-scroll-mode-pipe": {
|
||||||
|
"vertical": "Vertical",
|
||||||
|
"horizontal": "Horizontal",
|
||||||
|
"wrapped": "Wrapped",
|
||||||
|
"page": "Tap to Paginate"
|
||||||
|
},
|
||||||
|
|
||||||
|
"pdf-spread-mode-pipe": {
|
||||||
|
"off": "No Spreads",
|
||||||
|
"odd": "Odd Spreads",
|
||||||
|
"even": "Even Spreads"
|
||||||
},
|
},
|
||||||
|
|
||||||
"infinite-scroller": {
|
"infinite-scroller": {
|
||||||
@ -2189,7 +2219,17 @@
|
|||||||
"2-column": "2 Column",
|
"2-column": "2 Column",
|
||||||
"cards": "Cards",
|
"cards": "Cards",
|
||||||
"list": "List",
|
"list": "List",
|
||||||
"up-to-down": "Up to Down"
|
"up-to-down": "Up to Down",
|
||||||
|
"pdf-multiple": "Default",
|
||||||
|
"pdf-book": "Book",
|
||||||
|
"pdf-vertical": "Scroll Vertical",
|
||||||
|
"pdf-horizontal": "Scroll Horizontal",
|
||||||
|
"pdf-page": "Tap to Paginate",
|
||||||
|
"pdf-none": "None",
|
||||||
|
"pdf-odd": "Odd",
|
||||||
|
"pdf-even": "Even",
|
||||||
|
"pdf-light": "Light",
|
||||||
|
"pdf-dark": "Dark"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||||
// The list of file replacements can be found in `angular.json`.
|
// The list of file replacements can be found in `angular.json`.
|
||||||
|
|
||||||
|
const IP = 'localhost';
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiUrl: 'http://localhost:5000/api/',
|
apiUrl: 'http://' + IP + ':5000/api/',
|
||||||
hubUrl: 'http://localhost:5000/hubs/',
|
hubUrl: 'http://'+ IP + ':5000/hubs/',
|
||||||
buyLink: 'https://buy.stripe.com/test_9AQ5mi058h1PcIo3cf?prefilled_promo_code=FREETRIAL',
|
buyLink: 'https://buy.stripe.com/test_9AQ5mi058h1PcIo3cf?prefilled_promo_code=FREETRIAL',
|
||||||
manageLink: 'https://billing.stripe.com/p/login/test_14kfZocuh6Tz5ag7ss'
|
manageLink: 'https://billing.stripe.com/p/login/test_14kfZocuh6Tz5ag7ss'
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,8 @@ export class HttpLoader implements TranslocoLoader {
|
|||||||
getTranslation(langPath: string) {
|
getTranslation(langPath: string) {
|
||||||
const tokens = langPath.split('/');
|
const tokens = langPath.split('/');
|
||||||
const langCode = tokens[tokens.length - 1];
|
const langCode = tokens[tokens.length - 1];
|
||||||
return this.http.get<Translation>(`assets/langs/${langCode}.json?v=${(cacheBusting as { [key: string]: string })[langCode]}`);
|
const url = `assets/langs/${langCode}.json?v=${(cacheBusting as { [key: string]: string })[langCode]}`;
|
||||||
|
console.log('loading locale: ', url);
|
||||||
|
return this.http.get<Translation>(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,13 @@ import {provideTranslocoLocale} from "@ngneat/transloco-locale";
|
|||||||
import {provideTranslocoPersistTranslations} from "@ngneat/transloco-persist-translations";
|
import {provideTranslocoPersistTranslations} from "@ngneat/transloco-persist-translations";
|
||||||
import {LazyLoadImageModule} from "ng-lazyload-image";
|
import {LazyLoadImageModule} from "ng-lazyload-image";
|
||||||
import {getSaver, SAVER} from "./app/_providers/saver.provider";
|
import {getSaver, SAVER} from "./app/_providers/saver.provider";
|
||||||
|
import {distinctUntilChanged} from "rxjs/operators";
|
||||||
|
|
||||||
const disableAnimations = !('animate' in document.documentElement);
|
const disableAnimations = !('animate' in document.documentElement);
|
||||||
|
|
||||||
export function preloadUser(userService: AccountService, transloco: TranslocoService) {
|
export function preloadUser(userService: AccountService, transloco: TranslocoService) {
|
||||||
return function() {
|
return function() {
|
||||||
return userService.currentUser$.pipe(switchMap((user) => {
|
return userService.currentUser$.pipe(distinctUntilChanged(), switchMap((user) => {
|
||||||
if (user && user.preferences.locale) {
|
if (user && user.preferences.locale) {
|
||||||
transloco.setActiveLang(user.preferences.locale);
|
transloco.setActiveLang(user.preferences.locale);
|
||||||
return transloco.load(user.preferences.locale)
|
return transloco.load(user.preferences.locale)
|
||||||
|
88
openapi.json
88
openapi.json
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.7.14.9"
|
"version": "0.7.14.10"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@ -13300,9 +13300,6 @@
|
|||||||
"description": "Book Reader Option: Defines the writing styles vertical/horizontal",
|
"description": "Book Reader Option: Defines the writing styles vertical/horizontal",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
},
|
},
|
||||||
"theme": {
|
|
||||||
"$ref": "#/components/schemas/SiteTheme"
|
|
||||||
},
|
|
||||||
"bookThemeName": {
|
"bookThemeName": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Book Reader Option: The color theme to decorate the book contents",
|
"description": "Book Reader Option: The color theme to decorate the book contents",
|
||||||
@ -13322,6 +13319,47 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this."
|
"description": "Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this."
|
||||||
},
|
},
|
||||||
|
"pdfTheme": {
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"type": "integer",
|
||||||
|
"description": "PDF Reader: Theme of the Reader",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"pdfScrollMode": {
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"type": "integer",
|
||||||
|
"description": "PDF Reader: Scroll mode of the reader",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"pdfLayoutMode": {
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "integer",
|
||||||
|
"description": "PDF Reader: Layout Mode of the reader",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"pdfSpreadMode": {
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "integer",
|
||||||
|
"description": "PDF Reader: Spread Mode of the reader",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"$ref": "#/components/schemas/SiteTheme"
|
||||||
|
},
|
||||||
"globalPageLayoutMode": {
|
"globalPageLayoutMode": {
|
||||||
"enum": [
|
"enum": [
|
||||||
0,
|
0,
|
||||||
@ -20626,6 +20664,10 @@
|
|||||||
"locale",
|
"locale",
|
||||||
"noTransitions",
|
"noTransitions",
|
||||||
"pageSplitOption",
|
"pageSplitOption",
|
||||||
|
"pdfLayoutMode",
|
||||||
|
"pdfScrollMode",
|
||||||
|
"pdfSpreadMode",
|
||||||
|
"pdfTheme",
|
||||||
"promptForDownloadSize",
|
"promptForDownloadSize",
|
||||||
"readerMode",
|
"readerMode",
|
||||||
"readingDirection",
|
"readingDirection",
|
||||||
@ -20804,6 +20846,44 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "UI Site Global Setting: The language locale that should be used for the user"
|
"description": "UI Site Global Setting: The language locale that should be used for the user"
|
||||||
|
},
|
||||||
|
"pdfTheme": {
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"type": "integer",
|
||||||
|
"description": "PDF Reader: Theme of the Reader",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"pdfScrollMode": {
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"type": "integer",
|
||||||
|
"description": "PDF Reader: Scroll mode of the reader",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"pdfLayoutMode": {
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "integer",
|
||||||
|
"description": "PDF Reader: Layout Mode of the reader",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"pdfSpreadMode": {
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "integer",
|
||||||
|
"description": "PDF Reader: Spread Mode of the reader",
|
||||||
|
"format": "int32"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user