Fixes, Tweaks, and Series Filtering (#1217)

* From previous fix, added the other locking conditions on the update series metadata.

* Fixed a bug where custom series, collection tag, and reading list covers weren't being removed on cleanup.

* Ensure reading list detail has a margin to align to the standard

* Refactored some event stuff to use dedicated consts. Introduced a new event when users read something, which can update progress bars on cards.

* Added recomended and library tags to the library detail page. This will eventually offer more custom analytics

* Cleanup some code onc arousel

* Adjusted scale to height/width css to better fit

* Small css tweaks to better center images in the manga reader in both axis. This takes care of double page rendering as well.

* When a special has a Title set in the metadata, on series detail page, show that on the card rather than filename.

* Fixed a bug where when paging in manga reader, the scroll to top wasn't working due to changing where scrolling is done

* More css goodness for rendering images in manga reader

* Fixed a bug where clearing a typeahead externally wouldn't clear the x button

* Fixed a bug where filering then using keyboard would select wrong option

* Added a new sorting field for Last Chapter Added (new field) to get a similar on deck feel.

* Tweaked recently updated to hit the NFR of 500ms (300ms fresh start) and still give a much better experience.

* Refactored On deck to now go to all series and also sort by last updated. Recently Added Series now loads all series with sort by created.

* Some tweaks on css for cover image chooser

* Fixed a bug in pagination control where multiple pagination events could trigger on load and thus multiple requests for data on parent controller.

* Updated edit series modal to show when the last chapter was added and when user last read it.

* Implemented a highlight on the fitler button when a filter is active.

* Refactored metadata filter screens to perserve the filters in the url and thus when navigating back and forth, it will retain. users should click side nav to reset the state.

* Hide middle section on companion bar on phones

* Cleaned up some prefilters and console.logs

* Don't open drawer by default when a filter is active
This commit is contained in:
Joseph Milazzo 2022-04-14 16:55:06 -05:00 committed by GitHub
parent 5e629913b7
commit 553f9b0d98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 864 additions and 537 deletions

View file

@ -157,7 +157,7 @@ namespace API.Controllers
tag.CoverImageLocked = false;
tag.CoverImage = string.Empty;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(tag.Id, "collection"), false);
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
_unitOfWork.CollectionTagRepository.Update(tag);
}

View file

@ -11,6 +11,8 @@ using API.Entities;
using API.Extensions;
using API.Services;
using API.Services.Tasks;
using API.SignalR;
using Hangfire;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@ -25,23 +27,21 @@ namespace API.Controllers
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ReaderController> _logger;
private readonly IReaderService _readerService;
private readonly IDirectoryService _directoryService;
private readonly ICleanupService _cleanupService;
private readonly IBookmarkService _bookmarkService;
private readonly IEventHub _eventHub;
/// <inheritdoc />
public ReaderController(ICacheService cacheService,
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
IReaderService readerService, IDirectoryService directoryService,
ICleanupService cleanupService, IBookmarkService bookmarkService)
IReaderService readerService, IBookmarkService bookmarkService,
IEventHub eventHub)
{
_cacheService = cacheService;
_unitOfWork = unitOfWork;
_logger = logger;
_readerService = readerService;
_directoryService = directoryService;
_cleanupService = cleanupService;
_bookmarkService = bookmarkService;
_eventHub = eventHub;
}
/// <summary>
@ -111,13 +111,14 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId);
if (await _unitOfWork.CommitAsync())
{
return Ok();
}
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
return BadRequest("There was an issue saving progress");
// var series = new List<SeriesDto>()
// {await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(markReadDto.SeriesId, user.Id)};
// await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
// await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
// MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markReadDto.SeriesId, series[0], series[0].Pages));
return Ok();
}
@ -132,13 +133,19 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId);
if (await _unitOfWork.CommitAsync())
{
return Ok();
}
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
return BadRequest("There was an issue saving progress");
// Should I do this for every chapter? Maybe in a background task?
// foreach (var chapterId in await
// _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new List<int>() {markReadDto.SeriesId}))
// {
// await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
// MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, chapterId, MessageFactoryEntityTypes.Chapter, 0));
// }
//
// await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
// MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markReadDto.SeriesId, MessageFactoryEntityTypes.Series, 0));
return Ok();
}
/// <summary>

