UX Overhaul Part 1 (#3047)

Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
Robbie Davis 2024-08-09 13:55:31 -04:00 committed by GitHub
parent 5934d516f3
commit ff79710ac6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
324 changed files with 11589 additions and 4598 deletions

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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