Release Testing Day 3 (#1951)

* Code cleanup.

Fixed OPDS images missing api key.

Fixed theme color on site manifest not being black.

* Removed a console.log from timeago pipe

* Reading list page is now alphabetical and the modal for adding to a reading list is ordered by most recent.

* Fixed a bug where remove read from reading list failed due to Calculating Start and End date assuming chapter would always be there.

* Fixed a bug where reading list cover would get reset when editing the reading list.

* Fixed a bug where reading list item didn't have not read badge. It's on old style.

* Fixed a bug where user-preferences was hitting an admin only api when there was a better alternative

* Slight memory improvement on a common db call

* Fixed a bug where resetting to default theme when a theme was deleted was throwing an exception and failing.

* All Login dtos now have the active KavitaVersion to make external apps able to handle what version of the API they are connecting with.

* Fixed up a case where getVolume repo method always assumed there was a volume by that Id.
This commit is contained in:
Joe Milazzo 2023-05-02 16:02:33 -05:00 committed by GitHub
parent 875931b162
commit faf58e6985
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 139 additions and 158 deletions

View File

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

View File

@ -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<IUnitOfWork>(), Substitute.For<ILogger<ProcessSeries>>(),
Substitute.For<IEventHub>(), Substitute.For<IDirectoryService>()
, Substitute.For<ICacheHelper>(), Substitute.For<IReadingItemService>(), Substitute.For<IFileService>(),
Substitute.For<IMetadataService>(),
Substitute.For<IWordCountAnalyzerService>(),
Substitute.For<ICollectionTagService>(), Substitute.For<IReadingListService>());
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<IUnitOfWork>(), Substitute.For<ILogger<ProcessSeries>>(),
// Substitute.For<IEventHub>(), Substitute.For<IDirectoryService>()
// , Substitute.For<ICacheHelper>(), Substitute.For<IReadingItemService>(), Substitute.For<IFileService>(),
// Substitute.For<IMetadataService>(),
// Substitute.For<IWordCountAnalyzerService>(),
// Substitute.For<ICollectionTagService>(), Substitute.For<IReadingListService>());
//
// ps.UpdateChapterFromComicInfo(chapter, new ComicInfo()
// {
//
// });
// }
#endregion
}

View File

@ -150,7 +150,8 @@ public class AccountController : BaseApiController
Token = await _tokenService.CreateToken(user),
RefreshToken = await _tokenService.CreateRefreshToken(user),
ApiKey = user.ApiKey,
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
Preferences = _mapper.Map<UserPreferencesDto>(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<UserDto>(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<UserPreferencesDto>(user.UserPreferences)
Preferences = _mapper.Map<UserPreferencesDto>(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<UserPreferencesDto>(user.UserPreferences)
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences),
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
};
}

View File

@ -231,8 +231,8 @@ public class OpdsController : BaseApiController
Links = new List<FeedLink>()
{
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<FeedLink>()
{
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<FeedLink>()
{
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<FeedLink>()
{
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)

View File

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

View File

@ -47,13 +47,15 @@ public class ReadingListController : BaseApiController
/// </summary>
/// <param name="includePromoted">Include Promoted Reading Lists along with user's Reading Lists. Defaults to true</param>
/// <param name="userParams">Pagination parameters</param>
/// <param name="sortByLastModified">Sort by last modified (most recent first) or by title (alphabetical)</param>
/// <returns></returns>
[HttpPost("lists")]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, bool includePromoted = true)
public async Task<ActionResult<IEnumerable<ReadingListDto>>> 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);

View File

@ -111,7 +111,7 @@ public class SeriesController : BaseApiController
}
[HttpGet("volume")]
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
public async Task<ActionResult<VolumeDto?>> GetVolume(int volumeId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId));

View File

