From 0e0d8dca5bf5eb7b243e759fad1ed4c49b14b913 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Sat, 9 Nov 2024 14:05:17 -0600 Subject: [PATCH] Release Polish 2 (#3354) --- API.Benchmark/API.Benchmark.csproj | 2 +- API.Tests/API.Tests.csproj | 6 +- API.Tests/Helpers/ScannerHelper.cs | 207 ++++++++++++++++ API.Tests/Services/ScannerServiceTests.cs | 233 +++--------------- API/API.csproj | 10 +- .../age-rating-image.component.ts | 21 +- .../manage-email-settings.component.html | 15 +- .../manage-email-settings.component.scss | 4 + .../manage-email-settings.component.ts | 30 +-- .../manage-media-settings.component.html | 7 - .../manage-media-settings.component.scss | 4 - .../manage-settings.component.html | 28 +-- .../manage-settings.component.scss | 4 + .../library-settings-modal.component.html | 12 +- .../api-key/api-key.component.html | 4 +- .../manage-devices.component.html | 5 +- .../manage-user-preferences.component.html | 2 +- .../manage-user-preferences.component.ts | 4 +- UI/Web/src/index.html | 2 +- UI/Web/src/theme/components/_buttons.scss | 12 + UI/Web/src/theme/themes/dark.scss | 8 + 21 files changed, 333 insertions(+), 287 deletions(-) create mode 100644 API.Tests/Helpers/ScannerHelper.cs diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index 2dcf08f32..222213438 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -12,7 +12,7 @@ - + diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index af3d32e1f..df946c10b 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -8,9 +8,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/API.Tests/Helpers/ScannerHelper.cs b/API.Tests/Helpers/ScannerHelper.cs new file mode 100644 index 000000000..e164d015e --- /dev/null +++ b/API.Tests/Helpers/ScannerHelper.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; +using API.Data; +using API.Data.Metadata; +using API.Entities; +using API.Entities.Enums; +using API.Helpers; +using API.Helpers.Builders; +using API.Services; +using API.Services.Plus; +using API.Services.Tasks; +using API.Services.Tasks.Metadata; +using API.Services.Tasks.Scanner; +using API.SignalR; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit.Abstractions; + +namespace API.Tests.Helpers; + +public class ScannerHelper +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ITestOutputHelper _testOutputHelper; + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"); + private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/TestCases"); + private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/1x1.png"); + private static readonly string[] ComicInfoExtensions = new[] { ".cbz", ".cbr", ".zip", ".rar" }; + + public ScannerHelper(IUnitOfWork unitOfWork, ITestOutputHelper testOutputHelper) + { + _unitOfWork = unitOfWork; + _testOutputHelper = testOutputHelper; + } + + public async Task GenerateScannerData(string testcase, Dictionary comicInfos = null) + { + var testDirectoryPath = await GenerateTestDirectory(Path.Join(_testcasesDirectory, testcase), comicInfos); + + var (publisher, type) = SplitPublisherAndLibraryType(Path.GetFileNameWithoutExtension(testcase)); + + var library = new LibraryBuilder(publisher, type) + .WithFolders([new FolderPath() {Path = testDirectoryPath}]) + .Build(); + + var admin = new AppUserBuilder("admin", "admin@kavita.com", Seed.DefaultThemes[0]) + .WithLibrary(library) + .Build(); + + _unitOfWork.UserRepository.Add(admin); // Admin is needed for generating collections/reading lists + _unitOfWork.LibraryRepository.Add(library); + await _unitOfWork.CommitAsync(); + + return library; + } + + public ScannerService CreateServices() + { + var fs = new FileSystem(); + var ds = new DirectoryService(Substitute.For>(), fs); + var archiveService = new ArchiveService(Substitute.For>(), ds, + Substitute.For(), Substitute.For()); + var readingItemService = new ReadingItemService(archiveService, Substitute.For(), + Substitute.For(), ds, Substitute.For>()); + + + var processSeries = new ProcessSeries(_unitOfWork, Substitute.For>(), + Substitute.For(), + ds, Substitute.For(), readingItemService, new FileService(fs), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For()); + + var scanner = new ScannerService(_unitOfWork, Substitute.For>(), + Substitute.For(), + Substitute.For(), Substitute.For(), ds, + readingItemService, processSeries, Substitute.For()); + return scanner; + } + + private static (string Publisher, LibraryType Type) SplitPublisherAndLibraryType(string input) + { + // Split the input string based on " - " + var parts = input.Split(" - ", StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length != 2) + { + throw new ArgumentException("Input must be in the format 'Publisher - LibraryType'"); + } + + var publisher = parts[0].Trim(); + var libraryTypeString = parts[1].Trim(); + + // Try to parse the right-hand side as a LibraryType enum + if (!Enum.TryParse(libraryTypeString, out var libraryType)) + { + throw new ArgumentException($"'{libraryTypeString}' is not a valid LibraryType"); + } + + return (publisher, libraryType); + } + + + + private async Task GenerateTestDirectory(string mapPath, Dictionary comicInfos = null) + { + // Read the map file + var mapContent = await File.ReadAllTextAsync(mapPath); + + // Deserialize the JSON content into a list of strings using System.Text.Json + var filePaths = JsonSerializer.Deserialize>(mapContent); + + // Create a test directory + var testDirectory = Path.Combine(_testDirectory, Path.GetFileNameWithoutExtension(mapPath)); + if (Directory.Exists(testDirectory)) + { + Directory.Delete(testDirectory, true); + } + Directory.CreateDirectory(testDirectory); + + // Generate the files and folders + await Scaffold(testDirectory, filePaths, comicInfos); + + _testOutputHelper.WriteLine($"Test Directory Path: {testDirectory}"); + + return testDirectory; + } + + + public async Task Scaffold(string testDirectory, List filePaths, Dictionary comicInfos = null) + { + foreach (var relativePath in filePaths) + { + var fullPath = Path.Combine(testDirectory, relativePath); + var fileDir = Path.GetDirectoryName(fullPath); + + // Create the directory if it doesn't exist + if (!Directory.Exists(fileDir)) + { + Directory.CreateDirectory(fileDir); + Console.WriteLine($"Created directory: {fileDir}"); + } + + var ext = Path.GetExtension(fullPath).ToLower(); + if (ComicInfoExtensions.Contains(ext) && comicInfos != null && comicInfos.TryGetValue(Path.GetFileName(relativePath), out var info)) + { + CreateMinimalCbz(fullPath, info); + } + else + { + // Create an empty file + await File.Create(fullPath).DisposeAsync(); + Console.WriteLine($"Created empty file: {fullPath}"); + } + } + } + + private void CreateMinimalCbz(string filePath, ComicInfo? comicInfo = null) + { + using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Create)) + { + // Add the 1x1 image to the archive + archive.CreateEntryFromFile(_imagePath, "1x1.png"); + + if (comicInfo != null) + { + // Serialize ComicInfo object to XML + var comicInfoXml = SerializeComicInfoToXml(comicInfo); + + // Create an entry for ComicInfo.xml in the archive + var entry = archive.CreateEntry("ComicInfo.xml"); + using var entryStream = entry.Open(); + using var writer = new StreamWriter(entryStream, Encoding.UTF8); + + // Write the XML to the archive + writer.Write(comicInfoXml); + } + + } + Console.WriteLine($"Created minimal CBZ archive: {filePath} with{(comicInfo != null ? "" : "out")} metadata."); + } + + + private static string SerializeComicInfoToXml(ComicInfo comicInfo) + { + var xmlSerializer = new XmlSerializer(typeof(ComicInfo)); + using var stringWriter = new StringWriter(); + using (var xmlWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings { Indent = true, Encoding = new UTF8Encoding(false), OmitXmlDeclaration = false})) + { + xmlSerializer.Serialize(xmlWriter, comicInfo); + } + + // For the love of god, I spent 2 hours trying to get utf-8 with no BOM + return stringWriter.ToString().Replace("""""", + @""); + } +} diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 5fd8db860..2c09fe9b9 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -36,10 +36,8 @@ namespace API.Tests.Services; public class ScannerServiceTests : AbstractDbTest { private readonly ITestOutputHelper _testOutputHelper; + private readonly ScannerHelper _scannerHelper; private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"); - private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/TestCases"); - private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/1x1.png"); - private static readonly string[] ComicInfoExtensions = new[] { ".cbz", ".cbr", ".zip", ".rar" }; public ScannerServiceTests(ITestOutputHelper testOutputHelper) { @@ -47,6 +45,7 @@ public class ScannerServiceTests : AbstractDbTest // Set up Hangfire to use in-memory storage for testing GlobalConfiguration.Configuration.UseInMemoryStorage(); + _scannerHelper = new ScannerHelper(_unitOfWork, testOutputHelper); } protected override async Task ResetDb() @@ -59,8 +58,8 @@ public class ScannerServiceTests : AbstractDbTest public async Task ScanLibrary_ComicVine_PublisherFolder() { var testcase = "Publisher - ComicVine.json"; - var library = await GenerateScannerData(testcase); - var scanner = CreateServices(); + var library = await _scannerHelper.GenerateScannerData(testcase); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -72,8 +71,8 @@ public class ScannerServiceTests : AbstractDbTest public async Task ScanLibrary_ShouldCombineNestedFolder() { var testcase = "Series and Series-Series Combined - Manga.json"; - var library = await GenerateScannerData(testcase); - var scanner = CreateServices(); + var library = await _scannerHelper.GenerateScannerData(testcase); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -87,8 +86,8 @@ public class ScannerServiceTests : AbstractDbTest public async Task ScanLibrary_FlatSeries() { var testcase = "Flat Series - Manga.json"; - var library = await GenerateScannerData(testcase); - var scanner = CreateServices(); + var library = await _scannerHelper.GenerateScannerData(testcase); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -103,8 +102,8 @@ public class ScannerServiceTests : AbstractDbTest public async Task ScanLibrary_FlatSeriesWithSpecialFolder() { var testcase = "Flat Series with Specials Folder - Manga.json"; - var library = await GenerateScannerData(testcase); - var scanner = CreateServices(); + var library = await _scannerHelper.GenerateScannerData(testcase); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -119,8 +118,8 @@ public class ScannerServiceTests : AbstractDbTest { const string testcase = "Flat Special - Manga.json"; - var library = await GenerateScannerData(testcase); - var scanner = CreateServices(); + var library = await _scannerHelper.GenerateScannerData(testcase); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -136,8 +135,8 @@ public class ScannerServiceTests : AbstractDbTest { const string testcase = "Scan Library Parses as ( - Manga.json"; - var library = await GenerateScannerData(testcase); - var scanner = CreateServices(); + var library = await _scannerHelper.GenerateScannerData(testcase); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -165,10 +164,10 @@ public class ScannerServiceTests : AbstractDbTest LocalizedSeries = "Sono Bisque Doll wa Koi wo Suru" }); - var library = await GenerateScannerData(testcase, infos); + var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var scanner = CreateServices(); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -190,10 +189,10 @@ public class ScannerServiceTests : AbstractDbTest LocalizedSeries = "Futoku no Guild" // Filename has a capital N and localizedSeries has lowercase }); - var library = await GenerateScannerData(testcase, infos); + var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var scanner = CreateServices(); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -221,10 +220,10 @@ public class ScannerServiceTests : AbstractDbTest Series = "The Novel's Extra", }); - var library = await GenerateScannerData(testcase, infos); + var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var scanner = CreateServices(); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -245,10 +244,10 @@ public class ScannerServiceTests : AbstractDbTest { const string testcase = "Image Series with SP Folder - Manga.json"; - var library = await GenerateScannerData(testcase); + var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = CreateServices(); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -265,10 +264,10 @@ public class ScannerServiceTests : AbstractDbTest { const string testcase = "Image Series with SP Folder (Non English) - Image.json"; - var library = await GenerateScannerData(testcase); + var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = CreateServices(); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -303,10 +302,10 @@ public class ScannerServiceTests : AbstractDbTest Publisher = "Chapter Publisher" }); - var library = await GenerateScannerData(testcase, infos); + var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var scanner = CreateServices(); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -327,10 +326,10 @@ public class ScannerServiceTests : AbstractDbTest { const string testcase = "PDF Comic Chapters - Comic.json"; - var library = await GenerateScannerData(testcase); + var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = CreateServices(); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -346,10 +345,10 @@ public class ScannerServiceTests : AbstractDbTest { const string testcase = "PDF Comic Chapters - LightNovel.json"; - var library = await GenerateScannerData(testcase); + var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = CreateServices(); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -376,10 +375,10 @@ public class ScannerServiceTests : AbstractDbTest LocalizedSeries = "Sono Bisque Doll wa Koi wo Suru" }); - var library = await GenerateScannerData(testcase, infos); + var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var scanner = CreateServices(); + var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); @@ -391,7 +390,7 @@ public class ScannerServiceTests : AbstractDbTest // Bootstrap a new file in the nested "Sono Bisque Doll wa Koi wo Suru" directory and perform a series scan var testDirectory = Path.Combine(_testDirectory, Path.GetFileNameWithoutExtension(testcase)); - await Scaffold(testDirectory, ["My Dress-Up Darling/Sono Bisque Doll wa Koi wo Suru ch 11.cbz"]); + await _scannerHelper.Scaffold(testDirectory, ["My Dress-Up Darling/Sono Bisque Doll wa Koi wo Suru ch 11.cbz"]); // Now that a new file exists in the subdirectory, scan again await scanner.ScanSeries(series.Id); @@ -399,170 +398,4 @@ public class ScannerServiceTests : AbstractDbTest Assert.Equal(3, series.Volumes.Count); Assert.Equal(2, series.Volumes.First(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber)).Chapters.Count); } - - - #region Setup - private async Task GenerateScannerData(string testcase, Dictionary comicInfos = null) - { - var testDirectoryPath = await GenerateTestDirectory(Path.Join(_testcasesDirectory, testcase), comicInfos); - - var (publisher, type) = SplitPublisherAndLibraryType(Path.GetFileNameWithoutExtension(testcase)); - - var library = new LibraryBuilder(publisher, type) - .WithFolders([new FolderPath() {Path = testDirectoryPath}]) - .Build(); - - var admin = new AppUserBuilder("admin", "admin@kavita.com", Seed.DefaultThemes[0]) - .WithLibrary(library) - .Build(); - - _unitOfWork.UserRepository.Add(admin); // Admin is needed for generating collections/reading lists - _unitOfWork.LibraryRepository.Add(library); - await _unitOfWork.CommitAsync(); - - return library; - } - - private ScannerService CreateServices() - { - var fs = new FileSystem(); - var ds = new DirectoryService(Substitute.For>(), fs); - var archiveService = new ArchiveService(Substitute.For>(), ds, - Substitute.For(), Substitute.For()); - var readingItemService = new ReadingItemService(archiveService, Substitute.For(), - Substitute.For(), ds, Substitute.For>()); - - - var processSeries = new ProcessSeries(_unitOfWork, Substitute.For>(), - Substitute.For(), - ds, Substitute.For(), readingItemService, new FileService(fs), - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For()); - - var scanner = new ScannerService(_unitOfWork, Substitute.For>(), - Substitute.For(), - Substitute.For(), Substitute.For(), ds, - readingItemService, processSeries, Substitute.For()); - return scanner; - } - - private static (string Publisher, LibraryType Type) SplitPublisherAndLibraryType(string input) - { - // Split the input string based on " - " - var parts = input.Split(" - ", StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length != 2) - { - throw new ArgumentException("Input must be in the format 'Publisher - LibraryType'"); - } - - var publisher = parts[0].Trim(); - var libraryTypeString = parts[1].Trim(); - - // Try to parse the right-hand side as a LibraryType enum - if (!Enum.TryParse(libraryTypeString, out var libraryType)) - { - throw new ArgumentException($"'{libraryTypeString}' is not a valid LibraryType"); - } - - return (publisher, libraryType); - } - - - - private async Task GenerateTestDirectory(string mapPath, Dictionary comicInfos = null) - { - // Read the map file - var mapContent = await File.ReadAllTextAsync(mapPath); - - // Deserialize the JSON content into a list of strings using System.Text.Json - var filePaths = JsonSerializer.Deserialize>(mapContent); - - // Create a test directory - var testDirectory = Path.Combine(_testDirectory, Path.GetFileNameWithoutExtension(mapPath)); - if (Directory.Exists(testDirectory)) - { - Directory.Delete(testDirectory, true); - } - Directory.CreateDirectory(testDirectory); - - // Generate the files and folders - await Scaffold(testDirectory, filePaths, comicInfos); - - _testOutputHelper.WriteLine($"Test Directory Path: {testDirectory}"); - - return testDirectory; - } - - - private async Task Scaffold(string testDirectory, List filePaths, Dictionary comicInfos = null) - { - foreach (var relativePath in filePaths) - { - var fullPath = Path.Combine(testDirectory, relativePath); - var fileDir = Path.GetDirectoryName(fullPath); - - // Create the directory if it doesn't exist - if (!Directory.Exists(fileDir)) - { - Directory.CreateDirectory(fileDir); - Console.WriteLine($"Created directory: {fileDir}"); - } - - var ext = Path.GetExtension(fullPath).ToLower(); - if (ComicInfoExtensions.Contains(ext) && comicInfos != null && comicInfos.TryGetValue(Path.GetFileName(relativePath), out var info)) - { - CreateMinimalCbz(fullPath, info); - } - else - { - // Create an empty file - await File.Create(fullPath).DisposeAsync(); - Console.WriteLine($"Created empty file: {fullPath}"); - } - } - } - - private void CreateMinimalCbz(string filePath, ComicInfo? comicInfo = null) - { - using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Create)) - { - // Add the 1x1 image to the archive - archive.CreateEntryFromFile(_imagePath, "1x1.png"); - - if (comicInfo != null) - { - // Serialize ComicInfo object to XML - var comicInfoXml = SerializeComicInfoToXml(comicInfo); - - // Create an entry for ComicInfo.xml in the archive - var entry = archive.CreateEntry("ComicInfo.xml"); - using var entryStream = entry.Open(); - using var writer = new StreamWriter(entryStream, Encoding.UTF8); - - // Write the XML to the archive - writer.Write(comicInfoXml); - } - - } - Console.WriteLine($"Created minimal CBZ archive: {filePath} with{(comicInfo != null ? "" : "out")} metadata."); - } - - - private static string SerializeComicInfoToXml(ComicInfo comicInfo) - { - var xmlSerializer = new XmlSerializer(typeof(ComicInfo)); - using var stringWriter = new StringWriter(); - using (var xmlWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings { Indent = true, Encoding = new UTF8Encoding(false), OmitXmlDeclaration = false})) - { - xmlSerializer.Serialize(xmlWriter, comicInfo); - } - - // For the love of god, I spent 2 hours trying to get utf-8 with no BOM - return stringWriter.ToString().Replace("""""", - @""); - } - #endregion } diff --git a/API/API.csproj b/API/API.csproj index 73ab423e4..c0b4a5112 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -70,7 +70,7 @@ - + @@ -82,8 +82,8 @@ - - + + @@ -102,8 +102,8 @@ - - + + diff --git a/UI/Web/src/app/_single-modules/age-rating-image/age-rating-image.component.ts b/UI/Web/src/app/_single-modules/age-rating-image/age-rating-image.component.ts index b9bf2befd..46b63ed94 100644 --- a/UI/Web/src/app/_single-modules/age-rating-image/age-rating-image.component.ts +++ b/UI/Web/src/app/_single-modules/age-rating-image/age-rating-image.component.ts @@ -1,4 +1,13 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, + Input, + OnChanges, + OnInit, + SimpleChanges +} from '@angular/core'; import {AgeRating} from "../../_models/metadata/age-rating"; import {ImageComponent} from "../../shared/image/image.component"; import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; @@ -23,7 +32,7 @@ const basePath = './assets/images/ratings/'; styleUrl: './age-rating-image.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class AgeRatingImageComponent implements OnInit { +export class AgeRatingImageComponent implements OnInit, OnChanges { private readonly cdRef = inject(ChangeDetectorRef); private readonly filterUtilityService = inject(FilterUtilitiesService); @@ -34,6 +43,14 @@ export class AgeRatingImageComponent implements OnInit { imageUrl: string = 'unknown-rating.png'; ngOnInit() { + this.setImage(); + } + + ngOnChanges() { + this.setImage(); + } + + setImage() { switch (this.rating) { case AgeRating.Unknown: this.imageUrl = basePath + 'unknown-rating.png'; diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html index 099059b3c..1f3bf2559 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html @@ -1,6 +1,12 @@ + +
+ +
+

{{t('description')}}

+

{{t('setting-description')}} {{t('test-warning')}}

@@ -86,7 +92,7 @@ @if(settingsForm.get('enableSsl'); as formControl) { -
+
@@ -137,18 +143,13 @@ @if(settingsForm.get('customizedTemplates'); as formControl) { -
+
}
- -
- - -
diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.scss b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.scss index e69de29bb..4b988b958 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.scss +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.scss @@ -0,0 +1,4 @@ +.custom-position { + right: 15px; + top: -42px; +} diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts index 5c05e2d5e..f9c88092e 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts @@ -1,14 +1,14 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {ToastrService} from 'ngx-toastr'; -import {debounceTime, distinctUntilChanged, filter, map, switchMap, take, tap} from 'rxjs'; +import {debounceTime, distinctUntilChanged, filter, switchMap, take, tap} from 'rxjs'; import {SettingsService} from '../settings.service'; import {ServerSettings} from '../_models/server-settings'; import { NgbAlert, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; -import {AsyncPipe, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common'; +import {AsyncPipe, NgTemplateOutlet, TitleCasePipe} from '@angular/common'; import {translate, TranslocoModule} from "@jsverse/transloco"; import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; import {ManageMediaIssuesComponent} from "../manage-media-issues/manage-media-issues.component"; @@ -17,6 +17,7 @@ import {SettingSwitchComponent} from "../../settings/_components/setting-switch/ import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {BytesPipe} from "../../_pipes/bytes.pipe"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component"; @Component({ selector: 'app-manage-email-settings', @@ -24,8 +25,8 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; styleUrls: ['./manage-email-settings.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe, - ManageMediaIssuesComponent, TitleCasePipe, NgbAlert, SettingItemComponent, SettingSwitchComponent, DefaultValuePipe, BytesPipe, AsyncPipe] + imports: [ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe, + ManageMediaIssuesComponent, TitleCasePipe, NgbAlert, SettingItemComponent, SettingSwitchComponent, DefaultValuePipe, BytesPipe, AsyncPipe, CardActionablesComponent] }) export class ManageEmailSettingsComponent implements OnInit { @@ -125,27 +126,6 @@ export class ManageEmailSettingsComponent implements OnInit { return modelSettings; } - async saveSettings() { - const modelSettings = this.packData(); - - this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => { - this.serverSettings = settings; - this.resetForm(); - this.toastr.success(translate('toasts.server-settings-updated')); - }, (err: any) => { - console.error('error: ', err); - }); - } - - resetToDefaults() { - this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { - this.serverSettings = settings; - this.resetForm(); - this.toastr.success(translate('toasts.server-settings-updated')); - }, (err: any) => { - console.error('error: ', err); - }); - } test() { this.settingsService.testEmailServerSettings().subscribe(res => { diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html index b6707c890..21fdf537a 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html @@ -1,11 +1,4 @@ - -
- -
-

diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.scss b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.scss index 30e2c0819..e69de29bb 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.scss +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.scss @@ -1,4 +0,0 @@ -.custom-position { - right: 5px; - top: -42px; -} diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index ab3e50526..3f5c71d61 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -1,4 +1,10 @@ + +

+ +
+ +