View file

@ -211,12 +211,8 @@ namespace API.Controllers
{
return BadRequest("A list of this name already exists");
}
user.ReadingLists.Add(new ReadingList()
{
Promoted = false,
Title = dto.Title,
Summary = string.Empty
});
user.ReadingLists.Add(DbFactory.ReadingList(dto.Title, string.Empty, false));
if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list");
@ -257,7 +253,7 @@ namespace API.Controllers
readingList.CoverImageLocked = false;
readingList.CoverImage = string.Empty;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(readingList.Id, "readingList"), false);
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
_unitOfWork.ReadingListRepository.Update(readingList);
}
@ -474,14 +470,7 @@ namespace API.Controllers
foreach (var chapter in chaptersForSeries)
{
if (existingChapterExists.Contains(chapter.Id)) continue;
readingList.Items.Add(new ReadingListItem()
{
Order = index,
ChapterId = chapter.Id,
SeriesId = seriesId,
VolumeId = chapter.VolumeId
});
readingList.Items.Add(DbFactory.ReadingListItem(index, seriesId, chapter.VolumeId, chapter.Id));
index += 1;
}

View file

@ -149,7 +149,7 @@ namespace API.Controllers
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(tag.Id, "collection"), false);
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
return Ok();
}
@ -196,7 +196,7 @@ namespace API.Controllers
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(readingList.Id, "readingList"), false);
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
return Ok();
}

View file

@ -5,4 +5,5 @@ public enum SortField
SortName = 1,
CreatedDate = 2,
LastModifiedDate = 3,
LastChapterAdded = 4
}

View file

@ -22,6 +22,10 @@ namespace API.DTOs
/// </summary>
public DateTime LatestReadDate { get; set; }
/// <summary>
/// DateTime representing last time a chapter was added to the Series
/// </summary>
public DateTime LastChapterAdded { get; set; }
/// <summary>
/// Rating from logged in user. Calculated at API-time.
/// </summary>
public int UserRating { get; set; }

View file

