Performance Improvements (#2449)

This commit is contained in:
Joe Milazzo 2023-11-21 13:20:36 -06:00 committed by GitHub
parent 419a827d42
commit 5ed1eebd26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 389 additions and 132 deletions

View File

@ -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; }
}

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using API.DTOs.CollectionTags; using API.DTOs.CollectionTags;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Reader;
using API.DTOs.ReadingLists; using API.DTOs.ReadingLists;
namespace API.DTOs.Search; namespace API.DTOs.Search;
@ -19,6 +20,7 @@ public class SearchResultGroupDto
public IEnumerable<TagDto> Tags { get; set; } = default!; public IEnumerable<TagDto> Tags { get; set; } = default!;
public IEnumerable<MangaFileDto> Files { get; set; } = default!; public IEnumerable<MangaFileDto> Files { get; set; } = default!;
public IEnumerable<ChapterDto> Chapters { get; set; } = default!; public IEnumerable<ChapterDto> Chapters { get; set; } = default!;
public IEnumerable<BookmarkSearchResultDto> Bookmarks { get; set; } = default!;
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using API.DTOs.CollectionTags; using API.DTOs.CollectionTags;
namespace API.DTOs; namespace API.DTOs;

View File

@ -33,6 +33,9 @@ public interface ICollectionTagRepository
void Update(CollectionTag tag); void Update(CollectionTag tag);
Task<int> RemoveTagsWithoutSeries(); Task<int> RemoveTagsWithoutSeries();
Task<IEnumerable<CollectionTag>> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None); Task<IEnumerable<CollectionTag>> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None);
Task<IEnumerable<CollectionTag>> GetAllTagsByNamesAsync(IEnumerable<string> normalizedTitles,
CollectionTagIncludes includes = CollectionTagIncludes.None);
Task<IList<string>> GetAllCoverImagesAsync(); Task<IList<string>> GetAllCoverImagesAsync();
Task<bool> TagExists(string title); Task<bool> TagExists(string title);
Task<IList<CollectionTag>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task<IList<CollectionTag>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
@ -87,6 +90,15 @@ public class CollectionTagRepository : ICollectionTagRepository
.ToListAsync(); .ToListAsync();
} }
public async Task<IEnumerable<CollectionTag>> GetAllTagsByNamesAsync(IEnumerable<string> 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<string?> GetCoverImageAsync(int collectionTagId) public async Task<string?> GetCoverImageAsync(int collectionTagId)
{ {
return await _context.CollectionTag return await _context.CollectionTag

View File

@ -17,6 +17,7 @@ public interface IGenreRepository
void Remove(Genre genre); void Remove(Genre genre);
Task<Genre?> FindByNameAsync(string genreName); Task<Genre?> FindByNameAsync(string genreName);
Task<IList<Genre>> GetAllGenresAsync(); Task<IList<Genre>> GetAllGenresAsync();
Task<IList<Genre>> GetAllGenresByNamesAsync(IEnumerable<string> normalizedNames);
Task<IList<GenreTagDto>> GetAllGenreDtosAsync(int userId); Task<IList<GenreTagDto>> GetAllGenreDtosAsync(int userId);
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds, int userId); Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds, int userId);
@ -96,6 +97,13 @@ public class GenreRepository : IGenreRepository
return await _context.Genre.ToListAsync(); return await _context.Genre.ToListAsync();
} }
public async Task<IList<Genre>> GetAllGenresByNamesAsync(IEnumerable<string> normalizedNames)
{
return await _context.Genre
.Where(g => normalizedNames.Contains(g.NormalizedTitle))
.ToListAsync();
}
public async Task<IList<GenreTagDto>> GetAllGenreDtosAsync(int userId) public async Task<IList<GenreTagDto>> GetAllGenreDtosAsync(int userId)
{ {
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);

View File

@ -23,6 +23,7 @@ public interface IPersonRepository
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds, int userId); Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds, int userId);
Task<int> GetCountAsync(); Task<int> GetCountAsync();
Task<IList<Person>> GetAllPeopleByRoleAndNames(PersonRole role, IEnumerable<string> normalizeNames);
} }
public class PersonRepository : IPersonRepository public class PersonRepository : IPersonRepository
@ -80,6 +81,13 @@ public class PersonRepository : IPersonRepository
return await _context.Person.CountAsync(); return await _context.Person.CountAsync();
} }
public async Task<IList<Person>> GetAllPeopleByRoleAndNames(PersonRole role, IEnumerable<string> normalizeNames)
{
return await _context.Person
.Where(p => p.Role == role && normalizeNames.Contains(p.NormalizedName))
.ToListAsync();
}
public async Task<IList<Person>> GetAllPeople() public async Task<IList<Person>> GetAllPeople()
{ {

View File

@ -13,6 +13,7 @@ using API.DTOs.Dashboard;
using API.DTOs.Filtering; using API.DTOs.Filtering;
using API.DTOs.Filtering.v2; using API.DTOs.Filtering.v2;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Reader;
using API.DTOs.ReadingLists; using API.DTOs.ReadingLists;
using API.DTOs.Search; using API.DTOs.Search;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
@ -374,6 +375,33 @@ public class SeriesRepository : ISeriesRepository
.ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider) .ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider)
.AsEnumerable(); .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 result.ReadingLists = await _context.ReadingList
.Where(rl => rl.AppUserId == userId || rl.Promoted) .Where(rl => rl.AppUserId == userId || rl.Promoted)
.Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%")) .Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%"))

