diff --git a/API.Benchmark/EpubBenchmark.cs b/API.Benchmark/EpubBenchmark.cs index 3dd5e36c1..1d47889b1 100644 --- a/API.Benchmark/EpubBenchmark.cs +++ b/API.Benchmark/EpubBenchmark.cs @@ -18,7 +18,7 @@ namespace API.Benchmark; public class EpubBenchmark { private const string FilePath = @"E:\Books\Invaders of the Rokujouma\Invaders of the Rokujouma - Volume 01.epub"; - private readonly Regex WordRegex = new Regex(@"\b\w+\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex _wordRegex = new Regex(@"\b\w+\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); [Benchmark] public async Task GetWordCount_PassByRef() @@ -100,6 +100,6 @@ public class EpubBenchmark return doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]") - .Sum(node => WordRegex.Matches(node.InnerText).Count); + .Sum(node => _wordRegex.Matches(node.InnerText).Count); } } diff --git a/API.Tests/Services/ProcessSeriesTests.cs b/API.Tests/Services/ProcessSeriesTests.cs index 75ddf8808..ef5c45007 100644 --- a/API.Tests/Services/ProcessSeriesTests.cs +++ b/API.Tests/Services/ProcessSeriesTests.cs @@ -45,28 +45,28 @@ public class ProcessSeriesTests #region UpdateChapterFromComicInfo - public void UpdateChapterFromComicInfo_() - { - // TODO: Do this - var file = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 1.cbz"); - // Chapter and ComicInfo - var chapter = new ChapterBuilder("1") - .WithId(0) - .WithFile(new MangaFileBuilder(file, MangaFormat.Archive).Build()) - .Build(); - - var ps = new ProcessSeries(Substitute.For(), Substitute.For>(), - Substitute.For(), Substitute.For() - , Substitute.For(), Substitute.For(), Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For(), Substitute.For()); - - ps.UpdateChapterFromComicInfo(chapter, new ComicInfo() - { - - }); - } + // public void UpdateChapterFromComicInfo_() + // { + // // TODO: Do this + // var file = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 1.cbz"); + // // Chapter and ComicInfo + // var chapter = new ChapterBuilder("1") + // .WithId(0) + // .WithFile(new MangaFileBuilder(file, MangaFormat.Archive).Build()) + // .Build(); + // + // var ps = new ProcessSeries(Substitute.For(), Substitute.For>(), + // Substitute.For(), Substitute.For() + // , Substitute.For(), Substitute.For(), Substitute.For(), + // Substitute.For(), + // Substitute.For(), + // Substitute.For(), Substitute.For()); + // + // ps.UpdateChapterFromComicInfo(chapter, new ComicInfo() + // { + // + // }); + // } #endregion } diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 33ab1a1e4..ea9159d1c 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -150,7 +150,8 @@ public class AccountController : BaseApiController Token = await _tokenService.CreateToken(user), RefreshToken = await _tokenService.CreateRefreshToken(user), ApiKey = user.ApiKey, - Preferences = _mapper.Map(user.UserPreferences) + Preferences = _mapper.Map(user.UserPreferences), + KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value }; } catch (Exception ex) @@ -213,6 +214,8 @@ public class AccountController : BaseApiController var dto = _mapper.Map(user); dto.Token = await _tokenService.CreateToken(user); dto.RefreshToken = await _tokenService.CreateRefreshToken(user); + dto.KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)) + .Value; var pref = await _unitOfWork.UserRepository.GetPreferencesAsync(user.UserName!); if (pref == null) return Ok(dto); @@ -687,7 +690,8 @@ public class AccountController : BaseApiController Token = await _tokenService.CreateToken(user), RefreshToken = await _tokenService.CreateRefreshToken(user), ApiKey = user.ApiKey, - Preferences = _mapper.Map(user.UserPreferences) + Preferences = _mapper.Map(user.UserPreferences), + KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value }; } @@ -840,7 +844,8 @@ public class AccountController : BaseApiController Token = await _tokenService.CreateToken(user), RefreshToken = await _tokenService.CreateRefreshToken(user), ApiKey = user.ApiKey, - Preferences = _mapper.Map(user.UserPreferences) + Preferences = _mapper.Map(user.UserPreferences), + KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value }; } diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 587549a10..9925bb417 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -231,8 +231,8 @@ public class OpdsController : BaseApiController Links = new List() { CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections/{tag.Id}"), - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionId={tag.Id}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionId={tag.Id}") + CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionId={tag.Id}&apiKey={apiKey}"), + CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionId={tag.Id}&apiKey={apiKey}") } }); } @@ -294,7 +294,8 @@ public class OpdsController : BaseApiController var (baseUrl, prefix) = await GetPrefix(); var userId = await GetUser(apiKey); - var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, true, GetUserParams(pageNumber)); + var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, + true, GetUserParams(pageNumber), false); var feed = CreateFeed("All Reading Lists", $"{prefix}{apiKey}/reading-list", apiKey, prefix, baseUrl); @@ -480,9 +481,9 @@ public class OpdsController : BaseApiController CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections/{collection.Id}"), CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, - $"{baseUrl}api/image/collection-cover?collectionId={collection.Id}"), + $"{baseUrl}api/image/collection-cover?collectionId={collection.Id}&apiKey={apiKey}"), CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, - $"{baseUrl}api/image/collection-cover?collectionId={collection.Id}") + $"{baseUrl}api/image/collection-cover?collectionId={collection.Id}&apiKey={apiKey}") } }); } @@ -546,7 +547,7 @@ public class OpdsController : BaseApiController var feed = CreateFeed(series.Name + " - Storyline", $"{prefix}{apiKey}/series/{series.Id}", apiKey, prefix, baseUrl); SetFeedId(feed, $"series-{series.Id}"); - feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesId}")); + feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesId}&apiKey={apiKey}")); var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId); foreach (var volume in seriesDetail.Volumes) @@ -737,8 +738,8 @@ public class OpdsController : BaseApiController Links = new List() { CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/series/{seriesDto.Id}"), - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesDto.Id}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesDto.Id}") + CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesDto.Id}&apiKey={apiKey}"), + CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesDto.Id}&apiKey={apiKey}") } }; } @@ -752,8 +753,8 @@ public class OpdsController : BaseApiController Links = new List() { CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/series/{searchResultDto.SeriesId}"), - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={searchResultDto.SeriesId}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={searchResultDto.SeriesId}") + CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={searchResultDto.SeriesId}&apiKey={apiKey}"), + CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={searchResultDto.SeriesId}&apiKey={apiKey}") } }; } @@ -770,9 +771,9 @@ public class OpdsController : BaseApiController CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}"), CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, - $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}"), + $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"), CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, - $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}") + $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}") } }; } @@ -788,9 +789,10 @@ public class OpdsController : BaseApiController var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, await GetUser(apiKey)); + var title = $"{series.Name}"; - if (volume.Chapters.Count == 1) + if (volume!.Chapters.Count == 1) { SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType); if (volume.Name != "0") @@ -823,8 +825,8 @@ public class OpdsController : BaseApiController Format = mangaFile.Format.ToString(), Links = new List() { - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}"), + CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"), + CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"), // We can't not include acc link in the feed, panels doesn't work with just page streaming option. We have to block download directly accLink, await CreatePageStreamLink(series.LibraryId, seriesId, volumeId, chapterId, mangaFile, apiKey, prefix) diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs index a03cb3014..8e1e6027f 100644 --- a/API/Controllers/PluginController.cs +++ b/API/Controllers/PluginController.cs @@ -2,7 +2,9 @@ using System.Threading.Tasks; using API.Data; using API.DTOs; +using API.Entities.Enums; using API.Services; +using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -45,6 +47,7 @@ public class PluginController : BaseApiController Token = await _tokenService.CreateToken(user), RefreshToken = await _tokenService.CreateRefreshToken(user), ApiKey = user.ApiKey, + KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value }; } } diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index e54745abb..060e54cd6 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -47,13 +47,15 @@ public class ReadingListController : BaseApiController /// /// Include Promoted Reading Lists along with user's Reading Lists. Defaults to true /// Pagination parameters + /// Sort by last modified (most recent first) or by title (alphabetical) /// [HttpPost("lists")] - public async Task>> GetListsForUser([FromQuery] UserParams userParams, bool includePromoted = true) + public async Task>> GetListsForUser([FromQuery] UserParams userParams, + bool includePromoted = true, bool sortByLastModified = false) { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted, - userParams); + userParams, sortByLastModified); Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages); return Ok(items); diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index ee5208ea8..c729e7017 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -111,7 +111,7 @@ public class SeriesController : BaseApiController } [HttpGet("volume")] - public async Task> GetVolume(int volumeId) + public async Task> GetVolume(int volumeId) { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId)); diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 7d1d33396..e8221e3fb 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -53,19 +53,6 @@ public class ServerController : BaseApiController _taskScheduler = taskScheduler; } - /// - /// Attempts to Restart the server. Does not work, will shutdown the instance. - /// - /// - [HttpPost("restart")] - public ActionResult RestartServer() - { - _logger.LogInformation("{UserName} is restarting server from admin dashboard", User.GetUsername()); - - _applicationLifetime.StopApplication(); - return Ok(); - } - /// /// Performs an ad-hoc cleanup of Cache /// diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index b1db6a8ee..11ec8de31 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -45,7 +45,6 @@ public class SettingsController : BaseApiController _libraryWatcher = libraryWatcher; } - [AllowAnonymous] [HttpGet("base-url")] public async Task> GetBaseUrl() { diff --git a/API/DTOs/UserDto.cs b/API/DTOs/UserDto.cs index aad61b83e..f63a021f1 100644 --- a/API/DTOs/UserDto.cs +++ b/API/DTOs/UserDto.cs @@ -12,4 +12,5 @@ public class UserDto public string? ApiKey { get; init; } public UserPreferencesDto? Preferences { get; set; } public AgeRestrictionDto? AgeRestriction { get; init; } + public string KavitaVersion { get; set; } } diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 7b64b7e3a..4b2a8b7ea 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -27,7 +27,7 @@ public enum ReadingListIncludes public interface IReadingListRepository { - Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams); + Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true); Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None); Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId); Task GetReadingListDtoByIdAsync(int readingListId, int userId); @@ -166,17 +166,18 @@ public class ReadingListRepository : IReadingListRepository } - public async Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams) + public async Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true) { var userAgeRating = (await _context.AppUser.SingleAsync(u => u.Id == userId)).AgeRestriction; var query = _context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) - .Where(l => l.AgeRating >= userAgeRating) - .OrderBy(l => l.LastModified) - .ProjectTo(_mapper.ConfigurationProvider) + .Where(l => l.AgeRating >= userAgeRating); + query = sortByLastModified ? query.OrderByDescending(l => l.LastModified) : query.OrderBy(l => l.NormalizedTitle); + + var finalQuery = query.ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking(); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(finalQuery, userParams.PageNumber, userParams.PageSize); } public async Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted) diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index b0c793b64..e611e841c 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -140,10 +140,8 @@ public class SeriesRepository : ISeriesRepository private readonly DataContext _context; private readonly IMapper _mapper; - - // [GeneratedRegex(@"\d{4}", RegexOptions.Compiled, 50000)] - // private static partial Regex YearRegex(); - private readonly Regex _yearRegex = new Regex(@"\d{4}", RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout); + private readonly Regex _yearRegex = new Regex(@"\d{4}", RegexOptions.Compiled, + Services.Tasks.Scanner.Parser.Parser.RegexTimeout); public SeriesRepository(DataContext context, IMapper mapper) { diff --git a/API/Data/Repositories/SiteThemeRepository.cs b/API/Data/Repositories/SiteThemeRepository.cs index f79a84355..b2c082183 100644 --- a/API/Data/Repositories/SiteThemeRepository.cs +++ b/API/Data/Repositories/SiteThemeRepository.cs @@ -71,7 +71,7 @@ public class SiteThemeRepository : ISiteThemeRepository { var result = await _context.SiteTheme .Where(t => t.IsDefault) - .SingleOrDefaultAsync(); + .FirstOrDefaultAsync(); if (result == null) { diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 8e58805d8..de6f041e6 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -337,7 +337,7 @@ public class UserRepository : IUserRepository return await _context.AppUser .Where(u => u.ApiKey != null && u.ApiKey.Equals(apiKey)) .Select(u => u.Id) - .SingleOrDefaultAsync(); + .FirstOrDefaultAsync(); } diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs index 23ebd45bc..833ea9055 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/API/Data/Repositories/VolumeRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using API.DTOs; @@ -21,7 +22,7 @@ public interface IVolumeRepository Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds); Task> GetVolumesDtoAsync(int seriesId, int userId); Task GetVolumeAsync(int volumeId); - Task GetVolumeDtoAsync(int volumeId, int userId); + Task GetVolumeDtoAsync(int volumeId, int userId); Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false); Task> GetVolumes(int seriesId); Task GetVolumeByIdAsync(int volumeId); @@ -119,7 +120,7 @@ public class VolumeRepository : IVolumeRepository /// /// /// - public async Task GetVolumeDtoAsync(int volumeId, int userId) + public async Task GetVolumeDtoAsync(int volumeId, int userId) { var volume = await _context.Volume .Where(vol => vol.Id == volumeId) @@ -127,7 +128,9 @@ public class VolumeRepository : IVolumeRepository .ThenInclude(c => c.Files) .AsSplitQuery() .ProjectTo(_mapper.ConfigurationProvider) - .SingleAsync(vol => vol.Id == volumeId); + .SingleOrDefaultAsync(vol => vol.Id == volumeId); + + if (volume == null) return null; var volumeList = new List() {volume}; await AddVolumeModifiers(userId, volumeList); diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index 08f0922b0..866382587 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -20,8 +20,6 @@ public static class RestrictByAgeExtensions return q.Where(s => s.Metadata.AgeRating != AgeRating.Unknown); } - //q.WhereIf(!restriction.IncludeUnknowns, s => s.Metadata.AgeRating != AgeRating.Unknown); - return q; } diff --git a/API/Extensions/StringExtensions.cs b/API/Extensions/StringExtensions.cs index 1aee307c3..9ce29eaec 100644 --- a/API/Extensions/StringExtensions.cs +++ b/API/Extensions/StringExtensions.cs @@ -4,9 +4,6 @@ namespace API.Extensions; public static class StringExtensions { - // Wait for Rosyln bugfix - // [GeneratedRegex(@"(^[a-z])|\.\s+(.)", RegexOptions.ExplicitCapture | RegexOptions.Compiled)] - // private static partial Regex SentenceCaseRegex(); private static readonly Regex SentenceCaseRegex = new Regex(@"(^[a-z])|\.\s+(.)", RegexOptions.ExplicitCapture | RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout); diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 7c99f6161..58bd80793 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -72,27 +72,6 @@ public class BookService : IBookService } }; - // Use when Rosyln fixed - // [GeneratedRegex(@"/\*[\d\D]*?\*/", RegexOptions.Compiled)] - // private static partial Regex CssComment(); - // - // [GeneratedRegex(@"[a-zA-Z]+#", RegexOptions.Compiled)] - // private static partial Regex WhiteSpace1(); - // [GeneratedRegex(@"[\n\r]+\s*", RegexOptions.Compiled)] - // private static partial Regex WhiteSpace2(); - // [GeneratedRegex(@"\s+", RegexOptions.Compiled)] - // private static partial Regex WhiteSpace3(); - // [GeneratedRegex(@"\s?([:,;{}])\s?", RegexOptions.Compiled)] - // private static partial Regex WhiteSpace4(); - // [GeneratedRegex(@"([\s:]0)(px|pt|%|em)", RegexOptions.Compiled)] - // private static partial Regex UnitPadding(); - // - // [GeneratedRegex(@")", RegexOptions.Compiled)] - // private static partial Regex StartingScriptTag(); - // [GeneratedRegex(@")", RegexOptions.Compiled)] - // private static partial Regex StartingTitleTag(); - - public BookService(ILogger logger, IDirectoryService directoryService, IImageService imageService) { _logger = logger; @@ -287,7 +266,6 @@ public class BookService : IBookService if (images == null) return; - var parent = images.First().ParentNode; foreach (var image in images) diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 4f0ee7b31..9608fc469 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -76,23 +76,14 @@ public class DirectoryService : IDirectoryService public string BookmarkDirectory { get; } public string SiteThemeDirectory { get; } private readonly ILogger _logger; - private const int RegexTimeoutMs = 5000000; private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase; - // [GeneratedRegex(@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle", - // MatchOptions, matchTimeoutMilliseconds: RegexTimeoutMs)] - // private static partial Regex ExcludeDirectoriesRegex(); - // - // [GeneratedRegex(@"\(\d+\)", - // MatchOptions, matchTimeoutMilliseconds: RegexTimeoutMs)] - // private static partial Regex FileCopyAppendRegex(); - private static readonly Regex ExcludeDirectories = new Regex( @"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb", - RegexOptions.Compiled | RegexOptions.IgnoreCase, + MatchOptions, Tasks.Scanner.Parser.Parser.RegexTimeout); private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)", - RegexOptions.Compiled | RegexOptions.IgnoreCase, + MatchOptions, Tasks.Scanner.Parser.Parser.RegexTimeout); public static readonly string BackupDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "backups"); diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 33c8f3bea..23d8b59f7 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -260,7 +260,6 @@ public class ImageService : IImageService public static string CreateMergedImage(List coverImages, string dest) { - // TODO: Needs testing // Currently this doesn't work due to non-standard cover image sizes and dimensions var image = Image.Black(320*4, 160*4); diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 1b1327426..d044575f8 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -273,7 +273,8 @@ public class ReaderService : IReaderService { 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)); + MessageFactory.UserProgressUpdateEvent(userId, user!.UserName!, progressDto.SeriesId, + progressDto.VolumeId, progressDto.ChapterId, progressDto.PageNum)); return true; } } diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index e26bc7bf9..76202d63b 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -50,7 +50,7 @@ public interface IReadingListService /// /// Methods responsible for management of Reading Lists /// -/// If called from API layer, expected for to be called beforehand +/// If called from API layer, expected for to be called beforehand public class ReadingListService : IReadingListService { private readonly IUnitOfWork _unitOfWork; @@ -152,7 +152,7 @@ public class ReadingListService : IReadingListService readingList.Summary = dto.Summary; readingList.Title = dto.Title.Trim(); - readingList.NormalizedTitle = Tasks.Scanner.Parser.Parser.Normalize(readingList.Title); + readingList.NormalizedTitle = Parser.Normalize(readingList.Title); readingList.Promoted = dto.Promoted; readingList.CoverImageLocked = dto.CoverImageLocked; @@ -193,7 +193,7 @@ public class ReadingListService : IReadingListService /// /// Removes all entries that are fully read from the reading list. This commits /// - /// If called from API layer, expected for to be called beforehand + /// If called from API layer, expected for to be called beforehand /// Reading List Id /// User /// @@ -203,8 +203,9 @@ public class ReadingListService : IReadingListService items = await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(user.Id, items.ToList()); // Collect all Ids to remove - var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id); + var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id).ToList(); + if (!itemIdsToRemove.Any()) return true; try { var listItems = @@ -218,7 +219,6 @@ public class ReadingListService : IReadingListService await CalculateStartAndEndDates(readingList); if (!_unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); } catch @@ -313,8 +313,8 @@ public class ReadingListService : IReadingListService _logger.LogError("Tried to calculate release dates for Reading List, but missing Chapter entities"); return; } - var maxReleaseDate = items.Max(item => item.Chapter.ReleaseDate); - var minReleaseDate = items.Min(item => item.Chapter.ReleaseDate); + var maxReleaseDate = items.Where(item => item.Chapter != null).Max(item => item.Chapter.ReleaseDate); + var minReleaseDate = items.Where(item => item.Chapter != null).Min(item => item.Chapter.ReleaseDate); if (maxReleaseDate != DateTime.MinValue) { readingListWithItems.EndingMonth = maxReleaseDate.Month; diff --git a/API/Services/TachiyomiService.cs b/API/Services/TachiyomiService.cs index 13e2fae3a..ea7da471f 100644 --- a/API/Services/TachiyomiService.cs +++ b/API/Services/TachiyomiService.cs @@ -97,7 +97,7 @@ public class TachiyomiService : ITachiyomiService var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId); // We only encode for single-file volumes - if (volumeWithProgress.Number != 0 && volumeWithProgress.Chapters.Count == 1) + if (volumeWithProgress!.Number != 0 && volumeWithProgress.Chapters.Count == 1) { // The progress is on a volume, encode it as a fake chapterDTO return new ChapterDto() diff --git a/API/config/appsettings.Development.json b/API/config/appsettings.Development.json index cbf24d86b..2a9bbdd2c 100644 --- a/API/config/appsettings.Development.json +++ b/API/config/appsettings.Development.json @@ -2,5 +2,5 @@ "TokenKey": "super secret unguessable key", "Port": 5000, "IpAddresses": "", - "BaseUrl": "/test/" + "BaseUrl": "/" } \ No newline at end of file diff --git a/UI/Web/src/app/_services/reading-list.service.ts b/UI/Web/src/app/_services/reading-list.service.ts index b7d9761dc..51cf74e1e 100644 --- a/UI/Web/src/app/_services/reading-list.service.ts +++ b/UI/Web/src/app/_services/reading-list.service.ts @@ -24,11 +24,12 @@ export class ReadingListService { return this.httpClient.get(this.baseUrl + 'readinglist?readingListId=' + readingListId); } - getReadingLists(includePromoted: boolean = true, pageNum?: number, itemsPerPage?: number) { + getReadingLists(includePromoted: boolean = true, sortByLastModified: boolean = false, pageNum?: number, itemsPerPage?: number) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - return this.httpClient.post>(this.baseUrl + 'readinglist/lists?includePromoted=' + includePromoted, {}, {observe: 'response', params}).pipe( + return this.httpClient.post>(this.baseUrl + 'readinglist/lists?includePromoted=' + includePromoted + + '&sortByLastModified=' + sortByLastModified, {}, {observe: 'response', params}).pipe( map((response: any) => { return this.utilityService.createPaginatedResult(response, new PaginatedResult()); }) diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 680688cff..733a8b2f1 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -67,10 +67,6 @@ export class SeriesService { return this.httpClient.get(this.baseUrl + 'series/volumes?seriesId=' + seriesId); } - getVolume(volumeId: number) { - return this.httpClient.get(this.baseUrl + 'series/volume?volumeId=' + volumeId); - } - getChapter(chapterId: number) { return this.httpClient.get(this.baseUrl + 'series/chapter?chapterId=' + chapterId); } diff --git a/UI/Web/src/app/admin/settings.service.ts b/UI/Web/src/app/admin/settings.service.ts index a6fe0c9c6..558eade38 100644 --- a/UI/Web/src/app/admin/settings.service.ts +++ b/UI/Web/src/app/admin/settings.service.ts @@ -26,6 +26,10 @@ export class SettingsService { return this.http.get(this.baseUrl + 'settings'); } + getBaseUrl() { + return this.http.get(this.baseUrl + 'settings/base-url', TextResonse); + } + updateServerSettings(model: ServerSettings) { return this.http.post(this.baseUrl + 'settings', model); } diff --git a/UI/Web/src/app/pipe/time-ago.pipe.ts b/UI/Web/src/app/pipe/time-ago.pipe.ts index d44a483e8..fe8aa4eb6 100644 --- a/UI/Web/src/app/pipe/time-ago.pipe.ts +++ b/UI/Web/src/app/pipe/time-ago.pipe.ts @@ -34,11 +34,10 @@ and modified export class TimeAgoPipe implements PipeTransform, OnDestroy { private timer: number | null = null; - constructor(private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone) {} + constructor(private readonly changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone) {} transform(value: string) { this.removeTimer(); const d = new Date(value); - console.log('date: ', d); const now = new Date(); const seconds = Math.round(Math.abs((now.getTime() - d.getTime()) / 1000)); const timeToUpdate = (Number.isNaN(seconds)) ? 1000 : this.getSecondsUntilUpdate(seconds) * 1000; diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html index 1fa7a21ce..e83fc8bc9 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html @@ -1,10 +1,12 @@
-
-
-

