mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-23 15:30:34 -04:00
Filtering First Pass (#442)
# Added - Added: Added "In Progress" view to see everything you are currently reading - Added: Added the ability to filter series based on format from "In Progress", "Recently Added", "Library Detail" pages. - Added: Added total items to the above pages to showcase total series within Kavita ============================== * Added filtering to recently added * Cleaned up the documentation on the APIs and removed params no longer needed. * Implemented Filtering on library detail, in progress, and recently added for format. UI is non-final. * Moved filtering to an expander panel * Cleaned up filtering UI a bit * Cleaned up some code and added titles on touched pages * Fixed recently added not re-rendering page * Removed commented out code * Version bump * Added an animation to the filtering section * Stashing changes, needing to switch lazy loading libraries out due to current version not trigging on dom mutation events * Finally fixed all the lazy loading issues and made it so pagination works without reloading the whole page.
This commit is contained in:
parent
434bcdae4c
commit
b9f20f4d19
@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
@ -27,12 +28,12 @@ namespace API.Controllers
|
|||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpPost]
|
||||||
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams)
|
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
var series =
|
var series =
|
||||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id, userParams);
|
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id, userParams, filterDto);
|
||||||
|
|
||||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||||
if (series == null) return BadRequest("Could not get series for library");
|
if (series == null) return BadRequest("Could not get series for library");
|
||||||
@ -119,7 +120,7 @@ namespace API.Controllers
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost("update")]
|
||||||
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
|
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("{UserName} is updating Series {SeriesName}", User.GetUsername(), updateSeries.Name);
|
_logger.LogInformation("{UserName} is updating Series {SeriesName}", User.GetUsername(), updateSeries.Name);
|
||||||
@ -147,12 +148,12 @@ namespace API.Controllers
|
|||||||
return BadRequest("There was an error with updating the series");
|
return BadRequest("There was an error with updating the series");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("recently-added")]
|
[HttpPost("recently-added")]
|
||||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded([FromQuery] UserParams userParams, int libraryId = 0)
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
var series =
|
var series =
|
||||||
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, user.Id, userParams);
|
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, user.Id, userParams, filterDto);
|
||||||
|
|
||||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||||
if (series == null) return BadRequest("Could not get series");
|
if (series == null) return BadRequest("Could not get series");
|
||||||
@ -164,12 +165,11 @@ namespace API.Controllers
|
|||||||
return Ok(series);
|
return Ok(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("in-progress")]
|
[HttpPost("in-progress")]
|
||||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetInProgress(int libraryId = 0, int limit = 20)
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetInProgress(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
if (user == null) return Ok(Array.Empty<SeriesDto>());
|
return Ok((await _unitOfWork.SeriesRepository.GetInProgress(user.Id, libraryId, userParams, filterDto)).DistinctBy(s => s.Name));
|
||||||
return Ok(await _unitOfWork.SeriesRepository.GetInProgress(user.Id, libraryId, limit));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
10
API/DTOs/Filtering/FilterDto.cs
Normal file
10
API/DTOs/Filtering/FilterDto.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
|
namespace API.DTOs.Filtering
|
||||||
|
{
|
||||||
|
public class FilterDto
|
||||||
|
{
|
||||||
|
public MangaFormat? MangaFormat { get; init; } = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
@ -75,10 +76,10 @@ namespace API.Data
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams)
|
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter)
|
||||||
{
|
{
|
||||||
var query = _context.Series
|
var query = _context.Series
|
||||||
.Where(s => s.LibraryId == libraryId)
|
.Where(s => s.LibraryId == libraryId && (filter.MangaFormat == null || s.Format == filter.MangaFormat))
|
||||||
.OrderBy(s => s.SortName)
|
.OrderBy(s => s.SortName)
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
.AsNoTracking();
|
.AsNoTracking();
|
||||||
@ -120,7 +121,7 @@ namespace API.Data
|
|||||||
|
|
||||||
private void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
private void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
||||||
{
|
{
|
||||||
foreach (var v in volumes.Where(vdto => vdto.Number == 0))
|
foreach (var v in volumes.Where(vDto => vDto.Number == 0))
|
||||||
{
|
{
|
||||||
v.Chapters = v.Chapters.OrderBy(x => x.Range, _naturalSortComparer).ToList();
|
v.Chapters = v.Chapters.OrderBy(x => x.Range, _naturalSortComparer).ToList();
|
||||||
}
|
}
|
||||||
@ -302,8 +303,9 @@ namespace API.Data
|
|||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
||||||
/// <param name="userParams">Contains pagination information</param>
|
/// <param name="userParams">Contains pagination information</param>
|
||||||
|
/// <param name="filter">Optional filter on query</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams)
|
public async Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter)
|
||||||
{
|
{
|
||||||
if (libraryId == 0)
|
if (libraryId == 0)
|
||||||
{
|
{
|
||||||
@ -315,7 +317,7 @@ namespace API.Data
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var allQuery = _context.Series
|
var allQuery = _context.Series
|
||||||
.Where(s => userLibraries.Contains(s.LibraryId))
|
.Where(s => userLibraries.Contains(s.LibraryId) && (filter.MangaFormat == null || s.Format == filter.MangaFormat))
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.OrderByDescending(s => s.Created)
|
.OrderByDescending(s => s.Created)
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
@ -325,7 +327,7 @@ namespace API.Data
|
|||||||
}
|
}
|
||||||
|
|
||||||
var query = _context.Series
|
var query = _context.Series
|
||||||
.Where(s => s.LibraryId == libraryId)
|
.Where(s => s.LibraryId == libraryId && (filter.MangaFormat == null || s.Format == filter.MangaFormat))
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.OrderByDescending(s => s.Created)
|
.OrderByDescending(s => s.Created)
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
@ -338,19 +340,22 @@ namespace API.Data
|
|||||||
/// Returns Series that the user has some partial progress on
|
/// Returns Series that the user has some partial progress on
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="libraryId"></param>
|
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
||||||
/// <param name="limit"></param>
|
/// <param name="userParams">Pagination information</param>
|
||||||
|
/// <param name="filter">Optional (default null) filter on query</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IEnumerable<SeriesDto>> GetInProgress(int userId, int libraryId, int limit)
|
public async Task<PagedList<SeriesDto>> GetInProgress(int userId, int libraryId, UserParams userParams, FilterDto filter)
|
||||||
{
|
{
|
||||||
|
|
||||||
var series = _context.Series
|
var series = _context.Series
|
||||||
|
.Where(s => filter.MangaFormat == null || s.Format == filter.MangaFormat)
|
||||||
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new
|
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new
|
||||||
{
|
{
|
||||||
Series = s,
|
Series = s,
|
||||||
PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id).Sum(s1 => s1.PagesRead),
|
PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id).Sum(s1 => s1.PagesRead),
|
||||||
progress.AppUserId,
|
progress.AppUserId,
|
||||||
LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id).Max(p => p.LastModified)
|
LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id).Max(p => p.LastModified)
|
||||||
});
|
}).AsNoTracking();
|
||||||
if (libraryId == 0)
|
if (libraryId == 0)
|
||||||
{
|
{
|
||||||
var userLibraries = _context.Library
|
var userLibraries = _context.Library
|
||||||
@ -371,14 +376,14 @@ namespace API.Data
|
|||||||
&& s.PagesRead < s.Series.Pages
|
&& s.PagesRead < s.Series.Pages
|
||||||
&& s.Series.LibraryId == libraryId);
|
&& s.Series.LibraryId == libraryId);
|
||||||
}
|
}
|
||||||
var retSeries = await series
|
|
||||||
|
var retSeries = series
|
||||||
.OrderByDescending(s => s.LastModified)
|
.OrderByDescending(s => s.LastModified)
|
||||||
.Select(s => s.Series)
|
.Select(s => s.Series)
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
.AsNoTracking()
|
.AsNoTracking();
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return retSeries.DistinctBy(s => s.Name).Take(limit);
|
return await PagedList<SeriesDto>.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId)
|
public async Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ namespace API.Interfaces
|
|||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="userParams"></param>
|
/// <param name="userParams"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Does not add user information like progress, ratings, etc.
|
/// Does not add user information like progress, ratings, etc.
|
||||||
@ -57,8 +58,8 @@ namespace API.Interfaces
|
|||||||
|
|
||||||
Task<byte[]> GetVolumeCoverImageAsync(int volumeId);
|
Task<byte[]> GetVolumeCoverImageAsync(int volumeId);
|
||||||
Task<byte[]> GetSeriesCoverImageAsync(int seriesId);
|
Task<byte[]> GetSeriesCoverImageAsync(int seriesId);
|
||||||
Task<IEnumerable<SeriesDto>> GetInProgress(int userId, int libraryId, int limit);
|
Task<PagedList<SeriesDto>> GetInProgress(int userId, int libraryId, UserParams userParams, FilterDto filter);
|
||||||
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
||||||
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
||||||
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
||||||
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
|
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<Company>kavitareader.com</Company>
|
<Company>kavitareader.com</Company>
|
||||||
<Product>Kavita</Product>
|
<Product>Kavita</Product>
|
||||||
<AssemblyVersion>0.4.3.4</AssemblyVersion>
|
<AssemblyVersion>0.4.3.5</AssemblyVersion>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
@ -39,7 +39,10 @@
|
|||||||
"src/styles.scss",
|
"src/styles.scss",
|
||||||
"node_modules/@fortawesome/fontawesome-free/css/all.min.css"
|
"node_modules/@fortawesome/fontawesome-free/css/all.min.css"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [
|
||||||
|
"node_modules/lazysizes/lazysizes.min.js",
|
||||||
|
"node_modules/lazysizes/plugins/rias/ls.rias.min.js"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
11
UI/Web/package-lock.json
generated
11
UI/Web/package-lock.json
generated
@ -29,6 +29,7 @@
|
|||||||
"bootstrap": "^4.5.0",
|
"bootstrap": "^4.5.0",
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"lazysizes": "^5.3.2",
|
||||||
"ng-lazyload-image": "^9.1.0",
|
"ng-lazyload-image": "^9.1.0",
|
||||||
"ng-sidebar": "^9.4.2",
|
"ng-sidebar": "^9.4.2",
|
||||||
"ngx-toastr": "^13.2.1",
|
"ngx-toastr": "^13.2.1",
|
||||||
@ -11745,6 +11746,11 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lazysizes": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lazysizes/-/lazysizes-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-22UzWP+Vedi/sMeOr8O7FWimRVtiNJV2HCa+V8+peZOw6QbswN9k58VUhd7i6iK5bw5QkYrF01LJbeJe0PV8jg=="
|
||||||
|
},
|
||||||
"node_modules/less": {
|
"node_modules/less": {
|
||||||
"version": "3.12.2",
|
"version": "3.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
|
||||||
@ -30076,6 +30082,11 @@
|
|||||||
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==",
|
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"lazysizes": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lazysizes/-/lazysizes-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-22UzWP+Vedi/sMeOr8O7FWimRVtiNJV2HCa+V8+peZOw6QbswN9k58VUhd7i6iK5bw5QkYrF01LJbeJe0PV8jg=="
|
||||||
|
},
|
||||||
"less": {
|
"less": {
|
||||||
"version": "3.12.2",
|
"version": "3.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"bootstrap": "^4.5.0",
|
"bootstrap": "^4.5.0",
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"lazysizes": "^5.3.2",
|
||||||
"ng-lazyload-image": "^9.1.0",
|
"ng-lazyload-image": "^9.1.0",
|
||||||
"ng-sidebar": "^9.4.2",
|
"ng-sidebar": "^9.4.2",
|
||||||
"ngx-toastr": "^13.2.1",
|
"ngx-toastr": "^13.2.1",
|
||||||
|
39
UI/Web/src/app/_models/series-filter.ts
Normal file
39
UI/Web/src/app/_models/series-filter.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { MangaFormat } from "./manga-format";
|
||||||
|
|
||||||
|
export interface FilterItem {
|
||||||
|
title: string;
|
||||||
|
value: any;
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeriesFilter {
|
||||||
|
mangaFormat: MangaFormat | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mangaFormatFilters = [
|
||||||
|
{
|
||||||
|
title: 'Format: All',
|
||||||
|
value: null,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Format: Images',
|
||||||
|
value: MangaFormat.IMAGE,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Format: EPUB',
|
||||||
|
value: MangaFormat.EPUB,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Format: PDF',
|
||||||
|
value: MangaFormat.PDF,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Format: ARCHIVE',
|
||||||
|
value: MangaFormat.ARCHIVE,
|
||||||
|
selected: false
|
||||||
|
}
|
||||||
|
];
|
@ -38,4 +38,8 @@ export class ImageService {
|
|||||||
getChapterCoverImage(chapterId: number) {
|
getChapterCoverImage(chapterId: number) {
|
||||||
return this.baseUrl + 'image/chapter-cover?chapterId=' + chapterId;
|
return this.baseUrl + 'image/chapter-cover?chapterId=' + chapterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateErroredImage(event: any) {
|
||||||
|
event.target.src = this.placeholderImage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,10 @@ import { environment } from 'src/environments/environment';
|
|||||||
import { Chapter } from '../_models/chapter';
|
import { Chapter } from '../_models/chapter';
|
||||||
import { CollectionTag } from '../_models/collection-tag';
|
import { CollectionTag } from '../_models/collection-tag';
|
||||||
import { InProgressChapter } from '../_models/in-progress-chapter';
|
import { InProgressChapter } from '../_models/in-progress-chapter';
|
||||||
|
import { MangaFormat } from '../_models/manga-format';
|
||||||
import { PaginatedResult } from '../_models/pagination';
|
import { PaginatedResult } from '../_models/pagination';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../_models/series';
|
||||||
|
import { SeriesFilter } from '../_models/series-filter';
|
||||||
import { SeriesMetadata } from '../_models/series-metadata';
|
import { SeriesMetadata } from '../_models/series-metadata';
|
||||||
import { Volume } from '../_models/volume';
|
import { Volume } from '../_models/volume';
|
||||||
import { ImageService } from './image.service';
|
import { ImageService } from './image.service';
|
||||||
@ -38,12 +40,12 @@ export class SeriesService {
|
|||||||
return paginatedVariable;
|
return paginatedVariable;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSeriesForLibrary(libraryId: number, pageNum?: number, itemsPerPage?: number) {
|
getSeriesForLibrary(libraryId: number, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
|
|
||||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
const data = this.createSeriesFilter(filter);
|
||||||
|
|
||||||
return this.httpClient.get<PaginatedResult<Series[]>>(this.baseUrl + 'series?libraryId=' + libraryId, {observe: 'response', params}).pipe(
|
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
|
||||||
map((response: any) => {
|
map((response: any) => {
|
||||||
return this._cachePaginatedResults(response, this.paginatedResults);
|
return this._cachePaginatedResults(response, this.paginatedResults);
|
||||||
})
|
})
|
||||||
@ -79,7 +81,7 @@ export class SeriesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateSeries(model: any) {
|
updateSeries(model: any) {
|
||||||
return this.httpClient.post(this.baseUrl + 'series/', model);
|
return this.httpClient.post(this.baseUrl + 'series/update', model);
|
||||||
}
|
}
|
||||||
|
|
||||||
markRead(seriesId: number) {
|
markRead(seriesId: number) {
|
||||||
@ -90,22 +92,27 @@ export class SeriesService {
|
|||||||
return this.httpClient.post<void>(this.baseUrl + 'reader/mark-unread', {seriesId});
|
return this.httpClient.post<void>(this.baseUrl + 'reader/mark-unread', {seriesId});
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecentlyAdded(libraryId: number = 0, pageNum?: number, itemsPerPage?: number) {
|
getRecentlyAdded(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||||
|
const data = this.createSeriesFilter(filter);
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
|
|
||||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
|
||||||
return this.httpClient.get<Series[]>(this.baseUrl + 'series/recently-added', {observe: 'response', params}).pipe(
|
return this.httpClient.post<Series[]>(this.baseUrl + 'series/recently-added?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
|
||||||
map((response: any) => {
|
map(response => {
|
||||||
return this._cachePaginatedResults(response, this.paginatedSeriesForTagsResults);
|
return this._cachePaginatedResults(response, new PaginatedResult<Series[]>());
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInProgress(libraryId: number = 0) {
|
getInProgress(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||||
return this.httpClient.get<Series[]>(this.baseUrl + 'series/in-progress?libraryId=' + libraryId).pipe(map(series => {
|
const data = this.createSeriesFilter(filter);
|
||||||
series.forEach(s => s.coverImage = this.imageService.getSeriesCoverImage(s.id));
|
|
||||||
return series;
|
let params = new HttpParams();
|
||||||
|
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
|
||||||
|
return this.httpClient.post<Series[]>(this.baseUrl + 'series/in-progress?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
|
||||||
|
map(response => {
|
||||||
|
return this._cachePaginatedResults(response, new PaginatedResult<Series[]>());
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,4 +167,16 @@ export class SeriesService {
|
|||||||
}
|
}
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createSeriesFilter(filter?: SeriesFilter) {
|
||||||
|
const data: SeriesFilter = {
|
||||||
|
mangaFormat: null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
data.mangaFormat = filter.mangaFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import { UserLoginComponent } from './user-login/user-login.component';
|
|||||||
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
||||||
import { AuthGuard } from './_guards/auth.guard';
|
import { AuthGuard } from './_guards/auth.guard';
|
||||||
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
||||||
|
import { InProgressComponent } from './in-progress/in-progress.component';
|
||||||
|
|
||||||
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
|
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ const routes: Routes = [
|
|||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
children: [
|
children: [
|
||||||
{path: 'recently-added', component: RecentlyAddedComponent},
|
{path: 'recently-added', component: RecentlyAddedComponent},
|
||||||
|
{path: 'in-progress', component: InProgressComponent},
|
||||||
{path: 'collections', component: AllCollectionsComponent},
|
{path: 'collections', component: AllCollectionsComponent},
|
||||||
{path: 'collections/:id', component: AllCollectionsComponent},
|
{path: 'collections/:id', component: AllCollectionsComponent},
|
||||||
]
|
]
|
||||||
|
@ -33,3 +33,4 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ import { UserPreferencesComponent } from './user-preferences/user-preferences.co
|
|||||||
import { AutocompleteLibModule } from 'angular-ng-autocomplete';
|
import { AutocompleteLibModule } from 'angular-ng-autocomplete';
|
||||||
import { EditSeriesModalComponent } from './_modals/edit-series-modal/edit-series-modal.component';
|
import { EditSeriesModalComponent } from './_modals/edit-series-modal/edit-series-modal.component';
|
||||||
import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component';
|
import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component';
|
||||||
import { LazyLoadImageModule} from 'ng-lazyload-image';
|
|
||||||
import { CarouselModule } from './carousel/carousel.module';
|
import { CarouselModule } from './carousel/carousel.module';
|
||||||
import { NgxSliderModule } from '@angular-slider/ngx-slider';
|
import { NgxSliderModule } from '@angular-slider/ngx-slider';
|
||||||
|
|
||||||
@ -39,6 +38,7 @@ import { EditCollectionTagsComponent } from './_modals/edit-collection-tags/edit
|
|||||||
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
||||||
import { LibraryCardComponent } from './library-card/library-card.component';
|
import { LibraryCardComponent } from './library-card/library-card.component';
|
||||||
import { SeriesCardComponent } from './series-card/series-card.component';
|
import { SeriesCardComponent } from './series-card/series-card.component';
|
||||||
|
import { InProgressComponent } from './in-progress/in-progress.component';
|
||||||
|
|
||||||
let sentryProviders: any[] = [];
|
let sentryProviders: any[] = [];
|
||||||
|
|
||||||
@ -103,7 +103,8 @@ if (environment.production) {
|
|||||||
EditCollectionTagsComponent,
|
EditCollectionTagsComponent,
|
||||||
RecentlyAddedComponent,
|
RecentlyAddedComponent,
|
||||||
LibraryCardComponent,
|
LibraryCardComponent,
|
||||||
SeriesCardComponent
|
SeriesCardComponent,
|
||||||
|
InProgressComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
@ -120,7 +121,6 @@ if (environment.production) {
|
|||||||
NgbAccordionModule, // User Preferences
|
NgbAccordionModule, // User Preferences
|
||||||
NgxSliderModule, // User Preference
|
NgxSliderModule, // User Preference
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
LazyLoadImageModule,
|
|
||||||
SharedModule,
|
SharedModule,
|
||||||
CarouselModule,
|
CarouselModule,
|
||||||
TypeaheadModule,
|
TypeaheadModule,
|
||||||
@ -136,7 +136,6 @@ if (environment.production) {
|
|||||||
providers: [
|
providers: [
|
||||||
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
||||||
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
||||||
//{ provide: LAZYLOAD_IMAGE_HOOKS, useClass: ScrollHooks } // Great, but causes flashing after modals close
|
|
||||||
Title,
|
Title,
|
||||||
...sentryProviders,
|
...sentryProviders,
|
||||||
],
|
],
|
||||||
|
14
UI/Web/src/app/in-progress/in-progress.component.html
Normal file
14
UI/Web/src/app/in-progress/in-progress.component.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-card-detail-layout header="In Progress"
|
||||||
|
[isLoading]="isLoading"
|
||||||
|
[items]="recentlyAdded"
|
||||||
|
[filters]="filters"
|
||||||
|
[pagination]="pagination"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(applyFilter)="updateFilter($event)"
|
||||||
|
>
|
||||||
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
|
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()"></app-series-card>
|
||||||
|
</ng-template>
|
||||||
|
</app-card-detail-layout>
|
||||||
|
</ng-container>
|
76
UI/Web/src/app/in-progress/in-progress.component.ts
Normal file
76
UI/Web/src/app/in-progress/in-progress.component.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { UpdateFilterEvent } from '../shared/card-detail-layout/card-detail-layout.component';
|
||||||
|
import { Pagination } from '../_models/pagination';
|
||||||
|
import { Series } from '../_models/series';
|
||||||
|
import { FilterItem, SeriesFilter, mangaFormatFilters } from '../_models/series-filter';
|
||||||
|
import { SeriesService } from '../_services/series.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-in-progress',
|
||||||
|
templateUrl: './in-progress.component.html',
|
||||||
|
styleUrls: ['./in-progress.component.scss']
|
||||||
|
})
|
||||||
|
export class InProgressComponent implements OnInit {
|
||||||
|
|
||||||
|
isLoading: boolean = true;
|
||||||
|
recentlyAdded: Series[] = [];
|
||||||
|
pagination!: Pagination;
|
||||||
|
libraryId!: number;
|
||||||
|
filters: Array<FilterItem> = mangaFormatFilters;
|
||||||
|
filter: SeriesFilter = {
|
||||||
|
mangaFormat: null
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private titleService: Title) {
|
||||||
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
|
this.titleService.setTitle('Kavita - In Progress');
|
||||||
|
if (this.pagination === undefined || this.pagination === null) {
|
||||||
|
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||||
|
}
|
||||||
|
this.loadPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {}
|
||||||
|
|
||||||
|
seriesClicked(series: Series) {
|
||||||
|
this.router.navigate(['library', this.libraryId, 'series', series.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageChange(pagination: Pagination) {
|
||||||
|
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage);
|
||||||
|
this.loadPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilter(data: UpdateFilterEvent) {
|
||||||
|
this.filter.mangaFormat = data.filterItem.value;
|
||||||
|
if (this.pagination !== undefined && this.pagination !== null) {
|
||||||
|
this.pagination.currentPage = 1;
|
||||||
|
this.onPageChange(this.pagination);
|
||||||
|
} else {
|
||||||
|
this.loadPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPage() {
|
||||||
|
const page = this.getPage();
|
||||||
|
if (page != null) {
|
||||||
|
this.pagination.currentPage = parseInt(page, 10);
|
||||||
|
}
|
||||||
|
this.isLoading = true;
|
||||||
|
this.seriesService.getInProgress(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||||
|
this.recentlyAdded = series.result;
|
||||||
|
this.pagination = series.pagination;
|
||||||
|
this.isLoading = false;
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPage() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
return urlParams.get('page');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,6 +3,8 @@
|
|||||||
[items]="series"
|
[items]="series"
|
||||||
[actions]="actions"
|
[actions]="actions"
|
||||||
[pagination]="pagination"
|
[pagination]="pagination"
|
||||||
|
[filters]="filters"
|
||||||
|
(applyFilter)="updateFilter($event)"
|
||||||
(pageChange)="onPageChange($event)"
|
(pageChange)="onPageChange($event)"
|
||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
|
@ -2,9 +2,11 @@ import { Component, OnInit } 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 { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
|
import { UpdateFilterEvent } from '../shared/card-detail-layout/card-detail-layout.component';
|
||||||
import { Library } from '../_models/library';
|
import { Library } from '../_models/library';
|
||||||
import { Pagination } from '../_models/pagination';
|
import { Pagination } from '../_models/pagination';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../_models/series';
|
||||||
|
import { FilterItem, mangaFormatFilters, SeriesFilter } from '../_models/series-filter';
|
||||||
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
|
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
|
||||||
import { ActionService } from '../_services/action.service';
|
import { ActionService } from '../_services/action.service';
|
||||||
import { LibraryService } from '../_services/library.service';
|
import { LibraryService } from '../_services/library.service';
|
||||||
@ -23,9 +25,14 @@ export class LibraryDetailComponent implements OnInit {
|
|||||||
loadingSeries = false;
|
loadingSeries = false;
|
||||||
pagination!: Pagination;
|
pagination!: Pagination;
|
||||||
actions: ActionItem<Library>[] = [];
|
actions: ActionItem<Library>[] = [];
|
||||||
|
filters: Array<FilterItem> = mangaFormatFilters;
|
||||||
|
filter: SeriesFilter = {
|
||||||
|
mangaFormat: null
|
||||||
|
};
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
|
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
|
||||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService, private actionService: ActionService) {
|
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
||||||
|
private actionService: ActionService) {
|
||||||
const routeId = this.route.snapshot.paramMap.get('id');
|
const routeId = this.route.snapshot.paramMap.get('id');
|
||||||
if (routeId === null) {
|
if (routeId === null) {
|
||||||
this.router.navigateByUrl('/libraries');
|
this.router.navigateByUrl('/libraries');
|
||||||
@ -36,12 +43,14 @@ export class LibraryDetailComponent implements OnInit {
|
|||||||
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(names => {
|
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(names => {
|
||||||
this.libraryName = names[this.libraryId];
|
this.libraryName = names[this.libraryId];
|
||||||
this.titleService.setTitle('Kavita - ' + this.libraryName);
|
this.titleService.setTitle('Kavita - ' + this.libraryName);
|
||||||
})
|
});
|
||||||
this.loadPage();
|
|
||||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||||
|
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||||
|
this.loadPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAction(action: Action, library: Library) {
|
handleAction(action: Action, library: Library) {
|
||||||
@ -61,17 +70,24 @@ export class LibraryDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPage() {
|
updateFilter(data: UpdateFilterEvent) {
|
||||||
if (this.pagination == undefined || this.pagination == null) {
|
this.filter.mangaFormat = data.filterItem.value;
|
||||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
if (this.pagination !== undefined && this.pagination !== null) {
|
||||||
|
this.pagination.currentPage = 1;
|
||||||
|
this.onPageChange(this.pagination);
|
||||||
|
} else {
|
||||||
|
this.loadPage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = this.route.snapshot.queryParamMap.get('page');
|
loadPage() {
|
||||||
|
const page = this.getPage();
|
||||||
if (page != null) {
|
if (page != null) {
|
||||||
this.pagination.currentPage = parseInt(page, 10);
|
this.pagination.currentPage = parseInt(page, 10);
|
||||||
}
|
}
|
||||||
this.loadingSeries = true;
|
this.loadingSeries = true;
|
||||||
this.seriesService.getSeriesForLibrary(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage).pipe(take(1)).subscribe(series => {
|
|
||||||
|
this.seriesService.getSeriesForLibrary(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||||
this.series = series.result;
|
this.series = series.result;
|
||||||
this.pagination = series.pagination;
|
this.pagination = series.pagination;
|
||||||
this.loadingSeries = false;
|
this.loadingSeries = false;
|
||||||
@ -80,13 +96,19 @@ export class LibraryDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPageChange(pagination: Pagination) {
|
onPageChange(pagination: Pagination) {
|
||||||
this.router.navigate(['library', this.libraryId], {replaceUrl: true, queryParamsHandling: 'merge', queryParams: {page: this.pagination.currentPage} });
|
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage);
|
||||||
|
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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.originalName}_${item.localizedName}`;
|
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.originalName}_${item.localizedName}_${item.pagesRead}`;
|
||||||
|
|
||||||
|
getPage() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
return urlParams.get('page');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
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 { take } from 'rxjs/operators';
|
import { Subject } from 'rxjs';
|
||||||
|
import { take, takeUntil } from 'rxjs/operators';
|
||||||
import { EditCollectionTagsComponent } from '../_modals/edit-collection-tags/edit-collection-tags.component';
|
import { EditCollectionTagsComponent } from '../_modals/edit-collection-tags/edit-collection-tags.component';
|
||||||
import { CollectionTag } from '../_models/collection-tag';
|
import { CollectionTag } from '../_models/collection-tag';
|
||||||
import { InProgressChapter } from '../_models/in-progress-chapter';
|
import { InProgressChapter } from '../_models/in-progress-chapter';
|
||||||
@ -20,7 +21,7 @@ import { SeriesService } from '../_services/series.service';
|
|||||||
templateUrl: './library.component.html',
|
templateUrl: './library.component.html',
|
||||||
styleUrls: ['./library.component.scss']
|
styleUrls: ['./library.component.scss']
|
||||||
})
|
})
|
||||||
export class LibraryComponent implements OnInit {
|
export class LibraryComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
user: User | undefined;
|
user: User | undefined;
|
||||||
libraries: Library[] = [];
|
libraries: Library[] = [];
|
||||||
@ -33,6 +34,8 @@ export class LibraryComponent implements OnInit {
|
|||||||
collectionTags: CollectionTag[] = [];
|
collectionTags: CollectionTag[] = [];
|
||||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||||
|
|
||||||
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
seriesTrackBy = (index: number, item: any) => `${item.name}_${item.pagesRead}`;
|
seriesTrackBy = (index: number, item: any) => `${item.name}_${item.pagesRead}`;
|
||||||
|
|
||||||
constructor(public accountService: AccountService, private libraryService: LibraryService,
|
constructor(public accountService: AccountService, private libraryService: LibraryService,
|
||||||
@ -57,13 +60,18 @@ export class LibraryComponent implements OnInit {
|
|||||||
this.reloadSeries();
|
this.reloadSeries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.onDestroy.next();
|
||||||
|
this.onDestroy.complete();
|
||||||
|
}
|
||||||
|
|
||||||
reloadSeries() {
|
reloadSeries() {
|
||||||
this.seriesService.getRecentlyAdded(0, 0, 20).subscribe(updatedSeries => {
|
this.seriesService.getRecentlyAdded(0, 0, 20).pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
||||||
this.recentlyAdded = updatedSeries.result;
|
this.recentlyAdded = updatedSeries.result;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.seriesService.getInProgress().subscribe((updatedSeries) => {
|
this.seriesService.getInProgress().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||||
this.inProgress = updatedSeries;
|
this.inProgress = updatedSeries.result;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.reloadTags();
|
this.reloadTags();
|
||||||
@ -78,15 +86,15 @@ export class LibraryComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.seriesService.getInProgress().subscribe((updatedSeries) => {
|
this.seriesService.getInProgress().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||||
this.inProgress = updatedSeries;
|
this.inProgress = updatedSeries.result;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.reloadTags();
|
this.reloadTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadTags() {
|
reloadTags() {
|
||||||
this.collectionService.allTags().subscribe(tags => {
|
this.collectionService.allTags().pipe(takeUntil(this.onDestroy)).subscribe(tags => {
|
||||||
this.collectionTags = tags;
|
this.collectionTags = tags;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -96,6 +104,8 @@ export class LibraryComponent implements OnInit {
|
|||||||
this.router.navigate(['collections']);
|
this.router.navigate(['collections']);
|
||||||
} else if (sectionTitle.toLowerCase() === 'recently added') {
|
} else if (sectionTitle.toLowerCase() === 'recently added') {
|
||||||
this.router.navigate(['recently-added']);
|
this.router.navigate(['recently-added']);
|
||||||
|
} else if (sectionTitle.toLowerCase() === 'in progress') {
|
||||||
|
this.router.navigate(['in-progress']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
[isLoading]="isLoading"
|
[isLoading]="isLoading"
|
||||||
[items]="recentlyAdded"
|
[items]="recentlyAdded"
|
||||||
[pagination]="pagination"
|
[pagination]="pagination"
|
||||||
|
[filters]="filters"
|
||||||
|
(applyFilter)="updateFilter($event)"
|
||||||
(pageChange)="onPageChange($event)"
|
(pageChange)="onPageChange($event)"
|
||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { UpdateFilterEvent } from '../shared/card-detail-layout/card-detail-layout.component';
|
||||||
import { Pagination } from '../_models/pagination';
|
import { Pagination } from '../_models/pagination';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../_models/series';
|
||||||
|
import { FilterItem, mangaFormatFilters, SeriesFilter } from '../_models/series-filter';
|
||||||
import { SeriesService } from '../_services/series.service';
|
import { SeriesService } from '../_services/series.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,36 +23,57 @@ export class RecentlyAddedComponent implements OnInit {
|
|||||||
pagination!: Pagination;
|
pagination!: Pagination;
|
||||||
libraryId!: number;
|
libraryId!: number;
|
||||||
|
|
||||||
constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService) {
|
filters: Array<FilterItem> = mangaFormatFilters;
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
filter: SeriesFilter = {
|
||||||
}
|
mangaFormat: null
|
||||||
|
};
|
||||||
|
|
||||||
ngOnInit() {
|
constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private titleService: Title) {
|
||||||
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
|
this.titleService.setTitle('Kavita - Recently Added');
|
||||||
|
if (this.pagination === undefined || this.pagination === null) {
|
||||||
|
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||||
|
}
|
||||||
this.loadPage();
|
this.loadPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit() {}
|
||||||
|
|
||||||
seriesClicked(series: Series) {
|
seriesClicked(series: Series) {
|
||||||
this.router.navigate(['library', this.libraryId, 'series', series.id]);
|
this.router.navigate(['library', this.libraryId, 'series', series.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageChange(pagination: Pagination) {
|
onPageChange(pagination: Pagination) {
|
||||||
this.router.navigate(['recently-added'], {replaceUrl: true, queryParamsHandling: 'merge', queryParams: {page: this.pagination.currentPage} });
|
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage);
|
||||||
|
this.loadPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilter(data: UpdateFilterEvent) {
|
||||||
|
this.filter.mangaFormat = data.filterItem.value;
|
||||||
|
if (this.pagination !== undefined && this.pagination !== null) {
|
||||||
|
this.pagination.currentPage = 1;
|
||||||
|
this.onPageChange(this.pagination);
|
||||||
|
} else {
|
||||||
|
this.loadPage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPage() {
|
loadPage() {
|
||||||
const page = this.route.snapshot.queryParamMap.get('page');
|
const page = this.getPage();
|
||||||
if (page != null) {
|
if (page != null) {
|
||||||
if (this.pagination === undefined || this.pagination === null) {
|
|
||||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
|
||||||
}
|
|
||||||
this.pagination.currentPage = parseInt(page, 10);
|
this.pagination.currentPage = parseInt(page, 10);
|
||||||
}
|
}
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.seriesService.getRecentlyAdded(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage).subscribe(series => {
|
this.seriesService.getRecentlyAdded(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||||
this.recentlyAdded = series.result;
|
this.recentlyAdded = series.result;
|
||||||
this.pagination = series.pagination;
|
this.pagination = series.pagination;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPage() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
return urlParams.get('page');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<div class="container-fluid" *ngIf="series !== undefined" style="padding-top: 10px">
|
<div class="container-fluid" *ngIf="series !== undefined" style="padding-top: 10px">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-2 col-xs-4 col-sm-6">
|
<div class="col-md-2 col-xs-4 col-sm-6">
|
||||||
<img class="poster" [lazyLoad]="imageService.getSeriesCoverImage(series.id)" [defaultImage]="imageService.placeholderImage">
|
<img class="poster lazyload" [src]="imageSerivce.placeholderImage" [attr.data-src]="imageService.getSeriesCoverImage(series.id)"
|
||||||
|
(error)="imageSerivce.updateErroredImage($event)" aria-hidden="true">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-10 col-xs-8 col-sm-6">
|
<div class="col-md-10 col-xs-8 col-sm-6">
|
||||||
<div class="row no-gutters">
|
<div class="row no-gutters">
|
||||||
|
@ -86,7 +86,8 @@ export class SeriesDetailComponent implements OnInit {
|
|||||||
private accountService: AccountService, public imageService: ImageService,
|
private accountService: AccountService, public imageService: ImageService,
|
||||||
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
|
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
|
||||||
private confirmService: ConfirmService, private titleService: Title,
|
private confirmService: ConfirmService, private titleService: Title,
|
||||||
private downloadService: DownloadService, private actionService: ActionService) {
|
private downloadService: DownloadService, private actionService: ActionService,
|
||||||
|
public imageSerivce: ImageService) {
|
||||||
ratingConfig.max = 5;
|
ratingConfig.max = 5;
|
||||||
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 => {
|
||||||
|
@ -1,7 +1,30 @@
|
|||||||
<div class="container-fluid" style="padding-top: 10px">
|
<div class="container-fluid" style="padding-top: 10px">
|
||||||
<h2><span *ngIf="actions.length > 0" class="">
|
<div class="row no-gutters">
|
||||||
|
<div class="col mr-auto">
|
||||||
|
<h2 style="display: inline-block">
|
||||||
|
<span *ngIf="actions.length > 0" class="">
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>
|
||||||
</span> {{header}}</h2>
|
</span> {{header}} <span class="badge badge-primary badge-pill" attr.aria-label="{{pagination.totalItems}} total items" *ngIf="pagination != undefined">{{pagination.totalItems}}</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-secondary btn-small" (click)="collapse.toggle()" [attr.aria-expanded]="!filteringCollapsed" placement="left" ngbTooltip="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting">
|
||||||
|
<i class="fa fa-filter" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">Sort / Filter</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="row no-gutters filter-section" #collapse="ngbCollapse" [(ngbCollapse)]="filteringCollapsed">
|
||||||
|
<div class="col">
|
||||||
|
<form class="ml-2" [formGroup]="filterForm">
|
||||||
|
<div class="form-group" *ngIf="filters.length > 0">
|
||||||
|
<label for="series-filter">Filter</label>
|
||||||
|
<select class="form-control" id="series-filter" formControlName="filter" (ngModelChange)="handleFilterChange($event)" style="max-width: 200px;">
|
||||||
|
<option [value]="i" *ngFor="let opt of filters; let i = index">{{opt.title}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
|
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,33 @@
|
|||||||
import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core';
|
import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core';
|
||||||
|
import { FormGroup, FormControl } from '@angular/forms';
|
||||||
import { Pagination } from 'src/app/_models/pagination';
|
import { Pagination } from 'src/app/_models/pagination';
|
||||||
|
import { FilterItem } from 'src/app/_models/series-filter';
|
||||||
import { ActionItem } from 'src/app/_services/action-factory.service';
|
import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||||
|
|
||||||
const FILTER_PAG_REGEX = /[^0-9]/g;
|
const FILTER_PAG_REGEX = /[^0-9]/g;
|
||||||
|
|
||||||
|
export enum FilterAction {
|
||||||
|
/**
|
||||||
|
* If an option is selected on a multi select component
|
||||||
|
*/
|
||||||
|
Added = 0,
|
||||||
|
/**
|
||||||
|
* If an option is unselected on a multi select component
|
||||||
|
*/
|
||||||
|
Removed = 1,
|
||||||
|
/**
|
||||||
|
* If an option is selected on a single select component
|
||||||
|
*/
|
||||||
|
Selected = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateFilterEvent {
|
||||||
|
filterItem: FilterItem;
|
||||||
|
action: FilterAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANIMATION_SPEED = 300;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-card-detail-layout',
|
selector: 'app-card-detail-layout',
|
||||||
templateUrl: './card-detail-layout.component.html',
|
templateUrl: './card-detail-layout.component.html',
|
||||||
@ -15,13 +39,30 @@ export class CardDetailLayoutComponent implements OnInit {
|
|||||||
@Input() isLoading: boolean = false;
|
@Input() isLoading: boolean = false;
|
||||||
@Input() items: any[] = [];
|
@Input() items: any[] = [];
|
||||||
@Input() pagination!: Pagination;
|
@Input() pagination!: Pagination;
|
||||||
|
/**
|
||||||
|
* Any actions to exist on the header for the parent collection (library, collection)
|
||||||
|
*/
|
||||||
@Input() actions: ActionItem<any>[] = [];
|
@Input() actions: ActionItem<any>[] = [];
|
||||||
|
/**
|
||||||
|
* A list of Filters which can filter the data of the page. If nothing is passed, the control will not show.
|
||||||
|
*/
|
||||||
|
@Input() filters: Array<FilterItem> = [];
|
||||||
@Input() trackByIdentity!: (index: number, item: any) => string;
|
@Input() trackByIdentity!: (index: number, item: any) => string;
|
||||||
@Output() itemClicked: EventEmitter<any> = new EventEmitter();
|
@Output() itemClicked: EventEmitter<any> = new EventEmitter();
|
||||||
@Output() pageChange: EventEmitter<Pagination> = new EventEmitter();
|
@Output() pageChange: EventEmitter<Pagination> = new EventEmitter();
|
||||||
|
@Output() applyFilter: EventEmitter<UpdateFilterEvent> = new EventEmitter();
|
||||||
|
|
||||||
@ContentChild('cardItem') itemTemplate!: TemplateRef<any>;
|
@ContentChild('cardItem') itemTemplate!: TemplateRef<any>;
|
||||||
|
|
||||||
|
filterForm: FormGroup = new FormGroup({
|
||||||
|
filter: new FormControl(0, []),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls the visiblity of extended controls that sit below the main header.
|
||||||
|
*/
|
||||||
|
filteringCollapsed: boolean = true;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -47,4 +88,11 @@ export class CardDetailLayoutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleFilterChange(index: string) {
|
||||||
|
this.applyFilter.emit({
|
||||||
|
filterItem: this.filters[parseInt(index, 10)],
|
||||||
|
action: FilterAction.Selected
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="overlay" (click)="handleClick()">
|
<div class="overlay" (click)="handleClick()">
|
||||||
<img *ngIf="total > 0 || supressArchiveWarning" class="card-img-top" [lazyLoad]="imageUrl" [defaultImage]="imageSerivce.placeholderImage" alt="title">
|
<img *ngIf="total > 0 || supressArchiveWarning" class="card-img-top lazyload" [src]="imageSerivce.placeholderImage" [attr.data-src]="imageUrl"
|
||||||
<img *ngIf="total === 0 && !supressArchiveWarning" class="card-img-top" [lazyLoad]="imageSerivce.errorImage" alt="title">
|
(error)="imageSerivce.updateErroredImage($event)" aria-hidden="true" height="230px" width="158px">
|
||||||
|
<img *ngIf="total === 0 && !supressArchiveWarning" class="card-img-top lazyload" [src]="imageSerivce.errorImage" [attr.data-src]="imageUrl"
|
||||||
|
aria-hidden="true" height="230px" width="158px">
|
||||||
<div class="progress-banner" *ngIf="read < total && total > 0 && read !== (total -1)">
|
<div class="progress-banner" *ngIf="read < total && total > 0 && read !== (total -1)">
|
||||||
<p><ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar></p>
|
<p><ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,8 @@ import { ActionItem } from 'src/app/_services/action-factory.service';
|
|||||||
import { ImageService } from 'src/app/_services/image.service';
|
import { ImageService } from 'src/app/_services/image.service';
|
||||||
import { LibraryService } from 'src/app/_services/library.service';
|
import { LibraryService } from 'src/app/_services/library.service';
|
||||||
import { UtilityService } from '../_services/utility.service';
|
import { UtilityService } from '../_services/utility.service';
|
||||||
|
// import 'lazysizes';
|
||||||
|
// import 'lazysizes/plugins/attrchange/ls.attrchange';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-card-item',
|
selector: 'app-card-item',
|
||||||
@ -38,8 +40,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
constructor(public imageSerivce: ImageService, private libraryService: LibraryService, public utilityService: UtilityService) {
|
constructor(public imageSerivce: ImageService, private libraryService: LibraryService, public utilityService: UtilityService) {}
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
||||||
@ -59,6 +60,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.onDestroy.next();
|
this.onDestroy.next();
|
||||||
|
this.onDestroy.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick() {
|
handleClick() {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
// Custom animation for ng-lazyload-image
|
// Custom animation for ng-lazyload-image
|
||||||
img.ng-lazyloaded {
|
img.ng-lazyloaded {
|
||||||
animation: fadein .5s;
|
//animation: fadein .5s; // I think it might look better without animation
|
||||||
}
|
}
|
||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user