@ -82,6 +82,29 @@ namespace API.Data
};
}
public static ReadingList ReadingList(string title, string summary, bool promoted)
{
return new ReadingList()
{
NormalizedTitle = API.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
Title = title?.Trim(),
Summary = summary?.Trim(),
Promoted = promoted,
Items = new List<ReadingListItem>()
};
}
public static ReadingListItem ReadingListItem(int index, int seriesId, int volumeId, int chapterId)
{
return new ReadingListItem()
{
Order = index,
ChapterId = chapterId,
SeriesId = seriesId,
VolumeId = volumeId
};
}
public static Genre Genre(string name, bool external)
{
return new Genre()

View file

@ -27,6 +27,7 @@ public interface IReadingListRepository
void Update(ReadingList list);
Task<int> Count();
Task<string> GetCoverImageAsync(int readingListId);
Task<IList<string>> GetAllCoverImagesAsync();
}
public class ReadingListRepository : IReadingListRepository
@ -59,6 +60,15 @@ public class ReadingListRepository : IReadingListRepository
.SingleOrDefaultAsync();
}
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.ReadingList
.Select(t => t.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
}
public void Remove(ReadingListItem item)
{
_context.ReadingListItem.Remove(item);

View file

@ -95,7 +95,7 @@ public interface ISeriesRepository
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId);
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
}
public class SeriesRepository : ISeriesRepository
@ -231,7 +231,7 @@ public class SeriesRepository : ISeriesRepository
/// <summary>
/// Gets all series
/// </summary>
/// <param name="libraryId"></param>
/// <param name="libraryId">Restricts to just one library</param>
/// <param name="userId"></param>
/// <param name="userParams"></param>
/// <param name="filter"></param>
@ -617,6 +617,7 @@ public class SeriesRepository : ISeriesRepository
LastReadingProgress = _context.AppUserProgresses
.Where(p => p.Id == progress.Id && p.AppUserId == userId)
.Max(p => p.LastModified),
LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId).Max(p => p.LastModified),
s.LastChapterAdded
});
if (cutoffOnDate)
@ -628,8 +629,8 @@ public class SeriesRepository : ISeriesRepository
var retSeries = query.Where(s => s.AppUserId == userId
&& s.PagesRead > 0
&& s.PagesRead < s.Series.Pages)
.OrderByDescending(s => s.LastReadingProgress)
.ThenByDescending(s => s.LastChapterAdded)
.OrderByDescending(s => s.LastChapterAdded)
.ThenByDescending(s => s.LastReadingProgress)
.Select(s => s.Series)
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.AsSplitQuery()
@ -680,6 +681,7 @@ public class SeriesRepository : ISeriesRepository
SortField.SortName => query.OrderBy(s => s.SortName),
SortField.CreatedDate => query.OrderBy(s => s.Created),
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
_ => query
};
}
@ -690,6 +692,7 @@ public class SeriesRepository : ISeriesRepository
SortField.SortName => query.OrderByDescending(s => s.SortName),
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
_ => query
};
}
@ -900,16 +903,18 @@ public class SeriesRepository : ISeriesRepository
/// <summary>
/// Return recently updated series, regardless of read progress, and group the number of volume or chapters added.
/// </summary>
/// <remarks>This provides 2 levels of pagination. Fetching the individual chapters only looks at 3000. Then when performing grouping
/// in memory, we stop after 30 series. </remarks>
/// <param name="userId">Used to ensure user has access to libraries</param>
/// <returns></returns>
public async Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId)
public async Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30)
{
var ret = await GetRecentlyAddedChaptersQuery(userId, 150);
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
var index = 0;
foreach (var item in ret)
foreach (var item in await GetRecentlyAddedChaptersQuery(userId))
{
if (seriesMap.Keys.Count == pageSize) break;
if (seriesMap.ContainsKey(item.SeriesName))
{
seriesMap[item.SeriesName].Count += 1;
@ -932,43 +937,9 @@ public class SeriesRepository : ISeriesRepository
}
return seriesMap.Values.AsEnumerable();
//return seriesMap.Values.ToList();
// var libraries = await _context.AppUser
// .Where(u => u.Id == userId)
// .SelectMany(u => u.Libraries.Select(l => new {LibraryId = l.Id, LibraryType = l.Type}))
// .ToListAsync();
// var libraryIds = libraries.Select(l => l.LibraryId).ToList();
//
// var cuttoffDate = DateTime.Now - TimeSpan.FromDays(12);
//
// var ret2 = _context.Series
// .Where(s => s.LastChapterAdded >= cuttoffDate
// && libraryIds.Contains(s.LibraryId))
// .Select((s) => new GroupedSeriesDto
// {
// LibraryId = s.LibraryId,
// LibraryType = s.Library.Type,
// SeriesId = s.Id,
// SeriesName = s.Name,
// //Created = s.LastChapterAdded, // Hmm on first migration this wont work
// Created = s.Volumes.SelectMany(v => v.Chapters).Max(c => c.Created), // Hmm on first migration this wont work
// Count = s.Volumes.SelectMany(v => v.Chapters).Count(c => c.Created >= cuttoffDate),
// //Id = index,
// Format = s.Format
// })
// .Take(50)
// .OrderByDescending(c => c.Created)
// .AsSplitQuery()
// .AsEnumerable();
//
// return ret2;
}
private async Task<IEnumerable<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 50)
private async Task<IEnumerable<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 3000)
{
var libraries = await _context.AppUser
.Where(u => u.Id == userId)
@ -1000,7 +971,7 @@ public class SeriesRepository : ISeriesRepository
VolumeNumber = c.Volume.Number,
ChapterTitle = c.Title
})
.Take(maxRecords)
//.Take(maxRecords)
.AsSplitQuery()
.Where(c => c.Created >= withinLastWeek && libraryIds.Contains(c.LibraryId))
.AsEnumerable();

