Polish Round 4 (#2429)

This commit is contained in:
Joe Milazzo 2023-11-12 08:29:46 -06:00 committed by GitHub
parent cd7f876140
commit ee72727841
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 666 additions and 599 deletions

View File

@ -19,3 +19,6 @@ trim_trailing_whitespace = false
[*.yml] [*.yml]
indent_size = 2 indent_size = 2
[*.csproj]
indent_size = 2

View File

@ -62,7 +62,7 @@ public class DefaultParserTests
[Theory] [Theory]
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!")] [InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!")]
[InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!")] [InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!")]
[InlineData("/manga/Monster #8 (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")] [InlineData("/manga/Monster #8 (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "manga")]
[InlineData("/manga/Monster (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")] [InlineData("/manga/Monster (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")]
[InlineData("/manga/Foo 50/Specials/Foo 50 SP01.cbz", "Foo 50")] [InlineData("/manga/Foo 50/Specials/Foo 50 SP01.cbz", "Foo 50")]
[InlineData("/manga/Foo 50 (kiraa)/Specials/Foo 50 SP01.cbz", "Foo 50")] [InlineData("/manga/Foo 50 (kiraa)/Specials/Foo 50 SP01.cbz", "Foo 50")]
@ -293,7 +293,7 @@ public class DefaultParserTests
var expectedInfo2 = new ParserInfo var expectedInfo2 = new ParserInfo
{ {
Series = "Monster #8", Volumes = "0", Edition = "", Series = "Monster #8", Volumes = "0", Edition = "",
Chapters = "1", Filename = "13.jpg", Format = MangaFormat.Image, Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}; };
var actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Monster #8"); var actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Monster #8");
@ -314,7 +314,7 @@ public class DefaultParserTests
Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath); Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓"); _testOutputHelper.WriteLine("FullFilePath ✓");
filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Vol19\ch186\Vol. 19 p106.gif"; filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Vol19\ch. 186\Vol. 19 p106.gif";
expectedInfo2 = new ParserInfo expectedInfo2 = new ParserInfo
{ {
Series = "Just Images the second", Volumes = "19", Edition = "", Series = "Just Images the second", Volumes = "19", Edition = "",
@ -340,7 +340,7 @@ public class DefaultParserTests
Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath); Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓"); _testOutputHelper.WriteLine("FullFilePath ✓");
filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Blank Folder\Vol19\ch186\Vol. 19 p106.gif"; filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Blank Folder\Vol19\ch. 186\Vol. 19 p106.gif";
expectedInfo2 = new ParserInfo expectedInfo2 = new ParserInfo
{ {
Series = "Just Images the second", Volumes = "19", Edition = "", Series = "Just Images the second", Volumes = "19", Edition = "",

View File

@ -657,7 +657,7 @@ public class SeriesServiceTests : AbstractDbTest
{ {
SeriesId = 1, SeriesId = 1,
Publishers = new List<PersonDto>() {new () {Id = 0, Name = "Existing Person", Role = PersonRole.Publisher}}, Publishers = new List<PersonDto>() {new () {Id = 0, Name = "Existing Person", Role = PersonRole.Publisher}},
PublishersLocked = true PublisherLocked = true
}, },
CollectionTags = new List<CollectionTagDto>() CollectionTags = new List<CollectionTagDto>()
}); });

View File

@ -53,6 +53,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Docnet.Core" Version="2.6.0" /> <PackageReference Include="Docnet.Core" Version="2.6.0" />
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" /> <PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
@ -60,28 +64,15 @@
<PackageReference Include="Flurl" Version="3.0.7" /> <PackageReference Include="Flurl" Version="3.0.7" />
<PackageReference Include="Flurl.Http" Version="3.2.4" /> <PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Hangfire" Version="1.8.6" /> <PackageReference Include="Hangfire" Version="1.8.6" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.6" />
<PackageReference Include="Hangfire.InMemory" Version="0.6.0" /> <PackageReference Include="Hangfire.InMemory" Version="0.6.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" /> <PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" /> <PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.4" /> <PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.4" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.54" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" /> <PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
<PackageReference Include="ExCSS" Version="4.2.4" />
<PackageReference Include="Hangfire" Version="1.8.6" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.6" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.8.6" />
<PackageReference Include="Hangfire.InMemory" Version="0.6.0" /> <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.13" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.13" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.13" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.13" />

View File

@ -437,8 +437,7 @@ public class SeriesController : BaseApiController
[HttpGet("metadata")] [HttpGet("metadata")]
public async Task<ActionResult<SeriesMetadataDto>> GetSeriesMetadata(int seriesId) public async Task<ActionResult<SeriesMetadataDto>> GetSeriesMetadata(int seriesId)
{ {
var metadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId); return Ok(await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId));
return Ok(metadata);
} }
/// <summary> /// <summary>

View File

@ -75,16 +75,16 @@ public class SeriesMetadataDto
public bool PublicationStatusLocked { get; set; } public bool PublicationStatusLocked { get; set; }
public bool GenresLocked { get; set; } public bool GenresLocked { get; set; }
public bool TagsLocked { get; set; } public bool TagsLocked { get; set; }
public bool WritersLocked { get; set; } public bool WriterLocked { get; set; }
public bool CharactersLocked { get; set; } public bool CharacterLocked { get; set; }
public bool ColoristsLocked { get; set; } public bool ColoristLocked { get; set; }
public bool EditorsLocked { get; set; } public bool EditorLocked { get; set; }
public bool InkersLocked { get; set; } public bool InkerLocked { get; set; }
public bool LetterersLocked { get; set; } public bool LettererLocked { get; set; }
public bool PencillersLocked { get; set; } public bool PencillerLocked { get; set; }
public bool PublishersLocked { get; set; } public bool PublisherLocked { get; set; }
public bool TranslatorsLocked { get; set; } public bool TranslatorLocked { get; set; }
public bool CoverArtistsLocked { get; set; } public bool CoverArtistLocked { get; set; }
public bool ReleaseYearLocked { get; set; } public bool ReleaseYearLocked { get; set; }

View File

@ -73,12 +73,14 @@ public class AccountService : IAccountService
basePart = serverSettings.HostName; basePart = serverSettings.HostName;
if (!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl)) if (!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl))
{ {
basePart += serverSettings.BaseUrl.Substring(0, serverSettings.BaseUrl.Length - 1); var removeCount = serverSettings.BaseUrl.EndsWith("/") ? 2 : 1;
basePart += serverSettings.BaseUrl.Substring(0, serverSettings.BaseUrl.Length - removeCount);
} }
} }
if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}".Replace("//", "/"); if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}".Replace("//", "/"); return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}"
.Replace("//", "/");
} }
public async Task<IEnumerable<ApiException>> ChangeUserPassword(AppUser user, string newPassword) public async Task<IEnumerable<ApiException>> ChangeUserPassword(AppUser user, string newPassword)

View File

@ -107,6 +107,7 @@ public class SeriesService : ISeriesService
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
if (series == null) return false; if (series == null) return false;
var allCollectionTags = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).ToList(); var allCollectionTags = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).ToList();
// TODO: This is Diesel's performance problem with Komf. For some systems, this is too heavy of a call if komf is spamming updates.
var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresAsync()).ToList(); var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresAsync()).ToList();
var allPeople = (await _unitOfWork.PersonRepository.GetAllPeople()).ToList(); var allPeople = (await _unitOfWork.PersonRepository.GetAllPeople()).ToList();
var allTags = (await _unitOfWork.TagRepository.GetAllTagsAsync()).ToList(); var allTags = (await _unitOfWork.TagRepository.GetAllTagsAsync()).ToList();
@ -219,16 +220,16 @@ public class SeriesService : ISeriesService
series.Metadata.LanguageLocked = updateSeriesMetadataDto.SeriesMetadata.LanguageLocked; series.Metadata.LanguageLocked = updateSeriesMetadataDto.SeriesMetadata.LanguageLocked;
series.Metadata.GenresLocked = updateSeriesMetadataDto.SeriesMetadata.GenresLocked; series.Metadata.GenresLocked = updateSeriesMetadataDto.SeriesMetadata.GenresLocked;
series.Metadata.TagsLocked = updateSeriesMetadataDto.SeriesMetadata.TagsLocked; series.Metadata.TagsLocked = updateSeriesMetadataDto.SeriesMetadata.TagsLocked;
series.Metadata.CharacterLocked = updateSeriesMetadataDto.SeriesMetadata.CharactersLocked; series.Metadata.CharacterLocked = updateSeriesMetadataDto.SeriesMetadata.CharacterLocked;
series.Metadata.ColoristLocked = updateSeriesMetadataDto.SeriesMetadata.ColoristsLocked; series.Metadata.ColoristLocked = updateSeriesMetadataDto.SeriesMetadata.ColoristLocked;
series.Metadata.EditorLocked = updateSeriesMetadataDto.SeriesMetadata.EditorsLocked; series.Metadata.EditorLocked = updateSeriesMetadataDto.SeriesMetadata.EditorLocked;
series.Metadata.InkerLocked = updateSeriesMetadataDto.SeriesMetadata.InkersLocked; series.Metadata.InkerLocked = updateSeriesMetadataDto.SeriesMetadata.InkerLocked;
series.Metadata.LettererLocked = updateSeriesMetadataDto.SeriesMetadata.LetterersLocked; series.Metadata.LettererLocked = updateSeriesMetadataDto.SeriesMetadata.LettererLocked;
series.Metadata.PencillerLocked = updateSeriesMetadataDto.SeriesMetadata.PencillersLocked; series.Metadata.PencillerLocked = updateSeriesMetadataDto.SeriesMetadata.PencillerLocked;
series.Metadata.PublisherLocked = updateSeriesMetadataDto.SeriesMetadata.PublishersLocked; series.Metadata.PublisherLocked = updateSeriesMetadataDto.SeriesMetadata.PublisherLocked;
series.Metadata.TranslatorLocked = updateSeriesMetadataDto.SeriesMetadata.TranslatorsLocked; series.Metadata.TranslatorLocked = updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked;
series.Metadata.CoverArtistLocked = updateSeriesMetadataDto.SeriesMetadata.CoverArtistsLocked; series.Metadata.CoverArtistLocked = updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked;
series.Metadata.WriterLocked = updateSeriesMetadataDto.SeriesMetadata.WritersLocked; series.Metadata.WriterLocked = updateSeriesMetadataDto.SeriesMetadata.WriterLocked;
series.Metadata.SummaryLocked = updateSeriesMetadataDto.SeriesMetadata.SummaryLocked; series.Metadata.SummaryLocked = updateSeriesMetadataDto.SeriesMetadata.SummaryLocked;
series.Metadata.ReleaseYearLocked = updateSeriesMetadataDto.SeriesMetadata.ReleaseYearLocked; series.Metadata.ReleaseYearLocked = updateSeriesMetadataDto.SeriesMetadata.ReleaseYearLocked;