View File

@ -16,6 +16,7 @@ public interface ITagRepository
void Attach(Tag tag); void Attach(Tag tag);
void Remove(Tag tag); void Remove(Tag tag);
Task<IList<Tag>> GetAllTagsAsync(); Task<IList<Tag>> GetAllTagsAsync();
Task<IList<Tag>> GetAllTagsByNameAsync(IEnumerable<string> normalizedNames);
Task<IList<TagDto>> GetAllTagDtosAsync(int userId); Task<IList<TagDto>> GetAllTagDtosAsync(int userId);
Task RemoveAllTagNoLongerAssociated(); Task RemoveAllTagNoLongerAssociated();
Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds, int userId); Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds, int userId);
@ -76,6 +77,13 @@ public class TagRepository : ITagRepository
return await _context.Tag.ToListAsync(); return await _context.Tag.ToListAsync();
} }
public async Task<IList<Tag>> GetAllTagsByNameAsync(IEnumerable<string> normalizedNames)
{
return await _context.Tag
.Where(t => normalizedNames.Contains(t.NormalizedTitle))
.ToListAsync();
}
public async Task<IList<TagDto>> GetAllTagDtosAsync(int userId) public async Task<IList<TagDto>> GetAllTagDtosAsync(int userId)
{ {
var userRating = await _context.AppUser.GetUserAgeRestriction(userId); var userRating = await _context.AppUser.GetUserAgeRestriction(userId);

View File

@ -164,4 +164,19 @@ public static class PersonHelper
onModified(); 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();
}
} }

View File

@ -189,10 +189,10 @@
"user-no-access-library-from-series": "User does not have access to the library this series belongs to", "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", "series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions",
"next-volume-num": "Next Volume: {0}", "next-volume-num": "Upcoming Volume: {0}",
"next-book-num": "Next Book: {0}", "next-book-num": "Upcoming Book: {0}",
"next-issue-num": "Next Issue: {0}{1}", "next-issue-num": "Upcoming Issue: {0}{1}",
"next-chapter-num": "Next Chapter: {0}", "next-chapter-num": "Upcoming Chapter: {0}",
"volume-num": "Volume {0}", "volume-num": "Volume {0}",

View File

