From ebd4ec25bf8d366a6f799fd979c5b96a9dd30043 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Fri, 23 Jul 2021 18:02:14 -0500 Subject: [PATCH] Shakeout Fixes (#422) # Fixed - Fixed: Clean the pdf extension from Series title for PDF types - Fixed: Fixed a bug where a forced metadata refresh wouldn't trigger a volume to force a refresh of cover image - Fixed: Fixed an issue where Removing series no longer on disk would not use the Series Format and thus after deleting files, they would not be removed. - Fixed: Fixed an issue with reading a single image file, where the cache code would not properly move the file - Fixed: For Specials, Get Next/Prev Chapter should use the filename instead of arbitrary Number (which is always 0). Use the same sorting logic when requesting volumes on series detail, so sorting can happen in the backend. # Added - Added: (Accessibility) Nearly every page has had a title set for it =============================================================================== * Clean the pdf extension from ParseSeries * Fixed a bug where forced metadata refresh wouldn't trigger the volume to update it's image. * Added titles to most pages to help distinguish back/forward history. Fixed a bug in the scanner which didn't account for Format when calculating if we need to remove a series not on disk. * For Specials, Get Next/Prev Chapter should use the filename instead of arbitrary Number (which is always 0). Use the same sorting logic when requesting volumes on series detail, so sorting can happen in the backend. * Fixed unit tests --- API.Tests/Parser/BookParserTests.cs | 2 +- API.Tests/Services/ScannerServiceTests.cs | 2 +- API/Controllers/ReaderController.cs | 7 ++- API/Data/SeriesRepository.cs | 55 ++++++++++++------- API/Extensions/SeriesExtensions.cs | 2 +- API/Parser/Parser.cs | 6 ++ API/Services/CacheService.cs | 11 +++- API/Services/MetadataService.cs | 33 +++++------ .../Tasks/Scanner/ParseScannedFiles.cs | 24 +++++--- API/Services/Tasks/ScannerService.cs | 44 +++++++-------- Kavita.Common/KavitaException.cs | 2 +- UI/Web/src/app/_guards/auth.guard.ts | 2 +- .../admin/dashboard/dashboard.component.ts | 7 ++- .../all-collections.component.ts | 6 +- UI/Web/src/app/home/home.component.ts | 4 +- .../library-detail.component.ts | 2 +- UI/Web/src/app/library/library.component.ts | 7 ++- .../manga-reader/manga-reader.component.ts | 2 +- .../app/nav-header/nav-header.component.ts | 2 +- .../series-detail/series-detail.component.ts | 12 ++-- .../card-details-modal.component.ts | 3 +- .../shared/_services/natural-sort.service.ts | 1 + .../user-preferences.component.ts | 4 +- 23 files changed, 143 insertions(+), 97 deletions(-) diff --git a/API.Tests/Parser/BookParserTests.cs b/API.Tests/Parser/BookParserTests.cs index abeff081d..219f5d723 100644 --- a/API.Tests/Parser/BookParserTests.cs +++ b/API.Tests/Parser/BookParserTests.cs @@ -11,4 +11,4 @@ namespace API.Tests.Parser Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename)); } } -} \ No newline at end of file +} diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 8eb9bc60f..70244a8f1 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -52,7 +52,7 @@ namespace API.Tests.Services IUnitOfWork unitOfWork = new UnitOfWork(_context, Substitute.For(), null); - IMetadataService metadataService = Substitute.For(unitOfWork, _metadataLogger, _archiveService, _bookService, _directoryService, _imageService); + IMetadataService metadataService = Substitute.For(unitOfWork, _metadataLogger, _archiveService, _bookService, _imageService); _scannerService = new ScannerService(unitOfWork, _logger, _archiveService, metadataService, _bookService); } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index c9970b1e5..746d3a6ce 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -21,6 +21,7 @@ namespace API.Controllers private readonly IUnitOfWork _unitOfWork; private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer(); private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst(); + private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer(); public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork) { @@ -295,8 +296,8 @@ namespace API.Controllers var currentChapter = await _unitOfWork.VolumeRepository.GetChapterAsync(currentChapterId); if (currentVolume.Number == 0) { - // Handle specials - var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer), currentChapter.Number); + // Handle specials by sorting on their Filename aka Range + var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Range, _naturalSortComparer), currentChapter.Number); if (chapterId > 0) return Ok(chapterId); } @@ -362,7 +363,7 @@ namespace API.Controllers if (currentVolume.Number == 0) { - var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer).Reverse(), currentChapter.Number); + var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Range, _naturalSortComparer).Reverse(), currentChapter.Number); if (chapterId > 0) return Ok(chapterId); } diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index 07d7102e1..ad4b93e6b 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using API.Comparators; using API.DTOs; using API.Entities; using API.Extensions; @@ -16,7 +18,7 @@ namespace API.Data { private readonly DataContext _context; private readonly IMapper _mapper; - + private readonly NaturalSortComparer _naturalSortComparer = new (); public SeriesRepository(DataContext context, IMapper mapper) { _context = context; @@ -37,7 +39,7 @@ namespace API.Data { return await _context.SaveChangesAsync() > 0; } - + public bool SaveAll() { return _context.SaveChanges() > 0; @@ -60,12 +62,12 @@ namespace API.Data .Where(s => libraries.Contains(s.LibraryId) && s.Name == name) .CountAsync() > 1; } - + public Series GetSeriesByName(string name) { return _context.Series.SingleOrDefault(x => x.Name == name); } - + public async Task> GetSeriesForLibraryIdAsync(int libraryId) { return await _context.Series @@ -73,7 +75,7 @@ namespace API.Data .OrderBy(s => s.SortName) .ToListAsync(); } - + public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams) { var query = _context.Series @@ -84,12 +86,12 @@ namespace API.Data return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } - + public async Task> SearchSeries(int[] libraryIds, string searchQuery) { return await _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) - .Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%") + .Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%") || EF.Functions.Like(s.OriginalName, $"%{searchQuery}%") || EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%")) .Include(s => s.Library) @@ -108,12 +110,23 @@ namespace API.Data .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .ToListAsync(); - + await AddVolumeModifiers(userId, volumes); + SortSpecialChapters(volumes); + + return volumes; } + private void SortSpecialChapters(IEnumerable volumes) + { + foreach (var v in volumes.Where(vdto => vdto.Number == 0)) + { + v.Chapters = v.Chapters.OrderBy(x => x.Range, _naturalSortComparer).ToList(); + } + } + public async Task> GetVolumes(int seriesId) { @@ -133,7 +146,7 @@ namespace API.Data var seriesList = new List() {series}; await AddSeriesModifiers(userId, seriesList); - + return seriesList[0]; } @@ -152,7 +165,7 @@ namespace API.Data .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .SingleAsync(); - + } public async Task GetVolumeDtoAsync(int volumeId, int userId) @@ -163,7 +176,7 @@ namespace API.Data .ThenInclude(c => c.Files) .ProjectTo(_mapper.ConfigurationProvider) .SingleAsync(vol => vol.Id == volumeId); - + var volumeList = new List() {volume}; await AddVolumeModifiers(userId, volumeList); @@ -186,7 +199,7 @@ namespace API.Data { var series = await _context.Series.Where(s => s.Id == seriesId).SingleOrDefaultAsync(); _context.Series.Remove(series); - + return await _context.SaveChangesAsync() > 0; } @@ -212,7 +225,7 @@ namespace API.Data .Include(s => s.Volumes) .ThenInclude(v => v.Chapters) .ToListAsync(); - + IList chapterIds = new List(); foreach (var s in series) { @@ -266,7 +279,7 @@ namespace API.Data .SingleOrDefaultAsync(); } - private async Task AddVolumeModifiers(int userId, List volumes) + private async Task AddVolumeModifiers(int userId, IReadOnlyCollection volumes) { var userProgress = await _context.AppUserProgresses .Where(p => p.AppUserId == userId && volumes.Select(s => s.Id).Contains(p.VolumeId)) @@ -279,7 +292,7 @@ namespace API.Data { c.PagesRead = userProgress.Where(p => p.ChapterId == c.Id).Sum(p => p.PagesRead); } - + v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead); } } @@ -311,7 +324,7 @@ namespace API.Data return await PagedList.CreateAsync(allQuery, userParams.PageNumber, userParams.PageSize); } - + var query = _context.Series .Where(s => s.LibraryId == libraryId) .AsNoTracking() @@ -356,7 +369,7 @@ namespace API.Data { series = series.Where(s => s.AppUserId == userId && s.PagesRead > 0 - && s.PagesRead < s.Series.Pages + && s.PagesRead < s.Series.Pages && s.Series.LibraryId == libraryId); } var retSeries = await series @@ -365,7 +378,7 @@ namespace API.Data .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .ToListAsync(); - + return retSeries.DistinctBy(s => s.Name).Take(limit); } @@ -386,7 +399,7 @@ namespace API.Data .AsNoTracking() .ToListAsync(); } - + return metadataDto; } @@ -398,7 +411,7 @@ namespace API.Data .AsNoTracking() .Select(library => library.Id) .ToList(); - + var query = _context.CollectionTag .Where(s => s.Id == collectionId) .Include(c => c.SeriesMetadatas) @@ -423,4 +436,4 @@ namespace API.Data .ToListAsync(); } } -} \ No newline at end of file +} diff --git a/API/Extensions/SeriesExtensions.cs b/API/Extensions/SeriesExtensions.cs index cedeb3905..17698db39 100644 --- a/API/Extensions/SeriesExtensions.cs +++ b/API/Extensions/SeriesExtensions.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; using API.Entities; +using API.Entities.Enums; using API.Parser; -using API.Services; using API.Services.Tasks.Scanner; namespace API.Extensions diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index 9c6fccbe9..a50e6138d 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -498,6 +498,12 @@ namespace API.Parser ret.Series = CleanTitle(fileName); } + // Pdfs may have .pdf in the series name, remove that + if (IsPdf(fileName) && ret.Series.ToLower().EndsWith(".pdf")) + { + ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); + } + return ret.Series == string.Empty ? null : ret; } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 39c9e963b..174419090 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -60,8 +60,15 @@ namespace API.Services if (files.Count > 0 && files[0].Format == MangaFormat.Image) { DirectoryService.ExistOrCreate(extractPath); - var pattern = (files.Count == 1) ? (@"\" + Path.GetExtension(files[0].FilePath)) : Parser.Parser.ImageFileExtensions; - _directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, pattern); + if (files.Count == 1) + { + _directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); + } + else + { + _directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, Parser.Parser.ImageFileExtensions); + } + extractDi.Flatten(); return chapter; } diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index 3be35cd86..7b414afec 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -20,19 +20,17 @@ namespace API.Services private readonly ILogger _logger; private readonly IArchiveService _archiveService; private readonly IBookService _bookService; - private readonly IDirectoryService _directoryService; private readonly IImageService _imageService; private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer(); public static readonly int ThumbnailWidth = 320; // 153w x 230h public MetadataService(IUnitOfWork unitOfWork, ILogger logger, - IArchiveService archiveService, IBookService bookService, IDirectoryService directoryService, IImageService imageService) + IArchiveService archiveService, IBookService bookService, IImageService imageService) { _unitOfWork = unitOfWork; _logger = logger; _archiveService = archiveService; _bookService = bookService; - _directoryService = directoryService; _imageService = imageService; } @@ -71,25 +69,24 @@ namespace API.Services public void UpdateMetadata(Volume volume, bool forceUpdate) { - if (volume != null && ShouldFindCoverImage(volume.CoverImage, forceUpdate)) - { - volume.Chapters ??= new List(); - var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer).FirstOrDefault(); + if (volume == null || !ShouldFindCoverImage(volume.CoverImage, forceUpdate)) return; - // Skip calculating Cover Image (I/O) if the chapter already has it set - if (firstChapter == null || ShouldFindCoverImage(firstChapter.CoverImage)) + volume.Chapters ??= new List(); + var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer).FirstOrDefault(); + + // Skip calculating Cover Image (I/O) if the chapter already has it set + if (firstChapter == null || ShouldFindCoverImage(firstChapter.CoverImage, forceUpdate)) + { + var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault(); + if (firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified)) { - var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault(); - if (firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified)) - { - volume.CoverImage = GetCoverImage(firstFile); - } - } - else - { - volume.CoverImage = firstChapter.CoverImage; + volume.CoverImage = GetCoverImage(firstFile); } } + else + { + volume.CoverImage = firstChapter.CoverImage; + } } public void UpdateMetadata(Series series, bool forceUpdate) diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index c646d1c60..9be88dfde 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using API.Entities; using API.Entities.Enums; using API.Interfaces.Services; using API.Parser; @@ -38,6 +39,20 @@ namespace API.Services.Tasks.Scanner _scannedSeries = new ConcurrentDictionary>(); } + public static IList GetInfosByName(Dictionary> parsedSeries, Series series) + { + var existingKey = parsedSeries.Keys.FirstOrDefault(ps => + ps.Format == series.Format && ps.NormalizedName == Parser.Parser.Normalize(series.OriginalName)); + existingKey ??= new ParsedSeries() + { + Format = series.Format, + Name = series.OriginalName, + NormalizedName = Parser.Parser.Normalize(series.OriginalName) + }; + + return parsedSeries[existingKey]; + } + /// /// Processes files found during a library scan. /// Populates a collection of for DB updates later. @@ -144,7 +159,7 @@ namespace API.Services.Tasks.Scanner { var sw = Stopwatch.StartNew(); totalFiles = 0; - var searchPattern = GetLibrarySearchPattern(libraryType); + var searchPattern = GetLibrarySearchPattern(); foreach (var folderPath in folders) { try @@ -174,12 +189,7 @@ namespace API.Services.Tasks.Scanner return SeriesWithInfos(); } - /// - /// Given the Library Type, returns the regex pattern that restricts which files types will be found during a file scan. - /// - /// - /// - private static string GetLibrarySearchPattern(LibraryType libraryType) + private static string GetLibrarySearchPattern() { return Parser.Parser.SupportedExtensions; } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 763d3e712..a8caf68f9 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -27,7 +26,7 @@ namespace API.Services.Tasks private readonly IArchiveService _archiveService; private readonly IMetadataService _metadataService; private readonly IBookService _bookService; - private readonly NaturalSortComparer _naturalSort; + private readonly NaturalSortComparer _naturalSort = new (); public ScannerService(IUnitOfWork unitOfWork, ILogger logger, IArchiveService archiveService, IMetadataService metadataService, IBookService bookService) @@ -37,7 +36,6 @@ namespace API.Services.Tasks _archiveService = archiveService; _metadataService = metadataService; _bookService = bookService; - _naturalSort = new NaturalSortComparer(); } [DisableConcurrentExecution(timeoutInSeconds: 360)] @@ -200,10 +198,16 @@ namespace API.Services.Tasks _logger.LogInformation("Removed {RemoveMissingSeries} series that are no longer on disk:", removeCount); foreach (var s in missingSeries) { - _logger.LogDebug("Removed {SeriesName}", s.Name); + _logger.LogDebug("Removed {SeriesName} ({Format})", s.Name, s.Format); } } + if (library.Series.Count == 0) + { + _logger.LogDebug("Removed all Series, returning without checking reset of files scanned"); + return; + } + // Add new series that have parsedInfos foreach (var (key, infos) in parsedSeries) @@ -218,11 +222,11 @@ namespace API.Services.Tasks } catch (Exception e) { - _logger.LogCritical(e, "There are multiple series that map to normalized key {Key}. You can manually delete the entity via UI and rescan to fix it", key); + _logger.LogCritical(e, "There are multiple series that map to normalized key {Key}. You can manually delete the entity via UI and rescan to fix it", key.NormalizedName); var duplicateSeries = library.Series.Where(s => s.NormalizedName == key.NormalizedName || Parser.Parser.Normalize(s.OriginalName) == key.NormalizedName).ToList(); foreach (var series in duplicateSeries) { - _logger.LogCritical("{Key} maps with {Series}", key, series.OriginalName); + _logger.LogCritical("{Key} maps with {Series}", key.Name, series.OriginalName); } continue; @@ -247,7 +251,7 @@ namespace API.Services.Tasks try { _logger.LogInformation("Processing series {SeriesName}", series.OriginalName); - UpdateVolumes(series, GetInfosByName(parsedSeries, series).ToArray()); + UpdateVolumes(series, ParseScannedFiles.GetInfosByName(parsedSeries, series).ToArray()); series.Pages = series.Volumes.Sum(v => v.Pages); } catch (Exception ex) @@ -257,25 +261,15 @@ namespace API.Services.Tasks }); } - private static IList GetInfosByName(Dictionary> parsedSeries, Series series) - { - // TODO: Move this into a common place - var existingKey = parsedSeries.Keys.FirstOrDefault(ps => - ps.Format == series.Format && ps.NormalizedName == Parser.Parser.Normalize(series.OriginalName)); - existingKey ??= new ParsedSeries() - { - Format = series.Format, - Name = series.OriginalName, - NormalizedName = Parser.Parser.Normalize(series.OriginalName) - }; - - return parsedSeries[existingKey]; - } - public IEnumerable FindSeriesNotOnDisk(ICollection existingSeries, Dictionary> parsedSeries) { - var foundSeries = parsedSeries.Select(s => s.Key.Name).ToList(); - return existingSeries.Where(es => !es.NameInList(foundSeries)); + // It is safe to check only first since Parser ensures that a Series only has one type + var format = MangaFormat.Unknown; + var firstPs = parsedSeries.Keys.DistinctBy(ps => ps.Format).FirstOrDefault(); + if (firstPs != null) format = firstPs.Format; + + var foundSeries = parsedSeries.Select(s => s.Key.Name).ToList(); + return existingSeries.Where(es => !es.NameInList(foundSeries) || es.Format != format); } /// @@ -293,7 +287,7 @@ namespace API.Services.Tasks existingSeries = existingSeries.Where( s => !missingList.Exists( - m => m.NormalizedName.Equals(s.NormalizedName))).ToList(); + m => m.NormalizedName.Equals(s.NormalizedName) && m.Format == s.Format)).ToList(); removeCount = existingCount - existingSeries.Count; diff --git a/Kavita.Common/KavitaException.cs b/Kavita.Common/KavitaException.cs index e91525f7e..ee4efcb5a 100644 --- a/Kavita.Common/KavitaException.cs +++ b/Kavita.Common/KavitaException.cs @@ -18,4 +18,4 @@ namespace Kavita.Common } } -} \ No newline at end of file +} diff --git a/UI/Web/src/app/_guards/auth.guard.ts b/UI/Web/src/app/_guards/auth.guard.ts index bf3d5d576..c35e8ff87 100644 --- a/UI/Web/src/app/_guards/auth.guard.ts +++ b/UI/Web/src/app/_guards/auth.guard.ts @@ -21,7 +21,7 @@ export class AuthGuard implements CanActivate { } this.toastr.error('You are not authorized to view this page.'); localStorage.setItem(this.urlKey, window.location.pathname); - this.router.navigateByUrl('/home'); + this.router.navigateByUrl('/libraries'); return false; }) ); diff --git a/UI/Web/src/app/admin/dashboard/dashboard.component.ts b/UI/Web/src/app/admin/dashboard/dashboard.component.ts index 48bced7d2..756ae605a 100644 --- a/UI/Web/src/app/admin/dashboard/dashboard.component.ts +++ b/UI/Web/src/app/admin/dashboard/dashboard.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; import { ServerService } from 'src/app/_services/server.service'; import { saveAs } from 'file-saver'; +import { Title } from '@angular/platform-browser'; @@ -22,7 +23,7 @@ export class DashboardComponent implements OnInit { counter = this.tabs.length + 1; active = this.tabs[0]; - constructor(public route: ActivatedRoute, private serverService: ServerService, private toastr: ToastrService) { + constructor(public route: ActivatedRoute, private serverService: ServerService, private toastr: ToastrService, private titleService: Title) { this.route.fragment.subscribe(frag => { const tab = this.tabs.filter(item => item.fragment === frag); if (tab.length > 0) { @@ -34,7 +35,9 @@ export class DashboardComponent implements OnInit { } - ngOnInit() {} + ngOnInit() { + this.titleService.setTitle('Kavita - Admin Dashboard'); + } restartServer() { this.serverService.restart().subscribe(() => { diff --git a/UI/Web/src/app/all-collections/all-collections.component.ts b/UI/Web/src/app/all-collections/all-collections.component.ts index 6a37eb3d3..ddd4b6407 100644 --- a/UI/Web/src/app/all-collections/all-collections.component.ts +++ b/UI/Web/src/app/all-collections/all-collections.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from '@angular/core'; +import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; @@ -28,7 +29,9 @@ export class AllCollectionsComponent implements OnInit { seriesPagination!: Pagination; collectionTagActions: ActionItem[] = []; - constructor(private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService, private modalService: NgbModal) { + constructor(private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute, + private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService, + private modalService: NgbModal, private titleService: Title) { this.router.routeReuseStrategy.shouldReuseRoute = () => false; const routeId = this.route.snapshot.paramMap.get('id'); @@ -43,6 +46,7 @@ export class AllCollectionsComponent implements OnInit { return; } this.collectionTagName = tags.filter(item => item.id === this.collectionTagId)[0].title; + this.titleService.setTitle('Kavita - ' + this.collectionTagName + ' Collection'); }); } } diff --git a/UI/Web/src/app/home/home.component.ts b/UI/Web/src/app/home/home.component.ts index ff9c4f354..7c474cf82 100644 --- a/UI/Web/src/app/home/home.component.ts +++ b/UI/Web/src/app/home/home.component.ts @@ -4,6 +4,7 @@ import { Router } from '@angular/router'; import { take } from 'rxjs/operators'; import { MemberService } from '../_services/member.service'; import { AccountService } from '../_services/account.service'; +import { Title } from '@angular/platform-browser'; @Component({ selector: 'app-home', @@ -19,7 +20,7 @@ export class HomeComponent implements OnInit { password: new FormControl('', [Validators.required]) }); - constructor(public accountService: AccountService, private memberService: MemberService, private router: Router) { + constructor(public accountService: AccountService, private memberService: MemberService, private router: Router, private titleService: Title) { } ngOnInit(): void { @@ -31,6 +32,7 @@ export class HomeComponent implements OnInit { return; } + this.titleService.setTitle('Kavita'); this.accountService.currentUser$.pipe(take(1)).subscribe(user => { if (user) { this.router.navigateByUrl('/library'); diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index 616afb110..a933f38dd 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -28,7 +28,7 @@ export class LibraryDetailComponent implements OnInit { private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService, private actionService: ActionService) { const routeId = this.route.snapshot.paramMap.get('id'); if (routeId === null) { - this.router.navigateByUrl('/home'); + this.router.navigateByUrl('/libraries'); return; } this.router.routeReuseStrategy.shouldReuseRoute = () => false; diff --git a/UI/Web/src/app/library/library.component.ts b/UI/Web/src/app/library/library.component.ts index c7f852e2f..4d613b9b6 100644 --- a/UI/Web/src/app/library/library.component.ts +++ b/UI/Web/src/app/library/library.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from '@angular/core'; +import { Title } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { take } from 'rxjs/operators'; @@ -34,9 +35,13 @@ export class LibraryComponent implements OnInit { seriesTrackBy = (index: number, item: any) => `${item.name}_${item.pagesRead}`; - constructor(public accountService: AccountService, private libraryService: LibraryService, private seriesService: SeriesService, private actionFactoryService: ActionFactoryService, private collectionService: CollectionTagService, private router: Router, private modalService: NgbModal) { } + constructor(public accountService: AccountService, private libraryService: LibraryService, + private seriesService: SeriesService, private actionFactoryService: ActionFactoryService, + private collectionService: CollectionTagService, private router: Router, + private modalService: NgbModal, private titleService: Title) { } ngOnInit(): void { + this.titleService.setTitle('Kavita - Dashboard'); this.isLoading = true; this.accountService.currentUser$.pipe(take(1)).subscribe(user => { this.user = user; diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index 9009b903a..81395d811 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -245,7 +245,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { const chapterId = this.route.snapshot.paramMap.get('chapterId'); if (libraryId === null || seriesId === null || chapterId === null) { - this.router.navigateByUrl('/home'); + this.router.navigateByUrl('/libraries'); return; } diff --git a/UI/Web/src/app/nav-header/nav-header.component.ts b/UI/Web/src/app/nav-header/nav-header.component.ts index 1163d781e..0323b5bf3 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav-header/nav-header.component.ts @@ -61,7 +61,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy { logout() { this.accountService.logout(); this.navService.hideNavBar(); - this.router.navigateByUrl('/home'); + this.router.navigateByUrl('/login'); } moveFocus() { diff --git a/UI/Web/src/app/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/series-detail.component.ts index fec46f46f..4d07be6bc 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -1,14 +1,13 @@ import { Component, OnInit } from '@angular/core'; +import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; -import { forkJoin } from 'rxjs'; import { take } from 'rxjs/operators'; import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config'; import { ConfirmService } from '../shared/confirm.service'; import { CardDetailsModalComponent } from '../shared/_modals/card-details-modal/card-details-modal.component'; import { DownloadService } from '../shared/_services/download.service'; -import { NaturalSortService } from '../shared/_services/natural-sort.service'; import { UtilityService } from '../shared/_services/utility.service'; import { EditSeriesModalComponent } from '../_modals/edit-series-modal/edit-series-modal.component'; import { ReviewSeriesModalComponent } from '../_modals/review-series-modal/review-series-modal.component'; @@ -81,7 +80,7 @@ export class SeriesDetailComponent implements OnInit { public utilityService: UtilityService, private toastr: ToastrService, private accountService: AccountService, public imageService: ImageService, private actionFactoryService: ActionFactoryService, private libraryService: LibraryService, - private confirmService: ConfirmService, private naturalSort: NaturalSortService, + private confirmService: ConfirmService, private titleService: Title, private downloadService: DownloadService, private actionService: ActionService) { ratingConfig.max = 5; this.router.routeReuseStrategy.shouldReuseRoute = () => false; @@ -97,7 +96,7 @@ export class SeriesDetailComponent implements OnInit { const routeId = this.route.snapshot.paramMap.get('seriesId'); const libraryId = this.route.snapshot.paramMap.get('libraryId'); if (routeId === null || libraryId == null) { - this.router.navigateByUrl('/home'); + this.router.navigateByUrl('/libraries'); return; } @@ -227,6 +226,8 @@ export class SeriesDetailComponent implements OnInit { this.seriesService.getSeries(seriesId).subscribe(series => { this.series = series; this.createHTML(); + + this.titleService.setTitle('Kavita - ' + this.series.name + ' Details'); this.seriesService.getVolumes(this.series.id).subscribe(volumes => { @@ -240,7 +241,6 @@ export class SeriesDetailComponent implements OnInit { this.specials = vol0.map(v => v.chapters || []) .flat() .filter(c => c.isSpecial || isNaN(parseInt(c.range, 10))) - .sort((a, b) => this.naturalSort.compare(a.range, b.range, true)) .map(c => { c.title = this.utilityService.cleanSpecialTitle(c.title); c.range = this.utilityService.cleanSpecialTitle(c.range); @@ -255,6 +255,8 @@ export class SeriesDetailComponent implements OnInit { this.isLoading = false; }); + }, err => { + this.router.navigateByUrl('/libraries'); }); } diff --git a/UI/Web/src/app/shared/_modals/card-details-modal/card-details-modal.component.ts b/UI/Web/src/app/shared/_modals/card-details-modal/card-details-modal.component.ts index 4db350a9a..000b0358d 100644 --- a/UI/Web/src/app/shared/_modals/card-details-modal/card-details-modal.component.ts +++ b/UI/Web/src/app/shared/_modals/card-details-modal/card-details-modal.component.ts @@ -5,7 +5,6 @@ import { MangaFile } from 'src/app/_models/manga-file'; import { MangaFormat } from 'src/app/_models/manga-format'; import { Volume } from 'src/app/_models/volume'; import { ImageService } from 'src/app/_services/image.service'; -import { NaturalSortService } from '../../_services/natural-sort.service'; import { UtilityService } from '../../_services/utility.service'; @@ -27,7 +26,7 @@ export class CardDetailsModalComponent implements OnInit { formatKeys = Object.keys(MangaFormat); constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService, - public imageService: ImageService, public naturalSort: NaturalSortService) { } + public imageService: ImageService) { } ngOnInit(): void { this.isChapter = this.isObjectChapter(this.data); diff --git a/UI/Web/src/app/shared/_services/natural-sort.service.ts b/UI/Web/src/app/shared/_services/natural-sort.service.ts index 4c9ddd180..5f9d3b776 100644 --- a/UI/Web/src/app/shared/_services/natural-sort.service.ts +++ b/UI/Web/src/app/shared/_services/natural-sort.service.ts @@ -2,6 +2,7 @@ import { Injectable, OnDestroy } from '@angular/core'; /** * Soley repsonsible for performing a "natural" sort. This is the UI counterpart to the BE NaturalSortComparer. + * Note: This does not work the same. Better to have the Backend perform the sort before sending to UI. */ @Injectable({ providedIn: 'root', diff --git a/UI/Web/src/app/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-preferences/user-preferences.component.ts index e9c694f89..9254967d5 100644 --- a/UI/Web/src/app/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-preferences/user-preferences.component.ts @@ -8,6 +8,7 @@ import { AccountService } from '../_services/account.service'; import { Options } from '@angular-slider/ngx-slider'; import { BookService } from '../book-reader/book.service'; import { NavService } from '../_services/nav.service'; +import { Title } from '@angular/platform-browser'; @Component({ selector: 'app-user-preferences', @@ -47,11 +48,12 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { }; fontFamilies: Array = []; - constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService, private navService: NavService) { + constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService, private navService: NavService, private titleService: Title) { this.fontFamilies = this.bookService.getFontFamilies(); } ngOnInit(): void { + this.titleService.setTitle('Kavita - User Preferences'); this.accountService.currentUser$.pipe(take(1)).subscribe((user: User) => { if (user) { this.user = user;