From 5f11973696f0f5fd965446b3309bd4833c18e4f7 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Tue, 17 Oct 2023 11:05:14 -0500 Subject: [PATCH] More Polish (#2320) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- API/Controllers/AccountController.cs | 1 + API/Controllers/ReviewController.cs | 1 - API/Controllers/SeriesController.cs | 8 +- .../Recommendation/ExternalSeriesDetailDto.cs | 3 +- API/DTOs/Recommendation/PlusMediaFormat.cs | 15 ---- API/DTOs/Scrobbling/ScrobbleDto.cs | 11 ++- API/Services/Plus/ExternalMetadataService.cs | 66 ++++++++++++++--- API/Services/Plus/ScrobblingService.cs | 12 ++- API/Services/StreamService.cs | 6 +- API/Services/Tasks/Scanner/Parser/Parser.cs | 2 +- UI/Web/package-lock.json | 74 +++++++++++-------- UI/Web/package.json | 2 + UI/Web/src/app/_guards/auth.guard.ts | 1 + UI/Web/src/app/_services/series.service.ts | 5 +- .../series-preview-drawer.component.html | 26 +++++-- .../series-preview-drawer.component.scss | 4 + .../series-preview-drawer.component.ts | 36 +++++++-- .../bulk-operations.component.html | 26 +++---- .../bulk-operations.component.ts | 7 +- .../src/app/cards/bulk-selection.service.ts | 4 +- UI/Web/src/app/pipe/filter.pipe.ts | 4 +- .../draggable-ordered-list.component.html | 32 +++++--- .../draggable-ordered-list.component.ts | 29 ++++++-- .../reading-list-detail.component.html | 2 +- .../metadata-detail.component.html | 2 +- .../customize-dashboard-modal.component.ts | 8 +- ...customize-dashboard-streams.component.html | 2 +- ...customize-dashboard-streams.component.scss | 7 ++ .../customize-dashboard-streams.component.ts | 3 +- .../customize-sidenav-streams.component.html | 25 ++++--- .../customize-sidenav-streams.component.ts | 55 ++++++++++---- UI/Web/src/assets/langs/en.json | 8 +- UI/Web/src/theme/themes/dark.scss | 1 + openapi.json | 10 ++- 34 files changed, 337 insertions(+), 161 deletions(-) delete mode 100644 API/DTOs/Recommendation/PlusMediaFormat.cs diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index a8db4900c..cc7d0d857 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -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) { diff --git a/API/Controllers/ReviewController.cs b/API/Controllers/ReviewController.cs index e2424a6dc..50bc55649 100644 --- a/API/Controllers/ReviewController.cs +++ b/API/Controllers/ReviewController.cs @@ -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() diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 1cbb13712..0fe0a0f78 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -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 logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, @@ -584,14 +584,14 @@ public class SeriesController : BaseApiController [Authorize(Policy = "RequireAdminRole")] [HttpGet("external-series-detail")] - public async Task> GetExternalSeriesInfo(int? aniListId, long? malId) + public async Task> 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(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); } diff --git a/API/DTOs/Recommendation/ExternalSeriesDetailDto.cs b/API/DTOs/Recommendation/ExternalSeriesDetailDto.cs index c6ef6ab9d..b01f1369c 100644 --- a/API/DTOs/Recommendation/ExternalSeriesDetailDto.cs +++ b/API/DTOs/Recommendation/ExternalSeriesDetailDto.cs @@ -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 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 Genres { get; set; } diff --git a/API/DTOs/Recommendation/PlusMediaFormat.cs b/API/DTOs/Recommendation/PlusMediaFormat.cs deleted file mode 100644 index da563b688..000000000 --- a/API/DTOs/Recommendation/PlusMediaFormat.cs +++ /dev/null @@ -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 -} diff --git a/API/DTOs/Scrobbling/ScrobbleDto.cs b/API/DTOs/Scrobbling/ScrobbleDto.cs index e58de0576..ca2c2e528 100644 --- a/API/DTOs/Scrobbling/ScrobbleDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleDto.cs @@ -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 } +/// +/// Represents PlusMediaFormat +/// public enum MediaFormat { + [Description("Manga")] Manga = 1, + [Description("Comic")] Comic = 2, + [Description("LightNovel")] LightNovel = 3, - Book = 4 + [Description("Book")] + Book = 4, + Unknown = 5 } diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index f9756bfc0..8b8d046c1 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -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; +/// +/// Used for matching and fetching metadata on a series +/// +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 GetExternalSeriesDetail(int? aniListId, long? malId); + Task 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 GetExternalSeriesDetail(int? aniListId, long? malId) + public async Task 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 GetSeriesDetail(string license, int? anilistId, long? malId) + private async Task 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(series.Metadata.WebLinks, ScrobblingService.AniListWeblinkWebsite); + } + if (payload.MalId <= 0) + { + payload.MalId = ScrobblingService.ExtractId(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(); } @@ -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 + }; + } } diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index 7c9afaeee..53ff876a3 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -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 ProcessEvents(IEnumerable events, IDictionary userRateLimits, int usersToScrobble, int progressCounter, int totalProgress, Func> createEvent) { diff --git a/API/Services/StreamService.cs b/API/Services/StreamService.cs index 2455da99e..54edbbd12 100644 --- a/API/Services/StreamService.cs +++ b/API/Services/StreamService.cs @@ -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); } diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 30a2beddc..8dfe314a6 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -28,7 +28,7 @@ public static class Parser private static readonly ImmutableArray 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' }; diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index e16938c06..070822e59 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -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" diff --git a/UI/Web/package.json b/UI/Web/package.json index 53ac57192..ba4128128 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -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", diff --git a/UI/Web/src/app/_guards/auth.guard.ts b/UI/Web/src/app/_guards/auth.guard.ts index 3df2861b5..8ef7e5912 100644 --- a/UI/Web/src/app/_guards/auth.guard.ts +++ b/UI/Web/src/app/_guards/auth.guard.ts @@ -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) { diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index fad1c6d5d..e4d91b410 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -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(this.baseUrl + 'series/external-series-detail?aniListId=' + (aniListId || 0) + '&malId=' + (malId || 0)); + getExternalSeriesDetails(aniListId?: number, malId?: number, seriesId?: number) { + return this.httpClient.get(this.baseUrl + 'series/external-series-detail?aniListId=' + (aniListId || 0) + '&malId=' + (malId || 0) + '&seriesId=' + (seriesId || 0)); } + } diff --git a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.html b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.html index 76d98872a..618eba9fc 100644 --- a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.html +++ b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.html @@ -1,19 +1,22 @@
- - -
- {{name}} -
-
+ {{name}}
+ +
+ +
+
+ - {{t('series-preview-drawer.vols-and-chapters', {volCount: externalSeries.volumeCount, chpCount: externalSeries.chapterCount})}} +
+ {{t('series-preview-drawer.vols-and-chapters', {volCount: externalSeries.volumeCount, chpCount: externalSeries.chapterCount})}} +
@@ -64,7 +67,14 @@ - {{localSeries.publicationStatus | publicationStatus}} +
+ {{localSeries.publicationStatus | publicationStatus}} + +
diff --git a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.scss b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.scss index 4d1a6c6f5..f13699570 100644 --- a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.scss +++ b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.scss @@ -8,3 +8,7 @@ ::ng-deep .person-img { margin-top: 24px; margin-left: 24px; } + +.muted { + font-size: 14px; +} diff --git a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.ts b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.ts index 8715112e6..1e6a420b0 100644 --- a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.ts +++ b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.ts @@ -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() { diff --git a/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.html b/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.html index 173253428..ef9264f6f 100644 --- a/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.html +++ b/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.html @@ -3,21 +3,21 @@
- - - {{t('items-selected',{num: selectionCount | number})}} - + + + {{t('items-selected',{num: selectionCount | number})}} + - - - + + + Bulk Actions diff --git a/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts b/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts index 94c89ed74..8bfdf964f 100644 --- a/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts +++ b/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts @@ -40,14 +40,13 @@ export class BulkOperationsComponent implements OnInit { hasMarkAsRead: boolean = false; hasMarkAsUnread: boolean = false; actions: Array> = []; + 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, data: any) { - this.actionCallback(action, data); - } - performAction(action: ActionItem) { this.actionCallback(action, null); } diff --git a/UI/Web/src/app/cards/bulk-selection.service.ts b/UI/Web/src/app/cards/bulk-selection.service.ts index 7526514a8..be8d1c9e2 100644 --- a/UI/Web/src/app/cards/bulk-selection.service.ts +++ b/UI/Web/src/app/cards/bulk-selection.service.ts @@ -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); diff --git a/UI/Web/src/app/pipe/filter.pipe.ts b/UI/Web/src/app/pipe/filter.pipe.ts index cbbea512f..61890e6b2 100644 --- a/UI/Web/src/app/pipe/filter.pipe.ts +++ b/UI/Web/src/app/pipe/filter.pipe.ts @@ -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; } } diff --git a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html index df146801a..0eff1661a 100644 --- a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html +++ b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html @@ -2,11 +2,19 @@
+ + + + + + + + +
- @@ -16,13 +24,13 @@
+
-
- @@ -32,13 +40,6 @@
- - - -
@@ -50,8 +51,8 @@ - + @@ -60,6 +61,13 @@
+ + + +