View file

@ -26,8 +26,9 @@ public class ImageService : IImageService
private readonly ILogger<ImageService> _logger;
private readonly IDirectoryService _directoryService;
public const string ChapterCoverImageRegex = @"v\d+_c\d+";
public const string SeriesCoverImageRegex = @"series_\d+";
public const string CollectionTagCoverImageRegex = @"tag_\d+";
public const string SeriesCoverImageRegex = @"series\d+";
public const string CollectionTagCoverImageRegex = @"tag\d+";
public const string ReadingListCoverImageRegex = @"readinglist\d+";
/// <summary>

View file

@ -72,7 +72,7 @@ public class MetadataService : IMetadataService
_logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath);
chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath, ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.Id, "chapter"), false);
MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false);
return true;
}
@ -101,7 +101,7 @@ public class MetadataService : IMetadataService
if (firstChapter == null) return false;
volume.CoverImage = firstChapter.CoverImage;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(volume.Id, "volume"), false);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume), false);
return true;
}
@ -138,7 +138,7 @@ public class MetadataService : IMetadataService
}
}
series.CoverImage = firstCover?.CoverImage ?? coverImage;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, "series"), false);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
}
@ -300,7 +300,7 @@ public class MetadataService : IMetadataService
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
{
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, "series"), false);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
}
_logger.LogInformation("[MetadataService] Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds);

View file

@ -9,6 +9,7 @@ using API.Data.Repositories;
using API.DTOs;
using API.Entities;
using API.Extensions;
using API.SignalR;
using Kavita.Common;
using Microsoft.Extensions.Logging;
@ -33,13 +34,15 @@ public class ReaderService : IReaderService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ReaderService> _logger;
private readonly IEventHub _eventHub;
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger)
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger, IEventHub eventHub)
{
_unitOfWork = unitOfWork;
_logger = logger;
_eventHub = eventHub;
}
public static string FormatBookmarkFolderPath(string baseDirectory, int userId, int seriesId, int chapterId)
@ -211,9 +214,11 @@ public class ReaderService : IReaderService
_unitOfWork.AppUserProgressRepository.Update(userProgress);
}
if (!_unitOfWork.HasChanges()) return true;
if (await _unitOfWork.CommitAsync())
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
MessageFactory.UserProgressUpdateEvent(userId, user.UserName, progressDto.SeriesId, progressDto.VolumeId, progressDto.ChapterId, progressDto.PageNum));
return true;
}
}

View file

