mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Another round of bugfixes (#3707)
This commit is contained in:
parent
cbb97208b8
commit
93dc6534fc
@ -90,7 +90,7 @@ public class ScannerServiceTests : AbstractDbTest
|
||||
[Fact]
|
||||
public async Task ScanLibrary_FlatSeries()
|
||||
{
|
||||
var testcase = "Flat Series - Manga.json";
|
||||
const string testcase = "Flat Series - Manga.json";
|
||||
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||
var scanner = _scannerHelper.CreateServices();
|
||||
await scanner.ScanLibrary(library.Id);
|
||||
@ -106,7 +106,7 @@ public class ScannerServiceTests : AbstractDbTest
|
||||
[Fact]
|
||||
public async Task ScanLibrary_FlatSeriesWithSpecialFolder()
|
||||
{
|
||||
var testcase = "Flat Series with Specials Folder Alt Naming - Manga.json";
|
||||
const string testcase = "Flat Series with Specials Folder Alt Naming - Manga.json";
|
||||
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||
var scanner = _scannerHelper.CreateServices();
|
||||
await scanner.ScanLibrary(library.Id);
|
||||
@ -121,7 +121,7 @@ public class ScannerServiceTests : AbstractDbTest
|
||||
[Fact]
|
||||
public async Task ScanLibrary_FlatSeriesWithSpecialFolder_AlternativeNaming()
|
||||
{
|
||||
var testcase = "Flat Series with Specials Folder Alt Naming - Manga.json";
|
||||
const string testcase = "Flat Series with Specials Folder Alt Naming - Manga.json";
|
||||
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||
var scanner = _scannerHelper.CreateServices();
|
||||
await scanner.ScanLibrary(library.Id);
|
||||
@ -302,38 +302,38 @@ public class ScannerServiceTests : AbstractDbTest
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ScanLibrary_PublishersInheritFromChapters()
|
||||
[Fact]
|
||||
public async Task ScanLibrary_PublishersInheritFromChapters()
|
||||
{
|
||||
const string testcase = "Flat Special - Manga.json";
|
||||
|
||||
var infos = new Dictionary<string, ComicInfo>();
|
||||
infos.Add("Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz", new ComicInfo()
|
||||
{
|
||||
const string testcase = "Flat Special - Manga.json";
|
||||
Publisher = "Correct Publisher"
|
||||
});
|
||||
infos.Add("Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz", new ComicInfo()
|
||||
{
|
||||
Publisher = "Special Publisher"
|
||||
});
|
||||
infos.Add("Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz", new ComicInfo()
|
||||
{
|
||||
Publisher = "Chapter Publisher"
|
||||
});
|
||||
|
||||
var infos = new Dictionary<string, ComicInfo>();
|
||||
infos.Add("Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz", new ComicInfo()
|
||||
{
|
||||
Publisher = "Correct Publisher"
|
||||
});
|
||||
infos.Add("Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz", new ComicInfo()
|
||||
{
|
||||
Publisher = "Special Publisher"
|
||||
});
|
||||
infos.Add("Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz", new ComicInfo()
|
||||
{
|
||||
Publisher = "Chapter Publisher"
|
||||
});
|
||||
|
||||
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
|
||||
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
|
||||
|
||||
|
||||
var scanner = _scannerHelper.CreateServices();
|
||||
await scanner.ScanLibrary(library.Id);
|
||||
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||
var scanner = _scannerHelper.CreateServices();
|
||||
await scanner.ScanLibrary(library.Id);
|
||||
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||
|
||||
Assert.NotNull(postLib);
|
||||
Assert.Single(postLib.Series);
|
||||
var publishers = postLib.Series.First().Metadata.People
|
||||
.Where(p => p.Role == PersonRole.Publisher);
|
||||
Assert.Equal(3, publishers.Count());
|
||||
}
|
||||
Assert.NotNull(postLib);
|
||||
Assert.Single(postLib.Series);
|
||||
var publishers = postLib.Series.First().Metadata.People
|
||||
.Where(p => p.Role == PersonRole.Publisher);
|
||||
Assert.Equal(3, publishers.Count());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
@ -908,4 +908,34 @@ public class ScannerServiceTests : AbstractDbTest
|
||||
Assert.Equal(6, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count));
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Ensure when Kavita scans, the sort order of chapters is correct
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ScanLibrary_SortOrderWorks()
|
||||
{
|
||||
const string testcase = "Sort Order - Manga.json";
|
||||
|
||||
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);
|
||||
Assert.NotNull(postLib);
|
||||
|
||||
// Get the loose leaf volume and confirm each chapter aligns with expectation of Sort Order
|
||||
var series = postLib.Series.First();
|
||||
Assert.NotNull(series);
|
||||
|
||||
var volume = series.Volumes.FirstOrDefault();
|
||||
Assert.NotNull(volume);
|
||||
|
||||
var sortedChapters = volume.Chapters.OrderBy(c => c.SortOrder).ToList();
|
||||
Assert.True(sortedChapters[0].SortOrder.Is(1f));
|
||||
Assert.True(sortedChapters[1].SortOrder.Is(4f));
|
||||
Assert.True(sortedChapters[2].SortOrder.Is(5f));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 1-3.cbz",
|
||||
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 4.cbz",
|
||||
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 5.cbz"
|
||||
]
|
@ -151,7 +151,8 @@ public class SettingsController : BaseApiController
|
||||
|
||||
try
|
||||
{
|
||||
return Ok(await _settingsService.UpdateSettings(updateSettingsDto));
|
||||
var d = await _settingsService.UpdateSettings(updateSettingsDto);
|
||||
return Ok(d);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
|
@ -49,6 +49,7 @@ public interface IReadingListRepository
|
||||
Task<IList<string>> GetRandomCoverImagesAsync(int readingListId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<bool> ReadingListExists(string name);
|
||||
Task<bool> ReadingListExistsForUser(string name, int userId);
|
||||
IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role);
|
||||
Task<ReadingListCast> GetReadingListAllPeopleAsync(int readingListId);
|
||||
Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
@ -109,6 +110,7 @@ public class ReadingListRepository : IReadingListRepository
|
||||
.SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage))
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToListAsync();
|
||||
|
||||
return data
|
||||
.OrderBy(_ => random.Next())
|
||||
.Take(4)
|
||||
@ -123,6 +125,13 @@ public class ReadingListRepository : IReadingListRepository
|
||||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
|
||||
}
|
||||
|
||||
public async Task<bool> ReadingListExistsForUser(string name, int userId)
|
||||
{
|
||||
var normalized = name.ToNormalized();
|
||||
return await _context.ReadingList
|
||||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId);
|
||||
}
|
||||
|
||||
public IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role)
|
||||
{
|
||||
return _context.ReadingListItem
|
||||
|
@ -25,7 +25,7 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
||||
MinNumber = Parser.MinNumberFromRange(number),
|
||||
MaxNumber = Parser.MaxNumberFromRange(number),
|
||||
SortOrder = Parser.MinNumberFromRange(number),
|
||||
Files = new List<MangaFile>(),
|
||||
Files = [],
|
||||
Pages = 1,
|
||||
CreatedUtc = DateTime.UtcNow
|
||||
};
|
||||
|
@ -473,6 +473,7 @@ public class ReadingListService : IReadingListService
|
||||
_logger.LogInformation("Processing Reading Lists for {SeriesName}", series.Name);
|
||||
var user = await _unitOfWork.UserRepository.GetDefaultAdminUser();
|
||||
series.Metadata ??= new SeriesMetadataBuilder().Build();
|
||||
|
||||
foreach (var chapter in series.Volumes.SelectMany(v => v.Chapters))
|
||||
{
|
||||
var pairs = new List<Tuple<string, string>>();
|
||||
@ -578,14 +579,14 @@ public class ReadingListService : IReadingListService
|
||||
{
|
||||
CblName = cblReading.Name,
|
||||
Success = CblImportResult.Success,
|
||||
Results = new List<CblBookResult>(),
|
||||
Results = [],
|
||||
SuccessfulInserts = new List<CblBookResult>()
|
||||
};
|
||||
|
||||
if (IsCblEmpty(cblReading, importSummary, out var readingListFromCbl)) return readingListFromCbl;
|
||||
|
||||
// Is there another reading list with the same name?
|
||||
if (await _unitOfWork.ReadingListRepository.ReadingListExists(cblReading.Name))
|
||||
// Is there another reading list with the same name on the user's account?
|
||||
if (await _unitOfWork.ReadingListRepository.ReadingListExistsForUser(cblReading.Name, userId))
|
||||
{
|
||||
importSummary.Success = CblImportResult.Fail;
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
@ -600,9 +601,6 @@ public class ReadingListService : IReadingListService
|
||||
var userSeries =
|
||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||
|
||||
// How can we match properly with ComicVine library when year is part of the series unless we do this in 2 passes and see which has a better match
|
||||
|
||||
|
||||
if (userSeries.Count == 0)
|
||||
{
|
||||
// Report that no series exist in the reading list
|
||||
|
@ -21,8 +21,8 @@ namespace API.Services;
|
||||
|
||||
public interface ISettingsService
|
||||
{
|
||||
Task<ActionResult<MetadataSettingsDto>> UpdateMetadataSettings(MetadataSettingsDto dto);
|
||||
Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto);
|
||||
Task<MetadataSettingsDto> UpdateMetadataSettings(MetadataSettingsDto dto);
|
||||
Task<ServerSettingDto> UpdateSettings(ServerSettingDto updateSettingsDto);
|
||||
}
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ public class SettingsService : ISettingsService
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<ActionResult<MetadataSettingsDto>> UpdateMetadataSettings(MetadataSettingsDto dto)
|
||||
public async Task<MetadataSettingsDto> UpdateMetadataSettings(MetadataSettingsDto dto)
|
||||
{
|
||||
var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettings();
|
||||
existingMetadataSetting.Enabled = dto.Enabled;
|
||||
@ -108,7 +108,7 @@ public class SettingsService : ISettingsService
|
||||
/// <param name="updateSettingsDto"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="KavitaException"></exception>
|
||||
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||
public async Task<ServerSettingDto> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
// We do not allow CacheDirectory changes, so we will ignore.
|
||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||
|
@ -871,7 +871,10 @@ public class ParseScannedFiles
|
||||
var prevIssue = string.Empty;
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (float.TryParse(chapter.Chapters, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedChapter))
|
||||
// Use MinNumber in case there is a range, as otherwise sort order will cause it to be processed last
|
||||
var chapterNum =
|
||||
$"{Parser.Parser.MinNumberFromRange(chapter.Chapters).ToString(CultureInfo.InvariantCulture)}";
|
||||
if (float.TryParse(chapterNum, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedChapter))
|
||||
{
|
||||
// Parsed successfully, use the numeric value
|
||||
counter = parsedChapter;
|
||||
|
@ -24,7 +24,7 @@ public static partial class Parser
|
||||
|
||||
public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; // Don't forget to update CoverChooser
|
||||
public const string ImageFileExtensions = @"(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; // Don't forget to update CoverChooser
|
||||
public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt";
|
||||
public const string EpubFileExtension = @"\.epub";
|
||||
public const string PdfFileExtension = @"\.pdf";
|
||||
|
223
UI/Web/package-lock.json
generated
223
UI/Web/package-lock.json
generated
@ -9,16 +9,16 @@
|
||||
"version": "0.7.12.1",
|
||||
"dependencies": {
|
||||
"@angular-slider/ngx-slider": "^19.0.0",
|
||||
"@angular/animations": "^19.2.3",
|
||||
"@angular/cdk": "^19.2.6",
|
||||
"@angular/common": "^19.2.3",
|
||||
"@angular/compiler": "^19.2.3",
|
||||
"@angular/core": "^19.2.3",
|
||||
"@angular/forms": "^19.2.3",
|
||||
"@angular/localize": "^19.2.3",
|
||||
"@angular/platform-browser": "^19.2.3",
|
||||
"@angular/platform-browser-dynamic": "^19.2.3",
|
||||
"@angular/router": "^19.2.3",
|
||||
"@angular/animations": "^19.2.5",
|
||||
"@angular/cdk": "^19.2.8",
|
||||
"@angular/common": "^19.2.5",
|
||||
"@angular/compiler": "^19.2.5",
|
||||
"@angular/core": "^19.2.5",
|
||||
"@angular/forms": "^19.2.5",
|
||||
"@angular/localize": "^19.2.5",
|
||||
"@angular/platform-browser": "^19.2.5",
|
||||
"@angular/platform-browser-dynamic": "^19.2.5",
|
||||
"@angular/router": "^19.2.5",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@iharbeck/ngx-virtual-scroller": "^19.0.1",
|
||||
"@iplab/ngx-file-upload": "^19.0.3",
|
||||
@ -36,10 +36,10 @@
|
||||
"bootstrap": "^5.3.2",
|
||||
"charts.css": "^1.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"luxon": "^3.5.0",
|
||||
"luxon": "^3.6.1",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-lazyload-image": "^9.1.3",
|
||||
"ng-select2-component": "^17.2.2",
|
||||
"ng-select2-component": "^17.2.3",
|
||||
"ngx-color-picker": "^19.0.0",
|
||||
"ngx-extended-pdf-viewer": "^23.0.0-alpha.7",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
@ -58,12 +58,12 @@
|
||||
"@angular-eslint/eslint-plugin-template": "^19.3.0",
|
||||
"@angular-eslint/schematics": "^19.3.0",
|
||||
"@angular-eslint/template-parser": "^19.3.0",
|
||||
"@angular/build": "^19.2.4",
|
||||
"@angular/cli": "^19.2.4",
|
||||
"@angular/compiler-cli": "^19.2.3",
|
||||
"@angular/build": "^19.2.6",
|
||||
"@angular/cli": "^19.2.6",
|
||||
"@angular/compiler-cli": "^19.2.5",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/luxon": "^3.4.0",
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/node": "^22.13.13",
|
||||
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
||||
"@typescript-eslint/parser": "^8.28.0",
|
||||
@ -97,12 +97,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/architect": {
|
||||
"version": "0.1902.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.4.tgz",
|
||||
"integrity": "sha512-YTLiJ7uVItZTAxRuSgASP0M5qILESWH8xGmfR+YWR1JiJem09DWEOpWeLdha1BFzUui5L+6j1btzh4FUHJOtAg==",
|
||||
"version": "0.1902.6",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.6.tgz",
|
||||
"integrity": "sha512-Dx6yPxpaE5AhP6UtrVRDCc9Ihq9B65LAbmIh3dNOyeehratuaQS0TYNKjbpaevevJojW840DTg80N+CrlfYp9g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "19.2.4",
|
||||
"@angular-devkit/core": "19.2.6",
|
||||
"rxjs": "7.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -121,9 +121,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/core": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.4.tgz",
|
||||
"integrity": "sha512-dL6AmCQsKh+CFVvO/jxX8qZpamVwt9r4iIo7fYcAI2+mTSDGxxBGWbS+onIfdPFuRp2HgKa+AT6WiHmRqu63AA==",
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.6.tgz",
|
||||
"integrity": "sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ajv": "8.17.1",
|
||||
@ -157,12 +157,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/schematics": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.4.tgz",
|
||||
"integrity": "sha512-WaFe95ncm1A+QTlUHxQcFyGKIn67xgqGX7WCj8R0QlKOS0hLKx97SG4p19uwHlww0lmAcwk/QJP6G6sPL/CJ9w==",
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.6.tgz",
|
||||
"integrity": "sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "19.2.4",
|
||||
"@angular-devkit/core": "19.2.6",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"magic-string": "0.30.17",
|
||||
"ora": "5.4.1",
|
||||
@ -304,9 +304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/animations": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.3.tgz",
|
||||
"integrity": "sha512-HQexOmwEJFX3sHLspOCi7dVOdPW5Ad4jH6tJsf+zABkF0GjgIVf4jewe1uE5ZLKgoflr9f9vpcpy39IWl00kWw==",
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.5.tgz",
|
||||
"integrity": "sha512-m4RtY3z1JuHFCh6OrOHxo25oKEigBDdR/XmdCfXIwfTiObZzNA7VQhysgdrb9IISO99kXbjZUYKDtLzgWT8Klg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -314,17 +314,18 @@
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "19.2.3"
|
||||
"@angular/common": "19.2.5",
|
||||
"@angular/core": "19.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/build": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.4.tgz",
|
||||
"integrity": "sha512-poCXvmwKri3snWa9zVJ2sW7wyJatZjkwnH6GUBdJrM2dXRQ+LYLk/oXqEjlSRBYNna7P1ZcKNqBbzu0/SnnngA==",
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.6.tgz",
|
||||
"integrity": "sha512-+VBLb4ZPLswwJmgfsTFzGex+Sq/WveNc+uaIWyHYjwnuI17NXe1qAAg1rlp72CqGn0cirisfOyAUwPc/xZAgTg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "2.3.0",
|
||||
"@angular-devkit/architect": "0.1902.4",
|
||||
"@angular-devkit/architect": "0.1902.6",
|
||||
"@babel/core": "7.26.10",
|
||||
"@babel/helper-annotate-as-pure": "7.25.9",
|
||||
"@babel/helper-split-export-declaration": "7.24.7",
|
||||
@ -347,7 +348,7 @@
|
||||
"sass": "1.85.0",
|
||||
"semver": "7.7.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"vite": "6.2.0",
|
||||
"vite": "6.2.4",
|
||||
"watchpack": "2.4.2"
|
||||
},
|
||||
"engines": {
|
||||
@ -364,7 +365,7 @@
|
||||
"@angular/localize": "^19.0.0 || ^19.2.0-next.0",
|
||||
"@angular/platform-server": "^19.0.0 || ^19.2.0-next.0",
|
||||
"@angular/service-worker": "^19.0.0 || ^19.2.0-next.0",
|
||||
"@angular/ssr": "^19.2.4",
|
||||
"@angular/ssr": "^19.2.6",
|
||||
"karma": "^6.4.0",
|
||||
"less": "^4.2.0",
|
||||
"ng-packagr": "^19.0.0 || ^19.2.0-next.0",
|
||||
@ -464,15 +465,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cdk": {
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.6.tgz",
|
||||
"integrity": "sha512-AneN/NeAYU4+AwzicTwtYE9CkMcWA0cAJ41SNfSyoHaaHNXSkryzwSmTYS3FO+taqd7OGnBePeWJbW2uJXcvfA==",
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.8.tgz",
|
||||
"integrity": "sha512-ZZqWVYFF80TdjWkk2sc9Pn2luhiYeC78VH3Yjeln4wXMsTGDsvKPBcuOxSxxpJ31saaVBehDjBUuXMqGRj8KuA==",
|
||||
"dependencies": {
|
||||
"parse5": "^7.1.2",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"parse5": "^7.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^19.0.0 || ^20.0.0",
|
||||
"@angular/core": "^19.0.0 || ^20.0.0",
|
||||
@ -480,17 +479,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cli": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.4.tgz",
|
||||
"integrity": "sha512-YmZYrxdGBwSZsrnpS6vr9gQ8+PrZHzwyYW/3jU2NRAMtl0ZlipDyfpLIDgrfqYPeumzr7+SKtJYVvlsMnjkN4g==",
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.6.tgz",
|
||||
"integrity": "sha512-eZhFOSsDUHKaciwcWdU5C54ViAvPPdZJf42So93G2vZWDtEq6Uk47huocn1FY9cMhDvURfYLNrrLMpUDtUSsSA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/architect": "0.1902.4",
|
||||
"@angular-devkit/core": "19.2.4",
|
||||
"@angular-devkit/schematics": "19.2.4",
|
||||
"@angular-devkit/architect": "0.1902.6",
|
||||
"@angular-devkit/core": "19.2.6",
|
||||
"@angular-devkit/schematics": "19.2.6",
|
||||
"@inquirer/prompts": "7.3.2",
|
||||
"@listr2/prompt-adapter-inquirer": "2.0.18",
|
||||
"@schematics/angular": "19.2.4",
|
||||
"@schematics/angular": "19.2.6",
|
||||
"@yarnpkg/lockfile": "1.1.0",
|
||||
"ini": "5.0.0",
|
||||
"jsonc-parser": "3.3.1",
|
||||
@ -513,9 +512,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/common": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.3.tgz",
|
||||
"integrity": "sha512-cYOMRXFb6Sjtg9BI3bE/Ave+xU234jQmHYj7hBxr3MiqRSVJL4niy10KiA/Jiz6y76V5QfZfS+0aE65VuoqAvg==",
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.5.tgz",
|
||||
"integrity": "sha512-vFCBdas4C5PxP6ts/4TlRddWD3DUmI3aaO0QZdZvqyLHy428t84ruYdsJXKaeD8ie2U4/9F3a1tsklclRG/BBA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -523,14 +522,14 @@
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "19.2.3",
|
||||
"@angular/core": "19.2.5",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/compiler": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.3.tgz",
|
||||
"integrity": "sha512-TL/JIU7vzSWD+IrMq2PAiHZi7IUFSRhdHo8q6/WuZ8SQmbuXCK2pJvHZpTtUdLswdPeD/UVhkhTAhQzEyEdZVg==",
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.5.tgz",
|
||||
"integrity": "sha512-34J+HubQjwkbZ0AUtU5sa4Zouws9XtP/fKaysMQecoYJTZ3jewzLSRu3aAEZX1Y4gIrcVVKKIxM6oWoXKwYMOA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -539,9 +538,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/compiler-cli": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.3.tgz",
|
||||
"integrity": "sha512-ePh/7A6eEDAyfVn8QgLcAvrxhXBAf6mTqB/3+HwQeXLaka1gtN6xvZ6cjLEegP4s6kcYGhdfdLwzCcy0kjsY5g==",
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.5.tgz",
|
||||
"integrity": "sha512-b2cG41r6lilApXLlvja1Ra2D00dM3BxmQhoElKC1tOnpD6S3/krlH1DOnBB2I55RBn9iv4zdmPz1l8zPUSh7DQ==",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.26.9",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
@ -561,7 +560,7 @@
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "19.2.3",
|
||||
"@angular/compiler": "19.2.5",
|
||||
"typescript": ">=5.5 <5.9"
|
||||
}
|
||||
},
|
||||
@ -592,9 +591,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/core": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.3.tgz",
|
||||
"integrity": "sha512-uNDbQBDWdAfL8JhgG2l9eTEbikovZ+SthLUKERyR4fL7AVGSx85LjNySRuq4WAL4eiD1cRN1UUgu8o+WKqF/ow==",
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.5.tgz",
|
||||
"integrity": "sha512-NNEz1sEZz1mBpgf6Tz3aJ9b8KjqpTiMYhHfCYA9h9Ipe4D8gUmOsvPHPK2M755OX7p7PmUmzp1XCUHYrZMVHRw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -607,9 +606,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/forms": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.3.tgz",
|
||||
"integrity": "sha512-JEgNKiZd3taYBg9lsMvoana5cv1QGke8xkuryc9zesHPJjhw9QHllmDPOW2HyUuwPqXZ/YkHiuCMOk+4qPjsAw==",
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.5.tgz",
|
||||
"integrity": "sha512-2Zvy3qK1kOxiAX9fdSaeG48q7oyO/4RlMYlg1w+ra9qX1SrgwF3OQ2P2Vs+ojg1AxN3z9xFp4aYaaID/G2LZAw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -617,16 +616,16 @@
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "19.2.3",
|
||||
"@angular/core": "19.2.3",
|
||||
"@angular/platform-browser": "19.2.3",
|
||||
"@angular/common": "19.2.5",
|
||||
"@angular/core": "19.2.5",
|
||||
"@angular/platform-browser": "19.2.5",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/localize": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.2.3.tgz",
|
||||
"integrity": "sha512-4YUvii9uJCT5D1n3RR5W68UizrwsFr85ucIB9G3rQXL9y1LA2GbeKQkgazpF9nTCBDAfrux7tKvS27owofY5ww==",
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.2.5.tgz",
|
||||
"integrity": "sha512-oAc19bubk6Z/2Vv6OkV0MsjdgC8cUaUwBmwdc6blFVe1NCX1KjdaqDyC2EQAO3nWfcdV4uvOOuu8myxB64bamw==",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.26.9",
|
||||
"@types/babel__core": "7.20.5",
|
||||
@ -642,14 +641,14 @@
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "19.2.3",
|
||||
"@angular/compiler-cli": "19.2.3"
|
||||
"@angular/compiler": "19.2.5",
|
||||
"@angular/compiler-cli": "19.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.3.tgz",
|
||||
"integrity": "sha512-bz5mvUkCS8SxaMInjPgi/2dD7vpWkZePQesvr/bBRNQbYSE4cGTbovXcVl3X5hIxs5JoC6Het0lS2IxDq7j6qg==",
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.5.tgz",
|
||||
"integrity": "sha512-Lshy++X16cvl6OPvfzMySpsqEaCPKEJmDjz7q7oSt96oxlh6LvOeOUVLjsNyrNaIt9NadpWoqjlu/I9RTPJkpw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -657,9 +656,9 @@
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "19.2.3",
|
||||
"@angular/common": "19.2.3",
|
||||
"@angular/core": "19.2.3"
|
||||
"@angular/animations": "19.2.5",
|
||||
"@angular/common": "19.2.5",
|
||||
"@angular/core": "19.2.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@angular/animations": {
|
||||
@ -668,9 +667,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser-dynamic": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.3.tgz",
|
||||
"integrity": "sha512-PHmmtdGxSfe9HL8xR4g3PspnEaPqTEOGyzNviAHugfkZCgXCdSbYs36d3i0nPwhExMAeuIVXbbJyouDn2kyeOw==",
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.5.tgz",
|
||||
"integrity": "sha512-15in8u4552EcdWNTXY2h0MKuJbk3AuXwWr0zVTum4CfB/Ss2tNTrDEdWhgAbhnUI0e9jZQee/fhBbA1rleMYrA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -678,16 +677,16 @@
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "19.2.3",
|
||||
"@angular/compiler": "19.2.3",
|
||||
"@angular/core": "19.2.3",
|
||||
"@angular/platform-browser": "19.2.3"
|
||||
"@angular/common": "19.2.5",
|
||||
"@angular/compiler": "19.2.5",
|
||||
"@angular/core": "19.2.5",
|
||||
"@angular/platform-browser": "19.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/router": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.3.tgz",
|
||||
"integrity": "sha512-yYVMT7CceKqE+fBXxkhkAqEQUEdY/BHtLQr1vP9rEnAf30vwKghDEresugfegY6Ch4IGKTBtDG/QGmxWszgUAQ==",
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.5.tgz",
|
||||
"integrity": "sha512-9pSfmdNXLjaOKj0kd4UxBC7sFdCFOnRGbftp397G3KWqsLsGSKmNFzqhXNeA5QHkaVxnpmpm8HzXU+zYV5JwSg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -695,9 +694,9 @@
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "19.2.3",
|
||||
"@angular/core": "19.2.3",
|
||||
"@angular/platform-browser": "19.2.3",
|
||||
"@angular/common": "19.2.5",
|
||||
"@angular/core": "19.2.5",
|
||||
"@angular/platform-browser": "19.2.5",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
@ -3562,13 +3561,13 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@schematics/angular": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.4.tgz",
|
||||
"integrity": "sha512-P7fphIPbqHHYRVRPiFl7RAHYPYhINGSUYOXrcThVBBsgKQBX18oNdUWvhZA6ylwK/9T21XB20VyLjNy0d78H1Q==",
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.6.tgz",
|
||||
"integrity": "sha512-fmbF9ONmEZqxHocCwOSWG2mHp4a22d1uW+DZUBUgZSBUFIrnFw42deOxDq8mkZOZ1Tc73UpLN2GKI7iJeUqS2A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "19.2.4",
|
||||
"@angular-devkit/schematics": "19.2.4",
|
||||
"@angular-devkit/core": "19.2.6",
|
||||
"@angular-devkit/schematics": "19.2.6",
|
||||
"jsonc-parser": "3.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -4063,9 +4062,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
|
||||
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
@ -5372,7 +5371,6 @@
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
@ -7008,9 +7006,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
|
||||
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
|
||||
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -7414,9 +7412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ng-select2-component": {
|
||||
"version": "17.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-17.2.2.tgz",
|
||||
"integrity": "sha512-dAeUSqmjU9Gexi47vMEz1bXGQkl3Be2O0wl6QqpYwFvM+QEfUyQiY0zWpYvB8shO1sIHoCQNKt9yTFcRzvzW0g==",
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-17.2.3.tgz",
|
||||
"integrity": "sha512-JNik7OWqya4ERuqlfnYiJHkaqyZtHqUhATIZ9yUxmadWWNIn8I3Lwa7qt0KtPpR01O9HJC0PtHXhvev88Cju2A==",
|
||||
"dependencies": {
|
||||
"ngx-infinite-scroll": ">=18.0.0 || >=19.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
@ -7946,7 +7944,6 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
||||
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"entities": "^4.4.0"
|
||||
},
|
||||
@ -9212,9 +9209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
|
||||
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
|
||||
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
@ -17,16 +17,16 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-slider/ngx-slider": "^19.0.0",
|
||||
"@angular/animations": "^19.2.3",
|
||||
"@angular/cdk": "^19.2.6",
|
||||
"@angular/common": "^19.2.3",
|
||||
"@angular/compiler": "^19.2.3",
|
||||
"@angular/core": "^19.2.3",
|
||||
"@angular/forms": "^19.2.3",
|
||||
"@angular/localize": "^19.2.3",
|
||||
"@angular/platform-browser": "^19.2.3",
|
||||
"@angular/platform-browser-dynamic": "^19.2.3",
|
||||
"@angular/router": "^19.2.3",
|
||||
"@angular/animations": "^19.2.5",
|
||||
"@angular/cdk": "^19.2.8",
|
||||
"@angular/common": "^19.2.5",
|
||||
"@angular/compiler": "^19.2.5",
|
||||
"@angular/core": "^19.2.5",
|
||||
"@angular/forms": "^19.2.5",
|
||||
"@angular/localize": "^19.2.5",
|
||||
"@angular/platform-browser": "^19.2.5",
|
||||
"@angular/platform-browser-dynamic": "^19.2.5",
|
||||
"@angular/router": "^19.2.5",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@iharbeck/ngx-virtual-scroller": "^19.0.1",
|
||||
"@iplab/ngx-file-upload": "^19.0.3",
|
||||
@ -44,10 +44,10 @@
|
||||
"bootstrap": "^5.3.2",
|
||||
"charts.css": "^1.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"luxon": "^3.5.0",
|
||||
"luxon": "^3.6.1",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-lazyload-image": "^9.1.3",
|
||||
"ng-select2-component": "^17.2.2",
|
||||
"ng-select2-component": "^17.2.3",
|
||||
"ngx-color-picker": "^19.0.0",
|
||||
"ngx-extended-pdf-viewer": "^23.0.0-alpha.7",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
@ -66,12 +66,12 @@
|
||||
"@angular-eslint/eslint-plugin-template": "^19.3.0",
|
||||
"@angular-eslint/schematics": "^19.3.0",
|
||||
"@angular-eslint/template-parser": "^19.3.0",
|
||||
"@angular/build": "^19.2.4",
|
||||
"@angular/cli": "^19.2.4",
|
||||
"@angular/compiler-cli": "^19.2.3",
|
||||
"@angular/build": "^19.2.6",
|
||||
"@angular/cli": "^19.2.6",
|
||||
"@angular/compiler-cli": "^19.2.5",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/luxon": "^3.4.0",
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/node": "^22.13.13",
|
||||
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
||||
"@typescript-eslint/parser": "^8.28.0",
|
||||
|
@ -1,23 +1,27 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { BulkAddToCollectionComponent } from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component';
|
||||
import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component';
|
||||
import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component';
|
||||
import { ConfirmService } from '../shared/confirm.service';
|
||||
import { LibrarySettingsModalComponent } from '../sidenav/_modals/library-settings-modal/library-settings-modal.component';
|
||||
import { Chapter } from '../_models/chapter';
|
||||
import { Device } from '../_models/device/device';
|
||||
import { Library } from '../_models/library/library';
|
||||
import { ReadingList } from '../_models/reading-list';
|
||||
import { Series } from '../_models/series';
|
||||
import { Volume } from '../_models/volume';
|
||||
import { DeviceService } from './device.service';
|
||||
import { LibraryService } from './library.service';
|
||||
import { MemberService } from './member.service';
|
||||
import { ReaderService } from './reader.service';
|
||||
import { SeriesService } from './series.service';
|
||||
import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {BulkAddToCollectionComponent} from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component';
|
||||
import {ADD_FLOW, AddToListModalComponent} from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component';
|
||||
import {
|
||||
EditReadingListModalComponent
|
||||
} from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component';
|
||||
import {ConfirmService} from '../shared/confirm.service';
|
||||
import {
|
||||
LibrarySettingsModalComponent
|
||||
} from '../sidenav/_modals/library-settings-modal/library-settings-modal.component';
|
||||
import {Chapter} from '../_models/chapter';
|
||||
import {Device} from '../_models/device/device';
|
||||
import {Library} from '../_models/library/library';
|
||||
import {ReadingList} from '../_models/reading-list';
|
||||
import {Series} from '../_models/series';
|
||||
import {Volume} from '../_models/volume';
|
||||
import {DeviceService} from './device.service';
|
||||
import {LibraryService} from './library.service';
|
||||
import {MemberService} from './member.service';
|
||||
import {ReaderService} from './reader.service';
|
||||
import {SeriesService} from './series.service';
|
||||
import {translate} from "@jsverse/transloco";
|
||||
import {UserCollection} from "../_models/collection-tag";
|
||||
import {CollectionTagService} from "./collection-tag.service";
|
||||
@ -652,7 +656,7 @@ export class ActionService {
|
||||
}
|
||||
|
||||
editReadingList(readingList: ReadingList, callback?: ReadingListActionCallback) {
|
||||
const readingListModalRef = this.modalService.open(EditReadingListModalComponent, { scrollable: true, size: 'lg', fullscreen: 'md' });
|
||||
const readingListModalRef = this.modalService.open(EditReadingListModalComponent, DefaultModalOptions);
|
||||
readingListModalRef.componentInstance.readingList = readingList;
|
||||
readingListModalRef.closed.pipe(take(1)).subscribe((list) => {
|
||||
if (callback && list !== undefined) {
|
||||
@ -773,7 +777,7 @@ export class ActionService {
|
||||
}
|
||||
|
||||
matchSeries(series: Series, callback?: BooleanActionCallback) {
|
||||
const ref = this.modalService.open(MatchSeriesModalComponent, {size: 'lg'});
|
||||
const ref = this.modalService.open(MatchSeriesModalComponent, DefaultModalOptions);
|
||||
ref.componentInstance.series = series;
|
||||
ref.closed.subscribe(saved => {
|
||||
if (callback) {
|
||||
|
@ -47,30 +47,33 @@
|
||||
}
|
||||
|
||||
|
||||
<div class="setting-section-break" aria-hidden="true"></div>
|
||||
@if (!suppressEmptyGenres || genres.length > 0) {
|
||||
<div class="setting-section-break" aria-hidden="true"></div>
|
||||
|
||||
|
||||
<div class="mb-3 ms-1">
|
||||
<h4 class="header">{{t('genres-title')}}</h4>
|
||||
<div class="ms-3">
|
||||
<app-badge-expander [includeComma]="true" [items]="genres" [itemsTillExpander]="3" [defaultExpanded]="true">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Genres, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
<div class="mb-3 ms-1">
|
||||
<h4 class="header">{{t('genres-title')}}</h4>
|
||||
<div class="ms-3">
|
||||
<app-badge-expander [includeComma]="true" [items]="genres" [itemsTillExpander]="3" [defaultExpanded]="true">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Genres, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-3 ms-1">
|
||||
<h4 class="header">{{t('tags-title')}}</h4>
|
||||
<div class="ms-3">
|
||||
<app-badge-expander [includeComma]="true" [items]="tags" [itemsTillExpander]="3" [defaultExpanded]="true">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Tags, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
@if (!suppressEmptyTags || tags.length > 0) {
|
||||
<div class="mb-3 ms-1">
|
||||
<h4 class="header">{{t('tags-title')}}</h4>
|
||||
<div class="ms-3">
|
||||
<app-badge-expander [includeComma]="true" [items]="tags" [itemsTillExpander]="3" [defaultExpanded]="true">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Tags, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="webLinks" [title]="t('weblinks-title')">
|
||||
|
@ -61,6 +61,8 @@ export class DetailsTabComponent {
|
||||
@Input() genres: Array<Genre> = [];
|
||||
@Input() tags: Array<Tag> = [];
|
||||
@Input() webLinks: Array<string> = [];
|
||||
@Input() suppressEmptyGenres: boolean = false;
|
||||
@Input() suppressEmptyTags: boolean = false;
|
||||
|
||||
|
||||
openGeneric(queryParamName: FilterField, filter: string | number) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
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, switchMap, take, tap} from 'rxjs';
|
||||
import {debounceTime, distinctUntilChanged, filter, switchMap, tap} from 'rxjs';
|
||||
import {SettingsService} from '../settings.service';
|
||||
import {ServerSettings} from '../_models/server-settings';
|
||||
import {translate, TranslocoModule} from "@jsverse/transloco";
|
||||
@ -30,7 +30,7 @@ export class ManageEmailSettingsComponent implements OnInit {
|
||||
settingsForm: FormGroup = new FormGroup({});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.settingsService.getServerSettings().subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)]));
|
||||
|
||||
@ -100,6 +100,8 @@ export class ManageEmailSettingsComponent implements OnInit {
|
||||
|
||||
packData() {
|
||||
const modelSettings = Object.assign({}, this.serverSettings);
|
||||
|
||||
|
||||
modelSettings.emailServiceUrl = this.settingsForm.get('emailServiceUrl')?.value;
|
||||
modelSettings.hostName = this.settingsForm.get('hostName')?.value;
|
||||
|
||||
|
@ -47,15 +47,15 @@ export class ManageSettingsComponent implements OnInit {
|
||||
translate('manage-settings.allow-stats-tooltip-part-2');
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsService.getTaskFrequencies().pipe(take(1)).subscribe(frequencies => {
|
||||
this.settingsService.getTaskFrequencies().subscribe(frequencies => {
|
||||
this.taskFrequencies = frequencies;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.settingsService.getLoggingLevels().pipe(take(1)).subscribe(levels => {
|
||||
this.settingsService.getLoggingLevels().subscribe(levels => {
|
||||
this.logLevels = levels;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.settingsService.getServerSettings().subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.settingsForm.addControl('cacheDirectory', new FormControl(this.serverSettings.cacheDirectory, [Validators.required]));
|
||||
this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required]));
|
||||
|
@ -37,7 +37,7 @@
|
||||
</td>
|
||||
<td>
|
||||
@if (member.libraries.length > 0) {
|
||||
@if (hasAdminRole(member)) {
|
||||
@if (hasAdminRole(member) || member.libraries.length === libraryCount) {
|
||||
{{t('all-libraries')}}
|
||||
} @else {
|
||||
@if (member.libraries.length > 5) {
|
||||
|
@ -13,7 +13,7 @@ import {EditUserComponent} from '../edit-user/edit-user.component';
|
||||
import {Router} from '@angular/router';
|
||||
import {TagBadgeComponent} from '../../shared/tag-badge/tag-badge.component';
|
||||
import {AsyncPipe, NgClass, TitleCasePipe} from '@angular/common';
|
||||
import {translate, TranslocoModule, TranslocoService} from "@jsverse/transloco";
|
||||
import {TranslocoModule, TranslocoService} from "@jsverse/transloco";
|
||||
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
@ -50,6 +50,7 @@ export class ManageUsersComponent implements OnInit {
|
||||
members: Member[] = [];
|
||||
loggedInUsername = '';
|
||||
loadingMembers = false;
|
||||
libraryCount: number = 0;
|
||||
|
||||
|
||||
constructor() {
|
||||
@ -81,7 +82,11 @@ export class ManageUsersComponent implements OnInit {
|
||||
if (nameA < nameB) return -1;
|
||||
if (nameA > nameB) return 1;
|
||||
return 0;
|
||||
})
|
||||
});
|
||||
|
||||
// Get the admin and get their library count
|
||||
this.libraryCount = this.members.filter(m => this.hasAdminRole(m))[0].libraries.length;
|
||||
|
||||
this.loadingMembers = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
@ -142,16 +147,8 @@ export class ManageUsersComponent implements OnInit {
|
||||
modalRef.componentInstance.member = member;
|
||||
}
|
||||
|
||||
formatLibraries(member: Member) {
|
||||
if (member.libraries.length === 0) {
|
||||
return translate('manage-users.none');
|
||||
}
|
||||
|
||||
return member.libraries.map(item => item.name).join(', ');
|
||||
}
|
||||
|
||||
hasAdminRole(member: Member) {
|
||||
return member.roles.indexOf('Admin') >= 0;
|
||||
return member.roles.indexOf(Role.Admin) >= 0;
|
||||
}
|
||||
|
||||
getRoles(member: Member) {
|
||||
|
@ -722,6 +722,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// Update the window Height
|
||||
this.updateWidthAndHeightCalcs();
|
||||
this.updateImageSizes();
|
||||
|
||||
const resumeElement = this.getFirstVisibleElementXPath();
|
||||
if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) {
|
||||
this.scrollTo(resumeElement); // This works pretty well, but not perfect
|
||||
@ -944,7 +945,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.bookService.getBookPage(this.chapterId, this.pageNum).pipe(take(1)).subscribe(content => {
|
||||
this.isSingleImagePage = this.checkSingleImagePage(content) // This needs be performed before we set this.page to avoid image jumping
|
||||
this.updateSingleImagePageStyles()
|
||||
this.updateSingleImagePageStyles();
|
||||
this.page = this.domSanitizer.bypassSecurityTrustHtml(content); // PERF: Potential optimization to prefetch next/prev page and store in localStorage
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -52,12 +52,15 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) {
|
||||
@if (libraryType !== LibraryType.Images) {
|
||||
<div class="card-body meta-title">
|
||||
<span class="card-format">
|
||||
</span>
|
||||
<span class="card-format"></span>
|
||||
<div class="card-content d-flex justify-content-center align-items-center text-center" style="width:100%;min-height:58px;">
|
||||
{{volume.name}}
|
||||
@if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) {
|
||||
{{volume.name}}
|
||||
} @else {
|
||||
{{volume.chapters[0].titleName}}
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (actions && actions.length > 0) {
|
||||
|
@ -2,15 +2,15 @@
|
||||
@if (items.length > virtualizeAfter) {
|
||||
<div class="example-list list-group-flush">
|
||||
<virtual-scroller #scroll [items]="items" [bufferAmount]="BufferAmount" [parentScroll]="parentScroll">
|
||||
<div class="example-box" *ngFor="let item of scroll.viewPortItems; index as i; trackBy: trackByIdentity">
|
||||
|
||||
<div class="d-flex list-container">
|
||||
<ng-container [ngTemplateOutlet]="handle" [ngTemplateOutletContext]="{ $implicit: item, idx: i, isVirtualized: true }"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
<ng-container [ngTemplateOutlet]="removeBtn" [ngTemplateOutletContext]="{$implicit: item, idx: i}"></ng-container>
|
||||
@for (item of scroll.viewPortItems; track trackByIdentity(i, item); let i = $index) {
|
||||
<div class="example-box">
|
||||
<div class="d-flex list-container">
|
||||
<ng-container [ngTemplateOutlet]="handle" [ngTemplateOutletContext]="{ $implicit: item, idx: i, isVirtualized: true }"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="removeBtn" [ngTemplateOutletContext]="{$implicit: item, idx: i}"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</virtual-scroller>
|
||||
</div>
|
||||
} @else {
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
TrackByFunction
|
||||
} from '@angular/core';
|
||||
import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
|
||||
import {NgClass, NgFor, NgTemplateOutlet} from '@angular/common';
|
||||
import {NgClass, NgTemplateOutlet} from '@angular/common';
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {BulkSelectionService} from "../../../cards/bulk-selection.service";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
@ -36,13 +36,14 @@ export interface ItemRemoveEvent {
|
||||
templateUrl: './draggable-ordered-list.component.html',
|
||||
styleUrls: ['./draggable-ordered-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [VirtualScrollerModule, NgFor, NgTemplateOutlet, CdkDropList, CdkDrag,
|
||||
imports: [VirtualScrollerModule, NgTemplateOutlet, CdkDropList, CdkDrag,
|
||||
CdkDragHandle, TranslocoDirective, NgClass, FormsModule]
|
||||
})
|
||||
export class DraggableOrderedListComponent {
|
||||
|
||||
protected readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
|
||||
/**
|
||||
@ -84,7 +85,7 @@ export class DraggableOrderedListComponent {
|
||||
return Math.min(this.items.length / 20, 20);
|
||||
}
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) {
|
||||
constructor() {
|
||||
this.bulkSelectionService.selections$.pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe((s) => {
|
||||
|
@ -252,6 +252,8 @@
|
||||
@defer (when activeTabId === TabID.Details; prefetch on idle) {
|
||||
<app-details-tab [metadata]="castInfo"
|
||||
[readingTime]="rlInfo"
|
||||
[suppressEmptyGenres]="true"
|
||||
[suppressEmptyTags]="true"
|
||||
[ageRating]="readingList.ageRating"/>
|
||||
}
|
||||
</ng-template>
|
||||
|
@ -636,7 +636,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) {
|
||||
async handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) {
|
||||
switch (action.action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.markChapterAsRead(chapter);
|
||||
@ -657,6 +657,14 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
const device = (action._extra!.data as Device);
|
||||
this.actionService.sendToDevice([chapter.id], device);
|
||||
break;
|
||||
case (Action.Delete):
|
||||
await this.actionService.deleteChapter(chapter.id, (success) => {
|
||||
if (!success) return;
|
||||
|
||||
this.chapters = this.chapters.filter(c => c.id != chapter.id);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
.text-muted {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
|
@ -18,69 +18,69 @@
|
||||
|
||||
<ng-template #tooltip>{{t('format-tooltip')}}</ng-template>
|
||||
|
||||
@let files = files$ | async;
|
||||
|
||||
<ng-container *ngIf="files$ | async as files">
|
||||
<ng-container *ngIf="formControl.value; else tableLayout">
|
||||
<ngx-charts-advanced-pie-chart [results]="vizData2$ | async"></ngx-charts-advanced-pie-chart>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #tableLayout>
|
||||
@if (formControl.value) {
|
||||
<ngx-charts-advanced-pie-chart [results]="vizData2$ | async" />
|
||||
} @else {
|
||||
<div style="height: 242px; overflow-y: auto;">
|
||||
<table class="table table-striped table-striped table-sm scrollable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" sortable="extension" direction="asc" (sort)="onSort($event)">
|
||||
{{t('extension-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="format" (sort)="onSort($event)">
|
||||
{{t('format-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="totalSize" (sort)="onSort($event)">
|
||||
{{t('total-size-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="totalFiles" (sort)="onSort($event)">
|
||||
{{t('total-files-header')}}
|
||||
</th>
|
||||
<th scope="col">{{t('download-file-for-extension-header')}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col" sortable="extension" direction="asc" (sort)="onSort($event)">
|
||||
{{t('extension-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="format" (sort)="onSort($event)">
|
||||
{{t('format-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="totalSize" (sort)="onSort($event)">
|
||||
{{t('total-size-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="totalFiles" (sort)="onSort($event)">
|
||||
{{t('total-files-header')}}
|
||||
</th>
|
||||
<th scope="col">{{t('download-file-for-extension-header')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of files; let idx = index;">
|
||||
<td id="adhoctask--{{idx}}">
|
||||
{{item.extension || t('not-classified')}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.format | mangaFormat}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.totalSize | bytes}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.totalFiles | number:'1.0-0'}}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-icon" style="color: var(--primary-color)" (click)="export(item.extension)" [disabled]="downloadInProgress[item.extension]">
|
||||
@if (downloadInProgress[item.extension]) {
|
||||
<div class="spinner-border spinner-border-sm" aria-hidden="true"></div>
|
||||
} @else {
|
||||
<i class="fa-solid fa-file-arrow-down" aria-hidden="true"></i>
|
||||
}
|
||||
<span class="visually-hidden">{{t('download-file-for-extension-alt"', {extension: item.extension})}}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@for (item of files; track item.extension; let idx = $index) {
|
||||
<tr>
|
||||
<td id="adhoctask--{{idx}}">
|
||||
{{item.extension || t('not-classified')}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.format | mangaFormat}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.totalSize | bytes}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.totalFiles | number:'1.0-0'}}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-icon" style="color: var(--primary-color)" (click)="export(item.extension)" [disabled]="downloadInProgress[item.extension]">
|
||||
@if (downloadInProgress[item.extension]) {
|
||||
<div class="spinner-border spinner-border-sm" aria-hidden="true"></div>
|
||||
} @else {
|
||||
<i class="fa-solid fa-file-arrow-down" aria-hidden="true"></i>
|
||||
}
|
||||
<span class="visually-hidden">{{t('download-file-for-extension-alt', {extension: item.extension})}}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>{{t('total-file-size-title')}}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{((rawData$ | async)?.totalFileSize || 0) | bytes}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{t('total-file-size-title')}}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{((rawData$ | async)?.totalFileSize || 0) | bytes}}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -9,4 +9,8 @@
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
tfoot {
|
||||
color: var(--bs-body);
|
||||
}
|
||||
|
@ -1,26 +1,29 @@
|
||||
import {
|
||||
ChangeDetectionStrategy, ChangeDetectorRef,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
QueryList, TemplateRef, ViewChild,
|
||||
QueryList,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { PieChartModule } from '@swimlane/ngx-charts';
|
||||
import {Observable, BehaviorSubject, combineLatest, map, shareReplay, switchMap} from 'rxjs';
|
||||
import { StatisticsService } from 'src/app/_services/statistics.service';
|
||||
import { SortableHeader, SortEvent, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive';
|
||||
import { FileExtension, FileExtensionBreakdown } from '../../_models/file-breakdown';
|
||||
import { PieDataItem } from '../../_models/pie-data-item';
|
||||
import {FormControl, ReactiveFormsModule} from '@angular/forms';
|
||||
import {PieChartModule} from '@swimlane/ngx-charts';
|
||||
import {BehaviorSubject, combineLatest, map, Observable, shareReplay} from 'rxjs';
|
||||
import {StatisticsService} from 'src/app/_services/statistics.service';
|
||||
import {compare, SortableHeader, SortEvent} from 'src/app/_single-module/table/_directives/sortable-header.directive';
|
||||
import {FileExtension, FileExtensionBreakdown} from '../../_models/file-breakdown';
|
||||
import {PieDataItem} from '../../_models/pie-data-item';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { MangaFormatPipe } from '../../../_pipes/manga-format.pipe';
|
||||
import { BytesPipe } from '../../../_pipes/bytes.pipe';
|
||||
import { NgIf, NgFor, AsyncPipe, DecimalPipe } from '@angular/common';
|
||||
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
|
||||
import {Pagination} from "../../../_models/pagination";
|
||||
import {DownloadService} from "../../../shared/_services/download.service";
|
||||
import {MangaFormatPipe} from '../../../_pipes/manga-format.pipe';
|
||||
import {BytesPipe} from '../../../_pipes/bytes.pipe';
|
||||
import {AsyncPipe, DecimalPipe, NgFor, NgIf} from '@angular/common';
|
||||
import {TranslocoDirective, TranslocoService} from "@jsverse/transloco";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable";
|
||||
import {UtcToLocalTimePipe} from "../../../_pipes/utc-to-local-time.pipe";
|
||||
|
||||
export interface StackedBarChartDataItem {
|
||||
name: string,
|
||||
@ -32,7 +35,7 @@ export interface StackedBarChartDataItem {
|
||||
templateUrl: './file-breakdown-stats.component.html',
|
||||
styleUrls: ['./file-breakdown-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgbTooltip, ReactiveFormsModule, NgIf, PieChartModule, NgFor, AsyncPipe, DecimalPipe, BytesPipe, MangaFormatPipe, TranslocoDirective, SortableHeader]
|
||||
imports: [NgbTooltip, ReactiveFormsModule, NgIf, PieChartModule, NgFor, AsyncPipe, DecimalPipe, BytesPipe, MangaFormatPipe, TranslocoDirective, SortableHeader, NgxDatatableModule, UtcToLocalTimePipe]
|
||||
})
|
||||
export class FileBreakdownStatsComponent {
|
||||
|
||||
@ -103,4 +106,5 @@ export class FileBreakdownStatsComponent {
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly ColumnMode = ColumnMode;
|
||||
}
|
||||
|
@ -16,16 +16,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@let statuses = publicationStatues$ | async;
|
||||
|
||||
|
||||
<ng-container *ngIf="publicationStatues$ | async as statuses">
|
||||
<ng-container *ngIf="formControl.value; else tableLayout">
|
||||
<ngx-charts-advanced-pie-chart
|
||||
[results]="statuses"
|
||||
>
|
||||
</ngx-charts-advanced-pie-chart>
|
||||
</ng-container>
|
||||
<ng-template #tableLayout>
|
||||
@if (formControl.value) {
|
||||
<ngx-charts-advanced-pie-chart [results]="statuses" />
|
||||
} @else {
|
||||
<div style="height: 242px; overflow-y: auto;">
|
||||
<table class="table table-striped table-striped table-sm scrollable">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -48,8 +44,8 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -53,7 +53,6 @@
|
||||
<li (click)="handleOptionClick(option)"
|
||||
class="list-group-item" role="option" [attr.data-index]="index"
|
||||
(mouseenter)="focusedIndex = index + (showAddItem ? 1 : 0); updateHighlight();">
|
||||
{{settings.trackByIdentityFn(index, option)}}
|
||||
<ng-container [ngTemplateOutlet]="optionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index, value: typeaheadControl.value }"></ng-container>
|
||||
</li>
|
||||
}
|
||||
|
@ -1074,7 +1074,7 @@
|
||||
"donate": "Donate",
|
||||
"donate-tooltip": "You can remove this by subscribing to Kavita+",
|
||||
"back": "Back",
|
||||
"cancel-edit": "Cancel Edit",
|
||||
"cancel-edit": "Close Reorder",
|
||||
"more": "More",
|
||||
"customize": "{{settings.customize}}",
|
||||
"edit": "{{common.edit}}"
|
||||
|
@ -7,7 +7,7 @@ html {
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--body-font-family);
|
||||
font-family: var(--body-font-family), serif;
|
||||
color: var(--body-text-color);
|
||||
color-scheme: var(--color-scheme);
|
||||
max-height: 100%;
|
||||
@ -21,13 +21,13 @@ body {
|
||||
|
||||
hr {
|
||||
background-color: var(--hr-color);
|
||||
border-top: 0px;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.accent {
|
||||
background-color: var(--accent-bg-color) !important;
|
||||
color: var(--accent-text-color) !important;
|
||||
box-shadow: inset 0px 0px 8px 1px var(--accent-bg-color) !important;
|
||||
box-shadow: inset 0 0 8px 1px var(--accent-bg-color) !important;
|
||||
font-size: var(--accent-text-size) !important;
|
||||
font-style: italic;
|
||||
padding: 10px;
|
||||
@ -38,6 +38,12 @@ hr {
|
||||
color: var(--text-muted-color) !important;
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.form-switch .form-check-input:checked {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user