{{t('instructions-alt')}}

diff --git a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts index 36935c18b..1c5f3fb60 100644 --- a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts +++ b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts @@ -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 = (index: number, item: any) => `${item.id}_${item.order}_${item.title}`; @Output() orderUpdated: EventEmitter = new EventEmitter(); @Output() itemRemove: EventEmitter = new EventEmitter(); @ContentChild('draggableItem') itemTemplate!: TemplateRef; 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) { - 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); diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html index 52af401e7..6c244be10 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html @@ -127,7 +127,7 @@ + [showRemoveButton]="false" [itemHeight]="148" [virtualizeAfter]="10"> diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html index 2983385aa..5b96509af 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html @@ -1,4 +1,4 @@ -
+
{{heading}}
diff --git a/UI/Web/src/app/sidenav/_components/customize-dashboard-modal/customize-dashboard-modal.component.ts b/UI/Web/src/app/sidenav/_components/customize-dashboard-modal/customize-dashboard-modal.component.ts index bee0d9b31..b38feb3ed 100644 --- a/UI/Web/src/app/sidenav/_components/customize-dashboard-modal/customize-dashboard-modal.component.ts +++ b/UI/Web/src/app/sidenav/_components/customize-dashboard-modal/customize-dashboard-modal.component.ts @@ -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); diff --git a/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.html b/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.html index f2edfb97c..87689c238 100644 --- a/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.html +++ b/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.html @@ -1,6 +1,6 @@ + [showRemoveButton]="false" [itemHeight]="60"> diff --git a/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.scss b/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.scss index edd7671a1..6c4f625c0 100644 --- a/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.scss +++ b/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.scss @@ -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); +} + diff --git a/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.ts b/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.ts index cd4b96234..49be41ced 100644 --- a/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.ts +++ b/UI/Web/src/app/sidenav/_components/customize-dashboard-streams/customize-dashboard-streams.component.ts @@ -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(); } } diff --git a/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.html b/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.html index f1d9ab9a7..26d189ea9 100644 --- a/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.html +++ b/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.html @@ -23,18 +23,21 @@
- - - - - +
+ + + + + +
-
{{t('smart-filters-title')}}
+
{{t('smart-filters-title')}}
diff --git a/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.ts b/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.ts index 56ae474d1..87566d883 100644 --- a/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.ts +++ b/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.ts @@ -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, 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(); + }); }); } diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 4e1a6ee8a..c053e3a5c 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -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": { diff --git a/UI/Web/src/theme/themes/dark.scss b/UI/Web/src/theme/themes/dark.scss index bf8aa40a7..027a9d686 100644 --- a/UI/Web/src/theme/themes/dark.scss +++ b/UI/Web/src/theme/themes/dark.scss @@ -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; diff --git a/openapi.json b/openapi.json index 2290f598e..500c8dfac 100644 --- a/openapi.json +++ b/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": {