@ -53,19 +53,6 @@ public class ServerController : BaseApiController
_taskScheduler = taskScheduler;
}
/// <summary>
/// Attempts to Restart the server. Does not work, will shutdown the instance.
/// </summary>
/// <returns></returns>
[HttpPost("restart")]
public ActionResult RestartServer()
{
_logger.LogInformation("{UserName} is restarting server from admin dashboard", User.GetUsername());
_applicationLifetime.StopApplication();
return Ok();
}
/// <summary>
/// Performs an ad-hoc cleanup of Cache
/// </summary>

View File

@ -45,7 +45,6 @@ public class SettingsController : BaseApiController
_libraryWatcher = libraryWatcher;
}
[AllowAnonymous]
[HttpGet("base-url")]
public async Task<ActionResult<string>> GetBaseUrl()
{

View File

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

View File

@ -27,7 +27,7 @@ public enum ReadingListIncludes
public interface IReadingListRepository
{
Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams);
Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true);
Task<ReadingList?> GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None);
Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId);
Task<ReadingListDto?> GetReadingListDtoByIdAsync(int readingListId, int userId);
@ -166,17 +166,18 @@ public class ReadingListRepository : IReadingListRepository
}
public async Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams)
public async Task<PagedList<ReadingListDto>> 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<ReadingListDto>(_mapper.ConfigurationProvider)
.Where(l => l.AgeRating >= userAgeRating);
query = sortByLastModified ? query.OrderByDescending(l => l.LastModified) : query.OrderBy(l => l.NormalizedTitle);
var finalQuery = query.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.AsNoTracking();
return await PagedList<ReadingListDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
return await PagedList<ReadingListDto>.CreateAsync(finalQuery, userParams.PageNumber, userParams.PageSize);
}
public async Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted)

View File

@ -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)
{

View File

@ -71,7 +71,7 @@ public class SiteThemeRepository : ISiteThemeRepository
{
var result = await _context.SiteTheme
.Where(t => t.IsDefault)
.SingleOrDefaultAsync();
.FirstOrDefaultAsync();
if (result == null)
{

View File

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

View File

@ -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<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds);
Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId);
Task<Volume?> GetVolumeAsync(int volumeId);
Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId);
Task<VolumeDto?> GetVolumeDtoAsync(int volumeId, int userId);
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
Task<IEnumerable<Volume>> GetVolumes(int seriesId);
Task<Volume?> GetVolumeByIdAsync(int volumeId);
@ -119,7 +120,7 @@ public class VolumeRepository : IVolumeRepository
/// <param name="volumeId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId)
public async Task<VolumeDto?> 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<VolumeDto>(_mapper.ConfigurationProvider)
.SingleAsync(vol => vol.Id == volumeId);
.SingleOrDefaultAsync(vol => vol.Id == volumeId);
if (volume == null) return null;
var volumeList = new List<VolumeDto>() {volume};
await AddVolumeModifiers(userId, volumeList);

View File

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

View File

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

View File

@ -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(@"<script(.*)(/>)", RegexOptions.Compiled)]
// private static partial Regex StartingScriptTag();
// [GeneratedRegex(@"<title(.*)(/>)", RegexOptions.Compiled)]
// private static partial Regex StartingTitleTag();
public BookService(ILogger<BookService> 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)

View File

@ -76,23 +76,14 @@ public class DirectoryService : IDirectoryService
public string BookmarkDirectory { get; }
public string SiteThemeDirectory { get; }
private readonly ILogger<DirectoryService> _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");

View File

@ -260,7 +260,6 @@ public class ImageService : IImageService
public static string CreateMergedImage(List<string> 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);

View File

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

View File

@ -50,7 +50,7 @@ public interface IReadingListService
/// <summary>
/// Methods responsible for management of Reading Lists
/// </summary>
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess"/> to be called beforehand</remarks>
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, String)"/> to be called beforehand</remarks>
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
/// <summary>
/// Removes all entries that are fully read from the reading list. This commits
/// </summary>
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess"/> to be called beforehand</remarks>
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, String)"/> to be called beforehand</remarks>
/// <param name="readingListId">Reading List Id</param>
/// <param name="user">User</param>
/// <returns></returns>
@ -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;

