mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Release Polish (#1586)
* Fixed a scaling issue in the epub reader, where images could scale when they shouldn't. * Removed some caching on library/ api and added more output for a foreign key constraint * Hooked in Restricted Profile stat collection * Added a new boolean on age restrictions to explicitly allow unknowns or not. Since unknown is the default state of metadata, if users are allowed access to Unknown, age restricted content could leak. * Fixed a bug where sometimes series cover generation could fail under conditions where only specials existed. * Fixed foreign constraint issue when cleaning up series not seen at end of scan loop * Removed an additional epub parse when scanning and handled merging differently * Code smell
This commit is contained in:
parent
78762a5626
commit
9149c4cbca
@ -1,4 +1,7 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Data.Misc;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using Xunit;
|
||||
|
||||
@ -132,4 +135,33 @@ public class EnumerableExtensionsTests
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, 2)]
|
||||
[InlineData(false, 1)]
|
||||
public void RestrictAgainstAgeRestriction_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||
{
|
||||
var items = new List<RecentlyAddedSeries>()
|
||||
{
|
||||
new RecentlyAddedSeries()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
},
|
||||
new RecentlyAddedSeries()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
},
|
||||
new RecentlyAddedSeries()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus,
|
||||
},
|
||||
};
|
||||
|
||||
var filtered = items.RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
IncludeUnknowns = includeUnknowns
|
||||
});
|
||||
Assert.Equal(expectedCount, filtered.Count());
|
||||
}
|
||||
}
|
||||
|
131
API.Tests/Extensions/QueryableExtensionsTests.cs
Normal file
131
API.Tests/Extensions/QueryableExtensionsTests.cs
Normal file
@ -0,0 +1,131 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Data.Misc;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Extensions;
|
||||
|
||||
public class QueryableExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true, 2)]
|
||||
[InlineData(false, 1)]
|
||||
public void RestrictAgainstAgeRestriction_Series_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||
{
|
||||
var items = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
Metadata = new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
},
|
||||
new Series()
|
||||
{
|
||||
Metadata = new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
}
|
||||
},
|
||||
new Series()
|
||||
{
|
||||
Metadata = new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
IncludeUnknowns = includeUnknowns
|
||||
});
|
||||
Assert.Equal(expectedCount, filtered.Count());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, 2)]
|
||||
[InlineData(false, 1)]
|
||||
public void RestrictAgainstAgeRestriction_CollectionTag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||
{
|
||||
var items = new List<CollectionTag>()
|
||||
{
|
||||
new CollectionTag()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new CollectionTag()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
},
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new CollectionTag()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
IncludeUnknowns = includeUnknowns
|
||||
});
|
||||
Assert.Equal(expectedCount, filtered.Count());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, 2)]
|
||||
[InlineData(false, 1)]
|
||||
public void RestrictAgainstAgeRestriction_ReadingList_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||
{
|
||||
var items = new List<ReadingList>()
|
||||
{
|
||||
new ReadingList()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
},
|
||||
new ReadingList()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
},
|
||||
new ReadingList()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus
|
||||
},
|
||||
};
|
||||
|
||||
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
IncludeUnknowns = includeUnknowns
|
||||
});
|
||||
Assert.Equal(expectedCount, filtered.Count());
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Comparators;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
@ -81,11 +83,286 @@ public class SeriesExtensionsTests
|
||||
NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Services.Tasks.Scanner.Parser.Parser.Normalize(seriesInput[0]),
|
||||
Metadata = new SeriesMetadata()
|
||||
};
|
||||
var info = new ParserInfo();
|
||||
info.Series = parserSeries;
|
||||
var info = new ParserInfo
|
||||
{
|
||||
Series = parserSeries
|
||||
};
|
||||
|
||||
Assert.Equal(expected, series.NameInParserInfo(info));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_MultipleSpecials_Comics()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 2",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal("Special 1", series.GetCoverImage());
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_MultipleSpecials_Books()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Epub,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 2",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal("Special 1", series.GetCoverImage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_JustChapters_Comics()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2.5",
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2",
|
||||
CoverImage = "Special 2",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||
}
|
||||
|
||||
Assert.Equal("Special 2", series.GetCoverImage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_JustChaptersAndSpecials_Comics()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2.5",
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2",
|
||||
CoverImage = "Special 2",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 3",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||
}
|
||||
|
||||
Assert.Equal("Special 2", series.GetCoverImage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_VolumesChapters_Comics()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2.5",
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2",
|
||||
CoverImage = "Special 2",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 3",
|
||||
}
|
||||
},
|
||||
},
|
||||
new Volume()
|
||||
{
|
||||
Number = 1,
|
||||
Name = "1",
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "0",
|
||||
CoverImage = "Volume 1",
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||
}
|
||||
|
||||
Assert.Equal("Volume 1", series.GetCoverImage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_VolumesChaptersAndSpecials_Comics()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2.5",
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2",
|
||||
CoverImage = "Special 2",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 3",
|
||||
}
|
||||
},
|
||||
},
|
||||
new Volume()
|
||||
{
|
||||
Number = 1,
|
||||
Name = "1",
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "0",
|
||||
CoverImage = "Volume 1",
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||
}
|
||||
|
||||
Assert.Equal("Volume 1", series.GetCoverImage());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -140,12 +140,12 @@ public class SeriesRepositoryTests
|
||||
|
||||
[InlineData("Heion Sedai no Idaten-tachi", "", MangaFormat.Archive, "The Idaten Deities Know Only Peace")] // Matching on localized name in DB
|
||||
[InlineData("Heion Sedai no Idaten-tachi", "", MangaFormat.Pdf, null)]
|
||||
public async Task GetFullSeriesByAnyName_Should(string seriesName, string localizedName, string? expected)
|
||||
public async Task GetFullSeriesByAnyName_Should(string seriesName, MangaFormat format, string localizedName, string? expected)
|
||||
{
|
||||
var firstSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName,
|
||||
1, MangaFormat.Unknown);
|
||||
1, format);
|
||||
if (expected == null)
|
||||
{
|
||||
Assert.Null(series);
|
||||
@ -157,4 +157,6 @@ public class SeriesRepositoryTests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//public async Task
|
||||
}
|
||||
|
@ -351,7 +351,7 @@ public class CleanupServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||
ds);
|
||||
cleanupService.CleanupCacheDirectory();
|
||||
cleanupService.CleanupCacheAndTempDirectories();
|
||||
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories));
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@ public class CleanupServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||
ds);
|
||||
cleanupService.CleanupCacheDirectory();
|
||||
cleanupService.CleanupCacheAndTempDirectories();
|
||||
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories));
|
||||
}
|
||||
|
||||
|
@ -366,7 +366,9 @@ public class AccountController : BaseApiController
|
||||
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRestriction;
|
||||
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating;
|
||||
user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns;
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
@ -455,7 +457,9 @@ public class AccountController : BaseApiController
|
||||
lib.AppUsers.Add(user);
|
||||
}
|
||||
|
||||
user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction;
|
||||
user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating;
|
||||
user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns;
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
|
||||
@ -570,7 +574,8 @@ public class AccountController : BaseApiController
|
||||
lib.AppUsers.Add(user);
|
||||
}
|
||||
|
||||
user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction;
|
||||
user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating;
|
||||
user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns;
|
||||
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
if (string.IsNullOrEmpty(token))
|
||||
|
@ -113,7 +113,6 @@ public class LibraryController : BaseApiController
|
||||
}
|
||||
|
||||
|
||||
[ResponseCache(CacheProfileName = "10Minute")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
|
||||
{
|
||||
|
@ -72,7 +72,7 @@ public class ServerController : BaseApiController
|
||||
public ActionResult ClearCache()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", User.GetUsername());
|
||||
_cleanupService.CleanupCacheDirectory();
|
||||
_cleanupService.CleanupCacheAndTempDirectories();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
16
API/DTOs/Account/AgeRestrictionDto.cs
Normal file
16
API/DTOs/Account/AgeRestrictionDto.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.Account;
|
||||
|
||||
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;
|
||||
/// <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;
|
||||
}
|
@ -20,5 +20,5 @@ public class InviteUserDto
|
||||
/// <summary>
|
||||
/// An Age Rating which will limit the account to seeing everything equal to or below said rating.
|
||||
/// </summary>
|
||||
public AgeRating AgeRestriction { get; set; }
|
||||
public AgeRestrictionDto AgeRestriction { get; set; }
|
||||
}
|
||||
|
@ -6,5 +6,7 @@ namespace API.DTOs.Account;
|
||||
public class UpdateAgeRestrictionDto
|
||||
{
|
||||
[Required]
|
||||
public AgeRating AgeRestriction { get; set; }
|
||||
public AgeRating AgeRating { get; set; }
|
||||
[Required]
|
||||
public bool IncludeUnknowns { get; set; }
|
||||
}
|
||||
|
@ -19,6 +19,6 @@ public record UpdateUserDto
|
||||
/// <summary>
|
||||
/// An Age Rating which will limit the account to seeing everything equal to or below said rating.
|
||||
/// </summary>
|
||||
public AgeRating AgeRestriction { get; init; }
|
||||
public AgeRestrictionDto AgeRestriction { get; init; }
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Data.Misc;
|
||||
using API.DTOs.Account;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs;
|
||||
@ -12,11 +14,7 @@ public class MemberDto
|
||||
public int Id { get; init; }
|
||||
public string Username { get; init; }
|
||||
public string Email { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum age rating a user has access to. -1 if not applicable
|
||||
/// </summary>
|
||||
public AgeRating AgeRestriction { get; init; } = AgeRating.NotApplicable;
|
||||
public AgeRestrictionDto AgeRestriction { get; init; }
|
||||
public DateTime Created { get; init; }
|
||||
public DateTime LastActive { get; init; }
|
||||
public IEnumerable<LibraryDto> Libraries { get; init; }
|
||||
|
@ -140,4 +140,9 @@ public class ServerInfoDto
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.6.0</remarks>
|
||||
public IEnumerable<FileFormatDto> FileFormats { get; set; }
|
||||
/// <summary>
|
||||
/// If there is at least one user that is using an age restricted profile on the instance
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.6.0</remarks>
|
||||
public bool UsingRestrictedProfiles { get; set; }
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
|
||||
using API.DTOs.Account;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs;
|
||||
@ -11,8 +12,5 @@ public class UserDto
|
||||
public string RefreshToken { get; set; }
|
||||
public string ApiKey { get; init; }
|
||||
public UserPreferencesDto Preferences { get; set; }
|
||||
/// <summary>
|
||||
/// The highest age rating the user has access to. Not applicable for admins
|
||||
/// </summary>
|
||||
public AgeRating AgeRestriction { get; set; } = AgeRating.NotApplicable;
|
||||
public AgeRestrictionDto AgeRestriction { get; init; }
|
||||
}
|
||||
|
1673
API/Data/Migrations/20221017131711_IncludeUnknowns.Designer.cs
generated
Normal file
1673
API/Data/Migrations/20221017131711_IncludeUnknowns.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
API/Data/Migrations/20221017131711_IncludeUnknowns.cs
Normal file
26
API/Data/Migrations/20221017131711_IncludeUnknowns.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class IncludeUnknowns : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AgeRestrictionIncludeUnknowns",
|
||||
table: "AspNetUsers",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AgeRestrictionIncludeUnknowns",
|
||||
table: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
@ -56,6 +56,9 @@ namespace API.Data.Migrations
|
||||
b.Property<int>("AgeRestriction")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AgeRestrictionIncludeUnknowns")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
9
API/Data/Misc/AgeRestriction.cs
Normal file
9
API/Data/Misc/AgeRestriction.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Data.Misc;
|
||||
|
||||
public class AgeRestriction
|
||||
{
|
||||
public AgeRating AgeRating { get; set; }
|
||||
public bool IncludeUnknowns { get; set; }
|
||||
}
|
22
API/Data/Misc/RecentlyAddedSeries.cs
Normal file
22
API/Data/Misc/RecentlyAddedSeries.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Data.Misc;
|
||||
|
||||
public class RecentlyAddedSeries
|
||||
{
|
||||
public int LibraryId { get; init; }
|
||||
public LibraryType LibraryType { get; init; }
|
||||
public DateTime Created { get; init; }
|
||||
public int SeriesId { get; init; }
|
||||
public string SeriesName { get; init; }
|
||||
public MangaFormat Format { get; init; }
|
||||
public int ChapterId { get; init; }
|
||||
public int VolumeId { get; init; }
|
||||
public string ChapterNumber { get; init; }
|
||||
public string ChapterRange { get; init; }
|
||||
public string ChapterTitle { get; init; }
|
||||
public bool IsSpecial { get; init; }
|
||||
public int VolumeNumber { get; init; }
|
||||
public AgeRating AgeRating { get; init; }
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Misc;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
@ -96,7 +97,7 @@ public class CollectionTagRepository : ICollectionTagRepository
|
||||
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync(int userId)
|
||||
{
|
||||
var userRating = (await _context.AppUser.SingleAsync(u => u.Id == userId)).AgeRestriction;
|
||||
var userRating = await GetUserAgeRestriction(userId);
|
||||
return await _context.CollectionTag
|
||||
.Where(c => c.Promoted)
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
@ -122,9 +123,22 @@ public class CollectionTagRepository : ICollectionTagRepository
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
private async Task<AgeRestriction> GetUserAgeRestriction(int userId)
|
||||
{
|
||||
return await _context.AppUser
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u =>
|
||||
new AgeRestriction(){
|
||||
AgeRating = u.AgeRestriction,
|
||||
IncludeUnknowns = u.AgeRestrictionIncludeUnknowns
|
||||
})
|
||||
.SingleAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery, int userId)
|
||||
{
|
||||
var userRating = (await _context.AppUser.SingleAsync(u => u.Id == userId)).AgeRestriction;
|
||||
var userRating = await GetUserAgeRestriction(userId);
|
||||
return await _context.CollectionTag
|
||||
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Misc;
|
||||
using API.Data.Scanner;
|
||||
using API.DTOs;
|
||||
using API.DTOs.CollectionTags;
|
||||
@ -36,24 +37,6 @@ public enum SeriesIncludes
|
||||
Library = 16,
|
||||
}
|
||||
|
||||
internal class RecentlyAddedSeries
|
||||
{
|
||||
public int LibraryId { get; init; }
|
||||
public LibraryType LibraryType { get; init; }
|
||||
public DateTime Created { get; init; }
|
||||
public int SeriesId { get; init; }
|
||||
public string SeriesName { get; init; }
|
||||
public MangaFormat Format { get; init; }
|
||||
public int ChapterId { get; init; }
|
||||
public int VolumeId { get; init; }
|
||||
public string ChapterNumber { get; init; }
|
||||
public string ChapterRange { get; init; }
|
||||
public string ChapterTitle { get; init; }
|
||||
public bool IsSpecial { get; init; }
|
||||
public int VolumeNumber { get; init; }
|
||||
public AgeRating AgeRating { get; init; }
|
||||
}
|
||||
|
||||
public interface ISeriesRepository
|
||||
{
|
||||
void Add(Series series);
|
||||
@ -121,7 +104,7 @@ public interface ISeriesRepository
|
||||
Task<PagedList<SeriesDto>> GetWantToReadForUserAsync(int userId, UserParams userParams, FilterDto filter);
|
||||
Task<Series> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None);
|
||||
Task<Series> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);
|
||||
Task<List<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId);
|
||||
Task<IList<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId);
|
||||
Task<IDictionary<string, IList<SeriesModified>>> GetFolderPathMap(int libraryId);
|
||||
Task<AgeRating> GetMaxAgeRatingFromSeriesAsync(IEnumerable<int> seriesIds);
|
||||
}
|
||||
@ -767,7 +750,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
EF.Functions.Like(s.Name, $"%{filter.SeriesNameQuery}%")
|
||||
|| EF.Functions.Like(s.OriginalName, $"%{filter.SeriesNameQuery}%")
|
||||
|| EF.Functions.Like(s.LocalizedName, $"%{filter.SeriesNameQuery}%"));
|
||||
if (userRating != AgeRating.NotApplicable)
|
||||
if (userRating.AgeRating != AgeRating.NotApplicable)
|
||||
{
|
||||
query = query.RestrictAgainstAgeRestriction(userRating);
|
||||
}
|
||||
@ -1048,9 +1031,9 @@ public class SeriesRepository : ISeriesRepository
|
||||
var userRating = await GetUserAgeRestriction(userId);
|
||||
|
||||
var items = (await GetRecentlyAddedChaptersQuery(userId));
|
||||
if (userRating != AgeRating.NotApplicable)
|
||||
if (userRating.AgeRating != AgeRating.NotApplicable)
|
||||
{
|
||||
items = items.Where(c => c.AgeRating <= userRating);
|
||||
items = items.RestrictAgainstAgeRestriction(userRating);
|
||||
}
|
||||
foreach (var item in items)
|
||||
{
|
||||
@ -1080,9 +1063,17 @@ public class SeriesRepository : ISeriesRepository
|
||||
return seriesMap.Values.AsEnumerable();
|
||||
}
|
||||
|
||||
private async Task<AgeRating> GetUserAgeRestriction(int userId)
|
||||
private async Task<AgeRestriction> GetUserAgeRestriction(int userId)
|
||||
{
|
||||
return (await _context.AppUser.SingleAsync(u => u.Id == userId)).AgeRestriction;
|
||||
return await _context.AppUser
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u =>
|
||||
new AgeRestriction(){
|
||||
AgeRating = u.AgeRestriction,
|
||||
IncludeUnknowns = u.AgeRestrictionIncludeUnknowns
|
||||
})
|
||||
.SingleAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind)
|
||||
@ -1267,20 +1258,39 @@ public class SeriesRepository : ISeriesRepository
|
||||
/// </summary>
|
||||
/// <param name="seenSeries"></param>
|
||||
/// <param name="libraryId"></param>
|
||||
public async Task<List<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId)
|
||||
public async Task<IList<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId)
|
||||
{
|
||||
if (seenSeries.Count == 0) return new List<Series>();
|
||||
if (seenSeries.Count == 0) return Array.Empty<Series>();
|
||||
|
||||
var ids = new List<int>();
|
||||
foreach (var parsedSeries in seenSeries)
|
||||
{
|
||||
var series = await _context.Series
|
||||
.Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName &&
|
||||
s.LibraryId == libraryId)
|
||||
.Select(s => s.Id)
|
||||
.SingleOrDefaultAsync();
|
||||
if (series > 0)
|
||||
try
|
||||
{
|
||||
ids.Add(series);
|
||||
var seriesId = await _context.Series
|
||||
.Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName &&
|
||||
s.LibraryId == libraryId)
|
||||
.Select(s => s.Id)
|
||||
.SingleOrDefaultAsync();
|
||||
if (seriesId > 0)
|
||||
{
|
||||
ids.Add(seriesId);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// This is due to v0.5.6 introducing bugs where we could have multiple series get duplicated and no way to delete them
|
||||
// This here will delete the 2nd one as the first is the one to likely be used.
|
||||
var sId = _context.Series
|
||||
.Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName &&
|
||||
s.LibraryId == libraryId)
|
||||
.Select(s => s.Id)
|
||||
.OrderBy(s => s)
|
||||
.Last();
|
||||
if (sId > 0)
|
||||
{
|
||||
ids.Add(sId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1289,6 +1299,15 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Where(s => !ids.Contains(s.Id))
|
||||
.ToListAsync();
|
||||
|
||||
// If the series to remove has Relation (related series), we must manually unlink due to the DB not being
|
||||
// setup correctly (if this is not done, a foreign key constraint will be thrown)
|
||||
|
||||
foreach (var sr in seriesToRemove)
|
||||
{
|
||||
sr.Relations = new List<SeriesRelation>();
|
||||
Update(sr);
|
||||
}
|
||||
|
||||
_context.Series.RemoveRange(seriesToRemove);
|
||||
|
||||
return seriesToRemove;
|
||||
@ -1427,7 +1446,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Select(s => s.Id);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<SeriesDto>> GetRelatedSeriesQuery(int seriesId, IEnumerable<int> usersSeriesIds, RelationKind kind, AgeRating userRating)
|
||||
private async Task<IEnumerable<SeriesDto>> GetRelatedSeriesQuery(int seriesId, IEnumerable<int> usersSeriesIds, RelationKind kind, AgeRestriction userRating)
|
||||
{
|
||||
return await _context.Series.SelectMany(s =>
|
||||
s.Relations.Where(sr => sr.RelationKind == kind && sr.SeriesId == seriesId && usersSeriesIds.Contains(sr.TargetSeriesId))
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Account;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
@ -396,7 +397,11 @@ public class UserRepository : IUserRepository
|
||||
Created = u.Created,
|
||||
LastActive = u.LastActive,
|
||||
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
|
||||
AgeRestriction = u.AgeRestriction,
|
||||
AgeRestriction = new AgeRestrictionDto()
|
||||
{
|
||||
AgeRating = u.AgeRestriction,
|
||||
IncludeUnknowns = u.AgeRestrictionIncludeUnknowns
|
||||
},
|
||||
Libraries = u.Libraries.Select(l => new LibraryDto
|
||||
{
|
||||
Name = l.Name,
|
||||
@ -430,7 +435,11 @@ public class UserRepository : IUserRepository
|
||||
Created = u.Created,
|
||||
LastActive = u.LastActive,
|
||||
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
|
||||
AgeRestriction = u.AgeRestriction,
|
||||
AgeRestriction = new AgeRestrictionDto()
|
||||
{
|
||||
AgeRating = u.AgeRestriction,
|
||||
IncludeUnknowns = u.AgeRestrictionIncludeUnknowns
|
||||
},
|
||||
Libraries = u.Libraries.Select(l => new LibraryDto
|
||||
{
|
||||
Name = l.Name,
|
||||
|
@ -45,6 +45,10 @@ public class AppUser : IdentityUser<int>, IHasConcurrencyToken
|
||||
/// The highest age rating the user has access to. Not applicable for admins
|
||||
/// </summary>
|
||||
public AgeRating AgeRestriction { get; set; } = AgeRating.NotApplicable;
|
||||
/// <summary>
|
||||
/// If an age rating restriction is applied to the account, if Unknowns should be allowed for the user. Defaults to false.
|
||||
/// </summary>
|
||||
public bool AgeRestrictionIncludeUnknowns { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
[ConcurrencyCheck]
|
||||
|
@ -2,6 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.Data.Misc;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Extensions;
|
||||
|
||||
@ -27,4 +29,16 @@ public static class EnumerableExtensions
|
||||
|
||||
return list.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture);
|
||||
}
|
||||
|
||||
public static IEnumerable<RecentlyAddedSeries> RestrictAgainstAgeRestriction(this IEnumerable<RecentlyAddedSeries> items, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return items;
|
||||
var q = items.Where(s => s.AgeRating <= restriction.AgeRating);
|
||||
if (!restriction.IncludeUnknowns)
|
||||
{
|
||||
return q.Where(s => s.AgeRating != AgeRating.Unknown);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using API.Data.Misc;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
||||
@ -6,18 +7,42 @@ namespace API.Extensions;
|
||||
|
||||
public static class QueryableExtensions
|
||||
{
|
||||
public static IQueryable<Series> RestrictAgainstAgeRestriction(this IQueryable<Series> queryable, AgeRating rating)
|
||||
public static IQueryable<Series> RestrictAgainstAgeRestriction(this IQueryable<Series> queryable, AgeRestriction restriction)
|
||||
{
|
||||
return queryable.Where(s => rating == AgeRating.NotApplicable || s.Metadata.AgeRating <= rating);
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
var q = queryable.Where(s => s.Metadata.AgeRating <= restriction.AgeRating);
|
||||
if (!restriction.IncludeUnknowns)
|
||||
{
|
||||
return q.Where(s => s.Metadata.AgeRating != AgeRating.Unknown);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
public static IQueryable<CollectionTag> RestrictAgainstAgeRestriction(this IQueryable<CollectionTag> queryable, AgeRating rating)
|
||||
public static IQueryable<CollectionTag> RestrictAgainstAgeRestriction(this IQueryable<CollectionTag> queryable, AgeRestriction restriction)
|
||||
{
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm => sm.AgeRating <= rating));
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
|
||||
if (restriction.IncludeUnknowns)
|
||||
{
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating));
|
||||
}
|
||||
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
|
||||
}
|
||||
|
||||
public static IQueryable<ReadingList> RestrictAgainstAgeRestriction(this IQueryable<ReadingList> queryable, AgeRating rating)
|
||||
public static IQueryable<ReadingList> RestrictAgainstAgeRestriction(this IQueryable<ReadingList> queryable, AgeRestriction restriction)
|
||||
{
|
||||
return queryable.Where(rl => rl.AgeRating <= rating);
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
var q = queryable.Where(rl => rl.AgeRating <= restriction.AgeRating);
|
||||
|
||||
if (!restriction.IncludeUnknowns)
|
||||
{
|
||||
return q.Where(rl => rl.AgeRating != AgeRating.Unknown);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Comparators;
|
||||
using API.Entities;
|
||||
using API.Parser;
|
||||
using API.Services.Tasks.Scanner;
|
||||
@ -45,4 +46,26 @@ public static class SeriesExtensions
|
||||
|| info.Series == series.Name || info.Series == series.LocalizedName || info.Series == series.OriginalName
|
||||
|| Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the Cover Image for the Series
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>This is under the assumption that the Volume already has a Cover Image calculated and set</remarks>
|
||||
public static string GetCoverImage(this Series series)
|
||||
{
|
||||
var volumes = series.Volumes ?? new List<Volume>();
|
||||
var firstVolume = volumes.GetCoverImage(series.Format);
|
||||
string coverImage = null;
|
||||
|
||||
var chapters = firstVolume.Chapters.OrderBy(c => double.Parse(c.Number), ChapterSortComparerZeroFirst.Default).ToList();
|
||||
if (chapters.Count > 1 && chapters.Any(c => c.IsSpecial))
|
||||
{
|
||||
coverImage = chapters.FirstOrDefault(c => !c.IsSpecial)?.CoverImage ?? chapters.First().CoverImage;
|
||||
firstVolume = null;
|
||||
}
|
||||
|
||||
return firstVolume?.CoverImage ?? coverImage;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Account;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.DTOs.Device;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.Reader;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.Search;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.DTOs.Settings;
|
||||
using API.DTOs.Theme;
|
||||
using API.Entities;
|
||||
@ -98,7 +98,14 @@ public class AutoMapperProfiles : Profile
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor)));
|
||||
|
||||
CreateMap<AppUser, UserDto>();
|
||||
CreateMap<AppUser, UserDto>()
|
||||
.ForMember(dest => dest.AgeRestriction,
|
||||
opt =>
|
||||
opt.MapFrom(src => new AgeRestrictionDto()
|
||||
{
|
||||
AgeRating = src.AgeRestriction,
|
||||
IncludeUnknowns = src.AgeRestrictionIncludeUnknowns
|
||||
}));
|
||||
CreateMap<SiteTheme, SiteThemeDto>();
|
||||
CreateMap<AppUserPreferences, UserPreferencesDto>()
|
||||
.ForMember(dest => dest.Theme,
|
||||
@ -130,6 +137,13 @@ public class AutoMapperProfiles : Profile
|
||||
opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList()));
|
||||
|
||||
CreateMap<AppUser, MemberDto>()
|
||||
.ForMember(dest => dest.AgeRestriction,
|
||||
opt =>
|
||||
opt.MapFrom(src => new AgeRestrictionDto()
|
||||
{
|
||||
AgeRating = src.AgeRestriction,
|
||||
IncludeUnknowns = src.AgeRestrictionIncludeUnknowns
|
||||
}))
|
||||
.AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries));
|
||||
|
||||
CreateMap<RegisterDto, AppUser>();
|
||||
|
@ -130,17 +130,8 @@ public class MetadataService : IMetadataService
|
||||
return Task.CompletedTask;
|
||||
|
||||
series.Volumes ??= new List<Volume>();
|
||||
var firstCover = series.Volumes.GetCoverImage(series.Format);
|
||||
string coverImage = null;
|
||||
series.CoverImage = series.GetCoverImage();
|
||||
|
||||
var chapters = firstCover.Chapters.OrderBy(c => double.Parse(c.Number), ChapterSortComparerZeroFirst.Default).ToList();
|
||||
if (chapters.Count > 1 && chapters.Any(c => c.IsSpecial))
|
||||
{
|
||||
coverImage = chapters.First(c => !c.IsSpecial).CoverImage ?? chapters.First().CoverImage;
|
||||
firstCover = null;
|
||||
}
|
||||
|
||||
series.CoverImage = firstCover?.CoverImage ?? coverImage;
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
@ -72,8 +72,7 @@ public class ReadingItemService : IReadingItemService
|
||||
// This catches when original library type is Manga/Comic and when parsing with non
|
||||
if (Tasks.Scanner.Parser.Parser.IsEpub(path) && Tasks.Scanner.Parser.Parser.ParseVolume(info.Series) != Tasks.Scanner.Parser.Parser.DefaultVolume) // Shouldn't this be info.Volume != DefaultVolume?
|
||||
{
|
||||
info = _defaultParser.Parse(path, rootPath, LibraryType.Book);
|
||||
var info2 = Parse(path, rootPath, type);
|
||||
var info2 = _defaultParser.Parse(path, rootPath, LibraryType.Book);
|
||||
info.Merge(info2);
|
||||
}
|
||||
|
||||
|
@ -239,7 +239,7 @@ public class TaskScheduler : ITaskScheduler
|
||||
_logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId);
|
||||
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, force));
|
||||
// When we do a scan, force cache to re-unpack in case page numbers change
|
||||
BackgroundJob.Enqueue(() => _cleanupService.CleanupCacheDirectory());
|
||||
BackgroundJob.Enqueue(() => _cleanupService.CleanupCacheAndTempDirectories());
|
||||
}
|
||||
|
||||
public void CleanupChapters(int[] chapterIds)
|
||||
|
@ -20,7 +20,7 @@ public interface ICleanupService
|
||||
{
|
||||
Task Cleanup();
|
||||
Task CleanupDbEntries();
|
||||
void CleanupCacheDirectory();
|
||||
void CleanupCacheAndTempDirectories();
|
||||
Task DeleteSeriesCoverImages();
|
||||
Task DeleteChapterCoverImages();
|
||||
Task DeleteTagCoverImages();
|
||||
@ -65,7 +65,7 @@ public class CleanupService : ICleanupService
|
||||
_logger.LogInformation("Cleaning temp directory");
|
||||
_directoryService.ClearDirectory(_directoryService.TempDirectory);
|
||||
await SendProgress(0.1F, "Cleaning temp directory");
|
||||
CleanupCacheDirectory();
|
||||
CleanupCacheAndTempDirectories();
|
||||
await SendProgress(0.25F, "Cleaning old database backups");
|
||||
_logger.LogInformation("Cleaning old database backups");
|
||||
await CleanupBackups();
|
||||
@ -143,9 +143,9 @@ public class CleanupService : ICleanupService
|
||||
/// <summary>
|
||||
/// Removes all files and directories in the cache and temp directory
|
||||
/// </summary>
|
||||
public void CleanupCacheDirectory()
|
||||
public void CleanupCacheAndTempDirectories()
|
||||
{
|
||||
_logger.LogInformation("Performing cleanup of Cache directory");
|
||||
_logger.LogInformation("Performing cleanup of Cache & Temp directories");
|
||||
_directoryService.ExistOrCreate(_directoryService.CacheDirectory);
|
||||
_directoryService.ExistOrCreate(_directoryService.TempDirectory);
|
||||
|
||||
@ -159,7 +159,7 @@ public class CleanupService : ICleanupService
|
||||
_logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Cache directory purged");
|
||||
_logger.LogInformation("Cache and temp directory purged");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -29,12 +29,6 @@ public class ParsedSeries
|
||||
public MangaFormat Format { get; init; }
|
||||
}
|
||||
|
||||
public enum Modified
|
||||
{
|
||||
Modified = 1,
|
||||
NotModified = 2
|
||||
}
|
||||
|
||||
public class SeriesModified
|
||||
{
|
||||
public string FolderPath { get; set; }
|
||||
|
@ -506,11 +506,6 @@ public class ScannerService : IScannerService
|
||||
|
||||
library.LastScanned = time;
|
||||
|
||||
// Could I delete anything in a Library's Series where the LastScan date is before scanStart?
|
||||
// NOTE: This implementation is expensive
|
||||
_logger.LogDebug("Removing Series that were not found during the scan");
|
||||
var removedSeries = await _unitOfWork.SeriesRepository.RemoveSeriesNotInList(seenSeries, library.Id);
|
||||
_logger.LogDebug("Removing Series that were not found during the scan - complete");
|
||||
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
@ -528,10 +523,27 @@ public class ScannerService : IScannerService
|
||||
totalFiles, seenSeries.Count, sw.ElapsedMilliseconds, library.Name);
|
||||
}
|
||||
|
||||
foreach (var s in removedSeries)
|
||||
try
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved,
|
||||
MessageFactory.SeriesRemovedEvent(s.Id, s.Name, s.LibraryId), false);
|
||||
// Could I delete anything in a Library's Series where the LastScan date is before scanStart?
|
||||
// NOTE: This implementation is expensive
|
||||
_logger.LogDebug("[ScannerService] Removing Series that were not found during the scan");
|
||||
var removedSeries = await _unitOfWork.SeriesRepository.RemoveSeriesNotInList(seenSeries, library.Id);
|
||||
_logger.LogDebug("[ScannerService] Found {Count} series that needs to be removed: {SeriesList}",
|
||||
removedSeries.Count, removedSeries.Select(s => s.Name));
|
||||
_logger.LogDebug("[ScannerService] Removing Series that were not found during the scan - complete");
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
foreach (var s in removedSeries)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved,
|
||||
MessageFactory.SeriesRemovedEvent(s.Id, s.Name, s.LibraryId), false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical(ex, "[ScannerService] There was an issue deleting series for cleanup. Please check logs and rescan");
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -584,4 +596,5 @@ public class ScannerService : IScannerService
|
||||
{
|
||||
return existingSeries.Where(es => !ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(es, parsedSeries));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -134,6 +134,7 @@ public class StatsService : IStatsService
|
||||
MangaReaderPageSplittingModes = await AllMangaReaderPageSplitting(),
|
||||
MangaReaderLayoutModes = await AllMangaReaderLayoutModes(),
|
||||
FileFormats = AllFormats(),
|
||||
UsingRestrictedProfiles = await GetUsingRestrictedProfiles(),
|
||||
};
|
||||
|
||||
var usersWithPref = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences)).ToList();
|
||||
@ -261,4 +262,9 @@ public class StatsService : IStatsService
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private Task<bool> GetUsingRestrictedProfiles()
|
||||
{
|
||||
return _context.Users.AnyAsync(u => u.AgeRestriction > AgeRating.NotApplicable);
|
||||
}
|
||||
}
|
||||
|
6
UI/Web/src/app/_models/age-restriction.ts
Normal file
6
UI/Web/src/app/_models/age-restriction.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { AgeRating } from "./metadata/age-rating";
|
||||
|
||||
export interface AgeRestriction {
|
||||
ageRating: AgeRating;
|
||||
includeUnknowns: boolean;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { AgeRestriction } from './age-restriction';
|
||||
import { Library } from './library';
|
||||
import { AgeRating } from './metadata/age-rating';
|
||||
|
||||
export interface Member {
|
||||
id: number;
|
||||
@ -9,8 +9,5 @@ export interface Member {
|
||||
created: string; // datetime
|
||||
roles: string[];
|
||||
libraries: Library[];
|
||||
/**
|
||||
* If not applicable, will store a -1
|
||||
*/
|
||||
ageRestriction: AgeRating;
|
||||
ageRestriction: AgeRestriction;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { AgeRating } from './metadata/age-rating';
|
||||
import { AgeRestriction } from './age-restriction';
|
||||
import { Preferences } from './preferences/preferences';
|
||||
|
||||
// This interface is only used for login and storing/retreiving JWT from local storage
|
||||
@ -10,5 +10,5 @@ export interface User {
|
||||
preferences: Preferences;
|
||||
apiKey: string;
|
||||
email: string;
|
||||
ageRestriction: AgeRating;
|
||||
ageRestriction: AgeRestriction;
|
||||
}
|
@ -12,6 +12,7 @@ import { InviteUserResponse } from '../_models/invite-user-response';
|
||||
import { UserUpdateEvent } from '../_models/events/user-update-event';
|
||||
import { UpdateEmailResponse } from '../_models/email/update-email-response';
|
||||
import { AgeRating } from '../_models/metadata/age-rating';
|
||||
import { AgeRestriction } from '../_models/age-restriction';
|
||||
|
||||
export enum Role {
|
||||
Admin = 'Admin',
|
||||
@ -161,7 +162,7 @@ export class AccountService implements OnDestroy {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>, ageRestriction: AgeRating}) {
|
||||
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>, ageRestriction: AgeRestriction}) {
|
||||
return this.httpClient.post<InviteUserResponse>(this.baseUrl + 'account/invite', model);
|
||||
}
|
||||
|
||||
@ -198,7 +199,7 @@ export class AccountService implements OnDestroy {
|
||||
return this.httpClient.post(this.baseUrl + 'account/reset-password', {username, password, oldPassword}, {responseType: 'json' as 'text'});
|
||||
}
|
||||
|
||||
update(model: {email: string, roles: Array<string>, libraries: Array<number>, userId: number, ageRestriction: AgeRating}) {
|
||||
update(model: {email: string, roles: Array<string>, libraries: Array<number>, userId: number, ageRestriction: AgeRestriction}) {
|
||||
return this.httpClient.post(this.baseUrl + 'account/update', model);
|
||||
}
|
||||
|
||||
@ -206,8 +207,8 @@ export class AccountService implements OnDestroy {
|
||||
return this.httpClient.post<UpdateEmailResponse>(this.baseUrl + 'account/update/email', {email});
|
||||
}
|
||||
|
||||
updateAgeRestriction(ageRating: AgeRating) {
|
||||
return this.httpClient.post(this.baseUrl + 'account/update/age-restriction', {ageRating});
|
||||
updateAgeRestriction(ageRating: AgeRating, includeUnknowns: boolean) {
|
||||
return this.httpClient.post(this.baseUrl + 'account/update/age-restriction', {ageRating, includeUnknowns});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AgeRestriction } from 'src/app/_models/age-restriction';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { Member } from 'src/app/_models/member';
|
||||
import { AgeRating } from 'src/app/_models/metadata/age-rating';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
|
||||
// TODO: Rename this to EditUserModal
|
||||
@Component({
|
||||
selector: 'app-edit-user',
|
||||
templateUrl: './edit-user.component.html',
|
||||
@ -18,7 +17,7 @@ export class EditUserComponent implements OnInit {
|
||||
|
||||
selectedRoles: Array<string> = [];
|
||||
selectedLibraries: Array<number> = [];
|
||||
selectedRating: AgeRating = AgeRating.NotApplicable;
|
||||
selectedRestriction!: AgeRestriction;
|
||||
isSaving: boolean = false;
|
||||
|
||||
userForm: FormGroup = new FormGroup({});
|
||||
@ -41,8 +40,8 @@ export class EditUserComponent implements OnInit {
|
||||
this.selectedRoles = roles;
|
||||
}
|
||||
|
||||
updateRestrictionSelection(rating: AgeRating) {
|
||||
this.selectedRating = rating;
|
||||
updateRestrictionSelection(restriction: AgeRestriction) {
|
||||
this.selectedRestriction = restriction;
|
||||
}
|
||||
|
||||
updateLibrarySelection(libraries: Array<Library>) {
|
||||
@ -58,8 +57,7 @@ export class EditUserComponent implements OnInit {
|
||||
model.userId = this.member.id;
|
||||
model.roles = this.selectedRoles;
|
||||
model.libraries = this.selectedLibraries;
|
||||
model.ageRestriction = this.selectedRating || AgeRating.NotApplicable;
|
||||
console.log('rating: ', this.selectedRating);
|
||||
model.ageRestriction = this.selectedRestriction;
|
||||
this.accountService.update(model).subscribe(() => {
|
||||
this.modal.close(true);
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { AgeRestriction } from 'src/app/_models/age-restriction';
|
||||
import { InviteUserResponse } from 'src/app/_models/invite-user-response';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { AgeRating } from 'src/app/_models/metadata/age-rating';
|
||||
@ -21,7 +22,7 @@ export class InviteUserComponent implements OnInit {
|
||||
inviteForm: FormGroup = new FormGroup({});
|
||||
selectedRoles: Array<string> = [];
|
||||
selectedLibraries: Array<number> = [];
|
||||
selectedRating: AgeRating = AgeRating.NotApplicable;
|
||||
selectedRestriction: AgeRestriction = {ageRating: AgeRating.NotApplicable, includeUnknowns: false};
|
||||
emailLink: string = '';
|
||||
|
||||
makeLink: (val: string) => string = (val: string) => {return this.emailLink};
|
||||
@ -48,7 +49,7 @@ export class InviteUserComponent implements OnInit {
|
||||
email,
|
||||
libraries: this.selectedLibraries,
|
||||
roles: this.selectedRoles,
|
||||
ageRestriction: this.selectedRating
|
||||
ageRestriction: this.selectedRestriction
|
||||
}).subscribe((data: InviteUserResponse) => {
|
||||
this.emailLink = data.emailLink;
|
||||
this.isSending = false;
|
||||
@ -69,8 +70,8 @@ export class InviteUserComponent implements OnInit {
|
||||
this.selectedLibraries = libraries.map(l => l.id);
|
||||
}
|
||||
|
||||
updateRestrictionSelection(rating: AgeRating) {
|
||||
this.selectedRating = rating;
|
||||
updateRestrictionSelection(restriction: AgeRestriction) {
|
||||
this.selectedRestriction = restriction;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -264,8 +264,7 @@ $action-bar-height: 38px;
|
||||
// This is applied to images in the backend
|
||||
::ng-deep .kavita-scale-width-container {
|
||||
width: auto;
|
||||
// * 4 is just for extra buffer which is needed based on testing. --book-reader-content-max-height is set by us on calculation of columnHeight
|
||||
max-height: calc(var(--book-reader-content-max-height) - ($action-bar-height * 4)), calc((var(--vh)*100) - ($action-bar-height * 4)) !important;
|
||||
max-height: calc(var(--book-reader-content-max-height) - ($action-bar-height)) !important;
|
||||
}
|
||||
|
||||
// This is applied to images in the backend
|
||||
|
@ -10,7 +10,11 @@
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="isViewMode">
|
||||
<span >{{user?.ageRestriction | ageRating | async}}</span>
|
||||
<span>{{user?.ageRestriction?.ageRating| ageRating | async}}
|
||||
<ng-container *ngIf="user?.ageRestriction?.ageRating !== AgeRating.NotApplicable && user?.ageRestriction?.includeUnknowns">
|
||||
<span class="ms-1 me-1">+</span> Unknowns
|
||||
</ng-container>
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Observable, of, Subject, takeUntil, shareReplay, map, take } from 'rxjs';
|
||||
import { AgeRestriction } from 'src/app/_models/age-restriction';
|
||||
import { AgeRating } from 'src/app/_models/metadata/age-rating';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
@ -16,9 +17,9 @@ export class ChangeAgeRestrictionComponent implements OnInit {
|
||||
user: User | undefined = undefined;
|
||||
hasChangeAgeRestrictionAbility: Observable<boolean> = of(false);
|
||||
isViewMode: boolean = true;
|
||||
selectedRating: AgeRating = AgeRating.NotApplicable;
|
||||
originalRating!: AgeRating;
|
||||
reset: EventEmitter<AgeRating> = new EventEmitter();
|
||||
selectedRestriction!: AgeRestriction;
|
||||
originalRestriction!: AgeRestriction;
|
||||
reset: EventEmitter<AgeRestriction> = new EventEmitter();
|
||||
|
||||
get AgeRating() { return AgeRating; }
|
||||
|
||||
@ -28,8 +29,9 @@ export class ChangeAgeRestrictionComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy), shareReplay(), take(1)).subscribe(user => {
|
||||
if (!user) return;
|
||||
this.user = user;
|
||||
this.originalRating = this.user?.ageRestriction || AgeRating.NotApplicable;
|
||||
this.originalRestriction = this.user.ageRestriction;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
@ -39,8 +41,8 @@ export class ChangeAgeRestrictionComponent implements OnInit {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateRestrictionSelection(rating: AgeRating) {
|
||||
this.selectedRating = rating;
|
||||
updateRestrictionSelection(restriction: AgeRestriction) {
|
||||
this.selectedRestriction = restriction;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@ -50,18 +52,19 @@ export class ChangeAgeRestrictionComponent implements OnInit {
|
||||
|
||||
resetForm() {
|
||||
if (!this.user) return;
|
||||
this.reset.emit(this.originalRating);
|
||||
this.reset.emit(this.originalRestriction);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
saveForm() {
|
||||
if (this.user === undefined) { return; }
|
||||
|
||||
this.accountService.updateAgeRestriction(this.selectedRating).subscribe(() => {
|
||||
this.accountService.updateAgeRestriction(this.selectedRestriction.ageRating, this.selectedRestriction.includeUnknowns).subscribe(() => {
|
||||
this.toastr.success('Age Restriction has been updated');
|
||||
this.originalRating = this.selectedRating;
|
||||
this.originalRestriction = this.selectedRestriction;
|
||||
if (this.user) {
|
||||
this.user.ageRestriction = this.selectedRating;
|
||||
this.user.ageRestriction.ageRating = this.selectedRestriction.ageRating;
|
||||
this.user.ageRestriction.includeUnknowns = this.selectedRestriction.includeUnknowns;
|
||||
}
|
||||
this.resetForm();
|
||||
this.isViewMode = true;
|
||||
|
@ -15,5 +15,17 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="auto-close" role="switch" formControlName="ageRestrictionIncludeUnknowns" class="form-check-input" aria-describedby="include-unknowns-help" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="auto-close">Include Unknowns</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="includeUnknownsTooltip" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
|
||||
<ng-template #includeUnknownsTooltip>If true, Unknowns will be allowed with Age Restrcition. This could lead to untagged media leaking to users with Age restrictions.</ng-template>
|
||||
<span class="visually-hidden" id="include-unknowns-help">If true, Unknowns will be allowed with Age Restrcition. This could lead to untagged media leaking to users with Age restrictions.</span>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</ng-container>
|
@ -1,5 +1,6 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { AgeRestriction } from 'src/app/_models/age-restriction';
|
||||
import { Member } from 'src/app/_models/member';
|
||||
import { AgeRating } from 'src/app/_models/metadata/age-rating';
|
||||
import { AgeRatingDto } from 'src/app/_models/metadata/age-rating-dto';
|
||||
@ -20,8 +21,8 @@ export class RestrictionSelectorComponent implements OnInit, OnChanges {
|
||||
* Show labels and description around the form
|
||||
*/
|
||||
@Input() showContext: boolean = true;
|
||||
@Input() reset: EventEmitter<AgeRating> | undefined;
|
||||
@Output() selected: EventEmitter<AgeRating> = new EventEmitter<AgeRating>();
|
||||
@Input() reset: EventEmitter<AgeRestriction> | undefined;
|
||||
@Output() selected: EventEmitter<AgeRestriction> = new EventEmitter<AgeRestriction>();
|
||||
|
||||
|
||||
ageRatings: Array<AgeRatingDto> = [];
|
||||
@ -32,22 +33,36 @@ export class RestrictionSelectorComponent implements OnInit, OnChanges {
|
||||
ngOnInit(): void {
|
||||
|
||||
this.restrictionForm = new FormGroup({
|
||||
'ageRating': new FormControl(this.member?.ageRestriction || AgeRating.NotApplicable, [])
|
||||
'ageRating': new FormControl(this.member?.ageRestriction.ageRating || AgeRating.NotApplicable || AgeRating.NotApplicable, []),
|
||||
'ageRestrictionIncludeUnknowns': new FormControl(this.member?.ageRestriction.includeUnknowns, []),
|
||||
|
||||
});
|
||||
|
||||
if (this.isAdmin) {
|
||||
this.restrictionForm.get('ageRating')?.disable();
|
||||
this.restrictionForm.get('ageRestrictionIncludeUnknowns')?.disable();
|
||||
}
|
||||
|
||||
if (this.reset) {
|
||||
this.reset.subscribe(e => {
|
||||
this.restrictionForm?.get('ageRating')?.setValue(e);
|
||||
this.restrictionForm?.get('ageRating')?.setValue(e.ageRating);
|
||||
this.restrictionForm?.get('ageRestrictionIncludeUnknowns')?.setValue(e.includeUnknowns);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
this.restrictionForm.get('ageRating')?.valueChanges.subscribe(e => {
|
||||
this.selected.emit(parseInt(e, 10));
|
||||
this.selected.emit({
|
||||
ageRating: parseInt(e, 10),
|
||||
includeUnknowns: this.restrictionForm?.get('ageRestrictionIncludeUnknowns')?.value
|
||||
});
|
||||
});
|
||||
|
||||
this.restrictionForm.get('ageRestrictionIncludeUnknowns')?.valueChanges.subscribe(e => {
|
||||
this.selected.emit({
|
||||
ageRating: parseInt(this.restrictionForm?.get('ageRating')?.value, 10),
|
||||
includeUnknowns: e
|
||||
});
|
||||
});
|
||||
|
||||
this.metadataService.getAllAgeRatings().subscribe(ratings => {
|
||||
@ -60,8 +75,8 @@ export class RestrictionSelectorComponent implements OnInit, OnChanges {
|
||||
|
||||
ngOnChanges() {
|
||||
if (!this.member) return;
|
||||
console.log('changes: ');
|
||||
this.restrictionForm?.get('ageRating')?.setValue(this.member?.ageRestriction || AgeRating.NotApplicable);
|
||||
this.restrictionForm?.get('ageRating')?.setValue(this.member?.ageRestriction.ageRating || AgeRating.NotApplicable);
|
||||
this.restrictionForm?.get('ageRestrictionIncludeUnknowns')?.setValue(this.member?.ageRestriction.includeUnknowns);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user