@ -130,7 +130,7 @@ public class MetadataService : IMetadataService
return Task.CompletedTask; return Task.CompletedTask;
series.Volumes ??= new List<Volume>(); series.Volumes ??= new List<Volume>();
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)); _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series));
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -17,13 +17,14 @@ using API.Extensions;
using API.Helpers; using API.Helpers;
using API.Helpers.Builders; using API.Helpers.Builders;
using API.Services.Plus; using API.Services.Plus;
using API.Services.Tasks.Scanner.Parser;
using API.SignalR; using API.SignalR;
using Hangfire; using Hangfire;
using Kavita.Common; using Kavita.Common;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Services; namespace API.Services;
#nullable enable
public interface ISeriesService public interface ISeriesService
{ {
@ -51,7 +52,7 @@ public class SeriesService : ISeriesService
private readonly IScrobblingService _scrobblingService; private readonly IScrobblingService _scrobblingService;
private readonly ILocalizationService _localizationService; private readonly ILocalizationService _localizationService;
private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto() private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto
{ {
ExpectedDate = null, ExpectedDate = null,
ChapterNumber = 0, ChapterNumber = 0,
@ -106,11 +107,6 @@ public class SeriesService : ISeriesService
var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId; var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId;
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
if (series == null) return false; 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() series.Metadata ??= new SeriesMetadataBuilder()
.WithCollectionTags(updateSeriesMetadataDto.CollectionTags.Select(dto => .WithCollectionTags(updateSeriesMetadataDto.CollectionTags.Select(dto =>
@ -169,51 +165,102 @@ public class SeriesService : ISeriesService
} }
series.Metadata.CollectionTags ??= new List<CollectionTag>(); if (updateSeriesMetadataDto.CollectionTags.Any())
UpdateCollectionsList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, (tag) =>
{ {
series.Metadata.CollectionTags.Add(tag); var allCollectionTags = (await _unitOfWork.CollectionTagRepository
}); .GetAllTagsByNamesAsync(updateSeriesMetadataDto.CollectionTags.Select(t => Parser.Normalize(t.Title)))).ToList();
series.Metadata.CollectionTags ??= new List<CollectionTag>();
series.Metadata.Genres ??= new List<Genre>(); UpdateCollectionsList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, tag =>
GenreHelper.UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, (genre) => {
{ series.Metadata.CollectionTags.Add(tag);
series.Metadata.Genres.Add(genre); });
}, () => series.Metadata.GenresLocked = true); }
series.Metadata.Tags ??= new List<Tag>();
TagHelper.UpdateTagList(updateSeriesMetadataDto.SeriesMetadata?.Tags, series, allTags, (tag) => if (updateSeriesMetadataDto.SeriesMetadata?.Genres != null &&
{ updateSeriesMetadataDto.SeriesMetadata.Genres.Any())
series.Metadata.Tags.Add(tag); {
}, () => series.Metadata.TagsLocked = true); var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresByNamesAsync(updateSeriesMetadataDto.SeriesMetadata.Genres.Select(t => Parser.Normalize(t.Title)))).ToList();
series.Metadata.Genres ??= new List<Genre>();
void HandleAddPerson(Person person) GenreHelper.UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, genre =>
{ {
PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); series.Metadata.Genres.Add(genre);
allPeople.Add(person); }, () => 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<Tag>();
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<Person>();
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<Person>();
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.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked;
series.Metadata.PublicationStatusLocked = updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked; series.Metadata.PublicationStatusLocked = updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked;
@ -270,7 +317,7 @@ public class SeriesService : ISeriesService
} }
public static void UpdateCollectionsList(ICollection<CollectionTagDto>? tags, Series series, IReadOnlyCollection<CollectionTag> allTags, private static void UpdateCollectionsList(ICollection<CollectionTagDto>? tags, Series series, IReadOnlyCollection<CollectionTag> allTags,
Action<CollectionTag> handleAdd) Action<CollectionTag> handleAdd)
{ {
// TODO: Move UpdateCollectionsList to a helper so we can easily test // 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 libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)) var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
.OrderBy(v => Tasks.Scanner.Parser.Parser.MinNumberFromRange(v.Name)) .OrderBy(v => Parser.MinNumberFromRange(v.Name))
.ToList(); .ToList();
// For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number. // 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); retChapters = retChapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default);
} }
return new SeriesDetailDto() return new SeriesDetailDto
{ {
Specials = specials, Specials = specials,
Chapters = retChapters, Chapters = retChapters,
@ -517,7 +564,7 @@ public class SeriesService : ISeriesService
/// <returns></returns> /// <returns></returns>
private static bool ShouldIncludeChapter(ChapterDto chapter) 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") 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 (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); var title = Path.GetFileNameWithoutExtension(firstChapter.Range);
if (string.IsNullOrEmpty(title)) return; if (string.IsNullOrEmpty(title)) return;
volume.Name += $" - {title}"; volume.Name += $" - {title}";
@ -553,7 +600,7 @@ public class SeriesService : ISeriesService
if (isSpecial) if (isSpecial)
{ {
return Tasks.Scanner.Parser.Parser.CleanSpecialTitle(chapterTitle); return Parser.CleanSpecialTitle(chapterTitle);
} }
var hashSpot = withHash ? "#" : string.Empty; var hashSpot = withHash ? "#" : string.Empty;
@ -650,7 +697,7 @@ public class SeriesService : ISeriesService
r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) != r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) !=
null) continue; null) continue;
series.Relations.Add(new SeriesRelation() series.Relations.Add(new SeriesRelation
{ {
Series = series, Series = series,
SeriesId = series.Id, SeriesId = series.Id,
@ -669,7 +716,7 @@ public class SeriesService : ISeriesService
{ {
throw new UnauthorizedAccessException("user-no-access-library-from-series"); 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; return _emptyExpectedChapter;
} }
@ -732,7 +779,7 @@ public class SeriesService : ISeriesService
var lastVolumeNum = chapters.Select(c => c.Volume.Number).Max(); var lastVolumeNum = chapters.Select(c => c.Volume.Number).Max();
var result = new NextExpectedChapterDto() var result = new NextExpectedChapterDto
{ {
ChapterNumber = 0, ChapterNumber = 0,
VolumeNumber = 0, VolumeNumber = 0,
@ -746,21 +793,16 @@ public class SeriesService : ISeriesService
result.VolumeNumber = lastChapter.Volume.Number; result.VolumeNumber = lastChapter.Volume.Number;
result.Title = series.Library.Type switch result.Title = series.Library.Type switch
{ {
LibraryType.Manga => await _localizationService.Translate(userId, "next-chapter-num", LibraryType.Manga => await _localizationService.Translate(userId, "next-chapter-num", result.ChapterNumber),
new object[] {result.ChapterNumber}), LibraryType.Comic => await _localizationService.Translate(userId, "next-issue-num", "#", result.ChapterNumber),
LibraryType.Comic => await _localizationService.Translate(userId, "next-issue-num", LibraryType.Book => await _localizationService.Translate(userId, "next-book-num", result.ChapterNumber),
new object[] {"#", result.ChapterNumber}), _ => await _localizationService.Translate(userId, "next-chapter-num", 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})
}; };
} }
else else
{ {
result.VolumeNumber = lastVolumeNum + 1; result.VolumeNumber = lastVolumeNum + 1;
result.Title = await _localizationService.Translate(userId, "volume-num", result.Title = await _localizationService.Translate(userId, "volume-num", result.VolumeNumber);
new object[] {result.VolumeNumber});
} }

View File

@ -296,8 +296,8 @@ public class ScannerService : IScannerService
MessageFactory.ScanSeriesEvent(library.Id, seriesId, series.Name)); MessageFactory.ScanSeriesEvent(library.Id, seriesId, series.Name));
await _metadataService.RemoveAbandonedMetadataKeys(); await _metadataService.RemoveAbandonedMetadataKeys();
BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(series.LibraryId, seriesId, false)); //BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(series.LibraryId, seriesId, false));
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, false)); //BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, false));
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory)); BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory));
return; return;