View File

@ -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()

View File

@ -2,5 +2,5 @@
"TokenKey": "super secret unguessable key",
"Port": 5000,
"IpAddresses": "",
"BaseUrl": "/test/"
"BaseUrl": "/"
}

View File

@ -24,11 +24,12 @@ export class ReadingListService {
return this.httpClient.get<ReadingList>(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<PaginatedResult<ReadingList[]>>(this.baseUrl + 'readinglist/lists?includePromoted=' + includePromoted, {}, {observe: 'response', params}).pipe(
return this.httpClient.post<PaginatedResult<ReadingList[]>>(this.baseUrl + 'readinglist/lists?includePromoted=' + includePromoted
+ '&sortByLastModified=' + sortByLastModified, {}, {observe: 'response', params}).pipe(
map((response: any) => {
return this.utilityService.createPaginatedResult(response, new PaginatedResult<ReadingList[]>());
})

View File

@ -67,10 +67,6 @@ export class SeriesService {
return this.httpClient.get<Volume[]>(this.baseUrl + 'series/volumes?seriesId=' + seriesId);
}
getVolume(volumeId: number) {
return this.httpClient.get<Volume>(this.baseUrl + 'series/volume?volumeId=' + volumeId);
}
getChapter(chapterId: number) {
return this.httpClient.get<Chapter>(this.baseUrl + 'series/chapter?chapterId=' + chapterId);
}

View File

@ -26,6 +26,10 @@ export class SettingsService {
return this.http.get<ServerSettings>(this.baseUrl + 'settings');
}
getBaseUrl() {
return this.http.get<string>(this.baseUrl + 'settings/base-url', TextResonse);
}
updateServerSettings(model: ServerSettings) {
return this.http.post<ServerSettings>(this.baseUrl + 'settings', model);
}

View File

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

View File

@ -1,7 +1,9 @@
<div class="d-flex flex-row g-0 mb-2">
<div class="pe-2">
<app-image width="106px" maxHeight="125px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
<div class="not-read-badge" *ngIf="item.pagesRead === 0 && item.pagesTotal > 0"></div>
<ng-container *ngIf="item.pagesRead === 0 && item.pagesTotal > 0">
<div class="not-read-badge" ></div>
</ng-container>
<div class="progress-banner" *ngIf="item.pagesRead < item.pagesTotal && item.pagesTotal > 0 && item.pagesRead !== item.pagesTotal">
<p><ngb-progressbar type="primary" height="5px" [value]="item.pagesRead" [max]="item.pagesTotal"></ngb-progressbar></p>
</div>

View File

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

View File

@ -3,7 +3,7 @@
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
<span>Reading Lists</span>
</h2>
<h6 subtitle *ngIf="pagination">{{pagination.totalItems | number}} Items</h6>
<h6 subtitle class="subtitle-with-actionables" *ngIf="pagination">{{pagination.totalItems | number}} Items</h6>
</app-side-nav-companion-bar>
<app-card-detail-layout

View File

@ -97,7 +97,7 @@ export class ReadingListsComponent implements OnInit {
this.loadingLists = true;
this.cdRef.markForCheck();
this.readingListService.getReadingLists(true).pipe(take(1)).subscribe((readingLists: PaginatedResult<ReadingList[]>) => {
this.readingListService.getReadingLists(true, false).pipe(take(1)).subscribe((readingLists: PaginatedResult<ReadingList[]>) => {
this.lists = readingLists.result;
this.pagination = readingLists.pagination;
this.jumpbarKeys = this.jumpbarService.getJumpKeys(readingLists.result, (rl: ReadingList) => rl.title);

View File

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

View File

@ -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(),

View File

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

View File

@ -13,7 +13,7 @@
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"theme_color": "#000000",
"background_color": "#000000",
"display": "standalone"
}

View File

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