View File

@ -119,19 +119,46 @@ public class DefaultParser : IDefaultParser
{ {
ret.Volumes = Parser.DefaultVolume; ret.Volumes = Parser.DefaultVolume;
ret.Chapters = Parser.DefaultChapter; ret.Chapters = Parser.DefaultChapter;
// Next we need to see if the image has a folder between rootPath and filePath. var directoryName = _directoryService.FileSystem.DirectoryInfo.New(rootPath).Name;
// if so, take that folder as a volume 0 chapter 0 special and group everything under there (if we can't parse a volume/chapter) ret.Series = directoryName;
ParseFromFallbackFolders(filePath, rootPath, LibraryType.Image, ref ret); ParseFromFallbackFolders(filePath, rootPath, LibraryType.Image, ref ret);
if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters == Parser.DefaultChapter) &&
(string.IsNullOrEmpty(ret.Volumes) || ret.Volumes == Parser.DefaultVolume))
if (IsEmptyOrDefault(ret.Volumes, ret.Chapters))
{ {
ret.IsSpecial = true; ret.IsSpecial = true;
} }
else
{
var parsedVolume = Parser.ParseVolume(ret.Filename);
var parsedChapter = Parser.ParseChapter(ret.Filename);
if (IsEmptyOrDefault(ret.Volumes, string.Empty) && !parsedVolume.Equals(Parser.DefaultVolume))
{
ret.Volumes = parsedVolume;
}
if (IsEmptyOrDefault(string.Empty, ret.Chapters) && !parsedChapter.Equals(Parser.DefaultChapter))
{
ret.Chapters = parsedChapter;
}
}
// Override the series name, as fallback folders needs it to try and parse folder name
if (string.IsNullOrEmpty(ret.Series) || ret.Series.Equals(directoryName))
{
ret.Series = Parser.CleanTitle(directoryName, replaceSpecials: false);
}
ret.Series = _directoryService.FileSystem.DirectoryInfo.New(rootPath).Name;
return ret; return ret;
} }
private static bool IsEmptyOrDefault(string volumes, string chapters)
{
return (string.IsNullOrEmpty(chapters) || chapters == Parser.DefaultChapter) &&
(string.IsNullOrEmpty(volumes) || volumes == Parser.DefaultVolume);
}
/// <summary> /// <summary>
/// Fills out <see cref="ParserInfo"/> by trying to parse volume, chapters, and series from folders /// Fills out <see cref="ParserInfo"/> by trying to parse volume, chapters, and series from folders
/// </summary> /// </summary>
@ -193,7 +220,7 @@ public class DefaultParser : IDefaultParser
break; break;
} }
if (!string.IsNullOrEmpty(series) && (string.IsNullOrEmpty(ret.Series) || !folder.Contains(ret.Series))) if (!string.IsNullOrEmpty(series) && (string.IsNullOrEmpty(ret.Series) && !folder.Contains(ret.Series)))
{ {
ret.Series = series; ret.Series = series;
break; break;

View File

@ -843,23 +843,27 @@ public static class Parser
/// <param name="isComic"></param> /// <param name="isComic"></param>
/// <returns></returns> /// <returns></returns>
public static string CleanTitle(string title, bool isComic = false) public static string CleanTitle(string title, bool isComic = false, bool replaceSpecials = true)
{ {
title = ReplaceUnderscores(title); title = ReplaceUnderscores(title);
title = RemoveEditionTagHolders(title); title = RemoveEditionTagHolders(title);
if (isComic) if (replaceSpecials)
{ {
title = RemoveComicSpecialTags(title); if (isComic)
title = RemoveEuropeanTags(title); {
} title = RemoveComicSpecialTags(title);
else title = RemoveEuropeanTags(title);
{ }
title = RemoveMangaSpecialTags(title); else
{
title = RemoveMangaSpecialTags(title);
}
} }
title = title.Trim(SpacesAndSeparators); title = title.Trim(SpacesAndSeparators);
title = EmptySpaceRegex.Replace(title, " "); title = EmptySpaceRegex.Replace(title, " ");

View File

@ -293,6 +293,9 @@ public class ProcessSeries : IProcessSeries
if ((maxChapter == 0 || maxChapter > series.Metadata.TotalCount) && maxVolume <= series.Metadata.TotalCount) if ((maxChapter == 0 || maxChapter > series.Metadata.TotalCount) && maxVolume <= series.Metadata.TotalCount)
{
series.Metadata.MaxCount = maxVolume;
} else if (maxVolume == series.Metadata.TotalCount)
{ {
series.Metadata.MaxCount = maxVolume; series.Metadata.MaxCount = maxVolume;
} }

170
UI/Web/package-lock.json generated
View File

@ -8,16 +8,16 @@
"name": "kavita-webui", "name": "kavita-webui",
"version": "0.4.2", "version": "0.4.2",
"dependencies": { "dependencies": {
"@angular/animations": "^17.0.1", "@angular/animations": "^17.0.2",
"@angular/cdk": "^17.0.0", "@angular/cdk": "^17.0.0",
"@angular/common": "^17.0.1", "@angular/common": "^17.0.2",
"@angular/compiler": "^17.0.1", "@angular/compiler": "^17.0.2",
"@angular/core": "^17.0.1", "@angular/core": "^17.0.2",
"@angular/forms": "^17.0.1", "@angular/forms": "^17.0.2",
"@angular/localize": "^17.0.1", "@angular/localize": "^17.0.2",
"@angular/platform-browser": "^17.0.1", "@angular/platform-browser": "^17.0.2",
"@angular/platform-browser-dynamic": "^17.0.1", "@angular/platform-browser-dynamic": "^17.0.2",
"@angular/router": "^17.0.1", "@angular/router": "^17.0.2",
"@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-free": "^6.4.2",
"@iharbeck/ngx-virtual-scroller": "^16.0.0", "@iharbeck/ngx-virtual-scroller": "^16.0.0",
"@iplab/ngx-file-upload": "^16.0.2", "@iplab/ngx-file-upload": "^16.0.2",
@ -29,7 +29,7 @@
"@ngneat/transloco-persist-translations": "^5.0.0", "@ngneat/transloco-persist-translations": "^5.0.0",
"@ngneat/transloco-preload-langs": "^5.0.0", "@ngneat/transloco-preload-langs": "^5.0.0",
"@popperjs/core": "^2.11.7", "@popperjs/core": "^2.11.7",
"@swimlane/ngx-charts": "^20.1.2", "@swimlane/ngx-charts": "^20.5.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^21.0.0",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"charts.css": "^1.1.0", "charts.css": "^1.1.0",
@ -52,13 +52,13 @@
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.0.0", "@angular-devkit/build-angular": "^17.0.0",
"@angular-eslint/builder": "^17.0.0", "@angular-eslint/builder": "^17.0.1",
"@angular-eslint/eslint-plugin": "^17.0.0", "@angular-eslint/eslint-plugin": "^17.0.0",
"@angular-eslint/eslint-plugin-template": "^17.0.0", "@angular-eslint/eslint-plugin-template": "^17.0.0",
"@angular-eslint/schematics": "^17.0.1", "@angular-eslint/schematics": "^17.0.1",
"@angular-eslint/template-parser": "^17.0.1", "@angular-eslint/template-parser": "^17.0.1",
"@angular/cli": "^17.0.0", "@angular/cli": "^17.0.0",
"@angular/compiler-cli": "^17.0.1", "@angular/compiler-cli": "^17.0.2",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/luxon": "^3.3.4", "@types/luxon": "^3.3.4",
@ -744,9 +744,9 @@
} }
}, },
"node_modules/@angular-eslint/builder": { "node_modules/@angular-eslint/builder": {
"version": "17.0.0", "version": "17.0.1",
"resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.0.0.tgz", "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.0.1.tgz",
"integrity": "sha512-cquqJH0R/IIh2PElcGXdo9FTcrkwO78H2MXk9ChGFBjQrYjihFLhFm12VuQsih7X6bJjA0cmr2PL1KbtgjMk1Q==", "integrity": "sha512-bNXi5tdqIFdNDHxphDRUUbzA+7v6emOX2B/PFLG2pe+K6/JpHS0auwY/nq7hCroH7pMS5HZ+Q4i90q0GN/DWPg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@nx/devkit": "17.0.3", "@nx/devkit": "17.0.3",
@ -842,9 +842,9 @@
} }
}, },
"node_modules/@angular/animations": { "node_modules/@angular/animations": {
"version": "17.0.1", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.1.tgz", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.2.tgz",
"integrity": "sha512-Uee6E8zyU6XjDfKFozybcf+JZy0nUFQ1bUEmRwFP5HvYJSSJ5YiUDokNiVxyn9znwZ7zKHlM6Bq9ZY9cCmeKKQ==", "integrity": "sha512-32RHWhTgFLMonI3kRdstACay/nvetfxXjdwcTtABjcvBoND7nD9GMhkISQdgS+hcR/IhgXxaPidq8f2UAY5DBw==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -852,7 +852,7 @@
"node": "^18.13.0 || >=20.9.0" "node": "^18.13.0 || >=20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": "17.0.1" "@angular/core": "17.0.2"
} }
}, },
"node_modules/@angular/cdk": { "node_modules/@angular/cdk": {
@ -939,9 +939,9 @@
"dev": true "dev": true
}, },
"node_modules/@angular/common": { "node_modules/@angular/common": {
"version": "17.0.1", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.1.tgz", "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.2.tgz",
"integrity": "sha512-AvvhZc+PhX5lVEW/Vorxe3Zf1rIEJJvfduRuRv+nsjijo3ZGjdgYjTYEx4ighZgH60RLIAuwyBE24gPkT2Pm7g==", "integrity": "sha512-hCW0njHgrcwTWNoKZDwf02DnhYLVWNXM2FMw66MKpfxTp7McSyaXjGBU9/hchW3dZJ0xTwyxoyoqJFoHYvg0yg==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -949,14 +949,14 @@
"node": "^18.13.0 || >=20.9.0" "node": "^18.13.0 || >=20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": "17.0.1", "@angular/core": "17.0.2",
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
"node_modules/@angular/compiler": { "node_modules/@angular/compiler": {
"version": "17.0.1", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.1.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.2.tgz",
"integrity": "sha512-qlKqCvjoxPHJ1e8+UMaBl/n9zzrmGXI5eWMVhULSvQnQvPWkwNlUh5XFeoSFcTEQxORjaO2/08Z31DmTJAqlPA==", "integrity": "sha512-ewUFbKhMEhAmw2dGfk0ImhTlyrO2y4pJSKIZdFrkR1d0HiJX8bCHUdTiiR/2jeP7w2eamjXj15Rptb+iZZes2Q==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -964,7 +964,7 @@
"node": "^18.13.0 || >=20.9.0" "node": "^18.13.0 || >=20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": "17.0.1" "@angular/core": "17.0.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@angular/core": { "@angular/core": {
@ -973,9 +973,9 @@
} }
}, },
"node_modules/@angular/compiler-cli": { "node_modules/@angular/compiler-cli": {
"version": "17.0.1", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.1.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.2.tgz",
"integrity": "sha512-Rnvh2V2CYhG7NR5VI4cESGKk9jyqLat0HoqXa06v3TtbjkiZyjjwh0SyZ8NYOBMkQeWiQTHGcgxGvjKD3L3qqA==", "integrity": "sha512-IUYL3Yz5RbR0Z0/x7it4GK3sMb2qVihxu0tlgfUW53P1Vi6nU/Zda0bCJTu6Z64qEtS8zwCwF1Ekomuq6UaiKg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/core": "7.23.2", "@babel/core": "7.23.2",
@ -996,14 +996,14 @@
"node": "^18.13.0 || >=20.9.0" "node": "^18.13.0 || >=20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/compiler": "17.0.1", "@angular/compiler": "17.0.2",
"typescript": ">=5.2 <5.3" "typescript": ">=5.2 <5.3"
} }
}, },
"node_modules/@angular/core": { "node_modules/@angular/core": {
"version": "17.0.1", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.1.tgz", "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.2.tgz",
"integrity": "sha512-yVwU+oz0G8g6Q5ORyOCpgqMPdSiCdfW+uQhjI37WROnXHja3jY843AqrYTKE6mMx1r6q9h1wbDy+x2E61OWP7A==", "integrity": "sha512-MjDxWeyn3Txi0qo/V/I+B/gndh0uptQ0XWgBRwOx6Wcr5zRGeZIFlXBxPpyXnGTlJkeyErsTN7FfFCZ4C3kCPA==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -1016,9 +1016,9 @@
} }
}, },
"node_modules/@angular/forms": { "node_modules/@angular/forms": {
"version": "17.0.1", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.1.tgz", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.2.tgz",
"integrity": "sha512-FpmUf2kgzwZXVbFB4VrwbnrO0m88QLUBsDsbLfQVQQwb7KxwSaftUu/aIrjst1gFCdl9k0Vqtrq2gwLZKzdSGQ==", "integrity": "sha512-w1QKifaVG4daxUktcBNZqBtOH1vn8t0YiwDR3woEdUYt0XYKMipfDzQfyIK+6fIVPOJUd42pRns1nbWJQHOInA==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -1026,16 +1026,16 @@
"node": "^18.13.0 || >=20.9.0" "node": "^18.13.0 || >=20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "17.0.1", "@angular/common": "17.0.2",
"@angular/core": "17.0.1", "@angular/core": "17.0.2",
"@angular/platform-browser": "17.0.1", "@angular/platform-browser": "17.0.2",
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
"node_modules/@angular/localize": { "node_modules/@angular/localize": {
"version": "17.0.1", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.0.1.tgz", "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.0.2.tgz",
"integrity": "sha512-pNLLnEbXjoW1agKwA4cBcM/HnqGuwQMpIhx9H46Y/oC2JkAvTCMVyXLbZUESeXmhysC9x2JDmF+Awhu7JzVVCA==", "integrity": "sha512-ct8xEy8Xk+PRfjrHLu7uywSQDzozmzlz6ptUCuYkRHrS4rJabXn3c0Sz4w+mh9B58qrK6KM+JSmXEZngEMXMTw==",
"dependencies": { "dependencies": {
"@babel/core": "7.23.2", "@babel/core": "7.23.2",
"fast-glob": "3.3.1", "fast-glob": "3.3.1",
@ -1050,14 +1050,14 @@
"node": "^18.13.0 || >=20.9.0" "node": "^18.13.0 || >=20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/compiler": "17.0.1", "@angular/compiler": "17.0.2",
"@angular/compiler-cli": "17.0.1" "@angular/compiler-cli": "17.0.2"
} }
}, },
"node_modules/@angular/platform-browser": { "node_modules/@angular/platform-browser": {
"version": "17.0.1", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.1.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.2.tgz",
"integrity": "sha512-JpvU0YDEM5KYdHtxC0Kdzk/hdwvZPq5vju5lTmIjTVa2OOabApOrQ6cq1MpKlrvjv1rw8MClHIM0l5Y0g9KH5g==", "integrity": "sha512-eTnPILEA/eAMkVUR/+g6fWhhMTmnmOzcZSGX/bBgQcvOhayZrDDxA6/Qf+jIB4RwC0wd3KA9zT5BCMmNojoUsg==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -1065,9 +1065,9 @@
"node": "^18.13.0 || >=20.9.0" "node": "^18.13.0 || >=20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/animations": "17.0.1", "@angular/animations": "17.0.2",
"@angular/common": "17.0.1", "@angular/common": "17.0.2",
"@angular/core": "17.0.1" "@angular/core": "17.0.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@angular/animations": { "@angular/animations": {
@ -1076,9 +1076,9 @@
} }
}, },
"node_modules/@angular/platform-browser-dynamic": { "node_modules/@angular/platform-browser-dynamic": {
"version": "17.0.1", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.1.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.2.tgz",
"integrity": "sha512-xEcbB/ukXc65LaX4JBQYEM7D5Z8LcUIZniSJFneY7deZt3wNiKgmPZrPoXUyDV26QULh7N0IADEzvbcMF60AFQ==", "integrity": "sha512-clcHqHcfD00/TlTixDbJ3q4EQxpm0t2ZFG76rRFmGrmE5tKYUPfaofIa3hQCxy3q269MAYuF16wALhUtrEWyUA==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -1086,16 +1086,16 @@
"node": "^18.13.0 || >=20.9.0" "node": "^18.13.0 || >=20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "17.0.1", "@angular/common": "17.0.2",
"@angular/compiler": "17.0.1", "@angular/compiler": "17.0.2",
"@angular/core": "17.0.1", "@angular/core": "17.0.2",
"@angular/platform-browser": "17.0.1" "@angular/platform-browser": "17.0.2"
} }
}, },
"node_modules/@angular/router": { "node_modules/@angular/router": {
"version": "17.0.1", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.1.tgz", "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.2.tgz",
"integrity": "sha512-73PCDDsRAjemODMRndZhwEN6Tb9rVVbDfMWgLQ4HgfgKnjek8P9BoYf8rOf3qV5fXf3c1Sm9MmKtaPv+l5lU9Q==", "integrity": "sha512-A1Ulv4qBAtJyK5g1yBlK1qZHe+KaaL5vMPAaPWUxICH8lHEodDkJlbYAUI2e4VL2BN7zBmdOep6tlBKPmHY3mw==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -1103,9 +1103,9 @@
"node": "^18.13.0 || >=20.9.0" "node": "^18.13.0 || >=20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "17.0.1", "@angular/common": "17.0.2",
"@angular/core": "17.0.1", "@angular/core": "17.0.2",
"@angular/platform-browser": "17.0.1", "@angular/platform-browser": "17.0.2",
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
@ -4251,9 +4251,9 @@
"dev": true "dev": true
}, },
"node_modules/@swimlane/ngx-charts": { "node_modules/@swimlane/ngx-charts": {
"version": "20.4.1", "version": "20.5.0",
"resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-20.4.1.tgz", "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-20.5.0.tgz",
"integrity": "sha512-DyTQe0fcqLDoLEZca45gkdjxP8iLH7kh4pCkr+TCFIkmgEdfQ5DpavNBOOVO0qd5J5uV/tbtSnkYWSx8JkbFpg==", "integrity": "sha512-PNBIHdu/R3ceD7jnw1uCBVOj4k3T6IxfdW6xsDsglGkZyoWMEEq4tLoEurjLEKzmDtRv9c35kVNOXy0lkOuXeA==",
"dependencies": { "dependencies": {
"d3-array": "^3.1.1", "d3-array": "^3.1.1",
"d3-brush": "^3.0.0", "d3-brush": "^3.0.0",
@ -4262,6 +4262,7 @@
"d3-format": "^3.1.0", "d3-format": "^3.1.0",
"d3-hierarchy": "^3.1.0", "d3-hierarchy": "^3.1.0",
"d3-interpolate": "^3.0.1", "d3-interpolate": "^3.0.1",
"d3-sankey": "^0.12.3",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
@ -7166,6 +7167,41 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-sankey": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
"integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
"dependencies": {
"d3-array": "1 - 2",
"d3-shape": "^1.2.0"
}
},
"node_modules/d3-sankey/node_modules/d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
"dependencies": {
"internmap": "^1.0.0"
}
},
"node_modules/d3-sankey/node_modules/d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
},
"node_modules/d3-sankey/node_modules/d3-shape": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
"dependencies": {
"d3-path": "1"
}
},
"node_modules/d3-sankey/node_modules/internmap": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
},
"node_modules/d3-scale": { "node_modules/d3-scale": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",

View File

@ -13,16 +13,16 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^17.0.1", "@angular/animations": "^17.0.2",
"@angular/cdk": "^17.0.0", "@angular/cdk": "^17.0.0",
"@angular/common": "^17.0.1", "@angular/common": "^17.0.2",
"@angular/compiler": "^17.0.1", "@angular/compiler": "^17.0.2",
"@angular/core": "^17.0.1", "@angular/core": "^17.0.2",
"@angular/forms": "^17.0.1", "@angular/forms": "^17.0.2",
"@angular/localize": "^17.0.1", "@angular/localize": "^17.0.2",
"@angular/platform-browser": "^17.0.1", "@angular/platform-browser": "^17.0.2",
"@angular/platform-browser-dynamic": "^17.0.1", "@angular/platform-browser-dynamic": "^17.0.2",
"@angular/router": "^17.0.1", "@angular/router": "^17.0.2",
"@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-free": "^6.4.2",
"@iharbeck/ngx-virtual-scroller": "^16.0.0", "@iharbeck/ngx-virtual-scroller": "^16.0.0",
"@iplab/ngx-file-upload": "^16.0.2", "@iplab/ngx-file-upload": "^16.0.2",
@ -34,7 +34,7 @@
"@ngneat/transloco-persist-translations": "^5.0.0", "@ngneat/transloco-persist-translations": "^5.0.0",
"@ngneat/transloco-preload-langs": "^5.0.0", "@ngneat/transloco-preload-langs": "^5.0.0",
"@popperjs/core": "^2.11.7", "@popperjs/core": "^2.11.7",
"@swimlane/ngx-charts": "^20.1.2", "@swimlane/ngx-charts": "^20.5.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^21.0.0",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"charts.css": "^1.1.0", "charts.css": "^1.1.0",
@ -57,13 +57,13 @@
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.0.0", "@angular-devkit/build-angular": "^17.0.0",
"@angular-eslint/builder": "^17.0.0", "@angular-eslint/builder": "^17.0.1",
"@angular-eslint/eslint-plugin": "^17.0.0", "@angular-eslint/eslint-plugin": "^17.0.0",
"@angular-eslint/eslint-plugin-template": "^17.0.0", "@angular-eslint/eslint-plugin-template": "^17.0.0",
"@angular-eslint/schematics": "^17.0.1", "@angular-eslint/schematics": "^17.0.1",
"@angular-eslint/template-parser": "^17.0.1", "@angular-eslint/template-parser": "^17.0.1",
"@angular/cli": "^17.0.0", "@angular/cli": "^17.0.0",
"@angular/compiler-cli": "^17.0.1", "@angular/compiler-cli": "^17.0.2",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/luxon": "^3.3.4", "@types/luxon": "^3.3.4",

View File

@ -34,18 +34,18 @@ export interface SeriesMetadata {
summaryLocked: boolean; summaryLocked: boolean;
genresLocked: boolean; genresLocked: boolean;
tagsLocked: boolean; tagsLocked: boolean;
writersLocked: boolean; writerLocked: boolean;
coverArtistsLocked: boolean; coverArtistLocked: boolean;
publishersLocked: boolean; publisherLocked: boolean;
charactersLocked: boolean; characterLocked: boolean;
pencillersLocked: boolean; pencillerLocked: boolean;
inkersLocked: boolean; inkerLocked: boolean;
coloristsLocked: boolean; coloristLocked: boolean;
letterersLocked: boolean; lettererLocked: boolean;
editorsLocked: boolean; editorLocked: boolean;
translatorsLocked: boolean; translatorLocked: boolean;
ageRatingLocked: boolean; ageRatingLocked: boolean;
releaseYearLocked: boolean; releaseYearLocked: boolean;
languageLocked: boolean; languageLocked: boolean;
publicationStatusLocked: boolean; publicationStatusLocked: boolean;
} }