View File

@ -0,0 +1,8 @@
export interface BookmarkSearchResult {
libraryId: number;
seriesId: number;
volumeId: number;
chapterId: number;
seriesName: string;
localizedSeriesName: string;
}

View File

@ -3,6 +3,7 @@ import { Library } from "../library/library";
import { MangaFile } from "../manga-file"; import { MangaFile } from "../manga-file";
import { SearchResult } from "./search-result"; import { SearchResult } from "./search-result";
import { Tag } from "../tag"; import { Tag } from "../tag";
import {BookmarkSearchResult} from "./bookmark-search-result";
export class SearchResultGroup { export class SearchResultGroup {
libraries: Array<Library> = []; libraries: Array<Library> = [];
@ -14,6 +15,7 @@ export class SearchResultGroup {
tags: Array<Tag> = []; tags: Array<Tag> = [];
files: Array<MangaFile> = []; files: Array<MangaFile> = [];
chapters: Array<Chapter> = []; chapters: Array<Chapter> = [];
bookmarks: Array<BookmarkSearchResult> = [];
reset() { reset() {
this.libraries = []; this.libraries = [];
@ -25,5 +27,6 @@ export class SearchResultGroup {
this.tags = []; this.tags = [];
this.files = []; this.files = [];
this.chapters = []; this.chapters = [];
this.bookmarks = [];
} }
} }

View File

@ -102,7 +102,7 @@ export class ActionService implements OnDestroy {
} }
editLibrary(library: Partial<Library>, callback?: LibraryActionCallback) { editLibrary(library: Partial<Library>, 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.componentInstance.library = library;
modalRef.closed.subscribe((closeResult: {success: boolean, library: Library, coverImageUpdate: boolean}) => { modalRef.closed.subscribe((closeResult: {success: boolean, library: Library, coverImageUpdate: boolean}) => {
if (callback) callback(library) if (callback) callback(library)
@ -362,7 +362,7 @@ export class ActionService implements OnDestroy {
addMultipleToReadingList(seriesId: number, volumes: Array<Volume>, chapters?: Array<Chapter>, callback?: BooleanActionCallback) { addMultipleToReadingList(seriesId: number, volumes: Array<Volume>, chapters?: Array<Chapter>, callback?: BooleanActionCallback) {
if (this.readingListModalRef != null) { return; } 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.seriesId = seriesId;
this.readingListModalRef.componentInstance.volumeIds = volumes.map(v => v.id); this.readingListModalRef.componentInstance.volumeIds = volumes.map(v => v.id);
this.readingListModalRef.componentInstance.chapterIds = chapters?.map(c => c.id); this.readingListModalRef.componentInstance.chapterIds = chapters?.map(c => c.id);
@ -404,7 +404,7 @@ export class ActionService implements OnDestroy {
addMultipleSeriesToReadingList(series: Array<Series>, callback?: BooleanActionCallback) { addMultipleSeriesToReadingList(series: Array<Series>, callback?: BooleanActionCallback) {
if (this.readingListModalRef != null) { return; } 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.seriesIds = series.map(v => v.id);
this.readingListModalRef.componentInstance.title = 'Multiple Selections'; this.readingListModalRef.componentInstance.title = 'Multiple Selections';
this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple_Series; this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple_Series;
@ -432,7 +432,7 @@ export class ActionService implements OnDestroy {
*/ */
addMultipleSeriesToCollectionTag(series: Array<Series>, callback?: BooleanActionCallback) { addMultipleSeriesToCollectionTag(series: Array<Series>, callback?: BooleanActionCallback) {
if (this.collectionModalRef != null) { return; } 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.seriesIds = series.map(v => v.id);
this.collectionModalRef.componentInstance.title = 'New Collection'; this.collectionModalRef.componentInstance.title = 'New Collection';
@ -452,7 +452,7 @@ export class ActionService implements OnDestroy {
addSeriesToReadingList(series: Series, callback?: SeriesActionCallback) { addSeriesToReadingList(series: Series, callback?: SeriesActionCallback) {
if (this.readingListModalRef != null) { return; } 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.seriesId = series.id;
this.readingListModalRef.componentInstance.title = series.name; this.readingListModalRef.componentInstance.title = series.name;
this.readingListModalRef.componentInstance.type = ADD_FLOW.Series; this.readingListModalRef.componentInstance.type = ADD_FLOW.Series;
@ -474,7 +474,7 @@ export class ActionService implements OnDestroy {
addVolumeToReadingList(volume: Volume, seriesId: number, callback?: VolumeActionCallback) { addVolumeToReadingList(volume: Volume, seriesId: number, callback?: VolumeActionCallback) {
if (this.readingListModalRef != null) { return; } 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.seriesId = seriesId;
this.readingListModalRef.componentInstance.volumeId = volume.id; this.readingListModalRef.componentInstance.volumeId = volume.id;
this.readingListModalRef.componentInstance.type = ADD_FLOW.Volume; this.readingListModalRef.componentInstance.type = ADD_FLOW.Volume;
@ -496,7 +496,7 @@ export class ActionService implements OnDestroy {
addChapterToReadingList(chapter: Chapter, seriesId: number, callback?: ChapterActionCallback) { addChapterToReadingList(chapter: Chapter, seriesId: number, callback?: ChapterActionCallback) {
if (this.readingListModalRef != null) { return; } 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.seriesId = seriesId;
this.readingListModalRef.componentInstance.chapterId = chapter.id; this.readingListModalRef.componentInstance.chapterId = chapter.id;
this.readingListModalRef.componentInstance.type = ADD_FLOW.Chapter; this.readingListModalRef.componentInstance.type = ADD_FLOW.Chapter;
@ -517,7 +517,7 @@ export class ActionService implements OnDestroy {
} }
editReadingList(readingList: ReadingList, callback?: ReadingListActionCallback) { 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.componentInstance.readingList = readingList;
readingListModalRef.closed.pipe(take(1)).subscribe((list) => { readingListModalRef.closed.pipe(take(1)).subscribe((list) => {
if (callback && list !== undefined) { if (callback && list !== undefined) {

View File

@ -43,7 +43,7 @@ export class ReviewCardComponent implements OnInit {
} else { } else {
component = ReviewCardModalComponent; 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; ref.componentInstance.review = this.review;
} }

View File

@ -99,7 +99,7 @@ export class ManageLibraryComponent implements OnInit {
} }
editLibrary(library: Library) { 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.componentInstance.library = library;
modalRef.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(refresh => { modalRef.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(refresh => {
if (refresh) { if (refresh) {
@ -109,7 +109,7 @@ export class ManageLibraryComponent implements OnInit {
} }
addLibrary() { 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 => { modalRef.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(refresh => {
if (refresh) { if (refresh) {
this.getLibraries(); this.getLibraries();

View File

@ -96,7 +96,7 @@ export class ManageMediaSettingsComponent implements OnInit {
} }
openDirectoryChooser(existingDirectory: string, formControl: string) { 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.startingFolder = existingDirectory || '';
modalRef.componentInstance.helpUrl = ''; modalRef.componentInstance.helpUrl = '';
modalRef.closed.subscribe((closeResult: DirectoryPickerResult) => { modalRef.closed.subscribe((closeResult: DirectoryPickerResult) => {

View File

@ -5,7 +5,7 @@ import { AccountService } from './_services/account.service';
import { LibraryService } from './_services/library.service'; import { LibraryService } from './_services/library.service';
import { NavService } from './_services/nav.service'; import { NavService } from './_services/nav.service';
import { filter } from 'rxjs/operators'; 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 { DOCUMENT, NgClass, NgIf, AsyncPipe } from '@angular/common';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import {ThemeService} from "./_services/theme.service"; import {ThemeService} from "./_services/theme.service";
@ -32,7 +32,9 @@ export class AppComponent implements OnInit {
constructor(private accountService: AccountService, constructor(private accountService: AccountService,
private libraryService: LibraryService, private libraryService: LibraryService,
private router: Router, private ngbModal: NgbModal, ratingConfig: NgbRatingConfig, 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 // Setup default rating config
ratingConfig.max = 5; ratingConfig.max = 5;

View File

@ -13,10 +13,16 @@
(refreshToC)="refreshPersonalToC()"> (refreshToC)="refreshPersonalToC()">
</app-book-line-overlay> </app-book-line-overlay>
<app-drawer #commentDrawer="drawer" [(isOpen)]="drawerOpen" [options]="{topOffset: topOffset}"> <app-drawer #commentDrawer="drawer" [(isOpen)]="drawerOpen" [options]="{topOffset: topOffset}">
<h5 header> <div header>
{{t('title')}} <h5 class="mb-0">{{t('title')}}</h5>
</h5> <span style="font-size: 14px; color: var(--primary-color)" tabindex="0" role="button" (click)="closeReader()">{{t('close-reader')}}</span>
</div>
<div subheader> <div subheader>
<!-- <div class="g-0 text-center" *ngIf="!isLoading">-->
<!-- <span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" [attr.aria-label]="t('incognito-mode-alt')">-->
<!-- (<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-mode-label')}}</span>)</span>-->
<!-- <span class="book-title-text ms-1" [ngbTooltip]="bookTitle">{{bookTitle}}</span>-->
<!-- </div>-->
<div class="pagination-cont"> <div class="pagination-cont">
<ng-container *ngIf="layoutMode !== BookPageLayoutMode.Default"> <ng-container *ngIf="layoutMode !== BookPageLayoutMode.Default">
<div class="virt-pagination-cont"> <div class="virt-pagination-cont">
@ -148,7 +154,7 @@
<ng-template #showTitle> <ng-template #showTitle>
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" [attr.aria-label]="t('incognito-mode-alt')"> <span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" [attr.aria-label]="t('incognito-mode-alt')">
(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-mode-label')}}</span>)</span> (<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-mode-label')}}</span>)</span>
<span class="book-title-text ms-1" [ngbTooltip]="bookTitle">{{bookTitle}}</span> <span class="book-title-text ms-1" [ngbTooltip]="bookTitle">{{bookTitle}}</span>
</ng-template> </ng-template>
</div> </div>
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i></button> <button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i></button>

View File

@ -1,5 +1,14 @@
<ng-container *transloco="let t; read: 'manga-reader'"> <ng-container *transloco="let t; read: 'manga-reader'">
<div class="reader" #reader [ngStyle]="{overflow: (isFullscreen ? 'auto' : 'visible')}"> <div class="reader" #reader [ngStyle]="{overflow: (isFullscreen ? 'auto' : 'visible')}">
@if(debugMode) {
<div class="fixed-top overlay">
@for(img of cachedImages; track img.src) {
<ng-container *ngIf="this.readerService.imageUrlToPageNum(img.src) as imageNum">
<span class="me-1" [ngClass]="{'current': imageNum === this.pageNum, 'loaded': img.complete}">{{this.readerService.imageUrlToPageNum(img.src)}}</span>
</ng-container>
}
</div>
}
<div class="fixed-top overlay" *ngIf="menuOpen" [@slideFromTop]="menuOpen"> <div class="fixed-top overlay" *ngIf="menuOpen" [@slideFromTop]="menuOpen">
<div style="display: flex; margin-top: 5px;"> <div style="display: flex; margin-top: 5px;">
<button class="btn btn-icon" style="height: 100%" [title]="t('back')" (click)="closeReader()"> <button class="btn btn-icon" style="height: 100%" [title]="t('back')" (click)="closeReader()">
@ -191,34 +200,34 @@
<ng-container [ngSwitch]="layoutMode"> <ng-container [ngSwitch]="layoutMode">
<ng-container *ngSwitchCase="LayoutMode.Single"> <ng-container *ngSwitchCase="LayoutMode.Single">
<div class="split-double"> <div class="split-double">
<span class="fa-stack fa-1x"> <span class="fa-stack fa-1x">
<i class="fa-regular fa-square-full fa-stack-2x"></i> <i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fa fa-image fa-stack-1x"></i> <i class="fa fa-image fa-stack-1x"></i>
</span> </span>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="LayoutMode.Double"> <ng-container *ngSwitchCase="LayoutMode.Double">
<div class="split-double"> <div class="split-double">
<span class="fa-stack fa-1x"> <span class="fa-stack fa-1x">
<i class="fa-regular fa-square-full fa-stack-2x"></i> <i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-1 fa-stack-1x"></i> <i class="fab fa-1 fa-stack-1x"></i>
</span> </span>
<span class="fa-stack fa right"> <span class="fa-stack fa right">
<i class="fa-regular fa-square-full fa-stack-2x"></i> <i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-2 fa-stack-1x"></i> <i class="fab fa-2 fa-stack-1x"></i>
</span> </span>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="LayoutMode.DoubleReversed"> <ng-container *ngSwitchCase="LayoutMode.DoubleReversed">
<div class="split-double"> <div class="split-double">
<span class="fa-stack fa-1x"> <span class="fa-stack fa-1x">
<i class="fa-regular fa-square-full fa-stack-2x"></i> <i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-2 fa-stack-1x"></i> <i class="fab fa-2 fa-stack-1x"></i>
</span> </span>
<span class="fa-stack fa right"> <span class="fa-stack fa right">
<i class="fa-regular fa-square-full fa-stack-2x"></i> <i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-1 fa-stack-1x"></i> <i class="fab fa-1 fa-stack-1x"></i>
</span> </span>
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>
@ -258,7 +267,8 @@
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
<label for="darkness" class="form-label range-label">{{t('brightness-label')}}</label><span class="ms-1 range-text">{{generalSettingsForm.get('darkness')?.value + '%'}}</span> <label for="darkness" class="form-label range-label">{{t('brightness-label')}}</label>
<span class="ms-1 range-text">{{generalSettingsForm.get('darkness')?.value + '%'}}</span>
<input type="range" class="form-range" id="darkness" <input type="range" class="form-range" id="darkness"
min="10" max="100" step="1" formControlName="darkness"> min="10" max="100" step="1" formControlName="darkness">
</div> </div>

View File

@ -6,7 +6,13 @@ $pointer-offset: 5px;
@use '../../.././../manga-reader-common'; @use '../../.././../manga-reader-common';
.current {
background-color: var(--primary-color);
}
.loaded {
font-weight: bold;
}
.reading-area { .reading-area {

View File

@ -13,7 +13,7 @@ import {
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } 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 {ActivatedRoute, Router} from '@angular/router';
import { import {
BehaviorSubject, BehaviorSubject,
@ -124,7 +124,7 @@ enum KeyDirection {
imports: [NgStyle, NgIf, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent, imports: [NgStyle, NgIf, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent,
DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent, DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent,
NgxSliderModule, ReactiveFormsModule, NgFor, NgSwitch, NgSwitchCase, FittingIconPipe, ReaderModeIconPipe, NgxSliderModule, ReactiveFormsModule, NgFor, NgSwitch, NgSwitchCase, FittingIconPipe, ReaderModeIconPipe,
FullscreenIconPipe, TranslocoDirective, NgbProgressbar, PercentPipe] FullscreenIconPipe, TranslocoDirective, NgbProgressbar, PercentPipe, NgClass]
}) })
export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ -392,6 +392,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/ */
hasHitBottomTopScroll: boolean = false; hasHitBottomTopScroll: boolean = false;
/**
* Show and log debug information
*/
debugMode: boolean = false;
// Renderer interaction // Renderer interaction
readerSettings$!: Observable<ReaderSetting>; readerSettings$!: Observable<ReaderSetting>;
private currentImage: Subject<HTMLImageElement | null> = new ReplaySubject(1); private currentImage: Subject<HTMLImageElement | null> = new ReplaySubject(1);
@ -405,7 +410,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return this.readerService.getPageUrl(chapterId, pageNum); return this.readerService.getPageUrl(chapterId, pageNum);
} }
get CurrentPageBookmarked() { get CurrentPageBookmarked() {
return this.bookmarks.hasOwnProperty(this.pageNum); return this.bookmarks.hasOwnProperty(this.pageNum);
} }
@ -479,7 +483,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
this.getPageFn = this.getPage.bind(this); this.getPageFn = this.getPage.bind(this);
this.readerService.enableWakeLock(this.reader.nativeElement);
this.libraryId = parseInt(libraryId, 10); this.libraryId = parseInt(libraryId, 10);
this.seriesId = parseInt(seriesId, 10); this.seriesId = parseInt(seriesId, 10);
@ -864,6 +867,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.render(); this.render();
}); });
setTimeout(() => {
this.readerService.enableWakeLock(this.reader.nativeElement);
}, 1000);
return; return;
} }
@ -1342,6 +1348,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (cachedImagePageNum !== numOffset) { if (cachedImagePageNum !== numOffset) {
this.cachedImages[index] = new Image(); this.cachedImages[index] = new Image();
this.cachedImages[index].src = this.getPageUrl(numOffset); this.cachedImages[index].src = this.getPageUrl(numOffset);
this.cachedImages[index].onload = (evt) => {
this.cdRef.markForCheck();
}
} }
} }

View File

@ -45,6 +45,16 @@
</ul> </ul>
</ng-container> </ng-container>
<ng-container *ngIf="bookmarkTemplate !== undefined && groupedData.bookmarks.length > 0">
<li class="list-group-item section-header"><h5>{{t('bookmarks')}}</h5></li>
<ul class="list-group results">
<li *ngFor="let option of groupedData.bookmarks; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="bookmarkTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="libraryTemplate !== undefined && groupedData.libraries.length > 0"> <ng-container *ngIf="libraryTemplate !== undefined && groupedData.libraries.length > 0">
<li class="list-group-item section-header"><h5 id="libraries-group">{{t('libraries')}}</h5></li> <li class="list-group-item section-header"><h5 id="libraries-group">{{t('libraries')}}</h5></li>
<ul class="list-group results" role="group" aria-describedby="libraries-group"> <ul class="list-group results" role="group" aria-describedby="libraries-group">

View File

@ -80,6 +80,7 @@ export class GroupedTypeaheadComponent implements OnInit {
@ContentChild('readingListTemplate') readingListTemplate!: TemplateRef<any>; @ContentChild('readingListTemplate') readingListTemplate!: TemplateRef<any>;
@ContentChild('fileTemplate') fileTemplate!: TemplateRef<any>; @ContentChild('fileTemplate') fileTemplate!: TemplateRef<any>;
@ContentChild('chapterTemplate') chapterTemplate!: TemplateRef<any>; @ContentChild('chapterTemplate') chapterTemplate!: TemplateRef<any>;
@ContentChild('bookmarkTemplate') bookmarkTemplate!: TemplateRef<any>;
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
@ -96,7 +97,7 @@ export class GroupedTypeaheadComponent implements OnInit {
get hasData() { get hasData() {
return !(this.noResultsTemplate != undefined && !this.groupedData.persons.length && !this.groupedData.collections.length 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.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);
} }

View File

@ -51,6 +51,23 @@
</div> </div>
</ng-template> </ng-template>
<ng-template #bookmarkTemplate let-item>
<div style="display: flex;padding: 5px;" (click)="clickBookmarkSearchResult(item)">
<div style="width: 24px" class="me-1">
<app-image class="me-3 search-result" width="24px" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"></app-image>
</div>
<div class="ms-1">
<app-series-format [format]="item.format"></app-series-format>
<ng-container *ngIf="searchTerm.toLowerCase().trim() as st">
<span *ngIf="item.seriesName.toLowerCase().trim().indexOf(st) >= 0; else localizedName">{{item.seriesName}}</span>
<ng-template #localizedName>
<span [innerHTML]="item.localizedSeriesName"></span>
</ng-template>
</ng-container>
</div>
</div>
</ng-template>
<ng-template #collectionTemplate let-item> <ng-template #collectionTemplate let-item>
<div style="display: flex;padding: 5px;" (click)="clickCollectionSearchResult(item)"> <div style="display: flex;padding: 5px;" (click)="clickCollectionSearchResult(item)">
<div style="width: 24px" class="me-1"> <div style="width: 24px" class="me-1">

View File

@ -39,6 +39,7 @@ import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities
import {FilterStatement} from "../../../_models/metadata/v2/filter-statement"; import {FilterStatement} from "../../../_models/metadata/v2/filter-statement";
import {FilterField} from "../../../_models/metadata/v2/filter-field"; import {FilterField} from "../../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
import {BookmarkSearchResult} from "../../../_models/search/bookmark-search-result";
@Component({ @Component({
selector: 'app-nav-header', selector: 'app-nav-header',
@ -197,6 +198,15 @@ export class NavHeaderComponent implements OnInit {
this.router.navigate(['library', libraryId, 'series', seriesId]); 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) { clickFileSearchResult(item: MangaFile) {
this.clearSearch(); this.clearSearch();
this.searchService.getSeriesForMangaFile(item.id).subscribe(series => { this.searchService.getSeriesForMangaFile(item.id).subscribe(series => {

View File

@ -73,7 +73,7 @@ export class ReadingListsComponent implements OnInit {
} }
importCbl() { 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.closed.subscribe(result => this.loadPage());
ref.dismissed.subscribe(_ => this.loadPage()); ref.dismissed.subscribe(_ => this.loadPage());
} }

View File

@ -3,4 +3,5 @@
alt="" alt=""
aria-hidden="true" aria-hidden="true"
[lazyLoad]="imageUrl" [lazyLoad]="imageUrl"
[errorImage]="errorImage"
(onStateChange)="myCallbackFunction($event)"> (onStateChange)="myCallbackFunction($event)">

View File

@ -172,7 +172,7 @@ export class ImageComponent implements OnChanges {
case 'loading-failed': case 'loading-failed':
// The image could not be loaded for some reason. // The image could not be loaded for some reason.
// `event.data` is the error in this case // `event.data` is the error in this case
image.src = this.errorImage; this.renderer.removeClass(image, 'fade-in');
this.cdRef.markForCheck(); this.cdRef.markForCheck();
break; break;
case 'finally': case 'finally':

View File

@ -618,11 +618,12 @@
"settings-header": "Settings", "settings-header": "Settings",
"table-of-contents-header": "Table of Contents", "table-of-contents-header": "Table of Contents",
"bookmarks-header": "Bookmarks", "bookmarks-header": "{{side-nav.bookmarks}}",
"toc-header": "ToC", "toc-header": "ToC",
"loading-book": "Loading book…", "loading-book": "Loading book…",
"go-back": "Go Back", "go-back": "Go Back",
"close-reader": "Close Reader",
"incognito-mode-alt": "Incognito mode is on. Toggle to turn off.", "incognito-mode-alt": "Incognito mode is on. Toggle to turn off.",
"incognito-mode-label": "Incognito Mode", "incognito-mode-label": "Incognito Mode",
"next": "Next", "next": "Next",
@ -1409,6 +1410,7 @@
"people": "People", "people": "People",
"tags": "Tags", "tags": "Tags",
"genres": "Genres", "genres": "Genres",
"bookmarks": "{{side-nav.bookmarks}}",
"libraries": "Libraries", "libraries": "Libraries",
"reading-lists": "Reading Lists", "reading-lists": "Reading Lists",
"collections": "Collections", "collections": "Collections",

View File

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.10.15" "version": "0.7.10.16"
}, },
"servers": [ "servers": [
{ {
@ -13437,6 +13437,36 @@
}, },
"additionalProperties": false "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": { "BulkRemoveBookmarkForSeriesDto": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -17188,6 +17218,13 @@
"$ref": "#/components/schemas/ChapterDto" "$ref": "#/components/schemas/ChapterDto"
}, },
"nullable": true "nullable": true
},
"bookmarks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BookmarkSearchResultDto"
},
"nullable": true
} }
}, },
"additionalProperties": false, "additionalProperties": false,