diff --git a/API.Benchmark/ParseScannedFilesBenchmarks.cs b/API.Benchmark/ParseScannedFilesBenchmarks.cs index 7c244a5d4..1dcca79b9 100644 --- a/API.Benchmark/ParseScannedFilesBenchmarks.cs +++ b/API.Benchmark/ParseScannedFilesBenchmarks.cs @@ -1,5 +1,6 @@ using System.IO; using System.IO.Abstractions; +using System.Threading.Tasks; using API.Entities.Enums; using API.Parser; using API.Services; @@ -46,7 +47,7 @@ namespace API.Benchmark /// Generate a list of Series and another list with /// [Benchmark] - public void MergeName() + public async Task MergeName() { var libraryPath = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Manga"); @@ -61,7 +62,7 @@ namespace API.Benchmark Title = "A Town Where You Live", Volumes = "1" }; - _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new [] {libraryPath}, "Manga"); + await _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new [] {libraryPath}, "Manga"); _parseScannedFiles.MergeName(p1); } } diff --git a/API.Tests/Parser/DefaultParserTests.cs b/API.Tests/Parser/DefaultParserTests.cs index 200a6b16a..f32838dd3 100644 --- a/API.Tests/Parser/DefaultParserTests.cs +++ b/API.Tests/Parser/DefaultParserTests.cs @@ -122,7 +122,7 @@ public class DefaultParserTests filepath = @"E:\Manga\Tenjo Tenge (Color)\Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz"; expected.Add(filepath, new ParserInfo { - Series = "Tenjo Tenge", Volumes = "1", Edition = "Full Contact Edition", + Series = "Tenjo Tenge {Full Contact Edition}", Volumes = "1", Edition = "", Chapters = "0", Filename = "Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", Format = MangaFormat.Archive, FullFilePath = filepath }); diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index 78934be7d..a3d298e82 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -170,6 +170,7 @@ namespace API.Tests.Parser [InlineData("It's Witching Time! 001 (Digital) (Anonymous1234)", "It's Witching Time!")] [InlineData("Zettai Karen Children v02 c003 - The Invisible Guardian (2) [JS Scans]", "Zettai Karen Children")] [InlineData("My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras", "My Charms Are Wasted on Kuroiwa Medaka")] + [InlineData("Highschool of the Dead - Full Color Edition v02 [Uasaha] (Yen Press)", "Highschool of the Dead - Full Color Edition")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename)); @@ -253,13 +254,13 @@ namespace API.Tests.Parser [Theory] [InlineData("Tenjou Tenge Omnibus", "Omnibus")] - [InlineData("Tenjou Tenge {Full Contact Edition}", "Full Contact Edition")] - [InlineData("Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", "Full Contact Edition")] + [InlineData("Tenjou Tenge {Full Contact Edition}", "")] + [InlineData("Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", "")] [InlineData("Wotakoi - Love is Hard for Otaku Omnibus v01 (2018) (Digital) (danke-Empire)", "Omnibus")] [InlineData("To Love Ru v01 Uncensored (Ch.001-007)", "Uncensored")] [InlineData("Chobits Omnibus Edition v01 [Dark Horse]", "Omnibus Edition")] [InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "")] - [InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "Full Color")] + [InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "")] [InlineData("Love Hina Omnibus v05 (2015) (Digital-HD) (Asgard-Empire).cbz", "Omnibus")] public void ParseEditionTest(string input, string expected) { diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs index d64e71cea..d5dd233de 100644 --- a/API.Tests/Parser/ParserTest.cs +++ b/API.Tests/Parser/ParserTest.cs @@ -63,6 +63,7 @@ namespace API.Tests.Parser [InlineData("- The Title", false, "The Title")] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", false, "Kasumi Otoko no Ko v1.1")] [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 04 (2019) (digital) (Son of Ultron-Empire)", true, "Batman - Detective Comics - Rebirth Deluxe Edition")] + [InlineData("Something - Full Color Edition", false, "Something - Full Color Edition")] public void CleanTitleTest(string input, bool isComic, string expected) { Assert.Equal(expected, CleanTitle(input, isComic)); diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index 2cd7da805..08d5f29a7 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -364,7 +364,7 @@ public class CleanupServiceTests #region CleanupBackups [Fact] - public void CleanupBackups_LeaveOneFile_SinceAllAreExpired() + public async Task CleanupBackups_LeaveOneFile_SinceAllAreExpired() { var filesystem = CreateFileSystem(); var filesystemFile = new MockFileData("") @@ -378,12 +378,12 @@ public class CleanupServiceTests var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); - cleanupService.CleanupBackups(); + await cleanupService.CleanupBackups(); Assert.Single(ds.GetFiles(BackupDirectory, searchOption: SearchOption.AllDirectories)); } [Fact] - public void CleanupBackups_LeaveLestExpired() + public async Task CleanupBackups_LeaveLestExpired() { var filesystem = CreateFileSystem(); var filesystemFile = new MockFileData("") @@ -400,7 +400,7 @@ public class CleanupServiceTests var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); - cleanupService.CleanupBackups(); + await cleanupService.CleanupBackups(); Assert.True(filesystem.File.Exists($"{BackupDirectory}randomfile.zip")); } diff --git a/API/Controllers/RecommendedController.cs b/API/Controllers/RecommendedController.cs index 85842692a..acd200b97 100644 --- a/API/Controllers/RecommendedController.cs +++ b/API/Controllers/RecommendedController.cs @@ -30,6 +30,7 @@ public class RecommendedController : BaseApiController userParams ??= new UserParams(); var series = await _unitOfWork.SeriesRepository.GetQuickReads(user.Id, libraryId, userParams); + Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); } @@ -46,6 +47,7 @@ public class RecommendedController : BaseApiController userParams ??= new UserParams(); var series = await _unitOfWork.SeriesRepository.GetHighlyRated(user.Id, libraryId, userParams); + await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); } @@ -62,6 +64,8 @@ public class RecommendedController : BaseApiController userParams ??= new UserParams(); var series = await _unitOfWork.SeriesRepository.GetMoreIn(user.Id, libraryId, genreId, userParams); + await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series); + Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); } diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index a473e1fd7..39fd10938 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -342,6 +342,32 @@ namespace API.Controllers return await _seriesService.GetSeriesDetail(seriesId, userId); } + /// + /// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI), + /// then null is returned + /// + /// + /// + [HttpGet("series-for-mangafile")] + public async Task> GetSeriesForMangaFile(int mangaFileId) + { + var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId)); + } + + /// + /// Returns the series for the Chapter id. If the user does not have access (shouldn't happen by the UI), + /// then null is returned + /// + /// + /// + [HttpGet("series-for-chapter")] + public async Task> GetSeriesForChapter(int chapterId) + { + var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId)); + } + /// /// Fetches the related series for a given series /// diff --git a/API/DTOs/MangaFileDto.cs b/API/DTOs/MangaFileDto.cs index 786f85df7..c1220e28d 100644 --- a/API/DTOs/MangaFileDto.cs +++ b/API/DTOs/MangaFileDto.cs @@ -4,9 +4,10 @@ namespace API.DTOs { public class MangaFileDto { + public int Id { get; init; } public string FilePath { get; init; } public int Pages { get; init; } public MangaFormat Format { get; init; } - + } -} \ No newline at end of file +} diff --git a/API/DTOs/Search/SearchResultGroupDto.cs b/API/DTOs/Search/SearchResultGroupDto.cs index b21209dca..0a1fac402 100644 --- a/API/DTOs/Search/SearchResultGroupDto.cs +++ b/API/DTOs/Search/SearchResultGroupDto.cs @@ -17,5 +17,8 @@ public class SearchResultGroupDto public IEnumerable Persons { get; set; } public IEnumerable Genres { get; set; } public IEnumerable Tags { get; set; } + public IEnumerable Files { get; set; } + public IEnumerable Chapters { get; set; } + } diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index b0090f3c2..15384b8ed 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -21,7 +21,9 @@ using API.Services.Tasks; using AutoMapper; using AutoMapper.QueryableExtensions; using Kavita.Common.Extensions; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using SQLitePCL; namespace API.Data.Repositories; @@ -115,6 +117,8 @@ public interface ISeriesRepository Task> GetHighlyRated(int userId, int libraryId, UserParams userParams); Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams); Task> GetRediscover(int userId, int libraryId, UserParams userParams); + Task GetSeriesForMangaFile(int mangaFileId, int userId); + Task GetSeriesForChapter(int chapterId, int userId); } public class SeriesRepository : ISeriesRepository @@ -287,7 +291,7 @@ public class SeriesRepository : ISeriesRepository public async Task SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery) { - + const int maxRecords = 15; var result = new SearchResultGroupDto(); var searchQueryNormalized = Parser.Parser.Normalize(searchQuery); @@ -301,6 +305,7 @@ public class SeriesRepository : ISeriesRepository .Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%")) .OrderBy(l => l.Name) .AsSplitQuery() + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -308,7 +313,7 @@ public class SeriesRepository : ISeriesRepository var hasYearInQuery = !string.IsNullOrEmpty(justYear); var yearComparison = hasYearInQuery ? int.Parse(justYear) : 0; - result.Series = await _context.Series + result.Series = _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%") || EF.Functions.Like(s.OriginalName, $"%{searchQuery}%") @@ -319,14 +324,16 @@ public class SeriesRepository : ISeriesRepository .OrderBy(s => s.SortName) .AsNoTracking() .AsSplitQuery() + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .AsEnumerable(); result.ReadingLists = await _context.ReadingList .Where(rl => rl.AppUserId == userId || rl.Promoted) .Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%")) .AsSplitQuery() + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -336,6 +343,8 @@ public class SeriesRepository : ISeriesRepository .Where(s => s.Promoted || isAdmin) .OrderBy(s => s.Title) .AsNoTracking() + .AsSplitQuery() + .Take(maxRecords) .OrderBy(c => c.NormalizedTitle) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -344,6 +353,7 @@ public class SeriesRepository : ISeriesRepository .Where(sm => seriesIds.Contains(sm.SeriesId)) .SelectMany(sm => sm.People.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%"))) .AsSplitQuery() + .Take(maxRecords) .Distinct() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -354,6 +364,7 @@ public class SeriesRepository : ISeriesRepository .AsSplitQuery() .OrderBy(t => t.Title) .Distinct() + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -363,9 +374,34 @@ public class SeriesRepository : ISeriesRepository .AsSplitQuery() .OrderBy(t => t.Title) .Distinct() + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); + var fileIds = _context.Series + .Where(s => libraryIds.Contains(s.LibraryId)) + .AsSplitQuery() + .SelectMany(s => s.Volumes) + .SelectMany(v => v.Chapters) + .SelectMany(c => c.Files.Select(f => f.Id)); + + result.Files = await _context.MangaFile + .Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id)) + .AsSplitQuery() + .Take(maxRecords) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + + + result.Chapters = await _context.Chapter + .Include(c => c.Files) + .Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%")) + .Where(c => c.Files.All(f => fileIds.Contains(f.Id))) + .AsSplitQuery() + .Take(maxRecords) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + return result; } @@ -1044,6 +1080,33 @@ public class SeriesRepository : ISeriesRepository return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } + public async Task GetSeriesForMangaFile(int mangaFileId, int userId) + { + var libraryIds = GetLibraryIdsForUser(userId); + return await _context.MangaFile + .Where(m => m.Id == mangaFileId) + .AsSplitQuery() + .Select(f => f.Chapter) + .Select(c => c.Volume) + .Select(v => v.Series) + .Where(s => libraryIds.Contains(s.LibraryId)) + .ProjectTo(_mapper.ConfigurationProvider) + .SingleOrDefaultAsync(); + } + + public async Task GetSeriesForChapter(int chapterId, int userId) + { + var libraryIds = GetLibraryIdsForUser(userId); + return await _context.Chapter + .Where(m => m.Id == chapterId) + .AsSplitQuery() + .Select(c => c.Volume) + .Select(v => v.Series) + .Where(s => libraryIds.Contains(s.LibraryId)) + .ProjectTo(_mapper.ConfigurationProvider) + .SingleOrDefaultAsync(); + } + public async Task> GetHighlyRated(int userId, int libraryId, UserParams userParams) { diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index 57fe51526..67b97f9c2 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -452,10 +452,6 @@ namespace API.Parser }; private static readonly Regex[] MangaEditionRegex = { // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz - new Regex( - @"(?({|\(|\[).* Edition(}|\)|\]))", - MatchOptions, RegexTimeout), - // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz new Regex( @"(\b|_)(?Omnibus(( |_)?Edition)?)(\b|_)?", MatchOptions, RegexTimeout), @@ -463,10 +459,6 @@ namespace API.Parser new Regex( @"(\b|_)(?Uncensored)(\b|_)", MatchOptions, RegexTimeout), - // AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz - new Regex( - @"(\b|_)(?Full(?: |_)Color)(\b|_)?", - MatchOptions, RegexTimeout), }; private static readonly Regex[] CleanupRegex = diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index fd29ea07d..4320c9d0c 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; +using System.Text; using System.Threading.Tasks; using System.Xml.Serialization; using API.Archive; diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index e2106049f..92c0d6e1d 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -133,7 +133,14 @@ namespace API.Services.Tasks.Scanner } } - TrackSeries(info); + try + { + TrackSeries(info); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an exception that occurred during tracking {FilePath}. Skipping this file", info.FullFilePath); + } } @@ -183,8 +190,9 @@ namespace API.Services.Tasks.Scanner { var normalizedSeries = Parser.Parser.Normalize(info.Series); var normalizedLocalSeries = Parser.Parser.Normalize(info.LocalizedSeries); + // We use FirstOrDefault because this was introduced late in development and users might have 2 series with both names var existingName = - _scannedSeries.SingleOrDefault(p => + _scannedSeries.FirstOrDefault(p => (Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries || Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) && p.Key.Format == info.Format) .Key; diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 9b17dfaa2..a5c0f0d5d 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -195,7 +195,7 @@ public class ScannerService : IScannerService // Check if any of the folder roots are not available (ie disconnected from network, etc) and fail if any of them are if (folders.Any(f => !_directoryService.IsDriveMounted(f))) { - _logger.LogError("Some of the root folders for library ({LibraryName} are not accessible. Please check that drives are connected and rescan. Scan will be aborted", libraryName); + _logger.LogCritical("Some of the root folders for library ({LibraryName} are not accessible. Please check that drives are connected and rescan. Scan will be aborted", libraryName); await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent("Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted", @@ -267,6 +267,7 @@ public class ScannerService : IScannerService if (!await CheckMounts(library.Name, library.Folders.Select(f => f.Path).ToList())) { _logger.LogCritical("Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted"); + return; } diff --git a/UI/Web/src/app/_models/manga-file.ts b/UI/Web/src/app/_models/manga-file.ts index 159656807..5ae56ae2b 100644 --- a/UI/Web/src/app/_models/manga-file.ts +++ b/UI/Web/src/app/_models/manga-file.ts @@ -1,6 +1,7 @@ import { MangaFormat } from './manga-format'; export interface MangaFile { + id: number; filePath: string; pages: number; format: MangaFormat; diff --git a/UI/Web/src/app/_models/search/search-result-group.ts b/UI/Web/src/app/_models/search/search-result-group.ts index 377593669..901e63548 100644 --- a/UI/Web/src/app/_models/search/search-result-group.ts +++ b/UI/Web/src/app/_models/search/search-result-group.ts @@ -1,4 +1,6 @@ +import { Chapter } from "../chapter"; import { Library } from "../library"; +import { MangaFile } from "../manga-file"; import { SearchResult } from "../search-result"; import { Tag } from "../tag"; @@ -10,6 +12,8 @@ export class SearchResultGroup { persons: Array = []; genres: Array = []; tags: Array = []; + files: Array = []; + chapters: Array = []; reset() { this.libraries = []; @@ -19,5 +23,7 @@ export class SearchResultGroup { this.persons = []; this.genres = []; this.tags = []; + this.files = []; + this.chapters = []; } } \ No newline at end of file diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index 878dba34b..f99410894 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -1,17 +1,15 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import { of } from 'rxjs'; import { map } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { UtilityService } from '../shared/_services/utility.service'; -import { TypeaheadSettings } from '../typeahead/typeahead-settings'; -import { ChapterMetadata } from '../_models/chapter-metadata'; import { Genre } from '../_models/genre'; import { AgeRating } from '../_models/metadata/age-rating'; import { AgeRatingDto } from '../_models/metadata/age-rating-dto'; import { Language } from '../_models/metadata/language'; import { PublicationStatusDto } from '../_models/metadata/publication-status-dto'; -import { Person, PersonRole } from '../_models/person'; +import { Person } from '../_models/person'; import { Tag } from '../_models/tag'; @Injectable({ diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 6250a4f99..e62ec97a9 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -76,8 +76,12 @@ export class SeriesService { return this.httpClient.get(this.baseUrl + 'series/chapter-metadata?chapterId=' + chapterId); } - getData(id: number) { - return of(id); + getSeriesForMangaFile(mangaFileId: number) { + return this.httpClient.get(this.baseUrl + 'series/series-for-mangafile?mangaFileId=' + mangaFileId); + } + + getSeriesForChapter(chapterId: number) { + return this.httpClient.get(this.baseUrl + 'series/series-for-chapter?chapterId=' + chapterId); } delete(seriesId: number) { diff --git a/UI/Web/src/app/admin/invite-user/invite-user.component.html b/UI/Web/src/app/admin/invite-user/invite-user.component.html index 495b3f6e9..2010ee353 100644 --- a/UI/Web/src/app/admin/invite-user/invite-user.component.html +++ b/UI/Web/src/app/admin/invite-user/invite-user.component.html @@ -6,7 +6,8 @@ diff --git a/UI/Web/src/app/app-routing.module.ts b/UI/Web/src/app/app-routing.module.ts index 381c183ca..f3461be1d 100644 --- a/UI/Web/src/app/app-routing.module.ts +++ b/UI/Web/src/app/app-routing.module.ts @@ -77,7 +77,7 @@ const routes: Routes = [ loadChildren: () => import('../app/dev-only/dev-only.module').then(m => m.DevOnlyModule) }, {path: 'login', loadChildren: () => import('../app/registration/registration.module').then(m => m.RegistrationModule)}, - {path: '**', loadChildren: () => import('../app/dashboard/dashboard.module').then(m => m.DashboardModule), pathMatch: 'full'}, + {path: '**', pathMatch: 'full', redirectTo: 'libraries'}, ]; @NgModule({ diff --git a/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.html b/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.html index 10c0e85b2..1fb9f4573 100644 --- a/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.html +++ b/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.html @@ -1,4 +1,7 @@ + + + diff --git a/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.ts b/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.ts index 56e95a428..61576f72a 100644 --- a/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.ts +++ b/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.ts @@ -45,6 +45,8 @@ export class LibraryRecommendedComponent implements OnInit { this.genre$.subscribe(genre => { this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id).pipe(map(p => p.result), shareReplay()); }); + + } diff --git a/UI/Web/src/app/nav/events-widget/events-widget.component.html b/UI/Web/src/app/nav/events-widget/events-widget.component.html index 119b5c32f..447b9a3e9 100644 --- a/UI/Web/src/app/nav/events-widget/events-widget.component.html +++ b/UI/Web/src/app/nav/events-widget/events-widget.component.html @@ -48,7 +48,7 @@
  • {{message.title}}
    -
    {{message.subTitle}}
    +
    {{message.subTitle}}
    diff --git a/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.html b/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.html index 30ec66ce2..5944a5ced 100644 --- a/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.html +++ b/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.html @@ -85,8 +85,27 @@ - + +
  • Chapters
  • +
      +
    • + +
    • +
    +
    + + +
  • Files
  • +
      +
    • + +
    • +
    +
    + +
    • diff --git a/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.scss b/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.scss index fbbf72206..684f24954 100644 --- a/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.scss +++ b/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.scss @@ -68,7 +68,7 @@ form { } &.focused { - width: 100%; + width: 99%; border-color: var(--input-focused-border-color); } diff --git a/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.ts b/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.ts index 7c986de57..90d07f555 100644 --- a/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.ts +++ b/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.ts @@ -59,6 +59,8 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy { @ContentChild('noResultsTemplate') noResultsTemplate!: TemplateRef; @ContentChild('libraryTemplate') libraryTemplate!: TemplateRef; @ContentChild('readingListTemplate') readingListTemplate!: TemplateRef; + @ContentChild('fileTemplate') fileTemplate!: TemplateRef; + @ContentChild('chapterTemplate') chapterTemplate!: TemplateRef; hasFocus: boolean = false; @@ -74,7 +76,11 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy { } get hasData() { - return this.grouppedData.persons.length || this.grouppedData.collections.length || this.grouppedData.series.length || this.grouppedData.persons.length || this.grouppedData.tags.length || this.grouppedData.genres.length; + return !(this.noResultsTemplate != undefined && !this.grouppedData.persons.length && !this.grouppedData.collections.length + && !this.grouppedData.series.length && !this.grouppedData.persons.length && !this.grouppedData.tags.length && !this.grouppedData.genres.length && !this.grouppedData.libraries.length + && !this.grouppedData.files.length && !this.grouppedData.chapters.length); + + //return this.grouppedData.persons.length || this.grouppedData.collections.length || this.grouppedData.series.length || this.grouppedData.persons.length || this.grouppedData.tags.length || this.grouppedData.genres.length; } diff --git a/UI/Web/src/app/nav/nav-header/nav-header.component.html b/UI/Web/src/app/nav/nav-header/nav-header.component.html index 187b59643..e5c64466a 100644 --- a/UI/Web/src/app/nav/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav/nav-header/nav-header.component.html @@ -39,7 +39,7 @@ -
      in {{item.libraryName}}
      +
      in {{item.libraryName}}
      @@ -84,7 +84,7 @@
      -
      {{item.role | personRole}}
      +
      {{item.role | personRole}}
      @@ -97,6 +97,28 @@ + + + +
      +
      + + + + {{item.titleName}} +
      +
      +
      + + +
      +
      + + {{item.filePath}} +
      +
      +
      + No results found diff --git a/UI/Web/src/app/nav/nav-header/nav-header.component.ts b/UI/Web/src/app/nav/nav-header/nav-header.component.ts index ed86bdf01..6d8f35b7e 100644 --- a/UI/Web/src/app/nav/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav/nav-header/nav-header.component.ts @@ -3,7 +3,10 @@ import { Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@ import { Router } from '@angular/router'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { Chapter } from 'src/app/_models/chapter'; +import { MangaFile } from 'src/app/_models/manga-file'; import { ScrollService } from 'src/app/_services/scroll.service'; +import { SeriesService } from 'src/app/_services/series.service'; import { FilterQueryParam } from '../../shared/_services/filter-utilities.service'; import { CollectionTag } from '../../_models/collection-tag'; import { Library } from '../../_models/library'; @@ -48,7 +51,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy { constructor(public accountService: AccountService, private router: Router, public navService: NavService, private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document, - private scrollService: ScrollService) { } + private scrollService: ScrollService, private seriesService: SeriesService) { } ngOnInit(): void {} @@ -154,6 +157,24 @@ export class NavHeaderComponent implements OnInit, OnDestroy { this.router.navigate(['library', libraryId, 'series', seriesId]); } + clickFileSearchResult(item: MangaFile) { + this.clearSearch(); + this.seriesService.getSeriesForMangaFile(item.id).subscribe(series => { + if (series !== undefined && series !== null) { + this.router.navigate(['library', series.libraryId, 'series', series.id]); + } + }) + } + + clickChapterSearchResult(item: Chapter) { + this.clearSearch(); + this.seriesService.getSeriesForChapter(item.id).subscribe(series => { + if (series !== undefined && series !== null) { + this.router.navigate(['library', series.libraryId, 'series', series.id]); + } + }) + } + clickLibraryResult(item: Library) { this.router.navigate(['library', item.id]); } diff --git a/UI/Web/src/app/nav/nav.module.ts b/UI/Web/src/app/nav/nav.module.ts index 5e2053164..97e77b9f3 100644 --- a/UI/Web/src/app/nav/nav.module.ts +++ b/UI/Web/src/app/nav/nav.module.ts @@ -8,6 +8,7 @@ import { SharedModule } from '../shared/shared.module'; import { PipeModule } from '../pipe/pipe.module'; import { TypeaheadModule } from '../typeahead/typeahead.module'; import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; @@ -20,6 +21,7 @@ import { ReactiveFormsModule } from '@angular/forms'; imports: [ CommonModule, ReactiveFormsModule, + RouterModule, NgbDropdownModule, NgbPopoverModule, diff --git a/UI/Web/src/app/typeahead/typeahead.component.ts b/UI/Web/src/app/typeahead/typeahead.component.ts index a46201f28..4233f80c6 100644 --- a/UI/Web/src/app/typeahead/typeahead.component.ts +++ b/UI/Web/src/app/typeahead/typeahead.component.ts @@ -2,7 +2,7 @@ import { DOCUMENT } from '@angular/common'; import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, RendererStyleFlags2, TemplateRef, ViewChild } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { Observable, of, ReplaySubject, Subject } from 'rxjs'; -import { debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators'; +import { auditTime, distinctUntilChanged, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators'; import { KEY_CODES } from '../shared/_services/utility.service'; import { SelectionCompareFn, TypeaheadSettings } from './typeahead-settings'; @@ -206,26 +206,30 @@ export class TypeaheadComponent implements OnInit, OnDestroy { 'typeahead': this.typeaheadControl }); + this.filteredOptions = this.typeaheadForm.get('typeahead')!.valueChanges .pipe( // Adjust input box to grow tap(val => { if (this.inputElem != null && this.inputElem.nativeElement != null) { - this.renderer2.setStyle(this.inputElem.nativeElement, 'width', 15 * ((this.typeaheadControl.value + '').length + 1) + 'px'); + this.renderer2.setStyle(this.inputElem.nativeElement, 'width', 15 * (val.trim().length + 1) + 'px'); this.focusedIndex = 0; } }), - debounceTime(this.settings.debounce), + map(val => val.trim()), + auditTime(this.settings.debounce), + distinctUntilChanged(), filter(val => { // If minimum filter characters not met, do not filter if (this.settings.minCharacters === 0) return true; - if (!val || val.trim().length < this.settings.minCharacters) { + if (!val || val.length < this.settings.minCharacters) { return false; } return true; }), + switchMap(val => { this.isLoadingOptions = true; let results: Observable; @@ -241,12 +245,12 @@ export class TypeaheadComponent implements OnInit, OnDestroy { tap((filteredOptions) => { this.isLoadingOptions = false; this.focusedIndex = 0; - //this.updateShowAddItem(filteredOptions); setTimeout(() => { this.updateShowAddItem(filteredOptions); this.updateHighlight(); }, 10); setTimeout(() => this.updateHighlight(), 20); + }), shareReplay(), takeUntil(this.onDestroy)