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;