mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -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 API.Data;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
@ -27,12 +28,12 @@ namespace API.Controllers
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams)
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
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)
|
||||
if (series == null) return BadRequest("Could not get series for library");
|
||||
@ -119,7 +120,7 @@ namespace API.Controllers
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
|
||||
{
|
||||
_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");
|
||||
}
|
||||
|
||||
[HttpGet("recently-added")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded([FromQuery] UserParams userParams, int libraryId = 0)
|
||||
[HttpPost("recently-added")]
|
||||
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 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)
|
||||
if (series == null) return BadRequest("Could not get series");
|
||||
@ -164,12 +165,11 @@ namespace API.Controllers
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
[HttpGet("in-progress")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetInProgress(int libraryId = 0, int limit = 20)
|
||||
[HttpPost("in-progress")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetInProgress(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
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, limit));
|
||||
return Ok((await _unitOfWork.SeriesRepository.GetInProgress(user.Id, libraryId, userParams, filterDto)).DistinctBy(s => s.Name));
|
||||
}
|
||||
|
||||
[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 API.Comparators;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
@ -75,10 +76,10 @@ namespace API.Data
|
||||
.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
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
.Where(s => s.LibraryId == libraryId && (filter.MangaFormat == null || s.Format == filter.MangaFormat))
|
||||
.OrderBy(s => s.SortName)
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking();
|
||||
@ -120,7 +121,7 @@ namespace API.Data
|
||||
|
||||
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();
|
||||
}
|
||||
@ -302,8 +303,9 @@ namespace API.Data
|
||||
/// <param name="userId"></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="filter">Optional filter on query</param>
|
||||
/// <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)
|
||||
{
|
||||
@ -315,7 +317,7 @@ namespace API.Data
|
||||
.ToList();
|
||||
|
||||
var allQuery = _context.Series
|
||||
.Where(s => userLibraries.Contains(s.LibraryId))
|
||||
.Where(s => userLibraries.Contains(s.LibraryId) && (filter.MangaFormat == null || s.Format == filter.MangaFormat))
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(s => s.Created)
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
@ -325,7 +327,7 @@ namespace API.Data
|
||||
}
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
.Where(s => s.LibraryId == libraryId && (filter.MangaFormat == null || s.Format == filter.MangaFormat))
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(s => s.Created)
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
@ -338,19 +340,22 @@ namespace API.Data
|
||||
/// Returns Series that the user has some partial progress on
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="limit"></param>
|
||||
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
||||
/// <param name="userParams">Pagination information</param>
|
||||
/// <param name="filter">Optional (default null) filter on query</param>
|
||||
/// <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
|
||||
.Where(s => filter.MangaFormat == null || s.Format == filter.MangaFormat)
|
||||
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new
|
||||
{
|
||||
Series = s,
|
||||
PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id).Sum(s1 => s1.PagesRead),
|
||||
progress.AppUserId,
|
||||
LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id).Max(p => p.LastModified)
|
||||
});
|
||||
}).AsNoTracking();
|
||||
if (libraryId == 0)
|
||||
{
|
||||
var userLibraries = _context.Library
|
||||
@ -371,14 +376,14 @@ namespace API.Data
|
||||
&& s.PagesRead < s.Series.Pages
|
||||
&& s.Series.LibraryId == libraryId);
|
||||
}
|
||||
var retSeries = await series
|
||||
|
||||
var retSeries = series
|
||||
.OrderByDescending(s => s.LastModified)
|
||||
.Select(s => s.Series)
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
.AsNoTracking();
|
||||
|
||||
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)
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.Entities;
|
||||
using API.Helpers;
|
||||
|
||||
@ -21,7 +22,7 @@ namespace API.Interfaces
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <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>
|
||||
/// Does not add user information like progress, ratings, etc.
|
||||
@ -57,10 +58,10 @@ namespace API.Interfaces
|
||||
|
||||
Task<byte[]> GetVolumeCoverImageAsync(int volumeId);
|
||||
Task<byte[]> GetSeriesCoverImageAsync(int seriesId);
|
||||
Task<IEnumerable<SeriesDto>> GetInProgress(int userId, int libraryId, int limit);
|
||||
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams);
|
||||
Task<PagedList<SeriesDto>> GetInProgress(int userId, int libraryId, UserParams userParams, FilterDto filter);
|
||||
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
||||
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
||||
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
||||
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Company>kavitareader.com</Company>
|
||||
<Product>Kavita</Product>
|
||||
<AssemblyVersion>0.4.3.4</AssemblyVersion>
|
||||
<AssemblyVersion>0.4.3.5</AssemblyVersion>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -39,7 +39,10 @@
|
||||
"src/styles.scss",
|
||||
"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": {
|
||||
"production": {
|
||||
|
11
UI/Web/package-lock.json
generated
11
UI/Web/package-lock.json
generated
@ -29,6 +29,7 @@
|
||||
"bootstrap": "^4.5.0",
|
||||
"bowser": "^2.11.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"lazysizes": "^5.3.2",
|
||||
"ng-lazyload-image": "^9.1.0",
|
||||
"ng-sidebar": "^9.4.2",
|
||||
"ngx-toastr": "^13.2.1",
|
||||
@ -11745,6 +11746,11 @@
|
||||
"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": {
|
||||
"version": "3.12.2",
|
||||
"resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
|
||||
@ -30076,6 +30082,11 @@
|
||||
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==",
|
||||
"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": {
|
||||
"version": "3.12.2",
|
||||
"resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
|
||||
|
@ -36,6 +36,7 @@
|
||||
"bootstrap": "^4.5.0",
|
||||
"bowser": "^2.11.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"lazysizes": "^5.3.2",
|
||||
"ng-lazyload-image": "^9.1.0",
|
||||
"ng-sidebar": "^9.4.2",
|
||||
"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) {
|
||||
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 { CollectionTag } from '../_models/collection-tag';
|
||||
import { InProgressChapter } from '../_models/in-progress-chapter';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
import { PaginatedResult } from '../_models/pagination';
|
||||
import { Series } from '../_models/series';
|
||||
import { SeriesFilter } from '../_models/series-filter';
|
||||
import { SeriesMetadata } from '../_models/series-metadata';
|
||||
import { Volume } from '../_models/volume';
|
||||
import { ImageService } from './image.service';
|
||||
@ -38,12 +40,12 @@ export class SeriesService {
|
||||
return paginatedVariable;
|
||||
}
|
||||
|
||||
getSeriesForLibrary(libraryId: number, pageNum?: number, itemsPerPage?: number) {
|
||||
getSeriesForLibrary(libraryId: number, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||
let params = new HttpParams();
|
||||
|
||||
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) => {
|
||||
return this._cachePaginatedResults(response, this.paginatedResults);
|
||||
})
|
||||
@ -79,7 +81,7 @@ export class SeriesService {
|
||||
}
|
||||
|
||||
updateSeries(model: any) {
|
||||
return this.httpClient.post(this.baseUrl + 'series/', model);
|
||||
return this.httpClient.post(this.baseUrl + 'series/update', model);
|
||||
}
|
||||
|
||||
markRead(seriesId: number) {
|
||||
@ -90,22 +92,27 @@ export class SeriesService {
|
||||
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();
|
||||
|
||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
|
||||
return this.httpClient.get<Series[]>(this.baseUrl + 'series/recently-added', {observe: 'response', params}).pipe(
|
||||
map((response: any) => {
|
||||
return this._cachePaginatedResults(response, this.paginatedSeriesForTagsResults);
|
||||
return this.httpClient.post<Series[]>(this.baseUrl + 'series/recently-added?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
|
||||
map(response => {
|
||||
return this._cachePaginatedResults(response, new PaginatedResult<Series[]>());
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getInProgress(libraryId: number = 0) {
|
||||
return this.httpClient.get<Series[]>(this.baseUrl + 'series/in-progress?libraryId=' + libraryId).pipe(map(series => {
|
||||
series.forEach(s => s.coverImage = this.imageService.getSeriesCoverImage(s.id));
|
||||
return series;
|
||||
getInProgress(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||
const data = this.createSeriesFilter(filter);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 { AuthGuard } from './_guards/auth.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
|
||||
|
||||
@ -44,6 +45,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{path: 'recently-added', component: RecentlyAddedComponent},
|
||||
{path: 'in-progress', component: InProgressComponent},
|
||||
{path: 'collections', 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 { EditSeriesModalComponent } from './_modals/edit-series-modal/edit-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 { 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 { LibraryCardComponent } from './library-card/library-card.component';
|
||||
import { SeriesCardComponent } from './series-card/series-card.component';
|
||||
import { InProgressComponent } from './in-progress/in-progress.component';
|
||||
|
||||
let sentryProviders: any[] = [];
|
||||
|
||||
@ -103,7 +103,8 @@ if (environment.production) {
|
||||
EditCollectionTagsComponent,
|
||||
RecentlyAddedComponent,
|
||||
LibraryCardComponent,
|
||||
SeriesCardComponent
|
||||
SeriesCardComponent,
|
||||
InProgressComponent
|
||||
],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
@ -120,7 +121,6 @@ if (environment.production) {
|
||||
NgbAccordionModule, // User Preferences
|
||||
NgxSliderModule, // User Preference
|
||||
NgbPaginationModule,
|
||||
LazyLoadImageModule,
|
||||
SharedModule,
|
||||
CarouselModule,
|
||||
TypeaheadModule,
|
||||
@ -136,7 +136,6 @@ if (environment.production) {
|
||||
providers: [
|
||||
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
||||
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
||||
//{ provide: LAZYLOAD_IMAGE_HOOKS, useClass: ScrollHooks } // Great, but causes flashing after modals close
|
||||
Title,
|
||||
...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"
|
||||
[actions]="actions"
|
||||
[pagination]="pagination"
|
||||
[filters]="filters"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { UpdateFilterEvent } from '../shared/card-detail-layout/card-detail-layout.component';
|
||||
import { Library } from '../_models/library';
|
||||
import { Pagination } from '../_models/pagination';
|
||||
import { Series } from '../_models/series';
|
||||
import { FilterItem, mangaFormatFilters, SeriesFilter } from '../_models/series-filter';
|
||||
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
|
||||
import { ActionService } from '../_services/action.service';
|
||||
import { LibraryService } from '../_services/library.service';
|
||||
@ -23,9 +25,14 @@ export class LibraryDetailComponent implements OnInit {
|
||||
loadingSeries = false;
|
||||
pagination!: Pagination;
|
||||
actions: ActionItem<Library>[] = [];
|
||||
filters: Array<FilterItem> = mangaFormatFilters;
|
||||
filter: SeriesFilter = {
|
||||
mangaFormat: null
|
||||
};
|
||||
|
||||
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');
|
||||
if (routeId === null) {
|
||||
this.router.navigateByUrl('/libraries');
|
||||
@ -36,12 +43,14 @@ export class LibraryDetailComponent implements OnInit {
|
||||
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(names => {
|
||||
this.libraryName = names[this.libraryId];
|
||||
this.titleService.setTitle('Kavita - ' + this.libraryName);
|
||||
})
|
||||
this.loadPage();
|
||||
});
|
||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
}
|
||||
|
||||
handleAction(action: Action, library: Library) {
|
||||
@ -61,17 +70,24 @@ export class LibraryDetailComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
if (this.pagination == undefined || this.pagination == null) {
|
||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
const page = this.route.snapshot.queryParamMap.get('page');
|
||||
loadPage() {
|
||||
const page = this.getPage();
|
||||
if (page != null) {
|
||||
this.pagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
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.pagination = series.pagination;
|
||||
this.loadingSeries = false;
|
||||
@ -80,13 +96,19 @@ export class LibraryDetailComponent implements OnInit {
|
||||
}
|
||||
|
||||
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) {
|
||||
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 { Router } from '@angular/router';
|
||||
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 { CollectionTag } from '../_models/collection-tag';
|
||||
import { InProgressChapter } from '../_models/in-progress-chapter';
|
||||
@ -20,7 +21,7 @@ import { SeriesService } from '../_services/series.service';
|
||||
templateUrl: './library.component.html',
|
||||
styleUrls: ['./library.component.scss']
|
||||
})
|
||||
export class LibraryComponent implements OnInit {
|
||||
export class LibraryComponent implements OnInit, OnDestroy {
|
||||
|
||||
user: User | undefined;
|
||||
libraries: Library[] = [];
|
||||
@ -33,6 +34,8 @@ export class LibraryComponent implements OnInit {
|
||||
collectionTags: CollectionTag[] = [];
|
||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
seriesTrackBy = (index: number, item: any) => `${item.name}_${item.pagesRead}`;
|
||||
|
||||
constructor(public accountService: AccountService, private libraryService: LibraryService,
|
||||
@ -57,13 +60,18 @@ export class LibraryComponent implements OnInit {
|
||||
this.reloadSeries();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
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.seriesService.getInProgress().subscribe((updatedSeries) => {
|
||||
this.inProgress = updatedSeries;
|
||||
this.seriesService.getInProgress().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
this.inProgress = updatedSeries.result;
|
||||
});
|
||||
|
||||
this.reloadTags();
|
||||
@ -78,15 +86,15 @@ export class LibraryComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.seriesService.getInProgress().subscribe((updatedSeries) => {
|
||||
this.inProgress = updatedSeries;
|
||||
this.seriesService.getInProgress().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
this.inProgress = updatedSeries.result;
|
||||
});
|
||||
|
||||
this.reloadTags();
|
||||
}
|
||||
|
||||
reloadTags() {
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
this.collectionService.allTags().pipe(takeUntil(this.onDestroy)).subscribe(tags => {
|
||||
this.collectionTags = tags;
|
||||
});
|
||||
}
|
||||
@ -96,7 +104,9 @@ export class LibraryComponent implements OnInit {
|
||||
this.router.navigate(['collections']);
|
||||
} else if (sectionTitle.toLowerCase() === 'recently added') {
|
||||
this.router.navigate(['recently-added']);
|
||||
}
|
||||
} else if (sectionTitle.toLowerCase() === 'in progress') {
|
||||
this.router.navigate(['in-progress']);
|
||||
}
|
||||
}
|
||||
|
||||
loadCollection(item: CollectionTag) {
|
||||
|
@ -3,6 +3,8 @@
|
||||
[isLoading]="isLoading"
|
||||
[items]="recentlyAdded"
|
||||
[pagination]="pagination"
|
||||
[filters]="filters"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
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 { Series } from '../_models/series';
|
||||
import { FilterItem, mangaFormatFilters, SeriesFilter } from '../_models/series-filter';
|
||||
import { SeriesService } from '../_services/series.service';
|
||||
|
||||
/**
|
||||
@ -19,36 +23,57 @@ export class RecentlyAddedComponent implements OnInit {
|
||||
pagination!: Pagination;
|
||||
libraryId!: number;
|
||||
|
||||
constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
}
|
||||
filters: Array<FilterItem> = mangaFormatFilters;
|
||||
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();
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
seriesClicked(series: Series) {
|
||||
this.router.navigate(['library', this.libraryId, 'series', series.id]);
|
||||
}
|
||||
|
||||
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() {
|
||||
const page = this.route.snapshot.queryParamMap.get('page');
|
||||
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.isLoading = true;
|
||||
this.seriesService.getRecentlyAdded(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage).subscribe(series => {
|
||||
this.recentlyAdded = series.result;
|
||||
this.pagination = series.pagination;
|
||||
this.isLoading = false;
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
const page = this.getPage();
|
||||
if (page != null) {
|
||||
this.pagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.seriesService.getRecentlyAdded(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');
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
<div class="container-fluid" *ngIf="series !== undefined" style="padding-top: 10px">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6">
|
||||
<img class="poster" [lazyLoad]="imageService.getSeriesCoverImage(series.id)" [defaultImage]="imageService.placeholderImage">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6">
|
||||
<img class="poster lazyload" [src]="imageSerivce.placeholderImage" [attr.data-src]="imageService.getSeriesCoverImage(series.id)"
|
||||
(error)="imageSerivce.updateErroredImage($event)" aria-hidden="true">
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6">
|
||||
<div class="row no-gutters">
|
||||
|
@ -86,7 +86,8 @@ export class SeriesDetailComponent implements OnInit {
|
||||
private accountService: AccountService, public imageService: ImageService,
|
||||
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
|
||||
private confirmService: ConfirmService, private titleService: Title,
|
||||
private downloadService: DownloadService, private actionService: ActionService) {
|
||||
private downloadService: DownloadService, private actionService: ActionService,
|
||||
public imageSerivce: ImageService) {
|
||||
ratingConfig.max = 5;
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
|
@ -1,8 +1,31 @@
|
||||
<div class="container-fluid" style="padding-top: 10px">
|
||||
<h2><span *ngIf="actions.length > 0" class="">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>
|
||||
</span> {{header}}</h2>
|
||||
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
|
||||
<div class="row no-gutters">
|
||||
|
@ -1,9 +1,33 @@
|
||||
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 { FilterItem } from 'src/app/_models/series-filter';
|
||||
import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||
|
||||
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({
|
||||
selector: 'app-card-detail-layout',
|
||||
templateUrl: './card-detail-layout.component.html',
|
||||
@ -15,12 +39,29 @@ export class CardDetailLayoutComponent implements OnInit {
|
||||
@Input() isLoading: boolean = false;
|
||||
@Input() items: any[] = [];
|
||||
@Input() pagination!: Pagination;
|
||||
/**
|
||||
* Any actions to exist on the header for the parent collection (library, collection)
|
||||
*/
|
||||
@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;
|
||||
@Output() itemClicked: EventEmitter<any> = new EventEmitter();
|
||||
@Output() pageChange: EventEmitter<Pagination> = new EventEmitter();
|
||||
@Output() applyFilter: EventEmitter<UpdateFilterEvent> = new EventEmitter();
|
||||
|
||||
@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() { }
|
||||
|
||||
@ -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="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]="imageSerivce.errorImage" alt="title">
|
||||
<img *ngIf="total > 0 || supressArchiveWarning" class="card-img-top lazyload" [src]="imageSerivce.placeholderImage" [attr.data-src]="imageUrl"
|
||||
(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)">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar></p>
|
||||
</div>
|
||||
|
@ -10,6 +10,8 @@ import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { UtilityService } from '../_services/utility.service';
|
||||
// import 'lazysizes';
|
||||
// import 'lazysizes/plugins/attrchange/ls.attrchange';
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-item',
|
||||
@ -38,8 +40,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
|
||||
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 {
|
||||
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
||||
@ -59,6 +60,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
// Custom animation for ng-lazyload-image
|
||||
img.ng-lazyloaded {
|
||||
animation: fadein .5s;
|
||||
//animation: fadein .5s; // I think it might look better without animation
|
||||
}
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
|
Loading…
x
Reference in New Issue
Block a user