diff --git a/API/API.csproj b/API/API.csproj index 9cdecc8d1..c65c8ebcf 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -12,10 +12,10 @@ latestmajor - - - - + + + + false diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs index 475cafc71..a9f19a951 100644 --- a/API/Controllers/ChapterController.cs +++ b/API/Controllers/ChapterController.cs @@ -88,6 +88,7 @@ public class ChapterController : BaseApiController chapter.AgeRating = dto.AgeRating; } + dto.Summary ??= string.Empty; if (chapter.Summary != dto.Summary.Trim()) { @@ -260,6 +261,8 @@ public class ChapterController : BaseApiController #endregion + _unitOfWork.ChapterRepository.Update(chapter); + if (!_unitOfWork.HasChanges()) { return Ok(); diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 43058d90f..1b405f3b1 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -310,9 +310,9 @@ public class LibraryController : BaseApiController [Authorize(Policy = "RequireAdminRole")] [HttpPost("refresh-metadata")] - public ActionResult RefreshMetadata(int libraryId, bool force = true) + public ActionResult RefreshMetadata(int libraryId, bool force = true, bool forceColorscape = true) { - _taskScheduler.RefreshMetadata(libraryId, force); + _taskScheduler.RefreshMetadata(libraryId, force, forceColorscape); return Ok(); } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 159c8c922..fe96e3841 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -835,16 +835,26 @@ public class ReaderController : BaseApiController return Ok(_unitOfWork.UserTableOfContentRepository.GetPersonalToC(User.GetUserId(), chapterId)); } + /// + /// Deletes the user's personal table of content for the given chapter + /// + /// + /// + /// + /// [HttpDelete("ptoc")] public async Task DeletePersonalToc([FromQuery] int chapterId, [FromQuery] int pageNum, [FromQuery] string title) { var userId = User.GetUserId(); if (string.IsNullOrWhiteSpace(title)) return BadRequest(await _localizationService.Translate(userId, "name-required")); if (pageNum < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number")); + var toc = await _unitOfWork.UserTableOfContentRepository.Get(userId, chapterId, pageNum, title); if (toc == null) return Ok(); + _unitOfWork.UserTableOfContentRepository.Remove(toc); await _unitOfWork.CommitAsync(); + return Ok(); } diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index b0b25fb04..ef3f20fc6 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -402,7 +402,7 @@ public class SeriesController : BaseApiController [HttpPost("refresh-metadata")] public ActionResult RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto) { - _taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate); + _taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate, refreshSeriesDto.ForceColorscape); return Ok(); } diff --git a/API/DTOs/RefreshSeriesDto.cs b/API/DTOs/RefreshSeriesDto.cs index 64a684394..0e94fc44b 100644 --- a/API/DTOs/RefreshSeriesDto.cs +++ b/API/DTOs/RefreshSeriesDto.cs @@ -18,4 +18,9 @@ public class RefreshSeriesDto /// /// This is expensive if true. Defaults to true. public bool ForceUpdate { get; init; } = true; + /// + /// Should the task force re-calculation of colorscape. + /// + /// This is expensive if true. Defaults to true. + public bool ForceColorscape { get; init; } = false; } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 60b5c99fd..75a47f479 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -82,7 +82,7 @@ public class ImageService : IImageService public const string CollectionTagCoverImageRegex = @"tag\d+"; public const string ReadingListCoverImageRegex = @"readinglist\d+"; - private const double WhiteThreshold = 0.90; // Colors with lightness above this are considered too close to white + private const double WhiteThreshold = 0.95; // Colors with lightness above this are considered too close to white private const double BlackThreshold = 0.25; // Colors with lightness below this are considered too close to black @@ -486,9 +486,11 @@ public class ImageService : IImageService // Resize the image to speed up processing var resizedImage = image.Resize(0.1); + var processedImage = PreProcessImage(resizedImage); + // Convert image to RGB array - var pixels = resizedImage.WriteToMemory().ToArray(); + var pixels = processedImage.WriteToMemory().ToArray(); // Convert to list of Vector3 (RGB) var rgbPixels = new List(); @@ -502,6 +504,9 @@ public class ImageService : IImageService var sorted = SortByVibrancy(clusters); + // Ensure white and black are not selected as primary/secondary colors + sorted = sorted.Where(c => !IsCloseToWhiteOrBlack(c)).ToList(); + if (sorted.Count >= 2) { return (sorted[0], sorted[1]); @@ -535,17 +540,18 @@ public class ImageService : IImageService private static Image PreProcessImage(Image image) { + return image; // Create a mask for white and black pixels var whiteMask = image.Colourspace(Enums.Interpretation.Lab)[0] > (WhiteThreshold * 100); var blackMask = image.Colourspace(Enums.Interpretation.Lab)[0] < (BlackThreshold * 100); // Create a replacement color (e.g., medium gray) - var replacementColor = new[] { 128.0, 128.0, 128.0 }; + var replacementColor = new[] { 240.0, 240.0, 240.0 }; // Apply the masks to replace white and black pixels var processedImage = image.Copy(); processedImage = processedImage.Ifthenelse(whiteMask, replacementColor); - processedImage = processedImage.Ifthenelse(blackMask, replacementColor); + //processedImage = processedImage.Ifthenelse(blackMask, replacementColor); return processedImage; } @@ -627,6 +633,13 @@ public class ImageService : IImageService }).ToList(); } + private static bool IsCloseToWhiteOrBlack(Vector3 color) + { + var threshold = 30; + return (color.X > 255 - threshold && color.Y > 255 - threshold && color.Z > 255 - threshold) || + (color.X < threshold && color.Y < threshold && color.Z < threshold); + } + private static string RgbToHex(Vector3 color) { return $"#{(int)color.X:X2}{(int)color.Y:X2}{(int)color.Z:X2}"; diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index e92f9f77b..7782b8584 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Threading.Tasks; using API.Comparators; @@ -27,7 +26,7 @@ public interface IMetadataService /// [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false); + Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false); /// /// Performs a forced refresh of cover images just for a series and it's nested entities /// @@ -35,8 +34,8 @@ public interface IMetadataService /// /// Overrides any cache logic and forces execution - Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true); - Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false); + Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true, bool forceColorScape = true); + Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false, bool forceColorScape = true); Task RemoveAbandonedMetadataKeys(); } @@ -75,7 +74,8 @@ public class MetadataService : IMetadataService /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image /// Convert image to Encoding Format when extracting the cover - private Task UpdateChapterCoverImage(Chapter? chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize) + /// Force colorscape gen + private Task UpdateChapterCoverImage(Chapter? chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceColorScape = false) { if (chapter == null) return Task.FromResult(false); @@ -86,7 +86,7 @@ public class MetadataService : IMetadataService _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked)) { - if (NeedsColorSpace(chapter)) + if (NeedsColorSpace(chapter, forceColorScape)) { _imageService.UpdateColorScape(chapter); _unitOfWork.ChapterRepository.Update(chapter); @@ -118,9 +118,11 @@ public class MetadataService : IMetadataService firstFile.UpdateLastModified(); } - private static bool NeedsColorSpace(IHasCoverImage? entity) + private static bool NeedsColorSpace(IHasCoverImage? entity, bool force) { if (entity == null) return false; + if (force) return true; + return !string.IsNullOrEmpty(entity.CoverImage) && (string.IsNullOrEmpty(entity.PrimaryColor) || string.IsNullOrEmpty(entity.SecondaryColor)); } @@ -132,7 +134,8 @@ public class MetadataService : IMetadataService /// /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image - private Task UpdateVolumeCoverImage(Volume? volume, bool forceUpdate) + /// Force updating colorscape + private Task UpdateVolumeCoverImage(Volume? volume, bool forceUpdate, bool forceColorScape = false) { // We need to check if Volume coverImage matches first chapters if forceUpdate is false if (volume == null) return Task.FromResult(false); @@ -141,7 +144,7 @@ public class MetadataService : IMetadataService _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, volume.CoverImage), null, volume.Created, forceUpdate)) { - if (NeedsColorSpace(volume)) + if (NeedsColorSpace(volume, forceColorScape)) { _imageService.UpdateColorScape(volume); _unitOfWork.VolumeRepository.Update(volume); @@ -176,7 +179,7 @@ public class MetadataService : IMetadataService /// /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image - private Task UpdateSeriesCoverImage(Series? series, bool forceUpdate) + private Task UpdateSeriesCoverImage(Series? series, bool forceUpdate, bool forceColorScape = false) { if (series == null) return Task.CompletedTask; @@ -185,13 +188,12 @@ public class MetadataService : IMetadataService null, series.Created, forceUpdate, series.CoverImageLocked)) { // Check if we don't have a primary/seconary color - if (NeedsColorSpace(series)) + if (NeedsColorSpace(series, forceColorScape)) { _imageService.UpdateColorScape(series); _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); } - return Task.CompletedTask; } @@ -211,7 +213,7 @@ public class MetadataService : IMetadataService /// /// /// - private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize) + private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceColorScape = false) { _logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName); try @@ -224,7 +226,7 @@ public class MetadataService : IMetadataService var index = 0; foreach (var chapter in volume.Chapters) { - var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat, coverImageSize); + var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat, coverImageSize, forceColorScape); // If cover was update, either the file has changed or first scan and we should force a metadata update UpdateChapterLastModified(chapter, forceUpdate || chapterUpdated); if (index == 0 && chapterUpdated) @@ -235,7 +237,7 @@ public class MetadataService : IMetadataService index++; } - var volumeUpdated = await UpdateVolumeCoverImage(volume, firstChapterUpdated || forceUpdate); + var volumeUpdated = await UpdateVolumeCoverImage(volume, firstChapterUpdated || forceUpdate, forceColorScape); if (volumeIndex == 0 && volumeUpdated) { firstVolumeUpdated = true; @@ -243,7 +245,7 @@ public class MetadataService : IMetadataService volumeIndex++; } - await UpdateSeriesCoverImage(series, firstVolumeUpdated || forceUpdate); + await UpdateSeriesCoverImage(series, firstVolumeUpdated || forceUpdate, forceColorScape); } catch (Exception ex) { @@ -258,9 +260,10 @@ public class MetadataService : IMetadataService /// This can be heavy on memory first run /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image + /// Force updating colorscape [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false) + public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false) { var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); if (library == null) return; @@ -308,7 +311,7 @@ public class MetadataService : IMetadataService try { - await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize); + await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize, forceColorScape); } catch (Exception ex) { @@ -349,7 +352,8 @@ public class MetadataService : IMetadataService /// /// /// Overrides any cache logic and forces execution - public async Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true) + /// Will ensure that the colorscape is regenned + public async Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true, bool forceColorScape = true) { var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); if (series == null) @@ -361,7 +365,8 @@ public class MetadataService : IMetadataService var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var encodeFormat = settings.EncodeMediaAs; var coverImageSize = settings.CoverImageSize; - await GenerateCoversForSeries(series, encodeFormat, coverImageSize, forceUpdate); + + await GenerateCoversForSeries(series, encodeFormat, coverImageSize, forceUpdate, forceColorScape); } /// @@ -370,13 +375,14 @@ public class MetadataService : IMetadataService /// A full Series, with metadata, chapters, etc /// When saving the file, what encoding should be used /// - public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false) + /// Forces just colorscape generation + public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false, bool forceColorScape = true) { var sw = Stopwatch.StartNew(); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name)); - await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize); + await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize, forceColorScape); if (_unitOfWork.HasChanges()) diff --git a/API/Services/ReviewService.cs b/API/Services/ReviewService.cs index 69ab784ae..c2c876b4b 100644 --- a/API/Services/ReviewService.cs +++ b/API/Services/ReviewService.cs @@ -10,6 +10,7 @@ namespace API.Services; public static class ReviewService { + private const int BodyTextLimit = 175; public static IEnumerable SelectSpectrumOfReviews(IList reviews) { IList externalReviews; @@ -76,7 +77,7 @@ public static class ReviewService plainText = Regex.Replace(plainText, @"__", string.Empty); // Take the first 100 characters - plainText = plainText.Length > 100 ? plainText.Substring(0, 100) : plainText; + plainText = plainText.Length > 100 ? plainText.Substring(0, BodyTextLimit) : plainText; return plainText + "…"; } diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index ebfc2f145..cda8f68dd 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -27,8 +27,8 @@ public interface ITaskScheduler Task ScanLibrary(int libraryId, bool force = false); Task ScanLibraries(bool force = false); void CleanupChapters(int[] chapterIds); - void RefreshMetadata(int libraryId, bool forceUpdate = true); - void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false); + void RefreshMetadata(int libraryId, bool forceUpdate = true, bool forceColorscape = true); + void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false, bool forceColorscape = false); Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false); void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false); @@ -371,12 +371,12 @@ public class TaskScheduler : ITaskScheduler BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); } - public void RefreshMetadata(int libraryId, bool forceUpdate = true) + public void RefreshMetadata(int libraryId, bool forceUpdate = true, bool forceColorscape = true) { var alreadyEnqueued = HasAlreadyEnqueuedTask(MetadataService.Name, "GenerateCoversForLibrary", - [libraryId, true]) || + [libraryId, true, true]) || HasAlreadyEnqueuedTask("MetadataService", "GenerateCoversForLibrary", - [libraryId, false]); + [libraryId, false, false]); if (alreadyEnqueued) { _logger.LogInformation("A duplicate request to refresh metadata for library occured. Skipping"); @@ -384,19 +384,19 @@ public class TaskScheduler : ITaskScheduler } _logger.LogInformation("Enqueuing library metadata refresh for: {LibraryId}", libraryId); - BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForLibrary(libraryId, forceUpdate)); + BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForLibrary(libraryId, forceUpdate, forceColorscape)); } - public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false) + public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false, bool forceColorscape = false) { - if (HasAlreadyEnqueuedTask(MetadataService.Name,"GenerateCoversForSeries", [libraryId, seriesId, forceUpdate])) + if (HasAlreadyEnqueuedTask(MetadataService.Name,"GenerateCoversForSeries", [libraryId, seriesId, forceUpdate, forceColorscape])) { _logger.LogInformation("A duplicate request to refresh metadata for library occured. Skipping"); return; } _logger.LogInformation("Enqueuing series metadata refresh for: {SeriesId}", seriesId); - BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(libraryId, seriesId, forceUpdate)); + BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(libraryId, seriesId, forceUpdate, forceColorscape)); } public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false) diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 7156ba4ad..5fbc2602d 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -221,7 +221,7 @@ public class ScannerService : IScannerService var libraryPaths = library.Folders.Select(f => f.Path).ToList(); if (await ShouldScanSeries(seriesId, library, libraryPaths, series, true) != ScanCancelReason.NoCancel) { - BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(series.LibraryId, seriesId, false)); + BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(series.LibraryId, seriesId, false, false)); BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, bypassFolderOptimizationChecks)); return; } diff --git a/UI/Web/src/_series-detail-common.scss b/UI/Web/src/_series-detail-common.scss index ca4f7c849..88b97ffd1 100644 --- a/UI/Web/src/_series-detail-common.scss +++ b/UI/Web/src/_series-detail-common.scss @@ -16,9 +16,8 @@ font-size: 0.8rem; } -.btn { - //padding: 4px 8px !important; - //font-size: 0.8rem !important; +.main-container { + overflow: unset !important; } .btn-group > .btn.dropdown-toggle-split:not(first-child){ @@ -112,6 +111,7 @@ -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; scrollbar-width: none; + box-shadow: inset -1px -2px 0px -1px var(--elevation-layer9); } .carousel-tabs-container::-webkit-scrollbar { display: none; @@ -119,3 +119,92 @@ .nav-tabs { flex-wrap: nowrap; } + +.upper-details { + font-size: 0.9rem; +} + +::ng-deep .carousel-container .header i.fa-plus, ::ng-deep .carousel-container .header i.fa-pen{ + border-width: 1px; + border-style: solid; + border-radius: 5px; + border-color: var(--primary-color); + padding: 5px; + vertical-align: middle; + + &:hover { + background-color: var(--primary-color-dark-shade); + } +} + +::ng-deep .image-container.mobile-bg app-image img { + max-height: 400px; + object-fit: contain; +} + +@media (max-width: 768px) { + .carousel-tabs-container { + mask-image: linear-gradient(transparent, black 0%, black 90%, transparent 100%); + -webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%); + } +} + +::ng-deep .image-container.mobile-bg app-image img { + max-height: 100dvh !important; + object-fit: cover !important; +} + +/* col-lg */ +@media screen and (max-width: 991px) { + .image-container.mobile-bg{ + width: 100vw; + top: calc(var(--nav-offset) - 20px); + left: 0; + pointer-events: none; + position: fixed !important; + display: block !important; + max-height: unset !important; + max-width: unset !important; + height: 100dvh !important; + } + + ::ng-deep .image-container.mobile-bg app-image img { + max-height: unset !important; + opacity: 0.05 !important; + filter: blur(5px) !important; + max-width: 100dvw; + height: 100dvh !important; + overflow: hidden; + position: absolute; + top: 0; + left: 0; + object-fit: cover; + } + + .progress-banner { + display:none; + } + + .under-image { + display: none; + } + +} +.upper-details { + font-size: 0.9rem; +} + +@media (max-width: 768px) { + .carousel-tabs-container { + mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%); + -webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%); + } +} + +.under-image { + background-color: var(--breadcrumb-bg-color); + color: white; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + text-align: center; +} diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 72eb69931..5ccdf9b22 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -91,9 +91,10 @@ export class ActionService { * @param library Partial Library, must have id and name populated * @param callback Optional callback to perform actions after API completes * @param forceUpdate Optional Should we force + * @param forceColorscape Optional Should we force colorscape gen * @returns */ - async refreshLibraryMetadata(library: Partial, callback?: LibraryActionCallback, forceUpdate: boolean = true) { + async refreshLibraryMetadata(library: Partial, callback?: LibraryActionCallback, forceUpdate: boolean = true, forceColorscape: boolean = false) { if (!library.hasOwnProperty('id') || library.id === undefined) { return; } @@ -110,7 +111,7 @@ export class ActionService { const message = forceUpdate ? 'toasts.refresh-covers-queued' : 'toasts.generate-colorscape-queued'; - this.libraryService.refreshMetadata(library?.id, forceUpdate).subscribe((res: any) => { + this.libraryService.refreshMetadata(library?.id, forceUpdate, forceColorscape).subscribe((res: any) => { this.toastr.info(translate(message, {name: library.name})); if (callback) { @@ -236,8 +237,9 @@ export class ActionService { * @param series Series, must have libraryId, id and name populated * @param callback Optional callback to perform actions after API completes * @param forceUpdate If cache should be checked or not + * @param forceColorscape If cache should be checked or not */ - async refreshSeriesMetadata(series: Series, callback?: SeriesActionCallback, forceUpdate: boolean = true) { + async refreshSeriesMetadata(series: Series, callback?: SeriesActionCallback, forceUpdate: boolean = true, forceColorscape: boolean = false) { // Prompt the user if we are doing a forced call if (forceUpdate) { @@ -251,7 +253,7 @@ export class ActionService { const message = forceUpdate ? 'toasts.refresh-covers-queued' : 'toasts.generate-colorscape-queued'; - this.seriesService.refreshMetadata(series, forceUpdate).pipe(take(1)).subscribe((res: any) => { + this.seriesService.refreshMetadata(series, forceUpdate, forceColorscape).pipe(take(1)).subscribe((res: any) => { this.toastr.info(translate(message, {name: series.name})); if (callback) { callback(series); diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index e6fee578a..2373d1998 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -97,8 +97,8 @@ export class LibraryService { return this.httpClient.post(this.baseUrl + 'library/analyze?libraryId=' + libraryId, {}); } - refreshMetadata(libraryId: number, forceUpdate = false) { - return this.httpClient.post(this.baseUrl + 'library/refresh-metadata?libraryId=' + libraryId + '&force=' + forceUpdate, {}); + refreshMetadata(libraryId: number, forceUpdate = false, forceColorscape = false) { + return this.httpClient.post(this.baseUrl + `library/refresh-metadata?libraryId=${libraryId}&force=${forceUpdate}&forceColorscape=${forceColorscape}`, {}); } create(model: {name: string, type: number, folders: string[]}) { diff --git a/UI/Web/src/app/_services/nav.service.ts b/UI/Web/src/app/_services/nav.service.ts index 5364234b9..0578fc500 100644 --- a/UI/Web/src/app/_services/nav.service.ts +++ b/UI/Web/src/app/_services/nav.service.ts @@ -1,13 +1,12 @@ -import { DOCUMENT } from '@angular/common'; -import {DestroyRef, inject, Inject, Injectable, OnDestroy, Renderer2, RendererFactory2} from '@angular/core'; -import {filter, ReplaySubject, Subject, take} from 'rxjs'; +import {DOCUMENT} from '@angular/common'; +import {DestroyRef, inject, Inject, Injectable, Renderer2, RendererFactory2, RendererStyleFlags2} from '@angular/core'; +import {distinctUntilChanged, filter, ReplaySubject, take} from 'rxjs'; import {HttpClient} from "@angular/common/http"; import {environment} from "../../environments/environment"; import {SideNavStream} from "../_models/sidenav/sidenav-stream"; import {TextResonse} from "../_types/text-response"; -import {DashboardStream} from "../_models/dashboard/dashboard-stream"; import {AccountService} from "./account.service"; -import {map, tap} from "rxjs/operators"; +import {map} from "rxjs/operators"; import {NavigationEnd, Router} from "@angular/router"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; @@ -98,22 +97,28 @@ export class NavService { * Shows the top nav bar. This should be visible on all pages except the reader. */ showNavBar() { - this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', 'var(--nav-offset)'); - this.renderer.removeStyle(this.document.querySelector('body'), 'scrollbar-gutter'); - this.renderer.setStyle(this.document.querySelector('body'), 'height', 'calc(var(--vh)*100 - var(--nav-offset))'); - this.renderer.setStyle(this.document.querySelector('html'), 'height', 'calc(var(--vh)*100 - var(--nav-offset))'); - this.navbarVisibleSource.next(true); + setTimeout(() => { + const bodyElem = this.document.querySelector('body'); + this.renderer.setStyle(bodyElem, 'margin-top', 'var(--nav-offset)'); + this.renderer.removeStyle(bodyElem, 'scrollbar-gutter'); + this.renderer.setStyle(bodyElem, 'height', 'calc(var(--vh)*100 - var(--nav-offset))'); + this.renderer.setStyle(this.document.querySelector('html'), 'height', 'calc(var(--vh)*100 - var(--nav-offset))'); + this.navbarVisibleSource.next(true); + }, 10); } /** * Hides the top nav bar. */ hideNavBar() { - this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '0px'); - this.renderer.setStyle(this.document.querySelector('body'), 'scrollbar-gutter', 'initial'); - this.renderer.removeStyle(this.document.querySelector('body'), 'height'); - this.renderer.removeStyle(this.document.querySelector('html'), 'height'); - this.navbarVisibleSource.next(false); + setTimeout(() => { + const bodyElem = this.document.querySelector('body'); + this.renderer.removeStyle(bodyElem, 'height'); + this.renderer.removeStyle(this.document.querySelector('html'), 'height'); + this.renderer.setStyle(bodyElem, 'margin-top', '0px', RendererStyleFlags2.Important); + this.renderer.setStyle(bodyElem, 'scrollbar-gutter', 'initial', RendererStyleFlags2.Important); + this.navbarVisibleSource.next(false); + }, 10); } /** @@ -139,8 +144,8 @@ export class NavService { }); } - collapseSideNav(state: boolean) { - this.sideNavCollapseSource.next(state); - localStorage.setItem(this.localStorageSideNavKey, state + ''); + collapseSideNav(isCollapsed: boolean) { + this.sideNavCollapseSource.next(isCollapsed); + localStorage.setItem(this.localStorageSideNavKey, isCollapsed + ''); } } diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index f4da69c83..991a37bdb 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -375,7 +375,7 @@ export class ReaderService { } // Sort the chapters, then grab first if no reading progress - this.readChapter(libraryId, seriesId, [...volume.chapters].sort(this.utilityService.sortChapters)[0]); + this.readChapter(libraryId, seriesId, [...volume.chapters].sort(this.utilityService.sortChapters)[0], incognitoMode); } readChapter(libraryId: number, seriesId: number, chapter: Chapter, incognitoMode: boolean = false) { diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index e10a6744e..c5e46c75b 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -143,8 +143,8 @@ export class SeriesService { } - refreshMetadata(series: Series, force = true) { - return this.httpClient.post(this.baseUrl + 'series/refresh-metadata', {libraryId: series.libraryId, seriesId: series.id, forceUpdate: force}); + refreshMetadata(series: Series, force = true, forceColorscape = true) { + return this.httpClient.post(this.baseUrl + 'series/refresh-metadata', {libraryId: series.libraryId, seriesId: series.id, forceUpdate: force, forceColorscape}); } scan(libraryId: number, seriesId: number, force = false) { diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html index 5c3dcc9b9..bed1498e2 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html @@ -1,5 +1,5 @@ - +
@@ -132,4 +132,5 @@
+
diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.scss b/UI/Web/src/app/_single-module/details-tab/details-tab.component.scss index e69de29bb..8b1378917 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.scss +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.scss @@ -0,0 +1 @@ + diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts index 8af25489c..9f595cd72 100644 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts +++ b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts @@ -44,7 +44,6 @@ import {SettingButtonComponent} from "../../settings/_components/setting-button/ import {CoverImageChooserComponent} from "../../cards/cover-image-chooser/cover-image-chooser.component"; import {EditChapterProgressComponent} from "../../cards/edit-chapter-progress/edit-chapter-progress.component"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {EntityInfoCardsComponent} from "../../cards/entity-info-cards/entity-info-cards.component"; import {CompactNumberPipe} from "../../_pipes/compact-number.pipe"; import {IconAndTitleComponent} from "../../shared/icon-and-title/icon-and-title.component"; import {MangaFormat} from "../../_models/manga-format"; @@ -56,6 +55,7 @@ import {ImageComponent} from "../../shared/image/image.component"; import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; import {ReadTimePipe} from "../../_pipes/read-time.pipe"; import {ChapterService} from "../../_services/chapter.service"; +import {AgeRating} from "../../_models/metadata/age-rating"; enum TabID { General = 'general-tab', @@ -100,7 +100,6 @@ const blackList = [Action.Edit, Action.IncognitoRead, Action.AddToReadingList]; CoverImageChooserComponent, EditChapterProgressComponent, NgbInputDatepicker, - EntityInfoCardsComponent, CompactNumberPipe, IconAndTitleComponent, DefaultDatePipe, @@ -120,7 +119,6 @@ const blackList = [Action.Edit, Action.IncognitoRead, Action.AddToReadingList]; export class EditChapterModalComponent implements OnInit { protected readonly modal = inject(NgbActiveModal); - private readonly seriesService = inject(SeriesService); public readonly utilityService = inject(UtilityService); public readonly imageService = inject(ImageService); private readonly uploadService = inject(UploadService); @@ -183,7 +181,7 @@ export class EditChapterModalComponent implements OnInit { this.editForm.addControl('titleName', new FormControl(this.chapter.titleName, [])); this.editForm.addControl('sortOrder', new FormControl(this.chapter.sortOrder, [Validators.required, Validators.min(0)])); - this.editForm.addControl('summary', new FormControl(this.chapter.summary, [])); + this.editForm.addControl('summary', new FormControl(this.chapter.summary || '', [])); this.editForm.addControl('language', new FormControl(this.chapter.language, [])); this.editForm.addControl('isbn', new FormControl(this.chapter.isbn, [])); this.editForm.addControl('ageRating', new FormControl(this.chapter.ageRating, [])); @@ -251,6 +249,14 @@ export class EditChapterModalComponent implements OnInit { const selectedIndex = this.editForm.get('coverImageIndex')?.value || 0; this.chapter.releaseDate = model.releaseDate; + this.chapter.ageRating = model.ageRating as AgeRating; + this.chapter.genres = model.genres; + this.chapter.tags = model.tags; + this.chapter.sortOrder = model.sortOrder; + this.chapter.language = model.language; + this.chapter.titleName = model.titleName; + this.chapter.summary = model.summary; + this.chapter.isbn = model.isbn; const apis = [ diff --git a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.ts b/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.ts index 92e08cfef..36d1183dd 100644 --- a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.ts +++ b/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.ts @@ -17,7 +17,6 @@ import {EntityTitleComponent} from "../../cards/entity-title/entity-title.compon import {SettingButtonComponent} from "../../settings/_components/setting-button/setting-button.component"; import {CoverImageChooserComponent} from "../../cards/cover-image-chooser/cover-image-chooser.component"; import {EditChapterProgressComponent} from "../../cards/edit-chapter-progress/edit-chapter-progress.component"; -import {EntityInfoCardsComponent} from "../../cards/entity-info-cards/entity-info-cards.component"; import {CompactNumberPipe} from "../../_pipes/compact-number.pipe"; import {IconAndTitleComponent} from "../../shared/icon-and-title/icon-and-title.component"; import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; @@ -83,7 +82,6 @@ const blackList = [Action.Edit, Action.IncognitoRead, Action.AddToReadingList]; CoverImageChooserComponent, EditChapterProgressComponent, NgbInputDatepicker, - EntityInfoCardsComponent, CompactNumberPipe, IconAndTitleComponent, DefaultDatePipe, diff --git a/UI/Web/src/app/_single-module/review-card/review-card.component.html b/UI/Web/src/app/_single-module/review-card/review-card.component.html index 6ff63058d..b30f67901 100644 --- a/UI/Web/src/app/_single-module/review-card/review-card.component.html +++ b/UI/Web/src/app/_single-module/review-card/review-card.component.html @@ -1,15 +1,15 @@
-
+
@if (isMyReview) { - - + + } @else { - + }
-
+
- - {{item.title}} - - - - {{item.name}} - -
- -
- -
- - {{chapter.releaseDate | date:'shortDate' | defaultDate}} - -
-
-
- - -
- - {{chapter.ageRating | ageRating}} - -
-
-
- - -
- - {{t('pages-count', {num: totalPages | compactNumber})}} - -
-
-
- - -
- - {{t('words-count', {num: totalWordCount | compactNumber})}} - -
-
-
- - -
- - {{t('less-than-hour')}} - - {{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} {{readingTime.minHours > 1 ? t('hours') : t('hour')}} - - -
-
- - -
-
- - {{chapter.createdUtc | utcToLocalTime | translocoDate: {dateStyle: 'short', timeStyle: 'short' } | defaultDate}} - -
-
- - -
-
- - {{size | bytes}} - -
-
- - -
-
- - {{entity.id}} - -
- -
-
- - - - - -
-
- - -
-
- - {{chapter.isbn}} - -
-
- - -
-
- - {{chapter.lastReadingProgress | date: 'shortDate'}} - -
-
- - -
-
- - {{chapter.sortOrder}} - -
-
-
-
-
- - diff --git a/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.scss b/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts b/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts deleted file mode 100644 index 4541fa021..000000000 --- a/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - Input, - OnInit, - inject, -} from '@angular/core'; -import { UtilityService } from 'src/app/shared/_services/utility.service'; -import { Chapter } from 'src/app/_models/chapter'; -import { HourEstimateRange } from 'src/app/_models/series-detail/hour-estimate-range'; -import { MangaFormat } from 'src/app/_models/manga-format'; -import { AgeRating } from 'src/app/_models/metadata/age-rating'; -import { Volume } from 'src/app/_models/volume'; -import { SeriesService } from 'src/app/_services/series.service'; -import { ImageService } from 'src/app/_services/image.service'; -import {CommonModule} from "@angular/common"; -import {IconAndTitleComponent} from "../../shared/icon-and-title/icon-and-title.component"; -import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; -import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; -import {BytesPipe} from "../../_pipes/bytes.pipe"; -import {CompactNumberPipe} from "../../_pipes/compact-number.pipe"; -import {AgeRatingPipe} from "../../_pipes/age-rating.pipe"; -import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; -import {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component"; -import {TranslocoModule} from "@jsverse/transloco"; -import {TranslocoLocaleModule} from "@jsverse/transloco-locale"; -import {FilterField} from "../../_models/metadata/v2/filter-field"; -import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; -import {ImageComponent} from "../../shared/image/image.component"; - -@Component({ - selector: 'app-entity-info-cards', - standalone: true, - imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, - AgeRatingPipe, NgbTooltip, MetadataDetailComponent, TranslocoModule, CompactNumberPipe, TranslocoLocaleModule, - UtcToLocalTimePipe, ImageComponent], - templateUrl: './entity-info-cards.component.html', - styleUrls: ['./entity-info-cards.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class EntityInfoCardsComponent implements OnInit { - - protected readonly AgeRating = AgeRating; - protected readonly MangaFormat = MangaFormat; - protected readonly FilterField = FilterField; - - public readonly imageService = inject(ImageService); - - - @Input({required: true}) entity!: Volume | Chapter; - @Input({required: true}) libraryId!: number; - - /** - * Hide more system based fields, like id or Date Added - */ - @Input() showExtendedProperties: boolean = true; - - isChapter = false; - chapter!: Chapter; - - ageRating!: string; - totalPages: number = 0; - totalWordCount: number = 0; - readingTime: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1}; - size: number = 0; - - get WebLinks() { - if (this.chapter.webLinks === '') return []; - return this.chapter.webLinks.split(','); - } - - constructor(private utilityService: UtilityService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {} - - ngOnInit(): void { - this.isChapter = this.utilityService.isChapter(this.entity); - - this.chapter = this.utilityService.isChapter(this.entity) ? (this.entity as Chapter) : (this.entity as Volume).chapters[0]; - - - if (this.isChapter) { - this.size = this.utilityService.asChapter(this.entity).files.reduce((sum, v) => sum + v.bytes, 0); - } else { - this.size = this.utilityService.asVolume(this.entity).chapters.reduce((sum1, chapter) => { - return sum1 + chapter.files.reduce((sum2, file) => { - return sum2 + file.bytes; - }, 0); - }, 0); - } - - - this.totalPages = this.chapter.pages; - if (!this.isChapter) { - this.totalPages = this.utilityService.asVolume(this.entity).pages; - } - - this.totalWordCount = this.chapter.wordCount; - if (!this.isChapter) { - this.totalWordCount = this.utilityService.asVolume(this.entity).chapters.map(c => c.wordCount).reduce((sum, d) => sum + d); - } - - - - if (this.isChapter) { - this.readingTime.minHours = this.chapter.minHoursToRead; - this.readingTime.maxHours = this.chapter.maxHoursToRead; - this.readingTime.avgHours = this.chapter.avgHoursToRead; - } else { - const vol = this.utilityService.asVolume(this.entity); - this.readingTime.minHours = vol.minHoursToRead; - this.readingTime.maxHours = vol.maxHoursToRead; - this.readingTime.avgHours = vol.avgHoursToRead; - } - this.cdRef.markForCheck(); - } -} diff --git a/UI/Web/src/app/cards/entity-title/entity-title.component.html b/UI/Web/src/app/cards/entity-title/entity-title.component.html index 8b13f263d..e0fc0471d 100644 --- a/UI/Web/src/app/cards/entity-title/entity-title.component.html +++ b/UI/Web/src/app/cards/entity-title/entity-title.component.html @@ -2,9 +2,12 @@ @switch (libraryType) { @case (LibraryType.Comic) { @if (titleName !== '' && prioritizeTitleName) { + @if (isChapter && includeChapter) { + {{t('issue-num') + ' ' + number + ' - ' }} + } + {{titleName}} } @else { - {{seriesName.length > 0 ? seriesName + ' - ' : ''}} @if (includeVolume && volumeTitle !== '') { {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}} } @@ -14,9 +17,12 @@ @case (LibraryType.ComicVine) { @if (titleName !== '' && prioritizeTitleName) { + @if (isChapter && includeChapter) { + {{t('issue-num') + ' ' + number + ' - ' }} + } + {{titleName}} } @else { - {{seriesName.length > 0 ? seriesName + ' - ' : ''}} @if (includeVolume && volumeTitle !== '') { {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}} } @@ -26,12 +32,15 @@ @case (LibraryType.Manga) { @if (titleName !== '' && prioritizeTitleName) { + @if (isChapter && includeChapter) { + {{t('chapter') + ' ' + number + ' - ' }} + } {{titleName}} } @else { - {{seriesName.length > 0 ? seriesName + ' - ' : ''}} @if (includeVolume && volumeTitle !== '') { {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}} } + {{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}} } } diff --git a/UI/Web/src/app/cards/entity-title/entity-title.component.ts b/UI/Web/src/app/cards/entity-title/entity-title.component.ts index d70d47cae..1b62fe08d 100644 --- a/UI/Web/src/app/cards/entity-title/entity-title.component.ts +++ b/UI/Web/src/app/cards/entity-title/entity-title.component.ts @@ -28,12 +28,15 @@ export class EntityTitleComponent implements OnInit { * Library type for which the entity belongs */ @Input() libraryType: LibraryType = LibraryType.Manga; - @Input() seriesName: string = ''; @Input({required: true}) entity!: Volume | Chapter; /** * When generating the title, should this prepend 'Volume number' before the Chapter wording */ @Input() includeVolume: boolean = false; + /** + * When generating the title, should this prepend 'Chapter number' before the Chapter titlename + */ + @Input() includeChapter: boolean = false; /** * When a titleName (aka a title) is available on the entity, show it over Volume X Chapter Y */ diff --git a/UI/Web/src/app/cards/external-list-item/external-list-item.component.html b/UI/Web/src/app/cards/external-list-item/external-list-item.component.html deleted file mode 100644 index d464fd6be..000000000 --- a/UI/Web/src/app/cards/external-list-item/external-list-item.component.html +++ /dev/null @@ -1,17 +0,0 @@ -
-
- -
-
-
-
- -
- @if (summary && summary.length > 0) { -
- -
- } -
-
-
diff --git a/UI/Web/src/app/cards/external-list-item/external-list-item.component.scss b/UI/Web/src/app/cards/external-list-item/external-list-item.component.scss deleted file mode 100644 index 63c920be4..000000000 --- a/UI/Web/src/app/cards/external-list-item/external-list-item.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -.list-item-container { - background: var(--card-list-item-bg-color); - border-radius: 5px; - position: relative; -} - diff --git a/UI/Web/src/app/cards/external-list-item/external-list-item.component.ts b/UI/Web/src/app/cards/external-list-item/external-list-item.component.ts deleted file mode 100644 index a553d0201..000000000 --- a/UI/Web/src/app/cards/external-list-item/external-list-item.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {ImageComponent} from "../../shared/image/image.component"; -import {NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; -import {ReadMoreComponent} from "../../shared/read-more/read-more.component"; - -@Component({ - selector: 'app-external-list-item', - standalone: true, - imports: [CommonModule, ImageComponent, NgbProgressbar, NgbTooltip, ReadMoreComponent], - templateUrl: './external-list-item.component.html', - styleUrls: ['./external-list-item.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class ExternalListItemComponent { - - /** - * Image to show - */ - @Input() imageUrl: string = ''; - - /** - * Size of the Image Height. Defaults to 232.91px. - */ - @Input() imageHeight: string = '232.91px'; - /** - * Size of the Image Width Defaults to 160px. - */ - @Input() imageWidth: string = '160px'; - @Input() summary: string | null = ''; -} diff --git a/UI/Web/src/app/cards/list-item/list-item.component.html b/UI/Web/src/app/cards/list-item/list-item.component.html deleted file mode 100644 index 6cf7c201e..000000000 --- a/UI/Web/src/app/cards/list-item/list-item.component.html +++ /dev/null @@ -1,40 +0,0 @@ - -
-
- -
- - - -
-

-
-
-
-
-
- - - -
- -
{{Title}}
- -
- -
-
-
- -
-
-
-
- -
diff --git a/UI/Web/src/app/cards/list-item/list-item.component.scss b/UI/Web/src/app/cards/list-item/list-item.component.scss deleted file mode 100644 index 62bf84b0d..000000000 --- a/UI/Web/src/app/cards/list-item/list-item.component.scss +++ /dev/null @@ -1,40 +0,0 @@ -// with summary and cards, we have a height of 220px, we might want to default to 220px and let it grow from there to help with virtualization - -.download { - width: 80px; - height: 80px; - position: absolute; - top: 55px; - left: 20px; -} - -.progress-banner { - height: 5px; - - .progress { - color: var(--card-progress-bar-color); - background-color: transparent; - } -} - -.list-item-container { - background: var(--card-list-item-bg-color); - border-radius: 5px; - position: relative; -} - -.not-read-badge { - position: absolute; - top: 8px; - left: 110px; - width: 0; - height: 0; - border-style: solid; - border-width: 0 var(--card-progress-triangle-size) var(--card-progress-triangle-size) 0; - border-color: transparent var(--primary-color) transparent transparent; -} - -.subtitle-with-actionables { - font-size: 0.75rem; - word-break: break-all; - } diff --git a/UI/Web/src/app/cards/list-item/list-item.component.ts b/UI/Web/src/app/cards/list-item/list-item.component.ts deleted file mode 100644 index 5f85735fc..000000000 --- a/UI/Web/src/app/cards/list-item/list-item.component.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, DestroyRef, - EventEmitter, - inject, - Input, - OnInit, - Output -} from '@angular/core'; -import { ToastrService } from 'ngx-toastr'; -import { map, Observable } from 'rxjs'; -import { Download } from 'src/app/shared/_models/download'; -import { DownloadEvent, DownloadService } from 'src/app/shared/_services/download.service'; -import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service'; -import { Chapter } from 'src/app/_models/chapter'; -import { LibraryType } from 'src/app/_models/library/library'; -import { RelationKind } from 'src/app/_models/series-detail/relation-kind'; -import { Volume } from 'src/app/_models/volume'; -import { Action, ActionItem } from 'src/app/_services/action-factory.service'; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {ReadMoreComponent} from "../../shared/read-more/read-more.component"; -import {CommonModule} from "@angular/common"; -import {ImageComponent} from "../../shared/image/image.component"; -import {DownloadIndicatorComponent} from "../download-indicator/download-indicator.component"; -import {EntityInfoCardsComponent} from "../entity-info-cards/entity-info-cards.component"; -import {NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; -import {translate, TranslocoDirective} from "@jsverse/transloco"; -import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component"; - -@Component({ - selector: 'app-list-item', - standalone: true, - imports: [CommonModule, ReadMoreComponent, ImageComponent, DownloadIndicatorComponent, EntityInfoCardsComponent, CardActionablesComponent, NgbProgressbar, NgbTooltip, TranslocoDirective], - templateUrl: './list-item.component.html', - styleUrls: ['./list-item.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class ListItemComponent implements OnInit { - - /** - * Volume or Chapter to render - */ - @Input({required: true}) entity!: Volume | Chapter; - @Input({required: true}) libraryId!: number; - /** - * Image to show - */ - @Input() imageUrl: string = ''; - /** - * Actions to show - */ - @Input() actions: ActionItem[] = []; // Volume | Chapter - /** - * Library type to help with formatting title - */ - @Input() libraryType: LibraryType = LibraryType.Manga; - /** - * Name of the Series to show under the title - */ - @Input() seriesName: string = ''; - - /** - * Size of the Image Height. Defaults to 232.91px. - */ - @Input() imageHeight: string = '232.91px'; - /** - * Size of the Image Width Defaults to 160px. - */ - @Input() imageWidth: string = '160px'; - @Input() seriesLink: string = ''; - - @Input() pagesRead: number = 0; - @Input() totalPages: number = 0; - - @Input() relation: RelationKind | undefined = undefined; - - /** - * When generating the title, should this prepend 'Volume number' before the Chapter wording - */ - @Input() includeVolume: boolean = false; - /** - * Show's the title if available on entity - */ - @Input() showTitle: boolean = true; - /** - * Blur the summary for the list item - */ - @Input() blur: boolean = false; - - @Output() read: EventEmitter = new EventEmitter(); - private readonly destroyRef = inject(DestroyRef); - - actionInProgress: boolean = false; - summary: string = ''; - isChapter: boolean = false; - - - download$: Observable | null = null; - downloadInProgress: boolean = false; - - get Title() { - if (this.isChapter) return (this.entity as Chapter).titleName; - return ''; - } - - get ShowExtended() { - return this.utilityService.getActiveBreakpoint() === Breakpoint.Desktop; - } - - protected readonly Breakpoint = Breakpoint; - - - constructor(public utilityService: UtilityService, private downloadService: DownloadService, - private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { } - - ngOnInit(): void { - this.isChapter = this.utilityService.isChapter(this.entity); - if (this.isChapter) { - this.summary = this.utilityService.asChapter(this.entity).summary || ''; - } else { - this.summary = this.utilityService.asVolume(this.entity).chapters[0].summary || ''; - } - - this.cdRef.markForCheck(); - - - this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => { - if(this.utilityService.isVolume(this.entity)) return events.find(e => e.entityType === 'volume' && e.subTitle === this.downloadService.downloadSubtitle('volume', (this.entity as Volume))) || null; - if(this.utilityService.isChapter(this.entity)) return events.find(e => e.entityType === 'chapter' && e.subTitle === this.downloadService.downloadSubtitle('chapter', (this.entity as Chapter))) || null; - return null; - })); - } - - performAction(action: ActionItem) { - if (action.action == Action.Download) { - if (this.downloadInProgress) { - this.toastr.info(translate('toasts.download-in-progress')); - return; - } - - const statusUpdate = (d: Download | undefined) => { - if (d) return; - this.downloadInProgress = false; - }; - - if (this.utilityService.isVolume(this.entity)) { - const volume = this.utilityService.asVolume(this.entity); - this.downloadService.download('volume', volume, statusUpdate); - } else if (this.utilityService.isChapter(this.entity)) { - const chapter = this.utilityService.asChapter(this.entity); - this.downloadService.download('chapter', chapter, statusUpdate); - } - return; // Don't propagate the download from a card - } - - if (typeof action.callback === 'function') { - action.callback(action, this.entity); - } - } -} diff --git a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html index 9a5519450..eb99e4424 100644 --- a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html +++ b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html @@ -6,21 +6,22 @@
- -
-
+ @if (entity.title | safeHtml; as info) { + @if (info !== '') { +
+
-
Upcoming
- +
Upcoming
+ +
-
- + } + } +
+ + {{title}} + +
-
- - {{title}} -
- -
\ No newline at end of file diff --git a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts index 7dca289e8..ea2e8c729 100644 --- a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts +++ b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts @@ -1,5 +1,4 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; -import {CommonModule} from '@angular/common'; import {ImageComponent} from "../../shared/image/image.component"; import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; @@ -9,7 +8,7 @@ import {translate} from "@jsverse/transloco"; @Component({ selector: 'app-next-expected-card', standalone: true, - imports: [CommonModule, ImageComponent, SafeHtmlPipe], + imports: [ImageComponent, SafeHtmlPipe], templateUrl: './next-expected-card.component.html', styleUrl: './next-expected-card.component.scss', changeDetection: ChangeDetectionStrategy.OnPush diff --git a/UI/Web/src/app/cards/series-card/series-card.component.html b/UI/Web/src/app/cards/series-card/series-card.component.html index 4d67a6894..b557f8e3b 100644 --- a/UI/Web/src/app/cards/series-card/series-card.component.html +++ b/UI/Web/src/app/cards/series-card/series-card.component.html @@ -62,8 +62,9 @@
- - + + + {{series.name}} @@ -71,7 +72,7 @@ @if (actions && actions.length > 0) { - + }
diff --git a/UI/Web/src/app/cards/series-card/series-card.component.ts b/UI/Web/src/app/cards/series-card/series-card.component.ts index f451b7919..c2d2da063 100644 --- a/UI/Web/src/app/cards/series-card/series-card.component.ts +++ b/UI/Web/src/app/cards/series-card/series-card.component.ts @@ -39,6 +39,7 @@ import {BulkSelectionService} from "../bulk-selection.service"; import {User} from "../../_models/user"; import {ScrollService} from "../../_services/scroll.service"; import {ReaderService} from "../../_services/reader.service"; +import {SeriesFormatComponent} from "../../shared/series-format/series-format.component"; function deepClone(obj: any): any { if (obj === null || typeof obj !== 'object') { @@ -67,7 +68,7 @@ function deepClone(obj: any): any { @Component({ selector: 'app-series-card', standalone: true, - imports: [CommonModule, CardItemComponent, RelationshipPipe, CardActionablesComponent, DefaultValuePipe, DownloadIndicatorComponent, EntityTitleComponent, FormsModule, ImageComponent, NgbProgressbar, NgbTooltip, RouterLink, TranslocoDirective], + imports: [CommonModule, CardItemComponent, RelationshipPipe, CardActionablesComponent, DefaultValuePipe, DownloadIndicatorComponent, EntityTitleComponent, FormsModule, ImageComponent, NgbProgressbar, NgbTooltip, RouterLink, TranslocoDirective, SeriesFormatComponent], templateUrl: './series-card.component.html', styleUrls: ['./series-card.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -284,7 +285,7 @@ export class SeriesCardComponent implements OnInit, OnChanges { } async refreshMetadata(series: Series, forceUpdate = false) { - await this.actionService.refreshSeriesMetadata(series, undefined, forceUpdate); + await this.actionService.refreshSeriesMetadata(series, undefined, forceUpdate, forceUpdate); } async scanLibrary(series: Series) { diff --git a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html deleted file mode 100644 index 35d4137a6..000000000 --- a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html +++ /dev/null @@ -1,126 +0,0 @@ - -
- -
- - {{seriesMetadata.releaseYear}} - -
-
-
- - - -
- - {{this.seriesMetadata.ageRating | ageRating}} - -
-
-
- - -
- - {{seriesMetadata.language | defaultValue:'en' | languageName | async}} - -
-
-
-
- - -
- - - {{pubStatus}} - - -
-
-
- - -
- - - {{ isScrobbling ? t('on') : t('off') }} - - - {{t('disabled')}} - - - -
-
-
- - - -
- - {{series.format | mangaFormat}} - -
-
-
- - -
- - {{series.latestReadDate | timeAgo}} - -
-
-
- - - -
- - {{t('words-count', {num: series.wordCount | compactNumber})}} - -
-
-
- -
- -
- - {{t('pages-count', {num: series.pages | compactNumber})}} - -
-
-
- - -
- - {{t('less-than-hour')}} - - {{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} {{readingTime.minHours > 1 ? t('hours') : t('hour')}} - - -
-
- - -
-
- - {{readingTimeLeft | readTimeLeft}} - -
-
-
-
- -
diff --git a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.scss b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts deleted file mode 100644 index c4e7931c8..000000000 --- a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - DestroyRef, - EventEmitter, - inject, - Input, - OnChanges, - OnInit, - Output -} from '@angular/core'; -import {debounceTime, filter, map} from 'rxjs'; -import {UtilityService} from 'src/app/shared/_services/utility.service'; -import {UserProgressUpdateEvent} from 'src/app/_models/events/user-progress-update-event'; -import {HourEstimateRange} from 'src/app/_models/series-detail/hour-estimate-range'; -import {MangaFormat} from 'src/app/_models/manga-format'; -import {Series} from 'src/app/_models/series'; -import {SeriesMetadata} from 'src/app/_models/metadata/series-metadata'; -import {AccountService} from 'src/app/_services/account.service'; -import {EVENTS, MessageHubService} from 'src/app/_services/message-hub.service'; -import {ReaderService} from 'src/app/_services/reader.service'; -import {FilterField} from "../../_models/metadata/v2/filter-field"; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {ScrobblingService} from "../../_services/scrobbling.service"; -import {CommonModule} from "@angular/common"; -import {IconAndTitleComponent} from "../../shared/icon-and-title/icon-and-title.component"; -import {AgeRatingPipe} from "../../_pipes/age-rating.pipe"; -import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; -import {LanguageNamePipe} from "../../_pipes/language-name.pipe"; -import {PublicationStatusPipe} from "../../_pipes/publication-status.pipe"; -import {MangaFormatPipe} from "../../_pipes/manga-format.pipe"; -import {TimeAgoPipe} from "../../_pipes/time-ago.pipe"; -import {CompactNumberPipe} from "../../_pipes/compact-number.pipe"; -import {MangaFormatIconPipe} from "../../_pipes/manga-format-icon.pipe"; -import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {ReadTimeLeftPipe} from "../../_pipes/read-time-left.pipe"; - -@Component({ - selector: 'app-series-info-cards', - standalone: true, - imports: [CommonModule, IconAndTitleComponent, AgeRatingPipe, DefaultValuePipe, LanguageNamePipe, PublicationStatusPipe, MangaFormatPipe, TimeAgoPipe, CompactNumberPipe, MangaFormatIconPipe, NgbTooltip, TranslocoDirective, ReadTimeLeftPipe], - templateUrl: './series-info-cards.component.html', - styleUrls: ['./series-info-cards.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class SeriesInfoCardsComponent implements OnInit, OnChanges { - - private readonly destroyRef = inject(DestroyRef); - public readonly utilityService = inject(UtilityService); - private readonly readerService = inject(ReaderService); - private readonly cdRef = inject(ChangeDetectorRef); - private readonly messageHub = inject(MessageHubService); - public readonly accountService = inject(AccountService); - private readonly scrobbleService = inject(ScrobblingService); - - @Input({required: true}) series!: Series; - @Input({required: true}) seriesMetadata!: SeriesMetadata; - @Input() hasReadingProgress: boolean = false; - @Input() readingTimeLeft: HourEstimateRange | undefined; - /** - * If this should make an API call to request readingTimeLeft - */ - @Input() showReadingTimeLeft: boolean = true; - @Output() goTo: EventEmitter<{queryParamName: FilterField, filter: any}> = new EventEmitter(); - - readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0}; - isScrobbling: boolean = true; - libraryAllowsScrobbling: boolean = true; - - - protected readonly MangaFormat = MangaFormat; - protected readonly FilterField = FilterField; - - - constructor() { - // Listen for progress events and re-calculate getTimeLeft - this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate), - map(evt => evt.payload as UserProgressUpdateEvent), - debounceTime(500), - takeUntilDestroyed(this.destroyRef)) - .subscribe(updateEvent => { - this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => { - if (user === undefined || user.username !== updateEvent.username) return; - if (updateEvent.seriesId !== this.series.id) return; - this.getReadingTimeLeft(); - }); - }); - } - - ngOnInit(): void { - if (this.series !== null) { - this.getReadingTimeLeft(); - this.readingTime.minHours = this.series.minHoursToRead; - this.readingTime.maxHours = this.series.maxHoursToRead; - this.readingTime.avgHours = this.series.avgHoursToRead; - this.scrobbleService.hasHold(this.series.id).subscribe(res => { - this.isScrobbling = !res; - this.cdRef.markForCheck(); - }); - - this.scrobbleService.libraryAllowsScrobbling(this.series.id).subscribe(res => { - this.libraryAllowsScrobbling = res; - this.cdRef.markForCheck(); - }); - - this.cdRef.markForCheck(); - } - } - - ngOnChanges() { - this.cdRef.markForCheck(); - } - - - handleGoTo(queryParamName: FilterField, filter: any) { - // Ignore the default case added as this query combo would never be valid - if (filter + '' === '' && queryParamName === FilterField.SeriesName) return; - this.goTo.emit({queryParamName, filter}); - } - - private getReadingTimeLeft() { - if (this.showReadingTimeLeft) this.readerService.getTimeLeft(this.series.id).subscribe((timeLeft) => { - this.readingTimeLeft = timeLeft; - this.cdRef.markForCheck(); - }); - } - - toggleScrobbling(evt: any) { - evt.stopPropagation(); - if (this.isScrobbling) { - this.scrobbleService.addHold(this.series.id).subscribe(() => { - this.isScrobbling = !this.isScrobbling; - this.cdRef.markForCheck(); - }); - } else { - this.scrobbleService.removeHold(this.series.id).subscribe(() => { - this.isScrobbling = !this.isScrobbling; - this.cdRef.markForCheck(); - }); - } - } -} diff --git a/UI/Web/src/app/carousel/_components/carousel-tab/carousel-tab.component.html b/UI/Web/src/app/carousel/_components/carousel-tab/carousel-tab.component.html deleted file mode 100644 index 6dbc74306..000000000 --- a/UI/Web/src/app/carousel/_components/carousel-tab/carousel-tab.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/UI/Web/src/app/carousel/_components/carousel-tab/carousel-tab.component.scss b/UI/Web/src/app/carousel/_components/carousel-tab/carousel-tab.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/UI/Web/src/app/carousel/_components/carousel-tab/carousel-tab.component.ts b/UI/Web/src/app/carousel/_components/carousel-tab/carousel-tab.component.ts deleted file mode 100644 index e21d5f17f..000000000 --- a/UI/Web/src/app/carousel/_components/carousel-tab/carousel-tab.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {ChangeDetectionStrategy, Component, ContentChild, Input, TemplateRef} from '@angular/core'; -import {TabId} from "../carousel-tabs/carousel-tabs.component"; - -@Component({ - selector: 'app-carousel-tab', - standalone: true, - imports: [], - templateUrl: './carousel-tab.component.html', - styleUrl: './carousel-tab.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class CarouselTabComponent { - - @Input({required: true}) id!: TabId; - @ContentChild(TemplateRef, {static: true}) implicitContent!: TemplateRef; - -} diff --git a/UI/Web/src/app/carousel/_components/carousel-tabs/carousel-tabs.component.html b/UI/Web/src/app/carousel/_components/carousel-tabs/carousel-tabs.component.html deleted file mode 100644 index ae3e2de49..000000000 --- a/UI/Web/src/app/carousel/_components/carousel-tabs/carousel-tabs.component.html +++ /dev/null @@ -1,27 +0,0 @@ - - - -
- -
diff --git a/UI/Web/src/app/carousel/_components/carousel-tabs/carousel-tabs.component.scss b/UI/Web/src/app/carousel/_components/carousel-tabs/carousel-tabs.component.scss deleted file mode 100644 index aedfe8efc..000000000 --- a/UI/Web/src/app/carousel/_components/carousel-tabs/carousel-tabs.component.scss +++ /dev/null @@ -1,45 +0,0 @@ -.carousel-tabs-wrapper { - position: relative; - display: flex; - align-items: center; -} -.carousel-tabs-container { - overflow-x: auto; - white-space: nowrap; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; - scrollbar-width: none; - flex-grow: 1; -} -.carousel-tabs-container::-webkit-scrollbar { - display: none; -} -.nav-tabs { - flex-wrap: nowrap; -} -.scroll-button { - position: absolute; - top: 50%; - transform: translateY(-50%); - background-color: rgba(255, 255, 255, 0.7); - border: none; - border-radius: 50%; - width: 30px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - opacity: 0; - transition: opacity 0.3s ease; - z-index: 1; -} -.scroll-button.left { - left: 0; -} -.scroll-button.right { - right: 0; -} -.scroll-button.visible { - opacity: 1; -} diff --git a/UI/Web/src/app/carousel/_components/carousel-tabs/carousel-tabs.component.ts b/UI/Web/src/app/carousel/_components/carousel-tabs/carousel-tabs.component.ts deleted file mode 100644 index 8c9d5dafb..000000000 --- a/UI/Web/src/app/carousel/_components/carousel-tabs/carousel-tabs.component.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ContentChild, - ContentChildren, ElementRef, EventEmitter, HostListener, - inject, Input, OnInit, Output, QueryList, - TemplateRef, ViewChild -} from '@angular/core'; -import { - NgbNav, - NgbNavChangeEvent, - NgbNavContent, - NgbNavItem, - NgbNavLink, - NgbNavOutlet -} from "@ng-bootstrap/ng-bootstrap"; -import {CarouselTabComponent} from "../carousel-tab/carousel-tab.component"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {NgTemplateOutlet} from "@angular/common"; - -/** - * Any Tabs that use this Carousel should use these - */ -export enum TabId { - Related = 'related-tab', - Reviews = 'review-tab', // Only applicable for books - Details = 'details-tab', - Chapters = 'chapters-tab', -} - -@Component({ - selector: 'app-carousel-tabs', - standalone: true, - imports: [ - NgbNav, - TranslocoDirective, - NgbNavItem, - NgbNavLink, - NgTemplateOutlet, - NgbNavOutlet, - NgbNavContent - ], - templateUrl: './carousel-tabs.component.html', - styleUrl: './carousel-tabs.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class CarouselTabsComponent implements OnInit, AfterViewInit { - private readonly cdRef = inject(ChangeDetectorRef); - - @ContentChildren(CarouselTabComponent) tabComponents!: QueryList; - - @Input({required: true}) activeTabId!: TabId; - @Output() activeTabIdChange = new EventEmitter(); - @Output() navChange = new EventEmitter(); - - @ViewChild('scrollContainer') scrollContainer: ElementRef | undefined; - - tabs: { id: TabId; contentTemplate: any }[] = []; - showLeftArrow = false; - showRightArrow = false; - - - ngOnInit() { - this.checkOverflow(); - } - - ngAfterViewInit() { - this.initializeTabs(); - this.scrollToActiveTab(); - this.checkOverflow(); - } - - initializeTabs() { - this.tabs = this.tabComponents.map(tabComponent => ({ - id: tabComponent.id, - contentTemplate: tabComponent.implicitContent - })); - this.cdRef.markForCheck(); - } - - @HostListener('window:resize') - onResize() { - this.checkOverflow(); - } - - onNavChange(event: NgbNavChangeEvent) { - this.activeTabIdChange.emit(event.nextId); - this.navChange.emit(event); - this.scrollToActiveTab(); - } - - onScroll() { - this.checkOverflow(); - } - - scrollToActiveTab() { - setTimeout(() => { - const activeTab = this.scrollContainer?.nativeElement.querySelector('.active'); - if (activeTab) { - activeTab.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); - } - this.checkOverflow(); - }); - } - - checkOverflow() { - const element = this.scrollContainer?.nativeElement; - if (!element) return; - this.showLeftArrow = element.scrollLeft > 0; - this.showRightArrow = element.scrollLeft < element.scrollWidth - element.clientWidth; - this.cdRef.markForCheck(); - } - - scroll(direction: 'left' | 'right') { - const element = this.scrollContainer?.nativeElement; - if (!element) return; - const scrollAmount = element.clientWidth / 2; - if (direction === 'left') { - element.scrollBy({ left: -scrollAmount, behavior: 'smooth' }); - } else { - element.scrollBy({ left: scrollAmount, behavior: 'smooth' }); - } - } - -} diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.html b/UI/Web/src/app/chapter-detail/chapter-detail.component.html index 078c9a110..4936078e5 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.html +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.html @@ -4,11 +4,16 @@ @if (chapter && series && libraryType !== null) {
-
+
- - @if (chapter.pagesRead < chapter.pages && chapter.pagesRead > 0) { -
+ @if(mobileSeriesImgBackground === 'true') { + + } @else { + + } + + @if (chapter.pagesRead < chapter.pages && chapter.pagesRead > 0) { +
} @@ -25,25 +30,22 @@
-
+

{{series.name}}

- - + + - @if (chapter.titleName) { - - {{chapter.titleName}} - }
+ [libraryType]="libraryType" + [mangaFormat]="series.format"> @@ -92,6 +94,12 @@
} +
+
+ +
+
+
@@ -108,7 +116,7 @@
{{t('writers-title')}}
- + {{item.name}} @@ -116,11 +124,50 @@
- {{t('cover-artists-title')}} + @if (chapter.releaseDate !== '0001-01-01T00:00:00' && (libraryType === LibraryType.ComicVine || libraryType === LibraryType.Comic)) { + {{t('release-date-title')}} + + } @else { + {{t('cover-artists-title')}} +
+ + + {{item.name}} + + +
+ } +
+
+
+ +
+
+
+ {{t('genres-title')}} +
+ +
+ {{t('tags-title')}} + @@ -131,16 +178,8 @@
- - - - - - - - - -
-

- {{(readingList.startingMonth +'/01/2020')| date:'MMM'}} - , - {{readingList.startingYear}} - — - - {{(readingList.endingMonth +'/01/2020') | date:'MMM'}} - , - {{readingList.endingYear}} - -

-
+ @if (readingList.startingYear !== 0) { +
+

+ @if (readingList.startingMonth > 0) { + {{(readingList.startingMonth +'/01/2020')| date:'MMM'}} + } + @if (readingList.startingMonth > 0 && readingList.startingYear > 0) { + , + } + @if (readingList.startingYear > 0) { + {{readingList.startingYear}} + } + — + @if (readingList.endingYear > 0) { + @if (readingList.endingMonth > 0) { + {{(readingList.endingMonth +'/01/2020')| date:'MMM'}} + } + @if (readingList.endingMonth > 0 && readingList.endingYear > 0) { + , + } + @if (readingList.endingYear > 0) { + {{readingList.endingYear}} + } + } +

+
+ } + +
+ + @if (characters$ | async; as characters) { + @if (characters && characters.length > 0) { +
+
+
{{t('characters-title')}}
+ + + {{item.name}} + + +
+
+ } + }
- -
-
-
{{t('characters-title')}}
- - - {{item.name}} - - - -
-
-
-
- + @if (items.length === 0 && !isLoading) {
{{t('no-data')}}
-
- - - + } @else if(isLoading) { + + } diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.scss b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.scss index 1ce6ed148..75c495a3c 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.scss +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.scss @@ -22,7 +22,6 @@ .scroll-container { display: flex; flex-direction: row; - width: 100%; height: calc((var(--vh) *100) - 173px); margin-bottom: 10px; diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts index 78e7b0435..9f70c77b9 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts @@ -46,7 +46,7 @@ import {Title} from "@angular/platform-browser"; styleUrls: ['./reading-list-detail.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [SideNavCompanionBarComponent, NgIf, CardActionablesComponent, ImageComponent, NgbDropdown, + imports: [SideNavCompanionBarComponent, CardActionablesComponent, ImageComponent, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, ReadMoreComponent, BadgeExpanderComponent, PersonBadgeComponent, A11yClickDirective, LoadingComponent, DraggableOrderedListComponent, ReadingListItemComponent, NgClass, AsyncPipe, DecimalPipe, DatePipe, TranslocoDirective, diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html index f8a7c20a1..35984b63f 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html @@ -1,6 +1,6 @@
-
+
@if (item.pagesRead === 0 && item.pagesTotal > 0) {
@@ -16,18 +16,18 @@
{{item.title}} -
+
diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.scss b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.scss index 6a9691b3d..51810e4cd 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.scss +++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.scss @@ -34,3 +34,44 @@ $image-height: 125px; border-width: 0 var(--card-progress-triangle-size) var(--card-progress-triangle-size) 0; border-color: transparent var(--primary-color) transparent transparent; } + +::ng-deep .read-more-cont div { + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; + display:-webkit-box; + ; +} + +@media (max-width: 576px) { + ::ng-deep .read-more-cont div { + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; + display:-webkit-box; + ; + } +} + +@media (max-width: 800px) { + ::ng-deep .read-more-cont div { + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + display:-webkit-box; + ; + } +} + +@media (max-width: 768px) { + .actions { + display:flex; + flex-direction: column-reverse; + margin-left: 10px; + + .btn-primary { + margin-bottom: 10px; + margin-left: 0 !important; + } + } +} \ No newline at end of file diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html index 008be179d..00a6ceed5 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html @@ -1,8 +1,46 @@
-
- + + + @if (utilityService.activeBreakpoint$ | async; as activeBreakpoint) { + @if (activeBreakpoint <= Breakpoint.Tablet) { +
+ +
+ } @else { +
+ +
+ } + } + + @for (rating of ratings; track rating.provider + rating.averageScore) { +
+ + + {{rating.averageScore}}% + +
+ } + +
+ +
+ +
+ @for(link of webLinks; track link) { + + + + } +
+
+ + + @if (hasUserRated) { {{userRating * 20}} @@ -16,45 +54,23 @@ @if (hasUserRated || overallRating > 0) { % } - -
- - @for (rating of ratings; track rating.provider + rating.averageScore) { -
- - - {{rating.averageScore}}% - -
- } - -
- -
- -
- @for(link of webLinks; track link) { - - - - } -
-
+ + - {{userRating * 20}}% -
{{rating.favoriteCount}}
+
+ {{rating.favoriteCount}} +
+ @if (rating.providerUrl) { {{t('entry-label')}} } -
diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts index 86880f77c..e01a40a81 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts @@ -7,11 +7,10 @@ import { OnInit, ViewEncapsulation } from '@angular/core'; -import {CommonModule, NgOptimizedImage} from '@angular/common'; import {SeriesService} from "../../../_services/series.service"; import {Rating} from "../../../_models/rating"; import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe"; -import {NgbPopover, NgbRating} from "@ng-bootstrap/ng-bootstrap"; +import {NgbModal, NgbPopover, NgbRating} from "@ng-bootstrap/ng-bootstrap"; import {LoadingComponent} from "../../../shared/loading/loading.component"; import {LibraryType} from "../../../_models/library/library"; import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe"; @@ -22,11 +21,14 @@ import {ImageComponent} from "../../../shared/image/image.component"; import {TranslocoDirective} from "@jsverse/transloco"; import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe"; import {ImageService} from "../../../_services/image.service"; +import {AsyncPipe, NgOptimizedImage, NgTemplateOutlet} from "@angular/common"; +import {InviteUserComponent} from "../../../admin/invite-user/invite-user.component"; +import {RatingModalComponent} from "../rating-modal/rating-modal.component"; @Component({ selector: 'app-external-rating', standalone: true, - imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe, NgxStarsModule, ImageComponent, TranslocoDirective, SafeHtmlPipe], + imports: [ProviderImagePipe, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe, NgxStarsModule, ImageComponent, TranslocoDirective, SafeHtmlPipe, NgOptimizedImage, AsyncPipe, NgTemplateOutlet], templateUrl: './external-rating.component.html', styleUrls: ['./external-rating.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, @@ -40,6 +42,7 @@ export class ExternalRatingComponent implements OnInit { public readonly utilityService = inject(UtilityService); public readonly destroyRef = inject(DestroyRef); public readonly imageService = inject(ImageService); + public readonly modalService = inject(NgbModal); protected readonly Breakpoint = Breakpoint; @@ -65,4 +68,17 @@ export class ExternalRatingComponent implements OnInit { this.cdRef.markForCheck(); }); } + + openRatingModal() { + const modalRef = this.modalService.open(RatingModalComponent, {size: 'xl'}); + modalRef.componentInstance.userRating = this.userRating; + modalRef.componentInstance.seriesId = this.seriesId; + modalRef.componentInstance.hasUserRated = this.hasUserRated; + + modalRef.closed.subscribe((updated: {hasUserRated: boolean, userRating: number}) => { + this.userRating = updated.userRating; + this.hasUserRated = this.hasUserRated || updated.hasUserRated; + this.cdRef.markForCheck(); + }); + } } diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html index 71c36a4df..2d862abc3 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html +++ b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html @@ -11,6 +11,10 @@ + + + + @if (libraryType === LibraryType.Book || libraryType === LibraryType.LightNovel) { {{t('words-count', {num: readingTimeEntity.wordCount | compactNumber})}} } @else { diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts index af3843165..56c3bface 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts +++ b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts @@ -15,6 +15,10 @@ import {ImageService} from "../../../_services/image.service"; import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service"; import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; import {FilterField} from "../../../_models/metadata/v2/filter-field"; +import {MangaFormatPipe} from "../../../_pipes/manga-format.pipe"; +import {MangaFormat} from "../../../_models/manga-format"; +import {MangaFormatIconPipe} from "../../../_pipes/manga-format-icon.pipe"; +import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component"; @Component({ selector: 'app-metadata-detail-row', @@ -26,7 +30,10 @@ import {FilterField} from "../../../_models/metadata/v2/filter-field"; ReadTimePipe, NgbTooltip, TranslocoDirective, - ImageComponent + ImageComponent, + MangaFormatPipe, + MangaFormatIconPipe, + SeriesFormatComponent ], templateUrl: './metadata-detail-row.component.html', styleUrl: './metadata-detail-row.component.scss', @@ -44,6 +51,7 @@ export class MetadataDetailRowComponent { @Input() readingTimeLeft: HourEstimateRange | null = null; @Input({required: true}) ageRating: AgeRating = AgeRating.Unknown; @Input({required: true}) libraryType!: LibraryType; + @Input({required: true}) mangaFormat!: MangaFormat; openGeneric(queryParamName: FilterField, filter: string | number) { if (queryParamName === FilterField.None) return; 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 c1eb262b6..647e58aad 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 @@ -4,7 +4,7 @@
{{heading}}
- + @if(itemTemplate) { diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts index e4d25d177..90d0e2afa 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts @@ -28,6 +28,7 @@ export class MetadataDetailComponent { @Input({required: true}) libraryId!: number; @Input({required: true}) heading!: string; @Input() queryParam: FilterField = FilterField.None; + @Input() includeComma: boolean = true; @ContentChild('titleTemplate') titleTemplate!: TemplateRef; @ContentChild('itemTemplate') itemTemplate?: TemplateRef; diff --git a/UI/Web/src/app/series-detail/_components/rating-modal/rating-modal.component.html b/UI/Web/src/app/series-detail/_components/rating-modal/rating-modal.component.html new file mode 100644 index 000000000..77260688a --- /dev/null +++ b/UI/Web/src/app/series-detail/_components/rating-modal/rating-modal.component.html @@ -0,0 +1,16 @@ + + + + + diff --git a/UI/Web/src/app/cards/chapter-metadata-detail/chapter-metadata-detail.component.scss b/UI/Web/src/app/series-detail/_components/rating-modal/rating-modal.component.scss similarity index 100% rename from UI/Web/src/app/cards/chapter-metadata-detail/chapter-metadata-detail.component.scss rename to UI/Web/src/app/series-detail/_components/rating-modal/rating-modal.component.scss diff --git a/UI/Web/src/app/series-detail/_components/rating-modal/rating-modal.component.ts b/UI/Web/src/app/series-detail/_components/rating-modal/rating-modal.component.ts new file mode 100644 index 000000000..f9c343475 --- /dev/null +++ b/UI/Web/src/app/series-detail/_components/rating-modal/rating-modal.component.ts @@ -0,0 +1,46 @@ +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input} from '@angular/core'; +import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; +import {TranslocoDirective} from "@jsverse/transloco"; +import {Breakpoint} from "../../../shared/_services/utility.service"; +import {NgxStarsModule} from "ngx-stars"; +import {ThemeService} from "../../../_services/theme.service"; +import {SeriesService} from "../../../_services/series.service"; + +@Component({ + selector: 'app-rating-modal', + standalone: true, + imports: [ + TranslocoDirective, + NgxStarsModule + ], + templateUrl: './rating-modal.component.html', + styleUrl: './rating-modal.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RatingModalComponent { + + protected readonly modal = inject(NgbActiveModal); + protected readonly themeService = inject(ThemeService); + protected readonly seriesService = inject(SeriesService); + protected readonly cdRef = inject(ChangeDetectorRef); + + protected readonly Breakpoint = Breakpoint; + + @Input({required: true}) userRating!: number; + @Input({required: true}) seriesId!: number; + @Input({required: true}) hasUserRated!: boolean; + starColor = this.themeService.getCssVariable('--rating-star-color'); + + + updateRating(rating: number) { + this.seriesService.updateRating(this.seriesId, rating).subscribe(() => { + this.userRating = rating; + this.hasUserRated = true; + this.cdRef.markForCheck(); + }); + } + + close() { + this.modal.close({hasUserRated: this.hasUserRated, userRating: this.userRating}); + } +} diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index 074c60ad9..fe8e9697d 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -4,9 +4,12 @@ @if (series && seriesMetadata && libraryType !== null) {
-
- - +
+ @if(mobileSeriesImgBackground === 'true') { + + } @else { + + } @if (series.pagesRead < series.pages && hasReadingProgress) {
@@ -29,7 +32,7 @@
-
+

{{series.name}} @@ -52,7 +55,8 @@ [ageRating]="seriesMetadata.ageRating" [hasReadingProgress]="hasReadingProgress" [readingTimeEntity]="series" - [libraryType]="libraryType"> + [libraryType]="libraryType" + [mangaFormat]="series.format"> @@ -133,12 +137,13 @@
-
+
{{t('writers-title')}}
+ [allowToggle]="false" + (toggle)="switchTabsToDetail()"> {{item.name}} @@ -162,12 +167,13 @@
-
+
{{t('genres-title')}}
+ [allowToggle]="false" + (toggle)="switchTabsToDetail()"> {{item.title}} @@ -180,7 +186,8 @@
+ [allowToggle]="false" + (toggle)="switchTabsToDetail()"> {{item.title}} @@ -189,45 +196,11 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-