mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
More Polish (#2320)
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
parent
cd3a15fa3b
commit
5f11973696
@ -191,6 +191,7 @@ public class AccountController : BaseApiController
|
||||
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
|
||||
}
|
||||
|
||||
_logger.LogInformation("{UserName} attempting to login from {IpAddress}", loginDto.Username, HttpContext.Connection.RemoteIpAddress?.ToString());
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
|
@ -93,7 +93,6 @@ public class ReviewController : BaseApiController
|
||||
|
||||
if (totalReviews > 10)
|
||||
{
|
||||
//var stepSize = Math.Max(totalReviews / 10, 1); // Calculate step size, ensuring it's at least 1
|
||||
var stepSize = Math.Max((totalReviews - 4) / 8, 1);
|
||||
|
||||
var selectedReviews = new List<UserReviewDto>()
|
||||
|
@ -41,7 +41,7 @@ public class SeriesController : BaseApiController
|
||||
private readonly IEasyCachingProvider _reviewCacheProvider;
|
||||
private readonly IEasyCachingProvider _recommendationCacheProvider;
|
||||
private readonly IEasyCachingProvider _externalSeriesCacheProvider;
|
||||
public const string CacheKey = "recommendation_";
|
||||
private const string CacheKey = "recommendation_";
|
||||
|
||||
|
||||
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork,
|
||||
@ -584,14 +584,14 @@ public class SeriesController : BaseApiController
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("external-series-detail")]
|
||||
public async Task<ActionResult<ExternalSeriesDto>> GetExternalSeriesInfo(int? aniListId, long? malId)
|
||||
public async Task<ActionResult<ExternalSeriesDto>> GetExternalSeriesInfo(int? aniListId, long? malId, int? seriesId)
|
||||
{
|
||||
if (!await _licenseService.HasActiveLicense())
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var cacheKey = $"{CacheKey}-{aniListId ?? 0}-{malId ?? 0}";
|
||||
var cacheKey = $"{CacheKey}-{aniListId ?? 0}-{malId ?? 0}-{seriesId ?? 0}";
|
||||
var results = await _externalSeriesCacheProvider.GetAsync<ExternalSeriesDto>(cacheKey);
|
||||
if (results.HasValue)
|
||||
{
|
||||
@ -600,7 +600,7 @@ public class SeriesController : BaseApiController
|
||||
|
||||
try
|
||||
{
|
||||
var ret = await _externalMetadataService.GetExternalSeriesDetail(aniListId, malId);
|
||||
var ret = await _externalMetadataService.GetExternalSeriesDetail(aniListId, malId, seriesId);
|
||||
await _externalSeriesCacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromMinutes(15));
|
||||
return Ok(ret);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using API.DTOs.Scrobbling;
|
||||
|
||||
namespace API.DTOs.Recommendation;
|
||||
|
||||
@ -8,7 +9,7 @@ public class ExternalSeriesDetailDto
|
||||
public int? AniListId { get; set; }
|
||||
public long? MALId { get; set; }
|
||||
public IList<string> Synonyms { get; set; }
|
||||
public PlusMediaFormat PlusMediaFormat { get; set; }
|
||||
public MediaFormat PlusMediaFormat { get; set; }
|
||||
public string? SiteUrl { get; set; }
|
||||
public string? CoverUrl { get; set; }
|
||||
public IList<string> Genres { get; set; }
|
||||
|
@ -1,15 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace API.DTOs.Recommendation;
|
||||
|
||||
public enum PlusMediaFormat
|
||||
{
|
||||
[Description("Manga")]
|
||||
Manga = 1,
|
||||
[Description("Comic")]
|
||||
Comic = 2,
|
||||
[Description("LightNovel")]
|
||||
LightNovel = 3,
|
||||
[Description("Book")]
|
||||
Book = 4
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using API.DTOs.Recommendation;
|
||||
|
||||
namespace API.DTOs.Scrobbling;
|
||||
#nullable enable
|
||||
@ -18,12 +19,20 @@ public enum ScrobbleEventType
|
||||
Review = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents PlusMediaFormat
|
||||
/// </summary>
|
||||
public enum MediaFormat
|
||||
{
|
||||
[Description("Manga")]
|
||||
Manga = 1,
|
||||
[Description("Comic")]
|
||||
Comic = 2,
|
||||
[Description("LightNovel")]
|
||||
LightNovel = 3,
|
||||
Book = 4
|
||||
[Description("Book")]
|
||||
Book = 4,
|
||||
Unknown = 5
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,7 +2,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers.Builders;
|
||||
using Flurl.Http;
|
||||
@ -13,9 +15,22 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services.Plus;
|
||||
|
||||
/// <summary>
|
||||
/// Used for matching and fetching metadata on a series
|
||||
/// </summary>
|
||||
internal class ExternalMetadataIdsDto
|
||||
{
|
||||
public long? MalId { get; set; }
|
||||
public int? AniListId { get; set; }
|
||||
|
||||
public string? SeriesName { get; set; }
|
||||
public string? LocalizedSeriesName { get; set; }
|
||||
public MediaFormat? PlusMediaFormat { get; set; } = MediaFormat.Unknown;
|
||||
}
|
||||
|
||||
public interface IExternalMetadataService
|
||||
{
|
||||
Task<ExternalSeriesDetailDto> GetExternalSeriesDetail(int? aniListId, long? malId);
|
||||
Task<ExternalSeriesDetailDto> GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId);
|
||||
}
|
||||
|
||||
public class ExternalMetadataService : IExternalMetadataService
|
||||
@ -32,7 +47,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||
}
|
||||
|
||||
public async Task<ExternalSeriesDetailDto?> GetExternalSeriesDetail(int? aniListId, long? malId)
|
||||
public async Task<ExternalSeriesDetailDto?> GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId)
|
||||
{
|
||||
if (!aniListId.HasValue && !malId.HasValue)
|
||||
{
|
||||
@ -40,12 +55,38 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
}
|
||||
|
||||
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
|
||||
return await GetSeriesDetail(license, aniListId, malId);
|
||||
return await GetSeriesDetail(license, aniListId, malId, seriesId);
|
||||
|
||||
}
|
||||
|
||||
private async Task<ExternalSeriesDetailDto?> GetSeriesDetail(string license, int? anilistId, long? malId)
|
||||
private async Task<ExternalSeriesDetailDto?> GetSeriesDetail(string license, int? aniListId, long? malId, int? seriesId)
|
||||
{
|
||||
var payload = new ExternalMetadataIdsDto()
|
||||
{
|
||||
AniListId = aniListId,
|
||||
MalId = malId,
|
||||
SeriesName = string.Empty,
|
||||
LocalizedSeriesName = string.Empty
|
||||
};
|
||||
if (seriesId is > 0)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series != null)
|
||||
{
|
||||
if (payload.AniListId <= 0)
|
||||
{
|
||||
payload.AniListId = ScrobblingService.ExtractId<int>(series.Metadata.WebLinks, ScrobblingService.AniListWeblinkWebsite);
|
||||
}
|
||||
if (payload.MalId <= 0)
|
||||
{
|
||||
payload.MalId = ScrobblingService.ExtractId<long>(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite);
|
||||
}
|
||||
payload.SeriesName = series.Name;
|
||||
payload.LocalizedSeriesName = series.LocalizedName;
|
||||
payload.PlusMediaFormat = ConvertToMediaFormat(series.Library.Type, series.Format);
|
||||
}
|
||||
|
||||
}
|
||||
try
|
||||
{
|
||||
return await (Configuration.KavitaPlusApiUrl + "/api/metadata/series/detail")
|
||||
@ -56,11 +97,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
.WithHeader("x-kavita-version", BuildInfo.Version)
|
||||
.WithHeader("Content-Type", "application/json")
|
||||
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
|
||||
.PostJsonAsync(new
|
||||
{
|
||||
AnilistId = anilistId,
|
||||
MalId = malId,
|
||||
})
|
||||
.PostJsonAsync(payload)
|
||||
.ReceiveJson<ExternalSeriesDetailDto>();
|
||||
|
||||
}
|
||||
@ -71,4 +108,15 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MediaFormat ConvertToMediaFormat(LibraryType libraryType, MangaFormat seriesFormat)
|
||||
{
|
||||
return libraryType switch
|
||||
{
|
||||
LibraryType.Manga => seriesFormat == MangaFormat.Epub ? MediaFormat.LightNovel : MediaFormat.Manga,
|
||||
LibraryType.Comic => MediaFormat.Comic,
|
||||
LibraryType.Book => MediaFormat.Book,
|
||||
_ => MediaFormat.Unknown
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
@ -188,11 +189,19 @@ public class ScrobblingService : IScrobblingService
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
|
||||
_logger.LogInformation("Processing Scrobbling review event for {UserId} on {SeriesName}", userId, series.Name);
|
||||
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
|
||||
|
||||
if (string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 ||
|
||||
reviewTitle.Length > 120 ||
|
||||
reviewTitle.Length < 20))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Rejecting Scrobble event for {Series}. Review is not long enough to meet requirements", series.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id,
|
||||
ScrobbleEventType.Review);
|
||||
if (existingEvt is {IsProcessed: false})
|
||||
@ -695,6 +704,7 @@ public class ScrobblingService : IScrobblingService
|
||||
_logger.LogInformation("Scrobbling Events is complete");
|
||||
}
|
||||
|
||||
|
||||
private async Task<int> ProcessEvents(IEnumerable<ScrobbleEvent> events, IDictionary<int, int> userRateLimits,
|
||||
int usersToScrobble, int progressCounter, int totalProgress, Func<ScrobbleEvent, Task<ScrobbleDto>> createEvent)
|
||||
{
|
||||
|
@ -124,7 +124,7 @@ public class StreamService : IStreamService
|
||||
var stream = user?.DashboardStreams.FirstOrDefault(d => d.Id == dto.Id);
|
||||
if (stream == null)
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "dashboard-stream-doesnt-exist"));
|
||||
if (stream.Order == dto.ToPosition) return ;
|
||||
if (stream.Order == dto.ToPosition) return;
|
||||
|
||||
var list = user!.DashboardStreams.ToList();
|
||||
ReorderItems(list, stream.Id, dto.ToPosition);
|
||||
@ -132,6 +132,7 @@ public class StreamService : IStreamService
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
if (!stream.Visible) return;
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id),
|
||||
user.Id);
|
||||
}
|
||||
@ -264,9 +265,10 @@ public class StreamService : IStreamService
|
||||
var list = user!.SideNavStreams.ToList();
|
||||
ReorderItems(list, stream.Id, dto.ToPosition);
|
||||
user.SideNavStreams = list;
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
if (!stream.Visible) return;
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId),
|
||||
userId);
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ public static class Parser
|
||||
private static readonly ImmutableArray<string> FormatTagSpecialKeywords = ImmutableArray.Create(
|
||||
"Special", "Reference", "Director's Cut", "Box Set", "Box-Set", "Annual", "Anthology", "Epilogue",
|
||||
"One Shot", "One-Shot", "Prologue", "TPB", "Trade Paper Back", "Omnibus", "Compendium", "Absolute", "Graphic Novel",
|
||||
"GN", "FCBD");
|
||||
"GN", "FCBD", "Giant Size");
|
||||
|
||||
private static readonly char[] LeadingZeroesTrimChars = new[] { '0' };
|
||||
|
||||
|
74
UI/Web/package-lock.json
generated
74
UI/Web/package-lock.json
generated
@ -21,6 +21,8 @@
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
||||
"@iplab/ngx-file-upload": "^16.0.2",
|
||||
"@lithiumjs/angular": "^7.3.0",
|
||||
"@lithiumjs/ngx-virtual-scroll": "^0.3.0",
|
||||
"@microsoft/signalr": "^7.0.12",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
||||
"@ngneat/transloco": "^6.0.0",
|
||||
@ -996,7 +998,6 @@
|
||||
"version": "16.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.9.tgz",
|
||||
"integrity": "sha512-ecH2oOlijJdDqioD9IfgdqJGoRRHI6hAx5rwBxIaYk01ywj13KzvXWPrXbCIupeWtV/XUZUlbwf47nlmL5gxZg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.22.5",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
@ -1200,9 +1201,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
@ -3642,6 +3643,33 @@
|
||||
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@lithiumjs/angular": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@lithiumjs/angular/-/angular-7.3.0.tgz",
|
||||
"integrity": "sha512-81nXyT9I2J+VpeFEDtOvfP4imlrLueoqFYBZR8PCrlY9cjDzgFAZBq7mCOLxOhi0xL5wF9hM0iDqlmI9LDct1Q==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=11.0.0 <17.0.0",
|
||||
"rxjs": ">=7.x.x",
|
||||
"typescript": ">=4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lithiumjs/ngx-virtual-scroll": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@lithiumjs/ngx-virtual-scroll/-/ngx-virtual-scroll-0.3.0.tgz",
|
||||
"integrity": "sha512-fYZR1S66c4ATg6mDVwJaZxsZ8rT/jcJ07b95x5sZVV7gtiDv7DDUCiMa4mtvj71fqMzcoeRe99G0FivVUwvZ0Q==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "8.x.x - 16.x.x",
|
||||
"@angular/core": "8.x.x - 16.x.x",
|
||||
"@lithiumjs/angular": ">=7.0.0",
|
||||
"rxjs": "6.x.x - 7.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/signalr": {
|
||||
"version": "7.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-7.0.12.tgz",
|
||||
@ -5842,7 +5870,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
@ -6098,7 +6125,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@ -6406,7 +6432,6 @@
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@ -7586,7 +7611,6 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
@ -7596,7 +7620,6 @@
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@ -8693,7 +8716,6 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -9517,7 +9539,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
@ -9727,9 +9748,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-instrument/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@ -10295,9 +10316,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/less/node_modules/semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"bin": {
|
||||
@ -10564,9 +10585,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@ -11356,7 +11377,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -12697,7 +12717,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
@ -12708,8 +12727,7 @@
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
|
||||
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
|
||||
},
|
||||
"node_modules/regenerate": {
|
||||
"version": "1.4.2",
|
||||
@ -13145,7 +13163,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.64.1",
|
||||
@ -13272,7 +13290,6 @@
|
||||
"version": "7.5.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
|
||||
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
@ -13287,7 +13304,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
@ -13298,8 +13314,7 @@
|
||||
"node_modules/semver/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
@ -14415,7 +14430,6 @@
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
|
||||
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
@ -26,6 +26,8 @@
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
||||
"@iplab/ngx-file-upload": "^16.0.2",
|
||||
"@lithiumjs/angular": "^7.3.0",
|
||||
"@lithiumjs/ngx-virtual-scroll": "^0.3.0",
|
||||
"@microsoft/signalr": "^7.0.12",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
||||
"@ngneat/transloco": "^6.0.0",
|
||||
|
@ -22,6 +22,7 @@ export class AuthGuard implements CanActivate {
|
||||
if (user) {
|
||||
return true;
|
||||
}
|
||||
// TODO: Remove the error message stuff here and just redirect them. Don't need to tell them
|
||||
const errorMessage = this.translocoService.translate('toasts.unauthorized-1');
|
||||
const errorMessage2 = this.translocoService.translate('toasts.unauthorized-2');
|
||||
if (this.toastr.toasts.filter(toast => toast.message === errorMessage2 || toast.message === errorMessage).length === 0) {
|
||||
|
@ -229,7 +229,8 @@ export class SeriesService {
|
||||
return this.httpClient.post(this.baseUrl + 'series/remove-from-on-deck?seriesId=' + seriesId, {});
|
||||
}
|
||||
|
||||
getExternalSeriesDetails(aniListId?: number, malId?: number) {
|
||||
return this.httpClient.get<ExternalSeriesDetail>(this.baseUrl + 'series/external-series-detail?aniListId=' + (aniListId || 0) + '&malId=' + (malId || 0));
|
||||
getExternalSeriesDetails(aniListId?: number, malId?: number, seriesId?: number) {
|
||||
return this.httpClient.get<ExternalSeriesDetail>(this.baseUrl + 'series/external-series-detail?aniListId=' + (aniListId || 0) + '&malId=' + (malId || 0) + '&seriesId=' + (seriesId || 0));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,19 +1,22 @@
|
||||
<ng-container *transloco="let t">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title">
|
||||
<ng-container *ngIf="CoverUrl as coverUrl">
|
||||
<app-image *ngIf="coverUrl" height="230px" width="160px" maxHeight="230px" objectFit="contain" [imageUrl]="coverUrl"></app-image>
|
||||
<div class="">
|
||||
{{name}}
|
||||
</div>
|
||||
</ng-container>
|
||||
{{name}}
|
||||
</h5>
|
||||
<button type="button" class="btn-close text-reset" [attr.aria-label]="t('common.close')" (click)="close()"></button>
|
||||
</div>
|
||||
|
||||
<div class="offcanvas-body">
|
||||
<ng-container *ngIf="CoverUrl as coverUrl">
|
||||
<div style="width: 160px" class="mx-auto mb-3">
|
||||
<app-image *ngIf="coverUrl" height="230px" width="160px" maxHeight="230px" objectFit="contain" [imageUrl]="coverUrl"></app-image>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="externalSeries; else localSeriesBody">
|
||||
<span *ngIf="(externalSeries.volumeCount || 0) > 0 || (externalSeries.chapterCount || 0) > 0" class="text-muted" style="font-size: 14px; color: lightgrey">{{t('series-preview-drawer.vols-and-chapters', {volCount: externalSeries.volumeCount, chpCount: externalSeries.chapterCount})}}</span>
|
||||
<div *ngIf="(externalSeries.volumeCount || 0) > 0 || (externalSeries.chapterCount || 0) > 0" class="text-muted muted mb-2">
|
||||
{{t('series-preview-drawer.vols-and-chapters', {volCount: externalSeries.volumeCount, chpCount: externalSeries.chapterCount})}}
|
||||
</div>
|
||||
<app-read-more *ngIf="externalSeries.summary" [maxLength]="300" [text]="externalSeries.summary"></app-read-more>
|
||||
|
||||
<div class="mt-3">
|
||||
@ -64,7 +67,14 @@
|
||||
|
||||
<ng-template #localSeriesBody>
|
||||
<ng-container *ngIf="localSeries">
|
||||
<span class="text-muted" style="font-size: 14px; color: lightgrey">{{localSeries.publicationStatus | publicationStatus}}</span>
|
||||
<div class="d-inline-block mb-2" style="width: 100%">
|
||||
<span class="text-muted muted">{{localSeries.publicationStatus | publicationStatus}}</span>
|
||||
<button class="btn btn-secondary btn-sm float-end me-3"
|
||||
(click)="toggleWantToRead()"
|
||||
ngbTooltip="{{wantToRead ? t('series-preview-drawer.remove-from-want-to-read') : t('series-preview-drawer.add-to-want-to-read')}}">
|
||||
<i class="{{wantToRead ? 'fa-solid' : 'fa-regular'}} fa-star" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<app-read-more [maxLength]="300" [text]="localSeries.summary"></app-read-more>
|
||||
|
||||
<div class="mt-3">
|
||||
|
@ -8,3 +8,7 @@
|
||||
::ng-deep .person-img {
|
||||
margin-top: 24px; margin-left: 24px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {NgbActiveOffcanvas} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {NgbActiveOffcanvas, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ExternalSeriesDetail, SeriesStaff} from "../../_models/series-detail/external-series-detail";
|
||||
import {SeriesService} from "../../_services/series.service";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
@ -15,11 +15,12 @@ import {ImageService} from "../../_services/image.service";
|
||||
import {PublicationStatusPipe} from "../../pipe/publication-status.pipe";
|
||||
import {SeriesMetadata} from "../../_models/metadata/series-metadata";
|
||||
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||
import {ActionService} from "../../_services/action.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-preview-drawer',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslocoDirective, ImageComponent, LoadingComponent, SafeHtmlPipe, A11yClickDirective, MetadataDetailComponent, PersonBadgeComponent, TagBadgeComponent, PublicationStatusPipe, ReadMoreComponent],
|
||||
imports: [CommonModule, TranslocoDirective, ImageComponent, LoadingComponent, SafeHtmlPipe, A11yClickDirective, MetadataDetailComponent, PersonBadgeComponent, TagBadgeComponent, PublicationStatusPipe, ReadMoreComponent, NgbTooltip],
|
||||
templateUrl: './series-preview-drawer.component.html',
|
||||
styleUrls: ['./series-preview-drawer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@ -37,10 +38,12 @@ export class SeriesPreviewDrawerComponent implements OnInit {
|
||||
externalSeries: ExternalSeriesDetail | undefined;
|
||||
localSeries: SeriesMetadata | undefined;
|
||||
url: string = '';
|
||||
wantToRead: boolean = false;
|
||||
|
||||
private readonly activeOffcanvas = inject(NgbActiveOffcanvas);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly imageService = inject(ImageService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
get CoverUrl() {
|
||||
@ -56,29 +59,52 @@ export class SeriesPreviewDrawerComponent implements OnInit {
|
||||
if (this.isExternalSeries) {
|
||||
this.seriesService.getExternalSeriesDetails(this.aniListId, this.malId).subscribe(externalSeries => {
|
||||
this.externalSeries = externalSeries;
|
||||
|
||||
this.isLoading = false;
|
||||
if (this.externalSeries.siteUrl) {
|
||||
this.url = this.externalSeries.siteUrl;
|
||||
}
|
||||
|
||||
console.log('External Series Detail: ', this.externalSeries);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
} else {
|
||||
this.seriesService.getMetadata(this.seriesId!).subscribe(data => {
|
||||
this.localSeries = data;
|
||||
|
||||
// Consider the localSeries has no metadata, try to merge the external Series metadata
|
||||
if (this.localSeries.summary === '' && this.localSeries.genres.length === 0) {
|
||||
this.seriesService.getExternalSeriesDetails(0, 0, this.seriesId).subscribe(externalSeriesData => {
|
||||
this.isExternalSeries = true;
|
||||
this.externalSeries = externalSeriesData;
|
||||
this.cdRef.markForCheck();
|
||||
})
|
||||
}
|
||||
|
||||
this.seriesService.isWantToRead(this.seriesId!).subscribe(wantToRead => {
|
||||
this.wantToRead = wantToRead;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.isLoading = false;
|
||||
this.url = 'library/' + this.libraryId + '/series/' + this.seriesId;
|
||||
this.localStaff = data.writers.map(p => {
|
||||
return {name: p.name, role: 'Story & Art'} as SeriesStaff;
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
})
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
toggleWantToRead() {
|
||||
if (this.wantToRead) {
|
||||
this.actionService.removeMultipleSeriesFromWantToReadList([this.seriesId!]);
|
||||
} else {
|
||||
this.actionService.addMultipleSeriesToWantToReadList([this.seriesId!]);
|
||||
}
|
||||
|
||||
this.wantToRead = !this.wantToRead;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
close() {
|
||||
|
@ -3,21 +3,21 @@
|
||||
<div *ngIf="selectionCount > 0" class="bulk-select mb-3 {{modalMode ? '' : 'fixed-top}}" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
|
||||
<span class="highlight">
|
||||
<i class="fa fa-check me-1" aria-hidden="true"></i>
|
||||
{{t('items-selected',{num: selectionCount | number})}}
|
||||
</span>
|
||||
<span class="highlight">
|
||||
<i class="fa fa-check me-1" aria-hidden="true"></i>
|
||||
{{t('items-selected',{num: selectionCount | number})}}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<button *ngIf="hasMarkAsUnread" class="btn btn-icon" (click)="executeAction(Action.MarkAsUnread)" [ngbTooltip]="t('mark-as-unread')" placement="bottom">
|
||||
<i class="fa-regular fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('mark-as-unread')}}</span>
|
||||
</button>
|
||||
<button *ngIf="hasMarkAsRead" class="btn btn-icon" (click)="executeAction(Action.MarkAsRead)" [ngbTooltip]="t('mark-as-read')" placement="bottom">
|
||||
<i class="fa-solid fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('mark-as-read')}}</span>
|
||||
</button>
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
<button *ngIf="hasMarkAsUnread" class="btn btn-icon" (click)="executeAction(Action.MarkAsUnread)" [ngbTooltip]="t('mark-as-unread')" placement="bottom">
|
||||
<i class="fa-regular fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('mark-as-unread')}}</span>
|
||||
</button>
|
||||
<button *ngIf="hasMarkAsRead" class="btn btn-icon" (click)="executeAction(Action.MarkAsRead)" [ngbTooltip]="t('mark-as-read')" placement="bottom">
|
||||
<i class="fa-solid fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('mark-as-read')}}</span>
|
||||
</button>
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
</span>
|
||||
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
|
@ -40,14 +40,13 @@ export class BulkOperationsComponent implements OnInit {
|
||||
hasMarkAsRead: boolean = false;
|
||||
hasMarkAsUnread: boolean = false;
|
||||
actions: Array<ActionItem<any>> = [];
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
|
||||
protected readonly Action = Action;
|
||||
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -60,10 +59,6 @@ export class BulkOperationsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
handleActionCallback(action: ActionItem<any>, data: any) {
|
||||
this.actionCallback(action, data);
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
this.actionCallback(action, null);
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ export class BulkSelectionService {
|
||||
if (this.isShiftDown) {
|
||||
|
||||
if (dataSource === this.prevDataSource) {
|
||||
this.debugLog('Selecting ' + dataSource + ' cards from ' + this.prevIndex + ' to ' + index);
|
||||
this.debugLog('Selecting ' + dataSource + ' cards from ' + this.prevIndex + ' to ' + index + ' as ' + !wasSelected);
|
||||
this.selectCards(dataSource, this.prevIndex, index, !wasSelected);
|
||||
} else {
|
||||
const isForwardSelection = index > this.prevIndex;
|
||||
@ -128,7 +128,7 @@ export class BulkSelectionService {
|
||||
getSelectedCardsForSource(dataSource: DataSource) {
|
||||
if (!this.selectedCards.hasOwnProperty(dataSource)) return [];
|
||||
|
||||
let ret = [];
|
||||
const ret = [];
|
||||
for(let k in this.selectedCards[dataSource]) {
|
||||
if (this.selectedCards[dataSource][k]) {
|
||||
ret.push(k);
|
||||
|
@ -11,7 +11,9 @@ export class FilterPipe implements PipeTransform {
|
||||
if (!items || !callback) {
|
||||
return items;
|
||||
}
|
||||
return items.filter(item => callback(item));
|
||||
const ret = items.filter(item => callback(item));
|
||||
if (ret.length === items.length) return items; // This will prevent a re-render
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,11 +2,19 @@
|
||||
|
||||
<ng-container *ngIf="items.length > virtualizeAfter; else dragList">
|
||||
<div class="example-list list-group-flush">
|
||||
<!-- <li-virtual-scroll [items]="items" [itemHeight]="itemHeight" [viewCache]="BufferAmount">-->
|
||||
<!-- <div *liVirtualItem="let item; let i=index" class="d-flex list-container">-->
|
||||
<!-- <ng-container [ngTemplateOutlet]="handle" [ngTemplateOutletContext]="{ $implicit: item, idx: i, isVirtualized: true }"></ng-container>-->
|
||||
<!-- <ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>-->
|
||||
|
||||
<!-- <ng-container [ngTemplateOutlet]="removeBtn" [ngTemplateOutletContext]="{$implicit: item, idx: i}"></ng-container>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- </li-virtual-scroll>-->
|
||||
<virtual-scroller #scroll [items]="items" [bufferAmount]="BufferAmount" [parentScroll]="parentScroll">
|
||||
<div class="example-box" *ngFor="let item of scroll.viewPortItems; index as i; trackBy: trackByIdentity">
|
||||
|
||||
<div class="d-flex list-container">
|
||||
|
||||
<ng-container [ngTemplateOutlet]="handle" [ngTemplateOutletContext]="{ $implicit: item, idx: i, isVirtualized: true }"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
@ -16,13 +24,13 @@
|
||||
</virtual-scroller>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #dragList>
|
||||
<div cdkDropList class="{{items.length > 0 ? 'example-list list-group-flush' : ''}}" (cdkDropListDropped)="drop($event)">
|
||||
<div class="example-box" *ngFor="let item of items; index as i" cdkDrag
|
||||
<div class="example-box" *ngFor="let item of items; index as i;" cdkDrag
|
||||
[cdkDragData]="item" cdkDragBoundary=".example-list"
|
||||
[cdkDragDisabled]="accessibilityMode || disabled || bulkMode" cdkDragPreviewContainer="parent">
|
||||
<div class="d-flex list-container">
|
||||
|
||||
<ng-container [ngTemplateOutlet]="handle" [ngTemplateOutletContext]="{ $implicit: item, idx: i, isVirtualized: false }"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
@ -32,13 +40,6 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #removeBtn let-item let-idx>
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, idx)" *ngIf="showRemoveButton" [disabled]="disabled">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{idx}}">{{t('remove-item-alt')}}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #handle let-item let-idx="idx" let-isVirtualized="isVirtualized">
|
||||
<div class="me-3 align-middle">
|
||||
<div class="align-middle" [ngClass]="{'accessibility-padding': accessibilityMode, 'bulk-padding': bulkMode}" *ngIf="accessibilityMode || bulkMode">
|
||||
@ -50,8 +51,8 @@
|
||||
</ng-container>
|
||||
<ng-container *ngIf="bulkMode">
|
||||
<label for="select-{{idx}}" class="form-label visually-hidden">{{t('bulk-select-label')}}</label>
|
||||
<input id="select-{{idx}}" class="form-check-input mt-0" type="checkbox" (change)="selectItem($event, item, idx)"
|
||||
[value]="bulkSelectionService.isCardSelected('sideNavStream', idx)">
|
||||
<input id="select-{{idx}}" class="form-check-input mt-0" type="checkbox" (change)="selectItem($event, idx)"
|
||||
[ngModel]="bulkSelectionService.isCardSelected('sideNavStream', idx)" [ngModelOptions]="{standalone: true}">
|
||||
</ng-container>
|
||||
|
||||
|
||||
@ -60,6 +61,13 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #removeBtn let-item let-idx>
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, idx)" *ngIf="showRemoveButton" [disabled]="disabled">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{idx}}">{{t('remove-item-alt')}}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<p class="visually-hidden" id="instructions">
|
||||
{{t('instructions-alt')}}
|
||||
</p>
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChild,
|
||||
ContentChild, DestroyRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
@ -16,7 +16,9 @@ import {NgIf, NgFor, NgTemplateOutlet, NgClass} from '@angular/common';
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {BulkSelectionService} from "../../../cards/bulk-selection.service";
|
||||
import {SeriesCardComponent} from "../../../cards/series-card/series-card.component";
|
||||
import {SideNavStream} from "../../../_models/sidenav/sidenav-stream";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {NgxVirtualScrollModule} from "@lithiumjs/ngx-virtual-scroll";
|
||||
|
||||
export interface IndexUpdateEvent {
|
||||
fromPosition: number;
|
||||
@ -36,7 +38,9 @@ export interface ItemRemoveEvent {
|
||||
styleUrls: ['./draggable-ordered-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, VirtualScrollerModule, NgFor, NgTemplateOutlet, CdkDropList, CdkDrag, CdkDragHandle, TranslocoDirective, NgClass, SeriesCardComponent]
|
||||
imports: [NgIf, VirtualScrollerModule, NgFor, NgTemplateOutlet, CdkDropList, CdkDrag,
|
||||
CdkDragHandle, TranslocoDirective, NgClass, SeriesCardComponent, FormsModule,
|
||||
NgxVirtualScrollModule, NgxVirtualScrollModule]
|
||||
})
|
||||
export class DraggableOrderedListComponent {
|
||||
|
||||
@ -62,26 +66,37 @@ export class DraggableOrderedListComponent {
|
||||
* When enabled, draggability is disabled and a checkbox renders instead of order box or drag handle
|
||||
*/
|
||||
@Input() bulkMode: boolean = false;
|
||||
@Input({required: true}) itemHeight: number = 60;
|
||||
@Input() trackByIdentity: TrackByFunction<any> = (index: number, item: any) => `${item.id}_${item.order}_${item.title}`;
|
||||
@Output() orderUpdated: EventEmitter<IndexUpdateEvent> = new EventEmitter<IndexUpdateEvent>();
|
||||
@Output() itemRemove: EventEmitter<ItemRemoveEvent> = new EventEmitter<ItemRemoveEvent>();
|
||||
@ContentChild('draggableItem') itemTemplate!: TemplateRef<any>;
|
||||
|
||||
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
public readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
get BufferAmount() {
|
||||
return Math.min(this.items.length / 20, 20);
|
||||
}
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) { }
|
||||
log(a: any, b: any) {console.log('item: ', a, 'index', b)}
|
||||
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) {
|
||||
this.bulkSelectionService.selections$.pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe((s) => {
|
||||
this.cdRef.markForCheck()
|
||||
});
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
if (event.previousIndex === event.currentIndex) return;
|
||||
if (event.previousIndex === event.currentIndex) return;
|
||||
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
|
||||
this.orderUpdated.emit({
|
||||
fromPosition: event.previousIndex,
|
||||
toPosition: event.currentIndex,
|
||||
item: this.items[event.currentIndex],
|
||||
item: event.item.data,
|
||||
fromAccessibilityMode: false
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
@ -110,7 +125,7 @@ export class DraggableOrderedListComponent {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
selectItem(updatedVal: Event, item: SideNavStream, index: number) {
|
||||
selectItem(updatedVal: Event, index: number) {
|
||||
const boolVal = (updatedVal.target as HTMLInputElement).value == 'true';
|
||||
|
||||
this.bulkSelectionService.handleCardSelection('sideNavStream', index, this.items.length, boolVal);
|
||||
|
@ -127,7 +127,7 @@
|
||||
</ng-template>
|
||||
|
||||
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
|
||||
[showRemoveButton]="false">
|
||||
[showRemoveButton]="false" [itemHeight]="148" [virtualizeAfter]="10">
|
||||
<ng-template #draggableItem let-item let-position="idx">
|
||||
<app-reading-list-item [ngClass]="{'content-container': items.length < 100, 'non-virtualized-container': items.length >= 100}" [item]="item" [position]="position" [libraryTypes]="libraryTypes"
|
||||
[promoted]="item.promoted" (read)="readChapter($event)" (remove)="itemRemoved($event, position)"></app-reading-list-item>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="row g-0 mb-1" *ngIf="tags.length > 0">
|
||||
<div class="row g-0 mb-1" *ngIf="tags && tags.length > 0">
|
||||
<div class="col-lg-3 col-md-4 col-sm-12">
|
||||
<h5>{{heading}}</h5>
|
||||
</div>
|
||||
|
@ -5,17 +5,11 @@ import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {NgbActiveModal, NgbNav, NgbNavContent, NgbNavItem, NgbNavLink, NgbNavOutlet} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {
|
||||
DraggableOrderedListComponent,
|
||||
IndexUpdateEvent
|
||||
} from "../../../reading-list/_components/draggable-ordered-list/draggable-ordered-list.component";
|
||||
import {
|
||||
ReadingListItemComponent
|
||||
} from "../../../reading-list/_components/reading-list-item/reading-list-item.component";
|
||||
import {forkJoin} from "rxjs";
|
||||
import {FilterService} from "../../../_services/filter.service";
|
||||
import {DashboardStreamListItemComponent} from "../dashboard-stream-list-item/dashboard-stream-list-item.component";
|
||||
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
|
||||
import {DashboardService} from "../../../_services/dashboard.service";
|
||||
import {DashboardStream} from "../../../_models/dashboard/dashboard-stream";
|
||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||
import {CustomizeDashboardStreamsComponent} from "../customize-dashboard-streams/customize-dashboard-streams.component";
|
||||
import {CustomizeSidenavStreamsComponent} from "../customize-sidenav-streams/customize-sidenav-streams.component";
|
||||
@ -40,7 +34,7 @@ enum TabID {
|
||||
})
|
||||
export class CustomizeDashboardModalComponent {
|
||||
|
||||
activeTab = TabID.Dashboard;
|
||||
activeTab = TabID.SideNav;
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
|
@ -1,6 +1,6 @@
|
||||
<ng-container *transloco="let t; read: 'customize-dashboard-streams'">
|
||||
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
|
||||
[showRemoveButton]="false">
|
||||
[showRemoveButton]="false" [itemHeight]="60">
|
||||
<ng-template #draggableItem let-position="idx" let-item>
|
||||
<app-dashboard-stream-list-item [item]="item" [position]="position" (hide)="updateVisibility($event, position)"></app-dashboard-stream-list-item>
|
||||
</ng-template>
|
||||
|
@ -22,3 +22,10 @@ app-dashboard-stream-list-item {
|
||||
background-color: var(--list-group-hover-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.virtual-scroller, virtual-scroller {
|
||||
width: 100%;
|
||||
height: calc(100vh - 85px);
|
||||
max-height: calc(var(--vh)*100 - 170px);
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,6 @@ import {FilterService} from "../../../_services/filter.service";
|
||||
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {forkJoin} from "rxjs";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {CommonStream} from "../../../_models/common-stream";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||
import {FilterPipe} from "../../../pipe/filter.pipe";
|
||||
|
||||
@ -76,8 +75,8 @@ export class CustomizeDashboardStreamsComponent {
|
||||
|
||||
updateVisibility(item: DashboardStream, position: number) {
|
||||
this.items[position].visible = !this.items[position].visible;
|
||||
this.dashboardService.updateDashboardStream(this.items[position]).subscribe();
|
||||
this.cdRef.markForCheck();
|
||||
this.dashboardService.updateDashboardStream(this.items[position]).subscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,18 +23,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<app-bulk-operations [modalMode]="true" [topOffset]="0" [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<app-draggable-ordered-list [items]="items | filter: filterSideNavStreams" (orderUpdated)="orderUpdated($event)"
|
||||
[accessibilityMode]="pageOperationsForm.get('accessibilityMode')!.value"
|
||||
[showRemoveButton]="false" [disabled]="listForm.get('filterSideNavStream')?.value"
|
||||
[bulkMode]="pageOperationsForm.get('bulkMode')!.value"
|
||||
[virtualizeAfter]="100"
|
||||
>
|
||||
<ng-template #draggableItem let-position="idx" let-item>
|
||||
<app-sidenav-stream-list-item [item]="item" [position]="position" (hide)="updateVisibility($event, position)"></app-sidenav-stream-list-item>
|
||||
</ng-template>
|
||||
</app-draggable-ordered-list>
|
||||
<div style="max-height: 500px; overflow-y: auto">
|
||||
<app-draggable-ordered-list [items]="items | filter: filterSideNavStreams" (orderUpdated)="orderUpdated($event)"
|
||||
[accessibilityMode]="pageOperationsForm.get('accessibilityMode')!.value"
|
||||
[showRemoveButton]="false" [disabled]="listForm.get('filterSideNavStream')!.value"
|
||||
[bulkMode]="pageOperationsForm.get('bulkMode')!.value"
|
||||
[virtualizeAfter]="virtualizeAfter"
|
||||
[itemHeight]="60"
|
||||
>
|
||||
<ng-template #draggableItem let-position="idx" let-item>
|
||||
<app-sidenav-stream-list-item [item]="item" [position]="position" (hide)="updateVisibility($event, position)"></app-sidenav-stream-list-item>
|
||||
</ng-template>
|
||||
</app-draggable-ordered-list>
|
||||
</div>
|
||||
|
||||
<h5>{{t('smart-filters-title')}}</h5>
|
||||
<h5 class="mt-3">{{t('smart-filters-title')}}</h5>
|
||||
<div class="mb-3" *ngIf="smartFilters.length >= 6">
|
||||
<label for="smart-filter-filter" class="form-label">{{t('filter')}}</label>
|
||||
<div class="input-group">
|
||||
|
@ -1,4 +1,12 @@
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnDestroy} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef, EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
|
||||
import {FilterService} from "../../../_services/filter.service";
|
||||
@ -23,6 +31,7 @@ import {Action, ActionItem} from "../../../_services/action-factory.service";
|
||||
import {BulkSelectionService} from "../../../cards/bulk-selection.service";
|
||||
import {filter, tap} from "rxjs/operators";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {KEY_CODES} from "../../../shared/_services/utility.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-customize-sidenav-streams',
|
||||
@ -38,6 +47,7 @@ export class CustomizeSidenavStreamsComponent implements OnDestroy {
|
||||
items: SideNavStream[] = [];
|
||||
smartFilters: SmartFilter[] = [];
|
||||
externalSources: ExternalSource[] = [];
|
||||
virtualizeAfter = 100;
|
||||
|
||||
listForm: FormGroup = new FormGroup({
|
||||
'filterSideNavStream': new FormControl('', []),
|
||||
@ -65,7 +75,9 @@ export class CustomizeSidenavStreamsComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
bulkActionCallback = (action: ActionItem<SideNavStream>, data: SideNavStream) => {
|
||||
const streams = this.bulkSelectionService.getSelectedCardsForSource('sideNavStream').map(index => this.items[parseInt(index, 10)]);
|
||||
const selectedItems = this.bulkSelectionService.getSelectedCardsForSource('sideNavStream');
|
||||
const streams = selectedItems
|
||||
.map(index => this.items[parseInt(index, 10)]);
|
||||
let visibleState = false;
|
||||
switch (action.action) {
|
||||
case Action.MarkAsVisible:
|
||||
@ -76,13 +88,17 @@ export class CustomizeSidenavStreamsComponent implements OnDestroy {
|
||||
break;
|
||||
}
|
||||
|
||||
for(let index of this.bulkSelectionService.getSelectedCardsForSource('sideNavStream').map(s => parseInt(s, 10))) {
|
||||
for(let index of selectedItems.map(s => parseInt(s, 10))) {
|
||||
this.items[index].visible = visibleState;
|
||||
this.items[index] = {...this.items[index]};
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
// Make bulk call
|
||||
this.sideNavService.bulkToggleSideNavStreamVisibility(streams.map(s => s.id), visibleState).subscribe(() => this.bulkSelectionService.deselectAll());
|
||||
this.sideNavService.bulkToggleSideNavStreamVisibility(streams.map(s => s.id), visibleState)
|
||||
.subscribe(() => {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -91,7 +107,22 @@ export class CustomizeSidenavStreamsComponent implements OnDestroy {
|
||||
private readonly externalSourceService = inject(ExternalSourceService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
|
||||
@HostListener('document:keydown.shift', ['$event'])
|
||||
handleKeypress(event: KeyboardEvent) {
|
||||
if (event.key === KEY_CODES.SHIFT) {
|
||||
this.bulkSelectionService.isShiftDown = true;
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keyup.shift', ['$event'])
|
||||
handleKeyUp(event: KeyboardEvent) {
|
||||
if (event.key === KEY_CODES.SHIFT) {
|
||||
this.bulkSelectionService.isShiftDown = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
constructor(public modal: NgbActiveModal) {
|
||||
|
||||
@ -140,8 +171,8 @@ export class CustomizeSidenavStreamsComponent implements OnDestroy {
|
||||
]).subscribe(results => {
|
||||
this.items = results[0];
|
||||
|
||||
// After 100 items, drag and drop is disabled to use virtualization
|
||||
if (this.items.length > 100) {
|
||||
// After X items, drag and drop is disabled to use virtualization
|
||||
if (this.items.length > this.virtualizeAfter) {
|
||||
this.pageOperationsForm.get('accessibilityMode')?.setValue(true);
|
||||
}
|
||||
|
||||
@ -192,12 +223,10 @@ export class CustomizeSidenavStreamsComponent implements OnDestroy {
|
||||
|
||||
orderUpdated(event: IndexUpdateEvent) {
|
||||
this.sideNavService.updateSideNavStreamPosition(event.item.name, event.item.id, event.fromPosition, event.toPosition).subscribe(() => {
|
||||
if (event.fromAccessibilityMode) {
|
||||
this.sideNavService.getSideNavStreams(false).subscribe((data) => {
|
||||
this.items = [...data];
|
||||
this.cdRef.markForCheck();
|
||||
})
|
||||
}
|
||||
this.sideNavService.getSideNavStreams(false).subscribe((data) => {
|
||||
this.items = [...data];
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -686,8 +686,8 @@
|
||||
"continue-incognito": "Continue Incognito",
|
||||
"read-options-alt": "Read options",
|
||||
"incognito": "Incognito",
|
||||
"remove-from-want-to-read": "Remove from Want to Read",
|
||||
"add-to-want-to-read": "Add to Want to Read",
|
||||
"remove-from-want-to-read": "{{actionable.remove-from-want-to-read}}",
|
||||
"add-to-want-to-read": "{{actionable.add-to-want-to-read}}",
|
||||
"edit-series-alt": "Edit Series Information",
|
||||
"download-series--tooltip": "Download Series",
|
||||
"downloading-status": "Downloading…",
|
||||
@ -1686,7 +1686,9 @@
|
||||
"tags-label": "{{filter-field-pipe.tags}}",
|
||||
"genres-label": "{{filter-field-pipe.genres}}",
|
||||
"view-series": "View Series",
|
||||
"vols-and-chapters": "{{volCount}} Volumes / {{chpCount}} Chapters"
|
||||
"vols-and-chapters": "{{volCount}} Volumes / {{chpCount}} Chapters",
|
||||
"remove-from-want-to-read": "{{actionable.remove-from-want-to-read}}",
|
||||
"add-to-want-to-read": "{{actionable.add-to-want-to-read}}"
|
||||
},
|
||||
|
||||
"server-stats": {
|
||||
|
@ -11,6 +11,7 @@
|
||||
--body-text-color: #efefef;
|
||||
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
--primary-color-scrollbar: rgba(74,198,148,0.75);
|
||||
--text-muted-color: lightgrey;
|
||||
|
||||
/* Meta and Globals */
|
||||
--theme-color: #000000;
|
||||
|
10
openapi.json
10
openapi.json
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.8.10"
|
||||
"version": "0.7.8.11"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@ -9037,6 +9037,14 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seriesId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user