Massive UI Cleanup (#4466)

Co-authored-by: KindlyFire <10267586+kindlyfire@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Alexey <lewadedun@gmail.com>
Co-authored-by: Anon Bitardov <timurvolga23+weblate@gmail.com>
Co-authored-by: Ferran <ferrancette@gmail.com>
Co-authored-by: Gneb <goozi12345@gmail.com>
Co-authored-by: Robin Stolpe <robinstolpe@slashmad.com>
Co-authored-by: 안세훈 <on9686@gmail.com>
Co-authored-by: Tijl Van den Brugghen <contact@tijlvdb.me>
This commit is contained in:
Joe Milazzo
2026-02-28 13:19:00 -06:00
committed by GitHub
parent faae7fe402
commit 0bbb0ff28f
596 changed files with 14339 additions and 12501 deletions
+8 -6
View File
@@ -66,10 +66,12 @@ public class CollectionController : BaseApiController
/// <param name="collectionId"></param>
/// <returns></returns>
[HttpGet("single")]
public async Task<ActionResult<IEnumerable<AppUserCollectionDto>>> GetTag(int collectionId)
public async Task<ActionResult<AppUserCollectionDto>> GetTag(int collectionId)
{
var collections = await _unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(UserId, false);
return Ok(collections.FirstOrDefault(c => c.Id == collectionId));
var result = await _unitOfWork.CollectionTagRepository.GetCollectionDtoAsync(collectionId, UserId);
if (result == null) return NotFound(); // TODO: Figure out how to best handle restrictions/not found across the codebase
return Ok(result);
}
/// <summary>
@@ -101,10 +103,10 @@ public class CollectionController : BaseApiController
/// <remarks>UI does not contain controls to update title</remarks>
/// </summary>
/// <param name="updatedTag"></param>
/// <returns></returns>
/// <returns>The updated tag entity</returns>
[HttpPost("update")]
[DisallowRole(PolicyConstants.ReadOnlyRole)]
public async Task<ActionResult> UpdateTag(AppUserCollectionDto updatedTag)
public async Task<ActionResult<AppUserCollectionDto>> UpdateTag(AppUserCollectionDto updatedTag)
{
try
{
@@ -112,7 +114,7 @@ public class CollectionController : BaseApiController
{
await _eventHub.SendMessageAsync(MessageFactory.CollectionUpdated,
MessageFactory.CollectionUpdatedEvent(updatedTag.Id), false);
return Ok(await _localizationService.Translate(UserId, "collection-updated-successfully"));
return Ok(await _unitOfWork.CollectionTagRepository.GetCollectionDtoAsync(updatedTag.Id, UserId));
}
}
catch (KavitaException ex)
+14 -15
View File
@@ -24,6 +24,7 @@ using EasyCaching.Core;
using Hangfire;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -68,10 +69,10 @@ public class LibraryController : BaseApiController
/// Creates a new Library. Upon library creation, adds new library to all Admin accounts.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
/// <returns>Created Library</returns>
[Authorize(Policy = PolicyGroups.AdminPolicy)]
[HttpPost("create")]
public async Task<ActionResult> AddLibrary(UpdateLibraryDto dto)
public async Task<ActionResult<LibraryDto?>> AddLibrary(UpdateLibraryDto dto)
{
if (await _unitOfWork.LibraryRepository.LibraryExists(dto.Name))
{
@@ -161,7 +162,7 @@ public class LibraryController : BaseApiController
await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate,
MessageFactory.SideNavUpdateEvent(UserId), false);
return Ok();
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(library.Id));
}
/// <summary>
@@ -207,17 +208,18 @@ public class LibraryController : BaseApiController
/// <summary>
/// Return a specific library
/// </summary>
/// <remarks>If the user is not an admin, only id, type, and name will be returned</remarks>
/// <returns></returns>
[Authorize(Policy = PolicyGroups.AdminPolicy)]
[ProducesResponseType<LibraryDto>(StatusCodes.Status200OK)]
[ProducesResponseType<LiteLibraryDto>(StatusCodes.Status200OK)]
[HttpGet]
public async Task<ActionResult<LibraryDto?>> GetLibrary(int libraryId)
{
var username = Username!;
if (string.IsNullOrEmpty(username)) return Unauthorized();
var libraries = await GetLibrariesForUser(username);
return Ok(libraries.FirstOrDefault(l => l.Id == libraryId));
if (User.IsInRole(PolicyConstants.AdminRole))
{
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(libraryId));
}
return Ok(await _unitOfWork.LibraryRepository.GetLiteLibraryDtoByIdAsync(libraryId));
}
/// <summary>
@@ -227,10 +229,7 @@ public class LibraryController : BaseApiController
[HttpGet("libraries")]
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
{
var username = Username!;
if (string.IsNullOrEmpty(username)) return Unauthorized();
return Ok(await GetLibrariesForUser(username));
return Ok(await GetLibrariesForUser(Username!));
}
/// <summary>
@@ -674,7 +673,7 @@ public class LibraryController : BaseApiController
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
return Ok();
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(library.Id));
}
+4 -4
View File
@@ -159,9 +159,9 @@ public class SeriesController : BaseApiController
/// Updates the Series
/// </summary>
/// <param name="updateSeries"></param>
/// <returns></returns>
/// <returns>Updated Series</returns>
[HttpPost("update")]
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
public async Task<ActionResult<SeriesDto>> UpdateSeries(UpdateSeriesDto updateSeries)
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id);
if (series == null)
@@ -206,7 +206,7 @@ public class SeriesController : BaseApiController
await _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id);
}
return Ok();
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(series.Id, UserId));
}
/// <summary>
@@ -233,7 +233,7 @@ public class SeriesController : BaseApiController
/// <param name="userParams">Page size and offset</param>
/// <returns></returns>
[HttpPost("recently-updated-series")]
public async Task<ActionResult<IList<RecentlyAddedItemDto>>> GetRecentlyAddedChapters([FromQuery] UserParams? userParams)
public async Task<ActionResult<IList<GroupedSeriesDto>>> GetRecentlyAddedChapters([FromQuery] UserParams? userParams)
{
userParams ??= UserParams.Default;
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(UserId, userParams));
@@ -1,34 +0,0 @@
using System;
using API.Entities.Enums;
namespace API.DTOs.Dashboard;
/// <summary>
/// A mesh of data for Recently added volume/chapters
/// </summary>
public sealed record RecentlyAddedItemDto
{
public string SeriesName { get; set; } = default!;
public int SeriesId { get; set; }
public int LibraryId { get; set; }
public LibraryType LibraryType { get; set; }
/// <summary>
/// This will automatically map to Volume X, Chapter Y, etc.
/// </summary>
public string Title { get; set; } = default!;
public DateTime Created { get; set; }
/// <summary>
/// Chapter Id if this is a chapter. Not guaranteed to be set.
/// </summary>
public int ChapterId { get; set; } = 0;
/// <summary>
/// Volume Id if this is a chapter. Not guaranteed to be set.
/// </summary>
public int VolumeId { get; set; } = 0;
/// <summary>
/// This is used only on the UI. It is just index of being added.
/// </summary>
public int Id { get; set; }
public MangaFormat Format { get; set; }
}
+9 -2
View File
@@ -6,15 +6,22 @@ using API.Entities.Enums;
namespace API.DTOs;
#nullable enable
public sealed record LibraryDto
/// <summary>
/// This is a LibraryDto that non-admins can resolve that has the core information they need
/// </summary>
public record LiteLibraryDto
{
public int Id { get; init; }
public string? Name { get; init; }
public LibraryType Type { get; init; }
}
public sealed record LibraryDto : LiteLibraryDto
{
/// <summary>
/// Last time Library was scanned
/// </summary>
public DateTime LastScanned { get; init; }
public LibraryType Type { get; init; }
/// <summary>
/// An optional Cover Image or null
/// </summary>
+1 -1
View File
@@ -231,7 +231,7 @@ public class AutoMapperProfiles : Profile
.ForMember(dest => dest.LibraryName,
opt => opt.MapFrom(src => src.Library.Name));
CreateMap<Library, LiteLibraryDto>();
CreateMap<Library, LibraryDto>()
.ForMember(dest => dest.Folders,
opt =>
@@ -50,6 +50,11 @@ public interface ICollectionTagRepository
/// <param name="includePromoted"></param>
/// <returns></returns>
Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosAsync(int userId, bool includePromoted = false);
/// <summary>
/// Returns the collection if the user owns it or the collection is promoted
/// </summary>
/// <returns></returns>
Task<AppUserCollectionDto?> GetCollectionDtoAsync(int collectionId, int userId);
Task<PagedList<AppUserCollectionDto>> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false);
Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false);
@@ -108,6 +113,17 @@ public class CollectionTagRepository : ICollectionTagRepository
.ToListAsync();
}
public async Task<AppUserCollectionDto?> GetCollectionDtoAsync(int collectionId, int userId)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.AppUserCollection
.Where(uc => (uc.AppUserId == userId || uc.Promoted) && uc.Id == collectionId)
.WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating)
.OrderBy(uc => uc.Title)
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync();
}
public async Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosAsync(int userId, bool includePromoted = false)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
@@ -37,6 +37,8 @@ public interface ILibraryRepository
void Update(Library library);
void Delete(Library? library);
Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync();
Task<LibraryDto?> GetLibraryDtoByIdAsync(int libraryId);
Task<LiteLibraryDto?> GetLiteLibraryDtoByIdAsync(int libraryId);
Task<bool> LibraryExists(string libraryName);
Task<Library?> GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None);
Task<IList<LibraryDto>> GetLibraryDtosForUsernameAsync(string userName);
@@ -214,6 +216,23 @@ public class LibraryRepository : ILibraryRepository
.ToListAsync();
}
public async Task<LibraryDto?> GetLibraryDtoByIdAsync(int libraryId)
{
return await _context.Library
.Include(f => f.Folders)
.Include(l => l.LibraryFileTypes)
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
.AsSplitQuery()
.FirstOrDefaultAsync(l => l.Id == libraryId);
}
public async Task<LiteLibraryDto?> GetLiteLibraryDtoByIdAsync(int libraryId)
{
return await _context.Library
.ProjectTo<LiteLibraryDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync(l => l.Id == libraryId);
}
public async Task<Library?> GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None)
{
@@ -32,7 +32,7 @@ public static class SeriesSort
SortField.TimeToRead => query.DoOrderBy(s => s.AvgHoursToRead, sortOptions),
SortField.ReleaseYear => query.DoOrderBy(s => s.Metadata.ReleaseYear, sortOptions),
SortField.ReadProgress => query.DoOrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id && p.AppUserId == userId)
.Select(p => p.LastModified) // TODO: Migrate this to UTC
.Select(p => p.LastModifiedUtc)
.Max(), sortOptions),
SortField.AverageRating => query.DoOrderBy(s => s.ExternalSeriesMetadata.ExternalRatings
.Where(p => p.SeriesId == s.Id).Average(p => p.AverageScore), sortOptions),
+5 -1
View File
@@ -61,5 +61,9 @@
"reading-list-item-delete": "No s'han pogut suprimir els elements",
"generic-reading-list-create": "S'ha produït un problema en crear la llista de lectura",
"generic-reading-list-update": "S'ha produït un problema en actualitzar la llista de lectura",
"generic-create-temp-archive": "S'ha produït un problema en crear l'arxiu temporal"
"generic-create-temp-archive": "S'ha produït un problema en crear l'arxiu temporal",
"locked-out": "S'ha bloquejat l'accés degut a masses intents d'autentificació. Si us plau, espera 10 minuts.",
"disabled-account": "El seu compte s'ha deshabilitat. Contacti amb l'administrador.",
"register-user": "Hi ha hagut un error al registrar-se",
"validate-email": "Hi ha hagut un error al validar l'e-mail: {0}"
}
+1 -1
View File
@@ -87,7 +87,7 @@
"bookmark-dir-permissions": "Níl na ceadanna cearta ag an Eolaire Leabharmharcanna chun Kavita a úsáid",
"total-backups": "Caithfidh Cúltaca Iomlána a bheith idir 1 agus 30",
"total-logs": "Caithfidh an Logchomhaid Iomlán a bheith idir 1 agus 30",
"url-not-valid": "Ní sheolann URL íomhá bhailí ar ais nó éilíonn údarú",
"url-not-valid": "Ní thugann an URL íomhá bhailí ar ais nó teastaíonn údarú uaidh",
"url-required": "Caithfidh tú pas a fháil i url le húsáid",
"generic-cover-series-save": "Ní féidir íomhá an chlúdaigh a shábháil sa tSraith",
"generic-cover-collection-save": "Ní féidir íomhá an chlúdaigh a shábháil sa Bhailiúchán",
+3 -3
View File
@@ -1,5 +1,5 @@
{
"confirm-email": "Сначала Вы должны подтвердить свой адрес электронной почты",
"confirm-email": "Вы должны подтвердить адрес своей электронной почты",
"generate-token": "Возникла проблема с генерацией токена подтверждения электронной почты. Смотрите журналы",
"invalid-password": "Неверный пароль",
"invalid-email-confirmation": "Неверное подтверждение электронной почты",
@@ -9,7 +9,7 @@
"email-sent": "Электронное письмо отправлено",
"generic-password-update": "В процессе подтверждения нового пароля возникла непредвиденная ошибка",
"user-already-confirmed": "Пользователь уже подтвержден",
"user-migration-needed": "Для этой учётной записи требуется миграция. Попросите пользователя повторно авторизоваться для запуска процесса переноса",
"user-migration-needed": "Для этой учётной записи требуется перенос. Попросите пользователя повторно авторизоваться для запуска процесса переноса",
"generic-user-update": "При обновлении пользователя возникало исключение",
"disabled-account": "Ваша учетная запись отключена. Обратитесь к администратору сервера.",
"locked-out": "Доступ временно заблокирован из-за превышения числа попыток входа. Пожалуйста, подождите 10 минут.",
@@ -57,7 +57,7 @@
"generic-reading-list-create": "Возникла проблема с созданием читательского списка",
"no-cover-image": "Нет изображения обложки",
"collection-updated": "Коллекция успешно обновлена",
"critical-email-migration": "Возникла проблема в процессе смены электронной почты. Обратитесь в службу поддержки",
"critical-email-migration": "Возникла проблема в процессе переноса электронной почты. Обратитесь в службу поддержки",
"cache-file-find": "Не удалось найти кешированное изображение. Перезагрузите страницу и попробуйте снова.",
"duplicate-bookmark": "Закладка с такими параметрами уже существует",
"collection-tag-duplicate": "Коллекция с таким именем уже существует",
+4 -1
View File
@@ -220,5 +220,8 @@
"genre-doesnt-exist": "Žáner neexistuje",
"font-url-not-allowed": "Nahrávanie písma pomocou adresy URL je povolené iba z Google Fonts",
"annotation-export-failed": "Nepodarilo sa exportovať anotácie, skontrolujte protokoly",
"download-not-allowed": "Používateľ nemá povolenia na sťahovanie"
"download-not-allowed": "Používateľ nemá povolenia na sťahovanie",
"client-device-doesnt-exist": "Klientske zariadenie neexistuje",
"auth-key-unique": "Názov autorizačného kľúča musí byť jedinečný pre váš účet",
"role-restricted": "Prístup odmietnutý: Vaša rola nemá povolenie na túto akciu"
}
+3 -1
View File
@@ -208,5 +208,7 @@
"smart-filter-name-required": "Smart Filter namn krävs",
"sidenav-stream-only-delete-smart-filter": "Bara smarta-filter-strömmar kan tas bort från sidomenyn",
"smart-filter-system-name": "Du kan inte använda namnet från en ström som systemet tillhandahåller",
"password-authentication-disabled": "Lösenords autentisering har stängts av, logga in via OpenID Connect"
"password-authentication-disabled": "Lösenords autentisering har stängts av, logga in via OpenID Connect",
"oidc-managed": "Användare som hanteras av OIDC kan inte redigeras.",
"cannot-change-identity-provider-original-user": "Identitetsleverantören för det ursprungliga administratörskontot kan inte ändras"
}
+3 -3
View File
@@ -172,10 +172,10 @@ public class Program
private static void HandleFirstRunConfiguration()
{
var firstRunConfigFilePath = Path.Join(Directory.GetCurrentDirectory(), "config/appsettings-init.json");
if (File.Exists(firstRunConfigFilePath) &&
!File.Exists(Path.Join(Directory.GetCurrentDirectory(), "config/appsettings.json")))
var actualRunConfigFilePath = Path.Join(Directory.GetCurrentDirectory(), "config/appsettings.json");
if (File.Exists(firstRunConfigFilePath) && !File.Exists(actualRunConfigFilePath))
{
File.Move(firstRunConfigFilePath, Path.Join(Directory.GetCurrentDirectory(), "config/appsettings.json"));
File.Move(firstRunConfigFilePath, actualRunConfigFilePath);
}
}
+1 -1
View File
@@ -292,7 +292,7 @@ public class LocalizationService : ILocalizationService
try
{
var cultureInfo = new System.Globalization.CultureInfo(fileName.Replace('_', '-'));
return cultureInfo.NativeName;
return cultureInfo.EnglishName;
}
catch
{