mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Misc Fixes + Enhancements (#1875)
* Moved Collapse Series with relationships into a user preference rather than library setting. * Fixed bookmarks not converting to webp after initial save * Fixed a bug where when merging we'd print out a duplicate series error when we shouldn't have * Fixed a bug where clicking on a genre or tag from server stats wouldn't load all-series page in a filtered state. * Implemented the ability to have Login role and thus disable accounts. * Ensure first time flow gets the Login role * Refactored user management screen so that pending users can be edited or deleted before the end user accepts the invite. A side effect is old legacy users that were here before email was required can now be deleted. * Show a progress bar under the main series image on larger viewports to show whole series progress. * Removed code no longer needed * Cleanup tags, people, collections without connections after editing series metadata. * Moved the Entity Builders to the main project
This commit is contained in:
parent
c62e594792
commit
bd19b282d5
@ -36,7 +36,6 @@ public class ComicInfoTests
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
#region CalculatedCount
|
||||
|
||||
[Fact]
|
||||
|
@ -6,7 +6,7 @@ using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using API.Helpers.Builders;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Extensions;
|
||||
@ -226,40 +226,28 @@ public class QueryableExtensionsTests
|
||||
{
|
||||
var items = new List<Person>()
|
||||
{
|
||||
new Person()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
new PersonBuilder("Test", PersonRole.Character)
|
||||
.WithSeriesMetadata(new SeriesMetadata()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Person()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
AgeRating = AgeRating.Teen,
|
||||
})
|
||||
.Build(),
|
||||
new PersonBuilder("Test", PersonRole.Character)
|
||||
.WithSeriesMetadata(new SeriesMetadata()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
},
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Person()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
AgeRating = AgeRating.Unknown,
|
||||
})
|
||||
.WithSeriesMetadata(new SeriesMetadata()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus,
|
||||
}
|
||||
}
|
||||
},
|
||||
AgeRating = AgeRating.Teen,
|
||||
})
|
||||
.Build(),
|
||||
new PersonBuilder("Test", PersonRole.Character)
|
||||
.WithSeriesMetadata(new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus,
|
||||
})
|
||||
.Build(),
|
||||
};
|
||||
|
||||
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
|
@ -4,7 +4,7 @@ using API.Comparators;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using API.Helpers.Builders;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Extensions;
|
||||
|
@ -4,9 +4,9 @@ using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Parser;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Helpers;
|
||||
|
@ -8,8 +8,8 @@ using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -244,6 +244,17 @@ public class ArchiveServiceTests
|
||||
Assert.Equal(summaryInfo, comicInfo.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldHaveComicInfo_CanParseUmlaut()
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||
var archive = Path.Join(testDirectory, "Umlaut.zip");
|
||||
|
||||
var comicInfo = _archiveService.GetComicInfo(archive);
|
||||
Assert.NotNull(comicInfo);
|
||||
Assert.Equal("Belladonna", comicInfo.Series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldHaveComicInfo_WithAuthors()
|
||||
{
|
||||
|
@ -12,9 +12,9 @@ using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -12,11 +12,11 @@ using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
@ -12,10 +12,10 @@ using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -13,10 +13,10 @@ using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -4,11 +4,11 @@ using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Parser;
|
||||
using API.Services.Tasks;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.Tests.Helpers;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
|
@ -12,10 +12,10 @@ using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
@ -1,5 +1,5 @@
|
||||
using API.Extensions;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using API.Helpers.Builders;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
using System.Collections.Generic;
|
||||
|
Binary file not shown.
@ -121,6 +121,12 @@
|
||||
<None Remove="kavita.db" />
|
||||
<None Remove="covers\**" />
|
||||
<None Remove="wwwroot\**" />
|
||||
<None Remove="config\cache\**" />
|
||||
<None Remove="config\logs\**" />
|
||||
<None Remove="config\covers\**" />
|
||||
<None Remove="config\bookmarks\**" />
|
||||
<None Remove="config\backups\**" />
|
||||
<None Remove="config\temp\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -131,6 +137,12 @@
|
||||
<Compile Remove="temp\**" />
|
||||
<Compile Remove="covers\**" />
|
||||
<Compile Remove="wwwroot\**" />
|
||||
<Compile Remove="config\cache\**" />
|
||||
<Compile Remove="config\logs\**" />
|
||||
<Compile Remove="config\covers\**" />
|
||||
<Compile Remove="config\bookmarks\**" />
|
||||
<Compile Remove="config\backups\**" />
|
||||
<Compile Remove="config\temp\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -146,6 +158,8 @@
|
||||
<EmbeddedResource Remove="config\temp\**" />
|
||||
<EmbeddedResource Remove="config\stats\**" />
|
||||
<EmbeddedResource Remove="wwwroot\**" />
|
||||
<EmbeddedResource Remove="config\cache\**" />
|
||||
<EmbeddedResource Remove="config\bookmarks\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -169,6 +183,7 @@
|
||||
<Content Update="bin\$(Configuration)\$(AssemblyName).xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Remove="config\bookmarks\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -31,7 +31,11 @@ public static class PolicyConstants
|
||||
/// Used to give a user ability to Change Restrictions on their account
|
||||
/// </summary>
|
||||
public const string ChangeRestrictionRole = "Change Restriction";
|
||||
/// <summary>
|
||||
/// Used to give a user ability to Login to their account
|
||||
/// </summary>
|
||||
public const string LoginRole = "Login";
|
||||
|
||||
public static readonly ImmutableArray<string> ValidRoles =
|
||||
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole);
|
||||
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole);
|
||||
}
|
||||
|
@ -144,6 +144,7 @@ public class AccountController : BaseApiController
|
||||
|
||||
var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole);
|
||||
if (!roleResult.Succeeded) return BadRequest(result.Errors);
|
||||
await _userManager.AddToRoleAsync(user, PolicyConstants.LoginRole);
|
||||
|
||||
return new UserDto
|
||||
{
|
||||
@ -182,6 +183,8 @@ public class AccountController : BaseApiController
|
||||
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
|
||||
|
||||
if (user == null) return Unauthorized("Your credentials are not correct");
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized("Your account is disabled. Contact the server admin.");
|
||||
|
||||
var result = await _signInManager
|
||||
.CheckPasswordSignInAsync(user, loginDto.Password, true);
|
||||
|
@ -338,7 +338,6 @@ public class LibraryController : BaseApiController
|
||||
library.IncludeInRecommended = dto.IncludeInRecommended;
|
||||
library.IncludeInSearch = dto.IncludeInSearch;
|
||||
library.ManageCollections = dto.ManageCollections;
|
||||
library.CollapseSeriesRelationships = dto.CollapseSeriesRelationships;
|
||||
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
|
||||
|
@ -31,6 +31,7 @@ public class MetadataController : BaseApiController
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all genres</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("genres")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
@ -51,6 +52,7 @@ public class MetadataController : BaseApiController
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all people</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("people")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
@ -68,6 +70,7 @@ public class MetadataController : BaseApiController
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all tags</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("tags")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
@ -132,6 +135,7 @@ public class MetadataController : BaseApiController
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("languages")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<LanguageDto>>> GetAllLanguages(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||
@ -145,6 +149,7 @@ public class MetadataController : BaseApiController
|
||||
}
|
||||
|
||||
[HttpGet("all-languages")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)]
|
||||
public IEnumerable<LanguageDto> GetAllValidLanguages()
|
||||
{
|
||||
return CultureInfo.GetCultures(CultureTypes.AllCultures).Select(c =>
|
||||
|
@ -38,18 +38,16 @@ public class UsersController : BaseApiController
|
||||
return BadRequest("Could not delete the user.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all users of this server
|
||||
/// </summary>
|
||||
/// <param name="includePending">This will include pending members</param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers()
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers(bool includePending = false)
|
||||
{
|
||||
return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("pending")]
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetPendingUsers()
|
||||
{
|
||||
return Ok(await _unitOfWork.UserRepository.GetPendingMemberDtosAsync());
|
||||
return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync(!includePending));
|
||||
}
|
||||
|
||||
[HttpGet("myself")]
|
||||
@ -110,6 +108,7 @@ public class UsersController : BaseApiController
|
||||
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
||||
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
|
||||
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
||||
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
|
||||
|
||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||
|
||||
|
@ -7,10 +7,10 @@ public class AgeRestrictionDto
|
||||
/// <summary>
|
||||
/// The maximum age rating a user has access to. -1 if not applicable
|
||||
/// </summary>
|
||||
public AgeRating AgeRating { get; set; } = AgeRating.NotApplicable;
|
||||
public required AgeRating AgeRating { get; set; } = AgeRating.NotApplicable;
|
||||
/// <summary>
|
||||
/// Are Unknowns explicitly allowed against age rating
|
||||
/// </summary>
|
||||
/// <remarks>Unknown is always lowest and default age rating. Setting this to false will ensure Teen age rating applies and unknowns are still filtered</remarks>
|
||||
public bool IncludeUnknowns { get; set; } = false;
|
||||
public required bool IncludeUnknowns { get; set; } = false;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs.Account;
|
||||
|
||||
@ -17,5 +18,4 @@ public record UpdateUserDto
|
||||
/// An Age Rating which will limit the account to seeing everything equal to or below said rating.
|
||||
/// </summary>
|
||||
public AgeRestrictionDto AgeRestriction { get; init; } = default!;
|
||||
|
||||
}
|
||||
|
@ -12,6 +12,10 @@ public class MemberDto
|
||||
public int Id { get; init; }
|
||||
public string? Username { get; init; }
|
||||
public string? Email { get; init; }
|
||||
/// <summary>
|
||||
/// If the member is still pending or not
|
||||
/// </summary>
|
||||
public bool IsPending { get; init; }
|
||||
public AgeRestrictionDto? AgeRestriction { get; init; }
|
||||
public DateTime Created { get; init; }
|
||||
public DateTime LastActive { get; init; }
|
||||
|
@ -24,7 +24,4 @@ public class UpdateLibraryDto
|
||||
public bool IncludeInSearch { get; init; }
|
||||
[Required]
|
||||
public bool ManageCollections { get; init; }
|
||||
[Required]
|
||||
public bool CollapseSeriesRelationships { get; init; }
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Enums.UserPreferences;
|
||||
@ -137,4 +137,9 @@ public class UserPreferencesDto
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool NoTransitions { get; set; } = false;
|
||||
/// <summary>
|
||||
/// When showing series, only parent series or series with no relationships will be returned
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool CollapseSeriesRelationships { get; set; } = false;
|
||||
}
|
||||
|
36
API/Data/MigrateLoginRole.cs
Normal file
36
API/Data/MigrateLoginRole.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Added in v0.7.1.18
|
||||
/// </summary>
|
||||
public static class MigrateLoginRoles
|
||||
{
|
||||
/// <summary>
|
||||
/// Will not run if any users have the <see cref="PolicyConstants.LoginRole"/> role already
|
||||
/// </summary>
|
||||
/// <param name="unitOfWork"></param>
|
||||
/// <param name="userManager"></param>
|
||||
/// <param name="logger"></param>
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, UserManager<AppUser> userManager, ILogger<Program> logger)
|
||||
{
|
||||
var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.LoginRole);
|
||||
if (usersWithRole.Count != 0) return;
|
||||
|
||||
logger.LogCritical("Running MigrateLoginRoles migration");
|
||||
|
||||
var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync();
|
||||
foreach (var user in allUsers)
|
||||
{
|
||||
await userManager.RemoveFromRoleAsync(user, PolicyConstants.LoginRole);
|
||||
await userManager.AddToRoleAsync(user, PolicyConstants.LoginRole);
|
||||
}
|
||||
|
||||
logger.LogInformation("MigrateLoginRoles migration complete");
|
||||
}
|
||||
}
|
1858
API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs
generated
Normal file
1858
API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MoveCollapseSeriesToUserPref : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CollapseSeriesRelationships",
|
||||
table: "AppUserPreferences",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CollapseSeriesRelationships",
|
||||
table: "AppUserPreferences");
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
@ -221,10 +221,10 @@ namespace API.Data.Migrations
|
||||
b.Property<int>("BookReaderReadingDirection")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookReaderWritingStyle")
|
||||
b.Property<bool>("BookReaderTapToPaginate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("BookReaderTapToPaginate")
|
||||
b.Property<int>("BookReaderWritingStyle")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BookThemeName")
|
||||
@ -232,6 +232,9 @@ namespace API.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("Dark");
|
||||
|
||||
b.Property<bool>("CollapseSeriesRelationships")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EmulateBook")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -601,9 +604,6 @@ namespace API.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("CollapseSeriesRelationships")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -770,9 +770,9 @@ public class SeriesRepository : ISeriesRepository
|
||||
// NOTE: Why do we even have libraryId when the filter has the actual libraryIds?
|
||||
var userLibraries = await GetUserLibrariesForFilteredQuery(libraryId, userId, queryContext);
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var onlyParentSeries = await _context.Library.AsNoTracking()
|
||||
.Where(l => filter.Libraries.Contains(l.Id))
|
||||
.AllAsync(l => l.CollapseSeriesRelationships);
|
||||
var onlyParentSeries = await _context.AppUserPreferences.Where(u => u.AppUserId == userId)
|
||||
.Select(u => u.CollapseSeriesRelationships)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries,
|
||||
out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter,
|
||||
|
@ -39,8 +39,7 @@ public interface IUserRepository
|
||||
void Add(AppUserBookmark bookmark);
|
||||
public void Delete(AppUser? user);
|
||||
void Delete(AppUserBookmark bookmark);
|
||||
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync();
|
||||
Task<IEnumerable<MemberDto>> GetPendingMemberDtosAsync();
|
||||
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true);
|
||||
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
|
||||
Task<bool> IsUserAdminAsync(AppUser? user);
|
||||
Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId);
|
||||
@ -329,10 +328,10 @@ public class UserRepository : IUserRepository
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync()
|
||||
public async Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true)
|
||||
{
|
||||
return await _context.Users
|
||||
.Where(u => u.EmailConfirmed)
|
||||
.Where(u => (emailConfirmed && u.EmailConfirmed) || !emailConfirmed)
|
||||
.Include(x => x.Libraries)
|
||||
.Include(r => r.UserRoles)
|
||||
.ThenInclude(r => r.Role)
|
||||
@ -344,45 +343,8 @@ public class UserRepository : IUserRepository
|
||||
Email = u.Email,
|
||||
Created = u.Created,
|
||||
LastActive = u.LastActive,
|
||||
Roles = u.UserRoles.Select(r => r.Role.Name).ToList()!,
|
||||
AgeRestriction = new AgeRestrictionDto()
|
||||
{
|
||||
AgeRating = u.AgeRestriction,
|
||||
IncludeUnknowns = u.AgeRestrictionIncludeUnknowns
|
||||
},
|
||||
Libraries = u.Libraries.Select(l => new LibraryDto
|
||||
{
|
||||
Name = l.Name,
|
||||
Type = l.Type,
|
||||
LastScanned = l.LastScanned,
|
||||
Folders = l.Folders.Select(x => x.Path).ToList()
|
||||
}).ToList()
|
||||
})
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of users that are considered Pending by invite. This means email is unconfirmed and they have never logged in
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<MemberDto>> GetPendingMemberDtosAsync()
|
||||
{
|
||||
return await _context.Users
|
||||
.Where(u => !u.EmailConfirmed && u.LastActive == DateTime.MinValue)
|
||||
.Include(x => x.Libraries)
|
||||
.Include(r => r.UserRoles)
|
||||
.ThenInclude(r => r.Role)
|
||||
.OrderBy(u => u.UserName)
|
||||
.Select(u => new MemberDto
|
||||
{
|
||||
Id = u.Id,
|
||||
Username = u.UserName,
|
||||
Email = u.Email,
|
||||
Created = u.Created,
|
||||
LastActive = u.LastActive,
|
||||
Roles = u.UserRoles.Select(r => r.Role.Name).ToList()!,
|
||||
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
|
||||
IsPending = !u.EmailConfirmed,
|
||||
AgeRestriction = new AgeRestrictionDto()
|
||||
{
|
||||
AgeRating = u.AgeRestriction,
|
||||
|
@ -52,6 +52,7 @@ public class AppUser : IdentityUser<int>, IHasConcurrencyToken
|
||||
/// </summary>
|
||||
public bool AgeRestrictionIncludeUnknowns { get; set; } = false;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
[ConcurrencyCheck]
|
||||
public uint RowVersion { get; private set; }
|
||||
|
@ -119,6 +119,10 @@ public class AppUserPreferences
|
||||
/// UI Site Global Setting: Should Kavita disable CSS transitions
|
||||
/// </summary>
|
||||
public bool NoTransitions { get; set; } = false;
|
||||
/// <summary>
|
||||
/// UI Site Global Setting: When showing series, only parent series or series with no relationships will be returned
|
||||
/// </summary>
|
||||
public bool CollapseSeriesRelationships { get; set; } = false;
|
||||
|
||||
public AppUser AppUser { get; set; } = null!;
|
||||
public int AppUserId { get; set; }
|
||||
|
@ -31,10 +31,6 @@ public class Library : IEntityDate
|
||||
/// Should this library create and manage collections from Metadata
|
||||
/// </summary>
|
||||
public bool ManageCollections { get; set; } = true;
|
||||
/// <summary>
|
||||
/// When showing series, only parent series or series with no relationships will be returned
|
||||
/// </summary>
|
||||
public bool CollapseSeriesRelationships { get; set; } = false;
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
|
@ -7,9 +7,9 @@ namespace API.Entities;
|
||||
public class Person
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? NormalizedName { get; set; }
|
||||
public PersonRole Role { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string NormalizedName { get; set; }
|
||||
public required PersonRole Role { get; set; }
|
||||
|
||||
// Relationships
|
||||
public ICollection<SeriesMetadata> SeriesMetadatas { get; set; } = null!;
|
||||
|
@ -1,13 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Tests.Helpers.Builders;
|
||||
namespace API.Helpers.Builders;
|
||||
|
||||
public class ChapterBuilder : IEntityBuilder<Chapter>
|
||||
{
|
||||
private Chapter _chapter;
|
||||
private readonly Chapter _chapter;
|
||||
public Chapter Build() => _chapter;
|
||||
|
||||
public ChapterBuilder(string number, string? range=null)
|
@ -1,4 +1,4 @@
|
||||
namespace API.Tests.Helpers.Builders;
|
||||
namespace API.Helpers.Builders;
|
||||
|
||||
public interface IEntityBuilder<out T>
|
||||
{
|
32
API/Helpers/Builders/PersonBuilder.cs
Normal file
32
API/Helpers/Builders/PersonBuilder.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
|
||||
namespace API.Helpers.Builders;
|
||||
|
||||
public class PersonBuilder : IEntityBuilder<Person>
|
||||
{
|
||||
private readonly Person _person;
|
||||
public Person Build() => _person;
|
||||
|
||||
public PersonBuilder(string name, PersonRole role)
|
||||
{
|
||||
_person = new Person()
|
||||
{
|
||||
Name = name.Trim(),
|
||||
NormalizedName = name.ToNormalized(),
|
||||
Role = role,
|
||||
ChapterMetadatas = new List<Chapter>(),
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
};
|
||||
}
|
||||
|
||||
public PersonBuilder WithSeriesMetadata(SeriesMetadata metadata)
|
||||
{
|
||||
_person.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||
_person.SeriesMetadatas.Add(metadata);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
|
||||
namespace API.Tests.Helpers.Builders;
|
||||
namespace API.Helpers.Builders;
|
||||
|
||||
public class SeriesBuilder : IEntityBuilder<Series>
|
||||
{
|
@ -1,14 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
|
||||
namespace API.Tests.Helpers.Builders;
|
||||
namespace API.Helpers.Builders;
|
||||
|
||||
public class SeriesMetadataBuilder : IEntityBuilder<SeriesMetadata>
|
||||
{
|
||||
private SeriesMetadata _seriesMetadata;
|
||||
private readonly SeriesMetadata _seriesMetadata;
|
||||
public SeriesMetadata Build() => _seriesMetadata;
|
||||
|
||||
public SeriesMetadataBuilder()
|
@ -3,7 +3,7 @@ using System.Linq;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
|
||||
namespace API.Tests.Helpers.Builders;
|
||||
namespace API.Helpers.Builders;
|
||||
|
||||
public class VolumeBuilder : IEntityBuilder<Volume>
|
||||
{
|
@ -85,7 +85,7 @@ public static class PersonHelper
|
||||
foreach (var person in existingPeople)
|
||||
{
|
||||
var existingPerson = removeAllExcept
|
||||
.FirstOrDefault(p => person.NormalizedName != null && p.Role == person.Role && person.NormalizedName.Equals(p.NormalizedName));
|
||||
.FirstOrDefault(p => p.Role == person.Role && person.NormalizedName.Equals(p.NormalizedName));
|
||||
if (existingPerson == null)
|
||||
{
|
||||
action?.Invoke(person);
|
||||
@ -100,8 +100,9 @@ public static class PersonHelper
|
||||
/// <param name="person"></param>
|
||||
public static void AddPersonIfNotExists(ICollection<Person> metadataPeople, Person person)
|
||||
{
|
||||
if (string.IsNullOrEmpty(person.Name)) return;
|
||||
var existingPerson = metadataPeople.SingleOrDefault(p =>
|
||||
p.NormalizedName == person.Name?.ToNormalized() && p.Role == person.Role);
|
||||
p.NormalizedName == person.Name.ToNormalized() && p.Role == person.Role);
|
||||
if (existingPerson == null)
|
||||
{
|
||||
metadataPeople.Add(person);
|
||||
|
@ -348,8 +348,9 @@ public class BookmarkService : IBookmarkService
|
||||
/// <param name="filename">The file to convert</param>
|
||||
/// <param name="targetFolder">Full path to where files should be stored or any stem</param>
|
||||
/// <returns></returns>
|
||||
private async Task<string> SaveAsWebP(string imageDirectory, string filename, string targetFolder)
|
||||
public async Task<string> SaveAsWebP(string imageDirectory, string filename, string targetFolder)
|
||||
{
|
||||
// This must be Public as it's used in via Hangfire as a background task
|
||||
var fullSourcePath = _directoryService.FileSystem.Path.Join(imageDirectory, filename);
|
||||
var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(filename).Name, string.Empty);
|
||||
|
||||
|
@ -182,7 +182,12 @@ public class SeriesService : ISeriesService
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await _unitOfWork.CommitAsync() && updateSeriesMetadataDto.CollectionTags != null)
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
// Trigger code to cleanup tags, collections, people, etc
|
||||
await _taskScheduler.CleanupDbEntries();
|
||||
|
||||
if (updateSeriesMetadataDto.CollectionTags != null)
|
||||
{
|
||||
foreach (var tag in updateSeriesMetadataDto.CollectionTags)
|
||||
{
|
||||
|
@ -31,6 +31,7 @@ public interface ITaskScheduler
|
||||
Task RunStatCollection();
|
||||
void ScanSiteThemes();
|
||||
Task CovertAllCoversToWebP();
|
||||
Task CleanupDbEntries();
|
||||
}
|
||||
public class TaskScheduler : ITaskScheduler
|
||||
{
|
||||
@ -230,6 +231,11 @@ public class TaskScheduler : ITaskScheduler
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task CleanupDbEntries()
|
||||
{
|
||||
await _cleanupService.CleanupDbEntries();
|
||||
}
|
||||
|
||||
public void ScanLibraries()
|
||||
{
|
||||
if (RunningAnyTasksByMethod(ScanTasks, ScanQueue))
|
||||
|
@ -236,6 +236,11 @@ public class ParseScannedFiles
|
||||
p.Key.Format == info.Format)
|
||||
.Key;
|
||||
|
||||
if (existingName == null)
|
||||
{
|
||||
return info.Series;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(existingName.Name))
|
||||
{
|
||||
return existingName.Name;
|
||||
@ -297,7 +302,7 @@ public class ParseScannedFiles
|
||||
|
||||
_logger.LogDebug("[ScannerService] Found {Count} files for {Folder}", files.Count, folder);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.FileScanProgressEvent(folder, libraryName, ProgressEventType.Updated));
|
||||
MessageFactory.FileScanProgressEvent($"{files.Count} files in {folder}", libraryName, ProgressEventType.Updated));
|
||||
if (files.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("[ScannerService] {Folder} is empty or is no longer in this location", folder);
|
||||
|
@ -433,7 +433,7 @@ public class ProcessSeries : IProcessSeries
|
||||
var genres = chapters.SelectMany(c => c.Genres).ToList();
|
||||
GenreHelper.KeepOnlySameGenreBetweenLists(series.Metadata.Genres.ToList(), genres, genre =>
|
||||
{
|
||||
if (series.Metadata.GenresLocked) return;
|
||||
if (series.Metadata.GenresLocked) return; // NOTE: Doesn't it make sense to do the locked skip outside this loop?
|
||||
series.Metadata.Genres.Remove(genre);
|
||||
});
|
||||
|
||||
|
@ -520,12 +520,14 @@ public class ScannerService : IScannerService
|
||||
|
||||
var scanElapsedTime = await ScanFiles(library, libraryFolderPaths, shouldUseLibraryScan, TrackFiles, forceUpdate);
|
||||
|
||||
// NOTE: This runs sync after every file is scanned
|
||||
foreach (var task in processTasks)
|
||||
{
|
||||
await task();
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent(string.Empty, library.Name, ProgressEventType.Ended));
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.FileScanProgressEvent(string.Empty, library.Name, ProgressEventType.Ended));
|
||||
|
||||
_logger.LogInformation("[ScannerService] Finished file scan in {ScanAndUpdateTime} milliseconds. Updating database", scanElapsedTime);
|
||||
|
||||
|
@ -214,6 +214,7 @@ public class Startup
|
||||
|
||||
|
||||
logger.LogInformation("Running Migrations");
|
||||
|
||||
// Only run this if we are upgrading
|
||||
await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager);
|
||||
await MigrateRemoveExtraThemes.Migrate(unitOfWork, themeService);
|
||||
@ -235,6 +236,9 @@ public class Startup
|
||||
// v0.7
|
||||
await MigrateBrokenGMT1Dates.Migrate(unitOfWork, dataContext, logger);
|
||||
|
||||
// v0.7.2
|
||||
await MigrateLoginRoles.Migrate(unitOfWork, userManager, logger);
|
||||
|
||||
// Update the version in the DB after all migrations are run
|
||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
installVersion.Value = BuildInfo.Version.ToString();
|
||||
|
@ -10,4 +10,5 @@ export interface Member {
|
||||
roles: string[];
|
||||
libraries: Library[];
|
||||
ageRestriction: AgeRestriction;
|
||||
isPending: boolean;
|
||||
}
|
@ -40,6 +40,7 @@ export interface Preferences {
|
||||
blurUnreadSummaries: boolean;
|
||||
promptForDownloadSize: boolean;
|
||||
noTransitions: boolean;
|
||||
collapseSeriesRelationships: boolean;
|
||||
}
|
||||
|
||||
export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}];
|
||||
|
@ -12,8 +12,8 @@ export class MemberService {
|
||||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
getMembers() {
|
||||
return this.httpClient.get<Member[]>(this.baseUrl + 'users');
|
||||
getMembers(includePending: boolean = false) {
|
||||
return this.httpClient.get<Member[]>(this.baseUrl + 'users?includePending=' + includePending);
|
||||
}
|
||||
|
||||
getMemberNames() {
|
||||
|
@ -1,40 +1,11 @@
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<ng-container>
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>Pending Invites</h3></div>
|
||||
<div class="col-4"><button class="btn btn-primary float-end" (click)="inviteUser()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Invite</span></button></div>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item no-hover" *ngFor="let invite of pendingInvites; let idx = index;">
|
||||
<div>
|
||||
<h4>
|
||||
<span id="member-name--{{idx}}">{{invite.username | titlecase}} </span>
|
||||
<div class="float-end">
|
||||
<button class="btn btn-danger btn-sm me-2" (click)="deleteUser(invite)">Cancel</button>
|
||||
<button class="btn btn-secondary btn-sm me-2" (click)="resendEmail(invite)">Resend</button>
|
||||
<button class="btn btn-secondary btn-sm" (click)="setup(invite)">Setup</button>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>Active Users</h3></div>
|
||||
<div class="col-4"><button class="btn btn-primary float-end" (click)="inviteUser()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Invite</span></button></div>
|
||||
</div>
|
||||
|
||||
<div>Invited: {{invite.created | date: 'short'}}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="loadingMembers" class="list-group-item no-hover">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item no-hover" *ngIf="pendingInvites.length === 0 && !loadingMembers">
|
||||
There are no invited Users
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
<h3 class="mt-3">Active Users</h3>
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let member of members; let idx = index;" class="list-group-item no-hover">
|
||||
<div>
|
||||
@ -45,10 +16,14 @@
|
||||
<i class="fas fa-star" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">(You)</span>
|
||||
</span>
|
||||
<span class="badge bg-secondary text-dark" *ngIf="member.isPending">Pending</span>
|
||||
<div class="float-end" *ngIf="canEditMember(member)">
|
||||
<button class="btn btn-danger btn-sm me-2" (click)="deleteUser(member)" placement="top" ngbTooltip="Delete User" attr.aria-label="Delete User {{member.username | titlecase}}"><i class="fa fa-trash" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-secondary btn-sm me-2" (click)="updatePassword(member)" placement="top" ngbTooltip="Change Password" attr.aria-label="Change Password for {{member.username | titlecase}}"><i class="fa fa-key" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-primary btn-sm" (click)="openEditUser(member)" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{member.username | titlecase}}"><i class="fa fa-pen" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-primary btn-sm me-2" (click)="openEditUser(member)" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{member.username | titlecase}}"><i class="fa fa-pen" aria-hidden="true"></i></button>
|
||||
|
||||
<button *ngIf="member.isPending" class="btn btn-secondary btn-sm me-2" (click)="resendEmail(member)" placement="top" ngbTooltip="Resend Invite" attr.aria-label="Delete Invite {{member.username | titlecase}}">Resend</button>
|
||||
<button *ngIf="member.isPending" class="btn btn-secondary btn-sm me-2" (click)="setup(member)" placement="top" ngbTooltip="Setup User" attr.aria-label="Setup User {{member.username | titlecase}}">Setup</button>
|
||||
<button *ngIf="!member.isPending" class="btn btn-secondary btn-sm" (click)="updatePassword(member)" placement="top" ngbTooltip="Change Password" attr.aria-label="Change Password for {{member.username | titlecase}}"><i class="fa fa-key" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="user-info">
|
||||
|
@ -58,7 +58,7 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
|
||||
loadMembers() {
|
||||
this.loadingMembers = true;
|
||||
this.memberService.getMembers().subscribe(members => {
|
||||
this.memberService.getMembers(true).subscribe(members => {
|
||||
this.members = members;
|
||||
// Show logged in user at the top of the list
|
||||
this.members.sort((a: Member, b: Member) => {
|
||||
|
@ -53,8 +53,15 @@ export class RoleSelectorComponent implements OnInit {
|
||||
foundRole[0].selected = true;
|
||||
}
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
} else {
|
||||
// For new users, preselect LoginRole
|
||||
this.selectedRoles.forEach(role => {
|
||||
if (role.data == 'Login') {
|
||||
role.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleModelUpdate() {
|
||||
|
@ -63,6 +63,9 @@
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed" fillStyle="filled">{{unreadCount}}</app-tag-badge>
|
||||
</div>
|
||||
<app-image height="100%" maxHeight="400px" objectFit="contain" background="none" [imageUrl]="seriesImage"></app-image>
|
||||
<div class="progress-banner" *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
|
||||
<ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar>
|
||||
</div>
|
||||
<div class="under-image" *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
|
||||
Continue {{ContinuePointTitle}}
|
||||
</div>
|
||||
|
@ -53,3 +53,7 @@
|
||||
.info-container {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
::ng-deep .progress {
|
||||
border-radius: 0px;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SeriesDetailRoutingModule } from './series-detail-routing.module';
|
||||
import { NgbCollapseModule, NgbNavModule, NgbRatingModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgbCollapseModule, NgbNavModule, NgbProgressbarModule, NgbRatingModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SeriesMetadataDetailComponent } from './_components/series-metadata-detail/series-metadata-detail.component';
|
||||
import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
@ -26,6 +26,7 @@ import { SeriesDetailComponent } from './_components/series-detail/series-detail
|
||||
NgbNavModule,
|
||||
NgbRatingModule,
|
||||
NgbTooltipModule, // Series Detail, Extras Drawer
|
||||
NgbProgressbarModule,
|
||||
|
||||
TypeaheadModule,
|
||||
PipeModule,
|
||||
|
@ -106,20 +106,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="collapse-relationships" role="switch" formControlName="collapseSeriesRelationships" class="form-check-input" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="collapse-relationships">Collapse Series Relationships</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="accent">
|
||||
Experiemental: Should Kavita show Series that have no relationships or is the parent/prequel
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
|
@ -101,7 +101,7 @@ export class ServerStatsComponent implements OnDestroy {
|
||||
ref.componentInstance.title = 'Genres';
|
||||
ref.componentInstance.clicked = (item: string) => {
|
||||
const params: any = {};
|
||||
params[FilterQueryParam.Genres] = item;
|
||||
params[FilterQueryParam.Genres] = genres.filter(g => g.title === item)[0].id;
|
||||
params[FilterQueryParam.Page] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
};
|
||||
@ -115,7 +115,7 @@ export class ServerStatsComponent implements OnDestroy {
|
||||
ref.componentInstance.title = 'Tags';
|
||||
ref.componentInstance.clicked = (item: string) => {
|
||||
const params: any = {};
|
||||
params[FilterQueryParam.Tags] = item;
|
||||
params[FilterQueryParam.Tags] = tags.filter(g => g.title === item)[0].id;
|
||||
params[FilterQueryParam.Page] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
};
|
||||
|
@ -77,6 +77,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="collapse-relationships" role="switch" formControlName="collapseSeriesRelationships" aria-describedby="settings-collapse-relationships-help" class="form-check-input" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="collapse-relationships">Collapse Series Relationships</label>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #noTransitionsTooltip>Experiemental: Should Kavita show Series that have no relationships or is the parent/prequel</ng-template>
|
||||
<span class="visually-hidden" id="settings-collapse-relationships-help">Experiemental: Should Kavita show Series that have no relationships or is the parent/prequel</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">Save</button>
|
||||
|
@ -156,6 +156,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
this.settingsForm.addControl('blurUnreadSummaries', new FormControl(this.user.preferences.blurUnreadSummaries, []));
|
||||
this.settingsForm.addControl('promptForDownloadSize', new FormControl(this.user.preferences.promptForDownloadSize, []));
|
||||
this.settingsForm.addControl('noTransitions', new FormControl(this.user.preferences.noTransitions, []));
|
||||
this.settingsForm.addControl('collapseSeriesRelationships', new FormControl(this.user.preferences.collapseSeriesRelationships, []));
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
@ -202,6 +203,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
this.settingsForm.get('noTransitions')?.setValue(this.user.preferences.noTransitions);
|
||||
this.settingsForm.get('emulateBook')?.setValue(this.user.preferences.emulateBook);
|
||||
this.settingsForm.get('swipeToPaginate')?.setValue(this.user.preferences.swipeToPaginate);
|
||||
this.settingsForm.get('collapseSeriesRelationships')?.setValue(this.user.preferences.collapseSeriesRelationships);
|
||||
this.cdRef.markForCheck();
|
||||
this.settingsForm.markAsPristine();
|
||||
}
|
||||
@ -234,7 +236,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
promptForDownloadSize: modelSettings.promptForDownloadSize,
|
||||
noTransitions: modelSettings.noTransitions,
|
||||
emulateBook: modelSettings.emulateBook,
|
||||
swipeToPaginate: modelSettings.swipeToPaginate
|
||||
swipeToPaginate: modelSettings.swipeToPaginate,
|
||||
collapseSeriesRelationships: modelSettings.collapseSeriesRelationships
|
||||
};
|
||||
|
||||
this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||
|
72
openapi.json
72
openapi.json
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.1.13"
|
||||
"version": "0.7.1.16"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@ -8926,43 +8926,17 @@
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemberDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemberDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemberDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
"summary": "Returns all users of this server",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "includePending",
|
||||
"in": "query",
|
||||
"description": "This will include pending members",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Users/pending": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
@ -9811,6 +9785,10 @@
|
||||
"type": "boolean",
|
||||
"description": "UI Site Global Setting: Should Kavita disable CSS transitions"
|
||||
},
|
||||
"collapseSeriesRelationships": {
|
||||
"type": "boolean",
|
||||
"description": "UI Site Global Setting: When showing series, only parent series or series with no relationships will be returned"
|
||||
},
|
||||
"appUser": {
|
||||
"$ref": "#/components/schemas/AppUser"
|
||||
},
|
||||
@ -11729,10 +11707,6 @@
|
||||
"type": "boolean",
|
||||
"description": "Should this library create and manage collections from Metadata"
|
||||
},
|
||||
"collapseSeriesRelationships": {
|
||||
"type": "boolean",
|
||||
"description": "When showing series, only parent series or series with no relationships will be returned"
|
||||
},
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
@ -12075,6 +12049,10 @@
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"isPending": {
|
||||
"type": "boolean",
|
||||
"description": "If the member is still pending or not"
|
||||
},
|
||||
"ageRestriction": {
|
||||
"$ref": "#/components/schemas/AgeRestrictionDto"
|
||||
},
|
||||
@ -14130,6 +14108,11 @@
|
||||
"description": "Name of the Theme",
|
||||
"nullable": true
|
||||
},
|
||||
"normalizedName": {
|
||||
"type": "string",
|
||||
"description": "Normalized name for lookups",
|
||||
"nullable": true
|
||||
},
|
||||
"fileName": {
|
||||
"type": "string",
|
||||
"description": "File path to the content. Stored under API.Services.DirectoryService.SiteThemeDirectory.\r\nMust be a .css file",
|
||||
@ -14367,7 +14350,6 @@
|
||||
},
|
||||
"UpdateLibraryDto": {
|
||||
"required": [
|
||||
"collapseSeriesRelationships",
|
||||
"folders",
|
||||
"folderWatching",
|
||||
"id",
|
||||
@ -14411,9 +14393,6 @@
|
||||
},
|
||||
"manageCollections": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"collapseSeriesRelationships": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@ -14964,6 +14943,7 @@
|
||||
"bookReaderTapToPaginate",
|
||||
"bookReaderThemeName",
|
||||
"bookReaderWritingStyle",
|
||||
"collapseSeriesRelationships",
|
||||
"emulateBook",
|
||||
"globalPageLayoutMode",
|
||||
"layoutMode",
|
||||
@ -15073,6 +15053,10 @@
|
||||
"noTransitions": {
|
||||
"type": "boolean",
|
||||
"description": "UI Site Global Setting: Should Kavita disable CSS transitions"
|
||||
},
|
||||
"collapseSeriesRelationships": {
|
||||
"type": "boolean",
|
||||
"description": "When showing series, only parent series or series with no relationships will be returned"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
Loading…
x
Reference in New Issue
Block a user