mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
UX Pass 5 (#3128)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
dbc4f35107
commit
c93af3e56f
@ -12,10 +12,10 @@
|
||||
<LangVersion>latestmajor</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<Delete Files="../openapi.json" />
|
||||
<Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />
|
||||
</Target>
|
||||
<!-- <Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">-->
|
||||
<!-- <Delete Files="../openapi.json" />-->
|
||||
<!-- <Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />-->
|
||||
<!-- </Target>-->
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -835,16 +835,26 @@ public class ReaderController : BaseApiController
|
||||
return Ok(_unitOfWork.UserTableOfContentRepository.GetPersonalToC(User.GetUserId(), chapterId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the user's personal table of content for the given chapter
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="pageNum"></param>
|
||||
/// <param name="title"></param>
|
||||
/// <returns></returns>
|
||||
[HttpDelete("ptoc")]
|
||||
public async Task<ActionResult> 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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -18,4 +18,9 @@ public class RefreshSeriesDto
|
||||
/// </summary>
|
||||
/// <remarks>This is expensive if true. Defaults to true.</remarks>
|
||||
public bool ForceUpdate { get; init; } = true;
|
||||
/// <summary>
|
||||
/// Should the task force re-calculation of colorscape.
|
||||
/// </summary>
|
||||
/// <remarks>This is expensive if true. Defaults to true.</remarks>
|
||||
public bool ForceColorscape { get; init; } = false;
|
||||
}
|
||||
|
@ -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<Vector3>();
|
||||
@ -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}";
|
||||
|
@ -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
|
||||
/// <param name="forceUpdate"></param>
|
||||
[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);
|
||||
/// <summary>
|
||||
/// Performs a forced refresh of cover images just for a series and it's nested entities
|
||||
/// </summary>
|
||||
@ -35,8 +34,8 @@ public interface IMetadataService
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="forceUpdate">Overrides any cache logic and forces execution</param>
|
||||
|
||||
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
|
||||
/// <param name="chapter"></param>
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
/// <param name="encodeFormat">Convert image to Encoding Format when extracting the cover</param>
|
||||
private Task<bool> UpdateChapterCoverImage(Chapter? chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize)
|
||||
/// <param name="forceColorScape">Force colorscape gen</param>
|
||||
private Task<bool> 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
|
||||
/// </summary>
|
||||
/// <param name="volume"></param>
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
private Task<bool> UpdateVolumeCoverImage(Volume? volume, bool forceUpdate)
|
||||
/// <param name="forceColorScape">Force updating colorscape</param>
|
||||
private Task<bool> 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
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
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
|
||||
/// <param name="series"></param>
|
||||
/// <param name="forceUpdate"></param>
|
||||
/// <param name="encodeFormat"></param>
|
||||
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
|
||||
/// <remarks>This can be heavy on memory first run</remarks>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
/// <param name="forceColorScape">Force updating colorscape</param>
|
||||
[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
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="forceUpdate">Overrides any cache logic and forces execution</param>
|
||||
public async Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true)
|
||||
/// <param name="forceColorscape">Will ensure that the colorscape is regenned</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -370,13 +375,14 @@ public class MetadataService : IMetadataService
|
||||
/// <param name="series">A full Series, with metadata, chapters, etc</param>
|
||||
/// <param name="encodeFormat">When saving the file, what encoding should be used</param>
|
||||
/// <param name="forceUpdate"></param>
|
||||
public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false)
|
||||
/// <param name="forceColorScape">Forces just colorscape generation</param>
|
||||
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())
|
||||
|
@ -10,6 +10,7 @@ namespace API.Services;
|
||||
|
||||
public static class ReviewService
|
||||
{
|
||||
private const int BodyTextLimit = 175;
|
||||
public static IEnumerable<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
|
||||
{
|
||||
IList<UserReviewDto> 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 + "…";
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<Library>, callback?: LibraryActionCallback, forceUpdate: boolean = true) {
|
||||
async refreshLibraryMetadata(library: Partial<Library>, 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);
|
||||
|
@ -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[]}) {
|
||||
|
@ -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 + '');
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<ng-container *transloco="let t; read: 'details-tab'">
|
||||
|
||||
<div class="details pb-3">
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="genres" [title]="t('genres-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
@ -132,4 +132,5 @@
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -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 = [
|
||||
|
@ -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,
|
||||
|
@ -1,15 +1,15 @@
|
||||
<ng-container *transloco="let t; read:'review-card'">
|
||||
<div class="card review-card clickable mb-3" (click)="showModal()">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-2 d-none d-md-block p-2">
|
||||
<div class="col-md-2 col-sm-2 col-2 d-block p-2">
|
||||
@if (isMyReview) {
|
||||
<i class="d-md-none fa-solid fa-star me-2" aria-hidden="true" [title]="t('your-review')"></i>
|
||||
<img class="me-2" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="40" height="40" alt="">
|
||||
<i class="d-none fa-solid fa-star me-2" aria-hidden="true" [title]="t('your-review')"></i>
|
||||
<img class="me-2" [ngSrc]="ScrobbleProvider.Kavita | providerImage:true" width="40" height="40" alt="">
|
||||
} @else {
|
||||
<img class="me-2" [ngSrc]="review.provider | providerImage" width="40" height="40" alt="">
|
||||
<img class="me-2" [ngSrc]="review.provider | providerImage:true" width="40" height="40" alt="">
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="col-md-10 col-sm-10 col-10">
|
||||
<div class="card-body p-2">
|
||||
<!--
|
||||
<h6 class="card-title">
|
||||
|
@ -2,22 +2,25 @@
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title">
|
||||
{{name}}
|
||||
|
||||
</h5>
|
||||
<button type="button" class="btn-close text-reset" [attr.aria-label]="t('common.close')" (click)="close()"></button>
|
||||
</div>
|
||||
|
||||
<div class="offcanvas-body">
|
||||
<ng-container *ngIf="CoverUrl as coverUrl">
|
||||
@if (CoverUrl; as coverUrl) {
|
||||
<div style="width: 160px" class="mx-auto mb-3">
|
||||
<app-image *ngIf="coverUrl" height="232.91px" width="160px" [styles]="{'object-fit': 'contain', 'max-height': '232.91px'}" [imageUrl]="coverUrl"></app-image>
|
||||
@if (coverUrl) {
|
||||
<app-image height="232.91px" width="160px" [styles]="{'object-fit': 'contain', 'max-height': '232.91px'}" [imageUrl]="coverUrl"></app-image>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="externalSeries; else localSeriesBody">
|
||||
<div *ngIf="(externalSeries.volumeCount || 0) > 0 || (externalSeries.chapterCount || 0) > 0" class="text-muted muted mb-2">
|
||||
{{t('series-preview-drawer.vols-and-chapters', {volCount: externalSeries.volumeCount, chpCount: externalSeries.chapterCount})}}
|
||||
</div>
|
||||
@if (externalSeries) {
|
||||
@if ((externalSeries.volumeCount || 0) > 0 || (externalSeries.chapterCount || 0) > 0) {
|
||||
<div class="text-muted muted mb-2">
|
||||
{{t('series-preview-drawer.vols-and-chapters', {volCount: externalSeries.volumeCount, chpCount: externalSeries.chapterCount})}}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if(isExternalSeries && externalSeries) {
|
||||
<div class="text-muted muted mb-2">
|
||||
@ -26,14 +29,20 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<app-read-more *ngIf="externalSeries.summary" [maxLength]="300" [text]="externalSeries.summary"></app-read-more>
|
||||
@if (externalSeries.summary) {
|
||||
<app-read-more [maxLength]="300" [text]="externalSeries.summary"></app-read-more>
|
||||
}
|
||||
}
|
||||
|
||||
<a class="btn btn-primary col-12 mt-2" [href]="url" target="_blank" rel="noopener noreferrer">
|
||||
{{t('series-preview-drawer.view-series')}}
|
||||
</a>
|
||||
|
||||
@if (externalSeries) {
|
||||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="externalSeries.genres" [libraryId]="0" [heading]="t('series-preview-drawer.genres-label')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-tag-badge>
|
||||
{{item}}
|
||||
</app-tag-badge>
|
||||
<span class="dark-exempt btn-icon not-clickable">{{item}}</span>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
@ -41,25 +50,22 @@
|
||||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="externalSeries.tags" [libraryId]="0" [heading]="t('series-preview-drawer.tags-label')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-tag-badge>
|
||||
{{item.name}}
|
||||
</app-tag-badge>
|
||||
<span class="dark-exempt btn-icon not-clickable">{{item.name}}</span>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="externalSeries.staff" [libraryId]="0" [heading]="t('series-preview-drawer.staff-label')">
|
||||
<app-metadata-detail [tags]="externalSeries.staff" [libraryId]="0" [heading]="t('series-preview-drawer.staff-label')" [includeComma]="false">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<div class="card mb-3">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-3">
|
||||
<ng-container *ngIf="item.imageUrl && !item.imageUrl.endsWith('default.jpg'); else localPerson">
|
||||
@if (item.imageUrl && !item.imageUrl.endsWith('default.jpg')) {
|
||||
<app-image height="24px" width="24px" [styles]="{'object-fit': 'contain'}" [imageUrl]="item.imageUrl" classes="person-img"></app-image>
|
||||
</ng-container>
|
||||
<ng-template #localPerson>
|
||||
} @else {
|
||||
<i class="fa fa-user-circle align-self-center person-img" style="font-size: 28px;" aria-hidden="true"></i>
|
||||
</ng-template>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card-body">
|
||||
@ -72,67 +78,56 @@
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
@else if(localSeries) {
|
||||
<div class="d-inline-block mb-2 mt-2" style="width: 100%">
|
||||
<span class="text-muted muted">{{localSeries.publicationStatus | publicationStatus}}</span>
|
||||
<button class="btn btn-secondary btn-sm float-end me-3"
|
||||
(click)="toggleWantToRead()"
|
||||
ngbTooltip="{{wantToRead ? t('series-preview-drawer.remove-from-want-to-read') : t('series-preview-drawer.add-to-want-to-read')}}">
|
||||
<i class="{{wantToRead ? 'fa-solid' : 'fa-regular'}} fa-star" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-template #localSeriesBody>
|
||||
<ng-container *ngIf="localSeries">
|
||||
<div class="d-inline-block mb-2" style="width: 100%">
|
||||
<span class="text-muted muted">{{localSeries.publicationStatus | publicationStatus}}</span>
|
||||
<button class="btn btn-secondary btn-sm float-end me-3"
|
||||
(click)="toggleWantToRead()"
|
||||
ngbTooltip="{{wantToRead ? t('series-preview-drawer.remove-from-want-to-read') : t('series-preview-drawer.add-to-want-to-read')}}">
|
||||
<i class="{{wantToRead ? 'fa-solid' : 'fa-regular'}} fa-star" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<app-read-more [maxLength]="300" [text]="localSeries.summary"></app-read-more>
|
||||
<app-read-more [maxLength]="300" [text]="localSeries.summary"></app-read-more>
|
||||
|
||||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="localSeries.genres" [libraryId]="0" [heading]="t('series-preview-drawer.genres-label')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-tag-badge>
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="localSeries.genres" [libraryId]="0" [heading]="t('series-preview-drawer.genres-label')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<a class="dark-exempt btn-icon not-clickable">{{item.title}}</a>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="localSeries.tags" [libraryId]="0" [heading]="t('series-preview-drawer.tags-label')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-tag-badge>
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="localSeries.tags" [libraryId]="0" [heading]="t('series-preview-drawer.tags-label')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<span class="dark-exempt btn-icon not-clickable">{{item.title}}</span>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="localStaff" [libraryId]="0" [heading]="t('series-preview-drawer.staff-label')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<div class="card mb-3">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4">
|
||||
<i class="fa fa-user-circle align-self-center" style="font-size: 28px; margin-top: 24px; margin-left: 24px" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{item.name}}</h6>
|
||||
<p class="card-text" style="font-size: 14px"><small class="text-muted">{{item.role}}</small></p>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="localStaff" [libraryId]="0" [heading]="t('series-preview-drawer.staff-label')" [includeComma]="false">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<div class="card mb-3">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4">
|
||||
<i class="fa fa-user-circle align-self-center" style="font-size: 28px; margin-top: 24px; margin-left: 24px" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{item.name}}</h6>
|
||||
<p class="card-text" style="font-size: 14px"><small class="text-muted">{{item.role}}</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
}
|
||||
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
|
||||
<a class="btn btn-primary col-12 mt-2" [href]="url" target="_blank" rel="noopener noreferrer">
|
||||
{{t('series-preview-drawer.view-series')}}
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -15,4 +15,13 @@
|
||||
|
||||
a.read-more-link {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.not-clickable {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.offcanvas-body {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {NgOptimizedImage} from '@angular/common';
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {NgbActiveOffcanvas, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ExternalSeriesDetail, SeriesStaff} from "../../_models/series-detail/external-series-detail";
|
||||
@ -17,17 +17,26 @@ import {SeriesMetadata} from "../../_models/metadata/series-metadata";
|
||||
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||
import {ActionService} from "../../_services/action.service";
|
||||
import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
|
||||
import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
||||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-preview-drawer',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslocoDirective, ImageComponent, LoadingComponent, SafeHtmlPipe, A11yClickDirective, MetadataDetailComponent, PersonBadgeComponent, TagBadgeComponent, PublicationStatusPipe, ReadMoreComponent, NgbTooltip, NgOptimizedImage, ProviderImagePipe],
|
||||
imports: [TranslocoDirective, ImageComponent, LoadingComponent, SafeHtmlPipe, A11yClickDirective, MetadataDetailComponent, PersonBadgeComponent, TagBadgeComponent, PublicationStatusPipe, ReadMoreComponent, NgbTooltip, NgOptimizedImage, ProviderImagePipe],
|
||||
templateUrl: './series-preview-drawer.component.html',
|
||||
styleUrls: ['./series-preview-drawer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SeriesPreviewDrawerComponent implements OnInit {
|
||||
|
||||
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);
|
||||
|
||||
protected readonly FilterField = FilterField;
|
||||
|
||||
@Input({required: true}) name!: string;
|
||||
@Input() aniListId?: number;
|
||||
@Input() malId?: number;
|
||||
@ -42,11 +51,7 @@ export class SeriesPreviewDrawerComponent implements OnInit {
|
||||
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() {
|
||||
if (this.isExternalSeries) {
|
||||
|
@ -1,4 +1,24 @@
|
||||
<ng-container *transloco="let t; read: 'related-tab'">
|
||||
|
||||
@if (relations.length > 0) {
|
||||
<app-carousel-reel [items]="relations" [title]="t('relations-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card class="col-auto mt-2 mb-2" [series]="item.series" [libraryId]="item.series.libraryId" [relation]="item.relation"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
}
|
||||
|
||||
@if (collections.length > 0) {
|
||||
<app-carousel-reel [items]="collections" [title]="t('collections-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-card-item [title]="item.title" [entity]="item"
|
||||
[suppressLibraryLink]="true" [imageUrl]="imageService.getCollectionCoverImage(item.id)"
|
||||
(clicked)="openCollection(item)"></app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
}
|
||||
|
||||
|
||||
@if (readingLists.length > 0) {
|
||||
<app-carousel-reel [items]="readingLists" [title]="t('reading-lists-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
|
@ -4,6 +4,16 @@ import {CardItemComponent} from "../../cards/card-item/card-item.component";
|
||||
import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {UserCollection} from "../../_models/collection-tag";
|
||||
import {Router} from "@angular/router";
|
||||
import {SeriesCardComponent} from "../../cards/series-card/series-card.component";
|
||||
import {Series} from "../../_models/series";
|
||||
import {RelationKind} from "../../_models/series-detail/relation-kind";
|
||||
|
||||
export interface RelatedSeriesPair {
|
||||
series: Series;
|
||||
relation: RelationKind;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-related-tab',
|
||||
@ -11,7 +21,8 @@ import {TranslocoDirective} from "@jsverse/transloco";
|
||||
imports: [
|
||||
CardItemComponent,
|
||||
CarouselReelComponent,
|
||||
TranslocoDirective
|
||||
TranslocoDirective,
|
||||
SeriesCardComponent
|
||||
],
|
||||
templateUrl: './related-tab.component.html',
|
||||
styleUrl: './related-tab.component.scss',
|
||||
@ -20,11 +31,18 @@ import {TranslocoDirective} from "@jsverse/transloco";
|
||||
export class RelatedTabComponent {
|
||||
|
||||
protected readonly imageService = inject(ImageService);
|
||||
protected readonly router = inject(Router);
|
||||
|
||||
@Input() readingLists: Array<ReadingList> = [];
|
||||
@Input() collections: Array<UserCollection> = [];
|
||||
@Input() relations: Array<RelatedSeriesPair> = [];
|
||||
|
||||
openReadingList(readingList: ReadingList) {
|
||||
this.router.navigate(['lists', readingList.id]);
|
||||
}
|
||||
|
||||
openCollection(collection: UserCollection) {
|
||||
this.router.navigate(['collections', collection.id]);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,8 +16,8 @@
|
||||
<div class="input-group">
|
||||
<input id="settings-hostname" aria-describedby="hostname-validations" class="form-control" formControlName="hostName" type="text"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
<button class="btn btn-outline-secondary" (click)="autofillGmail()">{{t('gmail-label')}}</button>
|
||||
<button class="btn btn-outline-secondary" (click)="autofillOutlook()">{{t('outlook-label')}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="autofillGmail()">{{t('gmail-label')}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="autofillOutlook()">{{t('outlook-label')}}</button>
|
||||
</div>
|
||||
|
||||
@if(settingsForm.dirty || settingsForm.touched) {
|
||||
@ -39,7 +39,7 @@
|
||||
{{formControl.value | defaultValue}}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
<input type="text" class="form-control" aria-describedby="email-header" formControlName="senderAddress" id="settings-sender-address" />
|
||||
<input type="text" class="form-control" formControlName="senderAddress" id="settings-sender-address" />
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
@ -52,7 +52,7 @@
|
||||
{{formControl.value | defaultValue}}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
<input type="text" class="form-control" aria-describedby="email-header" formControlName="senderDisplayName" id="settings-sender-displayname" />
|
||||
<input type="text" class="form-control" formControlName="senderDisplayName" id="settings-sender-displayname" />
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
@ -78,7 +78,7 @@
|
||||
{{formControl.value | defaultValue}}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
<input type="number" inputmode="numeric" min="1" class="form-control" aria-describedby="email-header" formControlName="port" id="settings-port" />
|
||||
<input type="number" inputmode="numeric" min="1" class="form-control" formControlName="port" id="settings-port" />
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
@ -89,7 +89,7 @@
|
||||
<app-setting-switch [title]="t('enable-ssl-label')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch">
|
||||
<input id="setting-enable-ssl" type="checkbox" class="form-check-input" formControlName="enableOpds">
|
||||
<input id="setting-enable-ssl" type="checkbox" class="form-check-input" formControlName="enableSsl">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
@ -103,7 +103,7 @@
|
||||
{{formControl.value | defaultValue}}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
<input type="text" class="form-control" aria-describedby="email-header" formControlName="userName" id="settings-username" />
|
||||
<input type="text" class="form-control" formControlName="userName" id="settings-username" />
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
@ -116,7 +116,7 @@
|
||||
{{formControl.value ? '********' : null | defaultValue}}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
<input type="text" class="form-control" aria-describedby="email-header" formControlName="password" id="settings-password" />
|
||||
<input type="text" class="form-control" formControlName="password" id="settings-password" />
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
@ -129,7 +129,7 @@
|
||||
{{formControl.value | bytes}}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
<input type="number" inputmode="numeric" min="1" class="form-control" aria-describedby="email-header" formControlName="sizeLimit" id="settings-size-limit" />
|
||||
<input type="number" inputmode="numeric" min="1" class="form-control" formControlName="sizeLimit" id="settings-size-limit" />
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ export class ManageLibraryComponent implements OnInit {
|
||||
await this.actionService.refreshLibraryMetadata(library);
|
||||
break;
|
||||
case(Action.GenerateColorScape):
|
||||
await this.actionService.refreshLibraryMetadata(library, undefined, false);
|
||||
await this.actionService.refreshLibraryMetadata(library, undefined, false, true);
|
||||
break;
|
||||
case(Action.Edit):
|
||||
this.editLibrary(library)
|
||||
|
@ -39,7 +39,7 @@
|
||||
<div class="input-group">
|
||||
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
<button class="btn btn-outline-secondary" (click)="resetBaseUrl()">{{t('reset')}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="resetBaseUrl()">{{t('reset')}}</button>
|
||||
</div>
|
||||
|
||||
@if(settingsForm.dirty || settingsForm.touched) {
|
||||
@ -64,7 +64,7 @@
|
||||
<div class="input-group">
|
||||
<input id="settings-ipaddresses" aria-describedby="settings-ipaddresses-help" class="form-control" formControlName="ipAddresses" type="text"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
<button class="btn btn-outline-secondary" (click)="resetIPAddresses()">{{t('reset')}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="resetIPAddresses()">{{t('reset')}}</button>
|
||||
</div>
|
||||
|
||||
@if(settingsForm.dirty || settingsForm.touched) {
|
||||
|
@ -1,31 +1,59 @@
|
||||
.content-wrapper {
|
||||
padding: 0 10px 0;
|
||||
height: 100%;
|
||||
height: calc(var(--vh)* 100 - var(--nav-offset));
|
||||
}
|
||||
|
||||
|
||||
.companion-bar {
|
||||
transition: all var(--side-nav-companion-bar-transistion);
|
||||
margin-left: 40px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: calc(var(--vh)* 100 - var(--nav-offset));
|
||||
width: 100%;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent; /*make scrollbar space invisible */
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent; /*makes it invisible when not hovering*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.companion-bar-collapsed {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.companion-bar-content {
|
||||
margin-left: 190px;
|
||||
width: auto;
|
||||
mask-image: linear-gradient(to bottom, transparent, black 0%, black 96%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 96%, transparent 100%);
|
||||
width: calc(100% - 190px);
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 768px) {
|
||||
::ng-deep html {
|
||||
height: 100dvh !important;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 0 5px 0;
|
||||
overflow: hidden;
|
||||
height: calc(var(--vh)*100 - var(--nav-offset));
|
||||
height: calc(var(--vh)* 100 - var(--nav-mobile-offset));
|
||||
padding: 0 10px 0;
|
||||
|
||||
&.closed {
|
||||
overflow: auto;
|
||||
@ -35,11 +63,27 @@
|
||||
.companion-bar {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
width: calc(100vw - 30px);
|
||||
padding-top: 20px;
|
||||
height: calc(100dvh - var(--nav-mobile-offset));
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent; /*makes it invisible when not hovering*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.companion-bar-content {
|
||||
margin-left: 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,7 +111,7 @@
|
||||
height: 100vh;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background-color: #121212;
|
||||
background-color: var(--bs-body-bg);
|
||||
filter: blur(20px);
|
||||
object-fit: contain;
|
||||
transform: scale(1.1);
|
||||
@ -80,4 +124,3 @@
|
||||
height: 113vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
DestroyRef,
|
||||
HostListener,
|
||||
inject,
|
||||
Inject,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import {NavigationStart, Router, RouterOutlet} from '@angular/router';
|
||||
@ -46,11 +45,12 @@ export class AppComponent implements OnInit {
|
||||
private readonly ngbModal = inject(NgbModal);
|
||||
private readonly router = inject(Router);
|
||||
private readonly themeService = inject(ThemeService);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
|
||||
constructor(ratingConfig: NgbRatingConfig, @Inject(DOCUMENT) private document: Document, modalConfig: NgbModalConfig) {
|
||||
constructor(ratingConfig: NgbRatingConfig, modalConfig: NgbModalConfig) {
|
||||
|
||||
modalConfig.fullscreen = 'md';
|
||||
|
||||
@ -80,7 +80,6 @@ export class AppComponent implements OnInit {
|
||||
const currentRoute = this.router.routerState;
|
||||
await this.router.navigateByUrl(currentRoute.snapshot.url, { skipLocationChange: true });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -106,6 +105,7 @@ export class AppComponent implements OnInit {
|
||||
this.themeService.setColorScape('');
|
||||
}
|
||||
|
||||
|
||||
setCurrentUser() {
|
||||
const user = this.accountService.getUserFromLocalStorage();
|
||||
this.accountService.setCurrentUser(user);
|
||||
@ -114,8 +114,6 @@ export class AppComponent implements OnInit {
|
||||
// Bootstrap anything that's needed
|
||||
this.themeService.getThemes().subscribe();
|
||||
this.libraryService.getLibraryNames().pipe(take(1), shareReplay({refCount: true, bufferSize: 1})).subscribe();
|
||||
// On load, make an initial call for valid license
|
||||
this.accountService.hasValidLicense().subscribe();
|
||||
|
||||
// Every hour, have the UI check for an update. People seriously stay out of date
|
||||
interval(2* 60 * 60 * 1000) // 2 hours in milliseconds
|
||||
|
@ -629,7 +629,7 @@ export class EditSeriesModalComponent implements OnInit {
|
||||
await this.actionService.refreshSeriesMetadata(this.series);
|
||||
break;
|
||||
case Action.GenerateColorScape:
|
||||
await this.actionService.refreshSeriesMetadata(this.series, undefined, false);
|
||||
await this.actionService.refreshSeriesMetadata(this.series, undefined, false, true);
|
||||
break;
|
||||
case Action.AnalyzeFiles:
|
||||
this.actionService.analyzeFilesForSeries(this.series);
|
||||
|
@ -1,121 +0,0 @@
|
||||
<ng-container *transloco="let t; read: 'card-detail-drawer'">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title">
|
||||
<span class="modal-title" id="modal-basic-title">
|
||||
<app-entity-title [libraryType]="libraryType" [entity]="data" [seriesName]="parentName"></app-entity-title>
|
||||
</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close text-reset" aria-label="Close" (click)="activeOffcanvas.dismiss()"></button>
|
||||
</div>
|
||||
|
||||
<div class="offcanvas-body pb-3">
|
||||
<div class="d-flex">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="vertical" style="max-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[TabID.General]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.General].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="container-fluid" style="overflow: auto">
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="d-none d-md-block col-md-2 col-lg-1">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="coverImageUrl"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-lg-11">
|
||||
<ng-container *ngIf="summary.length > 0; else noSummary">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</ng-container>
|
||||
<ng-template #noSummary>
|
||||
{{t('no-summary')}}
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-entity-info-cards [entity]="data" [libraryId]="libraryId"></app-entity-info-cards>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Metadata]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Metadata].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-chapter-metadata-detail [chapter]="chapter"></app-chapter-metadata-detail>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Progress]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Progress].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-edit-chapter-progress [chapter]="chapter"></app-edit-chapter-progress>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="(accountService.isAdmin$ | async) === false">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Cover].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateCoverImageIndex($event)"
|
||||
(selectedBase64Url)="applyCoverImage($event)" [showReset]="chapter.coverImageLocked"
|
||||
(resetClicked)="resetCoverImage()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Files]" [disabled]="(accountService.isAdmin$ | async) === false">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Files].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
@if (!utilityService.isChapter(data)) {
|
||||
<h4>{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||
}
|
||||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" [title]="t('read')">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"></app-image>
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">
|
||||
<span>
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
<ng-container *ngIf="chapter.minNumber !== LooseLeafOrSpecialNumber; else specialHeader">
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</ng-container>
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill ms-1">
|
||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||
<span *ngIf="chapter.pagesRead === 0">{{t('unread') | uppercase}}</span>
|
||||
<span *ngIf="chapter.pagesRead === chapter.pages">{{t('read') | uppercase}}</span>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #specialHeader>{{t('files')}}</ng-template>
|
||||
</h5>
|
||||
<ul class="list-group">
|
||||
@for (file of chapter.files; track file.id) {
|
||||
<li class="list-group-item no-hover">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
{{t('pages')}} {{file.pages | number:''}}
|
||||
</div>
|
||||
@if (data.hasOwnProperty('created')) {
|
||||
<div class="col">
|
||||
{{t('added')}} {{file.created | date: 'short' | defaultDate}}
|
||||
</div>
|
||||
}
|
||||
<div class="col">
|
||||
{{t('size')}} {{file.bytes | bytes}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
@ -1,20 +0,0 @@
|
||||
.hide-if-empty:empty {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.offcanvas-body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.offcanvas-header {
|
||||
padding: 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
overflow: auto;
|
||||
height: calc(40vh - (46px + 1rem)); // drawer height - offcanvas heading height
|
||||
}
|
||||
|
||||
.h6 {
|
||||
font-weight: 600;
|
||||
}
|
@ -1,278 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
NgbActiveOffcanvas,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavOutlet
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import {Chapter, LooseLeafOrDefaultNumber} from 'src/app/_models/chapter';
|
||||
import { Device } from 'src/app/_models/device/device';
|
||||
import { LibraryType } from 'src/app/_models/library/library';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service';
|
||||
import { ActionService } from 'src/app/_services/action.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {EntityTitleComponent} from "../entity-title/entity-title.component";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||
import {EntityInfoCardsComponent} from "../entity-info-cards/entity-info-cards.component";
|
||||
import {CoverImageChooserComponent} from "../cover-image-chooser/cover-image-chooser.component";
|
||||
import {ChapterMetadataDetailComponent} from "../chapter-metadata-detail/chapter-metadata-detail.component";
|
||||
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
|
||||
import {BytesPipe} from "../../_pipes/bytes.pipe";
|
||||
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component";
|
||||
import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component";
|
||||
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
import {EditChapterProgressComponent} from "../edit-chapter-progress/edit-chapter-progress.component";
|
||||
import {CarouselTabsComponent} from "../../carousel/_components/carousel-tabs/carousel-tabs.component";
|
||||
import {CarouselTabComponent} from "../../carousel/_components/carousel-tab/carousel-tab.component";
|
||||
|
||||
enum TabID {
|
||||
General = 0,
|
||||
Metadata = 1,
|
||||
Cover = 2,
|
||||
Progress = 3,
|
||||
Files = 4
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-detail-drawer',
|
||||
standalone: true,
|
||||
imports: [CommonModule, EntityTitleComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, ImageComponent, ReadMoreComponent, EntityInfoCardsComponent, CoverImageChooserComponent, ChapterMetadataDetailComponent, CardActionablesComponent, DefaultDatePipe, BytesPipe, NgbNavOutlet, BadgeExpanderComponent, TagBadgeComponent, PersonBadgeComponent, TranslocoDirective, EditChapterProgressComponent, CarouselTabsComponent, CarouselTabComponent],
|
||||
templateUrl: './card-detail-drawer.component.html',
|
||||
styleUrls: ['./card-detail-drawer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CardDetailDrawerComponent implements OnInit {
|
||||
|
||||
protected readonly utilityService = inject(UtilityService);
|
||||
protected readonly imageService = inject(ImageService);
|
||||
private readonly uploadService = inject(UploadService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
protected readonly accountService = inject(AccountService);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly libraryService = inject(LibraryService);
|
||||
private readonly readerService = inject(ReaderService);
|
||||
protected readonly activeOffcanvas = inject(NgbActiveOffcanvas);
|
||||
private readonly downloadService = inject(DownloadService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly LibraryType = LibraryType;
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrDefaultNumber;
|
||||
|
||||
@Input() parentName = '';
|
||||
@Input() seriesId: number = 0;
|
||||
@Input() libraryId: number = 0;
|
||||
@Input({required: true}) data!: Volume | Chapter;
|
||||
|
||||
/**
|
||||
* If this is a volume, this will be first chapter for said volume.
|
||||
*/
|
||||
chapter!: Chapter;
|
||||
isChapter = false;
|
||||
chapters: Chapter[] = [];
|
||||
|
||||
imageUrls: Array<string> = [];
|
||||
/**
|
||||
* Cover image for the entity
|
||||
*/
|
||||
coverImageUrl!: string;
|
||||
|
||||
isAdmin$: Observable<boolean> = of(false);
|
||||
|
||||
|
||||
actions: ActionItem<any>[] = [];
|
||||
chapterActions: ActionItem<Chapter>[] = [];
|
||||
libraryType: LibraryType = LibraryType.Manga;
|
||||
|
||||
|
||||
tabs = [
|
||||
{title: 'general-tab', disabled: false},
|
||||
{title: 'metadata-tab', disabled: false},
|
||||
{title: 'cover-tab', disabled: false},
|
||||
{title: 'progress-tab', disabled: false},
|
||||
{title: 'info-tab', disabled: false}
|
||||
];
|
||||
active = this.tabs[0];
|
||||
|
||||
summary: string = '';
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.imageUrls = this.chapters.map(c => this.imageService.getChapterCoverImage(c.id));
|
||||
this.isChapter = this.utilityService.isChapter(this.data);
|
||||
this.chapter = this.utilityService.isChapter(this.data) ? (this.data as Chapter) : (this.data as Volume).chapters[0];
|
||||
|
||||
|
||||
if (this.isChapter) {
|
||||
this.coverImageUrl = this.imageService.getChapterCoverImage(this.data.id);
|
||||
this.summary = this.utilityService.asChapter(this.data).summary || '';
|
||||
this.chapters.push(this.data as Chapter);
|
||||
} else {
|
||||
this.coverImageUrl = this.imageService.getVolumeCoverImage(this.data.id);
|
||||
this.summary = this.utilityService.asVolume(this.data).chapters[0].summary || '';
|
||||
this.chapters.push(...(this.data as Volume).chapters);
|
||||
}
|
||||
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this))
|
||||
.filter(item => item.action !== Action.Edit);
|
||||
this.chapterActions.push({title: 'read', description: '', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false, children: []});
|
||||
if (this.isChapter) {
|
||||
const chapter = this.utilityService.asChapter(this.data);
|
||||
this.chapterActions = this.actionFactoryService.filterSendToAction(this.chapterActions, chapter);
|
||||
} else {
|
||||
this.chapterActions = this.actionFactoryService.filterSendToAction(this.chapterActions, this.chapters[0]);
|
||||
}
|
||||
|
||||
this.libraryService.getLibraryType(this.libraryId).subscribe(type => {
|
||||
this.libraryType = type;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
const collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
this.chapters.forEach((c: Chapter) => {
|
||||
c.files.sort((a: MangaFile, b: MangaFile) => collator.compare(a.filePath, b.filePath));
|
||||
});
|
||||
|
||||
this.imageUrls = this.chapters.map(c => this.imageService.getChapterCoverImage(c.id));
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
close() {
|
||||
this.activeOffcanvas.close();
|
||||
}
|
||||
|
||||
formatChapterNumber(chapter: Chapter) {
|
||||
if (chapter.minNumber === LooseLeafOrDefaultNumber) {
|
||||
return '1';
|
||||
}
|
||||
return chapter.range + '';
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>, chapter: Chapter) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, chapter);
|
||||
}
|
||||
}
|
||||
|
||||
applyCoverImage(coverUrl: string) {
|
||||
this.uploadService.updateChapterCoverImage(this.chapter.id, coverUrl).subscribe(() => {});
|
||||
}
|
||||
|
||||
updateCoverImageIndex(selectedIndex: number) {
|
||||
if (selectedIndex <= 0) return;
|
||||
this.applyCoverImage(this.imageUrls[selectedIndex]);
|
||||
}
|
||||
|
||||
resetCoverImage() {
|
||||
this.uploadService.resetChapterCoverLock(this.chapter.id).subscribe(() => {
|
||||
this.toastr.info(translate('toasts.regen-cover'));
|
||||
});
|
||||
}
|
||||
|
||||
markChapterAsRead(chapter: Chapter) {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsRead(this.libraryId, this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
|
||||
}
|
||||
|
||||
markChapterAsUnread(chapter: Chapter) {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsUnread(this.libraryId, this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) {
|
||||
switch (action.action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.markChapterAsRead(chapter);
|
||||
break;
|
||||
case(Action.MarkAsUnread):
|
||||
this.markChapterAsUnread(chapter);
|
||||
break;
|
||||
case(Action.AddToReadingList):
|
||||
this.actionService.addChapterToReadingList(chapter, this.seriesId);
|
||||
break;
|
||||
case (Action.IncognitoRead):
|
||||
this.readChapter(chapter, true);
|
||||
break;
|
||||
case (Action.Download):
|
||||
this.download(chapter);
|
||||
break;
|
||||
case (Action.Read):
|
||||
this.readChapter(chapter, false);
|
||||
break;
|
||||
case (Action.SendTo):
|
||||
{
|
||||
const device = (action._extra!.data as Device);
|
||||
this.actionService.sendToDevice([chapter.id], device);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
readChapter(chapter: Chapter, incognito: boolean = false) {
|
||||
if (chapter.pages === 0) {
|
||||
this.toastr.error(translate('toasts.no-pages'));
|
||||
return;
|
||||
}
|
||||
|
||||
const params = this.readerService.getQueryParamsObject(incognito, false);
|
||||
this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, chapter.files[0].format), {queryParams: params});
|
||||
this.close();
|
||||
}
|
||||
|
||||
download(chapter: Chapter) {
|
||||
if (this.downloadInProgress) {
|
||||
this.toastr.info(translate('toasts.download-in-progress'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.downloadInProgress = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.downloadService.download('chapter', chapter, (d) => {
|
||||
if (d) return;
|
||||
this.downloadInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
@ -32,6 +32,8 @@
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
align-items: start;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@ -92,13 +94,33 @@
|
||||
|
||||
.virtual-scroller, virtual-scroller {
|
||||
width: 100%;
|
||||
height: calc(var(--vh) * 100 - 173px);
|
||||
height: calc(var(--vh) * 100 - 143px);
|
||||
mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
overflow: auto;
|
||||
|
||||
}
|
||||
|
||||
virtual-scroller.empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vertical.selfScroll {
|
||||
&::-webkit-scrollbar {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent; /*makes it invisible when not hovering*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
|
@ -44,14 +44,19 @@
|
||||
}
|
||||
|
||||
<div class="card-overlay"></div>
|
||||
@if (overlayInformation | safeHtml; as info) {
|
||||
@if (info) {
|
||||
<div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}">
|
||||
<div class="position-relative">
|
||||
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="info" placement="top" [innerHTML]="info"></span>
|
||||
</div>
|
||||
|
||||
@if (showReadButton) {
|
||||
<div class="series overlay-information">
|
||||
<div class="overlay-information--centered">
|
||||
<span class="card-title library mx-auto" style="width: auto;">
|
||||
<span (click)="clickRead($event)">
|
||||
<div>
|
||||
<i class="fa-solid fa-book" aria-hidden="true"></i>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body meta-title">
|
||||
|
@ -134,13 +134,17 @@ export class CardItemComponent implements OnInit {
|
||||
*/
|
||||
@Input() count: number = 0;
|
||||
/**
|
||||
* Additional information to show on the overlay area. Will always render.
|
||||
* Show a read button. Emits on (readClicked)
|
||||
*/
|
||||
@Input() overlayInformation: string = '';
|
||||
@Input() showReadButton: boolean = false;
|
||||
/**
|
||||
* If overlay is enabled, should the text be centered or not
|
||||
*/
|
||||
@Input() centerOverlay = false;
|
||||
/**
|
||||
* Will generate a button to instantly read
|
||||
*/
|
||||
@Input() hasReadButton = false;
|
||||
/**
|
||||
* Event emitted when item is clicked
|
||||
*/
|
||||
@ -149,6 +153,7 @@ export class CardItemComponent implements OnInit {
|
||||
* When the card is selected.
|
||||
*/
|
||||
@Output() selection = new EventEmitter<boolean>();
|
||||
@Output() readClicked = new EventEmitter<Series | Volume | Chapter | UserCollection | PageBookmark | RecentlyAddedItem | NextExpectedChapter>();
|
||||
@ContentChild('subtitle') subtitleTemplate!: TemplateRef<any>;
|
||||
/**
|
||||
* Library name item belongs to
|
||||
@ -229,9 +234,10 @@ export class CardItemComponent implements OnInit {
|
||||
const nextDate = (this.entity as NextExpectedChapter);
|
||||
|
||||
const tokens = nextDate.title.split(':');
|
||||
this.overlayInformation = `
|
||||
<i class="fa-regular fa-clock mb-2" style="font-size: 26px" aria-hidden="true"></i>
|
||||
<div>${tokens[0]}</div><div>${tokens[1]}</div>`;
|
||||
// this.overlayInformation = `
|
||||
// <i class="fa-regular fa-clock mb-2" style="font-size: 26px" aria-hidden="true"></i>
|
||||
// <div>${tokens[0]}</div><div>${tokens[1]}</div>`;
|
||||
// // todo: figure out where this caller is
|
||||
this.centerOverlay = true;
|
||||
|
||||
if (nextDate.expectedDate) {
|
||||
@ -387,4 +393,11 @@ export class CardItemComponent implements OnInit {
|
||||
// return a.isAllowed(a, this.entity);
|
||||
// });
|
||||
}
|
||||
|
||||
clickRead(event: any) {
|
||||
event.stopPropagation();
|
||||
if (this.bulkSelectionService.hasSelections()) return;
|
||||
|
||||
this.readClicked.emit(this.entity);
|
||||
}
|
||||
}
|
||||
|
@ -1,132 +0,0 @@
|
||||
<ng-container *transloco="let t; read: 'chapter-metadata-detail'">
|
||||
<ng-container *ngIf="chapter !== undefined">
|
||||
<span *ngIf="chapter.writers.length === 0 && chapter.coverArtists.length === 0
|
||||
&& chapter.pencillers.length === 0 && chapter.inkers.length === 0
|
||||
&& chapter.colorists.length === 0 && chapter.letterers.length === 0
|
||||
&& chapter.editors.length === 0 && chapter.publishers.length === 0
|
||||
&& chapter.characters.length === 0 && chapter.translators.length === 0
|
||||
&& chapter.imprints.length === 0 && chapter.locations.length === 0
|
||||
&& chapter.teams.length === 0">
|
||||
{{t('no-data')}}
|
||||
</span>
|
||||
<div class="container-flex row row-cols-auto row-cols-lg-5 g-2 g-lg-3 me-0 mt-2">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.writers && chapter.writers.length > 0">
|
||||
<h6>{{t('writers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.coverArtists && chapter.coverArtists.length > 0">
|
||||
<h6>{{t('cover-artists-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.coverArtists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.pencillers && chapter.pencillers.length > 0">
|
||||
<h6>{{t('pencillers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.pencillers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.inkers && chapter.inkers.length > 0">
|
||||
<h6>{{t('inkers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.inkers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.colorists && chapter.colorists.length > 0">
|
||||
<h6>{{t('colorists-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.colorists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.letterers && chapter.letterers.length > 0">
|
||||
<h6>{{t('letterers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.letterers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.editors && chapter.editors.length > 0">
|
||||
<h6>{{t('editors-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.editors">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.publishers && chapter.publishers.length > 0">
|
||||
<h6>{{t('publishers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.imprints && chapter.imprints.length > 0">
|
||||
<h6>{{t('imprints-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.imprints">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.characters && chapter.characters.length > 0">
|
||||
<h6>{{t('characters-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.teams && chapter.teams.length > 0">
|
||||
<h6>{{t('teams-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.teams">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.locations && chapter.locations.length > 0">
|
||||
<h6>{{t('locations-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.locations">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.translators && chapter.translators.length > 0">
|
||||
<h6>{{t('translators-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.translators">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
@ -1,18 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component";
|
||||
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {Chapter} from "../../_models/chapter";
|
||||
|
||||
@Component({
|
||||
selector: 'app-chapter-metadata-detail',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BadgeExpanderComponent, PersonBadgeComponent, TranslocoDirective],
|
||||
templateUrl: './chapter-metadata-detail.component.html',
|
||||
styleUrls: ['./chapter-metadata-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ChapterMetadataDetailComponent {
|
||||
@Input() chapter: Chapter | undefined;
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
<ng-container *transloco="let t; read: 'entity-info-cards'">
|
||||
|
||||
<div class="mt-3 mb-3">
|
||||
<div class="row g-0" *ngIf="chapter ">
|
||||
<!-- Tags and Characters are used a lot of Hentai and Doujinshi type content, so showing in list item has value add on first glance -->
|
||||
<app-metadata-detail [tags]="chapter.tags" [libraryId]="libraryId" [queryParam]="FilterField.Tags" heading="Tags">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="chapter.characters" [libraryId]="libraryId" [queryParam]="FilterField.Characters" heading="Characters">
|
||||
<ng-template #titleTemplate let-item>{{item.name}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<ng-container *ngIf="chapter !== undefined && chapter.releaseDate && (chapter.releaseDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('release-date-tooltip')" [clickable]="false" fontClasses="fa-regular fa-calendar" [title]="t('release-date-title')">
|
||||
{{chapter.releaseDate | date:'shortDate' | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.ageRating !== AgeRating.Unknown">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('age-rating-title')" [clickable]="false" fontClasses="fas fa-eye" [title]="t('age-rating-title')">
|
||||
{{chapter.ageRating | ageRating}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="totalPages > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||
{{t('pages-count', {num: totalPages | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{t('words-count', {num: totalWordCount | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0 || chapter.files[0].format !== MangaFormat.EPUB">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('read-time-title')" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime">{{t('less-than-hour')}}</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} {{readingTime.minHours > 1 ? t('hours') : t('hour')}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties && chapter.createdUtc && chapter.createdUtc !== '' && (chapter.createdUtc | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('date-added-title')" [clickable]="false" fontClasses="fa-solid fa-file-import" [title]="t('date-added-title')">
|
||||
{{chapter.createdUtc | utcToLocalTime | translocoDate: {dateStyle: 'short', timeStyle: 'short' } | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties && size > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('size-title')" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" [title]="t('size-title')">
|
||||
{{size | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('id-title')" [clickable]="false" fontClasses="fa-solid fa-fingerprint" [title]="t('id-title')">
|
||||
{{entity.id}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<ng-container *ngIf="WebLinks.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('links-title')" [clickable]="false" fontClasses="fa-solid fa-link" [title]="t('links-title')">
|
||||
<a class="me-1" [href]="link | safeHtml" *ngFor="let link of WebLinks" target="_blank" rel="noopener noreferrer" [title]="link">
|
||||
<app-image height="24px" width="24px" aria-hidden="true" [imageUrl]="imageService.getWebLinkImage(link)"
|
||||
[errorImage]="imageService.errorWebLinkImage"></app-image>
|
||||
</a>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.isbn.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('isbn-title')" [clickable]="false" fontClasses="fa-solid fa-barcode" [title]="t('isbn-title')">
|
||||
{{chapter.isbn}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(chapter.lastReadingProgress | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('last-read-title')" [clickable]="false" fontClasses="fa-regular fa-clock" [ngbTooltip]="chapter.lastReadingProgress | date: 'medium'">
|
||||
{{chapter.lastReadingProgress | date: 'shortDate'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isChapter">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('sort-order-title')" [clickable]="false" fontClasses="fa-solid fa-arrow-down-1-9" [title]="t('sort-order-title')">
|
||||
{{chapter.sortOrder}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
@ -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();
|
||||
}
|
||||
}
|
@ -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')}}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -1,17 +0,0 @@
|
||||
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
||||
<div class="pe-2">
|
||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" [styles]="{'max-height': '200px'}" [width]="imageWidth"></app-image>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="g-0">
|
||||
<h5 class="mb-0">
|
||||
<ng-content select="[title]"></ng-content>
|
||||
</h5>
|
||||
@if (summary && summary.length > 0) {
|
||||
<div class="mt-2 ps-2">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,6 +0,0 @@
|
||||
.list-item-container {
|
||||
background: var(--card-list-item-bg-color);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
@ -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 = '';
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
<ng-container *transloco="let t; read: 'list-item'">
|
||||
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
||||
<div class="pe-2">
|
||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" [styles]="{'max-height': '200px'}" [width]="imageWidth"></app-image>
|
||||
<div class="not-read-badge" *ngIf="pagesRead === 0 && totalPages > 0"></div>
|
||||
<span class="download">
|
||||
<app-download-indicator [download$]="download$"></app-download-indicator>
|
||||
</span>
|
||||
<div class="progress-banner" *ngIf="pagesRead < totalPages && totalPages > 0 && pagesRead !== totalPages"
|
||||
ngbTooltip="{{(pagesRead / totalPages) | number:'1.0-1'}}% Read">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="pagesRead" [max]="totalPages"></ngb-progressbar></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="g-0">
|
||||
<h5 class="mb-0">
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="seriesName" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
<ng-content select="[title]"></ng-content>
|
||||
<button class="btn btn-primary float-end" (click)="read.emit()">
|
||||
<span>
|
||||
<i class="fa fa-book me-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="d-none d-sm-inline-block">{{t('read')}}</span>
|
||||
</button>
|
||||
</h5>
|
||||
|
||||
<h6 class="text-muted" [ngClass]="{'subtitle-with-actionables' : actions.length > 0}" *ngIf="Title !== '' && showTitle">{{Title}}</h6>
|
||||
<ng-container *ngIf="summary.length > 0">
|
||||
<div class="mt-2 ps-2">
|
||||
<app-read-more [text]="summary" [blur]="pagesRead === 0 && blur" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="ps-2 d-none d-md-inline-block" style="width: 100%">
|
||||
<app-entity-info-cards [entity]="entity" [libraryId]="libraryId" [showExtendedProperties]="ShowExtended"></app-entity-info-cards>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
@ -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;
|
||||
}
|
@ -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<any>[] = []; // 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<void> = new EventEmitter<void>();
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
summary: string = '';
|
||||
isChapter: boolean = false;
|
||||
|
||||
|
||||
download$: Observable<DownloadEvent | null> | 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<any>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,21 +6,22 @@
|
||||
<div class="card-overlay"></div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="entity.title | safeHtml as info">
|
||||
<div class="card-body meta-title" *ngIf="info !== ''">
|
||||
<div class="card-content d-flex flex-column pt-2 pb-2 justify-content-center align-items-center text-center">
|
||||
@if (entity.title | safeHtml; as info) {
|
||||
@if (info !== '') {
|
||||
<div class="card-body meta-title">
|
||||
<div class="card-content d-flex flex-column pt-2 pb-2 justify-content-center align-items-center text-center">
|
||||
|
||||
<div class="upcoming-header"><i class="fa-regular fa-clock me-1" aria-hidden="true"></i>Upcoming</div>
|
||||
<span [innerHTML]="info"></span>
|
||||
<div class="upcoming-header"><i class="fa-regular fa-clock me-1" aria-hidden="true"></i>Upcoming</div>
|
||||
<span [innerHTML]="info"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="card-title-container">
|
||||
<span class="card-title" tabindex="0">
|
||||
{{title}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="card-title-container">
|
||||
<span class="card-title" tabindex="0">
|
||||
{{title}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
@ -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
|
||||
|
@ -62,8 +62,9 @@
|
||||
</div>
|
||||
|
||||
<div class="card-title-container">
|
||||
<span class="card-title" [ngbTooltip]="series.name" id="{{series.id}}" tabindex="0">
|
||||
<a class="dark-exempt btn-icon" routerLink="/library/{{libraryId}}/series/{{series.id}}">
|
||||
<span class="card-title" [ngbTooltip]="series.name" id="{{series.id}}">
|
||||
<app-series-format [format]="series.format"></app-series-format>
|
||||
<a class="dark-exempt btn-icon ms-1" routerLink="/library/{{libraryId}}/series/{{series.id}}">
|
||||
{{series.name}}
|
||||
</a>
|
||||
</span>
|
||||
@ -71,7 +72,7 @@
|
||||
@if (actions && actions.length > 0) {
|
||||
<span class="card-actions float-end">
|
||||
<app-card-actionables (actionHandler)="handleSeriesActionCallback($event, series)" [actions]="actions" [labelBy]="series.name"></app-card-actionables>
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -1,126 +0,0 @@
|
||||
<ng-container *transloco="let t; read: 'series-info-cards'">
|
||||
<div class="row g-0 mt-3">
|
||||
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('release-date-title')" [clickable]="false" fontClasses="fa-regular fa-calendar" [title]="t('release-year-tooltip')">
|
||||
{{seriesMetadata.releaseYear}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="seriesMetadata">
|
||||
<ng-container *ngIf="seriesMetadata.ageRating">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('age-rating-title')" [clickable]="true" fontClasses="fas fa-eye" (click)="handleGoTo(FilterField.AgeRating, seriesMetadata.ageRating)" [title]="t('age-rating-title')">
|
||||
{{this.seriesMetadata.ageRating | ageRating}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('language-title')" [clickable]="true" fontClasses="fas fa-language" (click)="handleGoTo(FilterField.Languages, seriesMetadata.language)" [title]="t('language-title')">
|
||||
{{seriesMetadata.language | defaultValue:'en' | languageName | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus">
|
||||
<app-icon-and-title [label]="t('publication-status-title')" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === t('ongoing') ? 'empty' : 'end'}}"
|
||||
(click)="handleGoTo(FilterField.PublicationStatus, seriesMetadata.publicationStatus)"
|
||||
[ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')">
|
||||
{{pubStatus}}
|
||||
</app-icon-and-title>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="vr m-2 d-none d-lg-block"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="accountService.hasValidLicense$ | async">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('scrobbling-title')" [clickable]="libraryAllowsScrobbling"
|
||||
fontClasses="fa-solid fa-tower-{{(isScrobbling && libraryAllowsScrobbling) ? 'broadcast' : 'observation'}}"
|
||||
(click)="toggleScrobbling($event)"
|
||||
[ngbTooltip]="t('scrobbling-tooltip')">
|
||||
<ng-container *ngIf="libraryAllowsScrobbling; else noScrobble">
|
||||
{{ isScrobbling ? t('on') : t('off') }}
|
||||
</ng-container>
|
||||
<ng-template #noScrobble>
|
||||
{{t('disabled')}}
|
||||
</ng-template>
|
||||
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series">
|
||||
<ng-container>
|
||||
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('format-title')" [clickable]="true"
|
||||
[fontClasses]="series.format | mangaFormatIcon"
|
||||
(click)="handleGoTo(FilterField.Formats, series.format)" [title]="t('format-title')">
|
||||
{{series.format | mangaFormat}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('last-read-title')" [clickable]="false" fontClasses="fa-regular fa-clock" [title]="t('last-read-title')">
|
||||
{{series.latestReadDate | timeAgo}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB; else showPages">
|
||||
<ng-container *ngIf="series.wordCount > 0">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{t('words-count', {num: series.wordCount | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
<ng-template #showPages>
|
||||
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||
{{t('pages-count', {num: series.pages | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('read-time-title')" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime">{{t('less-than-hour')}}</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} {{readingTime.minHours > 1 ? t('hours') : t('hour')}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="hasReadingProgress && showReadingTimeLeft && readingTimeLeft && readingTimeLeft.avgHours !== 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Time Left" [clickable]="false" fontClasses="fa-solid fa-clock">
|
||||
{{readingTimeLeft | readTimeLeft}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
<ng-content></ng-content>
|
@ -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<any>;
|
||||
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<ng-container *transloco="let t;">
|
||||
<div class="carousel-tabs-wrapper">
|
||||
<button class="scroll-button left" (click)="scroll('left')" [class.visible]="showLeftArrow">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<div class="carousel-tabs-container" #scrollContainer (scroll)="onScroll()">
|
||||
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" (navChange)="onNavChange($event)">
|
||||
@for (tab of tabComponents; track tab) {
|
||||
<li [ngbNavItem]="tab.id">
|
||||
<a ngbNavLink>{{t('tabs.' + tab.id)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<!-- <ng-container [ngTemplateOutlet]="tab.contentTemplate"></ng-container>-->
|
||||
<ng-content select="app-carousel-tab[id='{{tab.id}}']"></ng-content>
|
||||
</ng-template>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<button class="scroll-button right" (click)="scroll('right')" [class.visible]="showRightArrow">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div [ngbNavOutlet]="nav" style="min-height: 300px"></div>
|
||||
|
||||
</ng-container>
|
@ -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;
|
||||
}
|
@ -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<CarouselTabComponent>;
|
||||
|
||||
@Input({required: true}) activeTabId!: TabId;
|
||||
@Output() activeTabIdChange = new EventEmitter<TabId>();
|
||||
@Output() navChange = new EventEmitter<NgbNavChangeEvent>();
|
||||
|
||||
@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' });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -4,11 +4,16 @@
|
||||
|
||||
@if (chapter && series && libraryType !== null) {
|
||||
<div class="row mb-0 mb-xl-3 info-container">
|
||||
<div class="image-container col-5 col-sm-6 col-md-5 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mb-3 position-relative">
|
||||
<div [ngClass]="mobileSeriesImgBackground === 'true' ? 'mobile-bg' : ''" class="image-container col-5 col-sm-12 col-md-12 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mb-3 position-relative">
|
||||
|
||||
<app-image [styles]="{'object-fit': 'contain', 'background': 'none', 'max-height': '400px'}" [imageUrl]="coverImage"></app-image>
|
||||
@if (chapter.pagesRead < chapter.pages && chapter.pagesRead > 0) {
|
||||
<div class="progress-banner" ngbTooltip="{{(chapter.pagesRead / chapter.pages) * 100 | number:'1.0-1'}}%">
|
||||
@if(mobileSeriesImgBackground === 'true') {
|
||||
<app-image [styles]="{'background': 'none'}" [imageUrl]="coverImage"></app-image>
|
||||
} @else {
|
||||
<app-image [styles]="{'object-fit': 'contain', 'background': 'none', 'max-height': '400px'}" [imageUrl]="coverImage"></app-image>
|
||||
}
|
||||
<!-- TODO: For when continue on chapter/issue is hooked up -->
|
||||
@if (chapter.pagesRead < chapter.pages && chapter.pagesRead > 0) {
|
||||
<div class="progress-banner series" ngbTooltip="{{(chapter.pagesRead / chapter.pages) * 100 | number:'1.0-1'}}%">
|
||||
<ngb-progressbar type="primary" [value]="chapter.pagesRead" [max]="chapter.pages" [showValue]="true"></ngb-progressbar>
|
||||
</div>
|
||||
}
|
||||
@ -25,25 +30,22 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-xl-10 col-lg-7 col-md-7 col-xs-8 col-sm-6">
|
||||
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12">
|
||||
<h4 class="title mb-2">
|
||||
<a routerLink="/library/{{series.libraryId}}/series/{{series.id}}" class="dark-exempt btn-icon">{{series.name}}</a>
|
||||
</h4>
|
||||
<div class="subtitle mt-2 mb-2">
|
||||
<span>
|
||||
<app-entity-title [libraryType]="libraryType!" [entity]="chapter" [prioritizeTitleName]="false"></app-entity-title>
|
||||
<span class="me-2">
|
||||
<app-entity-title [libraryType]="libraryType" [entity]="chapter" [prioritizeTitleName]="true" [includeChapter]="true"></app-entity-title>
|
||||
</span>
|
||||
@if (chapter.titleName) {
|
||||
<span class="ms-2 me-2"></span>
|
||||
<span>{{chapter.titleName}}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<app-metadata-detail-row [entity]="chapter"
|
||||
[ageRating]="chapter.ageRating"
|
||||
[hasReadingProgress]="chapter.pagesRead > 0"
|
||||
[readingTimeEntity]="chapter"
|
||||
[libraryType]="libraryType">
|
||||
[libraryType]="libraryType"
|
||||
[mangaFormat]="series.format">
|
||||
</app-metadata-detail-row>
|
||||
|
||||
|
||||
@ -92,6 +94,12 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="col-auto ms-2 d-none d-md-block">
|
||||
<div class="card-actions" [ngbTooltip]="t('more-alt')">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="chapterActions" [labelBy]="series.name + ' ' + chapter.minNumber" iconClass="fa-ellipsis-h" btnClass="btn-secondary-outline btn"></app-card-actionables>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto ms-2 d-none d-md-block">
|
||||
<app-download-button [download$]="download$" [entity]="chapter" entityType="chapter"></app-download-button>
|
||||
</div>
|
||||
@ -108,7 +116,7 @@
|
||||
<div class="col-6">
|
||||
<span class="fw-bold">{{t('writers-title')}}</span>
|
||||
<div>
|
||||
<app-badge-expander [items]="chapter.writers">
|
||||
<app-badge-expander [items]="chapter.writers" [allowToggle]="false" (toggle)="switchTabsToDetail()">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openPerson(FilterField.Writers, item.id)">{{item.name}}</a>
|
||||
</ng-template>
|
||||
@ -116,11 +124,50 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<span class="fw-bold">{{t('cover-artists-title')}}</span>
|
||||
@if (chapter.releaseDate !== '0001-01-01T00:00:00' && (libraryType === LibraryType.ComicVine || libraryType === LibraryType.Comic)) {
|
||||
<span class="fw-bold">{{t('release-date-title')}}</span>
|
||||
<div>
|
||||
<a class="dark-exempt btn-icon" href="javascript:void(0);">{{chapter.releaseDate | date: 'shortDate' | defaultDate:'—'}}</a>
|
||||
</div>
|
||||
} @else {
|
||||
<span class="fw-bold">{{t('cover-artists-title')}}</span>
|
||||
<div>
|
||||
<app-badge-expander [items]="chapter.coverArtists" [allowToggle]="false" (toggle)="switchTabsToDetail()">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openPerson(FilterField.CoverArtist, item.id)">{{item.name}}</a>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mb-2 upper-details">
|
||||
<div class="row g-0">
|
||||
<div class="col-6 pe-5">
|
||||
<span class="fw-bold">{{t('genres-title')}}</span>
|
||||
<div>
|
||||
<app-badge-expander [items]="chapter.coverArtists">
|
||||
<app-badge-expander [items]="chapter.genres"
|
||||
[itemsTillExpander]="3"
|
||||
[allowToggle]="false"
|
||||
(toggle)="switchTabsToDetail()">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openPerson(FilterField.CoverArtist, item.id)">{{item.name}}</a>
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openFilter(FilterField.Genres, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<span class="fw-bold">{{t('tags-title')}}</span>
|
||||
<div>
|
||||
<app-badge-expander [items]="chapter.tags"
|
||||
[itemsTillExpander]="3"
|
||||
[allowToggle]="false"
|
||||
(toggle)="switchTabsToDetail()">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openFilter(FilterField.Tags, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
@ -131,16 +178,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <app-carousel-tabs [(activeTabId)]="activeTabId">-->
|
||||
<!-- <app-carousel-tab [id]="TabId.Details">-->
|
||||
<!-- @defer (when activeTabId === TabId.Details; prefetch on idle) {-->
|
||||
<!-- <app-details-tab [metadata]="chapter" [genres]="chapter.genres" [tags]="chapter.tags"></app-details-tab>-->
|
||||
<!-- }-->
|
||||
<!-- </app-carousel-tab>-->
|
||||
<!-- </app-carousel-tabs>-->
|
||||
|
||||
<div class="carousel-tabs-container">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" (navChange)="onNavChange($event)">
|
||||
<div class="carousel-tabs-container mb-2">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs" (navChange)="onNavChange($event)">
|
||||
|
||||
@if (showDetailsTab) {
|
||||
<li [ngbNavItem]="TabID.Details">
|
||||
|
@ -9,10 +9,9 @@ import {
|
||||
} from '@angular/core';
|
||||
import {BulkOperationsComponent} from "../cards/bulk-operations/bulk-operations.component";
|
||||
import {TagBadgeComponent} from "../shared/tag-badge/tag-badge.component";
|
||||
import {AsyncPipe, DecimalPipe, DOCUMENT, NgStyle} from "@angular/common";
|
||||
import {AsyncPipe, DecimalPipe, DOCUMENT, NgStyle, NgClass, DatePipe} from "@angular/common";
|
||||
import {CardActionablesComponent} from "../_single-module/card-actionables/card-actionables.component";
|
||||
import {CarouselReelComponent} from "../carousel/_components/carousel-reel/carousel-reel.component";
|
||||
import {ExternalListItemComponent} from "../cards/external-list-item/external-list-item.component";
|
||||
import {ExternalSeriesCardComponent} from "../cards/external-series-card/external-series-card.component";
|
||||
import {ImageComponent} from "../shared/image/image.component";
|
||||
import {LoadingComponent} from "../shared/loading/loading.component";
|
||||
@ -73,9 +72,16 @@ import {
|
||||
} from "../series-detail/_components/metadata-detail-row/metadata-detail-row.component";
|
||||
import {DownloadButtonComponent} from "../series-detail/_components/download-button/download-button.component";
|
||||
import {hasAnyCast} from "../_models/common/i-has-cast";
|
||||
import {CarouselTabComponent} from "../carousel/_components/carousel-tab/carousel-tab.component";
|
||||
import {CarouselTabsComponent, TabId} from "../carousel/_components/carousel-tabs/carousel-tabs.component";
|
||||
import {Breakpoint, UtilityService} from "../shared/_services/utility.service";
|
||||
import {EVENTS, MessageHubService} from "../_services/message-hub.service";
|
||||
import {CoverUpdateEvent} from "../_models/events/cover-update-event";
|
||||
import {ChapterRemovedEvent} from "../_models/events/chapter-removed-event";
|
||||
import {Action, ActionFactoryService, ActionItem} from "../_services/action-factory.service";
|
||||
import {Device} from "../_models/device/device";
|
||||
import {ActionService} from "../_services/action.service";
|
||||
import {PublicationStatusPipe} from "../_pipes/publication-status.pipe";
|
||||
import {DefaultDatePipe} from "../_pipes/default-date.pipe";
|
||||
import {MangaFormatPipe} from "../_pipes/manga-format.pipe";
|
||||
|
||||
enum TabID {
|
||||
Related = 'related-tab',
|
||||
@ -86,53 +92,55 @@ enum TabID {
|
||||
@Component({
|
||||
selector: 'app-chapter-detail',
|
||||
standalone: true,
|
||||
imports: [
|
||||
BulkOperationsComponent,
|
||||
AsyncPipe,
|
||||
CardActionablesComponent,
|
||||
CarouselReelComponent,
|
||||
DecimalPipe,
|
||||
ExternalListItemComponent,
|
||||
ExternalSeriesCardComponent,
|
||||
ImageComponent,
|
||||
LoadingComponent,
|
||||
NgbDropdown,
|
||||
NgbDropdownItem,
|
||||
NgbDropdownMenu,
|
||||
NgbDropdownToggle,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavLink,
|
||||
NgbProgressbar,
|
||||
NgbTooltip,
|
||||
PersonBadgeComponent,
|
||||
ReviewCardComponent,
|
||||
SeriesCardComponent,
|
||||
TagBadgeComponent,
|
||||
VirtualScrollerModule,
|
||||
NgStyle,
|
||||
AgeRatingPipe,
|
||||
TimeDurationPipe,
|
||||
ExternalRatingComponent,
|
||||
TranslocoDirective,
|
||||
ReadMoreComponent,
|
||||
NgbNavItem,
|
||||
NgbNavOutlet,
|
||||
DetailsTabComponent,
|
||||
RouterLink,
|
||||
EntityTitleComponent,
|
||||
ReadTimePipe,
|
||||
DefaultValuePipe,
|
||||
CardItemComponent,
|
||||
RelatedTabComponent,
|
||||
AgeRatingImageComponent,
|
||||
CompactNumberPipe,
|
||||
BadgeExpanderComponent,
|
||||
MetadataDetailRowComponent,
|
||||
DownloadButtonComponent,
|
||||
CarouselTabComponent,
|
||||
CarouselTabsComponent
|
||||
],
|
||||
imports: [
|
||||
BulkOperationsComponent,
|
||||
AsyncPipe,
|
||||
CardActionablesComponent,
|
||||
CarouselReelComponent,
|
||||
DecimalPipe,
|
||||
ExternalSeriesCardComponent,
|
||||
ImageComponent,
|
||||
LoadingComponent,
|
||||
NgbDropdown,
|
||||
NgbDropdownItem,
|
||||
NgbDropdownMenu,
|
||||
NgbDropdownToggle,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavLink,
|
||||
NgbProgressbar,
|
||||
NgbTooltip,
|
||||
PersonBadgeComponent,
|
||||
ReviewCardComponent,
|
||||
SeriesCardComponent,
|
||||
TagBadgeComponent,
|
||||
VirtualScrollerModule,
|
||||
NgStyle,
|
||||
NgClass,
|
||||
AgeRatingPipe,
|
||||
TimeDurationPipe,
|
||||
ExternalRatingComponent,
|
||||
TranslocoDirective,
|
||||
ReadMoreComponent,
|
||||
NgbNavItem,
|
||||
NgbNavOutlet,
|
||||
DetailsTabComponent,
|
||||
RouterLink,
|
||||
EntityTitleComponent,
|
||||
ReadTimePipe,
|
||||
DefaultValuePipe,
|
||||
CardItemComponent,
|
||||
RelatedTabComponent,
|
||||
AgeRatingImageComponent,
|
||||
CompactNumberPipe,
|
||||
BadgeExpanderComponent,
|
||||
MetadataDetailRowComponent,
|
||||
DownloadButtonComponent,
|
||||
PublicationStatusPipe,
|
||||
DatePipe,
|
||||
DefaultDatePipe,
|
||||
MangaFormatPipe
|
||||
],
|
||||
templateUrl: './chapter-detail.component.html',
|
||||
styleUrl: './chapter-detail.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@ -158,11 +166,14 @@ export class ChapterDetailComponent implements OnInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly readingListService = inject(ReadingListService);
|
||||
protected readonly utilityService = inject(UtilityService);
|
||||
|
||||
private readonly messageHub = inject(MessageHubService);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
|
||||
protected readonly AgeRating = AgeRating;
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly FilterField = FilterField;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
|
||||
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
|
||||
@ -184,7 +195,8 @@ export class ChapterDetailComponent implements OnInit {
|
||||
downloadInProgress: boolean = false;
|
||||
readingLists: ReadingList[] = [];
|
||||
showDetailsTab: boolean = true;
|
||||
|
||||
mobileSeriesImgBackground: string | undefined;
|
||||
chapterActions: Array<ActionItem<Chapter>> = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
|
||||
|
||||
|
||||
get ScrollingBlockHeight() {
|
||||
@ -208,13 +220,28 @@ export class ChapterDetailComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.mobileSeriesImgBackground = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--mobile-series-img-background').trim();
|
||||
this.seriesId = parseInt(seriesId, 10);
|
||||
this.chapterId = parseInt(chapterId, 10);
|
||||
this.libraryId = parseInt(libraryId, 10);
|
||||
|
||||
this.coverImage = this.imageService.getChapterCoverImage(this.chapterId);
|
||||
|
||||
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
|
||||
if (event.event === EVENTS.CoverUpdate) {
|
||||
const coverUpdateEvent = event.payload as CoverUpdateEvent;
|
||||
if (coverUpdateEvent.entityType === 'chapter' && coverUpdateEvent.id === this.chapterId) {
|
||||
this.themeService.refreshColorScape('chapter', coverUpdateEvent.id).subscribe();
|
||||
}
|
||||
} else if (event.event === EVENTS.ChapterRemoved) {
|
||||
const removedEvent = event.payload as ChapterRemovedEvent;
|
||||
if (removedEvent.chapterId !== this.chapterId) return;
|
||||
|
||||
// This series has been deleted from disk, redirect to series
|
||||
this.router.navigate(['library', this.libraryId, 'series', this.seriesId]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
forkJoin({
|
||||
series: this.seriesService.getSeries(this.seriesId),
|
||||
@ -303,7 +330,7 @@ export class ChapterDetailComponent implements OnInit {
|
||||
|
||||
updateUrl(activeTab: TabID) {
|
||||
const newUrl = `${this.router.url.split('#')[0]}#${activeTab}`;
|
||||
//this.router.navigateByUrl(newUrl, { onSameUrlNavigation: 'ignore' });
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
|
||||
openPerson(field: FilterField, value: number) {
|
||||
@ -311,12 +338,61 @@ export class ChapterDetailComponent implements OnInit {
|
||||
}
|
||||
|
||||
downloadChapter() {
|
||||
if (this.downloadInProgress) return;
|
||||
this.downloadService.download('chapter', this.chapter!, (d) => {
|
||||
this.downloadInProgress = !!d;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly TabId = TabId;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
openFilter(field: FilterField, value: string | number) {
|
||||
this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe();
|
||||
}
|
||||
|
||||
switchTabsToDetail() {
|
||||
this.activeTabId = TabID.Details;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Chapter>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.chapter!);
|
||||
}
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) {
|
||||
switch (action.action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.actionService.markChapterAsRead(this.libraryId, this.seriesId, chapter, () => {
|
||||
this.loadData();
|
||||
});
|
||||
break;
|
||||
case(Action.MarkAsUnread):
|
||||
this.actionService.markChapterAsUnread(this.libraryId, this.seriesId, chapter, () => {
|
||||
this.loadData();
|
||||
});
|
||||
break;
|
||||
case(Action.Edit):
|
||||
this.openEditModal();
|
||||
break;
|
||||
case(Action.AddToReadingList):
|
||||
this.actionService.addChapterToReadingList(chapter, this.seriesId, () => {/* No Operation */ });
|
||||
break;
|
||||
case(Action.IncognitoRead):
|
||||
this.readerService.readChapter(this.libraryId, this.seriesId, chapter, true);
|
||||
break;
|
||||
case (Action.SendTo):
|
||||
const device = (action._extra!.data as Device);
|
||||
this.actionService.sendToDevice([chapter.id], device);
|
||||
break;
|
||||
case Action.Download:
|
||||
this.downloadChapter();
|
||||
break;
|
||||
case Action.Delete:
|
||||
this.router.navigate(['library', this.libraryId, 'series', this.seriesId]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly LibraryType = LibraryType;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
&& series.length !== collectionTag.totalSourceCount && collectionTag.totalSourceCount > 0) {
|
||||
<div class="under-image">
|
||||
<app-image [imageUrl]="collectionTag.source | providerImage"
|
||||
width="16px" height="16px" [styles]="{'vertical-align': 'text-top'}"
|
||||
width="16px" height="16px"
|
||||
[ngbTooltip]="collectionTag.source | providerName" tabindex="0"></app-image>
|
||||
<span class="ms-2 me-2">{{t('sync-progress', {title: series.length + ' / ' + collectionTag.totalSourceCount})}}</span>
|
||||
<i class="fa-solid fa-question-circle" aria-hidden="true" [ngbTooltip]="t('last-sync', {date: collectionTag.lastSyncUtc | date: 'short' | defaultDate })"></i>
|
||||
|
@ -66,9 +66,20 @@
|
||||
<app-carousel-reel [items]="data" [title]="t('recently-updated-title')" (sectionClick)="handleSectionClick(StreamId.RecentlyUpdatedSeries)">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-card-item [entity]="item" [title]="item.seriesName" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||
[suppressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
|
||||
[suppressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"
|
||||
[showReadButton]="true" (readClicked)="handleRecentlyAddedChapterRead(item)">
|
||||
|
||||
</app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
|
||||
<ng-template #itemOverlay let-item="item">
|
||||
<span (click)="handleRecentlyAddedChapterClick(item)">
|
||||
<div>
|
||||
<i class="fa-solid fa-book" aria-hidden="true"></i>
|
||||
</div>
|
||||
</span>
|
||||
</ng-template>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
|
@ -34,6 +34,7 @@ import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.se
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {ServerService} from "../../_services/server.service";
|
||||
import {SettingsTabId} from "../../sidenav/preference-nav/preference-nav.component";
|
||||
import {ReaderService} from "../../_services/reader.service";
|
||||
|
||||
enum StreamId {
|
||||
OnDeck,
|
||||
@ -69,7 +70,7 @@ export class DashboardComponent implements OnInit {
|
||||
private readonly dashboardService = inject(DashboardService);
|
||||
private readonly scrobblingService = inject(ScrobblingService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly serverService = inject(ServerService);
|
||||
private readonly readerService = inject(ReaderService);
|
||||
|
||||
libraries$: Observable<Library[]> = this.libraryService.getLibraries().pipe(take(1), takeUntilDestroyed(this.destroyRef))
|
||||
isLoadingDashboard = true;
|
||||
@ -203,6 +204,13 @@ export class DashboardComponent implements OnInit {
|
||||
await this.router.navigate(['library', item.libraryId, 'series', item.seriesId]);
|
||||
}
|
||||
|
||||
async handleRecentlyAddedChapterRead(item: RecentlyAddedItem) {
|
||||
// Get Continue Reading point and open directly
|
||||
this.readerService.getCurrentChapter(item.seriesId).subscribe(chapter => {
|
||||
this.readerService.readChapter(item.libraryId, item.seriesId, chapter, false);
|
||||
});
|
||||
}
|
||||
|
||||
async handleFilterSectionClick(stream: DashboardStream) {
|
||||
await this.router.navigateByUrl('all-series?' + stream.smartFilterEncoded);
|
||||
}
|
||||
|
@ -5,10 +5,9 @@
|
||||
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">{{t('skip-alt')}}</a>
|
||||
|
||||
@if (navService.sideNavVisibility$ | async) {
|
||||
<a class="side-nav-toggle" (click)="hideSideNav()"><i class="fas fa-bars"></i></a>
|
||||
<a class="side-nav-toggle" (click)="toggleSideNav($event)"><i class="fas fa-bars" aria-hidden="true"></i></a>
|
||||
}
|
||||
|
||||
|
||||
<a class="navbar-brand dark-exempt" routerLink="/home" routerLinkActive="active">
|
||||
<app-image width="28px" height="28px" imageUrl="assets/images/logo-32.png" classes="logo" />
|
||||
<span class="d-none d-md-inline logo"> Kavita</span>
|
||||
|
@ -296,7 +296,8 @@ export class NavHeaderComponent implements OnInit {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
hideSideNav() {
|
||||
toggleSideNav(event: any) {
|
||||
event.stopPropagation();
|
||||
this.navService.toggleSideNav();
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<a routerLink="/settings" [fragment]="SettingsTabId.Preferences" [title]="t('settings')">{{t('settings')}}</a>
|
||||
<a routerLink="/settings" [fragment]="SettingsTabId.Preferences" (click)="closeIfOnSettings()" [title]="t('settings')">{{t('settings')}}</a>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<a routerLink="/all-filters/">{{t('all-filters')}}</a>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Component, inject, Input} from '@angular/core';
|
||||
import {WikiLink} from "../../../_models/wiki";
|
||||
import {NgbActiveModal, NgbDropdownItem} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {RouterLink} from "@angular/router";
|
||||
import {ActivatedRoute, Router, RouterLink, UrlSegment} from "@angular/router";
|
||||
import {FilterPipe} from "../../../_pipes/filter.pipe";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {Select2Module} from "ng-select2-component";
|
||||
@ -27,8 +27,10 @@ export class NavLinkModalComponent {
|
||||
@Input({required: true}) logoutFn!: () => void;
|
||||
|
||||
private readonly modal = inject(NgbActiveModal);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
protected readonly WikiLink = WikiLink;
|
||||
protected readonly SettingsTabId = SettingsTabId;
|
||||
|
||||
close() {
|
||||
this.modal.close();
|
||||
@ -38,5 +40,14 @@ export class NavLinkModalComponent {
|
||||
this.logoutFn();
|
||||
}
|
||||
|
||||
protected readonly SettingsTabId = SettingsTabId;
|
||||
closeIfOnSettings() {
|
||||
setTimeout(() => {
|
||||
const currentUrl = this.router.url;
|
||||
if (currentUrl.startsWith('/settings')) {
|
||||
this.close();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,157 +1,162 @@
|
||||
<ng-container *transloco="let t; read: 'pdf-reader'">
|
||||
<div class="{{theme}}" *ngIf="accountService.currentUser$ | async as user" #container>
|
||||
@if (accountService.currentUser$ | async; as user) {
|
||||
<div class="{{theme}}" #container>
|
||||
|
||||
<ng-container *ngIf="isLoading">
|
||||
<div class="loading mx-auto" style="min-width: 200px; width: 600px;">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
{{t('loading-message')}}
|
||||
</div>
|
||||
<div class="progress-container row g-0 align-items-center">
|
||||
<div class="progress" style="height: 5px;">
|
||||
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': loadPercent + '%'}" [attr.aria-valuenow]="loadPercent" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
@if (isLoading) {
|
||||
<div class="loading mx-auto" style="min-width: 200px; width: 600px;">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
{{t('loading-message')}}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ngx-extended-pdf-viewer
|
||||
#pdfViewer
|
||||
[src]="readerService.downloadPdf(this.chapterId)"
|
||||
[authorization]="'Bearer ' + user.token"
|
||||
height="100vh"
|
||||
[(page)]="currentPage"
|
||||
[textLayer]="true"
|
||||
[useBrowserLocale]="true"
|
||||
[showHandToolButton]="true"
|
||||
[showOpenFileButton]="false"
|
||||
[showPrintButton]="false"
|
||||
[showRotateButton]="false"
|
||||
[showDownloadButton]="false"
|
||||
[showPropertiesButton]="false"
|
||||
[(zoom)]="zoomSetting"
|
||||
[showSecondaryToolbarButton]="true"
|
||||
[showBorders]="true"
|
||||
[theme]="theme"
|
||||
[backgroundColor]="backgroundColor"
|
||||
[customToolbar]="multiToolbar"
|
||||
[language]="user.preferences.locale"
|
||||
<div class="progress-container row g-0 align-items-center">
|
||||
<div class="progress" style="height: 5px;">
|
||||
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': loadPercent + '%'}" [attr.aria-valuenow]="loadPercent" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
[(scrollMode)]="scrollMode"
|
||||
[pageViewMode]="pageLayoutMode"
|
||||
[spread]="spreadMode"
|
||||
<ngx-extended-pdf-viewer
|
||||
#pdfViewer
|
||||
[src]="readerService.downloadPdf(this.chapterId)"
|
||||
[authorization]="'Bearer ' + user.token"
|
||||
height="100vh"
|
||||
[(page)]="currentPage"
|
||||
[textLayer]="true"
|
||||
[useBrowserLocale]="true"
|
||||
[showHandToolButton]="true"
|
||||
[showOpenFileButton]="false"
|
||||
[showPrintButton]="false"
|
||||
[showRotateButton]="false"
|
||||
[showDownloadButton]="false"
|
||||
[showPropertiesButton]="false"
|
||||
[(zoom)]="zoomSetting"
|
||||
[showSecondaryToolbarButton]="true"
|
||||
[showBorders]="true"
|
||||
[theme]="theme"
|
||||
[backgroundColor]="backgroundColor"
|
||||
[customToolbar]="multiToolbar"
|
||||
[language]="user.preferences.locale"
|
||||
|
||||
(pageChange)="saveProgress()"
|
||||
(pdfLoadingStarts)="updateLoading(true)"
|
||||
(pdfLoaded)="updateLoading(false)"
|
||||
(progress)="updateLoadProgress($event)"
|
||||
(zoomChange)="calcScrollbarNeeded()"
|
||||
(handToolChange)="updateHandTool($event)"
|
||||
>
|
||||
[(scrollMode)]="scrollMode"
|
||||
[pageViewMode]="pageLayoutMode"
|
||||
[spread]="spreadMode"
|
||||
|
||||
</ngx-extended-pdf-viewer>
|
||||
(pageChange)="saveProgress()"
|
||||
(pdfLoadingStarts)="updateLoading(true)"
|
||||
(pdfLoaded)="updateLoading(false)"
|
||||
(progress)="updateLoadProgress($event)"
|
||||
(zoomChange)="calcScrollbarNeeded()"
|
||||
(handToolChange)="updateHandTool($event)"
|
||||
>
|
||||
|
||||
@if (scrollMode === ScrollModeType.page && !isLoading) {
|
||||
<div class="left" (click)="prevPage()"></div>
|
||||
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}}" (click)="nextPage()"></div>
|
||||
}
|
||||
</ngx-extended-pdf-viewer>
|
||||
|
||||
<ng-template #multiToolbar>
|
||||
<div style="min-height: 36px" id="toolbarViewer" [ngStyle]="{'background-color': backgroundColor, 'color': fontColor}">
|
||||
<div id="toolbarViewerLeft">
|
||||
<pdf-toggle-sidebar></pdf-toggle-sidebar>
|
||||
<pdf-find-button [textLayer]='true'></pdf-find-button>
|
||||
<pdf-paging-area></pdf-paging-area>
|
||||
@if (scrollMode === ScrollModeType.page && !isLoading) {
|
||||
<div class="left" (click)="prevPage()"></div>
|
||||
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}}" (click)="nextPage()"></div>
|
||||
}
|
||||
|
||||
@if (utilityService.getActiveBreakpoint() > Breakpoint.Mobile) {
|
||||
<button class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton" [ngbTooltip]="bookTitle">
|
||||
<i class="toolbar-icon fa-solid fa-info" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{bookTitle}}</span>
|
||||
<ng-template #multiToolbar>
|
||||
<div style="min-height: 36px" id="toolbarViewer" [ngStyle]="{'background-color': backgroundColor, 'color': fontColor}">
|
||||
<div id="toolbarViewerLeft">
|
||||
<pdf-toggle-sidebar></pdf-toggle-sidebar>
|
||||
<pdf-find-button [textLayer]='true'></pdf-find-button>
|
||||
<pdf-paging-area></pdf-paging-area>
|
||||
|
||||
@if (utilityService.getActiveBreakpoint() > Breakpoint.Mobile) {
|
||||
<button class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton" [ngbTooltip]="bookTitle">
|
||||
<i class="toolbar-icon fa-solid fa-info" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{bookTitle}}</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (incognitoMode) {
|
||||
<button [ngbTooltip]="t('toggle-incognito')" (click)="turnOffIncognito()" class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton">
|
||||
<i class="toolbar-icon fa fa-glasses" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('incognito-mode')}}</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<button class="btn-icon col-2 col-xs-1 mt-0 mb-0 pt-1 pb-0 toolbarButton" (click)="closeReader()" [ngbTooltip]="t('close-reader-alt')">
|
||||
<i class="toolbar-icon fa fa-times-circle" aria-hidden="true" [ngStyle]="{color: fontColor}"></i>
|
||||
<span class="visually-hidden">{{t('close-reader-alt')}}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button *ngIf="incognitoMode" [ngbTooltip]="t('toggle-incognito')" (click)="turnOffIncognito()" class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton">
|
||||
<i class="toolbar-icon fa fa-glasses" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('incognito-mode')}}</span>
|
||||
</button>
|
||||
<pdf-zoom-toolbar ></pdf-zoom-toolbar>
|
||||
|
||||
|
||||
<div id="toolbarViewerRight">
|
||||
<pdf-hand-tool></pdf-hand-tool>
|
||||
<pdf-select-tool></pdf-select-tool>
|
||||
<pdf-presentation-mode></pdf-presentation-mode>
|
||||
|
||||
<!-- The book mode is messy, not ready for prime time -->
|
||||
<!-- @if (utilityService.getActiveBreakpoint() > Breakpoint.Mobile) {-->
|
||||
<!-- <button (click)="toggleBookPageMode()" class="btn-icon toolbarButton" [ngbTooltip]="pageLayoutMode | pdfLayoutMode" [disabled]="scrollMode === ScrollModeType.page">-->
|
||||
<!-- <i class="toolbar-icon fa-solid {{this.pageLayoutMode !== 'book' ? 'fa-book' : 'fa-book-open'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>-->
|
||||
<!-- <span class="visually-hidden">{{this.pageLayoutMode | pdfLayoutMode}}</span>-->
|
||||
<!-- </button>-->
|
||||
<!-- }-->
|
||||
|
||||
|
||||
<!-- scroll mode should be disabled when book mode is used -->
|
||||
<button (click)="toggleScrollMode()" class="btn-icon toolbarButton" [ngbTooltip]="scrollMode | pdfScrollModeType" [disabled]="this.pageLayoutMode === 'book'">
|
||||
@switch (scrollMode) {
|
||||
@case (ScrollModeType.vertical) {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM11 0v.5c0 1-.5 1.5-1.5 1.5h-3C5.5 2 5 1.5 5 .5V0h6zM11 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6z"></path></svg>
|
||||
}
|
||||
@case (ScrollModeType.page) {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,7V9H12V17H14V7H10Z"></path></svg>
|
||||
}
|
||||
@case (ScrollModeType.horizontal) {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px"> <path fill="currentColor" d="M0 4h1.5c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5H0zM9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM16 4h-1.5c-1 0-1.5.5-1.5 1.5v5c0 1 .5 1.5 1.5 1.5H16z"></path> </svg>
|
||||
}
|
||||
@case (ScrollModeType.wrapped) {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M5.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C1 4.5 1.5 4 2.5 4zM7 0v.5C7 1.5 6.5 2 5.5 2h-3C1.5 2 1 1.5 1 .5V0h6zM7 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6zM13.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5c0-1 .5-1.5 1.5-1.5zM15 0v.5c0 1-.5 1.5-1.5 1.5h-3C9.5 2 9 1.5 9 .5V0h6zM15 16v-.507c0-1-.5-1.5-1.5-1.5h-3C9.5 14 9 14.5 9 15.5v.5h6z"></path></svg>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="visually-hidden">{{scrollMode | pdfScrollModeType}}</span>
|
||||
</button>
|
||||
|
||||
<button (click)="toggleSpreadMode()" class="btn-icon toolbarButton" [ngbTooltip]="spreadMode | pdfSpreadType" [disabled]="this.pageLayoutMode === 'book'">
|
||||
|
||||
@switch (spreadMode) {
|
||||
@case ('off') {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M6 3c-1 0-1.5.5-1.5 1.5v7c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5v-7c0-1-.5-1.5-1.5-1.5z"></path></svg>
|
||||
}
|
||||
@case ('odd') {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M10.56 3.5C9.56 3.5 9 4 9 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.93 1.2c.8 0 1.4.2 1.8.64.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.44-.2.3-.6.6-1 .93l-.6.4c-.4.3-.6.4-.7.55-.1.1-.2.2-.3.4h3.2v1.27h-5c0-.5.1-1 .3-1.43.2-.49.7-1 1.5-1.54.7-.5 1.1-.8 1.3-1.02.3-.3.4-.7.4-1.05 0-.3-.1-.6-.3-.77-.2-.2-.4-.3-.7-.3-.4 0-.7.2-.9.5-.1.2-.1.5-.2.9h-1.4c0-.6.2-1.1.3-1.5.4-.7 1.1-1.1 2-1.1zM1.54 3.5C.54 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.54 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.8 1.125H4.5V12H3V6.9H1.3v-1c.5 0 .8 0 .97-.03.33-.07.53-.17.73-.37.1-.2.2-.3.25-.5.05-.2.05-.3.05-.3z"></path></svg>
|
||||
}
|
||||
@case ('even') {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px"><path fill="currentColor" d="M1.5 3.5C.5 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm2 1.2c.8 0 1.4.2 1.8.6.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.4-.2.3-.5.7-1 1l-.6.4c-.4.3-.6.4-.75.56-.15.14-.25.24-.35.44H6v1.3H1c0-.6.1-1.1.3-1.5.3-.6.7-1 1.5-1.6.7-.4 1.1-.8 1.28-1 .32-.3.42-.6.42-1 0-.3-.1-.6-.23-.8-.17-.2-.37-.3-.77-.3s-.7.1-.9.5c-.04.2-.1.5-.1.9H1.1c0-.6.1-1.1.3-1.5.4-.7 1.1-1.1 2.1-1.1zM10.54 3.54C9.5 3.54 9 4 9 5v6.5c0 1 .5 1.5 1.54 1.5h4c.96 0 1.46-.5 1.46-1.5V5c0-1-.5-1.46-1.5-1.46zm1.9.95c.7 0 1.3.2 1.7.5.4.4.6.8.6 1.4 0 .4-.1.8-.4 1.1-.2.2-.3.3-.5.4.1 0 .3.1.6.3.4.3.5.8.5 1.4 0 .6-.2 1.2-.6 1.6-.4.5-1.1.7-1.9.7-1 0-1.8-.3-2.2-1-.14-.29-.24-.69-.24-1.29h1.4c0 .3 0 .5.1.7.2.4.5.5 1 .5.3 0 .5-.1.7-.3.2-.2.3-.5.3-.8 0-.5-.2-.8-.6-.95-.2-.05-.5-.15-1-.15v-1c.5 0 .8-.1 1-.14.3-.1.5-.4.5-.9 0-.3-.1-.5-.2-.7-.2-.2-.4-.3-.7-.3-.3 0-.6.1-.75.3-.2.2-.2.5-.2.86h-1.34c0-.4.1-.7.19-1.1 0-.12.2-.32.4-.62.2-.2.4-.3.7-.4.3-.1.6-.1 1-.1z"></path></svg>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="visually-hidden">{{spreadMode | pdfSpreadType}}</span>
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- This is pretty experimental, so it might not work perfectly -->
|
||||
<button (click)="toggleTheme()" class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton toolbar-btn-fix">
|
||||
<i class="toolbar-icon fa-solid {{this.theme === 'light' ? 'fa-sun' : 'fa-moon'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{this.theme === 'light' ? t('light-theme-alt') : t('dark-theme-alt')}}</span>
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="verticalToolbarSeparator hiddenSmallView"></div>
|
||||
<pdf-toggle-secondary-toolbar></pdf-toggle-secondary-toolbar>
|
||||
</div>
|
||||
|
||||
<button class="btn-icon col-2 col-xs-1 mt-0 mb-0 pt-1 pb-0 toolbarButton" (click)="closeReader()" [ngbTooltip]="t('close-reader-alt')">
|
||||
<i class="toolbar-icon fa fa-times-circle" aria-hidden="true" [ngStyle]="{color: fontColor}"></i>
|
||||
<span class="visually-hidden">{{t('close-reader-alt')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<pdf-zoom-toolbar ></pdf-zoom-toolbar>
|
||||
|
||||
|
||||
<div id="toolbarViewerRight">
|
||||
<pdf-hand-tool></pdf-hand-tool>
|
||||
<pdf-select-tool></pdf-select-tool>
|
||||
<pdf-presentation-mode></pdf-presentation-mode>
|
||||
|
||||
<!-- The book mode is messy, not ready for prime time -->
|
||||
<!-- @if (utilityService.getActiveBreakpoint() > Breakpoint.Mobile) {-->
|
||||
<!-- <button (click)="toggleBookPageMode()" class="btn-icon toolbarButton" [ngbTooltip]="pageLayoutMode | pdfLayoutMode" [disabled]="scrollMode === ScrollModeType.page">-->
|
||||
<!-- <i class="toolbar-icon fa-solid {{this.pageLayoutMode !== 'book' ? 'fa-book' : 'fa-book-open'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>-->
|
||||
<!-- <span class="visually-hidden">{{this.pageLayoutMode | pdfLayoutMode}}</span>-->
|
||||
<!-- </button>-->
|
||||
<!-- }-->
|
||||
|
||||
|
||||
<!-- scroll mode should be disabled when book mode is used -->
|
||||
<button (click)="toggleScrollMode()" class="btn-icon toolbarButton" [ngbTooltip]="scrollMode | pdfScrollModeType" [disabled]="this.pageLayoutMode === 'book'">
|
||||
@switch (scrollMode) {
|
||||
@case (ScrollModeType.vertical) {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM11 0v.5c0 1-.5 1.5-1.5 1.5h-3C5.5 2 5 1.5 5 .5V0h6zM11 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6z"></path></svg>
|
||||
}
|
||||
@case (ScrollModeType.page) {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,7V9H12V17H14V7H10Z"></path></svg>
|
||||
}
|
||||
@case (ScrollModeType.horizontal) {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px"> <path fill="currentColor" d="M0 4h1.5c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5H0zM9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM16 4h-1.5c-1 0-1.5.5-1.5 1.5v5c0 1 .5 1.5 1.5 1.5H16z"></path> </svg>
|
||||
}
|
||||
@case (ScrollModeType.wrapped) {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M5.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C1 4.5 1.5 4 2.5 4zM7 0v.5C7 1.5 6.5 2 5.5 2h-3C1.5 2 1 1.5 1 .5V0h6zM7 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6zM13.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5c0-1 .5-1.5 1.5-1.5zM15 0v.5c0 1-.5 1.5-1.5 1.5h-3C9.5 2 9 1.5 9 .5V0h6zM15 16v-.507c0-1-.5-1.5-1.5-1.5h-3C9.5 14 9 14.5 9 15.5v.5h6z"></path></svg>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="visually-hidden">{{scrollMode | pdfScrollModeType}}</span>
|
||||
</button>
|
||||
|
||||
<button (click)="toggleSpreadMode()" class="btn-icon toolbarButton" [ngbTooltip]="spreadMode | pdfSpreadType" [disabled]="this.pageLayoutMode === 'book'">
|
||||
|
||||
@switch (spreadMode) {
|
||||
@case ('off') {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M6 3c-1 0-1.5.5-1.5 1.5v7c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5v-7c0-1-.5-1.5-1.5-1.5z"></path></svg>
|
||||
}
|
||||
@case ('odd') {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px" viewBox="0 0 24 24"><path fill="currentColor" d="M10.56 3.5C9.56 3.5 9 4 9 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.93 1.2c.8 0 1.4.2 1.8.64.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.44-.2.3-.6.6-1 .93l-.6.4c-.4.3-.6.4-.7.55-.1.1-.2.2-.3.4h3.2v1.27h-5c0-.5.1-1 .3-1.43.2-.49.7-1 1.5-1.54.7-.5 1.1-.8 1.3-1.02.3-.3.4-.7.4-1.05 0-.3-.1-.6-.3-.77-.2-.2-.4-.3-.7-.3-.4 0-.7.2-.9.5-.1.2-.1.5-.2.9h-1.4c0-.6.2-1.1.3-1.5.4-.7 1.1-1.1 2-1.1zM1.54 3.5C.54 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.54 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.8 1.125H4.5V12H3V6.9H1.3v-1c.5 0 .8 0 .97-.03.33-.07.53-.17.73-.37.1-.2.2-.3.25-.5.05-.2.05-.3.05-.3z"></path></svg>
|
||||
}
|
||||
@case ('even') {
|
||||
<svg aria-hidden="true" [ngStyle]="{color: fontColor}" style="width: 24px; height: 24px; margin-top: 3px"><path fill="currentColor" d="M1.5 3.5C.5 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm2 1.2c.8 0 1.4.2 1.8.6.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.4-.2.3-.5.7-1 1l-.6.4c-.4.3-.6.4-.75.56-.15.14-.25.24-.35.44H6v1.3H1c0-.6.1-1.1.3-1.5.3-.6.7-1 1.5-1.6.7-.4 1.1-.8 1.28-1 .32-.3.42-.6.42-1 0-.3-.1-.6-.23-.8-.17-.2-.37-.3-.77-.3s-.7.1-.9.5c-.04.2-.1.5-.1.9H1.1c0-.6.1-1.1.3-1.5.4-.7 1.1-1.1 2.1-1.1zM10.54 3.54C9.5 3.54 9 4 9 5v6.5c0 1 .5 1.5 1.54 1.5h4c.96 0 1.46-.5 1.46-1.5V5c0-1-.5-1.46-1.5-1.46zm1.9.95c.7 0 1.3.2 1.7.5.4.4.6.8.6 1.4 0 .4-.1.8-.4 1.1-.2.2-.3.3-.5.4.1 0 .3.1.6.3.4.3.5.8.5 1.4 0 .6-.2 1.2-.6 1.6-.4.5-1.1.7-1.9.7-1 0-1.8-.3-2.2-1-.14-.29-.24-.69-.24-1.29h1.4c0 .3 0 .5.1.7.2.4.5.5 1 .5.3 0 .5-.1.7-.3.2-.2.3-.5.3-.8 0-.5-.2-.8-.6-.95-.2-.05-.5-.15-1-.15v-1c.5 0 .8-.1 1-.14.3-.1.5-.4.5-.9 0-.3-.1-.5-.2-.7-.2-.2-.4-.3-.7-.3-.3 0-.6.1-.75.3-.2.2-.2.5-.2.86h-1.34c0-.4.1-.7.19-1.1 0-.12.2-.32.4-.62.2-.2.4-.3.7-.4.3-.1.6-.1 1-.1z"></path></svg>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="visually-hidden">{{spreadMode | pdfSpreadType}}</span>
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- This is pretty experimental, so it might not work perfectly -->
|
||||
<button (click)="toggleTheme()" class="btn-icon mt-0 mb-0 pt-1 pb-0 toolbarButton toolbar-btn-fix">
|
||||
<i class="toolbar-icon fa-solid {{this.theme === 'light' ? 'fa-sun' : 'fa-moon'}}" [ngStyle]="{color: fontColor}" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{this.theme === 'light' ? t('light-theme-alt') : t('dark-theme-alt')}}</span>
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="verticalToolbarSeparator hiddenSmallView"></div>
|
||||
<pdf-toggle-secondary-toolbar></pdf-toggle-secondary-toolbar>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
}
|
||||
|
||||
</ng-container>
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {NgxExtendedPdfViewerModule, PageViewModeType, ProgressBarEvent, ScrollModeType} from 'ngx-extended-pdf-viewer';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {Observable, take} from 'rxjs';
|
||||
import {take} from 'rxjs';
|
||||
import {BookService} from 'src/app/book-reader/_services/book.service';
|
||||
import {Breakpoint, KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {Chapter} from 'src/app/_models/chapter';
|
||||
@ -24,7 +24,7 @@ import {CHAPTER_ID_DOESNT_EXIST, ReaderService} from 'src/app/_services/reader.s
|
||||
import {SeriesService} from 'src/app/_services/series.service';
|
||||
import {ThemeService} from 'src/app/_services/theme.service';
|
||||
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {AsyncPipe, DOCUMENT, NgIf, NgStyle} from '@angular/common';
|
||||
import {AsyncPipe, DOCUMENT, NgStyle} from '@angular/common';
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {PdfLayoutMode} from "../../../_models/preferences/pdf-layout-mode";
|
||||
import {PdfScrollMode} from "../../../_models/preferences/pdf-scroll-mode";
|
||||
@ -41,7 +41,7 @@ import {PdfSpreadTypePipe} from "../../_pipe/pdf-spread-mode.pipe";
|
||||
styleUrls: ['./pdf-reader.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgStyle, NgxExtendedPdfViewerModule, NgbTooltip, AsyncPipe, TranslocoDirective,
|
||||
imports: [NgStyle, NgxExtendedPdfViewerModule, NgbTooltip, AsyncPipe, TranslocoDirective,
|
||||
PdfLayoutModePipe, PdfScrollModeTypePipe, PdfSpreadTypePipe]
|
||||
})
|
||||
export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
|
@ -1,17 +1,18 @@
|
||||
<ng-container *transloco="let t; read: 'shortcuts-modal'">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="modal.close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 mb-2" *ngFor="let shortcut of shortcuts">
|
||||
<span><code>{{shortcut.key}}</code> {{t(shortcut.description)}}</span>
|
||||
</div>
|
||||
@for(shortcut of shortcuts; track shortcut.key) {
|
||||
<div class="col-md-6 mb-2">
|
||||
<span><code>{{shortcut.key}}</code> {{t(shortcut.description)}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
|
||||
<button type="button" class="btn btn-primary" (click)="modal.close()">{{t('close')}}</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core';
|
||||
import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
@ -17,18 +16,14 @@ export interface KeyboardShortcut {
|
||||
@Component({
|
||||
selector: 'app-shortcuts-modal',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgbModalModule, TranslocoDirective],
|
||||
imports: [NgbModalModule, TranslocoDirective],
|
||||
templateUrl: './shortcuts-modal.component.html',
|
||||
styleUrls: ['./shortcuts-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ShortcutsModalComponent {
|
||||
|
||||
protected readonly modal = inject(NgbActiveModal);
|
||||
|
||||
@Input() shortcuts: Array<KeyboardShortcut> = [];
|
||||
|
||||
constructor(public modal: NgbActiveModal) { }
|
||||
|
||||
close() {
|
||||
this.modal.close();
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@
|
||||
@if (readingList?.promoted) {
|
||||
<span class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
}
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title" *ngIf="actions.length > 0"></app-card-actionables>
|
||||
@if (actions.length > 0) {
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title"></app-card-actionables>
|
||||
}
|
||||
</h4>
|
||||
<h5 subtitle class="subtitle-with-actionables">{{t('item-count', {num: items.length | number})}}</h5>
|
||||
|
||||
@ -90,50 +92,66 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mt-2" *ngIf="readingList.startingYear !== 0">
|
||||
<h4 class="reading-list-years">
|
||||
<ng-container *ngIf="readingList.startingMonth > 0">{{(readingList.startingMonth +'/01/2020')| date:'MMM'}}</ng-container>
|
||||
<ng-container *ngIf="readingList.startingMonth > 0 && readingList.startingYear > 0">, </ng-container>
|
||||
<ng-container *ngIf="readingList.startingYear > 0">{{readingList.startingYear}}</ng-container>
|
||||
—
|
||||
<ng-container *ngIf="readingList.endingYear > 0">
|
||||
<ng-container *ngIf="readingList.endingMonth > 0">{{(readingList.endingMonth +'/01/2020') | date:'MMM'}}</ng-container>
|
||||
<ng-container *ngIf="readingList.endingMonth > 0 && readingList.endingYear > 0">, </ng-container>
|
||||
<ng-container *ngIf="readingList.endingYear > 0">{{readingList.endingYear}}</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</h4>
|
||||
</div>
|
||||
@if (readingList.startingYear !== 0) {
|
||||
<div class="row g-0 mt-2">
|
||||
<h4 class="reading-list-years">
|
||||
@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}}
|
||||
}
|
||||
}
|
||||
</h4>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
<!-- Summary row-->
|
||||
<div class="row g-0 mt-2">
|
||||
<app-read-more [text]="readingListSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
|
||||
@if (characters$ | async; as characters) {
|
||||
@if (characters && characters.length > 0) {
|
||||
<div class="row mb-2">
|
||||
<div class="row">
|
||||
<h5>{{t('characters-title')}}</h5>
|
||||
<app-badge-expander [items]="characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="goToCharacter(item)">{{item.name}}</a>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="characters$ | async as characters">
|
||||
<div class="row mb-2">
|
||||
<div class="row" *ngIf="characters && characters.length > 0">
|
||||
<h5>{{t('characters-title')}}</h5>
|
||||
<app-badge-expander [items]="characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="goToCharacter(item)">{{item.name}}</a>
|
||||
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item" (click)="goToCharacter(item)"></app-person-badge>-->
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="row mb-1 scroll-container" #scrollingBlock>
|
||||
<ng-container *ngIf="items.length === 0 && !isLoading; else loading">
|
||||
@if (items.length === 0 && !isLoading) {
|
||||
<div class="mx-auto" style="width: 200px;">
|
||||
{{t('no-data')}}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #loading>
|
||||
<app-loading *ngIf="isLoading" [loading]="isLoading"></app-loading>
|
||||
</ng-template>
|
||||
} @else if(isLoading) {
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
}
|
||||
|
||||
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
|
||||
[showRemoveButton]="false">
|
||||
|
@ -22,7 +22,6 @@
|
||||
.scroll-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: calc((var(--vh) *100) - 173px);
|
||||
margin-bottom: 10px;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<ng-container *transloco="let t; read: 'reading-list-item'">
|
||||
<div class="d-flex flex-row g-0 mb-2 reading-list-item">
|
||||
<div class="pe-2">
|
||||
<div class="d-none d-md-block pe-2">
|
||||
<app-image width="106px" [styles]="{'max-height': '125px'}" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
||||
@if (item.pagesRead === 0 && item.pagesTotal > 0) {
|
||||
<div class="not-read-badge" ></div>
|
||||
@ -16,18 +16,18 @@
|
||||
<div class="g-0">
|
||||
<h5 class="mb-1 pb-0" id="item.id--{{position}}">
|
||||
{{item.title}}
|
||||
<div class="float-end">
|
||||
<div class="actions float-end">
|
||||
<button class="btn btn-danger" (click)="remove.emit(item)">
|
||||
<span>
|
||||
<i class="fa fa-trash me-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="d-none d-sm-inline-block">{{t('remove')}}</span>
|
||||
<span class="d-none d-md-inline-block">{{t('remove')}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary ms-2" (click)="readChapter(item)">
|
||||
<span>
|
||||
<i class="fa fa-book me-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="d-none d-sm-inline-block">{{t('read')}}</span>
|
||||
<span class="d-none d-md-inline-block">{{t('read')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,46 @@
|
||||
<ng-container *transloco="let t; read: 'external-rating'">
|
||||
<div class="row g-0">
|
||||
<div class="col-auto custom-col clickable" [ngbPopover]="popContent"
|
||||
[popoverTitle]="t('kavita-tooltip')" [popoverClass]="utilityService.getActiveBreakpoint() > Breakpoint.Mobile ? 'md-popover' : 'lg-popover'">
|
||||
<span class="badge rounded-pill ps-0 me-1">
|
||||
|
||||
|
||||
@if (utilityService.activeBreakpoint$ | async; as activeBreakpoint) {
|
||||
@if (activeBreakpoint <= Breakpoint.Tablet) {
|
||||
<div class="col-auto custom-col clickable" tabindex="0" role="button" (click)="openRatingModal()">
|
||||
<ng-container [ngTemplateOutlet]="kavitaRating"></ng-container>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="col-auto custom-col clickable" [ngbPopover]="popContent"
|
||||
[popoverTitle]="t('kavita-tooltip')" popoverClass="md-popover">
|
||||
<ng-container [ngTemplateOutlet]="kavitaRating"></ng-container>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@for (rating of ratings; track rating.provider + rating.averageScore) {
|
||||
<div class="col-auto custom-col clickable" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}"
|
||||
[popoverTitle]="rating.provider | providerName" popoverClass="sm-popover">
|
||||
<span class="badge rounded-pill me-1">
|
||||
<img class="me-1" [ngSrc]="rating.provider | providerImage:true" width="24" height="24" alt="" aria-hidden="true">
|
||||
{{rating.averageScore}}%
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="col-auto" style="padding-top: 8px">
|
||||
<app-loading [loading]="isLoading" size="spinner-border-sm"></app-loading>
|
||||
</div>
|
||||
|
||||
<div class="col-auto ms-2" style="padding-top: 4px">
|
||||
@for(link of webLinks; track link) {
|
||||
<a class="me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" [title]="link">
|
||||
<app-image height="24px" width="24px" aria-hidden="true" [imageUrl]="imageService.getWebLinkImage(link)"
|
||||
[errorImage]="imageService.errorWebLinkImage"></app-image>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #kavitaRating>
|
||||
<span class="badge rounded-pill ps-0 me-1">
|
||||
<app-image classes="me-1" imageUrl="assets/images/logo-32.png" width="24px" height="24px" />
|
||||
@if (hasUserRated) {
|
||||
{{userRating * 20}}
|
||||
@ -16,45 +54,23 @@
|
||||
@if (hasUserRated || overallRating > 0) {
|
||||
%
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@for (rating of ratings; track rating.provider + rating.averageScore) {
|
||||
<div class="col-auto custom-col clickable" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}"
|
||||
[popoverTitle]="rating.provider | providerName" popoverClass="sm-popover">
|
||||
<span class="badge rounded-pill me-1">
|
||||
<img class="me-1" [ngSrc]="rating.provider | providerImage:true" width="24" height="24" alt="" aria-hidden="true">
|
||||
{{rating.averageScore}}%
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="col-auto" style="padding-top: 8px">
|
||||
<app-loading [loading]="isLoading" size="spinner-border-sm"></app-loading>
|
||||
</div>
|
||||
|
||||
<div class="col-auto ms-2">
|
||||
@for(link of webLinks; track link) {
|
||||
<a class="me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" [title]="link">
|
||||
<app-image height="24px" width="24px" aria-hidden="true" [imageUrl]="imageService.getWebLinkImage(link)"
|
||||
[errorImage]="imageService.errorWebLinkImage"></app-image>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #popContent>
|
||||
<ngx-stars [initialStars]="userRating" (ratingOutput)="updateRating($event)" [size]="utilityService.getActiveBreakpoint() > Breakpoint.Mobile ? 1 : 2"
|
||||
<ngx-stars [initialStars]="userRating" (ratingOutput)="updateRating($event)" [size]="2"
|
||||
[maxStars]="5" [color]="starColor"></ngx-stars>
|
||||
{{userRating * 20}}%
|
||||
</ng-template>
|
||||
|
||||
<ng-template #externalPopContent let-rating="rating">
|
||||
<div><i class="fa-solid fa-heart" aria-hidden="true"></i> {{rating.favoriteCount}}</div>
|
||||
<div>
|
||||
<i class="fa-solid fa-heart" aria-hidden="true"></i> {{rating.favoriteCount}}
|
||||
</div>
|
||||
|
||||
@if (rating.providerUrl) {
|
||||
<a [href]="rating.providerUrl" target="_blank" rel="noreferrer nofollow">{{t('entry-label')}}</a>
|
||||
}
|
||||
|
||||
</ng-template>
|
||||
|
||||
</ng-container>
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,10 @@
|
||||
<app-age-rating-image [rating]="ageRating"></app-age-rating-image>
|
||||
</span>
|
||||
|
||||
<span class="me-2">
|
||||
<app-series-format [format]="mangaFormat" [useTitle]="false"></app-series-format>
|
||||
</span>
|
||||
|
||||
@if (libraryType === LibraryType.Book || libraryType === LibraryType.LightNovel) {
|
||||
<span class="word-count me-3">{{t('words-count', {num: readingTimeEntity.wordCount | compactNumber})}}</span>
|
||||
} @else {
|
||||
|
@ -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;
|
||||
|
@ -4,7 +4,7 @@
|
||||
<h5>{{heading}}</h5>
|
||||
</div>
|
||||
<div class="col-lg-9 col-md-8 col-sm-12">
|
||||
<app-badge-expander [items]="tags" [itemsTillExpander]="utilityService.getActiveBreakpoint() >= Breakpoint.Desktop ? 30 : 4">
|
||||
<app-badge-expander [items]="tags" [itemsTillExpander]="utilityService.getActiveBreakpoint() >= Breakpoint.Desktop ? 30 : 4" [includeComma]="includeComma">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
@if(itemTemplate) {
|
||||
<span (click)="goTo(queryParam, item.id)">
|
||||
|
@ -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<any>;
|
||||
@ContentChild('itemTemplate') itemTemplate?: TemplateRef<any>;
|
||||
|
||||
|
@ -0,0 +1,16 @@
|
||||
<ng-container *transloco="let t; read: 'external-rating'">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('kavita-rating-title')}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-0">
|
||||
<ngx-stars [initialStars]="userRating" (ratingOutput)="updateRating($event)" [size]="2"
|
||||
[maxStars]="5" [color]="starColor"></ngx-stars>
|
||||
{{userRating * 20}}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
|
||||
</div>
|
||||
</ng-container>
|
@ -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});
|
||||
}
|
||||
}
|
@ -4,9 +4,12 @@
|
||||
@if (series && seriesMetadata && libraryType !== null) {
|
||||
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid" #scrollingBlock>
|
||||
<div class="row mb-0 mb-xl-3 info-container">
|
||||
<div class="image-container col-5 col-sm-6 col-md-5 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mb-3 position-relative">
|
||||
|
||||
<app-image [styles]="{'object-fit': 'contain', 'background': 'none', 'max-height': '400px'}" [imageUrl]="seriesImage"></app-image>
|
||||
<div [ngClass]="mobileSeriesImgBackground === 'true' ? 'mobile-bg' : ''" class="image-container col-5 col-sm-6 col-md-5 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mb-3 position-relative">
|
||||
@if(mobileSeriesImgBackground === 'true') {
|
||||
<app-image [styles]="{'background': 'none'}" [imageUrl]="seriesImage"></app-image>
|
||||
} @else {
|
||||
<app-image [styles]="{'object-fit': 'contain', 'background': 'none', 'max-height': '400px'}" [imageUrl]="seriesImage"></app-image>
|
||||
}
|
||||
@if (series.pagesRead < series.pages && hasReadingProgress) {
|
||||
<div class="progress-banner series" ngbTooltip="{{(series.pagesRead / series.pages) * 100 | number:'1.0-1'}}%">
|
||||
<ngb-progressbar type="primary" [value]="series.pagesRead" [max]="series.pages" [showValue]="true"></ngb-progressbar>
|
||||
@ -29,7 +32,7 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-xl-10 col-lg-7 col-md-7 col-xs-8 col-sm-6">
|
||||
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12">
|
||||
<h4 class="title mb-2">
|
||||
<span>{{series.name}}
|
||||
|
||||
@ -52,7 +55,8 @@
|
||||
[ageRating]="seriesMetadata.ageRating"
|
||||
[hasReadingProgress]="hasReadingProgress"
|
||||
[readingTimeEntity]="series"
|
||||
[libraryType]="libraryType">
|
||||
[libraryType]="libraryType"
|
||||
[mangaFormat]="series.format">
|
||||
</app-metadata-detail-row>
|
||||
|
||||
<!-- Rating goes here (after I implement support for rating individual issues -->
|
||||
@ -133,12 +137,13 @@
|
||||
|
||||
<div class="mt-2 upper-details">
|
||||
<div class="row g-0">
|
||||
<div class="col-6">
|
||||
<div class="col-6 pe-5">
|
||||
<span class="fw-bold">{{t('writers-title')}}</span>
|
||||
<div>
|
||||
<app-badge-expander [items]="seriesMetadata.writers"
|
||||
[itemsTillExpander]="3"
|
||||
[allowToggle]="false">
|
||||
[allowToggle]="false"
|
||||
(toggle)="switchTabsToDetail()">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openFilter(FilterField.Writers, item.id)">{{item.name}}</a>
|
||||
</ng-template>
|
||||
@ -162,12 +167,13 @@
|
||||
|
||||
<div class="mt-3 mb-2 upper-details">
|
||||
<div class="row g-0">
|
||||
<div class="col-6">
|
||||
<div class="col-6 pe-5">
|
||||
<span class="fw-bold">{{t('genres-title')}}</span>
|
||||
<div>
|
||||
<app-badge-expander [items]="seriesMetadata.genres"
|
||||
[itemsTillExpander]="3"
|
||||
[allowToggle]="false">
|
||||
[allowToggle]="false"
|
||||
(toggle)="switchTabsToDetail()">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openFilter(FilterField.Genres, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
@ -180,7 +186,8 @@
|
||||
<div>
|
||||
<app-badge-expander [items]="seriesMetadata.tags"
|
||||
[itemsTillExpander]="3"
|
||||
[allowToggle]="false">
|
||||
[allowToggle]="false"
|
||||
(toggle)="switchTabsToDetail()">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openFilter(FilterField.Tags, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
@ -189,45 +196,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- <div class="mt-3 mb-2">-->
|
||||
<!-- <div class="row g-0">-->
|
||||
<!-- <div class="col-6">-->
|
||||
<!-- <span class="fw-bold">{{t('weblinks-title')}}</span>-->
|
||||
<!-- <div>-->
|
||||
<!-- @for(link of WebLinks; track link) {-->
|
||||
<!-- <a class="me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" [title]="link">-->
|
||||
<!-- <app-image height="24px" width="24px" aria-hidden="true" [imageUrl]="imageService.getWebLinkImage(link)"-->
|
||||
<!-- [errorImage]="imageService.errorWebLinkImage"></app-image>-->
|
||||
<!-- </a>-->
|
||||
<!-- } @empty {-->
|
||||
<!-- {{null | defaultValue}}-->
|
||||
<!-- }-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- <div class="col-6">-->
|
||||
<!-- <span class="fw-bold">{{t('publication-status-title')}}</span>-->
|
||||
<!-- <div>-->
|
||||
<!-- @if (seriesMetadata.publicationStatus | publicationStatus; as pubStatus) {-->
|
||||
<!-- <a class="dark-exempt btn-icon" (click)="openFilter(FilterField.PublicationStatus, seriesMetadata.publicationStatus)"-->
|
||||
<!-- href="javascript:void(0);"-->
|
||||
<!-- [ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')">-->
|
||||
<!-- {{pubStatus}}-->
|
||||
<!-- </a>-->
|
||||
<!-- }-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="carousel-tabs-container">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" [destroyOnHide]="false" (navChange)="onNavChange($event)">
|
||||
<div class="carousel-tabs-container mb-2">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs" [destroyOnHide]="false" (navChange)="onNavChange($event)">
|
||||
|
||||
@if (showStorylineTab) {
|
||||
<li [ngbNavItem]="TabID.Storyline">
|
||||
@ -316,138 +289,16 @@
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (hasRelations && relationShips) {
|
||||
@if (hasRelations || readingLists.length > 0 || collections.length > 0) {
|
||||
<li [ngbNavItem]="TabID.Related">
|
||||
<a ngbNavLink>
|
||||
{{t(TabID.Related)}}
|
||||
<span class="badge rounded-pill text-bg-secondary">{{relations.length}}</span>
|
||||
<span class="badge rounded-pill text-bg-secondary">{{relations.length + readingLists.length + collections.length}}</span>
|
||||
</a>
|
||||
<ng-template ngbNavContent>
|
||||
@defer (when activeTabId === TabID.Related; prefetch on idle) {
|
||||
<virtual-scroller #scroll [items]="relations" [parentScroll]="scrollingBlock" [childHeight]="1">
|
||||
<div class="card-container row g-0" #container>
|
||||
@for(item of scroll.viewPortItems; let idx = $index; track item.id) {
|
||||
<app-series-card class="col-auto mt-2 mb-2" [series]="item.series" [libraryId]="item.series.libraryId" [relation]="item.relation"></app-series-card>
|
||||
}
|
||||
</div>
|
||||
</virtual-scroller>
|
||||
<app-related-tab [readingLists]="readingLists" [collections]="collections" [relations]="relations"></app-related-tab>
|
||||
}
|
||||
|
||||
<!-- @if (relationShips.prequels.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.prequels" title="Prequels">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.Prequel"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.sequels.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.sequels" title="Sequels">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.Sequel"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.parent.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.parent" title="Parent">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.Parent"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.contains.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.contains" title="Contains">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.Contains"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.adaptations.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.adaptations" title="Contains">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.Adaptation"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.annuals.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.annuals" title="Annuals">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.Annual"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
|
||||
<!-- @if (relationShips.characters.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.characters" title="Characters">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.Character"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.alternativeSettings.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.alternativeSettings" title="Alternative Settings">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.AlternativeSetting"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.doujinshis.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.doujinshis" title="Doujinshis">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.Doujinshi"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.alternativeVersions.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.alternativeVersions" title="Alternative Versions">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.AlternativeVersion"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.editions.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.editions" title="Editions">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.Edition"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.others.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.others" title="Other">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.Other"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- @if (relationShips.sideStories.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.sideStories" title="Side Stories">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.SideStory"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
|
||||
<!-- @if (relationShips.spinOffs.length > 0) {-->
|
||||
<!-- <app-carousel-reel [items]="relationShips.spinOffs" title="Spin Offs">-->
|
||||
<!-- <ng-template #carouselItem let-item>-->
|
||||
<!-- <app-series-card class="col-auto mt-2 mb-2" [series]="item" [libraryId]="item.libraryId" [relation]="RelationKind.SpinOff"></app-series-card>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-carousel-reel>-->
|
||||
<!-- }-->
|
||||
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
}
|
||||
|
@ -7,19 +7,7 @@
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.under-image {
|
||||
background-color: var(--breadcrumb-bg-color);
|
||||
color: white;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
//
|
||||
//.rating-star {
|
||||
// margin-top: 2px;
|
||||
// font-size: 1.5rem;
|
||||
//}
|
||||
//
|
||||
|
||||
.card-container{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 160px);
|
||||
@ -27,7 +15,7 @@
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
::ng-deep .carousel-container .header i.fa-plus, ::ng-deep .carousel-container .header i.fa-pen{
|
||||
::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;
|
||||
@ -40,13 +28,4 @@
|
||||
}
|
||||
}
|
||||
|
||||
.upper-details {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@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%);
|
||||
}
|
||||
}
|
||||
|
@ -98,10 +98,6 @@ import {
|
||||
} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
|
||||
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
|
||||
import {ExternalSeries} from "../../../_models/series-detail/external-series";
|
||||
import {
|
||||
SeriesPreviewDrawerComponent
|
||||
} from "../../../_single-module/series-preview-drawer/series-preview-drawer.component";
|
||||
import {PublicationStatus} from "../../../_models/metadata/publication-status";
|
||||
import {NextExpectedChapter} from "../../../_models/series-detail/next-expected-chapter";
|
||||
import {NextExpectedCardComponent} from "../../../cards/next-expected-card/next-expected-card.component";
|
||||
@ -143,11 +139,13 @@ import {MetadataDetailRowComponent} from "../metadata-detail-row/metadata-detail
|
||||
import {DownloadButtonComponent} from "../download-button/download-button.component";
|
||||
import {hasAnyCast} from "../../../_models/common/i-has-cast";
|
||||
import {EditVolumeModalComponent} from "../../../_single-module/edit-volume-modal/edit-volume-modal.component";
|
||||
import {CoverUpdateEvent} from "../../../_models/events/cover-update-event";
|
||||
import {RelatedSeriesPair, RelatedTabComponent} from "../../../_single-modules/related-tab/related-tab.component";
|
||||
import {CollectionTagService} from "../../../_services/collection-tag.service";
|
||||
import {UserCollection} from "../../../_models/collection-tag";
|
||||
import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component";
|
||||
import {MangaFormatPipe} from "../../../_pipes/manga-format.pipe";
|
||||
|
||||
interface RelatedSeriesPair {
|
||||
series: Series;
|
||||
relation: RelationKind;
|
||||
}
|
||||
|
||||
enum TabID {
|
||||
Related = 'related-tab',
|
||||
@ -176,12 +174,12 @@ interface StoryLineItem {
|
||||
TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu,
|
||||
NgbDropdownItem, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent,
|
||||
NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, CardItemComponent,
|
||||
EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, NgbNavOutlet,
|
||||
EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, NgbNavOutlet,
|
||||
LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NextExpectedCardComponent,
|
||||
NgClass, NgOptimizedImage, ProviderImagePipe, AsyncPipe, PersonBadgeComponent, DetailsTabComponent, ChapterCardComponent,
|
||||
VolumeCardComponent, JsonPipe, AgeRatingPipe, DefaultValuePipe, ExternalRatingComponent, ReadMoreComponent, ReadTimePipe,
|
||||
RouterLink, TimeAgoPipe, AgeRatingImageComponent, CompactNumberPipe, IconAndTitleComponent, SafeHtmlPipe, BadgeExpanderComponent,
|
||||
A11yClickDirective, ReadTimeLeftPipe, PublicationStatusPipe, MetadataDetailRowComponent, DownloadButtonComponent]
|
||||
A11yClickDirective, ReadTimeLeftPipe, PublicationStatusPipe, MetadataDetailRowComponent, DownloadButtonComponent, RelatedTabComponent, SeriesFormatComponent, MangaFormatPipe]
|
||||
})
|
||||
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
@ -200,10 +198,9 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly messageHub = inject(MessageHubService);
|
||||
private readonly readingListService = inject(ReadingListService);
|
||||
private readonly offcanvasService = inject(NgbOffcanvas);
|
||||
private readonly collectionTagService = inject(CollectionTagService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly scrollService = inject(ScrollService);
|
||||
private readonly deviceService = inject(DeviceService);
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
protected readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
protected readonly utilityService = inject(UtilityService);
|
||||
@ -243,6 +240,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
isLoadingExtra = false;
|
||||
libraryAllowsScrobbling = false;
|
||||
isScrobbling: boolean = true;
|
||||
mobileSeriesImgBackground: string | undefined;
|
||||
|
||||
currentlyReadingChapter: Chapter | undefined = undefined;
|
||||
hasReadingProgress = false;
|
||||
@ -262,6 +260,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
libraryType: LibraryType = LibraryType.Manga;
|
||||
seriesMetadata: SeriesMetadata | null = null;
|
||||
readingLists: Array<ReadingList> = [];
|
||||
collections: Array<UserCollection> = [];
|
||||
isWantToRead: boolean = false;
|
||||
unreadCount: number = 0;
|
||||
totalCount: number = 0;
|
||||
@ -384,6 +383,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get UseBookLogic() {
|
||||
return this.libraryType === LibraryType.Book || this.libraryType === LibraryType.LightNovel;
|
||||
}
|
||||
@ -472,6 +472,9 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mobileSeriesImgBackground = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--mobile-series-img-background').trim();
|
||||
|
||||
|
||||
// Set up the download in progress
|
||||
this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => {
|
||||
@ -486,12 +489,15 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
this.router.navigateByUrl('/home');
|
||||
}
|
||||
} else if (event.event === EVENTS.ScanSeries) {
|
||||
const seriesCoverUpdatedEvent = event.payload as ScanSeriesEvent;
|
||||
if (seriesCoverUpdatedEvent.seriesId === this.seriesId) {
|
||||
const seriesScanEvent = event.payload as ScanSeriesEvent;
|
||||
if (seriesScanEvent.seriesId === this.seriesId) {
|
||||
this.loadSeries(this.seriesId);
|
||||
}
|
||||
} else if (event.event === EVENTS.CoverUpdate) {
|
||||
this.themeService.refreshColorScape('series', this.seriesId).subscribe();
|
||||
const coverUpdateEvent = event.payload as CoverUpdateEvent;
|
||||
if (coverUpdateEvent.id === this.seriesId) {
|
||||
this.themeService.refreshColorScape('series', this.seriesId).subscribe();
|
||||
}
|
||||
} else if (event.event === EVENTS.ChapterRemoved) {
|
||||
const removedEvent = event.payload as ChapterRemovedEvent;
|
||||
if (removedEvent.seriesId !== this.seriesId) return;
|
||||
@ -554,13 +560,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
updateUrl(activeTab: TabID) {
|
||||
var tokens = this.router.url.split('#');
|
||||
const newUrl = `${tokens[0]}#${activeTab}`;
|
||||
|
||||
// if (tokens.length === 1 || tokens[1] === activeTab + '') {
|
||||
// return;
|
||||
// }
|
||||
console.log('url:', newUrl);
|
||||
|
||||
//this.router.navigateByUrl(newUrl, { skipLocationChange: true, replaceUrl: true });
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
|
||||
handleSeriesActionCallback(action: ActionItem<Series>, series: Series) {
|
||||
@ -580,10 +580,10 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
this.actionService.scanSeries(series);
|
||||
break;
|
||||
case(Action.RefreshMetadata):
|
||||
this.actionService.refreshSeriesMetadata(series, undefined, true);
|
||||
this.actionService.refreshSeriesMetadata(series, undefined, true, false);
|
||||
break;
|
||||
case(Action.GenerateColorScape):
|
||||
this.actionService.refreshSeriesMetadata(series, undefined, false);
|
||||
this.actionService.refreshSeriesMetadata(series, undefined, false, true);
|
||||
break;
|
||||
case(Action.Delete):
|
||||
this.deleteSeries(series);
|
||||
@ -673,13 +673,9 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
this.openChapter(chapter, true);
|
||||
break;
|
||||
case (Action.SendTo):
|
||||
{
|
||||
const device = (action._extra!.data as Device);
|
||||
this.deviceService.sendTo([chapter.id], device.id).subscribe(() => {
|
||||
this.toastr.success(this.translocoService.translate('series-detail.send-to', {deviceName: device.name}));
|
||||
});
|
||||
break;
|
||||
}
|
||||
const device = (action._extra!.data as Device);
|
||||
this.actionService.sendToDevice([chapter.id], device);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -726,6 +722,11 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.collectionTagService.allCollectionsForSeries(seriesId, false).subscribe(tags => {
|
||||
this.collections = tags;
|
||||
this.cdRef.markForCheck();
|
||||
})
|
||||
|
||||
this.readerService.getTimeLeft(seriesId).subscribe((timeLeft) => {
|
||||
this.readingTimeLeft = timeLeft;
|
||||
this.cdRef.markForCheck();
|
||||
@ -1147,23 +1148,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
previewSeries(item: Series | ExternalSeries, isExternal: boolean) {
|
||||
const ref = this.offcanvasService.open(SeriesPreviewDrawerComponent, {position: 'end', panelClass: ''});
|
||||
ref.componentInstance.isExternalSeries = isExternal;
|
||||
ref.componentInstance.name = item.name;
|
||||
|
||||
if (isExternal) {
|
||||
const external = item as ExternalSeries;
|
||||
ref.componentInstance.aniListId = external.aniListId;
|
||||
ref.componentInstance.malId = external.malId;
|
||||
} else {
|
||||
const local = item as Series;
|
||||
ref.componentInstance.seriesId = local.id;
|
||||
ref.componentInstance.libraryId = local.libraryId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
openFilter(field: FilterField, value: string | number) {
|
||||
this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe();
|
||||
}
|
||||
@ -1183,4 +1167,9 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switchTabsToDetail() {
|
||||
this.activeTabId = TabID.Details;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,7 @@
|
||||
<ng-container *transloco="let t;">
|
||||
<div class="container-fluid">
|
||||
|
||||
|
||||
<ng-content></ng-content>
|
||||
|
||||
|
||||
|
||||
|
||||
@if (subtitle) {
|
||||
<div class="description text-muted" [innerHTML]="subtitle | safeHtml"></div>
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<ng-container *transloco="let t;">
|
||||
<div class="container-fluid">
|
||||
<div class="row g-0">
|
||||
<div class="col-11">
|
||||
<div class="col-10">
|
||||
<h6 class="section-title">
|
||||
@if(labelId) {
|
||||
<label class="reset-label" [for]="labelId">{{title}}</label>
|
||||
@ -13,9 +13,9 @@
|
||||
}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<div class="col-2 text-end align-self-end justify-content-end">
|
||||
@if (showEdit) {
|
||||
<button class="btn btn-text btn-sm" (click)="toggleEditMode()" [disabled]="!canEdit">
|
||||
<button type="button" class="btn btn-text btn-sm" (click)="toggleEditMode()" [disabled]="!canEdit">
|
||||
{{isEditMode ? t('common.close') : (editLabel || t('common.edit'))}}
|
||||
</button>
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
<ng-container *transloco="let t;">
|
||||
<div class="container-fluid">
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-11">
|
||||
<div class="col-10">
|
||||
<h6 class="section-title" [id]="id || title">{{title}}
|
||||
@if (titleExtraRef) {
|
||||
<ng-container [ngTemplateOutlet]="titleExtraRef"></ng-container>
|
||||
}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<button class="btn btn-text btn-sm" (click)="toggleViewMode()" [disabled]="!canEdit">{{isEditMode ? t('common.close') : t('common.edit')}}</button>
|
||||
<div class="col-2 text-end align-self-end justify-content-end">
|
||||
<button type="button" class="btn btn-text btn-sm" (click)="toggleViewMode()" [disabled]="!canEdit">{{isEditMode ? t('common.close') : t('common.edit')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,7 +34,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.Users; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Users) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-manage-users></app-manage-users>
|
||||
</div>
|
||||
}
|
||||
@ -42,7 +42,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.Libraries; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Libraries) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-manage-library></app-manage-library>
|
||||
</div>
|
||||
}
|
||||
@ -50,7 +50,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.MediaIssues; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.MediaIssues) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-manage-media-issues></app-manage-media-issues>
|
||||
</div>
|
||||
}
|
||||
@ -58,7 +58,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.System; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.System) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-manage-system></app-manage-system>
|
||||
</div>
|
||||
}
|
||||
@ -66,7 +66,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.Statistics; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Statistics) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-server-stats></app-server-stats>
|
||||
</div>
|
||||
}
|
||||
@ -74,7 +74,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.Tasks; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Tasks) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-manage-tasks-settings></app-manage-tasks-settings>
|
||||
</div>
|
||||
}
|
||||
@ -82,7 +82,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.KavitaPlus; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.KavitaPlus) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-manage-kavitaplus></app-manage-kavitaplus>
|
||||
</div>
|
||||
}
|
||||
@ -114,7 +114,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.Customize; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Customize) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-manage-customization></app-manage-customization>
|
||||
</div>
|
||||
}
|
||||
@ -130,7 +130,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.Theme; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Theme) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-theme-manager></app-theme-manager>
|
||||
</div>
|
||||
}
|
||||
@ -138,7 +138,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.Devices; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Devices) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-manage-devices></app-manage-devices>
|
||||
</div>
|
||||
}
|
||||
@ -146,7 +146,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.UserStats; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.UserStats) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-user-stats></app-user-stats>
|
||||
</div>
|
||||
}
|
||||
@ -154,7 +154,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.CBLImport; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.CBLImport) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-import-cbl></app-import-cbl>
|
||||
</div>
|
||||
}
|
||||
@ -162,7 +162,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.Scrobbling; prefetch on idle) {
|
||||
@if(hasActiveLicense && fragment === SettingsTabId.Scrobbling) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-manage-scrobling></app-manage-scrobling>
|
||||
</div>
|
||||
}
|
||||
@ -170,7 +170,7 @@
|
||||
|
||||
@defer (when fragment === SettingsTabId.MALStackImport; prefetch on idle) {
|
||||
@if(hasActiveLicense && fragment === SettingsTabId.MALStackImport) {
|
||||
<div class="col-md-12">
|
||||
<div class="scale col-md-12">
|
||||
<app-import-mal-collection></app-import-mal-collection>
|
||||
</div>
|
||||
}
|
||||
|
@ -2,3 +2,9 @@ h2 {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
::ng-deep .content-wrapper:not(.closed) {
|
||||
.scale {
|
||||
width: calc(100dvw - 200px) !important;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user