View File

@ -23,7 +23,7 @@ export class UtcToLocalTimePipe implements PipeTransform {
case 'short': case 'short':
return dateTime.toLocaleString(DateTime.DATETIME_SHORT); return dateTime.toLocaleString(DateTime.DATETIME_SHORT);
case 'shortDate': case 'shortDate':
return dateTime.toLocaleString(DateTime.DATE_MED); return dateTime.toLocaleString(DateTime.DATE_SHORT);
case 'shortTime': case 'shortTime':
return dateTime.toLocaleString(DateTime.TIME_SIMPLE); return dateTime.toLocaleString(DateTime.TIME_SIMPLE);
case 'full': case 'full':

View File

@ -8,7 +8,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="typeahead-focus" class="form-label">{{t('path-label')}}</label> <label for="typeahead-focus" class="form-label">{{t('path-label')}}</label>
<div class="input-group"> <div class="input-group">
<input id="typeahead-focus" type="text" class="form-control" [(ngModel)]="path" [ngbTypeahead]="search" <input id="typeahead-focus" type="text" class="form-control" style="width: 100%" [(ngModel)]="path" [ngbTypeahead]="search"
(focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)"
(ngModelChange)="updateTable()" #instance="ngbTypeahead" [placeholder]="t('path-placeholder')" (ngModelChange)="updateTable()" #instance="ngbTypeahead" [placeholder]="t('path-placeholder')"
[resultTemplate]="rt" /> [resultTemplate]="rt" />

View File

@ -17,10 +17,10 @@ export interface DirectoryPickerResult {
@Component({ @Component({
selector: 'app-directory-picker', selector: 'app-directory-picker',
templateUrl: './directory-picker.component.html', templateUrl: './directory-picker.component.html',
styleUrls: ['./directory-picker.component.scss'], styleUrls: ['./directory-picker.component.scss'],
standalone: true, standalone: true,
imports: [ReactiveFormsModule, NgbTypeahead, FormsModule, NgbHighlight, NgIf, NgFor, NgClass, TranslocoDirective] imports: [ReactiveFormsModule, NgbTypeahead, FormsModule, NgbHighlight, NgIf, NgFor, NgClass, TranslocoDirective]
}) })
export class DirectoryPickerComponent implements OnInit { export class DirectoryPickerComponent implements OnInit {
@ -37,7 +37,7 @@ export class DirectoryPickerComponent implements OnInit {
path: string = ''; path: string = '';
@ViewChild('instance', {static: true}) instance!: NgbTypeahead; @ViewChild('instance', {static: false}) instance!: NgbTypeahead;
focus$ = new Subject<string>(); focus$ = new Subject<string>();
click$ = new Subject<string>(); click$ = new Subject<string>();
searching: boolean = false; searching: boolean = false;

View File

@ -24,9 +24,6 @@
<ng-container *ngIf="tab.fragment === TabID.Libraries"> <ng-container *ngIf="tab.fragment === TabID.Libraries">
<app-manage-library></app-manage-library> <app-manage-library></app-manage-library>
</ng-container> </ng-container>
<ng-container *ngIf="tab.fragment === TabID.Logs">
<app-manage-logs></app-manage-logs>
</ng-container>
<ng-container *ngIf="tab.fragment === TabID.System"> <ng-container *ngIf="tab.fragment === TabID.System">
<app-manage-system></app-manage-system> <app-manage-system></app-manage-system>
</ng-container> </ng-container>

View File

@ -38,7 +38,10 @@ enum TabID {
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'], styleUrls: ['./dashboard.component.scss'],
standalone: true, standalone: true,
imports: [SideNavCompanionBarComponent, NgbNav, NgFor, NgbNavItem, NgbNavItemRole, NgbNavLink, RouterLink, NgbNavContent, NgIf, ManageSettingsComponent, ManageEmailSettingsComponent, ManageMediaSettingsComponent, ManageUsersComponent, ManageLibraryComponent, ManageLogsComponent, ManageSystemComponent, ServerStatsComponent, ManageTasksSettingsComponent, LicenseComponent, NgbNavOutlet, SentenceCasePipe, TranslocoDirective], imports: [SideNavCompanionBarComponent, NgbNav, NgFor, NgbNavItem, NgbNavItemRole, NgbNavLink, RouterLink,
NgbNavContent, NgIf, ManageSettingsComponent, ManageEmailSettingsComponent, ManageMediaSettingsComponent,
ManageUsersComponent, ManageLibraryComponent, ManageSystemComponent, ServerStatsComponent,
ManageTasksSettingsComponent, LicenseComponent, NgbNavOutlet, SentenceCasePipe, TranslocoDirective],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DashboardComponent implements OnInit { export class DashboardComponent implements OnInit {
@ -47,7 +50,6 @@ export class DashboardComponent implements OnInit {
{title: 'general-tab', fragment: TabID.General}, {title: 'general-tab', fragment: TabID.General},
{title: 'users-tab', fragment: TabID.Users}, {title: 'users-tab', fragment: TabID.Users},
{title: 'libraries-tab', fragment: TabID.Libraries}, {title: 'libraries-tab', fragment: TabID.Libraries},
//{title: 'logs-tab', fragment: TabID.Logs},
{title: 'media-tab', fragment: TabID.Media}, {title: 'media-tab', fragment: TabID.Media},
{title: 'email-tab', fragment: TabID.Email}, {title: 'email-tab', fragment: TabID.Email},
{title: 'tasks-tab', fragment: TabID.Tasks}, {title: 'tasks-tab', fragment: TabID.Tasks},

View File

@ -44,7 +44,7 @@ import {SeriesFilterV2} from "../../../_models/metadata/v2/series-filter-v2";
}) })
export class AllSeriesComponent implements OnInit { export class AllSeriesComponent implements OnInit {
title!: string; title: string = translate('side-nav.all-series');
series: Series[] = []; series: Series[] = [];
loadingSeries = false; loadingSeries = false;
pagination: Pagination = new Pagination(); pagination: Pagination = new Pagination();
@ -115,10 +115,8 @@ export class AllSeriesComponent implements OnInit {
this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => { this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => {
this.filter = filter; this.filter = filter;
this.title = this.route.snapshot.queryParamMap.get('title') || this.filter.name || this.title; this.title = this.route.snapshot.queryParamMap.get('title') || this.filter.name || this.title;
this.titleService.setTitle('Kavita - ' + this.title); this.titleService.setTitle('Kavita - ' + this.title);
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement());
this.filterSettings.presetsV2 = this.filter; this.filterSettings.presetsV2 = this.filter;
@ -128,7 +126,6 @@ export class AllSeriesComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.title = translate('all-series.title');
this.hubService.messages$.pipe(debounceTime(6000), takeUntilDestroyed(this.destroyRef)).subscribe((event: Message<any>) => { this.hubService.messages$.pipe(debounceTime(6000), takeUntilDestroyed(this.destroyRef)).subscribe((event: Message<any>) => {
if (event.event !== EVENTS.SeriesAdded) return; if (event.event !== EVENTS.SeriesAdded) return;
this.loadPage(); this.loadPage();

View File

@ -81,6 +81,7 @@ const routes: Routes = [
}, },
] ]
}, },
{path: '', pathMatch: 'full', redirectTo: 'home'},
] ]
}, },
{ {
@ -91,9 +92,10 @@ const routes: Routes = [
path: 'login', path: 'login',
loadChildren: () => import('./_routes/registration.router.module').then(m => m.routes) // TODO: Refactor so we just use /registration/login going forward loadChildren: () => import('./_routes/registration.router.module').then(m => m.routes) // TODO: Refactor so we just use /registration/login going forward
}, },
{path: '**', pathMatch: 'full', redirectTo: 'home'},
{path: 'libraries', pathMatch: 'full', redirectTo: 'home'}, {path: 'libraries', pathMatch: 'full', redirectTo: 'home'},
{path: '**', pathMatch: 'prefix', redirectTo: 'home'}, {path: '**', pathMatch: 'prefix', redirectTo: 'home'},
{path: '**', pathMatch: 'full', redirectTo: 'home'},
{path: '', pathMatch: 'full', redirectTo: 'home'},
]; ];
@NgModule({ @NgModule({

View File

@ -1617,9 +1617,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// Responsible for handling pagination only // Responsible for handling pagination only
handleContainerClick(event: MouseEvent) { handleContainerClick(event: MouseEvent) {
console.log('target: ', event.target); if (this.drawerOpen || ['action-bar'].some(className => (event.target as Element).classList.contains(className))) {
if (this.actionBarVisible || ['action-bar'].some(className => (event.target as Element).classList.contains(className))) {
//console.log('exiting early')
return; return;
} }

View File

@ -187,8 +187,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="writer" class="form-label">{{t('writer-label')}}</label> <label for="writer" class="form-label">{{t('writer-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)" <app-typeahead (selectedData)="updatePerson($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)"
[(locked)]="metadata.writersLocked" (onUnlock)="metadata.writersLocked = false" [(locked)]="metadata.writerLocked" (onUnlock)="metadata.writerLocked = false"
(newItemAdded)="metadata.writersLocked = true" (selectedData)="metadata.writersLocked = true"> (newItemAdded)="metadata.writerLocked = true" (selectedData)="metadata.writerLocked = true">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -202,8 +202,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="cover-artist" class="form-label">{{t('cover-artist-label')}}</label> <label for="cover-artist" class="form-label">{{t('cover-artist-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)" <app-typeahead (selectedData)="updatePerson($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)"
[(locked)]="metadata.coverArtistsLocked" (onUnlock)="metadata.coverArtistsLocked = false" [(locked)]="metadata.coverArtistLocked" (onUnlock)="metadata.coverArtistLocked = false"
(newItemAdded)="metadata.coverArtistsLocked = true" (selectedData)="metadata.coverArtistsLocked = true"> (newItemAdded)="metadata.coverArtistLocked = true" (selectedData)="metadata.coverArtistLocked = true">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -219,8 +219,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="publisher" class="form-label">{{t('publisher-label')}}</label> <label for="publisher" class="form-label">{{t('publisher-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)" <app-typeahead (selectedData)="updatePerson($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)"
[(locked)]="metadata.publishersLocked" (onUnlock)="metadata.publishersLocked = false" [(locked)]="metadata.publisherLocked" (onUnlock)="metadata.publisherLocked = false"
(newItemAdded)="metadata.publishersLocked = true" (selectedData)="metadata.publishersLocked = true"> (newItemAdded)="metadata.publisherLocked = true" (selectedData)="metadata.publisherLocked = true">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -234,8 +234,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="penciller" class="form-label">{{t('penciller-label')}}</label> <label for="penciller" class="form-label">{{t('penciller-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)" <app-typeahead (selectedData)="updatePerson($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)"
[(locked)]="metadata.pencillersLocked" (onUnlock)="metadata.pencillersLocked = false" [(locked)]="metadata.pencillerLocked" (onUnlock)="metadata.pencillerLocked = false"
(newItemAdded)="metadata.pencillersLocked = true" (selectedData)="metadata.pencillersLocked = true"> (newItemAdded)="metadata.pencillerLocked = true" (selectedData)="metadata.pencillerLocked = true">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -251,8 +251,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="letterer" class="form-label">{{t('letterer-label')}}</label> <label for="letterer" class="form-label">{{t('letterer-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)" <app-typeahead (selectedData)="updatePerson($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)"
[(locked)]="metadata.letterersLocked" (onUnlock)="metadata.letterersLocked = false" [(locked)]="metadata.lettererLocked" (onUnlock)="metadata.lettererLocked = false"
(newItemAdded)="metadata.letterersLocked = true" (selectedData)="metadata.letterersLocked = true"> (newItemAdded)="metadata.lettererLocked = true" (selectedData)="metadata.lettererLocked = true">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -266,8 +266,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="inker" class="form-label">{{t('inker-label')}}</label> <label for="inker" class="form-label">{{t('inker-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)" <app-typeahead (selectedData)="updatePerson($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)"
[(locked)]="metadata.inkersLocked" (onUnlock)="metadata.inkersLocked = false" [(locked)]="metadata.inkerLocked" (onUnlock)="metadata.inkerLocked = false"
(newItemAdded)="metadata.inkersLocked = true" (selectedData)="metadata.inkersLocked = true"> (newItemAdded)="metadata.inkerLocked = true" (selectedData)="metadata.inkerLocked = true">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -284,8 +284,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="editor" class="form-label">{{t('editor-label')}}</label> <label for="editor" class="form-label">{{t('editor-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)" <app-typeahead (selectedData)="updatePerson($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)"
[(locked)]="metadata.editorsLocked" (onUnlock)="metadata.editorsLocked = false" [(locked)]="metadata.editorLocked" (onUnlock)="metadata.editorLocked = false"
(newItemAdded)="metadata.editorsLocked = true" (selectedData)="metadata.editorsLocked = true"> (newItemAdded)="metadata.editorLocked = true" (selectedData)="metadata.editorLocked = true">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -299,8 +299,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="colorist" class="form-label">{{t('colorist-label')}}</label> <label for="colorist" class="form-label">{{t('colorist-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)" <app-typeahead (selectedData)="updatePerson($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)"
[(locked)]="metadata.coloristsLocked" (onUnlock)="metadata.coloristsLocked = false" [(locked)]="metadata.coloristLocked" (onUnlock)="metadata.coloristLocked = false"
(newItemAdded)="metadata.coloristsLocked = true" (selectedData)="metadata.coloristsLocked = true"> (newItemAdded)="metadata.coloristLocked = true" (selectedData)="metadata.coloristLocked = true">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -317,8 +317,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="character" class="form-label">{{t('character-label')}}</label> <label for="character" class="form-label">{{t('character-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)" <app-typeahead (selectedData)="updatePerson($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)"
[(locked)]="metadata.charactersLocked" (onUnlock)="metadata.charactersLocked = false" [(locked)]="metadata.characterLocked" (onUnlock)="metadata.characterLocked = false"
(newItemAdded)="metadata.charactersLocked = true" (selectedData)="metadata.charactersLocked = true"> (newItemAdded)="metadata.characterLocked = true" (selectedData)="metadata.characterLocked = true">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -332,8 +332,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="translator" class="form-label">{{t('translator-label')}}</label> <label for="translator" class="form-label">{{t('translator-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)" <app-typeahead (selectedData)="updatePerson($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)"
[(locked)]="metadata.translatorsLocked" (onUnlock)="metadata.translatorsLocked = false" [(locked)]="metadata.translatorLocked" (onUnlock)="metadata.translatorLocked = false"
(newItemAdded)="metadata.translatorsLocked = true" (selectedData)="metadata.translatorsLocked = true"> (newItemAdded)="metadata.translatorLocked = true" (selectedData)="metadata.translatorLocked = true">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>

View File

@ -4,6 +4,7 @@ import {ImageComponent} from "../../shared/image/image.component";
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter"; import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {translate} from "@ngneat/transloco";
@Component({ @Component({
selector: 'app-next-expected-card', selector: 'app-next-expected-card',
@ -39,7 +40,7 @@ export class NextExpectedCardComponent {
if (this.entity.expectedDate) { if (this.entity.expectedDate) {
const utcPipe = new UtcToLocalTimePipe(); const utcPipe = new UtcToLocalTimePipe();
this.title = '~ ' + utcPipe.transform(this.entity.expectedDate, 'shortDate'); this.title = translate('next-expected-card.title', {date: utcPipe.transform(this.entity.expectedDate, 'shortDate')});
} }
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }

View File

@ -83,7 +83,7 @@ export class ReadingListDetailComponent implements OnInit {
this.router.navigateByUrl('/home'); this.router.navigateByUrl('/home');
return; return;
} }
this.titleService.setTitle('Kavita - ' + translate('side-nav.reading-lists'));
this.listId = parseInt(listId, 10); this.listId = parseInt(listId, 10);
this.characters$ = this.readingListService.getCharacters(this.listId); this.characters$ = this.readingListService.getCharacters(this.listId);
@ -94,6 +94,8 @@ export class ReadingListDetailComponent implements OnInit {
const libraries = results[0]; const libraries = results[0];
const readingList = results[1]; const readingList = results[1];
this.titleService.setTitle('Kavita - ' + readingList.title);
libraries.forEach(lib => { libraries.forEach(lib => {
this.libraryTypes[lib.id] = lib.type; this.libraryTypes[lib.id] = lib.type;
}); });

View File

@ -17,9 +17,9 @@ import { CardItemComponent } from '../../../cards/card-item/card-item.component'
import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/card-detail-layout.component'; import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/card-detail-layout.component';
import { NgIf, DecimalPipe } from '@angular/common'; import { NgIf, DecimalPipe } from '@angular/common';
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco"; import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component"; import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {CollectionTag} from "../../../_models/collection-tag"; import {Title} from "@angular/platform-browser";
@Component({ @Component({
selector: 'app-reading-lists', selector: 'app-reading-lists',
@ -43,13 +43,14 @@ export class ReadingListsComponent implements OnInit {
translocoService = inject(TranslocoService); translocoService = inject(TranslocoService);
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService, constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService, private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService,
private jumpbarService: JumpbarService, private readonly cdRef: ChangeDetectorRef, private ngbModal: NgbModal) { } private jumpbarService: JumpbarService, private readonly cdRef: ChangeDetectorRef, private ngbModal: NgbModal, private titleService: Title) { }
ngOnInit(): void { ngOnInit(): void {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => { this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) { if (user) {
this.isAdmin = this.accountService.hasAdminRole(user); this.isAdmin = this.accountService.hasAdminRole(user);
this.loadPage(); this.loadPage();
this.titleService.setTitle('Kavita - ' + translate('side-nav.reading-lists'));
} }
}); });
} }

View File

@ -64,7 +64,7 @@ export class ImportCblModalComponent {
{title: this.translocoService.translate('import-cbl-modal.import-step'), index: Step.Import, active: true, icon: 'fa-solid fa-file-arrow-up'}, {title: this.translocoService.translate('import-cbl-modal.import-step'), index: Step.Import, active: true, icon: 'fa-solid fa-file-arrow-up'},
{title: this.translocoService.translate('import-cbl-modal.validate-cbl-step'), index: Step.Validate, active: false, icon: 'fa-solid fa-spell-check'}, {title: this.translocoService.translate('import-cbl-modal.validate-cbl-step'), index: Step.Validate, active: false, icon: 'fa-solid fa-spell-check'},
{title: this.translocoService.translate('import-cbl-modal.dry-run-step'), index: Step.DryRun, active: false, icon: 'fa-solid fa-gears'}, {title: this.translocoService.translate('import-cbl-modal.dry-run-step'), index: Step.DryRun, active: false, icon: 'fa-solid fa-gears'},
{title: this.translocoService.translate('import-cbl-final-import.import-step'), index: Step.Finalize, active: false, icon: 'fa-solid fa-floppy-disk'}, {title: this.translocoService.translate('import-cbl-modal.final-import-step'), index: Step.Finalize, active: false, icon: 'fa-solid fa-floppy-disk'},
]; ];
currentStepIndex = this.steps[0].index; currentStepIndex = this.steps[0].index;

View File

@ -80,7 +80,7 @@
<p *ngIf="isAddLibrary" class="alert alert-secondary" role="alert">{{t('cover-description')}}</p> <p *ngIf="isAddLibrary" class="alert alert-secondary" role="alert">{{t('cover-description')}}</p>
<p>{{t('cover-description-extra')}}</p> <p>{{t('cover-description-extra')}}</p>
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateCoverImageIndex($event)" <app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateCoverImageIndex($event)"
(selectedBase64Url)="applyCoverImage($event)" [showReset]="library.coverImage !== null" (selectedBase64Url)="applyCoverImage($event)" [showReset]="library?.coverImage !== null"
(resetClicked)="resetCoverImage()"></app-cover-image-chooser> (resetClicked)="resetCoverImage()"></app-cover-image-chooser>
</ng-template> </ng-template>
</li> </li>

View File

@ -1,5 +1,5 @@
.confirm-icon { .confirm-icon {
color: var(--primary-color); color: var(--primary-color);
font-size: 14px; font-size: 14px;
margin-bottom: 10px; vertical-align: middle;
} }

View File

@ -9,15 +9,17 @@
<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>
<ng-container *ngIf="tab.fragment === FragmentID.Account"> @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-anilist-key></app-anilist-key> <app-anilist-key></app-anilist-key>
</ng-container> }
@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">
@ -25,420 +27,418 @@
<div ngbAccordionItem [id]="AccordionPanelID.GlobalSettings" [collapsed]="false"> <div ngbAccordionItem [id]="AccordionPanelID.GlobalSettings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader> <h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)" aria-controls="collapseOne"> <button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)" aria-controls="collapseOne">
{{t('global-settings-title')}} {{t('global-settings-title')}}
</button> </button>
</h2> </h2>
<div ngbAccordionCollapse> <div ngbAccordionCollapse>
<div ngbAccordionBody> <div ngbAccordionBody>
<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-global-layoutmode" class="form-label">{{t('page-layout-mode-label')}}</label> <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> <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> <ng-template #layoutModeTooltip>{{t('page-layout-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-layoutmode-help"> <span class="visually-hidden" id="settings-global-layoutmode-help">
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container> <ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span> </span>
<select class="form-select" aria-describedby="manga-header" formControlName="globalPageLayoutMode" id="settings-global-layoutmode"> <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> <option *ngFor="let opt of pageLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select> </select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label>
<i class="fa fa-info-circle ms-1"
aria-hidden="true" placement="right" [ngbTooltip]="localeTooltip" role="button" tabindex="0"></i>
<ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-locale-help">
<ng-container [ngTemplateOutlet]="localeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="locale" id="settings-global-locale">
<option *ngFor="let opt of locales" [value]="opt.isoCode">{{opt.title | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="blur-unread-summaries" role="switch" formControlName="blurUnreadSummaries" class="form-check-input" aria-describedby="settings-global-blurUnreadSummaries-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="blur-unread-summaries">{{t('blur-unread-summaries-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="blurUnreadSummariesTooltip" role="button" tabindex="0"></i>
</div> </div>
<ng-template #blurUnreadSummariesTooltip>{{t('blur-unread-summaries-tooltip')}}</ng-template> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<span class="visually-hidden" id="settings-global-blurUnreadSummaries-help"> <label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label>
<ng-container [ngTemplateOutlet]="blurUnreadSummariesTooltip"></ng-container> <i class="fa fa-info-circle ms-1"
</span> aria-hidden="true" placement="right" [ngbTooltip]="localeTooltip" role="button" tabindex="0"></i>
</div> <ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template>
</div> <span class="visually-hidden" id="settings-global-locale-help">
<ng-container [ngTemplateOutlet]="localeTooltip"></ng-container>
<div class="row g-0"> </span>
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <select class="form-select" aria-describedby="manga-header" formControlName="locale" id="settings-global-locale">
<div class="form-check form-switch"> <option *ngFor="let opt of locales" [value]="opt.isoCode">{{opt.title | titlecase}}</option>
<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"> </select>
<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> </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>
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <div class="row g-0">
<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> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<ng-template #scalingOptionTooltip>{{t('scaling-option-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-scaling-option-help">
<ng-container [ngTemplateOutlet]="scalingOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
<option *ngFor="let opt of scalingOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-pagesplit-option" class="form-label">{{t('page-splitting-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
<ng-template #pageSplitOptionTooltip>{{t('page-splitting-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-pagesplit-option-help">
<ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-readingmode-option" class="form-label">{{t('reading-mode-label')}}</label>
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
<option *ngFor="let opt of readingModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="true">
<label for="settings-layoutmode-option" class="form-label">{{t('layout-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #layoutModeTooltip>{{t('layout-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-layoutmode-option-help">
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-background-color-option" class="form-label">{{t('background-color-label')}}</label>
<input [value]="user!.preferences!.backgroundColor"
class="form-control"
id="settings-background-color-option"
(colorPickerChange)="handleBackgroundColorChange()"
[style.background]="user!.preferences!.backgroundColor"
[cpAlphaChannel]="'disabled'"
[(colorPicker)]="user!.preferences!.backgroundColor"/>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<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="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="auto-close">{{t('auto-close-menu-label')}}</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> </div>
</div>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="show-screen-hints">{{t('show-screen-hints-label')}}</label>
</div>
</div>
</div>
</div>
<div class="row g-0"> <ng-template #blurUnreadSummariesTooltip>{{t('blur-unread-summaries-tooltip')}}</ng-template>
<div class="col-md-6 col-sm-12 pe-2 mb-2"> <span class="visually-hidden" id="settings-global-blurUnreadSummaries-help">
<div class="mb-3 mt-1"> <ng-container [ngTemplateOutlet]="blurUnreadSummariesTooltip"></ng-container>
<div class="form-check form-switch">
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook" class="form-check-input" [value]="true">
<label class="form-check-label me-1" for="emulate-book">{{t('emulate-comic-book-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button" tabindex="0"></i>
</div>
</div>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="swipe-to-paginate" role="switch" formControlName="swipeToPaginate" class="form-check-input" [value]="true">
<label class="form-check-label me-1" for="swipe-to-paginate">{{t('swipe-to-paginate-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered" role="button" tabindex="0"></i>
</div>
</div>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem [id]="AccordionPanelID.BookReader">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
{{t('book-reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="taptopaginate-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
<label for="taptopaginate" class="form-check-label">{{t('tap-to-paginate-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
<ng-template #tapToPaginateOptionTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-taptopaginate-option-help">
<ng-container [ngTemplateOutlet]="tapToPaginateOptionTooltip"></ng-container>
</span>
</div>
</div>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="immersivemode-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label">
<label for="immersivemode" class="form-check-label">{{t('immersive-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
<ng-template #immersivemodeOptionTooltip>{{t('immersive-mode-label')}}</ng-template>
<span class="visually-hidden" id="settings-immersivemode-option-help">
<ng-container [ngTemplateOutlet]="immersivemodeOptionTooltip"></ng-container>
</span>
</div>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-reading-direction-book-help">
<ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container>
</span>
<select id="settings-book-reading-direction" class="form-select" aria-describedby="settings-book-reading-direction-help" formControlName="bookReaderReadingDirection">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-fontfamily-option" class="form-label">{{t('font-family-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
<ng-template #fontFamilyOptionTooltip>{{t('font-family-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-fontfamily-option-help">
<ng-container [ngTemplateOutlet]="fontFamilyOptionTooltip"></ng-container>
</span>
<select id="settings-fontfamily-option" class="form-select" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | 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-book-writing-style" class="form-label me-1">{{t('writing-style-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" aria-describedby="settings-book-writing-style-help" placement="right" [ngbTooltip]="bookWritingStyleToolTip" role="button" tabindex="0"></i>
<ng-template #bookWritingStyleToolTip>{{t('writing-style-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-writing-style-help">
<ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-writing-style-help" formControlName="bookReaderWritingStyle" id="settings-book-writing-style" >
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-layout-mode" class="form-label">{{t('layout-mode-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #bookLayoutModeTooltip>{{t('layout-mode-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-layout-mode-help">
<ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-color-theme-option" class="form-label">{{t('color-theme-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
<ng-template #bookColorThemeTooltip>{{t('color-theme-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-color-theme-option-help">
<ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">{{opt.name | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label for="fontsize" class="form-label range-label">{{t('font-size-book-label')}}</label>
<input type="range" class="form-range" id="fontsize"
min="50" max="300" step="10" formControlName="bookReaderFontSize">
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<div class="range-label">
<label class="form-label" for="linespacing">{{t('line-height-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookLineHeightOptionTooltip>{{t('line-height-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-booklineheight-option-help">
<ng-container [ngTemplateOutlet]="bookLineHeightOptionTooltip"></ng-container>
</span> </span>
</div> </div>
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help">
<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="row g-0">
<div class="range-label"> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<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> <div class="form-check form-switch">
<ng-template #bookReaderMarginOptionTooltip>{{t('margin-book-tooltip')}}</ng-template> <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">
<span class="visually-hidden" id="settings-bookmargin-option-help"> <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>
<ng-container [ngTemplateOutlet]="bookReaderMarginOptionTooltip"></ng-container> </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> </span>
</div> </div>
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin">
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
</div> </div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3"> <div class="row g-0">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button> <div class="col-md-6 col-sm-12 pe-2 mb-2">
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button> <div class="form-check form-switch">
</div> <input type="checkbox" id="no-transitions" role="switch" formControlName="noTransitions" class="form-check-input"
</ng-template> aria-describedby="settings-global-noTransitions-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="no-transitions">{{t('disable-animations-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="noTransitionsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #noTransitionsTooltip>{{t('disable-animations-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-noTransitions-help">
<ng-container [ngTemplateOutlet]="noTransitionsTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="collapse-relationships" role="switch" formControlName="collapseSeriesRelationships"
aria-describedby="settings-collapse-relationships-help" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="collapse-relationships">{{t('collapse-series-relationships-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="collapseSeriesRelationshipsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #collapseSeriesRelationshipsTooltip>{{t('collapse-series-relationships-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-collapse-relationships-help">
<ng-container [ngTemplateOutlet]="collapseSeriesRelationshipsTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="share-reviews" role="switch" formControlName="shareReviews"
aria-describedby="settings-share-reviews-help" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="share-reviews">{{t('share-series-reviews-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="shareReviewsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #shareReviewsTooltip>{{t('share-series-reviews-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-share-reviews-help">
<ng-container [ngTemplateOutlet]="shareReviewsTooltip"></ng-container>
</span>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem [id]="AccordionPanelID.ImageReader">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
{{t('image-reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #readingDirectionTooltip>{{t('reading-direction-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-reading-direction-help">
<ng-container [ngTemplateOutlet]="readingDirectionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-scaling-option" class="form-label">{{t('scaling-option-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="scalingOptionTooltip" role="button" tabindex="0"></i>
<ng-template #scalingOptionTooltip>{{t('scaling-option-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-scaling-option-help">
<ng-container [ngTemplateOutlet]="scalingOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
<option *ngFor="let opt of scalingOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-pagesplit-option" class="form-label">{{t('page-splitting-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
<ng-template #pageSplitOptionTooltip>{{t('page-splitting-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-pagesplit-option-help">
<ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-readingmode-option" class="form-label">{{t('reading-mode-label')}}</label>
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
<option *ngFor="let opt of readingModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="true">
<label for="settings-layoutmode-option" class="form-label">{{t('layout-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #layoutModeTooltip>{{t('layout-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-layoutmode-option-help">
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-background-color-option" class="form-label">{{t('background-color-label')}}</label>
<input [value]="user!.preferences!.backgroundColor"
class="form-control"
id="settings-background-color-option"
(colorPickerChange)="handleBackgroundColorChange()"
[style.background]="user!.preferences!.backgroundColor"
[cpAlphaChannel]="'disabled'"
[(colorPicker)]="user!.preferences!.backgroundColor"/>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="auto-close">{{t('auto-close-menu-label')}}</label>
</div>
</div>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="show-screen-hints">{{t('show-screen-hints-label')}}</label>
</div>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook" class="form-check-input" [value]="true">
<label class="form-check-label me-1" for="emulate-book">{{t('emulate-comic-book-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button" tabindex="0"></i>
</div>
</div>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="swipe-to-paginate" role="switch" formControlName="swipeToPaginate" class="form-check-input" [value]="true">
<label class="form-check-label me-1" for="swipe-to-paginate">{{t('swipe-to-paginate-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered" role="button" tabindex="0"></i>
</div>
</div>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem [id]="AccordionPanelID.BookReader">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
{{t('book-reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="taptopaginate-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
<label for="taptopaginate" class="form-check-label">{{t('tap-to-paginate-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
<ng-template #tapToPaginateOptionTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-taptopaginate-option-help">
<ng-container [ngTemplateOutlet]="tapToPaginateOptionTooltip"></ng-container>
</span>
</div>
</div>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="immersivemode-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label">
<label for="immersivemode" class="form-check-label">{{t('immersive-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
<ng-template #immersivemodeOptionTooltip>{{t('immersive-mode-label')}}</ng-template>
<span class="visually-hidden" id="settings-immersivemode-option-help">
<ng-container [ngTemplateOutlet]="immersivemodeOptionTooltip"></ng-container>
</span>
</div>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-reading-direction-book-help">
<ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container>
</span>
<select id="settings-book-reading-direction" class="form-select" aria-describedby="settings-book-reading-direction-help" formControlName="bookReaderReadingDirection">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-fontfamily-option" class="form-label">{{t('font-family-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
<ng-template #fontFamilyOptionTooltip>{{t('font-family-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-fontfamily-option-help">
<ng-container [ngTemplateOutlet]="fontFamilyOptionTooltip"></ng-container>
</span>
<select id="settings-fontfamily-option" class="form-select" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | 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-book-writing-style" class="form-label me-1">{{t('writing-style-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" aria-describedby="settings-book-writing-style-help" placement="right" [ngbTooltip]="bookWritingStyleToolTip" role="button" tabindex="0"></i>
<ng-template #bookWritingStyleToolTip>{{t('writing-style-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-writing-style-help">
<ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-writing-style-help" formControlName="bookReaderWritingStyle" id="settings-book-writing-style" >
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-layout-mode" class="form-label">{{t('layout-mode-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #bookLayoutModeTooltip>{{t('layout-mode-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-layout-mode-help">
<ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-color-theme-option" class="form-label">{{t('color-theme-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
<ng-template #bookColorThemeTooltip>{{t('color-theme-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-color-theme-option-help">
<ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">{{opt.name | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label for="fontsize" class="form-label range-label">{{t('font-size-book-label')}}</label>
<input type="range" class="form-range" id="fontsize"
min="50" max="300" step="10" formControlName="bookReaderFontSize">
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<div class="range-label">
<label class="form-label" for="linespacing">{{t('line-height-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookLineHeightOptionTooltip>{{t('line-height-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-booklineheight-option-help">
<ng-container [ngTemplateOutlet]="bookLineHeightOptionTooltip"></ng-container>
</span>
</div>
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help">
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<div class="range-label">
<label class="form-label">{{t('margin-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReaderMarginOptionTooltip>{{t('margin-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-bookmargin-option-help">
<ng-container [ngTemplateOutlet]="bookReaderMarginOptionTooltip"></ng-container>
</span>
</div>
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin">
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </form>
</form> </ng-container>
</ng-container> }
<ng-container *ngIf="tab.fragment === FragmentID.Clients">
@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>
</ng-container> }
<!-- @defer (when tab.fragment === FragmentID.Theme; prefetch on idle) {--> @defer (when tab.fragment === FragmentID.Theme; prefetch on idle) {
<!-- <app-theme-manager></app-theme-manager>-->
<!-- }-->
<!-- @placeholder {-->
<!-- <app-loading [loading]="true"></app-loading>-->
<!-- }-->
<ng-container *ngIf="tab.fragment === FragmentID.Theme">
<app-theme-manager></app-theme-manager> <app-theme-manager></app-theme-manager>
</ng-container> }
<ng-container *ngIf="tab.fragment === FragmentID.Devices"> @defer (when tab.fragment === FragmentID.Devices; prefetch on idle) {
<app-manage-devices></app-manage-devices> <app-manage-devices></app-manage-devices>
</ng-container> }
<ng-container *ngIf="tab.fragment === FragmentID.Stats">
@defer (when tab.fragment === FragmentID.Stats; prefetch on idle) {
<app-user-stats></app-user-stats> <app-user-stats></app-user-stats>
</ng-container> }
<ng-container *ngIf="tab.fragment === FragmentID.Scrobbling">
@defer (when tab.fragment === FragmentID.Scrobbling; prefetch on idle) {
<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-container> }
</ng-template> </ng-template>
</li> </li>
</ul> </ul>

View File

@ -49,11 +49,6 @@ import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav
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 {
provideTranslocoPersistTranslations,
TranslocoPersistTranslations
} from "@ngneat/transloco-persist-translations";
import {HttpLoader} from "../../../httpLoader";
import {LoadingComponent} from "../../shared/loading/loading.component"; import {LoadingComponent} from "../../shared/loading/loading.component";
enum AccordionPanelID { enum AccordionPanelID {
@ -333,4 +328,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
d.text = translate('preferences.' + o.text); d.text = translate('preferences.' + o.text);
return d; return d;
} }
protected readonly undefined = undefined;
} }

View File

@ -1707,6 +1707,10 @@
"add-to-want-to-read": "{{actionable.add-to-want-to-read}}" "add-to-want-to-read": "{{actionable.add-to-want-to-read}}"
}, },
"next-expected-card": {
"title": "~{{date}}"
},
"server-stats": { "server-stats": {
"total-series-label": "Total Series", "total-series-label": "Total Series",
"total-series-tooltip": "Total Series: {{count}}", "total-series-tooltip": "Total Series: {{count}}",

View File

@ -17860,34 +17860,34 @@
"tagsLocked": { "tagsLocked": {
"type": "boolean" "type": "boolean"
}, },
"writersLocked": { "writerLocked": {
"type": "boolean" "type": "boolean"
}, },
"charactersLocked": { "characterLocked": {
"type": "boolean" "type": "boolean"
}, },
"coloristsLocked": { "coloristLocked": {
"type": "boolean" "type": "boolean"
}, },
"editorsLocked": { "editorLocked": {
"type": "boolean" "type": "boolean"
}, },
"inkersLocked": { "inkerLocked": {
"type": "boolean" "type": "boolean"
}, },
"letterersLocked": { "lettererLocked": {
"type": "boolean" "type": "boolean"
}, },
"pencillersLocked": { "pencillerLocked": {
"type": "boolean" "type": "boolean"
}, },
"publishersLocked": { "publisherLocked": {
"type": "boolean" "type": "boolean"
}, },
"translatorsLocked": { "translatorLocked": {
"type": "boolean" "type": "boolean"
}, },
"coverArtistsLocked": { "coverArtistLocked": {
"type": "boolean" "type": "boolean"
}, },
"releaseYearLocked": { "releaseYearLocked": {