-
+ +
+
+
+

+
diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.scss b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.scss index 297f9bb78..f2038b729 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.scss +++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.scss @@ -1,3 +1,5 @@ +$image-height: 125px; + .progress-banner { height: 5px; @@ -13,10 +15,20 @@ position: relative; } -.not-read-badge { +.badge-container { + border-radius: 4px; + display: block; + height: $image-height; + overflow: hidden; + pointer-events: none; position: absolute; - top: 8px; - left: 108px; + width: 106px; +} + +.not-read-badge { + position: relative; + top: -125px; + left: 78px; width: 0; height: 0; border-style: solid; diff --git a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html index faa8333cd..a796d1631 100644 --- a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html @@ -3,7 +3,7 @@ Reading Lists -
{{pagination.totalItems | number}} Items
+
{{pagination.totalItems | number}} Items
) => { + this.readingListService.getReadingLists(true, false).pipe(take(1)).subscribe((readingLists: PaginatedResult) => { this.lists = readingLists.result; this.pagination = readingLists.pagination; this.jumpbarKeys = this.jumpbarService.getJumpKeys(readingLists.result, (rl: ReadingList) => rl.title); diff --git a/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.ts b/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.ts index fc8212d3e..af84420b9 100644 --- a/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.ts +++ b/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.ts @@ -73,7 +73,7 @@ export class AddToListModalComponent implements OnInit, AfterViewInit { this.listForm.addControl('filterQuery', new FormControl('', [])); this.loading = true; - this.readingListService.getReadingLists(false).subscribe(lists => { + this.readingListService.getReadingLists(false, true).subscribe(lists => { this.lists = lists.result; this.loading = false; }); diff --git a/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.ts b/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.ts index ee625c2ed..f7b0f2252 100644 --- a/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.ts +++ b/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.ts @@ -55,6 +55,8 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy { endingYear: new FormControl(this.readingList.endingYear, { nonNullable: true, validators: [Validators.min(1000)] }), }); + this.coverImageLocked = this.readingList.coverImageLocked; + this.reviewGroup.get('title')?.valueChanges.pipe( debounceTime(100), distinctUntilChanged(), diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index 56686100c..ce43fe363 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -107,7 +107,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.cdRef.markForCheck(); }); - this.settingsService.getServerSettings().subscribe(settings => this.baseUrl = settings.baseUrl); + this.settingsService.getBaseUrl().subscribe(url => this.baseUrl = url); this.settingsService.getOpdsEnabled().subscribe(res => { this.opdsEnabled = res; diff --git a/UI/Web/src/site.webmanifest b/UI/Web/src/site.webmanifest index a77109e84..4bd350739 100644 --- a/UI/Web/src/site.webmanifest +++ b/UI/Web/src/site.webmanifest @@ -13,7 +13,7 @@ "type": "image/png" } ], - "theme_color": "#ffffff", - "background_color": "#ffffff", + "theme_color": "#000000", + "background_color": "#000000", "display": "standalone" } diff --git a/openapi.json b/openapi.json index 8e9dd72f5..a9ff04c97 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.1.43" + "version": "0.7.1.44" }, "servers": [ { @@ -5007,6 +5007,15 @@ "type": "boolean", "default": true } + }, + { + "name": "sortByLastModified", + "in": "query", + "description": "Sort by last modified (most recent first) or by title (alphabetical)", + "schema": { + "type": "boolean", + "default": false + } } ], "responses": { @@ -7369,19 +7378,6 @@ } } }, - "/api/Server/restart": { - "post": { - "tags": [ - "Server" - ], - "summary": "Attempts to Restart the server. Does not work, will shutdown the instance.", - "responses": { - "200": { - "description": "Success" - } - } - } - }, "/api/Server/clear-cache": { "post": { "tags": [ @@ -15172,6 +15168,10 @@ }, "ageRestriction": { "$ref": "#/components/schemas/AgeRestrictionDto" + }, + "kavitaVersion": { + "type": "string", + "nullable": true } }, "additionalProperties": false