mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Release Polish 2 (#3354)
This commit is contained in:
parent
e1aba57783
commit
0e0d8dca5b
@ -12,7 +12,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.14.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -8,9 +8,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.0.29" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.0.29" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.1.3" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.1.3" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
207
API.Tests/Helpers/ScannerHelper.cs
Normal file
207
API.Tests/Helpers/ScannerHelper.cs
Normal file
@ -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<Library> GenerateScannerData(string testcase, Dictionary<string, ComicInfo> 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<ILogger<DirectoryService>>(), fs);
|
||||
var archiveService = new ArchiveService(Substitute.For<ILogger<ArchiveService>>(), ds,
|
||||
Substitute.For<IImageService>(), Substitute.For<IMediaErrorService>());
|
||||
var readingItemService = new ReadingItemService(archiveService, Substitute.For<IBookService>(),
|
||||
Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>());
|
||||
|
||||
|
||||
var processSeries = new ProcessSeries(_unitOfWork, Substitute.For<ILogger<ProcessSeries>>(),
|
||||
Substitute.For<IEventHub>(),
|
||||
ds, Substitute.For<ICacheHelper>(), readingItemService, new FileService(fs),
|
||||
Substitute.For<IMetadataService>(),
|
||||
Substitute.For<IWordCountAnalyzerService>(),
|
||||
Substitute.For<IReadingListService>(),
|
||||
Substitute.For<IExternalMetadataService>());
|
||||
|
||||
var scanner = new ScannerService(_unitOfWork, Substitute.For<ILogger<ScannerService>>(),
|
||||
Substitute.For<IMetadataService>(),
|
||||
Substitute.For<ICacheService>(), Substitute.For<IEventHub>(), ds,
|
||||
readingItemService, processSeries, Substitute.For<IWordCountAnalyzerService>());
|
||||
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<LibraryType>(libraryTypeString, out var libraryType))
|
||||
{
|
||||
throw new ArgumentException($"'{libraryTypeString}' is not a valid LibraryType");
|
||||
}
|
||||
|
||||
return (publisher, libraryType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task<string> GenerateTestDirectory(string mapPath, Dictionary<string, ComicInfo> 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<List<string>>(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<string> filePaths, Dictionary<string, ComicInfo> 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("""<?xml version="1.0" encoding="utf-16"?>""",
|
||||
@"<?xml version='1.0' encoding='utf-8'?>");
|
||||
}
|
||||
}
|
@ -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<Library> GenerateScannerData(string testcase, Dictionary<string, ComicInfo> 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<ILogger<DirectoryService>>(), fs);
|
||||
var archiveService = new ArchiveService(Substitute.For<ILogger<ArchiveService>>(), ds,
|
||||
Substitute.For<IImageService>(), Substitute.For<IMediaErrorService>());
|
||||
var readingItemService = new ReadingItemService(archiveService, Substitute.For<IBookService>(),
|
||||
Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>());
|
||||
|
||||
|
||||
var processSeries = new ProcessSeries(_unitOfWork, Substitute.For<ILogger<ProcessSeries>>(),
|
||||
Substitute.For<IEventHub>(),
|
||||
ds, Substitute.For<ICacheHelper>(), readingItemService, new FileService(fs),
|
||||
Substitute.For<IMetadataService>(),
|
||||
Substitute.For<IWordCountAnalyzerService>(),
|
||||
Substitute.For<IReadingListService>(),
|
||||
Substitute.For<IExternalMetadataService>());
|
||||
|
||||
var scanner = new ScannerService(_unitOfWork, Substitute.For<ILogger<ScannerService>>(),
|
||||
Substitute.For<IMetadataService>(),
|
||||
Substitute.For<ICacheService>(), Substitute.For<IEventHub>(), ds,
|
||||
readingItemService, processSeries, Substitute.For<IWordCountAnalyzerService>());
|
||||
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<LibraryType>(libraryTypeString, out var libraryType))
|
||||
{
|
||||
throw new ArgumentException($"'{libraryTypeString}' is not a valid LibraryType");
|
||||
}
|
||||
|
||||
return (publisher, libraryType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task<string> GenerateTestDirectory(string mapPath, Dictionary<string, ComicInfo> 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<List<string>>(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<string> filePaths, Dictionary<string, ComicInfo> 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("""<?xml version="1.0" encoding="utf-16"?>""",
|
||||
@"<?xml version='1.0' encoding='utf-8'?>");
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
@ -70,7 +70,7 @@
|
||||
<PackageReference Include="Hangfire.InMemory" Version="1.0.0" />
|
||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.2" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.69" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.70" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
@ -82,8 +82,8 @@
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||
<PackageReference Include="NetVips" Version="2.4.2" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.15.3" />
|
||||
<PackageReference Include="NetVips" Version="3.0.0" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.16.0" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.2.1" />
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
@ -102,8 +102,8 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.1.2" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="21.0.29" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="21.1.3" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.2" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.1.3" />
|
||||
|
@ -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';
|
||||
|
@ -1,6 +1,12 @@
|
||||
<ng-container *transloco="let t; read: 'manage-email-settings'">
|
||||
|
||||
<div class="position-relative">
|
||||
<button type="button" class="btn btn-primary-outline position-absolute custom-position" (click)="test()">{{t('test')}}</button>
|
||||
</div>
|
||||
|
||||
<p>{{t('description')}}</p>
|
||||
|
||||
|
||||
<form [formGroup]="settingsForm">
|
||||
<p class="alert alert-warning">{{t('setting-description')}} {{t('test-warning')}}</p>
|
||||
|
||||
@ -86,7 +92,7 @@
|
||||
@if(settingsForm.get('enableSsl'); as formControl) {
|
||||
<app-setting-switch [title]="t('enable-ssl-label')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="setting-enable-ssl" type="checkbox" class="form-check-input" formControlName="enableSsl">
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -137,18 +143,13 @@
|
||||
@if(settingsForm.get('customizedTemplates'); as formControl) {
|
||||
<app-setting-switch [title]="t('customized-templates-label')" [subtitle]="t('customized-templates-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="settings-customized-templates" type="checkbox" class="form-check-input" formControlName="customizedTemplates">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mt-4">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="test()">{{t('test')}}</button>
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</ng-container>
|
||||
|
@ -0,0 +1,4 @@
|
||||
.custom-position {
|
||||
right: 15px;
|
||||
top: -42px;
|
||||
}
|
@ -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 => {
|
||||
|
@ -1,11 +1,4 @@
|
||||
<ng-container *transloco="let t; read: 'manage-media-settings'">
|
||||
|
||||
<div class="position-relative">
|
||||
<button class="btn btn-secondary-outline position-absolute custom-position" (click)="resetToDefaults()" [title]="t('reset-to-default')">
|
||||
<span class="phone-hidden ms-1">{{t('reset-to-default')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="settingsForm">
|
||||
<div class="mb-4">
|
||||
<p>
|
||||
|
@ -1,4 +0,0 @@
|
||||
.custom-position {
|
||||
right: 5px;
|
||||
top: -42px;
|
||||
}
|
@ -1,4 +1,10 @@
|
||||
<ng-container *transloco="let t; read: 'manage-settings'">
|
||||
|
||||
<div class="position-relative">
|
||||
<button type="button" class="btn btn-primary-outline position-absolute custom-position" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
|
||||
</div>
|
||||
|
||||
|
||||
<form [formGroup]="settingsForm">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>{{t('notice')}}</strong> {{t('restart-required')}}
|
||||
@ -217,10 +223,8 @@
|
||||
@if(settingsForm.get('enableOpds'); as formControl) {
|
||||
<app-setting-switch [title]="t('opds-label')" [subtitle]="t('opds-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch">
|
||||
<input id="opds" type="checkbox" [attr.aria-label]="t('opds-label')" class="form-check-input" formControlName="enableOpds">
|
||||
</div>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="opds" type="checkbox" [attr.aria-label]="t('opds-label')" class="form-check-input" formControlName="enableOpds">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
@ -231,10 +235,8 @@
|
||||
@if(settingsForm.get('enableFolderWatching'); as formControl) {
|
||||
<app-setting-switch [title]="t('folder-watching-label')" [subtitle]="t('folder-watching-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch">
|
||||
<input id="folder-watching" type="checkbox" class="form-check-input" formControlName="enableFolderWatching" role="switch">
|
||||
</div>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="folder-watching" type="checkbox" class="form-check-input" formControlName="enableFolderWatching" role="switch">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
@ -245,10 +247,8 @@
|
||||
@if(settingsForm.get('allowStatCollection'); as formControl) {
|
||||
<app-setting-switch [title]="t('allow-stats-label')" [subtitle]="allowStatsTooltip">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch">
|
||||
<input id="stat-collection" type="checkbox" class="form-check-input" formControlName="allowStatCollection" role="switch">
|
||||
</div>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="stat-collection" type="checkbox" class="form-check-input" formControlName="allowStatCollection" role="switch">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
@ -315,10 +315,6 @@
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mt-4">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</ng-container>
|
||||
|
@ -1,3 +1,7 @@
|
||||
.invalid-feedback {
|
||||
display: inherit;
|
||||
}
|
||||
.custom-position {
|
||||
right: 5px;
|
||||
top: -42px;
|
||||
}
|
||||
|
@ -130,7 +130,7 @@
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('manage-collection-label')" [subtitle]="t('manage-collection-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch float-end">
|
||||
<input type="checkbox" id="manage-collections" role="switch" formControlName="manageCollections" class="form-check-input">
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -140,7 +140,7 @@
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('manage-reading-list-label')" [subtitle]="t('manage-reading-list-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch float-end">
|
||||
<input type="checkbox" id="manage-readinglists" role="switch" formControlName="manageReadingLists" class="form-check-input">
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -150,7 +150,7 @@
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('allow-scrobbling-label')" [subtitle]="t('allow-scrobbling-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch float-end">
|
||||
<input type="checkbox" id="scrobbling" role="switch" formControlName="allowScrobbling" class="form-check-input">
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -160,7 +160,7 @@
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('folder-watching-label')" [subtitle]="t('folder-watching-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch float-end">
|
||||
<input type="checkbox" id="lib-folder-watching" role="switch" formControlName="folderWatching" class="form-check-input">
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -170,7 +170,7 @@
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('include-in-dashboard-label')" [subtitle]="t('include-in-dashboard-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch float-end">
|
||||
<input type="checkbox" id="include-dashboard" role="switch" formControlName="includeInDashboard" class="form-check-input">
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -181,7 +181,7 @@
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('include-in-search-label')" [subtitle]="t('include-in-search-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch float-end">
|
||||
<input type="checkbox" id="include-search" role="switch" formControlName="includeInSearch" class="form-check-input">
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
<app-setting-item [title]="title" [showEdit]="false" [canEdit]="false" [subtitle]="tooltipText" [toggleOnViewClick]="false">
|
||||
<ng-template #view>
|
||||
<input #apiKey [type]="InputType" readonly class="d-inline-flex form-control" style="width: 80%" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||
<input #apiKey [type]="InputType" readonly class="d-inline-flex form-control" style="width: 80%" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||
|
||||
<div class="d-inline-flex">
|
||||
@if (hideData) {
|
||||
@ -18,7 +18,7 @@
|
||||
</ng-template>
|
||||
@if (showRefresh) {
|
||||
<ng-template #titleActions>
|
||||
<button class="btn btn-danger-text" [ngbTooltip]="tipContent" (click)="refresh()">Reset</button>
|
||||
<button class="btn btn-danger-outline" [ngbTooltip]="tipContent" (click)="refresh()">Reset</button>
|
||||
</ng-template>
|
||||
}
|
||||
</app-setting-item>
|
||||
|
@ -2,10 +2,7 @@
|
||||
|
||||
<div class="position-relative">
|
||||
<button class="btn btn-primary-outline position-absolute custom-position" (click)="addDevice()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
<span class="phone-hidden ms-1">
|
||||
{{t('add')}}
|
||||
</span>
|
||||
<i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden ms-1">{{t('add')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('blur-unread-summaries-label')" [subtitle]="t('blur-unread-summaries-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end float-end">
|
||||
<div class="form-check form-switch float-end">
|
||||
<input type="checkbox" role="switch"
|
||||
formControlName="blurUnreadSummaries" class="form-check-input"
|
||||
aria-labelledby="auto-close-label">
|
||||
|
@ -139,6 +139,7 @@ export class ManageUserPreferencesComponent implements OnInit {
|
||||
|
||||
this.localizationService.getLocales().subscribe(res => {
|
||||
this.locales = res;
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
@ -197,9 +198,6 @@ export class ManageUserPreferencesComponent implements OnInit {
|
||||
this.settingsForm.addControl('shareReviews', new FormControl(this.user.preferences.shareReviews, []));
|
||||
this.settingsForm.addControl('locale', new FormControl(this.user.preferences.locale || 'en', []));
|
||||
|
||||
if (this.locales.length === 1) {
|
||||
this.settingsForm.get('locale')?.disable();
|
||||
}
|
||||
|
||||
// Automatically save settings as we edit them
|
||||
this.settingsForm.valueChanges.pipe(
|
||||
|
@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Kavita</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/icons/favicon-16x16.png">
|
||||
|
@ -40,6 +40,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn-danger-outline {
|
||||
color: var(--btn-danger-outline-text-color);
|
||||
background-color: var(--btn-danger-outline-bg-color);
|
||||
border-color: var(--btn-danger-outline-border-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--btn-danger-outline-hover-text-color);
|
||||
background-color: var(--btn-danger-outline-hover-bg-color);
|
||||
border-color: var(--btn-danger-outline-hover-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-actions {
|
||||
color: var(--btn-fa-icon-color);
|
||||
border-radius: var(--btn-actions-border-radius);
|
||||
|
@ -145,7 +145,15 @@
|
||||
--btn-secondary-outline-font-weight: bold;
|
||||
--btn-primary-text-text-color: white;
|
||||
--btn-secondary-text-text-color: lightgrey;
|
||||
|
||||
--btn-danger-text-text-color: var(--error-color);
|
||||
--btn-danger-outline-text-color: white;
|
||||
--btn-danger-outline-bg-color: transparent;
|
||||
--btn-danger-outline-border-color: var(--error-color);
|
||||
--btn-danger-outline-hover-text-color: white;
|
||||
--btn-danger-outline-hover-bg-color: var(--error-color);
|
||||
--btn-danger-outline-hover-border-color: var(--error-color);
|
||||
|
||||
--btn-alt-bg-color: #424c72;
|
||||
--btn-alt-border-color: #444f75;
|
||||
--btn-alt-hover-bg-color: #3b4466;
|
||||
|
Loading…
x
Reference in New Issue
Block a user