mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
All Around Polish (#1328)
* Added --card-list-item-bg-color for the card list items * Updated the card list item progress to match how cards render * Implemented the ability to configure how many backups are retained. * Fixed a bug where odd jump keys could cause a bad index error for jump bar * Commented out more code for the pagination route if we go with that. * Reverted a move of DisableConcurrentExecution to interface, as it seems to not work there. * Updated manga format utility code to pipes * Fixed bulk selection on series detail page * Fixed bulk selection on all other pages * Changed card item to OnPush * Updated image component to OnPush * Updated Series Card to OnPush * Updated Series Detail to OnPush * Lots of changes here. Integrated parentscroll support on card detail layout. Added jump bar (custom js implementation) on collection, reading list and all series pages. Updated UserParams to default to no pagination. Lots of cleanup all around * Updated some notes on a module use * Some code cleanup * Fixed up a broken test due to the mapper not being configured in the test. * Applied TabID pattern to edit collection tags * Applied css from series detail to collection detail page to remove double scrollbar * Implemented the ability to sort by Time To Read. * Throw an error to the UI when we extract an archive and it contains invalid characters in the filename for the Server OS. * Tweaked how the page scrolls for jumpbar on collection detail. We will have to polish another release * Cleaned up the styling on directory picker * Put some code in but it doesn't work for scroll to top on virtual scrolling. I'll do it later. * Fixed a container bug
This commit is contained in:
parent
2ed0aca866
commit
f54eb5865b
@ -6,8 +6,11 @@ using System.IO.Abstractions.TestingHelpers;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
|
using API.DTOs.Settings;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.Helpers.Converters;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
@ -48,7 +51,10 @@ public class CleanupServiceTests
|
|||||||
_context = new DataContext(contextOptions);
|
_context = new DataContext(contextOptions);
|
||||||
Task.Run(SeedDb).GetAwaiter().GetResult();
|
Task.Run(SeedDb).GetAwaiter().GetResult();
|
||||||
|
|
||||||
_unitOfWork = new UnitOfWork(_context, Substitute.For<IMapper>(), null);
|
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
||||||
|
var mapper = config.CreateMapper();
|
||||||
|
|
||||||
|
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Setup
|
#region Setup
|
||||||
|
@ -72,8 +72,9 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
||||||
|
return Ok(items);
|
||||||
|
|
||||||
return Ok(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()));
|
//return Ok(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -463,7 +464,7 @@ namespace API.Controllers
|
|||||||
|
|
||||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||||
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds))
|
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds))
|
||||||
.OrderBy(c => float.Parse(c.Volume.Name))
|
.OrderBy(c => Parser.Parser.MinNumberFromRange(c.Volume.Name))
|
||||||
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
||||||
|
|
||||||
var index = lastOrder + 1;
|
var index = lastOrder + 1;
|
||||||
|
@ -118,7 +118,7 @@ namespace API.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files, "logs");
|
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files, "logs");
|
||||||
return File(fileBytes, "application/zip", Path.GetFileName(zipPath));
|
return File(fileBytes, "application/zip", Path.GetFileName(zipPath), true);
|
||||||
}
|
}
|
||||||
catch (KavitaException ex)
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
|
@ -54,9 +54,6 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||||
{
|
{
|
||||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
// TODO: Is this needed as it gets updated in the DB on startup
|
|
||||||
settingsDto.Port = Configuration.Port;
|
|
||||||
settingsDto.LoggingLevel = Configuration.LogLevel;
|
|
||||||
return Ok(settingsDto);
|
return Ok(settingsDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,6 +209,16 @@ namespace API.Controllers
|
|||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.TotalBackups && updateSettingsDto.TotalBackups + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
|
||||||
|
{
|
||||||
|
return BadRequest("Total Backups must be between 1 and 30");
|
||||||
|
}
|
||||||
|
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
||||||
{
|
{
|
||||||
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
||||||
|
@ -2,8 +2,24 @@
|
|||||||
|
|
||||||
public enum SortField
|
public enum SortField
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sort Name of Series
|
||||||
|
/// </summary>
|
||||||
SortName = 1,
|
SortName = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// Date entity was created/imported into Kavita
|
||||||
|
/// </summary>
|
||||||
CreatedDate = 2,
|
CreatedDate = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// Date entity was last modified (tag update, etc)
|
||||||
|
/// </summary>
|
||||||
LastModifiedDate = 3,
|
LastModifiedDate = 3,
|
||||||
LastChapterAdded = 4
|
/// <summary>
|
||||||
|
/// Date series had a chapter added to it
|
||||||
|
/// </summary>
|
||||||
|
LastChapterAdded = 4,
|
||||||
|
/// <summary>
|
||||||
|
/// Time it takes to read. Uses Average.
|
||||||
|
/// </summary>
|
||||||
|
TimeToRead = 5
|
||||||
}
|
}
|
||||||
|
@ -10,5 +10,9 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Promoted { get; set; }
|
public bool Promoted { get; set; }
|
||||||
public bool CoverImageLocked { get; set; }
|
public bool CoverImageLocked { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This is used to tell the UI if it should request a Cover Image or not. If null or empty, it has not been set.
|
||||||
|
/// </summary>
|
||||||
|
public string CoverImage { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using API.Services;
|
using System.Collections.Generic;
|
||||||
|
using API.Services;
|
||||||
|
|
||||||
namespace API.DTOs.Settings
|
namespace API.DTOs.Settings
|
||||||
{
|
{
|
||||||
@ -44,5 +45,11 @@ namespace API.DTOs.Settings
|
|||||||
/// If the Swagger UI Should be exposed. Does not require authentication, but does require a JWT.
|
/// If the Swagger UI Should be exposed. Does not require authentication, but does require a JWT.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableSwaggerUi { get; set; }
|
public bool EnableSwaggerUi { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The amount of Backups before cleanup
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Value should be between 1 and 30</remarks>
|
||||||
|
public int TotalBackups { get; set; } = 30;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -753,6 +753,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
||||||
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
||||||
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
||||||
|
SortField.TimeToRead => query.OrderBy(s => s.AvgHoursToRead),
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -764,6 +765,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
||||||
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
||||||
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
||||||
|
SortField.TimeToRead => query.OrderByDescending(s => s.AvgHoursToRead),
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using API.DTOs.Settings;
|
|||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Data.Repositories;
|
namespace API.Data.Repositories;
|
||||||
|
@ -102,6 +102,7 @@ namespace API.Data
|
|||||||
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
||||||
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
|
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
|
||||||
new() {Key = ServerSettingKey.EnableSwaggerUi, Value = "false"},
|
new() {Key = ServerSettingKey.EnableSwaggerUi, Value = "false"},
|
||||||
|
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
|
||||||
}.ToArray());
|
}.ToArray());
|
||||||
|
|
||||||
foreach (var defaultSetting in DefaultSettings)
|
foreach (var defaultSetting in DefaultSettings)
|
||||||
|
@ -86,5 +86,10 @@ namespace API.Entities.Enums
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("EnableSwaggerUi")]
|
[Description("EnableSwaggerUi")]
|
||||||
EnableSwaggerUi = 15,
|
EnableSwaggerUi = 15,
|
||||||
|
/// <summary>
|
||||||
|
/// Total Number of Backups to maintain before cleaning. Default 30, min 1.
|
||||||
|
/// </summary>
|
||||||
|
[Description("TotalBackups")]
|
||||||
|
TotalBackups = 16,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ namespace API.Extensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
|
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
|
||||||
IWebHostEnvironment env)
|
IHostEnvironment env)
|
||||||
{
|
{
|
||||||
services.AddDbContext<DataContext>(options =>
|
services.AddDbContext<DataContext>(options =>
|
||||||
{
|
{
|
||||||
|
@ -138,7 +138,8 @@ namespace API.Helpers
|
|||||||
|
|
||||||
CreateMap<RegisterDto, AppUser>();
|
CreateMap<RegisterDto, AppUser>();
|
||||||
|
|
||||||
|
CreateMap<IList<ServerSetting>, ServerSettingDto>()
|
||||||
|
.ConvertUsing<ServerSettingConverter>();
|
||||||
|
|
||||||
CreateMap<IEnumerable<ServerSetting>, ServerSettingDto>()
|
CreateMap<IEnumerable<ServerSetting>, ServerSettingDto>()
|
||||||
.ConvertUsing<ServerSettingConverter>();
|
.ConvertUsing<ServerSettingConverter>();
|
||||||
|
@ -54,6 +54,9 @@ namespace API.Helpers.Converters
|
|||||||
case ServerSettingKey.EnableSwaggerUi:
|
case ServerSettingKey.EnableSwaggerUi:
|
||||||
destination.EnableSwaggerUi = bool.Parse(row.Value);
|
destination.EnableSwaggerUi = bool.Parse(row.Value);
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.TotalBackups:
|
||||||
|
destination.TotalBackups = int.Parse(row.Value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{
|
{
|
||||||
private const int MaxPageSize = int.MaxValue;
|
private const int MaxPageSize = int.MaxValue;
|
||||||
public int PageNumber { get; init; } = 1;
|
public int PageNumber { get; init; } = 1;
|
||||||
private readonly int _pageSize = 30;
|
private readonly int _pageSize = MaxPageSize;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If set to 0, will set as MaxInt
|
/// If set to 0, will set as MaxInt
|
||||||
|
@ -412,7 +412,6 @@ namespace API.Services
|
|||||||
|
|
||||||
private void ExtractArchiveEntries(ZipArchive archive, string extractPath)
|
private void ExtractArchiveEntries(ZipArchive archive, string extractPath)
|
||||||
{
|
{
|
||||||
// TODO: In cases where we try to extract, but there are InvalidPathChars, we need to inform the user (throw exception, let middleware inform user)
|
|
||||||
var needsFlattening = ArchiveNeedsFlattening(archive);
|
var needsFlattening = ArchiveNeedsFlattening(archive);
|
||||||
if (!archive.HasFiles() && !needsFlattening) return;
|
if (!archive.HasFiles() && !needsFlattening) return;
|
||||||
|
|
||||||
@ -476,7 +475,8 @@ namespace API.Services
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(e, "[ExtractArchive] There was a problem extracting {ArchivePath} to {ExtractPath}",archivePath, extractPath);
|
_logger.LogWarning(e, "[ExtractArchive] There was a problem extracting {ArchivePath} to {ExtractPath}",archivePath, extractPath);
|
||||||
return;
|
throw new KavitaException(
|
||||||
|
$"There was an error when extracting {archivePath}. Check the file exists, has read permissions or the server OS can support all path characters.");
|
||||||
}
|
}
|
||||||
_logger.LogDebug("Extracted archive to {ExtractPath} in {ElapsedMilliseconds} milliseconds", extractPath, sw.ElapsedMilliseconds);
|
_logger.LogDebug("Extracted archive to {ExtractPath} in {ElapsedMilliseconds} milliseconds", extractPath, sw.ElapsedMilliseconds);
|
||||||
}
|
}
|
||||||
|
@ -174,6 +174,7 @@ public class BookmarkService : IBookmarkService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is a long-running job that will convert all bookmarks into WebP. Do not invoke anyway except via Hangfire.
|
/// This is a long-running job that will convert all bookmarks into WebP. Do not invoke anyway except via Hangfire.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||||
public async Task ConvertAllBookmarkToWebP()
|
public async Task ConvertAllBookmarkToWebP()
|
||||||
{
|
{
|
||||||
var bookmarkDirectory =
|
var bookmarkDirectory =
|
||||||
|
@ -198,6 +198,8 @@ public class MetadataService : IMetadataService
|
|||||||
/// <remarks>This can be heavy on memory first run</remarks>
|
/// <remarks>This can be heavy on memory first run</remarks>
|
||||||
/// <param name="libraryId"></param>
|
/// <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="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||||
|
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||||
|
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
public async Task RefreshMetadata(int libraryId, bool forceUpdate = false)
|
public async Task RefreshMetadata(int libraryId, bool forceUpdate = false)
|
||||||
{
|
{
|
||||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||||
|
@ -147,11 +147,11 @@ namespace API.Services.Tasks
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes Database backups older than 30 days. If all backups are older than 30 days, the latest is kept.
|
/// Removes Database backups older than configured total backups. If all backups are older than total backups days, only the latest is kept.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task CleanupBackups()
|
public async Task CleanupBackups()
|
||||||
{
|
{
|
||||||
const int dayThreshold = 30; // TODO: We can make this a config option
|
var dayThreshold = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).TotalBackups;
|
||||||
_logger.LogInformation("Beginning cleanup of Database backups at {Time}", DateTime.Now);
|
_logger.LogInformation("Beginning cleanup of Database backups at {Time}", DateTime.Now);
|
||||||
var backupDirectory =
|
var backupDirectory =
|
||||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value;
|
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value;
|
||||||
|
@ -45,6 +45,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||||
|
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
public async Task ScanLibrary(int libraryId, bool forceUpdate = false)
|
public async Task ScanLibrary(int libraryId, bool forceUpdate = false)
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
@ -69,6 +69,8 @@ public class ScannerService : IScannerService
|
|||||||
_wordCountAnalyzerService = wordCountAnalyzerService;
|
_wordCountAnalyzerService = wordCountAnalyzerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||||
|
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
public async Task ScanSeries(int libraryId, int seriesId, CancellationToken token)
|
public async Task ScanSeries(int libraryId, int seriesId, CancellationToken token)
|
||||||
{
|
{
|
||||||
var sw = new Stopwatch();
|
var sw = new Stopwatch();
|
||||||
@ -250,7 +252,8 @@ public class ScannerService : IScannerService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||||
|
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
public async Task ScanLibraries()
|
public async Task ScanLibraries()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Starting Scan of All Libraries");
|
_logger.LogInformation("Starting Scan of All Libraries");
|
||||||
@ -269,7 +272,8 @@ public class ScannerService : IScannerService
|
|||||||
/// ie) all entities will be rechecked for new cover images and comicInfo.xml changes
|
/// ie) all entities will be rechecked for new cover images and comicInfo.xml changes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryId"></param>
|
/// <param name="libraryId"></param>
|
||||||
|
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||||
|
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
public async Task ScanLibrary(int libraryId)
|
public async Task ScanLibrary(int libraryId)
|
||||||
{
|
{
|
||||||
Library library;
|
Library library;
|
||||||
|
@ -21,4 +21,8 @@ export interface ReadingList {
|
|||||||
promoted: boolean;
|
promoted: boolean;
|
||||||
coverImageLocked: boolean;
|
coverImageLocked: boolean;
|
||||||
items: Array<ReadingListItem>;
|
items: Array<ReadingListItem>;
|
||||||
|
/**
|
||||||
|
* If this is empty or null, the cover image isn't set. Do not use this externally.
|
||||||
|
*/
|
||||||
|
coverImage: string;
|
||||||
}
|
}
|
@ -41,7 +41,8 @@ export enum SortField {
|
|||||||
SortName = 1,
|
SortName = 1,
|
||||||
Created = 2,
|
Created = 2,
|
||||||
LastModified = 3,
|
LastModified = 3,
|
||||||
LastChapterAdded = 4
|
LastChapterAdded = 4,
|
||||||
|
TimeToRead = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReadStatus {
|
export interface ReadStatus {
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
export interface DirectoryDto {
|
export interface DirectoryDto {
|
||||||
name: string;
|
name: string;
|
||||||
fullPath: string;
|
fullPath: string;
|
||||||
|
/**
|
||||||
|
* This is only on the UI to disable paths
|
||||||
|
*/
|
||||||
|
disabled: boolean;
|
||||||
}
|
}
|
@ -35,7 +35,8 @@ export class AccountService implements OnDestroy {
|
|||||||
constructor(private httpClient: HttpClient, private router: Router,
|
constructor(private httpClient: HttpClient, private router: Router,
|
||||||
private messageHub: MessageHubService, private themeService: ThemeService) {
|
private messageHub: MessageHubService, private themeService: ThemeService) {
|
||||||
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
|
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
|
||||||
map(evt => evt.payload as UserUpdateEvent),
|
map(evt => evt.payload as UserUpdateEvent),
|
||||||
|
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
|
||||||
switchMap(() => this.refreshToken()))
|
switchMap(() => this.refreshToken()))
|
||||||
.subscribe(() => {});
|
.subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ElementRef, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -3,14 +3,6 @@
|
|||||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<!-- <div class="mb-3">
|
|
||||||
<label for="filter" class="form-label">Filter</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input id="filter" autocomplete="off" class="form-control" [(ngModel)]="filterQuery" type="text" aria-describedby="reset-input">
|
|
||||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="filterQuery = '';">Clear</button>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="filter" class="form-label">Path</label>
|
<label for="filter" class="form-label">Path</label>
|
||||||
@ -46,7 +38,7 @@
|
|||||||
<table class="table table-striped scrollable">
|
<table class="table table-striped scrollable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Type</th>
|
<th scope="col" style="width: 40px;">Type</th>
|
||||||
<th scope="col">Name</th>
|
<th scope="col">Name</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -55,7 +47,7 @@
|
|||||||
<td><i class="fa-solid fa-arrow-turn-up" aria-hidden="true"></i></td>
|
<td><i class="fa-solid fa-arrow-turn-up" aria-hidden="true"></i></td>
|
||||||
<td>...</td>
|
<td>...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngFor="let folder of folders; let idx = index;" (click)="selectNode(folder)">
|
<tr *ngFor="let folder of folders; let idx = index;" (click)="selectNode(folder)" style="cursor: pointer;" [ngClass]="{'disabled': folder.disabled}">
|
||||||
<td><i class="fa-regular fa-folder" aria-hidden="true"></i></td>
|
<td><i class="fa-regular fa-folder" aria-hidden="true"></i></td>
|
||||||
<td id="folder--{{idx}}">
|
<td id="folder--{{idx}}">
|
||||||
{{folder.name}}
|
{{folder.name}}
|
||||||
|
@ -16,4 +16,10 @@ $breadcrumb-divider: quote(">");
|
|||||||
|
|
||||||
.table {
|
.table {
|
||||||
background-color: lightgrey;
|
background-color: lightgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
color: lightgrey !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
background-color: var(--error-color);
|
||||||
}
|
}
|
@ -91,6 +91,7 @@ export class DirectoryPickerComponent implements OnInit {
|
|||||||
|
|
||||||
|
|
||||||
selectNode(folder: DirectoryDto) {
|
selectNode(folder: DirectoryDto) {
|
||||||
|
if (folder.disabled) return;
|
||||||
this.currentRoot = folder.name;
|
this.currentRoot = folder.name;
|
||||||
this.routeStack.push(folder.name);
|
this.routeStack.push(folder.name);
|
||||||
this.path = folder.fullPath;
|
this.path = folder.fullPath;
|
||||||
@ -116,6 +117,10 @@ export class DirectoryPickerComponent implements OnInit {
|
|||||||
}, err => {
|
}, err => {
|
||||||
// If there was an error, pop off last directory added to stack
|
// If there was an error, pop off last directory added to stack
|
||||||
this.routeStack.pop();
|
this.routeStack.pop();
|
||||||
|
const item = this.folders.find(f => f.fullPath === path);
|
||||||
|
if (item) {
|
||||||
|
item.disabled = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,4 +11,5 @@ export interface ServerSettings {
|
|||||||
emailServiceUrl: string;
|
emailServiceUrl: string;
|
||||||
convertBookmarkToWebP: boolean;
|
convertBookmarkToWebP: boolean;
|
||||||
enableSwaggerUi: boolean;
|
enableSwaggerUi: boolean;
|
||||||
|
totalBackups: number;
|
||||||
}
|
}
|
||||||
|
@ -21,14 +21,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0 mb-2">
|
<div class="row g-0 mb-2">
|
||||||
<div class="col-md-6 col-sm-12 pe-2">
|
<div class="col-md-4 col-sm-12 pe-2">
|
||||||
<label for="settings-port" class="form-label">Port</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
<label for="settings-port" class="form-label">Port</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>
|
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>
|
||||||
<span class="visually-hidden" id="settings-port-help">Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</span>
|
<span class="visually-hidden" id="settings-port-help">Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</span>
|
||||||
<input id="settings-port" aria-describedby="settings-port-help" class="form-control" formControlName="port" type="number" step="1" min="1" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
<input id="settings-port" aria-describedby="settings-port-help" class="form-control" formControlName="port" type="number" step="1" min="1" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4 col-sm-12 pe-2">
|
||||||
|
<label for="backup-tasks" class="form-label">Backup Tasks</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="backupTasksTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #backupTasksTooltip>The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</ng-template>
|
||||||
|
<span class="visually-hidden" id="backup-tasks-help">The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</span>
|
||||||
|
<input id="backup-tasks" aria-describedby="backup-tasks-help" class="form-control" formControlName="totalBackups" type="number" step="1" min="1" max="30" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
||||||
|
<ng-container *ngIf="settingsForm.get('totalBackups')?.errors as errors">
|
||||||
|
<p class="invalid-feedback" *ngIf="errors.min">
|
||||||
|
You must have at least 1 backup
|
||||||
|
</p>
|
||||||
|
<p class="invalid-feedback" *ngIf="errors.max">
|
||||||
|
You cannot have more than {{errors.max.max}} backups
|
||||||
|
</p>
|
||||||
|
<p class="invalid-feedback" *ngIf="errors.required">
|
||||||
|
This field is required
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="col-md-4 col-sm-12">
|
||||||
<label for="logging-level-port" class="form-label">Logging Level</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="loggingLevelTooltip" role="button" tabindex="0"></i>
|
<label for="logging-level-port" class="form-label">Logging Level</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="loggingLevelTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #loggingLevelTooltip>Use debug to help identify issues. Debug can eat up a lot of disk space. Requires restart to take effect.</ng-template>
|
<ng-template #loggingLevelTooltip>Use debug to help identify issues. Debug can eat up a lot of disk space. Requires restart to take effect.</ng-template>
|
||||||
<span class="visually-hidden" id="logging-level-port-help">Port the server listens on. Requires restart to take effect.</span>
|
<span class="visually-hidden" id="logging-level-port-help">Port the server listens on. Requires restart to take effect.</span>
|
||||||
|
@ -5,4 +5,8 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
color: black;
|
color: black;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: inherit;
|
||||||
}
|
}
|
@ -43,6 +43,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.addControl('baseUrl', new FormControl(this.serverSettings.baseUrl, [Validators.required]));
|
this.settingsForm.addControl('baseUrl', new FormControl(this.serverSettings.baseUrl, [Validators.required]));
|
||||||
this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
|
this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
|
||||||
this.settingsForm.addControl('enableSwaggerUi', new FormControl(this.serverSettings.enableSwaggerUi, [Validators.required]));
|
this.settingsForm.addControl('enableSwaggerUi', new FormControl(this.serverSettings.enableSwaggerUi, [Validators.required]));
|
||||||
|
this.settingsForm.addControl('totalBackups', new FormControl(this.serverSettings.totalBackups, [Validators.required, Validators.min(1), Validators.max(30)]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.get('baseUrl')?.setValue(this.serverSettings.baseUrl);
|
this.settingsForm.get('baseUrl')?.setValue(this.serverSettings.baseUrl);
|
||||||
this.settingsForm.get('emailServiceUrl')?.setValue(this.serverSettings.emailServiceUrl);
|
this.settingsForm.get('emailServiceUrl')?.setValue(this.serverSettings.emailServiceUrl);
|
||||||
this.settingsForm.get('enableSwaggerUi')?.setValue(this.serverSettings.enableSwaggerUi);
|
this.settingsForm.get('enableSwaggerUi')?.setValue(this.serverSettings.enableSwaggerUi);
|
||||||
|
this.settingsForm.get('totalBackups')?.setValue(this.serverSettings.totalBackups);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
|
@ -12,8 +12,8 @@
|
|||||||
[pagination]="pagination"
|
[pagination]="pagination"
|
||||||
[filterSettings]="filterSettings"
|
[filterSettings]="filterSettings"
|
||||||
[filterOpen]="filterOpen"
|
[filterOpen]="filterOpen"
|
||||||
|
[jumpBarKeys]="jumpbarKeys"
|
||||||
(applyFilter)="updateFilter($event)"
|
(applyFilter)="updateFilter($event)"
|
||||||
(pageChange)="onPageChange($event)"
|
|
||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()"
|
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()"
|
||||||
|
@ -7,11 +7,13 @@ import { BulkSelectionService } from '../cards/bulk-selection.service';
|
|||||||
import { FilterSettings } from '../metadata-filter/filter-settings';
|
import { FilterSettings } from '../metadata-filter/filter-settings';
|
||||||
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||||
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||||
|
import { JumpKey } from '../_models/jumpbar/jump-key';
|
||||||
import { Pagination } from '../_models/pagination';
|
import { Pagination } from '../_models/pagination';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../_models/series';
|
||||||
import { FilterEvent, SeriesFilter } from '../_models/series-filter';
|
import { FilterEvent, SeriesFilter } from '../_models/series-filter';
|
||||||
import { Action } from '../_services/action-factory.service';
|
import { Action } from '../_services/action-factory.service';
|
||||||
import { ActionService } from '../_services/action.service';
|
import { ActionService } from '../_services/action.service';
|
||||||
|
import { LibraryService } from '../_services/library.service';
|
||||||
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
|
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
|
||||||
import { SeriesService } from '../_services/series.service';
|
import { SeriesService } from '../_services/series.service';
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||||
filterActiveCheck!: SeriesFilter;
|
filterActiveCheck!: SeriesFilter;
|
||||||
filterActive: boolean = false;
|
filterActive: boolean = false;
|
||||||
|
jumpbarKeys: Array<JumpKey> = [];
|
||||||
|
|
||||||
bulkActionCallback = (action: Action, data: any) => {
|
bulkActionCallback = (action: Action, data: any) => {
|
||||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||||
@ -73,7 +76,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||||||
private titleService: Title, private actionService: ActionService,
|
private titleService: Title, private actionService: ActionService,
|
||||||
public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||||
private utilityService: UtilityService, private route: ActivatedRoute,
|
private utilityService: UtilityService, private route: ActivatedRoute,
|
||||||
private filterUtilityService: FilterUtilitiesService) {
|
private filterUtilityService: FilterUtilitiesService, private libraryService: LibraryService) {
|
||||||
|
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
this.titleService.setTitle('Kavita - All Series');
|
this.titleService.setTitle('Kavita - All Series');
|
||||||
@ -108,6 +111,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||||||
this.bulkSelectionService.isShiftDown = false;
|
this.bulkSelectionService.isShiftDown = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
updateFilter(data: FilterEvent) {
|
updateFilter(data: FilterEvent) {
|
||||||
this.filter = data.filter;
|
this.filter = data.filter;
|
||||||
@ -118,18 +122,35 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
loadPage() {
|
loadPage() {
|
||||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
||||||
this.seriesService.getAllSeries(this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
this.seriesService.getAllSeries(undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
|
||||||
this.series = series.result;
|
this.series = series.result;
|
||||||
|
const keys: {[key: string]: number} = {};
|
||||||
|
series.result.forEach(s => {
|
||||||
|
let ch = s.name.charAt(0);
|
||||||
|
if (/\d|\#|!|%|@|\(|\)|\^|\*/g.test(ch)) {
|
||||||
|
ch = '#';
|
||||||
|
}
|
||||||
|
if (!keys.hasOwnProperty(ch)) {
|
||||||
|
keys[ch] = 0;
|
||||||
|
}
|
||||||
|
keys[ch] += 1;
|
||||||
|
});
|
||||||
|
this.jumpbarKeys = Object.keys(keys).map(k => {
|
||||||
|
return {
|
||||||
|
key: k,
|
||||||
|
size: keys[k],
|
||||||
|
title: k.toUpperCase()
|
||||||
|
}
|
||||||
|
}).sort((a, b) => {
|
||||||
|
if (a.key < b.key) return -1;
|
||||||
|
if (a.key > b.key) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
this.pagination = series.pagination;
|
this.pagination = series.pagination;
|
||||||
this.loadingSeries = false;
|
this.loadingSeries = false;
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageChange(pagination: Pagination) {
|
|
||||||
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);
|
|
||||||
this.loadPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
|
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
<div class="modal-body {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||||
<li [ngbNavItem]="tabs[0]">
|
<li [ngbNavItem]="tabs[TabID.General].id">
|
||||||
<a ngbNavLink>{{tabs[0]}}</a>
|
<a ngbNavLink>{{tabs[TabID.General].title}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<p>
|
<p>
|
||||||
This tag is currently {{tag?.promoted ? 'promoted' : 'not promoted'}} (<i class="fa fa-angle-double-up" aria-hidden="true"></i>).
|
This tag is currently {{tag?.promoted ? 'promoted' : 'not promoted'}} (<i class="fa fa-angle-double-up" aria-hidden="true"></i>).
|
||||||
@ -49,8 +49,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
<li [ngbNavItem]="tabs[1]">
|
<li [ngbNavItem]="tabs[TabID.CoverImage].id">
|
||||||
<a ngbNavLink>{{tabs[1]}}</a>
|
<a ngbNavLink>{{tabs[TabID.CoverImage].title}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<p class="alert alert-primary" role="alert">
|
<p class="alert alert-primary" role="alert">
|
||||||
Upload and choose a new cover image. Press Save to upload and override the cover.
|
Upload and choose a new cover image. Press Save to upload and override the cover.
|
||||||
|
@ -16,6 +16,11 @@ import { SeriesService } from 'src/app/_services/series.service';
|
|||||||
import { UploadService } from 'src/app/_services/upload.service';
|
import { UploadService } from 'src/app/_services/upload.service';
|
||||||
|
|
||||||
|
|
||||||
|
enum TabID {
|
||||||
|
General = 0,
|
||||||
|
CoverImage = 1,
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-edit-collection-tags',
|
selector: 'app-edit-collection-tags',
|
||||||
templateUrl: './edit-collection-tags.component.html',
|
templateUrl: './edit-collection-tags.component.html',
|
||||||
@ -32,8 +37,8 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||||||
selectAll: boolean = true;
|
selectAll: boolean = true;
|
||||||
libraryNames!: any;
|
libraryNames!: any;
|
||||||
collectionTagForm!: FormGroup;
|
collectionTagForm!: FormGroup;
|
||||||
tabs = ['General', 'Cover Image'];
|
tabs = [{title: 'General', id: TabID.General}, {title: 'Cover Image', id: TabID.CoverImage}];
|
||||||
active = this.tabs[0];
|
active = TabID.General;
|
||||||
imageUrls: Array<string> = [];
|
imageUrls: Array<string> = [];
|
||||||
selectedCover: string = '';
|
selectedCover: string = '';
|
||||||
|
|
||||||
@ -45,6 +50,10 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||||||
return Breakpoint;
|
return Breakpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get TabID() {
|
||||||
|
return TabID;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
|
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
|
||||||
private collectionService: CollectionTagService, private toastr: ToastrService,
|
private collectionService: CollectionTagService, private toastr: ToastrService,
|
||||||
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
|
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
|
||||||
|
@ -341,7 +341,7 @@
|
|||||||
<h4>Information</h4>
|
<h4>Information</h4>
|
||||||
<div class="row g-0 mb-2">
|
<div class="row g-0 mb-2">
|
||||||
<div class="col-md-6" *ngIf="libraryName">Library: {{libraryName | sentenceCase}}</div>
|
<div class="col-md-6" *ngIf="libraryName">Library: {{libraryName | sentenceCase}}</div>
|
||||||
<div class="col-md-6">Format: <app-tag-badge>{{utilityService.mangaFormat(series.format)}}</app-tag-badge></div>
|
<div class="col-md-6">Format: <app-tag-badge>{{series.format | mangaFormat}}</app-tag-badge></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-0 mb-2">
|
<div class="row g-0 mb-2">
|
||||||
<div class="col-md-6" >Created: {{series.created | date:'shortDate'}}</div>
|
<div class="col-md-6" >Created: {{series.created | date:'shortDate'}}</div>
|
||||||
|
@ -13,14 +13,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
|
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
|
||||||
<div class="viewport-container" #scrollingBlock>
|
<div class="viewport-container">
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
|
|
||||||
<div class="card-container mt-2 mb-2">
|
<div class="card-container mt-2 mb-2">
|
||||||
<virtual-scroller #scroll [items]="items" (vsEnd)="fetchMore($event)" [bufferAmount]="1">
|
<virtual-scroller #scroll [items]="items" [bufferAmount]="1" [parentScroll]="parentScroll">
|
||||||
<div class="grid row g-0" #container>
|
<div class="grid row g-0" #container>
|
||||||
<div class="card col-auto mt-2 mb-2" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
|
<div class="card col-auto mt-2 mb-2" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
|
||||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: scroll.viewPortInfo.startIndexWithBuffer + i }"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</virtual-scroller>
|
</virtual-scroller>
|
||||||
@ -48,53 +48,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #paginationTemplate let-id="id">
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-center mb-0" *ngIf="pagination && items.length > 0">
|
|
||||||
<ngb-pagination
|
|
||||||
*ngIf="pagination.totalPages > 1"
|
|
||||||
[maxSize]="8"
|
|
||||||
[rotate]="true"
|
|
||||||
[ellipses]="false"
|
|
||||||
[(page)]="pagination.currentPage"
|
|
||||||
[pageSize]="pagination.itemsPerPage"
|
|
||||||
(pageChange)="onPageChange($event)"
|
|
||||||
[collectionSize]="pagination.totalItems">
|
|
||||||
|
|
||||||
<ng-template ngbPaginationPages let-page let-pages="pages" *ngIf="pagination.totalItems / pagination.itemsPerPage > 20">
|
|
||||||
<li class="ngb-custom-pages-item" *ngIf="pagination.totalPages > 1">
|
|
||||||
<div class="d-flex flex-nowrap px-2">
|
|
||||||
<label
|
|
||||||
id="paginationInputLabel-{{id}}"
|
|
||||||
for="paginationInput-{{id}}"
|
|
||||||
class="col-form-label me-2 ms-1 form-label"
|
|
||||||
>Page</label>
|
|
||||||
<input #i
|
|
||||||
type="text"
|
|
||||||
inputmode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
class="form-control custom-pages-input"
|
|
||||||
id="paginationInput-{{id}}"
|
|
||||||
[value]="page"
|
|
||||||
(keyup.enter)="selectPageStr(i.value)"
|
|
||||||
(blur)="selectPageStr(i.value)"
|
|
||||||
(input)="formatInput($any($event).target)"
|
|
||||||
attr.aria-labelledby="paginationInputLabel-{{id}} paginationDescription-{{id}}"
|
|
||||||
[ngStyle]="{width: (0.5 + pagination.currentPage + '').length + 'rem'} "
|
|
||||||
/>
|
|
||||||
<span id="paginationDescription-{{id}}" class="col-form-label text-nowrap px-2">
|
|
||||||
of {{pagination.totalPages}}</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
</ngb-pagination>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- <ng-container *ngIf="pagination && items.length > 0 && id == 'bottom' && pagination.totalPages > 1 " [ngTemplateOutlet]="jumpBar"></ng-container> -->
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="mx-auto" *ngIf="isLoading" style="width: 200px;">
|
<div class="mx-auto" *ngIf="isLoading" style="width: 200px;">
|
||||||
<div class="spinner-border text-secondary loading" role="status">
|
<div class="spinner-border text-secondary loading" role="status">
|
||||||
<span class="invisible">Loading...</span>
|
<span class="invisible">Loading...</span>
|
||||||
|
@ -1,34 +1,35 @@
|
|||||||
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { AfterViewInit, Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, NgZone, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewChild } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewChild } from '@angular/core';
|
||||||
import { IPageInfo, VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
|
import { VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
|
||||||
import { filter, from, map, pairwise, Subject, tap, throttleTime } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
|
import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
|
||||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||||
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||||
import { Library } from 'src/app/_models/library';
|
import { Library } from 'src/app/_models/library';
|
||||||
import { PaginatedResult, Pagination } from 'src/app/_models/pagination';
|
import { Pagination } from 'src/app/_models/pagination';
|
||||||
import { FilterEvent, FilterItem, SeriesFilter } from 'src/app/_models/series-filter';
|
import { FilterEvent, FilterItem, SeriesFilter } from 'src/app/_models/series-filter';
|
||||||
import { ActionItem } from 'src/app/_services/action-factory.service';
|
import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||||
import { SeriesService } from 'src/app/_services/series.service';
|
import { SeriesService } from 'src/app/_services/series.service';
|
||||||
|
|
||||||
const FILTER_PAG_REGEX = /[^0-9]/g;
|
|
||||||
const SCROLL_BREAKPOINT = 300;
|
|
||||||
const keySize = 24;
|
const keySize = 24;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-card-detail-layout',
|
selector: 'app-card-detail-layout',
|
||||||
templateUrl: './card-detail-layout.component.html',
|
templateUrl: './card-detail-layout.component.html',
|
||||||
styleUrls: ['./card-detail-layout.component.scss']
|
styleUrls: ['./card-detail-layout.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewInit, OnChanges {
|
export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
||||||
|
|
||||||
@Input() header: string = '';
|
@Input() header: string = '';
|
||||||
@Input() isLoading: boolean = false;
|
@Input() isLoading: boolean = false;
|
||||||
@Input() items: any[] = [];
|
@Input() items: any[] = [];
|
||||||
// ?! we need to have chunks to render in, because if we scroll down, then up, then down, we don't want to trigger a duplicate call
|
|
||||||
@Input() paginatedItems: PaginatedResult<any> | undefined;
|
|
||||||
@Input() pagination!: Pagination;
|
@Input() pagination!: Pagination;
|
||||||
|
/**
|
||||||
|
* Parent scroll for virtualize pagination
|
||||||
|
*/
|
||||||
|
@Input() parentScroll!: Element | Window;
|
||||||
|
|
||||||
// Filter Code
|
// Filter Code
|
||||||
@Input() filterOpen!: EventEmitter<boolean>;
|
@Input() filterOpen!: EventEmitter<boolean>;
|
||||||
@ -48,8 +49,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||||||
jumpBarKeysToRender: Array<JumpKey> = []; // Original
|
jumpBarKeysToRender: Array<JumpKey> = []; // Original
|
||||||
|
|
||||||
@Output() itemClicked: EventEmitter<any> = new EventEmitter();
|
@Output() itemClicked: EventEmitter<any> = new EventEmitter();
|
||||||
@Output() pageChange: EventEmitter<Pagination> = new EventEmitter();
|
|
||||||
@Output() pageChangeWithDirection: EventEmitter<0 | 1> = new EventEmitter();
|
|
||||||
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
|
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
|
||||||
|
|
||||||
@ContentChild('cardItem') itemTemplate!: TemplateRef<any>;
|
@ContentChild('cardItem') itemTemplate!: TemplateRef<any>;
|
||||||
@ -59,8 +58,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||||||
|
|
||||||
@ViewChild(VirtualScrollerComponent) private virtualScroller!: VirtualScrollerComponent;
|
@ViewChild(VirtualScrollerComponent) private virtualScroller!: VirtualScrollerComponent;
|
||||||
|
|
||||||
itemSize: number = 100; // Idk what this actually does. Less results in more items rendering, 5 works well with pagination. 230 is technically what a card is height wise
|
|
||||||
|
|
||||||
filter!: SeriesFilter;
|
filter!: SeriesFilter;
|
||||||
libraries: Array<FilterItem<Library>> = [];
|
libraries: Array<FilterItem<Library>> = [];
|
||||||
|
|
||||||
@ -73,8 +70,9 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(private seriesService: SeriesService, public utilityService: UtilityService,
|
constructor(private seriesService: SeriesService, public utilityService: UtilityService,
|
||||||
@Inject(DOCUMENT) private document: Document, private ngZone: NgZone) {
|
@Inject(DOCUMENT) private document: Document, private changeDetectionRef: ChangeDetectorRef) {
|
||||||
this.filter = this.seriesService.createSeriesFilter();
|
this.filter = this.seriesService.createSeriesFilter();
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
@HostListener('window:resize', ['$event'])
|
||||||
@ -83,6 +81,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||||||
const fullSize = (this.jumpBarKeys.length * keySize);
|
const fullSize = (this.jumpBarKeys.length * keySize);
|
||||||
const currentSize = (this.document.querySelector('.viewport-container')?.getBoundingClientRect().height || 10) - 30;
|
const currentSize = (this.document.querySelector('.viewport-container')?.getBoundingClientRect().height || 10) - 30;
|
||||||
if (currentSize >= fullSize) {
|
if (currentSize >= fullSize) {
|
||||||
|
this.jumpBarKeysToRender = [...this.jumpBarKeys];
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,12 +94,13 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||||||
this.jumpBarKeysToRender = [];
|
this.jumpBarKeysToRender = [];
|
||||||
|
|
||||||
const removalTimes = Math.ceil(removeCount / 2);
|
const removalTimes = Math.ceil(removeCount / 2);
|
||||||
const midPoint = this.jumpBarKeys.length / 2;
|
const midPoint = Math.floor(this.jumpBarKeys.length / 2);
|
||||||
this.jumpBarKeysToRender.push(this.jumpBarKeys[0]);
|
this.jumpBarKeysToRender.push(this.jumpBarKeys[0]);
|
||||||
this.removeFirstPartOfJumpBar(midPoint, removalTimes);
|
this.removeFirstPartOfJumpBar(midPoint, removalTimes);
|
||||||
this.jumpBarKeysToRender.push(this.jumpBarKeys[midPoint]);
|
this.jumpBarKeysToRender.push(this.jumpBarKeys[midPoint]);
|
||||||
this.removeSecondPartOfJumpBar(midPoint, removalTimes);
|
this.removeSecondPartOfJumpBar(midPoint, removalTimes);
|
||||||
this.jumpBarKeysToRender.push(this.jumpBarKeys[this.jumpBarKeys.length - 1]);
|
this.jumpBarKeysToRender.push(this.jumpBarKeys[this.jumpBarKeys.length - 1]);
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSecondPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1) {
|
removeSecondPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1) {
|
||||||
@ -141,16 +142,18 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.trackByIdentity === undefined) {
|
if (this.trackByIdentity === undefined) {
|
||||||
this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`; // ${this.pagination?.currentPage}_
|
this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (this.filterSettings === undefined) {
|
if (this.filterSettings === undefined) {
|
||||||
this.filterSettings = new FilterSettings();
|
this.filterSettings = new FilterSettings();
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.pagination === undefined) {
|
if (this.pagination === undefined) {
|
||||||
this.pagination = {currentPage: 1, itemsPerPage: this.items.length, totalItems: this.items.length, totalPages: 1}
|
this.pagination = {currentPage: 1, itemsPerPage: this.items.length, totalItems: this.items.length, totalPages: 1};
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,58 +162,12 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||||||
this.resizeJumpBar();
|
this.resizeJumpBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
|
||||||
// this.scroller.elementScrolled().pipe(
|
|
||||||
// map(() => this.scroller.measureScrollOffset('bottom')),
|
|
||||||
// pairwise(),
|
|
||||||
// filter(([y1, y2]) => ((y2 < y1 && y2 < SCROLL_BREAKPOINT))), // 140
|
|
||||||
// throttleTime(200)
|
|
||||||
// ).subscribe(([y1, y2]) => {
|
|
||||||
// const movingForward = y2 < y1;
|
|
||||||
// if (this.pagination.currentPage === this.pagination.totalPages || this.pagination.currentPage === 1 && !movingForward) return;
|
|
||||||
// this.ngZone.run(() => {
|
|
||||||
// console.log('Load next pages');
|
|
||||||
|
|
||||||
// this.pagination.currentPage = this.pagination.currentPage + 1;
|
|
||||||
// this.pageChangeWithDirection.emit(1);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// this.scroller.elementScrolled().pipe(
|
|
||||||
// map(() => this.scroller.measureScrollOffset('top')),
|
|
||||||
// pairwise(),
|
|
||||||
// filter(([y1, y2]) => y2 >= y1 && y2 < SCROLL_BREAKPOINT),
|
|
||||||
// throttleTime(200)
|
|
||||||
// ).subscribe(([y1, y2]) => {
|
|
||||||
// if (this.pagination.currentPage === 1) return;
|
|
||||||
// this.ngZone.run(() => {
|
|
||||||
// console.log('Load prev pages');
|
|
||||||
|
|
||||||
// this.pagination.currentPage = this.pagination.currentPage - 1;
|
|
||||||
// this.pageChangeWithDirection.emit(0);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.onDestory.next();
|
this.onDestory.next();
|
||||||
this.onDestory.complete();
|
this.onDestory.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onPageChange(page: number) {
|
|
||||||
this.pageChange.emit(this.pagination);
|
|
||||||
}
|
|
||||||
|
|
||||||
selectPageStr(page: string) {
|
|
||||||
this.pagination.currentPage = parseInt(page, 10) || 1;
|
|
||||||
this.onPageChange(this.pagination.currentPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
formatInput(input: HTMLInputElement) {
|
|
||||||
input.value = input.value.replace(FILTER_PAG_REGEX, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
performAction(action: ActionItem<any>) {
|
performAction(action: ActionItem<any>) {
|
||||||
if (typeof action.callback === 'function') {
|
if (typeof action.callback === 'function') {
|
||||||
action.callback(action.action, undefined);
|
action.callback(action.action, undefined);
|
||||||
@ -220,63 +177,19 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||||||
applyMetadataFilter(event: FilterEvent) {
|
applyMetadataFilter(event: FilterEvent) {
|
||||||
this.applyFilter.emit(event);
|
this.applyFilter.emit(event);
|
||||||
this.updateApplied++;
|
this.updateApplied++;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
loading: boolean = false;
|
|
||||||
fetchMore(event: IPageInfo) {
|
|
||||||
if (event.endIndex !== this.items.length - 1) return;
|
|
||||||
if (event.startIndex < 0) return;
|
|
||||||
console.log('Requesting next page ', (this.pagination.currentPage + 1), 'of data', event);
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
// this.pagination.currentPage = this.pagination.currentPage + 1;
|
|
||||||
// this.pageChangeWithDirection.emit(1);
|
|
||||||
|
|
||||||
// this.fetchNextChunk(this.items.length, 10).then(chunk => {
|
|
||||||
// this.items = this.items.concat(chunk);
|
|
||||||
// this.loading = false;
|
|
||||||
// }, () => this.loading = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollTo(jumpKey: JumpKey) {
|
scrollTo(jumpKey: JumpKey) {
|
||||||
// TODO: Figure out how to do this
|
|
||||||
|
|
||||||
let targetIndex = 0;
|
let targetIndex = 0;
|
||||||
for(var i = 0; i < this.jumpBarKeys.length; i++) {
|
for(var i = 0; i < this.jumpBarKeys.length; i++) {
|
||||||
if (this.jumpBarKeys[i].key === jumpKey.key) break;
|
if (this.jumpBarKeys[i].key === jumpKey.key) break;
|
||||||
targetIndex += this.jumpBarKeys[i].size;
|
targetIndex += this.jumpBarKeys[i].size;
|
||||||
}
|
}
|
||||||
//console.log('scrolling to card that starts with ', jumpKey.key, + ' with index of ', targetIndex);
|
|
||||||
|
|
||||||
// Infinite scroll
|
|
||||||
this.virtualScroller.scrollToIndex(targetIndex, true, undefined, 1000);
|
this.virtualScroller.scrollToIndex(targetIndex, true, undefined, 1000);
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Basic implementation based on itemsPerPage being the same.
|
|
||||||
//var minIndex = this.pagination.currentPage * this.pagination.itemsPerPage;
|
|
||||||
var targetPage = Math.max(Math.ceil(targetIndex / this.pagination.itemsPerPage), 1);
|
|
||||||
//console.log('We are on page ', this.pagination.currentPage, ' and our target page is ', targetPage);
|
|
||||||
if (targetPage === this.pagination.currentPage) {
|
|
||||||
// Scroll to the element
|
|
||||||
const elem = this.document.querySelector(`div[id="jumpbar-index--${targetIndex}"`);
|
|
||||||
if (elem !== null) {
|
|
||||||
|
|
||||||
this.virtualScroller.scrollToIndex(targetIndex);
|
|
||||||
// elem.scrollIntoView({
|
|
||||||
// behavior: 'smooth'
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// With infinite scroll, we can't just jump to a random place, because then our list of items would be out of sync.
|
|
||||||
this.selectPageStr(targetPage + '');
|
|
||||||
//this.pageChangeWithDirection.emit(1);
|
|
||||||
|
|
||||||
// if (minIndex > targetIndex) {
|
|
||||||
// // We need to scroll forward (potentially to another page)
|
|
||||||
// } else if (minIndex < targetIndex) {
|
|
||||||
// // We need to scroll back (potentially to another page)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,10 @@
|
|||||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||||
<span class="visually-hidden">(promoted)</span>
|
<span class="visually-hidden">(promoted)</span>
|
||||||
</span>
|
</span>
|
||||||
<i class="fa {{utilityService.mangaFormatIcon(format)}}" aria-hidden="true" *ngIf="format != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(format)}}"></i><span class="visually-hidden">{{utilityService.mangaFormat(format)}}</span>
|
<ng-container *ngIf="format | mangaFormat as formatString">
|
||||||
|
<i class="fa {{format | mangaFormatIcon}}" aria-hidden="true" *ngIf="format != MangaFormat.UNKNOWN" title="{{formatString}}"></i>
|
||||||
|
<span class="visually-hidden">{{formatString}}</span>
|
||||||
|
</ng-container>
|
||||||
{{title}}
|
{{title}}
|
||||||
</span>
|
</span>
|
||||||
<span class="card-actions float-end">
|
<span class="card-actions float-end">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import { filter, finalize, map, take, takeUntil, takeWhile } from 'rxjs/operators';
|
import { filter, finalize, map, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||||
@ -25,7 +25,8 @@ import { BulkSelectionService } from '../bulk-selection.service';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-card-item',
|
selector: 'app-card-item',
|
||||||
templateUrl: './card-item.component.html',
|
templateUrl: './card-item.component.html',
|
||||||
styleUrls: ['./card-item.component.scss']
|
styleUrls: ['./card-item.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class CardItemComponent implements OnInit, OnDestroy {
|
export class CardItemComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@ -112,6 +113,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
* Handles touch events for selection on mobile devices to ensure you aren't touch scrolling
|
* Handles touch events for selection on mobile devices to ensure you aren't touch scrolling
|
||||||
*/
|
*/
|
||||||
prevOffset: number = 0;
|
prevOffset: number = 0;
|
||||||
|
selectionInProgress: boolean = false;
|
||||||
|
|
||||||
private user: User | undefined;
|
private user: User | undefined;
|
||||||
|
|
||||||
@ -130,11 +132,12 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
constructor(public imageService: ImageService, private libraryService: LibraryService,
|
constructor(public imageService: ImageService, private libraryService: LibraryService,
|
||||||
public utilityService: UtilityService, private downloadService: DownloadService,
|
public utilityService: UtilityService, private downloadService: DownloadService,
|
||||||
private toastr: ToastrService, public bulkSelectionService: BulkSelectionService,
|
private toastr: ToastrService, public bulkSelectionService: BulkSelectionService,
|
||||||
private messageHub: MessageHubService, private accountService: AccountService, private scrollService: ScrollService) {}
|
private messageHub: MessageHubService, private accountService: AccountService, private scrollService: ScrollService, private changeDetectionRef: ChangeDetectorRef) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
||||||
this.supressArchiveWarning = true;
|
this.supressArchiveWarning = true;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.suppressLibraryLink === false) {
|
if (this.suppressLibraryLink === false) {
|
||||||
@ -145,6 +148,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
if (this.libraryId !== undefined && this.libraryId > 0) {
|
if (this.libraryId !== undefined && this.libraryId > 0) {
|
||||||
this.libraryService.getLibraryName(this.libraryId).pipe(takeUntil(this.onDestroy)).subscribe(name => {
|
this.libraryService.getLibraryName(this.libraryId).pipe(takeUntil(this.onDestroy)).subscribe(name => {
|
||||||
this.libraryName = name;
|
this.libraryName = name;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,6 +175,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
||||||
|
|
||||||
this.read = updateEvent.pagesRead;
|
this.read = updateEvent.pagesRead;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,6 +184,12 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
this.onDestroy.complete();
|
this.onDestroy.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HostListener('touchmove', ['$event'])
|
||||||
|
onTouchMove(event: TouchEvent) {
|
||||||
|
if (!this.allowSelection) return;
|
||||||
|
|
||||||
|
this.selectionInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
@HostListener('touchstart', ['$event'])
|
@HostListener('touchstart', ['$event'])
|
||||||
onTouchStart(event: TouchEvent) {
|
onTouchStart(event: TouchEvent) {
|
||||||
@ -186,6 +197,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.prevTouchTime = event.timeStamp;
|
this.prevTouchTime = event.timeStamp;
|
||||||
this.prevOffset = this.scrollService.scrollPosition;
|
this.prevOffset = this.scrollService.scrollPosition;
|
||||||
|
this.selectionInProgress = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('touchend', ['$event'])
|
@HostListener('touchend', ['$event'])
|
||||||
@ -194,12 +206,13 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
const delta = event.timeStamp - this.prevTouchTime;
|
const delta = event.timeStamp - this.prevTouchTime;
|
||||||
const verticalOffset = this.scrollService.scrollPosition;
|
const verticalOffset = this.scrollService.scrollPosition;
|
||||||
|
|
||||||
if (delta >= 300 && delta <= 1000 && (verticalOffset === this.prevOffset)) {
|
if (delta >= 300 && delta <= 1000 && (verticalOffset === this.prevOffset) && this.selectionInProgress) {
|
||||||
this.handleSelection();
|
this.handleSelection();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
this.prevTouchTime = 0;
|
this.prevTouchTime = 0;
|
||||||
|
this.selectionInProgress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -207,10 +220,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
this.clicked.emit(this.title);
|
this.clicked.emit(this.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
isNullOrEmpty(val: string) {
|
|
||||||
return val === null || val === undefined || val === '';
|
|
||||||
}
|
|
||||||
|
|
||||||
preventClick(event: any) {
|
preventClick(event: any) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -229,6 +238,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
const wantToDownload = await this.downloadService.confirmSize(size, 'volume');
|
const wantToDownload = await this.downloadService.confirmSize(size, 'volume');
|
||||||
if (!wantToDownload) { return; }
|
if (!wantToDownload) { return; }
|
||||||
this.downloadInProgress = true;
|
this.downloadInProgress = true;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
this.download$ = this.downloadService.downloadVolume(volume).pipe(
|
this.download$ = this.downloadService.downloadVolume(volume).pipe(
|
||||||
takeWhile(val => {
|
takeWhile(val => {
|
||||||
return val.state != 'DONE';
|
return val.state != 'DONE';
|
||||||
@ -236,6 +246,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.download$ = null;
|
this.download$ = null;
|
||||||
this.downloadInProgress = false;
|
this.downloadInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
} else if (this.utilityService.isChapter(this.entity)) {
|
} else if (this.utilityService.isChapter(this.entity)) {
|
||||||
@ -244,6 +255,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
const wantToDownload = await this.downloadService.confirmSize(size, 'chapter');
|
const wantToDownload = await this.downloadService.confirmSize(size, 'chapter');
|
||||||
if (!wantToDownload) { return; }
|
if (!wantToDownload) { return; }
|
||||||
this.downloadInProgress = true;
|
this.downloadInProgress = true;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
this.download$ = this.downloadService.downloadChapter(chapter).pipe(
|
this.download$ = this.downloadService.downloadChapter(chapter).pipe(
|
||||||
takeWhile(val => {
|
takeWhile(val => {
|
||||||
return val.state != 'DONE';
|
return val.state != 'DONE';
|
||||||
@ -251,6 +263,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.download$ = null;
|
this.download$ = null;
|
||||||
this.downloadInProgress = false;
|
this.downloadInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
} else if (this.utilityService.isSeries(this.entity)) {
|
} else if (this.utilityService.isSeries(this.entity)) {
|
||||||
@ -259,6 +272,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
const wantToDownload = await this.downloadService.confirmSize(size, 'series');
|
const wantToDownload = await this.downloadService.confirmSize(size, 'series');
|
||||||
if (!wantToDownload) { return; }
|
if (!wantToDownload) { return; }
|
||||||
this.downloadInProgress = true;
|
this.downloadInProgress = true;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
this.download$ = this.downloadService.downloadSeries(series).pipe(
|
this.download$ = this.downloadService.downloadSeries(series).pipe(
|
||||||
takeWhile(val => {
|
takeWhile(val => {
|
||||||
return val.state != 'DONE';
|
return val.state != 'DONE';
|
||||||
@ -266,6 +280,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.download$ = null;
|
this.download$ = null;
|
||||||
this.downloadInProgress = false;
|
this.downloadInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ import { SeriesInfoCardsComponent } from './series-info-cards/series-info-cards.
|
|||||||
|
|
||||||
NgbOffcanvasModule, // Series Detail, action of cards
|
NgbOffcanvasModule, // Series Detail, action of cards
|
||||||
NgbNavModule, //Series Detail
|
NgbNavModule, //Series Detail
|
||||||
NgbPaginationModule, // CardDetailLayoutComponent
|
NgbPaginationModule, // EditCollectionTagsComponent
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbProgressbarModule,
|
NgbProgressbarModule,
|
||||||
NgxFileDropModule, // Cover Chooser
|
NgxFileDropModule, // Cover Chooser
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
||||||
<div class="pe-2">
|
<div class="pe-2">
|
||||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" [width]="imageWidth"></app-image>
|
<app-image [imageUrl]="imageUrl" [height]="imageHeight" [width]="imageWidth"></app-image>
|
||||||
|
<div class="not-read-badge" *ngIf="pagesRead === 0 && totalPages > 0"></div>
|
||||||
<span class="download" *ngIf="download$ | async as download">
|
<span class="download" *ngIf="download$ | async as download">
|
||||||
<app-circular-loader [currentValue]="download.progress"></app-circular-loader>
|
<app-circular-loader [currentValue]="download.progress"></app-circular-loader>
|
||||||
<span class="visually-hidden" role="status">
|
<span class="visually-hidden" role="status">
|
||||||
{{download.progress}}% downloaded
|
{{download.progress}}% downloaded
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="progress-banner" *ngIf="totalPages > 0">
|
<div class="progress-banner" *ngIf="pagesRead < totalPages && totalPages > 0 && pagesRead !== totalPages">
|
||||||
<p><ngb-progressbar type="primary" height="5px" [value]="pagesRead" [max]="totalPages"></ngb-progressbar></p>
|
<p><ngb-progressbar type="primary" height="5px" [value]="pagesRead" [max]="totalPages"></ngb-progressbar></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
$image-height: 230px;
|
$image-height: 230px;
|
||||||
$image-width: 160px;
|
$image-width: 160px;
|
||||||
|
$triangle-size: 30px;
|
||||||
|
|
||||||
.download {
|
.download {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
@ -19,8 +20,18 @@ $image-width: 160px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-item-container {
|
.list-item-container {
|
||||||
background: rgb(0,0,0);
|
background: var(--card-list-item-bg-color);
|
||||||
background: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-read-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 108px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0 $triangle-size $triangle-size 0;
|
||||||
|
border-color: transparent var(--primary-color) transparent transparent;
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
@ -8,17 +8,16 @@ import { AccountService } from 'src/app/_services/account.service';
|
|||||||
import { ImageService } from 'src/app/_services/image.service';
|
import { ImageService } from 'src/app/_services/image.service';
|
||||||
import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service';
|
import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||||
import { SeriesService } from 'src/app/_services/series.service';
|
import { SeriesService } from 'src/app/_services/series.service';
|
||||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
|
||||||
import { ActionService } from 'src/app/_services/action.service';
|
import { ActionService } from 'src/app/_services/action.service';
|
||||||
import { EditSeriesModalComponent } from '../_modals/edit-series-modal/edit-series-modal.component';
|
import { EditSeriesModalComponent } from '../_modals/edit-series-modal/edit-series-modal.component';
|
||||||
import { MessageHubService } from 'src/app/_services/message-hub.service';
|
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-series-card',
|
selector: 'app-series-card',
|
||||||
templateUrl: './series-card.component.html',
|
templateUrl: './series-card.component.html',
|
||||||
styleUrls: ['./series-card.component.scss']
|
styleUrls: ['./series-card.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() data!: Series;
|
@Input() data!: Series;
|
||||||
@ -52,9 +51,9 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
constructor(private accountService: AccountService, private router: Router,
|
constructor(private accountService: AccountService, private router: Router,
|
||||||
private seriesService: SeriesService, private toastr: ToastrService,
|
private seriesService: SeriesService, private toastr: ToastrService,
|
||||||
private modalService: NgbModal, private confirmService: ConfirmService,
|
private modalService: NgbModal, private imageService: ImageService,
|
||||||
public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
private actionFactoryService: ActionFactoryService,
|
||||||
private actionService: ActionService, private hubService: MessageHubService) {
|
private actionService: ActionService, private changeDetectionRef: ChangeDetectorRef) {
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||||
@ -72,8 +71,6 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
ngOnChanges(changes: any) {
|
ngOnChanges(changes: any) {
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series));
|
this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series));
|
||||||
//this.imageUrl = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.data.id));
|
|
||||||
this.imageUrl = this.imageService.getSeriesCoverImage(this.data.id); // TODO: Do I need to do this since image now handles updates?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,13 +117,10 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
const modalRef = this.modalService.open(EditSeriesModalComponent, { size: 'lg' });
|
const modalRef = this.modalService.open(EditSeriesModalComponent, { size: 'lg' });
|
||||||
modalRef.componentInstance.series = data;
|
modalRef.componentInstance.series = data;
|
||||||
modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => {
|
modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => {
|
||||||
window.scrollTo(0, 0);
|
|
||||||
if (closeResult.success) {
|
if (closeResult.success) {
|
||||||
if (closeResult.coverImageUpdate) {
|
|
||||||
this.imageUrl = this.imageService.randomize(this.imageService.getSeriesCoverImage(closeResult.series.id));
|
|
||||||
}
|
|
||||||
this.seriesService.getSeries(data.id).subscribe(series => {
|
this.seriesService.getSeries(data.id).subscribe(series => {
|
||||||
this.data = series;
|
this.data = series;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
this.reload.emit(true);
|
this.reload.emit(true);
|
||||||
this.dataChanged.emit(series);
|
this.dataChanged.emit(series);
|
||||||
});
|
});
|
||||||
@ -156,6 +150,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.actionService.markSeriesAsUnread(series, () => {
|
this.actionService.markSeriesAsUnread(series, () => {
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.data.pagesRead = 0;
|
this.data.pagesRead = 0;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dataChanged.emit(series);
|
this.dataChanged.emit(series);
|
||||||
@ -166,6 +161,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.actionService.markSeriesAsRead(series, () => {
|
this.actionService.markSeriesAsRead(series, () => {
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.data.pagesRead = series.pages;
|
this.data.pagesRead = series.pages;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
this.dataChanged.emit(series);
|
this.dataChanged.emit(series);
|
||||||
});
|
});
|
||||||
|
@ -43,8 +43,8 @@
|
|||||||
<ng-container *ngIf="series">
|
<ng-container *ngIf="series">
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||||
<app-icon-and-title label="Format" [clickable]="true" [fontClasses]="'fa ' + utilityService.mangaFormatIcon(series.format)" (click)="handleGoTo(FilterQueryParam.Format, series.format)" title="Format">
|
<app-icon-and-title label="Format" [clickable]="true" [fontClasses]="'fa ' + (series.format | mangaFormatIcon)" (click)="handleGoTo(FilterQueryParam.Format, series.format)" title="Format">
|
||||||
{{utilityService.mangaFormat(series.format)}}
|
{{series.format | mangaFormat}}
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
</div>
|
</div>
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
[isLoading]="isLoading"
|
[isLoading]="isLoading"
|
||||||
[items]="collections"
|
[items]="collections"
|
||||||
[filterOpen]="filterOpen"
|
[filterOpen]="filterOpen"
|
||||||
|
[jumpBarKeys]="jumpbarKeys"
|
||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions" [imageUrl]="item.coverImage" (clicked)="loadCollection(item)"></app-card-item>
|
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions" [imageUrl]="item.coverImage" (clicked)="loadCollection(item)"></app-card-item>
|
||||||
|
@ -4,6 +4,7 @@ import { Router } from '@angular/router';
|
|||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
||||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||||
|
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||||
import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service';
|
import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service';
|
||||||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||||
import { ImageService } from 'src/app/_services/image.service';
|
import { ImageService } from 'src/app/_services/image.service';
|
||||||
@ -19,6 +20,7 @@ export class AllCollectionsComponent implements OnInit {
|
|||||||
isLoading: boolean = true;
|
isLoading: boolean = true;
|
||||||
collections: CollectionTag[] = [];
|
collections: CollectionTag[] = [];
|
||||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||||
|
jumpbarKeys: Array<JumpKey> = [];
|
||||||
|
|
||||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||||
|
|
||||||
@ -44,7 +46,31 @@ export class AllCollectionsComponent implements OnInit {
|
|||||||
this.collectionService.allTags().subscribe(tags => {
|
this.collectionService.allTags().subscribe(tags => {
|
||||||
this.collections = tags;
|
this.collections = tags;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
|
const keys: {[key: string]: number} = {};
|
||||||
|
tags.forEach(s => {
|
||||||
|
let ch = s.title.charAt(0);
|
||||||
|
if (/\d|\#|!|%|@|\(|\)|\^|\*/g.test(ch)) {
|
||||||
|
ch = '#';
|
||||||
|
}
|
||||||
|
if (!keys.hasOwnProperty(ch)) {
|
||||||
|
keys[ch] = 0;
|
||||||
|
}
|
||||||
|
keys[ch] += 1;
|
||||||
|
});
|
||||||
|
this.jumpbarKeys = Object.keys(keys).map(k => {
|
||||||
|
return {
|
||||||
|
key: k,
|
||||||
|
size: keys[k],
|
||||||
|
title: k.toUpperCase()
|
||||||
|
}
|
||||||
|
}).sort((a, b) => {
|
||||||
|
if (a.key < b.key) return -1;
|
||||||
|
if (a.key > b.key) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCollectionActionCallback(action: Action, collectionTag: CollectionTag) {
|
handleCollectionActionCallback(action: Action, collectionTag: CollectionTag) {
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
|
<div #companionBar>
|
||||||
<ng-container title>
|
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
|
||||||
<h2 style="margin-bottom: 0px" *ngIf="collectionTag !== undefined">
|
<ng-container title>
|
||||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
|
<h2 style="margin-bottom: 0px" *ngIf="collectionTag !== undefined">
|
||||||
{{collectionTag.title}}<span *ngIf="collectionTag.promoted"> (<i aria-hidden="true" class="fa fa-angle-double-up"></i>)</span>
|
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||||
</h2>
|
{{collectionTag.title}}<span class="ms-1" *ngIf="collectionTag.promoted">(<i aria-hidden="true" class="fa fa-angle-double-up"></i>)</span>
|
||||||
</ng-container>
|
</h2>
|
||||||
</app-side-nav-companion-bar>
|
</ng-container>
|
||||||
<div class="container-fluid pt-2" *ngIf="collectionTag !== undefined">
|
</app-side-nav-companion-bar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="collectionTag !== undefined" #scrollingBlock>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
||||||
<app-image maxWidth="481px" [imageUrl]="tagImage"></app-image>
|
<app-image maxWidth="481px" [imageUrl]="tagImage"></app-image>
|
||||||
@ -19,15 +22,15 @@
|
|||||||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||||
|
|
||||||
<app-card-detail-layout
|
<app-card-detail-layout
|
||||||
header="Series"
|
header="Series"
|
||||||
[isLoading]="isLoading"
|
[isLoading]="isLoading"
|
||||||
[items]="series"
|
[items]="series"
|
||||||
[pagination]="seriesPagination"
|
[pagination]="seriesPagination"
|
||||||
[filterSettings]="filterSettings"
|
[filterSettings]="filterSettings"
|
||||||
[filterOpen]="filterOpen"
|
[filterOpen]="filterOpen"
|
||||||
(pageChange)="onPageChange($event)"
|
|
||||||
(applyFilter)="updateFilter($event)"
|
[jumpBarKeys]="jumpbarKeys"
|
||||||
>
|
(applyFilter)="updateFilter($event)">
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()"
|
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()"
|
||||||
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"
|
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"
|
||||||
|
@ -13,4 +13,26 @@
|
|||||||
.read-btn--text {
|
.read-btn--text {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, 158px);
|
||||||
|
grid-gap: 0.5rem;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.virtual-scroller, virtual-scroller {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 85px);
|
||||||
|
max-height: calc(var(--vh)*100 - 170px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is responsible for ensuring we scroll down and only tabs and companion bar is visible
|
||||||
|
.main-container {
|
||||||
|
// Height set dynamically by get ScrollingBlockHeight()
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
overscroll-behavior-y: none;
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angular/core';
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
@ -12,6 +13,7 @@ import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilitie
|
|||||||
import { KEY_CODES, UtilityService } from 'src/app/shared/_services/utility.service';
|
import { KEY_CODES, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||||
import { SeriesAddedToCollectionEvent } from 'src/app/_models/events/series-added-to-collection-event';
|
import { SeriesAddedToCollectionEvent } from 'src/app/_models/events/series-added-to-collection-event';
|
||||||
|
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||||
import { Pagination } from 'src/app/_models/pagination';
|
import { Pagination } from 'src/app/_models/pagination';
|
||||||
import { Series } from 'src/app/_models/series';
|
import { Series } from 'src/app/_models/series';
|
||||||
import { FilterEvent, SeriesFilter } from 'src/app/_models/series-filter';
|
import { FilterEvent, SeriesFilter } from 'src/app/_models/series-filter';
|
||||||
@ -29,6 +31,9 @@ import { SeriesService } from 'src/app/_services/series.service';
|
|||||||
})
|
})
|
||||||
export class CollectionDetailComponent implements OnInit, OnDestroy {
|
export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
|
||||||
|
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
|
||||||
|
|
||||||
collectionTag!: CollectionTag;
|
collectionTag!: CollectionTag;
|
||||||
tagImage: string = '';
|
tagImage: string = '';
|
||||||
isLoading: boolean = true;
|
isLoading: boolean = true;
|
||||||
@ -43,8 +48,10 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||||||
filterActiveCheck!: SeriesFilter;
|
filterActiveCheck!: SeriesFilter;
|
||||||
filterActive: boolean = false;
|
filterActive: boolean = false;
|
||||||
|
|
||||||
|
jumpbarKeys: Array<JumpKey> = [];
|
||||||
|
|
||||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||||
|
|
||||||
|
|
||||||
private onDestory: Subject<void> = new Subject<void>();
|
private onDestory: Subject<void> = new Subject<void>();
|
||||||
|
|
||||||
@ -84,11 +91,22 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get ScrollingBlockHeight() {
|
||||||
|
if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)';
|
||||||
|
const navbar = this.document.querySelector('.navbar') as HTMLElement;
|
||||||
|
if (navbar === null) return 'calc(var(--vh)*100)';
|
||||||
|
|
||||||
|
const companionHeight = this.companionBar!.nativeElement.offsetHeight;
|
||||||
|
const navbarHeight = navbar.offsetHeight;
|
||||||
|
const totalHeight = companionHeight + navbarHeight + 21; //21px to account for padding
|
||||||
|
return 'calc(var(--vh)*100 - ' + totalHeight + 'px)';
|
||||||
|
}
|
||||||
|
|
||||||
constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute,
|
constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute,
|
||||||
private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService,
|
private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService,
|
||||||
private modalService: NgbModal, private titleService: Title,
|
private modalService: NgbModal, private titleService: Title,
|
||||||
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
|
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
|
||||||
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService) {
|
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document) {
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
|
|
||||||
const routeId = this.route.snapshot.paramMap.get('id');
|
const routeId = this.route.snapshot.paramMap.get('id');
|
||||||
@ -157,16 +175,40 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageChange(pagination: Pagination) {
|
// onPageChange(pagination: Pagination) {
|
||||||
this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, undefined);
|
// this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, undefined);
|
||||||
this.loadPage();
|
// this.loadPage();
|
||||||
}
|
// }
|
||||||
|
|
||||||
loadPage() {
|
loadPage() {
|
||||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
||||||
this.seriesService.getAllSeries(this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
this.seriesService.getAllSeries(undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
|
||||||
this.series = series.result;
|
this.series = series.result;
|
||||||
this.seriesPagination = series.pagination;
|
this.seriesPagination = series.pagination;
|
||||||
|
|
||||||
|
const keys: {[key: string]: number} = {};
|
||||||
|
series.result.forEach(s => {
|
||||||
|
let ch = s.name.charAt(0);
|
||||||
|
if (/\d|\#|!|%|@|\(|\)|\^|\*/g.test(ch)) {
|
||||||
|
ch = '#';
|
||||||
|
}
|
||||||
|
if (!keys.hasOwnProperty(ch)) {
|
||||||
|
keys[ch] = 0;
|
||||||
|
}
|
||||||
|
keys[ch] += 1;
|
||||||
|
});
|
||||||
|
this.jumpbarKeys = Object.keys(keys).map(k => {
|
||||||
|
return {
|
||||||
|
key: k,
|
||||||
|
size: keys[k],
|
||||||
|
title: k.toUpperCase()
|
||||||
|
}
|
||||||
|
}).sort((a, b) => {
|
||||||
|
if (a.key < b.key) return -1;
|
||||||
|
if (a.key > b.key) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
});
|
});
|
||||||
|
@ -27,8 +27,6 @@
|
|||||||
[filterOpen]="filterOpen"
|
[filterOpen]="filterOpen"
|
||||||
[jumpBarKeys]="jumpKeys"
|
[jumpBarKeys]="jumpKeys"
|
||||||
(applyFilter)="updateFilter($event)"
|
(applyFilter)="updateFilter($event)"
|
||||||
(pageChange)="onPageChange($event)"
|
|
||||||
(pageChangeWithDirection)="handlePaginationChange($event)"
|
|
||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()"
|
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()"
|
||||||
|
@ -106,13 +106,11 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.libraryService.getJumpBar(this.libraryId).subscribe(barDetails => {
|
this.libraryService.getJumpBar(this.libraryId).subscribe(barDetails => {
|
||||||
//console.log('JumpBar: ', barDetails);
|
|
||||||
this.jumpKeys = barDetails;
|
this.jumpKeys = barDetails;
|
||||||
});
|
});
|
||||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||||
|
|
||||||
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
|
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
|
||||||
this.pagination.itemsPerPage = 0; // TODO: Validate what pagination setting is ideal
|
|
||||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
|
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
|
||||||
if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId];
|
if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId];
|
||||||
// Setup filterActiveCheck to check filter against
|
// Setup filterActiveCheck to check filter against
|
||||||
@ -185,12 +183,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||||||
this.loadPage();
|
this.loadPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePaginationChange(direction: 0 | 1) {
|
loadPage() {
|
||||||
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);
|
|
||||||
this.loadPage(direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPage(direction: 0 | 1 = 1) {
|
|
||||||
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
|
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
|
||||||
if (this.filter == undefined) {
|
if (this.filter == undefined) {
|
||||||
this.filter = this.seriesService.createSeriesFilter();
|
this.filter = this.seriesService.createSeriesFilter();
|
||||||
@ -199,32 +192,14 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.loadingSeries = true;
|
this.loadingSeries = true;
|
||||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
||||||
this.seriesService.getSeriesForLibrary(0, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
this.seriesService.getSeriesForLibrary(0, undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
|
||||||
this.series = series.result;
|
this.series = series.result;
|
||||||
|
|
||||||
// For Pagination
|
|
||||||
// if (this.series.length === 0) {
|
|
||||||
// this.series = series.result;
|
|
||||||
// } else {
|
|
||||||
// if (direction === 1) {
|
|
||||||
// //this.series = [...this.series, ...series.result];
|
|
||||||
// this.series.concat(series.result);
|
|
||||||
// } else {
|
|
||||||
// this.series = [...series.result, ...this.series];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
this.pagination = series.pagination;
|
this.pagination = series.pagination;
|
||||||
this.loadingSeries = false;
|
this.loadingSeries = false;
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageChange(pagination: Pagination) {
|
|
||||||
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);
|
|
||||||
this.loadPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
seriesClicked(series: Series) {
|
seriesClicked(series: Series) {
|
||||||
this.router.navigate(['library', this.libraryId, 'series', series.id]);
|
this.router.navigate(['library', this.libraryId, 'series', series.id]);
|
||||||
}
|
}
|
||||||
|
@ -340,6 +340,7 @@
|
|||||||
<option [value]="SortField.Created">Created</option>
|
<option [value]="SortField.Created">Created</option>
|
||||||
<option [value]="SortField.LastModified">Last Modified</option>
|
<option [value]="SortField.LastModified">Last Modified</option>
|
||||||
<option [value]="SortField.LastChapterAdded">Item Added</option>
|
<option [value]="SortField.LastChapterAdded">Item Added</option>
|
||||||
|
<option [value]="SortField.TimeToRead">Time to Read</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { Component, ContentChildren, ElementRef, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { NavigationStart, Router } from '@angular/router';
|
||||||
import { Subject } from 'rxjs';
|
import { fromEvent, Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { debounceTime, filter, takeUntil } from 'rxjs/operators';
|
||||||
import { Chapter } from 'src/app/_models/chapter';
|
import { Chapter } from 'src/app/_models/chapter';
|
||||||
import { MangaFile } from 'src/app/_models/manga-file';
|
import { MangaFile } from 'src/app/_models/manga-file';
|
||||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||||
@ -51,12 +51,34 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(public accountService: AccountService, private router: Router, public navService: NavService,
|
constructor(public accountService: AccountService, private router: Router, public navService: NavService,
|
||||||
private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document,
|
private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document,
|
||||||
private scrollService: ScrollService, private seriesService: SeriesService) { }
|
private scrollService: ScrollService, private seriesService: SeriesService,) { }
|
||||||
|
|
||||||
ngOnInit(): void {}
|
ngOnInit(): void {
|
||||||
|
// setTimeout(() => this.setupScrollChecker(), 1000);
|
||||||
|
// // TODO: on router change, reset the scroll check
|
||||||
|
|
||||||
|
// this.router.events
|
||||||
|
// .pipe(filter(event => event instanceof NavigationStart))
|
||||||
|
// .subscribe((event) => {
|
||||||
|
// setTimeout(() => this.setupScrollChecker(), 1000);
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupScrollChecker() {
|
||||||
|
// const viewportScroller = this.document.querySelector('.viewport-container');
|
||||||
|
// console.log('viewport container', viewportScroller);
|
||||||
|
|
||||||
|
// if (viewportScroller) {
|
||||||
|
// fromEvent(viewportScroller, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded());
|
||||||
|
// } else {
|
||||||
|
// fromEvent(this.document.body, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
@HostListener('body:scroll', [])
|
@HostListener('body:scroll', [])
|
||||||
checkBackToTopNeeded() {
|
checkBackToTopNeeded() {
|
||||||
|
// TODO: This somehow needs to hook into the scrolling for virtual scroll
|
||||||
|
|
||||||
const offset = this.scrollService.scrollPosition;
|
const offset = this.scrollService.scrollPosition;
|
||||||
if (offset > 100) {
|
if (offset > 100) {
|
||||||
this.backToTopNeeded = true;
|
this.backToTopNeeded = true;
|
||||||
|
27
UI/Web/src/app/pipe/manga-format-icon.pipe.ts
Normal file
27
UI/Web/src/app/pipe/manga-format-icon.pipe.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { MangaFormat } from '../_models/manga-format';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the icon class representing the format
|
||||||
|
*/
|
||||||
|
@Pipe({
|
||||||
|
name: 'mangaFormatIcon'
|
||||||
|
})
|
||||||
|
export class MangaFormatIconPipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform(format: MangaFormat): string {
|
||||||
|
switch (format) {
|
||||||
|
case MangaFormat.EPUB:
|
||||||
|
return 'fa-book';
|
||||||
|
case MangaFormat.ARCHIVE:
|
||||||
|
return 'fa-file-archive';
|
||||||
|
case MangaFormat.IMAGE:
|
||||||
|
return 'fa-image';
|
||||||
|
case MangaFormat.PDF:
|
||||||
|
return 'fa-file-pdf';
|
||||||
|
case MangaFormat.UNKNOWN:
|
||||||
|
return 'fa-question';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
27
UI/Web/src/app/pipe/manga-format.pipe.ts
Normal file
27
UI/Web/src/app/pipe/manga-format.pipe.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { MangaFormat } from '../_models/manga-format';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the string name for the format
|
||||||
|
*/
|
||||||
|
@Pipe({
|
||||||
|
name: 'mangaFormat'
|
||||||
|
})
|
||||||
|
export class MangaFormatPipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform(format: MangaFormat): string {
|
||||||
|
switch (format) {
|
||||||
|
case MangaFormat.EPUB:
|
||||||
|
return 'EPUB';
|
||||||
|
case MangaFormat.ARCHIVE:
|
||||||
|
return 'Archive';
|
||||||
|
case MangaFormat.IMAGE:
|
||||||
|
return 'Image';
|
||||||
|
case MangaFormat.PDF:
|
||||||
|
return 'PDF';
|
||||||
|
case MangaFormat.UNKNOWN:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -10,6 +10,8 @@ import { DefaultValuePipe } from './default-value.pipe';
|
|||||||
import { CompactNumberPipe } from './compact-number.pipe';
|
import { CompactNumberPipe } from './compact-number.pipe';
|
||||||
import { LanguageNamePipe } from './language-name.pipe';
|
import { LanguageNamePipe } from './language-name.pipe';
|
||||||
import { AgeRatingPipe } from './age-rating.pipe';
|
import { AgeRatingPipe } from './age-rating.pipe';
|
||||||
|
import { MangaFormatPipe } from './manga-format.pipe';
|
||||||
|
import { MangaFormatIconPipe } from './manga-format-icon.pipe';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -24,7 +26,9 @@ import { AgeRatingPipe } from './age-rating.pipe';
|
|||||||
DefaultValuePipe,
|
DefaultValuePipe,
|
||||||
CompactNumberPipe,
|
CompactNumberPipe,
|
||||||
LanguageNamePipe,
|
LanguageNamePipe,
|
||||||
AgeRatingPipe
|
AgeRatingPipe,
|
||||||
|
MangaFormatPipe,
|
||||||
|
MangaFormatIconPipe
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -39,7 +43,9 @@ import { AgeRatingPipe } from './age-rating.pipe';
|
|||||||
DefaultValuePipe,
|
DefaultValuePipe,
|
||||||
CompactNumberPipe,
|
CompactNumberPipe,
|
||||||
LanguageNamePipe,
|
LanguageNamePipe,
|
||||||
AgeRatingPipe
|
AgeRatingPipe,
|
||||||
|
MangaFormatPipe,
|
||||||
|
MangaFormatIconPipe
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class PipeModule { }
|
export class PipeModule { }
|
||||||
|
@ -3,14 +3,15 @@
|
|||||||
<span *ngIf="actions.length > 0">
|
<span *ngIf="actions.length > 0">
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>
|
||||||
</span>
|
</span>
|
||||||
{{readingList?.title}} <span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
{{readingList?.title}}
|
||||||
|
<span *ngIf="readingList?.promoted" class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||||
</h2>
|
</h2>
|
||||||
<h6 subtitle class="subtitle-with-actionables">{{items.length}} Items</h6>
|
<h6 subtitle class="subtitle-with-actionables">{{items.length}} Items</h6>
|
||||||
</app-side-nav-companion-bar>
|
</app-side-nav-companion-bar>
|
||||||
<div class="container-fluid mt-2" *ngIf="readingList">
|
<div class="container-fluid mt-2" *ngIf="readingList">
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="readingList.coverImage !== '' || readingList.coverImage !== undefined">
|
||||||
<app-image maxWidth="300px" [imageUrl]="readingListImage"></app-image>
|
<app-image maxWidth="300px" [imageUrl]="readingListImage"></app-image>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||||
@ -49,21 +50,25 @@
|
|||||||
Nothing added
|
Nothing added
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- TODO: This needs virtualization -->
|
||||||
<app-dragable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" (itemRemove)="itemRemoved($event)" [accessibilityMode]="accessibilityMode">
|
<app-dragable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" (itemRemove)="itemRemoved($event)" [accessibilityMode]="accessibilityMode">
|
||||||
<ng-template #draggableItem let-item let-position="idx">
|
<ng-template #draggableItem let-item let-position="idx">
|
||||||
<div class="d-flex" style="width: 100%;">
|
<div class="d-flex" style="width: 100%;">
|
||||||
<app-image width="74px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
<app-image width="74px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h5 class="mt-0 mb-1" id="item.id--{{position}}">{{formatTitle(item)}}
|
<h5 class="mt-0 mb-1" id="item.id--{{position}}">
|
||||||
|
{{formatTitle(item)}}
|
||||||
|
<!-- TODO: Create a read/unread badge -->
|
||||||
<span class="badge bg-primary rounded-pill">
|
<span class="badge bg-primary rounded-pill">
|
||||||
<span *ngIf="item.pagesRead > 0 && item.pagesRead < item.pagesTotal">{{item.pagesRead}} / {{item.pagesTotal}}</span>
|
<span *ngIf="item.pagesRead > 0 && item.pagesRead < item.pagesTotal">{{item.pagesRead}} / {{item.pagesTotal}}</span>
|
||||||
<span *ngIf="item.pagesRead === 0">UNREAD</span>
|
<span *ngIf="item.pagesRead === 0">UNREAD</span>
|
||||||
<span *ngIf="item.pagesRead === item.pagesTotal">READ</span>
|
<span *ngIf="item.pagesRead === item.pagesTotal">READ</span>
|
||||||
</span>
|
</span>
|
||||||
</h5>
|
</h5>
|
||||||
<i class="fa {{utilityService.mangaFormatIcon(item.seriesFormat)}}" aria-hidden="true" *ngIf="item.seriesFormat != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(item.seriesFormat)}}"></i>
|
<ng-container *ngIf="item.seriesFormat | mangaFormat as formatString">
|
||||||
<span class="visually-hidden">{{utilityService.mangaFormat(item.seriesFormat)}}</span>
|
<i class="fa {{item.seriesFormat | mangaFormatIcon}}" aria-hidden="true" *ngIf="item.seriesFormat != MangaFormat.UNKNOWN" title="{{formatString}}"></i>
|
||||||
|
<span class="visually-hidden">{{formatString}}</span>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<a href="/library/{{item.libraryId}}/series/{{item.seriesId}}">{{item.seriesName}}</a>
|
<a href="/library/{{item.libraryId}}/series/{{item.seriesId}}">{{item.seriesName}}</a>
|
||||||
<span *ngIf="item.promoted">
|
<span *ngIf="item.promoted">
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
.container-sm {
|
.container-sm {
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.virtual-scroller, virtual-scroller {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 85px);
|
||||||
|
max-height: calc(var(--vh)*100 - 170px);
|
||||||
|
}
|
@ -60,7 +60,7 @@ export class ReadingListDetailComponent implements OnInit {
|
|||||||
|
|
||||||
this.listId = parseInt(listId, 10);
|
this.listId = parseInt(listId, 10);
|
||||||
|
|
||||||
this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
|
//this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
|
||||||
|
|
||||||
this.libraryService.getLibraries().subscribe(libs => {
|
this.libraryService.getLibraries().subscribe(libs => {
|
||||||
|
|
||||||
@ -146,6 +146,7 @@ export class ReadingListDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatTitle(item: ReadingListItem) {
|
formatTitle(item: ReadingListItem) {
|
||||||
|
// TODO: Use new app-entity-title component instead
|
||||||
if (item.chapterNumber === '0') {
|
if (item.chapterNumber === '0') {
|
||||||
return 'Volume ' + item.volumeNumber;
|
return 'Volume ' + item.volumeNumber;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
[actions]="actions"
|
[actions]="actions"
|
||||||
[pagination]="pagination"
|
[pagination]="pagination"
|
||||||
[filteringDisabled]="true"
|
[filteringDisabled]="true"
|
||||||
(pageChange)="onPageChange($event)"
|
|
||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)" (clicked)="handleClick(item)"></app-card-item>
|
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)" (clicked)="handleClick(item)"></app-card-item>
|
||||||
|
@ -77,7 +77,7 @@ export class ReadingListsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.loadingLists = true;
|
this.loadingLists = true;
|
||||||
|
|
||||||
this.readingListService.getReadingLists(true, this.pagination?.currentPage, this.pagination?.itemsPerPage).pipe(take(1)).subscribe((readingLists: PaginatedResult<ReadingList[]>) => {
|
this.readingListService.getReadingLists(true).pipe(take(1)).subscribe((readingLists: PaginatedResult<ReadingList[]>) => {
|
||||||
this.lists = readingLists.result;
|
this.lists = readingLists.result;
|
||||||
this.pagination = readingLists.pagination;
|
this.pagination = readingLists.pagination;
|
||||||
this.loadingLists = false;
|
this.loadingLists = false;
|
||||||
|
@ -1,57 +1,57 @@
|
|||||||
<div #companionBar>
|
<div #companionBar>
|
||||||
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasExtras]="true" [extraDrawer]="extrasDrawer">
|
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasExtras]="true" [extraDrawer]="extrasDrawer">
|
||||||
<ng-container title>
|
<ng-container title>
|
||||||
<h2 style="margin-bottom: 0px">
|
<h2 style="margin-bottom: 0px">
|
||||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-v"></app-card-actionables>
|
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||||
<span>{{series?.name}}</span>
|
<span>{{series?.name}}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container subtitle *ngIf="series?.localizedName !== series?.name">
|
<ng-container subtitle *ngIf="series?.localizedName !== series?.name">
|
||||||
<h6 class="subtitle-with-actionables" title="Localized Name">{{series?.localizedName}}</h6>
|
<h6 class="subtitle-with-actionables" title="Localized Name">{{series?.localizedName}}</h6>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #extrasDrawer let-offcanvas>
|
<ng-template #extrasDrawer let-offcanvas>
|
||||||
<div class="offcanvas-header">
|
<div class="offcanvas-header">
|
||||||
<h4 class="offcanvas-title" id="offcanvas-basic-title">Page Settings</h4>
|
<h4 class="offcanvas-title" id="offcanvas-basic-title">Page Settings</h4>
|
||||||
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
|
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="offcanvas-body">
|
<div class="offcanvas-body">
|
||||||
<form [formGroup]="pageExtrasGroup">
|
<form [formGroup]="pageExtrasGroup">
|
||||||
<!-- <div class="row g-0">
|
<!-- <div class="row g-0">
|
||||||
<div class="col-md-12 col-sm-12 pe-2 mb-3">
|
<div class="col-md-12 col-sm-12 pe-2 mb-3">
|
||||||
<label for="settings-book-reading-direction" class="form-label">Sort Order</label>
|
<label for="settings-book-reading-direction" class="form-label">Sort Order</label>
|
||||||
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
|
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
|
||||||
<i class="fa fa-arrow-up" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
|
<i class="fa fa-arrow-up" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
|
||||||
<ng-template #descSort>
|
<ng-template #descSort>
|
||||||
<i class="fa fa-arrow-down" title="Descending"></i>
|
<i class="fa fa-arrow-down" title="Descending"></i>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="sortingOption">
|
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="sortingOption">
|
||||||
<option *ngFor="let opt of sortingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
<option *ngFor="let opt of sortingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-12 col-sm-12 pe-2 mb-3">
|
<div class="col-md-12 col-sm-12 pe-2 mb-3">
|
||||||
<label id="list-layout-mode-label" class="form-label">Layout Mode</label>
|
<label id="list-layout-mode-label" class="form-label">Layout Mode</label>
|
||||||
<br/>
|
<br/>
|
||||||
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
|
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
|
||||||
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.Cards" class="btn-check" id="layout-mode-default" autocomplete="off">
|
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.Cards" class="btn-check" id="layout-mode-default" autocomplete="off">
|
||||||
<label class="btn btn-outline-primary" for="layout-mode-default">Card</label>
|
<label class="btn btn-outline-primary" for="layout-mode-default">Card</label>
|
||||||
|
|
||||||
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.List" class="btn-check" id="layout-mode-col1" autocomplete="off">
|
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.List" class="btn-check" id="layout-mode-col1" autocomplete="off">
|
||||||
<label class="btn btn-outline-primary" for="layout-mode-col1">List</label>
|
<label class="btn btn-outline-primary" for="layout-mode-col1">List</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</ng-template>
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
|
|
||||||
</app-side-nav-companion-bar>
|
</app-side-nav-companion-bar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="series !== undefined" #scrollingBlock>
|
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="series !== undefined" #scrollingBlock>
|
||||||
@ -122,12 +122,16 @@
|
|||||||
<ng-container *ngIf="!item.isChapter; else chapterCardItem">
|
<ng-container *ngIf="!item.isChapter; else chapterCardItem">
|
||||||
<app-card-item class="col-auto mt-2 mb-2" *ngIf="item.volume.number != 0" [entity]="item.volume" [title]="item.volume.name" (click)="openVolume(item.volume)"
|
<app-card-item class="col-auto mt-2 mb-2" *ngIf="item.volume.number != 0" [entity]="item.volume" [title]="item.volume.name" (click)="openVolume(item.volume)"
|
||||||
[imageUrl]="imageService.getVolumeCoverImage(item.volume.id)"
|
[imageUrl]="imageService.getVolumeCoverImage(item.volume.id)"
|
||||||
[read]="item.volume.pagesRead" [total]="item.volume.pages" [actions]="volumeActions" (selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)" [selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true"></app-card-item>
|
[read]="item.volume.pagesRead" [total]="item.volume.pages" [actions]="volumeActions"
|
||||||
|
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, volumes.length, $event)"
|
||||||
|
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #chapterCardItem>
|
<ng-template #chapterCardItem>
|
||||||
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.chapter.isSpecial" [entity]="item.chapter" [title]="item.chapter.title" (click)="openChapter(item.chapter)"
|
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.chapter.isSpecial" [entity]="item.chapter" [title]="item.chapter.title" (click)="openChapter(item.chapter)"
|
||||||
[imageUrl]="imageService.getChapterCoverImage(item.chapter.id)"
|
[imageUrl]="imageService.getChapterCoverImage(item.chapter.id)"
|
||||||
[read]="item.chapter.pagesRead" [total]="item.chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('chapter', idx, storyChapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item>
|
[read]="item.chapter.pagesRead" [total]="item.chapter.pages" [actions]="chapterActions"
|
||||||
|
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, storyChapters.length, $event)"
|
||||||
|
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
@ -170,8 +174,8 @@
|
|||||||
<app-card-item class="col-auto mt-2 mb-2" [entity]="item" [title]="item.name" (click)="openVolume(item)"
|
<app-card-item class="col-auto mt-2 mb-2" [entity]="item" [title]="item.name" (click)="openVolume(item)"
|
||||||
[imageUrl]="imageService.getVolumeCoverImage(item.id)"
|
[imageUrl]="imageService.getVolumeCoverImage(item.id)"
|
||||||
[read]="item.pagesRead" [total]="item.pages" [actions]="volumeActions"
|
[read]="item.pagesRead" [total]="item.pages" [actions]="volumeActions"
|
||||||
(selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)"
|
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, volumes.length, $event)"
|
||||||
[selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true">
|
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
|
||||||
</app-card-item>
|
</app-card-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
@ -203,8 +207,8 @@
|
|||||||
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.isSpecial" [entity]="item" [title]="item.title" (click)="openChapter(item)"
|
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.isSpecial" [entity]="item" [title]="item.title" (click)="openChapter(item)"
|
||||||
[imageUrl]="imageService.getChapterCoverImage(item.id)"
|
[imageUrl]="imageService.getChapterCoverImage(item.id)"
|
||||||
[read]="item.pagesRead" [total]="item.pages" [actions]="chapterActions"
|
[read]="item.pagesRead" [total]="item.pages" [actions]="chapterActions"
|
||||||
(selection)="bulkSelectionService.handleCardSelection('chapter', idx, chapters.length, $event)"
|
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, chapters.length, $event)"
|
||||||
[selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true">
|
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
|
||||||
<ng-container title>
|
<ng-container title>
|
||||||
<app-entity-title [libraryType]="libraryType" [entity]="item" [seriesName]="series.name" [includeVolume]="true"></app-entity-title>
|
<app-entity-title [libraryType]="libraryType" [entity]="item" [seriesName]="series.name" [includeVolume]="true"></app-entity-title>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -239,8 +243,8 @@
|
|||||||
<app-card-item class="col-auto mt-2 mb-2" [entity]="item" [title]="item.title || item.range" (click)="openChapter(item)"
|
<app-card-item class="col-auto mt-2 mb-2" [entity]="item" [title]="item.title || item.range" (click)="openChapter(item)"
|
||||||
[imageUrl]="imageService.getChapterCoverImage(item.id)"
|
[imageUrl]="imageService.getChapterCoverImage(item.id)"
|
||||||
[read]="item.pagesRead" [total]="item.pages" [actions]="chapterActions"
|
[read]="item.pagesRead" [total]="item.pages" [actions]="chapterActions"
|
||||||
(selection)="bulkSelectionService.handleCardSelection('special', idx, chapters.length, $event)"
|
(selection)="bulkSelectionService.handleCardSelection('special', scroll.viewPortInfo.startIndexWithBuffer + idx, chapters.length, $event)"
|
||||||
[selected]="bulkSelectionService.isCardSelected('special', idx)" [allowSelection]="true">
|
[selected]="bulkSelectionService.isCardSelected('special', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
|
||||||
</app-card-item>
|
</app-card-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
// This is responsible for ensuring we scroll down and only tabs and companion bar is visible
|
// This is responsible for ensuring we scroll down and only tabs and companion bar is visible
|
||||||
.main-container {
|
.main-container {
|
||||||
// Height set dynamically by getHeight()
|
// Height set dynamically by get ScrollingBlockHeight()
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, Renderer2, AfterViewInit, Inject } from '@angular/core';
|
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, AfterViewInit, Inject, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { forkJoin, fromEvent, Subject, debounceTime } from 'rxjs';
|
import { forkJoin, Subject } from 'rxjs';
|
||||||
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
|
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||||
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
|
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
|
||||||
@ -61,7 +61,8 @@ interface StoryLineItem {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-series-detail',
|
selector: 'app-series-detail',
|
||||||
templateUrl: './series-detail.component.html',
|
templateUrl: './series-detail.component.html',
|
||||||
styleUrls: ['./series-detail.component.scss']
|
styleUrls: ['./series-detail.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
|
|
||||||
@ -177,6 +178,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
case Action.AddToReadingList:
|
case Action.AddToReadingList:
|
||||||
this.actionService.addMultipleToReadingList(seriesId, selectedVolumeIds, chapters, () => {
|
this.actionService.addMultipleToReadingList(seriesId, selectedVolumeIds, chapters, () => {
|
||||||
this.actionInProgress = false;
|
this.actionInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
this.bulkSelectionService.deselectAll();
|
this.bulkSelectionService.deselectAll();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -184,6 +186,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.actionService.markMultipleAsRead(seriesId, selectedVolumeIds, chapters, () => {
|
this.actionService.markMultipleAsRead(seriesId, selectedVolumeIds, chapters, () => {
|
||||||
this.setContinuePoint();
|
this.setContinuePoint();
|
||||||
this.actionInProgress = false;
|
this.actionInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
this.bulkSelectionService.deselectAll();
|
this.bulkSelectionService.deselectAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -192,6 +195,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.actionService.markMultipleAsUnread(seriesId, selectedVolumeIds, chapters, () => {
|
this.actionService.markMultipleAsUnread(seriesId, selectedVolumeIds, chapters, () => {
|
||||||
this.setContinuePoint();
|
this.setContinuePoint();
|
||||||
this.actionInProgress = false;
|
this.actionInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
this.bulkSelectionService.deselectAll();
|
this.bulkSelectionService.deselectAll();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -242,8 +246,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
private downloadService: DownloadService, private actionService: ActionService,
|
private downloadService: DownloadService, private actionService: ActionService,
|
||||||
public imageSerivce: ImageService, private messageHub: MessageHubService,
|
public imageSerivce: ImageService, private messageHub: MessageHubService,
|
||||||
private readingListService: ReadingListService, public navService: NavService,
|
private readingListService: ReadingListService, public navService: NavService,
|
||||||
private offcanvasService: NgbOffcanvas, private renderer: Renderer2,
|
private offcanvasService: NgbOffcanvas, @Inject(DOCUMENT) private document: Document,
|
||||||
@Inject(DOCUMENT) private document: Document
|
private changeDetectionRef: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
@ -252,6 +256,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.hasDownloadingRole = this.accountService.hasDownloadRole(user);
|
this.hasDownloadingRole = this.accountService.hasDownloadRole(user);
|
||||||
this.renderMode = user.preferences.globalPageLayoutMode;
|
this.renderMode = user.preferences.globalPageLayoutMode;
|
||||||
this.pageExtrasGroup.get('renderMode')?.setValue(this.renderMode);
|
this.pageExtrasGroup.get('renderMode')?.setValue(this.renderMode);
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -283,10 +288,12 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.seriesId = parseInt(routeId, 10);
|
this.seriesId = parseInt(routeId, 10);
|
||||||
this.libraryId = parseInt(libraryId, 10);
|
this.libraryId = parseInt(libraryId, 10);
|
||||||
this.seriesImage = this.imageService.getSeriesCoverImage(this.seriesId);
|
this.seriesImage = this.imageService.getSeriesCoverImage(this.seriesId);
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
this.loadSeries(this.seriesId);
|
this.loadSeries(this.seriesId);
|
||||||
|
|
||||||
this.pageExtrasGroup.get('renderMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((val: PageLayoutMode) => {
|
this.pageExtrasGroup.get('renderMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((val: PageLayoutMode) => {
|
||||||
this.renderMode = val;
|
this.renderMode = val;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,15 +303,16 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.initScroll();
|
// this.initScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
initScroll() {
|
// initScroll() {
|
||||||
if (this.scrollingBlock === undefined || this.scrollingBlock.nativeElement === undefined) {
|
// // TODO: Remove this code?
|
||||||
setTimeout(() => {this.initScroll()}, 10);
|
// if (this.scrollingBlock === undefined || this.scrollingBlock.nativeElement === undefined) {
|
||||||
return;
|
// setTimeout(() => {this.initScroll()}, 10);
|
||||||
}
|
// return;
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
@HostListener('document:keydown.shift', ['$event'])
|
@HostListener('document:keydown.shift', ['$event'])
|
||||||
handleKeypress(event: KeyboardEvent) {
|
handleKeypress(event: KeyboardEvent) {
|
||||||
@ -322,10 +330,12 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
|
|
||||||
onNavChange(event: NgbNavChangeEvent) {
|
onNavChange(event: NgbNavChangeEvent) {
|
||||||
this.bulkSelectionService.deselectAll();
|
this.bulkSelectionService.deselectAll();
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSeriesActionCallback(action: Action, series: Series) {
|
handleSeriesActionCallback(action: Action, series: Series) {
|
||||||
this.actionInProgress = true;
|
this.actionInProgress = true;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
switch(action) {
|
switch(action) {
|
||||||
case(Action.MarkAsRead):
|
case(Action.MarkAsRead):
|
||||||
this.actionService.markSeriesAsRead(series, (series: Series) => {
|
this.actionService.markSeriesAsRead(series, (series: Series) => {
|
||||||
@ -416,6 +426,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
async deleteSeries(series: Series) {
|
async deleteSeries(series: Series) {
|
||||||
this.actionService.deleteSeries(series, (result: boolean) => {
|
this.actionService.deleteSeries(series, (result: boolean) => {
|
||||||
this.actionInProgress = false;
|
this.actionInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
if (result) {
|
if (result) {
|
||||||
this.router.navigate(['library', this.libraryId]);
|
this.router.navigate(['library', this.libraryId]);
|
||||||
}
|
}
|
||||||
@ -426,6 +437,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.seriesService.getMetadata(seriesId).subscribe(metadata => this.seriesMetadata = metadata);
|
this.seriesService.getMetadata(seriesId).subscribe(metadata => this.seriesMetadata = metadata);
|
||||||
this.readingListService.getReadingListsForSeries(seriesId).subscribe(lists => {
|
this.readingListService.getReadingListsForSeries(seriesId).subscribe(lists => {
|
||||||
this.readingLists = lists;
|
this.readingLists = lists;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
});
|
});
|
||||||
this.setContinuePoint();
|
this.setContinuePoint();
|
||||||
|
|
||||||
@ -464,6 +476,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
];
|
];
|
||||||
if (this.relations.length > 0) {
|
if (this.relations.length > 0) {
|
||||||
this.hasRelations = true;
|
this.hasRelations = true;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -487,6 +500,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
|
|
||||||
this.updateSelectedTab();
|
this.updateSelectedTab();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
});
|
});
|
||||||
}, err => {
|
}, err => {
|
||||||
this.router.navigateByUrl('/libraries');
|
this.router.navigateByUrl('/libraries');
|
||||||
@ -523,11 +537,18 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
|
|
||||||
createHTML() {
|
createHTML() {
|
||||||
this.userReview = (this.series.userReview === null ? '' : this.series.userReview).replace(/\n/g, '<br>');
|
this.userReview = (this.series.userReview === null ? '' : this.series.userReview).replace(/\n/g, '<br>');
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
setContinuePoint() {
|
setContinuePoint() {
|
||||||
this.readerService.hasSeriesProgress(this.seriesId).subscribe(hasProgress => this.hasReadingProgress = hasProgress);
|
this.readerService.hasSeriesProgress(this.seriesId).subscribe(hasProgress => {
|
||||||
this.readerService.getCurrentChapter(this.seriesId).subscribe(chapter => this.currentlyReadingChapter = chapter);
|
this.hasReadingProgress = hasProgress;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
|
});
|
||||||
|
this.readerService.getCurrentChapter(this.seriesId).subscribe(chapter => {
|
||||||
|
this.currentlyReadingChapter = chapter;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
markVolumeAsRead(vol: Volume) {
|
markVolumeAsRead(vol: Volume) {
|
||||||
@ -538,6 +559,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.actionService.markVolumeAsRead(this.seriesId, vol, () => {
|
this.actionService.markVolumeAsRead(this.seriesId, vol, () => {
|
||||||
this.setContinuePoint();
|
this.setContinuePoint();
|
||||||
this.actionInProgress = false;
|
this.actionInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -549,6 +571,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.actionService.markVolumeAsUnread(this.seriesId, vol, () => {
|
this.actionService.markVolumeAsUnread(this.seriesId, vol, () => {
|
||||||
this.setContinuePoint();
|
this.setContinuePoint();
|
||||||
this.actionInProgress = false;
|
this.actionInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,6 +583,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.actionService.markChapterAsRead(this.seriesId, chapter, () => {
|
this.actionService.markChapterAsRead(this.seriesId, chapter, () => {
|
||||||
this.setContinuePoint();
|
this.setContinuePoint();
|
||||||
this.actionInProgress = false;
|
this.actionInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -571,6 +595,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => {
|
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => {
|
||||||
this.setContinuePoint();
|
this.setContinuePoint();
|
||||||
this.actionInProgress = false;
|
this.actionInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -626,7 +651,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isNullOrEmpty(val: string) {
|
isNullOrEmpty(val: string) {
|
||||||
return val === null || val === undefined || val === '';
|
return val === null || val === undefined || val === ''; // TODO: Validate if this code is used
|
||||||
}
|
}
|
||||||
|
|
||||||
openViewInfo(data: Volume | Chapter) {
|
openViewInfo(data: Volume | Chapter) {
|
||||||
@ -645,6 +670,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
if (closeResult.success) {
|
if (closeResult.success) {
|
||||||
this.seriesService.getSeries(this.seriesId).subscribe(s => {
|
this.seriesService.getSeries(this.seriesId).subscribe(s => {
|
||||||
this.series = s;
|
this.series = s;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.loadSeries(this.seriesId);
|
this.loadSeries(this.seriesId);
|
||||||
@ -693,12 +719,14 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
const wantToDownload = await this.downloadService.confirmSize(size, 'series');
|
const wantToDownload = await this.downloadService.confirmSize(size, 'series');
|
||||||
if (!wantToDownload) { return; }
|
if (!wantToDownload) { return; }
|
||||||
this.downloadInProgress = true;
|
this.downloadInProgress = true;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
this.downloadService.downloadSeries(this.series).pipe(
|
this.downloadService.downloadSeries(this.series).pipe(
|
||||||
takeWhile(val => {
|
takeWhile(val => {
|
||||||
return val.state != 'DONE';
|
return val.state != 'DONE';
|
||||||
}),
|
}),
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.downloadInProgress = false;
|
this.downloadInProgress = false;
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
})).subscribe(() => {/* No Operation */});;
|
})).subscribe(() => {/* No Operation */});;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -96,47 +96,6 @@ export class UtilityService {
|
|||||||
return input.toUpperCase().replace(reg, '').includes(filter.toUpperCase().replace(reg, ''));
|
return input.toUpperCase().replace(reg, '').includes(filter.toUpperCase().replace(reg, ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
mangaFormat(format: MangaFormat): string {
|
|
||||||
switch (format) {
|
|
||||||
case MangaFormat.EPUB:
|
|
||||||
return 'EPUB';
|
|
||||||
case MangaFormat.ARCHIVE:
|
|
||||||
return 'Archive';
|
|
||||||
case MangaFormat.IMAGE:
|
|
||||||
return 'Image';
|
|
||||||
case MangaFormat.PDF:
|
|
||||||
return 'PDF';
|
|
||||||
case MangaFormat.UNKNOWN:
|
|
||||||
return 'Unknown';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mangaFormatIcon(format: MangaFormat): string {
|
|
||||||
switch (format) {
|
|
||||||
case MangaFormat.EPUB:
|
|
||||||
return 'fa-book';
|
|
||||||
case MangaFormat.ARCHIVE:
|
|
||||||
return 'fa-file-archive';
|
|
||||||
case MangaFormat.IMAGE:
|
|
||||||
return 'fa-image';
|
|
||||||
case MangaFormat.PDF:
|
|
||||||
return 'fa-file-pdf';
|
|
||||||
case MangaFormat.UNKNOWN:
|
|
||||||
return 'fa-question';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getLibraryTypeIcon(format: LibraryType) {
|
|
||||||
switch (format) {
|
|
||||||
case LibraryType.Book:
|
|
||||||
return 'fa-book';
|
|
||||||
case LibraryType.Comic:
|
|
||||||
case LibraryType.Manga:
|
|
||||||
return 'fa-book-open';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isVolume(d: any) {
|
isVolume(d: any) {
|
||||||
return d != null && d.hasOwnProperty('chapters');
|
return d != null && d.hasOwnProperty('chapters');
|
||||||
}
|
}
|
||||||
@ -237,4 +196,5 @@ export class UtilityService {
|
|||||||
|| document.body.clientHeight;
|
|| document.body.clientHeight;
|
||||||
return [windowWidth, windowHeight];
|
return [windowWidth, windowHeight];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ElementRef, Input, OnChanges, OnDestroy, Renderer2, ViewChild } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, OnDestroy, Renderer2, ViewChild } from '@angular/core';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
import { CoverUpdateEvent } from 'src/app/_models/events/cover-update-event';
|
import { CoverUpdateEvent } from 'src/app/_models/events/cover-update-event';
|
||||||
@ -11,7 +11,8 @@ import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-image',
|
selector: 'app-image',
|
||||||
templateUrl: './image.component.html',
|
templateUrl: './image.component.html',
|
||||||
styleUrls: ['./image.component.scss']
|
styleUrls: ['./image.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class ImageComponent implements OnChanges, OnDestroy {
|
export class ImageComponent implements OnChanges, OnDestroy {
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ export class ImageComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
constructor(public imageService: ImageService, private renderer: Renderer2, private hubService: MessageHubService) {
|
constructor(public imageService: ImageService, private renderer: Renderer2, private hubService: MessageHubService, private changeDetectionRef: ChangeDetectorRef) {
|
||||||
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
|
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
|
||||||
if (!this.processEvents) return;
|
if (!this.processEvents) return;
|
||||||
if (res.event === EVENTS.CoverUpdate) {
|
if (res.event === EVENTS.CoverUpdate) {
|
||||||
@ -65,6 +66,7 @@ export class ImageComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
if (id === (updateEvent.id + '')) {
|
if (id === (updateEvent.id + '')) {
|
||||||
this.imageUrl = this.imageService.randomize(this.imageUrl);
|
this.imageUrl = this.imageService.randomize(this.imageUrl);
|
||||||
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<ng-container *ngIf="format != MangaFormat.UNKNOWN">
|
<ng-container *ngIf="format != MangaFormat.UNKNOWN">
|
||||||
<i class="fa {{utilityService.mangaFormatIcon(format)}}" aria-hidden="true" title="{{utilityService.mangaFormat(format)}}"></i>
|
<i class="fa {{format | mangaFormatIcon}}" aria-hidden="true" title="{{format | mangaFormat}}"></i>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</ng-container>
|
</ng-container>
|
@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-side-nav-item *ngFor="let library of libraries | filter: filterLibrary" [link]="'/library/' + library.id + '/'"
|
<app-side-nav-item *ngFor="let library of libraries | filter: filterLibrary" [link]="'/library/' + library.id + '/'"
|
||||||
[icon]="utilityService.getLibraryTypeIcon(library.type)" [title]="library.name" [comparisonMethod]="'startsWith'">
|
[icon]="getLibraryTypeIcon(library.type)" [title]="library.name" [comparisonMethod]="'startsWith'">
|
||||||
<ng-container actions>
|
<ng-container actions>
|
||||||
<app-card-actionables [actions]="actions" [labelBy]="library.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event, library)"></app-card-actionables>
|
<app-card-actionables [actions]="actions" [labelBy]="library.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event, library)"></app-card-actionables>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { NavigationEnd, Router } from '@angular/router';
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { filter, map, take, takeUntil, takeWhile } from 'rxjs/operators';
|
import { filter, map, take, takeUntil } from 'rxjs/operators';
|
||||||
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||||
import { Breakpoint, UtilityService } from '../../shared/_services/utility.service';
|
import { Breakpoint, UtilityService } from '../../shared/_services/utility.service';
|
||||||
import { Library } from '../../_models/library';
|
import { Library, LibraryType } from '../../_models/library';
|
||||||
import { User } from '../../_models/user';
|
import { User } from '../../_models/user';
|
||||||
import { AccountService } from '../../_services/account.service';
|
import { AccountService } from '../../_services/account.service';
|
||||||
import { Action, ActionFactoryService, ActionItem } from '../../_services/action-factory.service';
|
import { Action, ActionFactoryService, ActionItem } from '../../_services/action-factory.service';
|
||||||
@ -99,4 +99,14 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLibraryTypeIcon(format: LibraryType) {
|
||||||
|
switch (format) {
|
||||||
|
case LibraryType.Book:
|
||||||
|
return 'fa-book';
|
||||||
|
case LibraryType.Comic:
|
||||||
|
case LibraryType.Manga:
|
||||||
|
return 'fa-book-open';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -12,4 +12,4 @@
|
|||||||
background-color: var(--card-overlay-hover-bg-color);
|
background-color: var(--card-overlay-hover-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -234,4 +234,7 @@
|
|||||||
--bulk-selection-text-color: var(--navbar-text-color);
|
--bulk-selection-text-color: var(--navbar-text-color);
|
||||||
--bulk-selection-highlight-text-color: var(--primary-color);
|
--bulk-selection-highlight-text-color: var(--primary-color);
|
||||||
|
|
||||||
|
/* List Card Item */
|
||||||
|
--card-list-item-bg-color: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user