From 5ed1eebd26787164582b5f51101238e6a882863f Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Tue, 21 Nov 2023 13:20:36 -0600 Subject: [PATCH] Performance Improvements (#2449) --- API/DTOs/Search/BookmarkSearchResultDto.cs | 11 ++ API/DTOs/Search/SearchResultGroupDto.cs | 2 + API/DTOs/UpdateSeriesMetadataDto.cs | 1 + .../Repositories/CollectionTagRepository.cs | 12 ++ API/Data/Repositories/GenreRepository.cs | 8 + API/Data/Repositories/PersonRepository.cs | 8 + API/Data/Repositories/SeriesRepository.cs | 28 +++ API/Data/Repositories/TagRepository.cs | 8 + API/Helpers/PersonHelper.cs | 15 ++ API/I18N/en.json | 8 +- API/Services/MetadataService.cs | 2 +- API/Services/SeriesService.cs | 178 +++++++++++------- API/Services/Tasks/ScannerService.cs | 4 +- .../_models/search/bookmark-search-result.ts | 8 + .../app/_models/search/search-result-group.ts | 3 + UI/Web/src/app/_services/action.service.ts | 16 +- .../review-card/review-card.component.ts | 2 +- .../manage-library.component.ts | 4 +- .../manage-media-settings.component.ts | 2 +- UI/Web/src/app/app.component.ts | 6 +- .../book-reader/book-reader.component.html | 14 +- .../manga-reader/manga-reader.component.html | 48 +++-- .../manga-reader/manga-reader.component.scss | 28 +-- .../manga-reader/manga-reader.component.ts | 17 +- .../grouped-typeahead.component.html | 10 + .../grouped-typeahead.component.ts | 3 +- .../nav-header/nav-header.component.html | 17 ++ .../nav-header/nav-header.component.ts | 10 + .../reading-lists/reading-lists.component.ts | 2 +- .../src/app/shared/image/image.component.html | 1 + .../src/app/shared/image/image.component.ts | 2 +- UI/Web/src/assets/langs/en.json | 4 +- identifier.sqlite | 0 openapi.json | 39 +++- 34 files changed, 389 insertions(+), 132 deletions(-) create mode 100644 API/DTOs/Search/BookmarkSearchResultDto.cs create mode 100644 UI/Web/src/app/_models/search/bookmark-search-result.ts delete mode 100644 identifier.sqlite diff --git a/API/DTOs/Search/BookmarkSearchResultDto.cs b/API/DTOs/Search/BookmarkSearchResultDto.cs new file mode 100644 index 000000000..5d53add1f --- /dev/null +++ b/API/DTOs/Search/BookmarkSearchResultDto.cs @@ -0,0 +1,11 @@ +namespace API.DTOs.Search; + +public class BookmarkSearchResultDto +{ + public int LibraryId { get; set; } + public int VolumeId { get; set; } + public int SeriesId { get; set; } + public int ChapterId { get; set; } + public string SeriesName { get; set; } + public string LocalizedSeriesName { get; set; } +} diff --git a/API/DTOs/Search/SearchResultGroupDto.cs b/API/DTOs/Search/SearchResultGroupDto.cs index 66370fb0a..eb47579f1 100644 --- a/API/DTOs/Search/SearchResultGroupDto.cs +++ b/API/DTOs/Search/SearchResultGroupDto.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using API.DTOs.CollectionTags; using API.DTOs.Metadata; +using API.DTOs.Reader; using API.DTOs.ReadingLists; namespace API.DTOs.Search; @@ -19,6 +20,7 @@ public class SearchResultGroupDto public IEnumerable Tags { get; set; } = default!; public IEnumerable Files { get; set; } = default!; public IEnumerable Chapters { get; set; } = default!; + public IEnumerable Bookmarks { get; set; } = default!; } diff --git a/API/DTOs/UpdateSeriesMetadataDto.cs b/API/DTOs/UpdateSeriesMetadataDto.cs index cdd6c7502..43318fe0f 100644 --- a/API/DTOs/UpdateSeriesMetadataDto.cs +++ b/API/DTOs/UpdateSeriesMetadataDto.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using API.DTOs.CollectionTags; namespace API.DTOs; diff --git a/API/Data/Repositories/CollectionTagRepository.cs b/API/Data/Repositories/CollectionTagRepository.cs index 4e35ef613..a7c942734 100644 --- a/API/Data/Repositories/CollectionTagRepository.cs +++ b/API/Data/Repositories/CollectionTagRepository.cs @@ -33,6 +33,9 @@ public interface ICollectionTagRepository void Update(CollectionTag tag); Task RemoveTagsWithoutSeries(); Task> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None); + + Task> GetAllTagsByNamesAsync(IEnumerable normalizedTitles, + CollectionTagIncludes includes = CollectionTagIncludes.None); Task> GetAllCoverImagesAsync(); Task TagExists(string title); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); @@ -87,6 +90,15 @@ public class CollectionTagRepository : ICollectionTagRepository .ToListAsync(); } + public async Task> GetAllTagsByNamesAsync(IEnumerable normalizedTitles, CollectionTagIncludes includes = CollectionTagIncludes.None) + { + return await _context.CollectionTag + .Where(c => normalizedTitles.Contains(c.NormalizedTitle)) + .OrderBy(c => c.NormalizedTitle) + .Includes(includes) + .ToListAsync(); + } + public async Task GetCoverImageAsync(int collectionTagId) { return await _context.CollectionTag diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index b552093e7..dedafeb9a 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -17,6 +17,7 @@ public interface IGenreRepository void Remove(Genre genre); Task FindByNameAsync(string genreName); Task> GetAllGenresAsync(); + Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames); Task> GetAllGenreDtosAsync(int userId); Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); Task> GetAllGenreDtosForLibrariesAsync(IList libraryIds, int userId); @@ -96,6 +97,13 @@ public class GenreRepository : IGenreRepository return await _context.Genre.ToListAsync(); } + public async Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames) + { + return await _context.Genre + .Where(g => normalizedNames.Contains(g.NormalizedTitle)) + .ToListAsync(); + } + public async Task> GetAllGenreDtosAsync(int userId) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index 146479740..1c9e71e1e 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -23,6 +23,7 @@ public interface IPersonRepository Task> GetAllPeopleDtosForLibrariesAsync(List libraryIds, int userId); Task GetCountAsync(); + Task> GetAllPeopleByRoleAndNames(PersonRole role, IEnumerable normalizeNames); } public class PersonRepository : IPersonRepository @@ -80,6 +81,13 @@ public class PersonRepository : IPersonRepository return await _context.Person.CountAsync(); } + public async Task> GetAllPeopleByRoleAndNames(PersonRole role, IEnumerable normalizeNames) + { + return await _context.Person + .Where(p => p.Role == role && normalizeNames.Contains(p.NormalizedName)) + .ToListAsync(); + } + public async Task> GetAllPeople() { diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 298d63610..5d2dc30b7 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -13,6 +13,7 @@ using API.DTOs.Dashboard; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.DTOs.Metadata; +using API.DTOs.Reader; using API.DTOs.ReadingLists; using API.DTOs.Search; using API.DTOs.SeriesDetail; @@ -374,6 +375,33 @@ public class SeriesRepository : ISeriesRepository .ProjectTo(_mapper.ConfigurationProvider) .AsEnumerable(); + result.Bookmarks = (await _context.AppUserBookmark + .Join( + _context.Series, + bookmark => bookmark.SeriesId, + series => series.Id, + (bookmark, series) => new {Bookmark = bookmark, Series = series} + ) + .Where(joined => joined.Bookmark.AppUserId == userId && + (EF.Functions.Like(joined.Series.Name, $"%{searchQuery}%") || + (joined.Series.OriginalName != null && + EF.Functions.Like(joined.Series.OriginalName, $"%{searchQuery}%")) || + (joined.Series.LocalizedName != null && + EF.Functions.Like(joined.Series.LocalizedName, $"%{searchQuery}%")))) + .OrderBy(joined => joined.Series.Name) + .Take(maxRecords) + .Select(joined => new BookmarkSearchResultDto() + { + SeriesName = joined.Series.Name, + LocalizedSeriesName = joined.Series.LocalizedName, + LibraryId = joined.Series.LibraryId, + SeriesId = joined.Bookmark.SeriesId, + ChapterId = joined.Bookmark.ChapterId, + VolumeId = joined.Bookmark.VolumeId + }) + .ToListAsync()).DistinctBy(s => s.SeriesId); + + result.ReadingLists = await _context.ReadingList .Where(rl => rl.AppUserId == userId || rl.Promoted) .Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%")) diff --git a/API/Data/Repositories/TagRepository.cs b/API/Data/Repositories/TagRepository.cs index 7544694ea..4423c09e3 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/API/Data/Repositories/TagRepository.cs @@ -16,6 +16,7 @@ public interface ITagRepository void Attach(Tag tag); void Remove(Tag tag); Task> GetAllTagsAsync(); + Task> GetAllTagsByNameAsync(IEnumerable normalizedNames); Task> GetAllTagDtosAsync(int userId); Task RemoveAllTagNoLongerAssociated(); Task> GetAllTagDtosForLibrariesAsync(IList libraryIds, int userId); @@ -76,6 +77,13 @@ public class TagRepository : ITagRepository return await _context.Tag.ToListAsync(); } + public async Task> GetAllTagsByNameAsync(IEnumerable normalizedNames) + { + return await _context.Tag + .Where(t => normalizedNames.Contains(t.NormalizedTitle)) + .ToListAsync(); + } + public async Task> GetAllTagDtosAsync(int userId) { var userRating = await _context.AppUser.GetUserAgeRestriction(userId); diff --git a/API/Helpers/PersonHelper.cs b/API/Helpers/PersonHelper.cs index aa8e7bcd3..fcfb1f984 100644 --- a/API/Helpers/PersonHelper.cs +++ b/API/Helpers/PersonHelper.cs @@ -164,4 +164,19 @@ public static class PersonHelper onModified(); } } + + public static bool HasAnyPeople(SeriesMetadataDto? seriesMetadata) + { + if (seriesMetadata == null) return false; + return seriesMetadata.Writers.Any() || + seriesMetadata.CoverArtists.Any() || + seriesMetadata.Publishers.Any() || + seriesMetadata.Characters.Any() || + seriesMetadata.Pencillers.Any() || + seriesMetadata.Inkers.Any() || + seriesMetadata.Colorists.Any() || + seriesMetadata.Letterers.Any() || + seriesMetadata.Editors.Any() || + seriesMetadata.Translators.Any(); + } } diff --git a/API/I18N/en.json b/API/I18N/en.json index e8235d3d5..0648b04df 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -189,10 +189,10 @@ "user-no-access-library-from-series": "User does not have access to the library this series belongs to", "series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions", - "next-volume-num": "Next Volume: {0}", - "next-book-num": "Next Book: {0}", - "next-issue-num": "Next Issue: {0}{1}", - "next-chapter-num": "Next Chapter: {0}", + "next-volume-num": "Upcoming Volume: {0}", + "next-book-num": "Upcoming Book: {0}", + "next-issue-num": "Upcoming Issue: {0}{1}", + "next-chapter-num": "Upcoming Chapter: {0}", "volume-num": "Volume {0}", diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index eba7977e8..3a65f4e57 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -130,7 +130,7 @@ public class MetadataService : IMetadataService return Task.CompletedTask; series.Volumes ??= new List(); - series.CoverImage = series.GetCoverImage(); + series.CoverImage = series.GetCoverImage(); // BUG: At this point the volume or chapter hasn't regenerated the cover _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); return Task.CompletedTask; diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index af763d5c9..acc41e870 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -17,13 +17,14 @@ using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services.Plus; +using API.Services.Tasks.Scanner.Parser; using API.SignalR; using Hangfire; using Kavita.Common; using Microsoft.Extensions.Logging; namespace API.Services; - +#nullable enable public interface ISeriesService { @@ -51,7 +52,7 @@ public class SeriesService : ISeriesService private readonly IScrobblingService _scrobblingService; private readonly ILocalizationService _localizationService; - private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto() + private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto { ExpectedDate = null, ChapterNumber = 0, @@ -106,11 +107,6 @@ public class SeriesService : ISeriesService var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId; var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); if (series == null) return false; - var allCollectionTags = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).ToList(); - // TODO: This is Diesel's performance problem with Komf. For some systems, this is too heavy of a call if komf is spamming updates. - var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresAsync()).ToList(); - var allPeople = (await _unitOfWork.PersonRepository.GetAllPeople()).ToList(); - var allTags = (await _unitOfWork.TagRepository.GetAllTagsAsync()).ToList(); series.Metadata ??= new SeriesMetadataBuilder() .WithCollectionTags(updateSeriesMetadataDto.CollectionTags.Select(dto => @@ -169,51 +165,102 @@ public class SeriesService : ISeriesService } - series.Metadata.CollectionTags ??= new List(); - UpdateCollectionsList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, (tag) => + if (updateSeriesMetadataDto.CollectionTags.Any()) { - series.Metadata.CollectionTags.Add(tag); - }); - - series.Metadata.Genres ??= new List(); - GenreHelper.UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, (genre) => - { - series.Metadata.Genres.Add(genre); - }, () => series.Metadata.GenresLocked = true); - - series.Metadata.Tags ??= new List(); - TagHelper.UpdateTagList(updateSeriesMetadataDto.SeriesMetadata?.Tags, series, allTags, (tag) => - { - series.Metadata.Tags.Add(tag); - }, () => series.Metadata.TagsLocked = true); - - void HandleAddPerson(Person person) - { - PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); - allPeople.Add(person); + var allCollectionTags = (await _unitOfWork.CollectionTagRepository + .GetAllTagsByNamesAsync(updateSeriesMetadataDto.CollectionTags.Select(t => Parser.Normalize(t.Title)))).ToList(); + series.Metadata.CollectionTags ??= new List(); + UpdateCollectionsList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, tag => + { + series.Metadata.CollectionTags.Add(tag); + }); + } + + + if (updateSeriesMetadataDto.SeriesMetadata?.Genres != null && + updateSeriesMetadataDto.SeriesMetadata.Genres.Any()) + { + var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresByNamesAsync(updateSeriesMetadataDto.SeriesMetadata.Genres.Select(t => Parser.Normalize(t.Title)))).ToList(); + series.Metadata.Genres ??= new List(); + GenreHelper.UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, genre => + { + series.Metadata.Genres.Add(genre); + }, () => series.Metadata.GenresLocked = true); + } + + + if (updateSeriesMetadataDto.SeriesMetadata?.Tags != null && updateSeriesMetadataDto.SeriesMetadata.Tags.Any()) + { + var allTags = (await _unitOfWork.TagRepository + .GetAllTagsByNameAsync(updateSeriesMetadataDto.SeriesMetadata.Tags.Select(t => Parser.Normalize(t.Title)))) + .ToList(); + series.Metadata.Tags ??= new List(); + TagHelper.UpdateTagList(updateSeriesMetadataDto.SeriesMetadata?.Tags, series, allTags, tag => + { + series.Metadata.Tags.Add(tag); + }, () => series.Metadata.TagsLocked = true); + } + + + if (PersonHelper.HasAnyPeople(updateSeriesMetadataDto.SeriesMetadata)) + { + void HandleAddPerson(Person person) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + + series.Metadata.People ??= new List(); + var allWriters = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Writer, + updateSeriesMetadataDto.SeriesMetadata!.Writers.Select(p => Parser.Normalize(p.Name))); + PersonHelper.UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata!.Writers, series, allWriters.AsReadOnly(), + HandleAddPerson, () => series.Metadata.WriterLocked = true); + + var allCharacters = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Character, + updateSeriesMetadataDto.SeriesMetadata!.Characters.Select(p => Parser.Normalize(p.Name))); + PersonHelper.UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allCharacters.AsReadOnly(), + HandleAddPerson, () => series.Metadata.CharacterLocked = true); + + var allColorists = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Colorist, + updateSeriesMetadataDto.SeriesMetadata!.Colorists.Select(p => Parser.Normalize(p.Name))); + PersonHelper.UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allColorists.AsReadOnly(), + HandleAddPerson, () => series.Metadata.ColoristLocked = true); + + var allEditors = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Editor, + updateSeriesMetadataDto.SeriesMetadata!.Editors.Select(p => Parser.Normalize(p.Name))); + PersonHelper.UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allEditors.AsReadOnly(), + HandleAddPerson, () => series.Metadata.EditorLocked = true); + + var allInkers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Inker, + updateSeriesMetadataDto.SeriesMetadata!.Inkers.Select(p => Parser.Normalize(p.Name))); + PersonHelper.UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allInkers.AsReadOnly(), + HandleAddPerson, () => series.Metadata.InkerLocked = true); + + var allLetterers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Letterer, + updateSeriesMetadataDto.SeriesMetadata!.Letterers.Select(p => Parser.Normalize(p.Name))); + PersonHelper.UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allLetterers.AsReadOnly(), + HandleAddPerson, () => series.Metadata.LettererLocked = true); + + var allPencillers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Penciller, + updateSeriesMetadataDto.SeriesMetadata!.Pencillers.Select(p => Parser.Normalize(p.Name))); + PersonHelper.UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPencillers.AsReadOnly(), + HandleAddPerson, () => series.Metadata.PencillerLocked = true); + + var allPublishers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Publisher, + updateSeriesMetadataDto.SeriesMetadata!.Publishers.Select(p => Parser.Normalize(p.Name))); + PersonHelper.UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPublishers.AsReadOnly(), + HandleAddPerson, () => series.Metadata.PublisherLocked = true); + + var allTranslators = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Translator, + updateSeriesMetadataDto.SeriesMetadata!.Translators.Select(p => Parser.Normalize(p.Name))); + PersonHelper.UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allTranslators.AsReadOnly(), + HandleAddPerson, () => series.Metadata.TranslatorLocked = true); + + var allCoverArtists = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.CoverArtist, + updateSeriesMetadataDto.SeriesMetadata!.CoverArtists.Select(p => Parser.Normalize(p.Name))); + PersonHelper.UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allCoverArtists.AsReadOnly(), + HandleAddPerson, () => series.Metadata.CoverArtistLocked = true); } - series.Metadata.People ??= new List(); - PersonHelper.UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata!.Writers, series, allPeople, - HandleAddPerson, () => series.Metadata.WriterLocked = true); - PersonHelper.UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allPeople, - HandleAddPerson, () => series.Metadata.CharacterLocked = true); - PersonHelper.UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allPeople, - HandleAddPerson, () => series.Metadata.ColoristLocked = true); - PersonHelper.UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allPeople, - HandleAddPerson, () => series.Metadata.EditorLocked = true); - PersonHelper.UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allPeople, - HandleAddPerson, () => series.Metadata.InkerLocked = true); - PersonHelper.UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allPeople, - HandleAddPerson, () => series.Metadata.LettererLocked = true); - PersonHelper.UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPeople, - HandleAddPerson, () => series.Metadata.PencillerLocked = true); - PersonHelper.UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPeople, - HandleAddPerson, () => series.Metadata.PublisherLocked = true); - PersonHelper.UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allPeople, - HandleAddPerson, () => series.Metadata.TranslatorLocked = true); - PersonHelper.UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allPeople, - HandleAddPerson, () => series.Metadata.CoverArtistLocked = true); series.Metadata.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked; series.Metadata.PublicationStatusLocked = updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked; @@ -270,7 +317,7 @@ public class SeriesService : ISeriesService } - public static void UpdateCollectionsList(ICollection? tags, Series series, IReadOnlyCollection allTags, + private static void UpdateCollectionsList(ICollection? tags, Series series, IReadOnlyCollection allTags, Action handleAdd) { // TODO: Move UpdateCollectionsList to a helper so we can easily test @@ -430,7 +477,7 @@ public class SeriesService : ISeriesService var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)) - .OrderBy(v => Tasks.Scanner.Parser.Parser.MinNumberFromRange(v.Name)) + .OrderBy(v => Parser.MinNumberFromRange(v.Name)) .ToList(); // For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number. @@ -499,7 +546,7 @@ public class SeriesService : ISeriesService retChapters = retChapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default); } - return new SeriesDetailDto() + return new SeriesDetailDto { Specials = specials, Chapters = retChapters, @@ -517,7 +564,7 @@ public class SeriesService : ISeriesService /// private static bool ShouldIncludeChapter(ChapterDto chapter) { - return !chapter.IsSpecial && !chapter.Number.Equals(Tasks.Scanner.Parser.Parser.DefaultChapter); + return !chapter.IsSpecial && !chapter.Number.Equals(Parser.DefaultChapter); } public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume") @@ -526,7 +573,7 @@ public class SeriesService : ISeriesService { if (string.IsNullOrEmpty(firstChapter.TitleName)) { - if (firstChapter.Range.Equals(Tasks.Scanner.Parser.Parser.DefaultVolume)) return; + if (firstChapter.Range.Equals(Parser.DefaultVolume)) return; var title = Path.GetFileNameWithoutExtension(firstChapter.Range); if (string.IsNullOrEmpty(title)) return; volume.Name += $" - {title}"; @@ -553,7 +600,7 @@ public class SeriesService : ISeriesService if (isSpecial) { - return Tasks.Scanner.Parser.Parser.CleanSpecialTitle(chapterTitle); + return Parser.CleanSpecialTitle(chapterTitle); } var hashSpot = withHash ? "#" : string.Empty; @@ -650,7 +697,7 @@ public class SeriesService : ISeriesService r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) != null) continue; - series.Relations.Add(new SeriesRelation() + series.Relations.Add(new SeriesRelation { Series = series, SeriesId = series.Id, @@ -669,7 +716,7 @@ public class SeriesService : ISeriesService { throw new UnauthorizedAccessException("user-no-access-library-from-series"); } - if (series?.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book) + if (series.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book) { return _emptyExpectedChapter; } @@ -732,7 +779,7 @@ public class SeriesService : ISeriesService var lastVolumeNum = chapters.Select(c => c.Volume.Number).Max(); - var result = new NextExpectedChapterDto() + var result = new NextExpectedChapterDto { ChapterNumber = 0, VolumeNumber = 0, @@ -746,21 +793,16 @@ public class SeriesService : ISeriesService result.VolumeNumber = lastChapter.Volume.Number; result.Title = series.Library.Type switch { - LibraryType.Manga => await _localizationService.Translate(userId, "next-chapter-num", - new object[] {result.ChapterNumber}), - LibraryType.Comic => await _localizationService.Translate(userId, "next-issue-num", - new object[] {"#", result.ChapterNumber}), - LibraryType.Book => await _localizationService.Translate(userId, "next-book-num", - new object[] {result.ChapterNumber}), - _ => await _localizationService.Translate(userId, "next-chapter-num", - new object[] {result.ChapterNumber}) + LibraryType.Manga => await _localizationService.Translate(userId, "next-chapter-num", result.ChapterNumber), + LibraryType.Comic => await _localizationService.Translate(userId, "next-issue-num", "#", result.ChapterNumber), + LibraryType.Book => await _localizationService.Translate(userId, "next-book-num", result.ChapterNumber), + _ => await _localizationService.Translate(userId, "next-chapter-num", result.ChapterNumber) }; } else { result.VolumeNumber = lastVolumeNum + 1; - result.Title = await _localizationService.Translate(userId, "volume-num", - new object[] {result.VolumeNumber}); + result.Title = await _localizationService.Translate(userId, "volume-num", result.VolumeNumber); } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index c69ead9c8..c59cb4cef 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -296,8 +296,8 @@ public class ScannerService : IScannerService MessageFactory.ScanSeriesEvent(library.Id, seriesId, series.Name)); await _metadataService.RemoveAbandonedMetadataKeys(); - BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(series.LibraryId, seriesId, false)); - BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, false)); + //BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(series.LibraryId, seriesId, false)); + //BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, false)); BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory)); return; diff --git a/UI/Web/src/app/_models/search/bookmark-search-result.ts b/UI/Web/src/app/_models/search/bookmark-search-result.ts new file mode 100644 index 000000000..726772f73 --- /dev/null +++ b/UI/Web/src/app/_models/search/bookmark-search-result.ts @@ -0,0 +1,8 @@ +export interface BookmarkSearchResult { + libraryId: number; + seriesId: number; + volumeId: number; + chapterId: number; + seriesName: string; + localizedSeriesName: string; +} 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 0e5b389f2..a96670aaf 100644 --- a/UI/Web/src/app/_models/search/search-result-group.ts +++ b/UI/Web/src/app/_models/search/search-result-group.ts @@ -3,6 +3,7 @@ import { Library } from "../library/library"; import { MangaFile } from "../manga-file"; import { SearchResult } from "./search-result"; import { Tag } from "../tag"; +import {BookmarkSearchResult} from "./bookmark-search-result"; export class SearchResultGroup { libraries: Array = []; @@ -14,6 +15,7 @@ export class SearchResultGroup { tags: Array = []; files: Array = []; chapters: Array = []; + bookmarks: Array = []; reset() { this.libraries = []; @@ -25,5 +27,6 @@ export class SearchResultGroup { this.tags = []; this.files = []; this.chapters = []; + this.bookmarks = []; } } diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 580744f43..fa80a98c8 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -102,7 +102,7 @@ export class ActionService implements OnDestroy { } editLibrary(library: Partial, callback?: LibraryActionCallback) { - const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' }); + const modalRef = this.modalService.open(LibrarySettingsModalComponent, {size: 'xl', fullscreen: 'md'}); modalRef.componentInstance.library = library; modalRef.closed.subscribe((closeResult: {success: boolean, library: Library, coverImageUpdate: boolean}) => { if (callback) callback(library) @@ -362,7 +362,7 @@ export class ActionService implements OnDestroy { addMultipleToReadingList(seriesId: number, volumes: Array, chapters?: Array, callback?: BooleanActionCallback) { if (this.readingListModalRef != null) { return; } - this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); + this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); this.readingListModalRef.componentInstance.seriesId = seriesId; this.readingListModalRef.componentInstance.volumeIds = volumes.map(v => v.id); this.readingListModalRef.componentInstance.chapterIds = chapters?.map(c => c.id); @@ -404,7 +404,7 @@ export class ActionService implements OnDestroy { addMultipleSeriesToReadingList(series: Array, callback?: BooleanActionCallback) { if (this.readingListModalRef != null) { return; } - this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); + this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); this.readingListModalRef.componentInstance.seriesIds = series.map(v => v.id); this.readingListModalRef.componentInstance.title = 'Multiple Selections'; this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple_Series; @@ -432,7 +432,7 @@ export class ActionService implements OnDestroy { */ addMultipleSeriesToCollectionTag(series: Array, callback?: BooleanActionCallback) { if (this.collectionModalRef != null) { return; } - this.collectionModalRef = this.modalService.open(BulkAddToCollectionComponent, { scrollable: true, size: 'md', windowClass: 'collection' }); + this.collectionModalRef = this.modalService.open(BulkAddToCollectionComponent, { scrollable: true, size: 'md', windowClass: 'collection', fullscreen: 'md' }); this.collectionModalRef.componentInstance.seriesIds = series.map(v => v.id); this.collectionModalRef.componentInstance.title = 'New Collection'; @@ -452,7 +452,7 @@ export class ActionService implements OnDestroy { addSeriesToReadingList(series: Series, callback?: SeriesActionCallback) { if (this.readingListModalRef != null) { return; } - this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); + this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); this.readingListModalRef.componentInstance.seriesId = series.id; this.readingListModalRef.componentInstance.title = series.name; this.readingListModalRef.componentInstance.type = ADD_FLOW.Series; @@ -474,7 +474,7 @@ export class ActionService implements OnDestroy { addVolumeToReadingList(volume: Volume, seriesId: number, callback?: VolumeActionCallback) { if (this.readingListModalRef != null) { return; } - this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); + this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); this.readingListModalRef.componentInstance.seriesId = seriesId; this.readingListModalRef.componentInstance.volumeId = volume.id; this.readingListModalRef.componentInstance.type = ADD_FLOW.Volume; @@ -496,7 +496,7 @@ export class ActionService implements OnDestroy { addChapterToReadingList(chapter: Chapter, seriesId: number, callback?: ChapterActionCallback) { if (this.readingListModalRef != null) { return; } - this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); + this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); this.readingListModalRef.componentInstance.seriesId = seriesId; this.readingListModalRef.componentInstance.chapterId = chapter.id; this.readingListModalRef.componentInstance.type = ADD_FLOW.Chapter; @@ -517,7 +517,7 @@ export class ActionService implements OnDestroy { } editReadingList(readingList: ReadingList, callback?: ReadingListActionCallback) { - const readingListModalRef = this.modalService.open(EditReadingListModalComponent, { scrollable: true, size: 'lg' }); + const readingListModalRef = this.modalService.open(EditReadingListModalComponent, { scrollable: true, size: 'lg', fullscreen: 'md' }); readingListModalRef.componentInstance.readingList = readingList; readingListModalRef.closed.pipe(take(1)).subscribe((list) => { if (callback && list !== undefined) { diff --git a/UI/Web/src/app/_single-module/review-card/review-card.component.ts b/UI/Web/src/app/_single-module/review-card/review-card.component.ts index 2f0d419a9..1d3816326 100644 --- a/UI/Web/src/app/_single-module/review-card/review-card.component.ts +++ b/UI/Web/src/app/_single-module/review-card/review-card.component.ts @@ -43,7 +43,7 @@ export class ReviewCardComponent implements OnInit { } else { component = ReviewCardModalComponent; } - const ref = this.modalService.open(component, {size: "lg"}); + const ref = this.modalService.open(component, {size: 'lg', fullscreen: 'md'}); ref.componentInstance.review = this.review; } diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.ts b/UI/Web/src/app/admin/manage-library/manage-library.component.ts index e31dbb0ff..bf48db286 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.ts +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.ts @@ -99,7 +99,7 @@ export class ManageLibraryComponent implements OnInit { } editLibrary(library: Library) { - const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' }); + const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl', fullscreen: 'md' }); modalRef.componentInstance.library = library; modalRef.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(refresh => { if (refresh) { @@ -109,7 +109,7 @@ export class ManageLibraryComponent implements OnInit { } addLibrary() { - const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' }); + const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl', fullscreen: 'md' }); modalRef.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(refresh => { if (refresh) { this.getLibraries(); diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts index f1de0ed37..e5f7f131d 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts @@ -96,7 +96,7 @@ export class ManageMediaSettingsComponent implements OnInit { } openDirectoryChooser(existingDirectory: string, formControl: string) { - const modalRef = this.modalService.open(DirectoryPickerComponent, { scrollable: true, size: 'lg' }); + const modalRef = this.modalService.open(DirectoryPickerComponent, { scrollable: true, size: 'lg', fullscreen: 'md' }); modalRef.componentInstance.startingFolder = existingDirectory || ''; modalRef.componentInstance.helpUrl = ''; modalRef.closed.subscribe((closeResult: DirectoryPickerResult) => { diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index f93ea8416..351e2ff10 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -5,7 +5,7 @@ import { AccountService } from './_services/account.service'; import { LibraryService } from './_services/library.service'; import { NavService } from './_services/nav.service'; import { filter } from 'rxjs/operators'; -import {NgbModal, NgbOffcanvas, NgbRatingConfig} from '@ng-bootstrap/ng-bootstrap'; +import {NgbModal, NgbModalConfig, NgbOffcanvas, NgbRatingConfig} from '@ng-bootstrap/ng-bootstrap'; import { DOCUMENT, NgClass, NgIf, AsyncPipe } from '@angular/common'; import { Observable } from 'rxjs'; import {ThemeService} from "./_services/theme.service"; @@ -32,7 +32,9 @@ export class AppComponent implements OnInit { constructor(private accountService: AccountService, private libraryService: LibraryService, private router: Router, private ngbModal: NgbModal, ratingConfig: NgbRatingConfig, - @Inject(DOCUMENT) private document: Document, private themeService: ThemeService) { + @Inject(DOCUMENT) private document: Document, private themeService: ThemeService, private modalConfig: NgbModalConfig) { + + modalConfig.fullscreen = 'md'; // Setup default rating config ratingConfig.max = 5; diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html index 78a988091..be5101d5a 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html @@ -13,10 +13,16 @@ (refreshToC)="refreshPersonalToC()"> -
- {{t('title')}} -
+
+
{{t('title')}}
+ {{t('close-reader')}} +
+ + + + +
@@ -148,7 +154,7 @@ ({{t('incognito-mode-label')}}) - {{bookTitle}} + {{bookTitle}}
diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html index dbfe89871..5dd6da6fb 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html @@ -1,5 +1,14 @@
+ @if(debugMode) { +
+ @for(img of cachedImages; track img.src) { + + {{this.readerService.imageUrlToPageNum(img.src)}} + + } +
+ }
- {{generalSettingsForm.get('darkness')?.value + '%'}} + + {{generalSettingsForm.get('darkness')?.value + '%'}}
diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.scss b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.scss index c224a4359..1f4a6391d 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.scss +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.scss @@ -6,7 +6,13 @@ $pointer-offset: 5px; @use '../../.././../manga-reader-common'; +.current { + background-color: var(--primary-color); +} +.loaded { + font-weight: bold; +} .reading-area { @@ -52,7 +58,7 @@ $pointer-offset: 5px; .overlay .right .i { right: 20px; } -} +} // Splitting Icon .split { @@ -76,7 +82,7 @@ $pointer-offset: 5px; background-color: rgba(255, 255, 255, 0.6); margin-top: -16px; } - + /* Control the right side */ .right-side { height: 20px; @@ -112,7 +118,7 @@ $pointer-offset: 5px; .custom-slider .ngx-slider .ngx-slider-selection { background: var(--primary-color); } - + .custom-slider .ngx-slider .ngx-slider-pointer { width: 8px; height: 16px; @@ -122,23 +128,23 @@ $pointer-offset: 5px; border-top-left-radius: 3px; border-top-right-radius: 3px; } - + .custom-slider .ngx-slider .ngx-slider-pointer:after { display: none; } - + .custom-slider .ngx-slider .ngx-slider-bubble { bottom: 14px; font-weight: bold; } - + .custom-slider .ngx-slider .ngx-slider-limit { font-weight: bold; color: white !important; } - + .custom-slider .ngx-slider .ngx-slider-tick { width: 1px; height: 10px; @@ -147,7 +153,7 @@ $pointer-offset: 5px; background: #ffe4d1; top: -1px; } - + .custom-slider .ngx-slider .ngx-slider-tick.ngx-slider-selected { background: var(--primary-color); } @@ -179,7 +185,7 @@ $pointer-offset: 5px; max-height: calc(var(--vh)*100); z-index: 100; } - + .top { position: absolute; right: 0px; @@ -188,7 +194,7 @@ $pointer-offset: 5px; background: $pagination-bg; z-index: 100; } - + .left { position: absolute; left: 0px; @@ -198,7 +204,7 @@ $pointer-offset: 5px; max-height: calc(var(--vh)*100); z-index: 100; } - + .bottom { position: fixed; // I couldn't figure out how to do this with abs, so only the bottom bar will not be scrollable left: 0px; diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index 69b9d81e3..0315848de 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -13,7 +13,7 @@ import { OnInit, ViewChild } from '@angular/core'; -import {DOCUMENT, NgStyle, NgIf, NgFor, NgSwitch, NgSwitchCase, PercentPipe} from '@angular/common'; +import {DOCUMENT, NgStyle, NgIf, NgFor, NgSwitch, NgSwitchCase, PercentPipe, NgClass} from '@angular/common'; import {ActivatedRoute, Router} from '@angular/router'; import { BehaviorSubject, @@ -124,7 +124,7 @@ enum KeyDirection { imports: [NgStyle, NgIf, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent, DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent, NgxSliderModule, ReactiveFormsModule, NgFor, NgSwitch, NgSwitchCase, FittingIconPipe, ReaderModeIconPipe, - FullscreenIconPipe, TranslocoDirective, NgbProgressbar, PercentPipe] + FullscreenIconPipe, TranslocoDirective, NgbProgressbar, PercentPipe, NgClass] }) export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { @@ -392,6 +392,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { */ hasHitBottomTopScroll: boolean = false; + /** + * Show and log debug information + */ + debugMode: boolean = false; + // Renderer interaction readerSettings$!: Observable; private currentImage: Subject = new ReplaySubject(1); @@ -405,7 +410,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return this.readerService.getPageUrl(chapterId, pageNum); } - get CurrentPageBookmarked() { return this.bookmarks.hasOwnProperty(this.pageNum); } @@ -479,7 +483,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } this.getPageFn = this.getPage.bind(this); - this.readerService.enableWakeLock(this.reader.nativeElement); this.libraryId = parseInt(libraryId, 10); this.seriesId = parseInt(seriesId, 10); @@ -864,6 +867,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.render(); }); + setTimeout(() => { + this.readerService.enableWakeLock(this.reader.nativeElement); + }, 1000); return; } @@ -1342,6 +1348,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (cachedImagePageNum !== numOffset) { this.cachedImages[index] = new Image(); this.cachedImages[index].src = this.getPageUrl(numOffset); + this.cachedImages[index].onload = (evt) => { + this.cdRef.markForCheck(); + } } } diff --git a/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html b/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html index 8a7847511..f55ce8f77 100644 --- a/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html +++ b/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html @@ -45,6 +45,16 @@ + +
  • {{t('bookmarks')}}
  • +
      +
    • + +
    • +
    +
    +
  • {{t('libraries')}}
    • diff --git a/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.ts b/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.ts index 41ba22114..4fef13241 100644 --- a/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.ts +++ b/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.ts @@ -80,6 +80,7 @@ export class GroupedTypeaheadComponent implements OnInit { @ContentChild('readingListTemplate') readingListTemplate!: TemplateRef; @ContentChild('fileTemplate') fileTemplate!: TemplateRef; @ContentChild('chapterTemplate') chapterTemplate!: TemplateRef; + @ContentChild('bookmarkTemplate') bookmarkTemplate!: TemplateRef; private readonly destroyRef = inject(DestroyRef); @@ -96,7 +97,7 @@ export class GroupedTypeaheadComponent implements OnInit { get hasData() { return !(this.noResultsTemplate != undefined && !this.groupedData.persons.length && !this.groupedData.collections.length && !this.groupedData.series.length && !this.groupedData.persons.length && !this.groupedData.tags.length && !this.groupedData.genres.length && !this.groupedData.libraries.length - && !this.groupedData.files.length && !this.groupedData.chapters.length); + && !this.groupedData.files.length && !this.groupedData.chapters.length && !this.groupedData.bookmarks.length); } diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html index a271c3ee8..eecd4c809 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html @@ -51,6 +51,23 @@
    + +
    +
    + +
    +
    + + + {{item.seriesName}} + + + + +
    +
    +
    +
    diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts index 1ff9bfbab..7661c2c22 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts @@ -39,6 +39,7 @@ import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities import {FilterStatement} from "../../../_models/metadata/v2/filter-statement"; import {FilterField} from "../../../_models/metadata/v2/filter-field"; import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; +import {BookmarkSearchResult} from "../../../_models/search/bookmark-search-result"; @Component({ selector: 'app-nav-header', @@ -197,6 +198,15 @@ export class NavHeaderComponent implements OnInit { this.router.navigate(['library', libraryId, 'series', seriesId]); } + clickBookmarkSearchResult(item: BookmarkSearchResult) { + this.clearSearch(); + const libraryId = item.libraryId; + const seriesId = item.seriesId; + this.router.navigate(['library', libraryId, 'series', seriesId, 'manga', item.chapterId], {queryParams: { + incognitoMode: false, bookmarkMode: true + }}); + } + clickFileSearchResult(item: MangaFile) { this.clearSearch(); this.searchService.getSeriesForMangaFile(item.id).subscribe(series => { diff --git a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts index 3bd0b8bad..58d91edf9 100644 --- a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts @@ -73,7 +73,7 @@ export class ReadingListsComponent implements OnInit { } importCbl() { - const ref = this.ngbModal.open(ImportCblModalComponent, {size: 'xl'}); + const ref = this.ngbModal.open(ImportCblModalComponent, {size: 'xl', fullscreen: 'md'}); ref.closed.subscribe(result => this.loadPage()); ref.dismissed.subscribe(_ => this.loadPage()); } diff --git a/UI/Web/src/app/shared/image/image.component.html b/UI/Web/src/app/shared/image/image.component.html index 5c103e8bc..aef9819b9 100644 --- a/UI/Web/src/app/shared/image/image.component.html +++ b/UI/Web/src/app/shared/image/image.component.html @@ -3,4 +3,5 @@ alt="" aria-hidden="true" [lazyLoad]="imageUrl" + [errorImage]="errorImage" (onStateChange)="myCallbackFunction($event)"> diff --git a/UI/Web/src/app/shared/image/image.component.ts b/UI/Web/src/app/shared/image/image.component.ts index d2a0545a9..bfa45fbb2 100644 --- a/UI/Web/src/app/shared/image/image.component.ts +++ b/UI/Web/src/app/shared/image/image.component.ts @@ -172,7 +172,7 @@ export class ImageComponent implements OnChanges { case 'loading-failed': // The image could not be loaded for some reason. // `event.data` is the error in this case - image.src = this.errorImage; + this.renderer.removeClass(image, 'fade-in'); this.cdRef.markForCheck(); break; case 'finally': diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 16b27ff73..0db260715 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -618,11 +618,12 @@ "settings-header": "Settings", "table-of-contents-header": "Table of Contents", - "bookmarks-header": "Bookmarks", + "bookmarks-header": "{{side-nav.bookmarks}}", "toc-header": "ToC", "loading-book": "Loading book…", "go-back": "Go Back", + "close-reader": "Close Reader", "incognito-mode-alt": "Incognito mode is on. Toggle to turn off.", "incognito-mode-label": "Incognito Mode", "next": "Next", @@ -1409,6 +1410,7 @@ "people": "People", "tags": "Tags", "genres": "Genres", + "bookmarks": "{{side-nav.bookmarks}}", "libraries": "Libraries", "reading-lists": "Reading Lists", "collections": "Collections", diff --git a/identifier.sqlite b/identifier.sqlite deleted file mode 100644 index e69de29bb..000000000 diff --git a/openapi.json b/openapi.json index 38ec0a061..1b6ef9983 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.10.15" + "version": "0.7.10.16" }, "servers": [ { @@ -13437,6 +13437,36 @@ }, "additionalProperties": false }, + "BookmarkSearchResultDto": { + "type": "object", + "properties": { + "libraryId": { + "type": "integer", + "format": "int32" + }, + "volumeId": { + "type": "integer", + "format": "int32" + }, + "seriesId": { + "type": "integer", + "format": "int32" + }, + "chapterId": { + "type": "integer", + "format": "int32" + }, + "seriesName": { + "type": "string", + "nullable": true + }, + "localizedSeriesName": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "BulkRemoveBookmarkForSeriesDto": { "type": "object", "properties": { @@ -17188,6 +17218,13 @@ "$ref": "#/components/schemas/ChapterDto" }, "nullable": true + }, + "bookmarks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BookmarkSearchResultDto" + }, + "nullable": true } }, "additionalProperties": false,