@ -151,26 +151,22 @@ public class SeriesService : ISeriesService
UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allPeople,
HandleAddPerson, () => series.Metadata.CoverArtistLocked = true);
if (!updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked) series.Metadata.AgeRatingLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked) series.Metadata.PublicationStatusLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.LanguageLocked) series.Metadata.LanguageLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.GenresLocked) series.Metadata.GenresLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.TagsLocked) series.Metadata.TagsLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.CharacterLocked) series.Metadata.CharacterLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.ColoristLocked) series.Metadata.ColoristLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.EditorLocked) series.Metadata.EditorLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.InkerLocked) series.Metadata.InkerLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.LettererLocked) series.Metadata.LettererLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.PencillerLocked) series.Metadata.PencillerLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.PublisherLocked) series.Metadata.PublisherLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked) series.Metadata.TranslatorLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked) series.Metadata.CoverArtistLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.WriterLocked) series.Metadata.WriterLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.SummaryLocked) series.Metadata.SummaryLocked = false;
series.Metadata.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked;
series.Metadata.PublicationStatusLocked = updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked;
series.Metadata.LanguageLocked = updateSeriesMetadataDto.SeriesMetadata.LanguageLocked;
series.Metadata.GenresLocked = updateSeriesMetadataDto.SeriesMetadata.GenresLocked;
series.Metadata.TagsLocked = updateSeriesMetadataDto.SeriesMetadata.TagsLocked;
series.Metadata.CharacterLocked = updateSeriesMetadataDto.SeriesMetadata.CharacterLocked;
series.Metadata.ColoristLocked = updateSeriesMetadataDto.SeriesMetadata.ColoristLocked;
series.Metadata.EditorLocked = updateSeriesMetadataDto.SeriesMetadata.EditorLocked;
series.Metadata.InkerLocked = updateSeriesMetadataDto.SeriesMetadata.InkerLocked;
series.Metadata.LettererLocked = updateSeriesMetadataDto.SeriesMetadata.LettererLocked;
series.Metadata.PencillerLocked = updateSeriesMetadataDto.SeriesMetadata.PencillerLocked;
series.Metadata.PublisherLocked = updateSeriesMetadataDto.SeriesMetadata.PublisherLocked;
series.Metadata.TranslatorLocked = updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked;
series.Metadata.CoverArtistLocked = updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked;
series.Metadata.WriterLocked = updateSeriesMetadataDto.SeriesMetadata.WriterLocked;
series.Metadata.SummaryLocked = updateSeriesMetadataDto.SeriesMetadata.SummaryLocked;
if (!_unitOfWork.HasChanges())
{
@ -491,10 +487,10 @@ public class SeriesService : ISeriesService
foreach (var chapter in chapters)
{
chapter.Title = FormatChapterTitle(chapter, libraryType);
if (chapter.IsSpecial)
{
specials.Add(chapter);
}
if (!chapter.IsSpecial) continue;
if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName;
specials.Add(chapter);
}

View file

@ -64,6 +64,7 @@ namespace API.Services.Tasks
await DeleteChapterCoverImages();
await SendProgress(0.7F, "Cleaning deleted cover images");
await DeleteTagCoverImages();
await DeleteReadingListCoverImages();
await SendProgress(0.8F, "Cleaning deleted cover images");
await SendProgress(1F, "Cleanup finished");
_logger.LogInformation("Cleanup finished");
@ -116,6 +117,16 @@ namespace API.Services.Tasks
_directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file))));
}
/// <summary>
/// Removes all reading list images that are not in the database. They must follow <see cref="ImageService.ReadingListCoverImageRegex"/> filename pattern.
/// </summary>
public async Task DeleteReadingListCoverImages()
{
var images = await _unitOfWork.ReadingListRepository.GetAllCoverImagesAsync();
var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.ReadingListCoverImageRegex);
_directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file))));
}
/// <summary>
/// Removes all files and directories in the cache directory
/// </summary>

View file

@ -7,6 +7,14 @@ using API.Entities;
namespace API.SignalR
{
public static class MessageFactoryEntityTypes
{
public const string Series = "series";
public const string Volume = "volume";
public const string Chapter = "chapter";
public const string CollectionTag = "collection";
public const string ReadingList = "readingList";
}
public static class MessageFactory
{
/// <summary>
@ -78,6 +86,11 @@ namespace API.SignalR
/// When a library is created/deleted in the Server
/// </summary>
public const string LibraryModified = "LibraryModified";
/// <summary>
/// A user's progress was modified
/// </summary>
public const string UserProgressUpdate = "UserProgressUpdate";
public static SignalRMessage ScanSeriesEvent(int libraryId, int seriesId, string seriesName)
@ -320,6 +333,25 @@ namespace API.SignalR
};
}
public static SignalRMessage UserProgressUpdateEvent(int userId, string username, int seriesId, int volumeId, int chapterId, int pagesRead)
{
return new SignalRMessage()
{
Name = UserProgressUpdate,
Title = "Updating User Progress",
Progress = ProgressType.None,
Body = new
{
UserId = userId,
Username = username,
SeriesId = seriesId,
VolumeId = volumeId,
ChapterId = chapterId,
PagesRead = pagesRead,
}
};
}
public static SignalRMessage SiteThemeProgressEvent(string subtitle, string themeName, string eventType)
{
return new SignalRMessage()