UX Overhaul Part 1 (#3047)
Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
parent
5934d516f3
commit
ff79710ac6
324 changed files with 11589 additions and 4598 deletions
|
@ -55,8 +55,8 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||
<PackageReference Include="MailKit" Version="4.7.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PackageReference Include="MailKit" Version="4.7.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@ -70,14 +70,14 @@
|
|||
<PackageReference Include="Hangfire.InMemory" Version="0.10.3" />
|
||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.2" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.61" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.62" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.14" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||
|
@ -85,26 +85,26 @@
|
|||
<PackageReference Include="NetVips" Version="2.4.1" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.15.2" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.2.1" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
||||
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.37.2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.28.0.94264">
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.31.0.96804">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.2" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="21.0.22" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.6" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="21.0.29" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.7" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
64
API/Controllers/ColorScapeController.cs
Normal file
64
API/Controllers/ColorScapeController.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Theme;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class ColorScapeController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public ColorScapeController(IUnitOfWork unitOfWork)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the color scape for a series
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series")]
|
||||
public async Task<ActionResult<ColorScapeDto>> GetColorScapeForSeries(int id)
|
||||
{
|
||||
var entity = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(id, User.GetUserId());
|
||||
return GetColorSpaceDto(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the color scape for a volume
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("volume")]
|
||||
public async Task<ActionResult<ColorScapeDto>> GetColorScapeForVolume(int id)
|
||||
{
|
||||
var entity = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(id, User.GetUserId());
|
||||
return GetColorSpaceDto(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the color scape for a chapter
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult<ColorScapeDto>> GetColorScapeForChapter(int id)
|
||||
{
|
||||
var entity = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(id);
|
||||
return GetColorSpaceDto(entity);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private ActionResult<ColorScapeDto> GetColorSpaceDto(IHasCoverImage entity)
|
||||
{
|
||||
if (entity == null) return Ok(ColorScapeDto.Empty);
|
||||
return Ok(new ColorScapeDto(entity.PrimaryColor, entity.SecondaryColor));
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ using API.DTOs.Device;
|
|||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -24,20 +25,27 @@ public class DeviceController : BaseApiController
|
|||
private readonly IEmailService _emailService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService,
|
||||
IEmailService emailService, IEventHub eventHub, ILocalizationService localizationService)
|
||||
IEmailService emailService, IEventHub eventHub, ILocalizationService localizationService, IMapper mapper)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_deviceService = deviceService;
|
||||
_emailService = emailService;
|
||||
_eventHub = eventHub;
|
||||
_localizationService = localizationService;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Device
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("create")]
|
||||
public async Task<ActionResult> CreateOrUpdateDevice(CreateDeviceDto dto)
|
||||
public async Task<ActionResult<DeviceDto>> CreateOrUpdateDevice(CreateDeviceDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||
if (user == null) return Unauthorized();
|
||||
|
@ -46,20 +54,22 @@ public class DeviceController : BaseApiController
|
|||
var device = await _deviceService.Create(dto, user);
|
||||
if (device == null)
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-create"));
|
||||
|
||||
return Ok(_mapper.Map<DeviceDto>(device));
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing Device
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateDevice(UpdateDeviceDto dto)
|
||||
public async Task<ActionResult<DeviceDto>> UpdateDevice(UpdateDeviceDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||
if (user == null) return Unauthorized();
|
||||
|
@ -67,7 +77,7 @@ public class DeviceController : BaseApiController
|
|||
|
||||
if (device == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-update"));
|
||||
|
||||
return Ok();
|
||||
return Ok(_mapper.Map<DeviceDto>(device));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -25,15 +25,18 @@ public class ImageController : BaseApiController
|
|||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IReadingListService _readingListService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService,
|
||||
IImageService imageService, ILocalizationService localizationService)
|
||||
IImageService imageService, ILocalizationService localizationService,
|
||||
IReadingListService readingListService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_directoryService = directoryService;
|
||||
_imageService = imageService;
|
||||
_localizationService = localizationService;
|
||||
_readingListService = readingListService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -42,7 +45,7 @@ public class ImageController : BaseApiController
|
|||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId", "apiKey"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["chapterId", "apiKey"])]
|
||||
public async Task<ActionResult> GetChapterCoverImage(int chapterId, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
|
@ -60,7 +63,7 @@ public class ImageController : BaseApiController
|
|||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("library-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"libraryId", "apiKey"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["libraryId", "apiKey"])]
|
||||
public async Task<ActionResult> GetLibraryCoverImage(int libraryId, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
|
@ -78,7 +81,7 @@ public class ImageController : BaseApiController
|
|||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("volume-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"volumeId", "apiKey"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["volumeId", "apiKey"])]
|
||||
public async Task<ActionResult> GetVolumeCoverImage(int volumeId, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
|
@ -95,7 +98,7 @@ public class ImageController : BaseApiController
|
|||
/// </summary>
|
||||
/// <param name="seriesId">Id of Series</param>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"seriesId", "apiKey"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["seriesId", "apiKey"])]
|
||||
[HttpGet("series-cover")]
|
||||
public async Task<ActionResult> GetSeriesCoverImage(int seriesId, string apiKey)
|
||||
{
|
||||
|
@ -116,7 +119,7 @@ public class ImageController : BaseApiController
|
|||
/// <param name="collectionTagId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("collection-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"collectionTagId", "apiKey"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["collectionTagId", "apiKey"])]
|
||||
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
|
@ -141,15 +144,17 @@ public class ImageController : BaseApiController
|
|||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("readinglist-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"readingListId", "apiKey"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["readingListId", "apiKey"])]
|
||||
public async Task<ActionResult> GetReadingListCoverImage(int readingListId, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
|
||||
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
{
|
||||
var destFile = await GenerateReadingListCoverImage(readingListId);
|
||||
var destFile = await _readingListService.GenerateReadingListCoverImage(readingListId);
|
||||
if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
|
||||
return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), _directoryService.FileSystem.Path.GetFileName(destFile));
|
||||
}
|
||||
|
@ -158,22 +163,6 @@ public class ImageController : BaseApiController
|
|||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
private async Task<string> GenerateReadingListCoverImage(int readingListId)
|
||||
{
|
||||
var covers = await _unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId);
|
||||
var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory,
|
||||
ImageService.GetReadingListFormat(readingListId));
|
||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
destFile += settings.EncodeMediaAs.GetExtension();
|
||||
|
||||
if (_directoryService.FileSystem.File.Exists(destFile)) return destFile;
|
||||
ImageService.CreateMergedImage(
|
||||
covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(),
|
||||
settings.CoverImageSize,
|
||||
destFile);
|
||||
return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateCollectionCoverImage(int collectionId)
|
||||
{
|
||||
var covers = await _unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId);
|
||||
|
@ -186,6 +175,7 @@ public class ImageController : BaseApiController
|
|||
covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(),
|
||||
settings.CoverImageSize,
|
||||
destFile);
|
||||
// TODO: Refactor this so that collections have a dedicated cover image so we can calculate primary/secondary colors
|
||||
return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile;
|
||||
}
|
||||
|
||||
|
@ -198,7 +188,8 @@ public class ImageController : BaseApiController
|
|||
/// <param name="apiKey">API Key for user. Needed to authenticate request</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("bookmark")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId", "pageNum", "apiKey"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["chapterId", "pageNum", "apiKey"
|
||||
])]
|
||||
public async Task<ActionResult> GetBookmarkImage(int chapterId, int pageNum, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
|
@ -220,7 +211,7 @@ public class ImageController : BaseApiController
|
|||
/// <param name="apiKey"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("web-link")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = new []{"url", "apiKey"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["url", "apiKey"])]
|
||||
public async Task<ActionResult> GetWebLinkImage(string url, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
|
@ -258,7 +249,7 @@ public class ImageController : BaseApiController
|
|||
/// <returns></returns>
|
||||
[Authorize(Policy="RequireAdminRole")]
|
||||
[HttpGet("cover-upload")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"filename", "apiKey"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["filename", "apiKey"])]
|
||||
public async Task<ActionResult> GetCoverUploadImage(string filename, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
|
|
|
@ -471,6 +471,7 @@ public class OpdsController : BaseApiController
|
|||
var feed = CreateFeed(await _localizationService.Translate(userId, "collections"), $"{apiKey}/collections", apiKey, prefix);
|
||||
SetFeedId(feed, "collections");
|
||||
|
||||
|
||||
feed.Entries.AddRange(tags.Select(tag => new FeedEntry()
|
||||
{
|
||||
Id = tag.Id.ToString(),
|
||||
|
@ -539,6 +540,8 @@ public class OpdsController : BaseApiController
|
|||
|
||||
var feed = CreateFeed("All Reading Lists", $"{apiKey}/reading-list", apiKey, prefix);
|
||||
SetFeedId(feed, "reading-list");
|
||||
AddPagination(feed, readingLists, $"{prefix}{apiKey}/reading-list/");
|
||||
|
||||
foreach (var readingListDto in readingLists)
|
||||
{
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
|
@ -555,6 +558,7 @@ public class OpdsController : BaseApiController
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
|
@ -1014,7 +1018,7 @@ public class OpdsController : BaseApiController
|
|||
};
|
||||
}
|
||||
|
||||
private static void AddPagination(Feed feed, PagedList<SeriesDto> list, string href)
|
||||
private static void AddPagination<T>(Feed feed, PagedList<T> list, string href)
|
||||
{
|
||||
var url = href;
|
||||
if (href.Contains('?'))
|
||||
|
|
|
@ -748,7 +748,7 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return new UnauthorizedResult();
|
||||
if (user.Bookmarks.IsNullOrEmpty()) return Ok();
|
||||
if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok();
|
||||
|
||||
if (!await _accountService.HasBookmarkPermission(user))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-permission"));
|
||||
|
|
|
@ -210,9 +210,13 @@ public class ServerController : BaseApiController
|
|||
/// Pull the Changelog for Kavita from Github and display
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("changelog")]
|
||||
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
|
||||
{
|
||||
// Strange bug where [Authorize] doesn't work
|
||||
if (User.GetUserId() == 0) return Unauthorized();
|
||||
|
||||
return Ok(await _versionUpdaterService.GetAllReleases());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
|
@ -109,6 +108,7 @@ public class UploadController : BaseApiController
|
|||
{
|
||||
series.CoverImage = filePath;
|
||||
series.CoverImageLocked = true;
|
||||
_imageService.UpdateColorScape(series);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
|
||||
|
@ -157,6 +157,7 @@ public class UploadController : BaseApiController
|
|||
{
|
||||
tag.CoverImage = filePath;
|
||||
tag.CoverImageLocked = true;
|
||||
_imageService.UpdateColorScape(tag);
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
}
|
||||
|
||||
|
@ -208,6 +209,7 @@ public class UploadController : BaseApiController
|
|||
{
|
||||
readingList.CoverImage = filePath;
|
||||
readingList.CoverImageLocked = true;
|
||||
_imageService.UpdateColorScape(readingList);
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
|
@ -327,15 +329,18 @@ public class UploadController : BaseApiController
|
|||
{
|
||||
chapter.CoverImage = filePath;
|
||||
chapter.CoverImageLocked = true;
|
||||
_imageService.UpdateColorScape(chapter);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
_imageService.UpdateColorScape(volume);
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
|
@ -391,6 +396,7 @@ public class UploadController : BaseApiController
|
|||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
library.CoverImage = filePath;
|
||||
_imageService.UpdateColorScape(library);
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
}
|
||||
|
||||
|
@ -426,12 +432,15 @@ public class UploadController : BaseApiController
|
|||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var originalFile = chapter.CoverImage;
|
||||
|
||||
chapter.CoverImage = string.Empty;
|
||||
chapter.CoverImageLocked = false;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
|
||||
var volume = (await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId))!;
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
|
||||
var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!;
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
|
@ -451,7 +460,4 @@ public class UploadController : BaseApiController
|
|||
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reset-chapter-lock"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace API.DTOs;
|
|||
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
|
||||
/// file (abstracted from type).
|
||||
/// </summary>
|
||||
public class ChapterDto : IHasReadTimeEstimate
|
||||
public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage
|
||||
{
|
||||
public int Id { get; init; }
|
||||
/// <summary>
|
||||
|
@ -159,4 +159,8 @@ public class ChapterDto : IHasReadTimeEstimate
|
|||
public int TotalCount { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
public string CoverImage { get; set; }
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
using System;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Services.Plus;
|
||||
|
||||
namespace API.DTOs.Collection;
|
||||
#nullable enable
|
||||
|
||||
public class AppUserCollectionDto
|
||||
public class AppUserCollectionDto : IHasCoverImage
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Title { get; set; } = default!;
|
||||
|
@ -17,6 +18,9 @@ public class AppUserCollectionDto
|
|||
/// This is used to tell the UI if it should request a Cover Image or not. If null or empty, it has not been set.
|
||||
/// </summary>
|
||||
public string? CoverImage { get; set; } = string.Empty;
|
||||
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
10
API/DTOs/ColorScape.cs
Normal file
10
API/DTOs/ColorScape.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace API.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// A primary and secondary color
|
||||
/// </summary>
|
||||
public class ColorScape
|
||||
{
|
||||
public required string? Primary { get; set; }
|
||||
public required string? Secondary { get; set; }
|
||||
}
|
|
@ -20,4 +20,6 @@ public class MediaErrorDto
|
|||
/// Exception message
|
||||
/// </summary>
|
||||
public string Details { get; set; }
|
||||
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
using System;
|
||||
using API.Entities.Interfaces;
|
||||
|
||||
namespace API.DTOs.ReadingLists;
|
||||
#nullable enable
|
||||
|
||||
public class ReadingListDto
|
||||
public class ReadingListDto : IHasCoverImage
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Title { get; set; } = default!;
|
||||
|
@ -17,6 +18,10 @@ public class ReadingListDto
|
|||
/// This is used to tell the UI if it should request a Cover Image or not. If null or empty, it has not been set.
|
||||
/// </summary>
|
||||
public string? CoverImage { get; set; } = string.Empty;
|
||||
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum Year the Reading List starts
|
||||
/// </summary>
|
||||
|
|
|
@ -5,7 +5,7 @@ using API.Entities.Interfaces;
|
|||
namespace API.DTOs;
|
||||
#nullable enable
|
||||
|
||||
public class SeriesDto : IHasReadTimeEstimate
|
||||
public class SeriesDto : IHasReadTimeEstimate, IHasCoverImage
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string? Name { get; init; }
|
||||
|
@ -62,4 +62,8 @@ public class SeriesDto : IHasReadTimeEstimate
|
|||
/// The last time the folder for this series was scanned
|
||||
/// </summary>
|
||||
public DateTime LastFolderScanned { get; set; }
|
||||
|
||||
public string? CoverImage { get; set; }
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
}
|
||||
|
|
19
API/DTOs/Theme/ColorScapeDto.cs
Normal file
19
API/DTOs/Theme/ColorScapeDto.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace API.DTOs.Theme;
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// A set of colors for the color scape system in the UI
|
||||
/// </summary>
|
||||
public class ColorScapeDto
|
||||
{
|
||||
public string? Primary { get; set; }
|
||||
public string? Secondary { get; set; }
|
||||
|
||||
public ColorScapeDto(string? primary, string? secondary)
|
||||
{
|
||||
Primary = primary;
|
||||
Secondary = secondary;
|
||||
}
|
||||
|
||||
public static readonly ColorScapeDto Empty = new ColorScapeDto(null, null);
|
||||
}
|
|
@ -174,4 +174,6 @@ public class UserPreferencesDto
|
|||
/// </summary>
|
||||
[Required]
|
||||
public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None;
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ using API.Services.Tasks.Scanner.Parser;
|
|||
|
||||
namespace API.DTOs;
|
||||
|
||||
public class VolumeDto : IHasReadTimeEstimate
|
||||
public class VolumeDto : IHasReadTimeEstimate, IHasCoverImage
|
||||
{
|
||||
public int Id { get; set; }
|
||||
/// <inheritdoc cref="Volume.MinNumber"/>
|
||||
|
@ -62,4 +62,8 @@ public class VolumeDto : IHasReadTimeEstimate
|
|||
{
|
||||
return MinNumber.Is(Parser.SpecialVolumeNumber);
|
||||
}
|
||||
|
||||
public string CoverImage { get; set; }
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
}
|
||||
|
|
3079
API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs
generated
Normal file
3079
API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
138
API/Data/Migrations/20240808100353_CoverPrimaryColors.cs
Normal file
138
API/Data/Migrations/20240808100353_CoverPrimaryColors.cs
Normal file
|
@ -0,0 +1,138 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CoverPrimaryColors : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PrimaryColor",
|
||||
table: "Volume",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SecondaryColor",
|
||||
table: "Volume",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PrimaryColor",
|
||||
table: "Series",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SecondaryColor",
|
||||
table: "Series",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PrimaryColor",
|
||||
table: "ReadingList",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SecondaryColor",
|
||||
table: "ReadingList",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PrimaryColor",
|
||||
table: "Library",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SecondaryColor",
|
||||
table: "Library",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PrimaryColor",
|
||||
table: "Chapter",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SecondaryColor",
|
||||
table: "Chapter",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PrimaryColor",
|
||||
table: "AppUserCollection",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SecondaryColor",
|
||||
table: "AppUserCollection",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PrimaryColor",
|
||||
table: "Volume");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecondaryColor",
|
||||
table: "Volume");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PrimaryColor",
|
||||
table: "Series");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecondaryColor",
|
||||
table: "Series");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PrimaryColor",
|
||||
table: "ReadingList");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecondaryColor",
|
||||
table: "ReadingList");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PrimaryColor",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecondaryColor",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PrimaryColor",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecondaryColor",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PrimaryColor",
|
||||
table: "AppUserCollection");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecondaryColor",
|
||||
table: "AppUserCollection");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.4");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
|
@ -230,9 +230,15 @@ namespace API.Data.Migrations
|
|||
b.Property<string>("NormalizedTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PrimaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Promoted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecondaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Source")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -775,12 +781,18 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("Pages")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PrimaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Range")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ReleaseDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecondaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SeriesGroup")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -999,6 +1011,12 @@ namespace API.Data.Migrations
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PrimaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecondaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -1504,9 +1522,15 @@ namespace API.Data.Migrations
|
|||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PrimaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Promoted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecondaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("StartingMonth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -1794,6 +1818,12 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("Pages")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PrimaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecondaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SortName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -1989,6 +2019,12 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("Pages")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PrimaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecondaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ public class ReadingListRepository : IReadingListRepository
|
|||
return await _context.ReadingList
|
||||
.Where(c => c.Id == readingListId)
|
||||
.Select(c => c.CoverImage)
|
||||
.SingleOrDefaultAsync();
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetAllCoverImagesAsync()
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace API.Entities;
|
|||
/// <summary>
|
||||
/// Represents a Collection of Series for a given User
|
||||
/// </summary>
|
||||
public class AppUserCollection : IEntityDate
|
||||
public class AppUserCollection : IEntityDate, IHasCoverImage
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public required string Title { get; set; }
|
||||
|
@ -23,11 +23,9 @@ public class AppUserCollection : IEntityDate
|
|||
/// Reading lists that are promoted are only done by admins
|
||||
/// </summary>
|
||||
public bool Promoted { get; set; }
|
||||
/// <summary>
|
||||
/// Path to the (managed) image file
|
||||
/// </summary>
|
||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||
public string? CoverImage { get; set; }
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
/// <summary>
|
||||
/// The highest age rating from all Series within the collection
|
||||
|
|
|
@ -9,7 +9,7 @@ using API.Services.Tasks.Scanner.Parser;
|
|||
|
||||
namespace API.Entities;
|
||||
|
||||
public class Chapter : IEntityDate, IHasReadTimeEstimate
|
||||
public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
||||
{
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
|
@ -46,11 +46,9 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
|
|||
public DateTime CreatedUtc { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Relative path to the (managed) image file representing the cover image
|
||||
/// </summary>
|
||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||
public string? CoverImage { get; set; }
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
/// <summary>
|
||||
/// Total number of pages in all MangaFiles
|
||||
|
|
19
API/Entities/Interfaces/IHasCoverImage.cs
Normal file
19
API/Entities/Interfaces/IHasCoverImage.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace API.Entities.Interfaces;
|
||||
|
||||
public interface IHasCoverImage
|
||||
{
|
||||
/// <summary>
|
||||
/// Absolute path to the (managed) image file
|
||||
/// </summary>
|
||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||
public string? CoverImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Primary color derived from the Cover Image
|
||||
/// </summary>
|
||||
public string? PrimaryColor { get; set; }
|
||||
/// <summary>
|
||||
/// Secondary color derived from the Cover Image
|
||||
/// </summary>
|
||||
public string? SecondaryColor { get; set; }
|
||||
}
|
|
@ -5,11 +5,13 @@ using API.Entities.Interfaces;
|
|||
|
||||
namespace API.Entities;
|
||||
|
||||
public class Library : IEntityDate
|
||||
public class Library : IEntityDate, IHasCoverImage
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string? CoverImage { get; set; }
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
public LibraryType Type { get; set; }
|
||||
/// <summary>
|
||||
/// If Folder Watching is enabled for this library
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace API.Entities;
|
|||
/// <summary>
|
||||
/// This is a collection of <see cref="ReadingListItem"/> which represent individual chapters and an order.
|
||||
/// </summary>
|
||||
public class ReadingList : IEntityDate
|
||||
public class ReadingList : IEntityDate, IHasCoverImage
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public required string Title { get; set; }
|
||||
|
@ -23,11 +23,9 @@ public class ReadingList : IEntityDate
|
|||
/// Reading lists that are promoted are only done by admins
|
||||
/// </summary>
|
||||
public bool Promoted { get; set; }
|
||||
/// <summary>
|
||||
/// Absolute path to the (managed) image file
|
||||
/// </summary>
|
||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||
public string? CoverImage { get; set; }
|
||||
public string? PrimaryColor { get; set; }
|
||||
public string? SecondaryColor { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -3,11 +3,10 @@ using System.Collections.Generic;
|
|||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
|
||||
namespace API.Entities;
|
||||
|
||||
public class Series : IEntityDate, IHasReadTimeEstimate
|
||||
public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
||||
{
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
|
@ -82,6 +81,9 @@ public class Series : IEntityDate, IHasReadTimeEstimate
|
|||
/// </summary>
|
||||
public MangaFormat Format { get; set; } = MangaFormat.Unknown;
|
||||
|
||||
public string PrimaryColor { get; set; } = string.Empty;
|
||||
public string SecondaryColor { get; set; } = string.Empty;
|
||||
|
||||
public bool SortNameLocked { get; set; }
|
||||
public bool LocalizedNameLocked { get; set; }
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ using API.Services.Tasks.Scanner.Parser;
|
|||
|
||||
namespace API.Entities;
|
||||
|
||||
public class Volume : IEntityDate, IHasReadTimeEstimate
|
||||
public class Volume : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
||||
{
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
|
@ -38,11 +38,10 @@ public class Volume : IEntityDate, IHasReadTimeEstimate
|
|||
public DateTime CreatedUtc { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path to the (managed) image file
|
||||
/// </summary>
|
||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||
public string? CoverImage { get; set; }
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total pages of all chapters in this volume
|
||||
/// </summary>
|
||||
|
|
|
@ -1223,7 +1223,7 @@ public class BookService : IBookService
|
|||
{
|
||||
// Try to get the cover image from OPF file, if not set, try to parse it from all the files, then result to the first one.
|
||||
var coverImageContent = epubBook.Content.Cover
|
||||
?? epubBook.Content.Images.Local.FirstOrDefault(file => Parser.IsCoverImage(file.FilePath)) // FileName -> FilePath
|
||||
?? epubBook.Content.Images.Local.FirstOrDefault(file => Parser.IsCoverImage(file.FilePath))
|
||||
?? epubBook.Content.Images.Local.FirstOrDefault();
|
||||
|
||||
if (coverImageContent == null) return string.Empty;
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.DTOs;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Extensions;
|
||||
using EasyCaching.Core;
|
||||
using Flurl;
|
||||
|
@ -13,6 +17,9 @@ using HtmlAgilityPack;
|
|||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Processing.Processors.Quantization;
|
||||
using Image = NetVips.Image;
|
||||
|
||||
namespace API.Services;
|
||||
|
@ -60,6 +67,7 @@ public interface IImageService
|
|||
Task<string> ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat);
|
||||
Task<bool> IsImage(string filePath);
|
||||
Task<string> DownloadFaviconAsync(string url, EncodeFormat encodeFormat);
|
||||
void UpdateColorScape(IHasCoverImage entity);
|
||||
}
|
||||
|
||||
public class ImageService : IImageService
|
||||
|
@ -73,6 +81,9 @@ public class ImageService : IImageService
|
|||
public const string CollectionTagCoverImageRegex = @"tag\d+";
|
||||
public const string ReadingListCoverImageRegex = @"readinglist\d+";
|
||||
|
||||
private const double WhiteThreshold = 0.90; // Colors with lightness above this are considered too close to white
|
||||
private const double BlackThreshold = 0.25; // Colors with lightness below this are considered too close to black
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Width of the Thumbnail generation
|
||||
|
@ -415,13 +426,266 @@ public class ImageService : IImageService
|
|||
|
||||
_logger.LogDebug("Favicon.png for {Domain} downloaded and saved successfully", domain);
|
||||
return filename;
|
||||
}catch (Exception ex)
|
||||
} catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error downloading favicon.png for {Domain}", domain);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static (Vector3?, Vector3?) GetPrimarySecondaryColors(string imagePath)
|
||||
{
|
||||
using var image = Image.NewFromFile(imagePath);
|
||||
// Resize the image to speed up processing
|
||||
var resizedImage = image.Resize(0.1);
|
||||
|
||||
|
||||
// Convert image to RGB array
|
||||
var pixels = resizedImage.WriteToMemory().ToArray();
|
||||
|
||||
// Convert to list of Vector3 (RGB)
|
||||
var rgbPixels = new List<Vector3>();
|
||||
for (var i = 0; i < pixels.Length - 2; i += 3)
|
||||
{
|
||||
rgbPixels.Add(new Vector3(pixels[i], pixels[i + 1], pixels[i + 2]));
|
||||
}
|
||||
|
||||
// Perform k-means clustering
|
||||
var clusters = KMeansClustering(rgbPixels, 4);
|
||||
|
||||
var sorted = SortByVibrancy(clusters);
|
||||
|
||||
if (sorted.Count >= 2)
|
||||
{
|
||||
return (sorted[0], sorted[1]);
|
||||
}
|
||||
if (sorted.Count == 1)
|
||||
{
|
||||
return (sorted[0], null);
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
private static (Vector3?, Vector3?) GetPrimaryColorSharp(string imagePath)
|
||||
{
|
||||
using var image = SixLabors.ImageSharp.Image.Load<Rgb24>(imagePath);
|
||||
|
||||
image.Mutate(
|
||||
x => x
|
||||
// Scale the image down preserving the aspect ratio. This will speed up quantization.
|
||||
// We use nearest neighbor as it will be the fastest approach.
|
||||
.Resize(new ResizeOptions() { Sampler = KnownResamplers.NearestNeighbor, Size = new SixLabors.ImageSharp.Size(100, 0) })
|
||||
|
||||
// Reduce the color palette to 1 color without dithering.
|
||||
.Quantize(new OctreeQuantizer(new QuantizerOptions { MaxColors = 4 })));
|
||||
|
||||
Rgb24 dominantColor = image[0, 0];
|
||||
|
||||
// This will give you a dominant color in HEX format i.e #5E35B1FF
|
||||
return (new Vector3(dominantColor.R, dominantColor.G, dominantColor.B), new Vector3(dominantColor.R, dominantColor.G, dominantColor.B));
|
||||
}
|
||||
|
||||
private static Image PreProcessImage(Image image)
|
||||
{
|
||||
// Create a mask for white and black pixels
|
||||
var whiteMask = image.Colourspace(Enums.Interpretation.Lab)[0] > (WhiteThreshold * 100);
|
||||
var blackMask = image.Colourspace(Enums.Interpretation.Lab)[0] < (BlackThreshold * 100);
|
||||
|
||||
// Create a replacement color (e.g., medium gray)
|
||||
var replacementColor = new[] { 128.0, 128.0, 128.0 };
|
||||
|
||||
// Apply the masks to replace white and black pixels
|
||||
var processedImage = image.Copy();
|
||||
processedImage = processedImage.Ifthenelse(whiteMask, replacementColor);
|
||||
processedImage = processedImage.Ifthenelse(blackMask, replacementColor);
|
||||
|
||||
return processedImage;
|
||||
}
|
||||
|
||||
private static Dictionary<Vector3, int> GenerateColorHistogram(Image image)
|
||||
{
|
||||
var pixels = image.WriteToMemory().ToArray();
|
||||
var histogram = new Dictionary<Vector3, int>();
|
||||
|
||||
for (var i = 0; i < pixels.Length; i += 3)
|
||||
{
|
||||
var color = new Vector3(pixels[i], pixels[i + 1], pixels[i + 2]);
|
||||
if (!histogram.TryAdd(color, 1))
|
||||
{
|
||||
histogram[color]++;
|
||||
}
|
||||
}
|
||||
|
||||
return histogram;
|
||||
}
|
||||
|
||||
private static bool IsColorCloseToWhiteOrBlack(Vector3 color)
|
||||
{
|
||||
var (_, _, lightness) = RgbToHsl(color);
|
||||
return lightness is > WhiteThreshold or < BlackThreshold;
|
||||
}
|
||||
|
||||
private static List<Vector3> KMeansClustering(List<Vector3> points, int k, int maxIterations = 100)
|
||||
{
|
||||
var random = new Random();
|
||||
var centroids = points.OrderBy(x => random.Next()).Take(k).ToList();
|
||||
|
||||
for (var i = 0; i < maxIterations; i++)
|
||||
{
|
||||
var clusters = new List<Vector3>[k];
|
||||
for (var j = 0; j < k; j++)
|
||||
{
|
||||
clusters[j] = [];
|
||||
}
|
||||
|
||||
foreach (var point in points)
|
||||
{
|
||||
var nearestCentroidIndex = centroids
|
||||
.Select((centroid, index) => new { Index = index, Distance = Vector3.DistanceSquared(centroid, point) })
|
||||
.OrderBy(x => x.Distance)
|
||||
.First().Index;
|
||||
clusters[nearestCentroidIndex].Add(point);
|
||||
}
|
||||
|
||||
var newCentroids = clusters.Select(cluster =>
|
||||
cluster.Count != 0 ? new Vector3(
|
||||
cluster.Average(p => p.X),
|
||||
cluster.Average(p => p.Y),
|
||||
cluster.Average(p => p.Z)
|
||||
) : Vector3.Zero
|
||||
).ToList();
|
||||
|
||||
if (centroids.SequenceEqual(newCentroids))
|
||||
break;
|
||||
|
||||
centroids = newCentroids;
|
||||
}
|
||||
|
||||
return centroids;
|
||||
}
|
||||
|
||||
// public static Vector3 GetComplementaryColor(Vector3 color)
|
||||
// {
|
||||
// // Simple complementary color calculation
|
||||
// return new Vector3(255 - color.X, 255 - color.Y, 255 - color.Z);
|
||||
// }
|
||||
|
||||
public static List<Vector3> SortByBrightness(List<Vector3> colors)
|
||||
{
|
||||
return colors.OrderBy(c => 0.299 * c.X + 0.587 * c.Y + 0.114 * c.Z).ToList();
|
||||
}
|
||||
|
||||
public static List<Vector3> SortByVibrancy(List<Vector3> colors)
|
||||
{
|
||||
return colors.OrderByDescending(c =>
|
||||
{
|
||||
float max = Math.Max(c.X, Math.Max(c.Y, c.Z));
|
||||
float min = Math.Min(c.X, Math.Min(c.Y, c.Z));
|
||||
return (max - min) / max;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static string RgbToHex(Vector3 color)
|
||||
{
|
||||
return $"#{(int)color.X:X2}{(int)color.Y:X2}{(int)color.Z:X2}";
|
||||
}
|
||||
|
||||
private static Vector3 GetComplementaryColor(Vector3 color)
|
||||
{
|
||||
// Convert RGB to HSL
|
||||
var (h, s, l) = RgbToHsl(color);
|
||||
|
||||
// Rotate hue by 180 degrees
|
||||
h = (h + 180) % 360;
|
||||
|
||||
// Convert back to RGB
|
||||
return HslToRgb(h, s, l);
|
||||
}
|
||||
|
||||
private static (double H, double S, double L) RgbToHsl(Vector3 rgb)
|
||||
{
|
||||
double r = rgb.X / 255;
|
||||
double g = rgb.Y / 255;
|
||||
double b = rgb.Z / 255;
|
||||
|
||||
var max = Math.Max(r, Math.Max(g, b));
|
||||
var min = Math.Min(r, Math.Min(g, b));
|
||||
var diff = max - min;
|
||||
|
||||
double h = 0;
|
||||
double s = 0;
|
||||
var l = (max + min) / 2;
|
||||
|
||||
if (Math.Abs(diff) > 0.00001)
|
||||
{
|
||||
s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min);
|
||||
|
||||
if (max == r)
|
||||
h = (g - b) / diff + (g < b ? 6 : 0);
|
||||
else if (max == g)
|
||||
h = (b - r) / diff + 2;
|
||||
else if (max == b)
|
||||
h = (r - g) / diff + 4;
|
||||
|
||||
h *= 60;
|
||||
}
|
||||
|
||||
return (h, s, l);
|
||||
}
|
||||
|
||||
private static Vector3 HslToRgb(double h, double s, double l)
|
||||
{
|
||||
double r, g, b;
|
||||
|
||||
if (Math.Abs(s) < 0.00001)
|
||||
{
|
||||
r = g = b = l;
|
||||
}
|
||||
else
|
||||
{
|
||||
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
var p = 2 * l - q;
|
||||
r = HueToRgb(p, q, h + 120);
|
||||
g = HueToRgb(p, q, h);
|
||||
b = HueToRgb(p, q, h - 120);
|
||||
}
|
||||
|
||||
return new Vector3((float)(r * 255), (float)(g * 255), (float)(b * 255));
|
||||
}
|
||||
|
||||
private static double HueToRgb(double p, double q, double t)
|
||||
{
|
||||
if (t < 0) t += 360;
|
||||
if (t > 360) t -= 360;
|
||||
return t switch
|
||||
{
|
||||
< 60 => p + (q - p) * t / 60,
|
||||
< 180 => q,
|
||||
< 240 => p + (q - p) * (240 - t) / 60,
|
||||
_ => p
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the Primary and Secondary colors from a file
|
||||
/// </summary>
|
||||
/// <remarks>This may use a second most common color or a complementary color. It's up to implemenation to choose what's best</remarks>
|
||||
/// <param name="sourceFile"></param>
|
||||
/// <returns></returns>
|
||||
public static ColorScape CalculateColorScape(string sourceFile)
|
||||
{
|
||||
if (!File.Exists(sourceFile)) return new ColorScape() {Primary = null, Secondary = null};
|
||||
|
||||
var colors = GetPrimarySecondaryColors(sourceFile);
|
||||
|
||||
return new ColorScape()
|
||||
{
|
||||
Primary = colors.Item1 == null ? null : RgbToHex(colors.Item1.Value),
|
||||
Secondary = colors.Item2 == null ? null : RgbToHex(colors.Item2.Value)
|
||||
};
|
||||
}
|
||||
|
||||
private static string FallbackToKavitaReaderFavicon(string baseUrl)
|
||||
{
|
||||
var correctSizeLink = string.Empty;
|
||||
|
@ -582,4 +846,39 @@ public class ImageService : IImageService
|
|||
|
||||
image.WriteToFile(dest);
|
||||
}
|
||||
|
||||
public void UpdateColorScape(IHasCoverImage entity)
|
||||
{
|
||||
var colors = CalculateColorScape(
|
||||
_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, entity.CoverImage));
|
||||
entity.PrimaryColor = colors.Primary;
|
||||
entity.SecondaryColor = colors.Secondary;
|
||||
}
|
||||
|
||||
public static Color HexToRgb(string? hex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hex)) throw new ArgumentException("Hex cannot be null");
|
||||
|
||||
// Remove the leading '#' if present
|
||||
hex = hex.TrimStart('#');
|
||||
|
||||
// Ensure the hex string is valid
|
||||
if (hex.Length != 6 && hex.Length != 3)
|
||||
{
|
||||
throw new ArgumentException("Hex string should be 6 or 3 characters long.");
|
||||
}
|
||||
|
||||
if (hex.Length == 3)
|
||||
{
|
||||
// Expand shorthand notation to full form (e.g., "abc" -> "aabbcc")
|
||||
hex = string.Concat(hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]);
|
||||
}
|
||||
|
||||
// Parse the hex string into RGB components
|
||||
var r = Convert.ToInt32(hex.Substring(0, 2), 16);
|
||||
var g = Convert.ToInt32(hex.Substring(2, 2), 16);
|
||||
var b = Convert.ToInt32(hex.Substring(4, 2), 16);
|
||||
|
||||
return Color.FromArgb(r, g, b);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.SignalR;
|
||||
|
@ -38,6 +40,9 @@ public interface IMetadataService
|
|||
Task RemoveAbandonedMetadataKeys();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles everything around Cover/ColorScape management
|
||||
/// </summary>
|
||||
public class MetadataService : IMetadataService
|
||||
{
|
||||
public const string Name = "MetadataService";
|
||||
|
@ -47,10 +52,13 @@ public class MetadataService : IMetadataService
|
|||
private readonly ICacheHelper _cacheHelper;
|
||||
private readonly IReadingItemService _readingItemService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly IList<SignalRMessage> _updateEvents = new List<SignalRMessage>();
|
||||
|
||||
public MetadataService(IUnitOfWork unitOfWork, ILogger<MetadataService> logger,
|
||||
IEventHub eventHub, ICacheHelper cacheHelper,
|
||||
IReadingItemService readingItemService, IDirectoryService directoryService)
|
||||
IReadingItemService readingItemService, IDirectoryService directoryService,
|
||||
IImageService imageService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
|
@ -58,6 +66,7 @@ public class MetadataService : IMetadataService
|
|||
_cacheHelper = cacheHelper;
|
||||
_readingItemService = readingItemService;
|
||||
_directoryService = directoryService;
|
||||
_imageService = imageService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -71,16 +80,28 @@ public class MetadataService : IMetadataService
|
|||
var firstFile = chapter.Files.MinBy(x => x.Chapter);
|
||||
if (firstFile == null) return Task.FromResult(false);
|
||||
|
||||
if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage),
|
||||
if (!_cacheHelper.ShouldUpdateCoverImage(
|
||||
_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage),
|
||||
firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked))
|
||||
return Task.FromResult(false);
|
||||
{
|
||||
if (NeedsColorSpace(chapter))
|
||||
{
|
||||
_imageService.UpdateColorScape(chapter);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter));
|
||||
}
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
|
||||
_logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath);
|
||||
|
||||
chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath,
|
||||
ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, encodeFormat, coverImageSize);
|
||||
|
||||
_imageService.UpdateColorScape(chapter);
|
||||
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter));
|
||||
|
@ -95,6 +116,15 @@ public class MetadataService : IMetadataService
|
|||
firstFile.UpdateLastModified();
|
||||
}
|
||||
|
||||
private static bool NeedsColorSpace(IHasCoverImage? entity)
|
||||
{
|
||||
if (entity == null) return false;
|
||||
return !string.IsNullOrEmpty(entity.CoverImage) &&
|
||||
(string.IsNullOrEmpty(entity.PrimaryColor) || string.IsNullOrEmpty(entity.SecondaryColor));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Updates the cover image for a Volume
|
||||
/// </summary>
|
||||
|
@ -105,8 +135,16 @@ public class MetadataService : IMetadataService
|
|||
// We need to check if Volume coverImage matches first chapters if forceUpdate is false
|
||||
if (volume == null || !_cacheHelper.ShouldUpdateCoverImage(
|
||||
_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, volume.CoverImage),
|
||||
null, volume.Created, forceUpdate)) return Task.FromResult(false);
|
||||
|
||||
null, volume.Created, forceUpdate))
|
||||
{
|
||||
if (NeedsColorSpace(volume))
|
||||
{
|
||||
_imageService.UpdateColorScape(volume);
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume));
|
||||
}
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
// For cover selection, chapters need to try for issue 1 first, then fallback to first sort order
|
||||
volume.Chapters ??= new List<Chapter>();
|
||||
|
@ -118,7 +156,10 @@ public class MetadataService : IMetadataService
|
|||
if (firstChapter == null) return Task.FromResult(false);
|
||||
}
|
||||
|
||||
|
||||
volume.CoverImage = firstChapter.CoverImage;
|
||||
_imageService.UpdateColorScape(volume);
|
||||
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume));
|
||||
|
||||
return Task.FromResult(true);
|
||||
|
@ -133,13 +174,26 @@ public class MetadataService : IMetadataService
|
|||
{
|
||||
if (series == null) return Task.CompletedTask;
|
||||
|
||||
if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, series.CoverImage),
|
||||
if (!_cacheHelper.ShouldUpdateCoverImage(
|
||||
_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, series.CoverImage),
|
||||
null, series.Created, forceUpdate, series.CoverImageLocked))
|
||||
{
|
||||
// Check if we don't have a primary/seconary color
|
||||
if (NeedsColorSpace(series))
|
||||
{
|
||||
_imageService.UpdateColorScape(series);
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series));
|
||||
}
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
series.Volumes ??= [];
|
||||
series.CoverImage = series.GetCoverImage();
|
||||
|
||||
_imageService.UpdateColorScape(series);
|
||||
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ public interface IReadingListService
|
|||
Task CreateReadingListsFromSeries(Series series, Library library);
|
||||
|
||||
Task CreateReadingListsFromSeries(int libraryId, int seriesId);
|
||||
Task<string> GenerateReadingListCoverImage(int readingListId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -59,15 +60,20 @@ public class ReadingListService : IReadingListService
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<ReadingListService> _logger;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ChapterSortComparerDefaultFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerDefaultFirst.Default;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
private static readonly Regex JustNumbers = new Regex(@"^\d+$", RegexOptions.Compiled | RegexOptions.IgnoreCase,
|
||||
Parser.RegexTimeout);
|
||||
|
||||
public ReadingListService(IUnitOfWork unitOfWork, ILogger<ReadingListService> logger, IEventHub eventHub)
|
||||
public ReadingListService(IUnitOfWork unitOfWork, ILogger<ReadingListService> logger,
|
||||
IEventHub eventHub, IImageService imageService, IDirectoryService directoryService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
_eventHub = eventHub;
|
||||
_imageService = imageService;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
public static string FormatTitle(ReadingListItemDto item)
|
||||
|
@ -488,8 +494,12 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
if (!_unitOfWork.HasChanges()) continue;
|
||||
|
||||
|
||||
_imageService.UpdateColorScape(readingList);
|
||||
await CalculateReadingListAgeRating(readingList);
|
||||
|
||||
await _unitOfWork.CommitAsync(); // TODO: See if we can avoid this extra commit by reworking bottom logic
|
||||
|
||||
await CalculateStartAndEndDates(await _unitOfWork.ReadingListRepository.GetReadingListByTitleAsync(arcPair.Item1,
|
||||
user.Id, ReadingListIncludes.Items | ReadingListIncludes.ItemChapter));
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -632,6 +642,7 @@ public class ReadingListService : IReadingListService
|
|||
var allSeriesLocalized = userSeries.ToDictionary(s => s.NormalizedLocalizedName);
|
||||
|
||||
var readingListNameNormalized = Parser.Normalize(cblReading.Name);
|
||||
|
||||
// Get all the user's reading lists
|
||||
var allReadingLists = (user.ReadingLists).ToDictionary(s => s.NormalizedTitle);
|
||||
if (!allReadingLists.TryGetValue(readingListNameNormalized, out var readingList))
|
||||
|
@ -736,7 +747,10 @@ public class ReadingListService : IReadingListService
|
|||
}
|
||||
|
||||
// If there are no items, don't create a blank list
|
||||
if (!_unitOfWork.HasChanges() || !readingList.Items.Any()) return importSummary;
|
||||
if (!_unitOfWork.HasChanges() || readingList.Items.Count == 0) return importSummary;
|
||||
|
||||
|
||||
_imageService.UpdateColorScape(readingList);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
|
||||
|
@ -787,4 +801,33 @@ public class ReadingListService : IReadingListService
|
|||
file.Close();
|
||||
return cblReadingList;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateReadingListCoverImage(int readingListId)
|
||||
{
|
||||
// TODO: Currently reading lists are dynamically generated at runtime. This needs to be overhauled to be generated and stored within
|
||||
// the Reading List (and just expire every so often) so we can utilize ColorScapes.
|
||||
// Check if a cover already exists for the reading list
|
||||
// var potentialExistingCoverPath = _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory,
|
||||
// ImageService.GetReadingListFormat(readingListId));
|
||||
// if (_directoryService.FileSystem.File.Exists(potentialExistingCoverPath))
|
||||
// {
|
||||
// // Check if we need to update CoverScape
|
||||
//
|
||||
// }
|
||||
|
||||
var covers = await _unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId);
|
||||
var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory,
|
||||
ImageService.GetReadingListFormat(readingListId));
|
||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
destFile += settings.EncodeMediaAs.GetExtension();
|
||||
|
||||
if (_directoryService.FileSystem.File.Exists(destFile)) return destFile;
|
||||
ImageService.CreateMergedImage(
|
||||
covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(),
|
||||
settings.CoverImageSize,
|
||||
destFile);
|
||||
// TODO: Refactor this so that reading lists have a dedicated cover image so we can calculate primary/secondary colors
|
||||
|
||||
return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,7 +97,6 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||
// isNightly can be true when we compare something like v0.8.1 vs v0.8.1.0
|
||||
if (IsVersionEqualToBuildVersion(updateVersion))
|
||||
{
|
||||
//latestRelease.UpdateVersion = BuildInfo.Version.ToString();
|
||||
isNightly = false;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue