mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-31 14:33:50 -04:00
Library Recomendations (#1236)
* Updated cover regex for finding cover images in archives to ignore back_cover or back-cover * Fixed an issue where Tags wouldn't save due to not pulling them from the DB. * Refactored All series to it's own lazy loaded module * Modularized Dashboard and library detail. Had to change main dashboard page to be libraries. Subject to change. * Refactored login component into registration module * Series Detail module created * Refactored nav stuff into it's own module, not lazy loaded, but self contained. * Refactored theme component into a dev only module so we don't incur load for temp testing modules * Finished off modularization code. Only missing thing is to re-introduce some dashboard functionality for library view. * Implemented a basic recommendation page for library detail
This commit is contained in:
parent
743a3ba935
commit
f237aa7ab7
@ -198,6 +198,7 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("ch1/cover.png", true)]
|
[InlineData("ch1/cover.png", true)]
|
||||||
[InlineData("ch1/backcover.png", false)]
|
[InlineData("ch1/backcover.png", false)]
|
||||||
[InlineData("backcover.png", false)]
|
[InlineData("backcover.png", false)]
|
||||||
|
[InlineData("back_cover.png", false)]
|
||||||
public void IsCoverImageTest(string inputPath, bool expected)
|
public void IsCoverImageTest(string inputPath, bool expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, IsCoverImage(inputPath));
|
Assert.Equal(expected, IsCoverImage(inputPath));
|
||||||
|
86
API/Controllers/RecommendedController.cs
Normal file
86
API/Controllers/RecommendedController.cs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.Extensions;
|
||||||
|
using API.Helpers;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
public class RecommendedController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
public RecommendedController(IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quick Reads are series that are less than 2K pages in total.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryId">Library to restrict series to</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("quick-reads")]
|
||||||
|
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
|
||||||
|
userParams ??= new UserParams();
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetQuickReads(user.Id, libraryId, userParams);
|
||||||
|
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||||
|
return Ok(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Highly Rated based on other users ratings. Will pull series with ratings > 4.0, weighted by count of other users.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryId">Library to restrict series to</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("highly-rated")]
|
||||||
|
public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
|
||||||
|
userParams ??= new UserParams();
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetHighlyRated(user.Id, libraryId, userParams);
|
||||||
|
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||||
|
return Ok(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chooses a random genre and shows series that are in that without reading progress
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryId">Library to restrict series to</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("more-in")]
|
||||||
|
public async Task<ActionResult<PagedList<SeriesDto>>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
|
||||||
|
userParams ??= new UserParams();
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetMoreIn(user.Id, libraryId, genreId, userParams);
|
||||||
|
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||||
|
return Ok(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Series that are fully read by the user in no particular order
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryId">Library to restrict series to</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("rediscover")]
|
||||||
|
public async Task<ActionResult<PagedList<SeriesDto>>> GetRediscover(int libraryId, [FromQuery] UserParams userParams)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
|
||||||
|
userParams ??= new UserParams();
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetRediscover(user.Id, libraryId, userParams);
|
||||||
|
|
||||||
|
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||||
|
return Ok(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -104,13 +104,17 @@ public interface ISeriesRepository
|
|||||||
Task<Series> GetFullSeriesForSeriesIdAsync(int seriesId);
|
Task<Series> GetFullSeriesForSeriesIdAsync(int seriesId);
|
||||||
Task<Chunk> GetChunkInfo(int libraryId = 0);
|
Task<Chunk> GetChunkInfo(int libraryId = 0);
|
||||||
Task<IList<SeriesMetadata>> GetSeriesMetadataForIdsAsync(IEnumerable<int> seriesIds);
|
Task<IList<SeriesMetadata>> GetSeriesMetadataForIdsAsync(IEnumerable<int> seriesIds);
|
||||||
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
|
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||||
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||||
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
|
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
|
||||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||||
|
|
||||||
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
|
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
|
||||||
|
Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams);
|
||||||
|
Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams);
|
||||||
|
Task<PagedList<SeriesDto>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams);
|
||||||
|
Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SeriesRepository : ISeriesRepository
|
public class SeriesRepository : ISeriesRepository
|
||||||
@ -416,7 +420,9 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.Include(s => s.Metadata)
|
.Include(s => s.Metadata)
|
||||||
.ThenInclude(m => m.Genres)
|
.ThenInclude(m => m.Genres)
|
||||||
.Include(s => s.Metadata)
|
.Include(s => s.Metadata)
|
||||||
.ThenInclude(m => m.People);
|
.ThenInclude(m => m.People)
|
||||||
|
.Include(s => s.Metadata)
|
||||||
|
.ThenInclude(m => m.Tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await query.SingleOrDefaultAsync();
|
return await query.SingleOrDefaultAsync();
|
||||||
@ -972,9 +978,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
|
|
||||||
public async Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind)
|
public async Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind)
|
||||||
{
|
{
|
||||||
var libraryIds = _context.AppUser
|
var libraryIds = GetLibraryIdsForUser(userId);
|
||||||
.Where(u => u.Id == userId)
|
|
||||||
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
|
|
||||||
var usersSeriesIds = _context.Series
|
var usersSeriesIds = _context.Series
|
||||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||||
.Select(s => s.Id);
|
.Select(s => s.Id);
|
||||||
@ -995,14 +999,100 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId)
|
private IQueryable<int> GetLibraryIdsForUser(int userId)
|
||||||
|
{
|
||||||
|
return _context.AppUser
|
||||||
|
.Where(u => u.Id == userId)
|
||||||
|
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedList<SeriesDto>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams)
|
||||||
{
|
{
|
||||||
var libraryIds = _context.AppUser
|
var libraryIds = _context.AppUser
|
||||||
.Where(u => u.Id == userId)
|
.Where(u => u.Id == userId)
|
||||||
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
|
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
||||||
var usersSeriesIds = _context.Series
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
|
||||||
.Select(s => s.Id);
|
var query = _context.Series
|
||||||
|
.Where(s => s.Metadata.Genres.Select(g => g.Id).Contains(genreId))
|
||||||
|
.Where(s => usersSeriesIds.Contains(s.Id))
|
||||||
|
.AsSplitQuery()
|
||||||
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||||
|
|
||||||
|
|
||||||
|
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams)
|
||||||
|
{
|
||||||
|
var libraryIds = _context.AppUser
|
||||||
|
.Where(u => u.Id == userId)
|
||||||
|
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
||||||
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
|
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||||
|
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||||
|
.Select(p => p.SeriesId)
|
||||||
|
.Distinct();
|
||||||
|
|
||||||
|
var query = _context.Series
|
||||||
|
.Where(s => distinctSeriesIdsWithProgress.Contains(s.Id) &&
|
||||||
|
_context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId)
|
||||||
|
.Sum(s1 => s1.PagesRead) >= s.Pages)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||||
|
|
||||||
|
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams)
|
||||||
|
{
|
||||||
|
var libraryIds = _context.AppUser
|
||||||
|
.Where(u => u.Id == userId)
|
||||||
|
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
||||||
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
|
var distinctSeriesIdsWithHighRating = _context.AppUserRating
|
||||||
|
.Where(s => usersSeriesIds.Contains(s.SeriesId) && s.Rating > 4)
|
||||||
|
.Select(p => p.SeriesId)
|
||||||
|
.Distinct();
|
||||||
|
|
||||||
|
var query = _context.Series
|
||||||
|
.Where(s => distinctSeriesIdsWithHighRating.Contains(s.Id))
|
||||||
|
.AsSplitQuery()
|
||||||
|
.OrderByDescending(s => _context.AppUserRating.Where(r => r.SeriesId == s.Id).Select(r => r.Rating).Average())
|
||||||
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||||
|
|
||||||
|
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams)
|
||||||
|
{
|
||||||
|
var libraryIds = _context.AppUser
|
||||||
|
.Where(u => u.Id == userId)
|
||||||
|
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
||||||
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
|
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||||
|
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||||
|
.Select(p => p.SeriesId)
|
||||||
|
.Distinct();
|
||||||
|
|
||||||
|
|
||||||
|
var query = _context.Series
|
||||||
|
.Where(s => s.Pages < 2000 && !distinctSeriesIdsWithProgress.Contains(s.Id) &&
|
||||||
|
usersSeriesIds.Contains(s.Id))
|
||||||
|
.Where(s => s.Metadata.PublicationStatus != PublicationStatus.OnGoing)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||||
|
|
||||||
|
|
||||||
|
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId)
|
||||||
|
{
|
||||||
|
var libraryIds = GetLibraryIdsForUser(userId);
|
||||||
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
|
|
||||||
return new RelatedSeriesDto()
|
return new RelatedSeriesDto()
|
||||||
{
|
{
|
||||||
@ -1032,6 +1122,13 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IQueryable<int> GetSeriesIdsForLibraryIds(IQueryable<int> libraryIds)
|
||||||
|
{
|
||||||
|
return _context.Series
|
||||||
|
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||||
|
.Select(s => s.Id);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<SeriesDto>> GetRelatedSeriesQuery(int seriesId, IEnumerable<int> usersSeriesIds, RelationKind kind)
|
private async Task<IEnumerable<SeriesDto>> GetRelatedSeriesQuery(int seriesId, IEnumerable<int> usersSeriesIds, RelationKind kind)
|
||||||
{
|
{
|
||||||
return await _context.Series.SelectMany(s =>
|
return await _context.Series.SelectMany(s =>
|
||||||
|
@ -54,7 +54,7 @@ namespace API.Parser
|
|||||||
MatchOptions, RegexTimeout);
|
MatchOptions, RegexTimeout);
|
||||||
private static readonly Regex BookFileRegex = new Regex(BookFileExtensions,
|
private static readonly Regex BookFileRegex = new Regex(BookFileExtensions,
|
||||||
MatchOptions, RegexTimeout);
|
MatchOptions, RegexTimeout);
|
||||||
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)((?<!back)cover|folder)(?![\w\d])",
|
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)(?<!back)(?<!back_)(?<!back-)(cover|folder)(?![\w\d])",
|
||||||
MatchOptions, RegexTimeout);
|
MatchOptions, RegexTimeout);
|
||||||
|
|
||||||
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]",
|
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]",
|
||||||
|
@ -285,15 +285,12 @@ public class SeriesService : ISeriesService
|
|||||||
var isModified = false;
|
var isModified = false;
|
||||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||||
var existingTags = series.Metadata.Tags.ToList();
|
var existingTags = series.Metadata.Tags.ToList();
|
||||||
foreach (var existing in existingTags)
|
foreach (var existing in existingTags.Where(existing => tags.SingleOrDefault(t => t.Id == existing.Id) == null))
|
||||||
{
|
|
||||||
if (tags.SingleOrDefault(t => t.Id == existing.Id) == null)
|
|
||||||
{
|
{
|
||||||
// Remove tag
|
// Remove tag
|
||||||
series.Metadata.Tags.Remove(existing);
|
series.Metadata.Tags.Remove(existing);
|
||||||
isModified = true;
|
isModified = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, all tags that aren't in dto have been removed.
|
// At this point, all tags that aren't in dto have been removed.
|
||||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||||
|
@ -7,7 +7,7 @@ import { Preferences } from '../_models/preferences/preferences';
|
|||||||
import { User } from '../_models/user';
|
import { User } from '../_models/user';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MessageHubService } from './message-hub.service';
|
import { MessageHubService } from './message-hub.service';
|
||||||
import { ThemeService } from '../theme.service';
|
import { ThemeService } from './theme.service';
|
||||||
import { InviteUserResponse } from '../_models/invite-user-response';
|
import { InviteUserResponse } from '../_models/invite-user-response';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import { Inject, Injectable, OnDestroy } from '@angular/core';
|
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { ThemeService } from '../theme.service';
|
import { ThemeService } from './theme.service';
|
||||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||||
import { AccountService } from './account.service';
|
import { AccountService } from './account.service';
|
||||||
import { NavService } from './nav.service';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
45
UI/Web/src/app/_services/recommendation.service.ts
Normal file
45
UI/Web/src/app/_services/recommendation.service.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { map } from 'rxjs';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { UtilityService } from '../shared/_services/utility.service';
|
||||||
|
import { PaginatedResult } from '../_models/pagination';
|
||||||
|
import { Series } from '../_models/series';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RecommendationService {
|
||||||
|
|
||||||
|
private baseUrl = environment.apiUrl;
|
||||||
|
|
||||||
|
constructor(private httpClient: HttpClient, private utilityService: UtilityService) { }
|
||||||
|
|
||||||
|
getQuickReads(libraryId: number, pageNum?: number, itemsPerPage?: number) {
|
||||||
|
let params = new HttpParams();
|
||||||
|
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
return this.httpClient.get<PaginatedResult<Series[]>>(this.baseUrl + 'recommended/quick-reads?libraryId=' + libraryId, {observe: 'response', params})
|
||||||
|
.pipe(map(response => this.utilityService.createPaginatedResult(response)));
|
||||||
|
}
|
||||||
|
|
||||||
|
getHighlyRated(libraryId: number, pageNum?: number, itemsPerPage?: number) {
|
||||||
|
let params = new HttpParams();
|
||||||
|
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
return this.httpClient.get<PaginatedResult<Series[]>>(this.baseUrl + 'recommended/highly-rated?libraryId=' + libraryId, {observe: 'response', params})
|
||||||
|
.pipe(map(response => this.utilityService.createPaginatedResult(response)));
|
||||||
|
}
|
||||||
|
|
||||||
|
getRediscover(libraryId: number, pageNum?: number, itemsPerPage?: number) {
|
||||||
|
let params = new HttpParams();
|
||||||
|
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
return this.httpClient.get<PaginatedResult<Series[]>>(this.baseUrl + 'recommended/rediscover?libraryId=' + libraryId, {observe: 'response', params})
|
||||||
|
.pipe(map(response => this.utilityService.createPaginatedResult(response)));
|
||||||
|
}
|
||||||
|
|
||||||
|
getMoreIn(libraryId: number, genreId: number, pageNum?: number, itemsPerPage?: number) {
|
||||||
|
let params = new HttpParams();
|
||||||
|
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
return this.httpClient.get<PaginatedResult<Series[]>>(this.baseUrl + 'recommended/more-in?libraryId=' + libraryId + '&genreId=' + genreId, {observe: 'response', params})
|
||||||
|
.pipe(map(response => this.utilityService.createPaginatedResult(response)));
|
||||||
|
}
|
||||||
|
}
|
@ -3,14 +3,13 @@ import { Injectable } from '@angular/core';
|
|||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { UtilityService } from '../shared/_services/utility.service';
|
||||||
import { Chapter } from '../_models/chapter';
|
import { Chapter } from '../_models/chapter';
|
||||||
import { ChapterMetadata } from '../_models/chapter-metadata';
|
import { ChapterMetadata } from '../_models/chapter-metadata';
|
||||||
import { CollectionTag } from '../_models/collection-tag';
|
import { CollectionTag } from '../_models/collection-tag';
|
||||||
import { PaginatedResult } from '../_models/pagination';
|
import { PaginatedResult } from '../_models/pagination';
|
||||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../_models/series';
|
||||||
import { RelatedSeries } from '../_models/series-detail/related-series';
|
import { RelatedSeries } from '../_models/series-detail/related-series';
|
||||||
import { RelationKind } from '../_models/series-detail/relation-kind';
|
|
||||||
import { SeriesDetail } from '../_models/series-detail/series-detail';
|
import { SeriesDetail } from '../_models/series-detail/series-detail';
|
||||||
import { SeriesFilter } from '../_models/series-filter';
|
import { SeriesFilter } from '../_models/series-filter';
|
||||||
import { SeriesGroup } from '../_models/series-group';
|
import { SeriesGroup } from '../_models/series-group';
|
||||||
@ -27,43 +26,28 @@ export class SeriesService {
|
|||||||
paginatedResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>();
|
paginatedResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>();
|
||||||
paginatedSeriesForTagsResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>();
|
paginatedSeriesForTagsResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>();
|
||||||
|
|
||||||
constructor(private httpClient: HttpClient, private imageService: ImageService) { }
|
constructor(private httpClient: HttpClient, private imageService: ImageService, private utilityService: UtilityService) { }
|
||||||
|
|
||||||
_cachePaginatedResults(response: any, paginatedVariable: PaginatedResult<any[]>) {
|
|
||||||
if (response.body === null) {
|
|
||||||
paginatedVariable.result = [];
|
|
||||||
} else {
|
|
||||||
paginatedVariable.result = response.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageHeader = response.headers.get('Pagination');
|
|
||||||
if (pageHeader !== null) {
|
|
||||||
paginatedVariable.pagination = JSON.parse(pageHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
return paginatedVariable;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllSeries(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
getAllSeries(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
const data = this.createSeriesFilter(filter);
|
const data = this.createSeriesFilter(filter);
|
||||||
|
|
||||||
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all', data, {observe: 'response', params}).pipe(
|
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all', data, {observe: 'response', params}).pipe(
|
||||||
map((response: any) => {
|
map((response: any) => {
|
||||||
return this._cachePaginatedResults(response, this.paginatedResults);
|
return this.utilityService.createPaginatedResult(response, this.paginatedResults);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSeriesForLibrary(libraryId: number, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
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.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
const data = this.createSeriesFilter(filter);
|
const data = this.createSeriesFilter(filter);
|
||||||
|
|
||||||
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series?libraryId=' + libraryId, data, {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.utilityService.createPaginatedResult(response, this.paginatedResults);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -123,11 +107,11 @@ export class SeriesService {
|
|||||||
getRecentlyAdded(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
getRecentlyAdded(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||||
const data = this.createSeriesFilter(filter);
|
const data = this.createSeriesFilter(filter);
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
|
||||||
return this.httpClient.post<Series[]>(this.baseUrl + 'series/recently-added?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
|
return this.httpClient.post<Series[]>(this.baseUrl + 'series/recently-added?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
|
||||||
map(response => {
|
map(response => {
|
||||||
return this._cachePaginatedResults(response, new PaginatedResult<Series[]>());
|
return this.utilityService.createPaginatedResult(response, new PaginatedResult<Series[]>());
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -140,11 +124,11 @@ export class SeriesService {
|
|||||||
const data = this.createSeriesFilter(filter);
|
const data = this.createSeriesFilter(filter);
|
||||||
|
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
|
||||||
return this.httpClient.post<Series[]>(this.baseUrl + 'series/on-deck?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
|
return this.httpClient.post<Series[]>(this.baseUrl + 'series/on-deck?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
|
||||||
map(response => {
|
map(response => {
|
||||||
return this._cachePaginatedResults(response, new PaginatedResult<Series[]>());
|
return this.utilityService.createPaginatedResult(response, new PaginatedResult<Series[]>());
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,11 +159,11 @@ export class SeriesService {
|
|||||||
getSeriesForTag(collectionTagId: number, pageNum?: number, itemsPerPage?: number) {
|
getSeriesForTag(collectionTagId: number, pageNum?: number, itemsPerPage?: number) {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
|
|
||||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
|
||||||
return this.httpClient.get<PaginatedResult<Series[]>>(this.baseUrl + 'series/series-by-collection?collectionId=' + collectionTagId, {observe: 'response', params}).pipe(
|
return this.httpClient.get<PaginatedResult<Series[]>>(this.baseUrl + 'series/series-by-collection?collectionId=' + collectionTagId, {observe: 'response', params}).pipe(
|
||||||
map((response: any) => {
|
map((response: any) => {
|
||||||
return this._cachePaginatedResults(response, this.paginatedSeriesForTagsResults);
|
return this.utilityService.createPaginatedResult(response, this.paginatedSeriesForTagsResults);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -201,13 +185,7 @@ export class SeriesService {
|
|||||||
return this.httpClient.get<SeriesDetail>(this.baseUrl + 'series/series-detail?seriesId=' + seriesId);
|
return this.httpClient.get<SeriesDetail>(this.baseUrl + 'series/series-detail?seriesId=' + seriesId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_addPaginationIfExists(params: HttpParams, pageNum?: number, itemsPerPage?: number) {
|
|
||||||
if (pageNum !== null && pageNum !== undefined && itemsPerPage !== null && itemsPerPage !== undefined) {
|
|
||||||
params = params.append('pageNumber', pageNum + '');
|
|
||||||
params = params.append('pageSize', itemsPerPage + '');
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
createSeriesFilter(filter?: SeriesFilter) {
|
createSeriesFilter(filter?: SeriesFilter) {
|
||||||
if (filter !== undefined) return filter;
|
if (filter !== undefined) return filter;
|
||||||
|
@ -4,11 +4,10 @@ import { Inject, Injectable, OnDestroy, Renderer2, RendererFactory2, SecurityCon
|
|||||||
import { DomSanitizer } from '@angular/platform-browser';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
import { map, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
import { map, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { ConfirmService } from './shared/confirm.service';
|
import { ConfirmService } from '../shared/confirm.service';
|
||||||
import { NotificationProgressEvent } from './_models/events/notification-progress-event';
|
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
||||||
import { SiteThemeProgressEvent } from './_models/events/site-theme-progress-event';
|
import { SiteTheme, ThemeProvider } from '../_models/preferences/site-theme';
|
||||||
import { SiteTheme, ThemeProvider } from './_models/preferences/site-theme';
|
import { EVENTS, MessageHubService } from './message-hub.service';
|
||||||
import { EVENTS, MessageHubService } from './_services/message-hub.service';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
20
UI/Web/src/app/all-series/all-series-routing.module.ts
Normal file
20
UI/Web/src/app/all-series/all-series-routing.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { Routes, RouterModule } from "@angular/router";
|
||||||
|
import { AuthGuard } from "../_guards/auth.guard";
|
||||||
|
import { AllSeriesComponent } from "./all-series.component";
|
||||||
|
const routes: Routes = [
|
||||||
|
{path: '**', component: AllSeriesComponent, pathMatch: 'full', canActivate: [AuthGuard]},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: AllSeriesComponent,
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AllSeriesRoutingModule { }
|
@ -7,11 +7,10 @@ import { BulkSelectionService } from '../cards/bulk-selection.service';
|
|||||||
import { FilterSettings } from '../metadata-filter/filter-settings';
|
import { FilterSettings } from '../metadata-filter/filter-settings';
|
||||||
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||||
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||||
import { 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 { FilterEvent, SeriesFilter } from '../_models/series-filter';
|
import { FilterEvent, SeriesFilter } from '../_models/series-filter';
|
||||||
import { ActionItem, Action } from '../_services/action-factory.service';
|
import { Action } from '../_services/action-factory.service';
|
||||||
import { ActionService } from '../_services/action.service';
|
import { ActionService } from '../_services/action.service';
|
||||||
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
|
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
|
||||||
import { SeriesService } from '../_services/series.service';
|
import { SeriesService } from '../_services/series.service';
|
||||||
|
21
UI/Web/src/app/all-series/all-series.module.ts
Normal file
21
UI/Web/src/app/all-series/all-series.module.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AllSeriesComponent } from './all-series.component';
|
||||||
|
import { AllSeriesRoutingModule } from './all-series-routing.module';
|
||||||
|
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AllSeriesComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
AllSeriesRoutingModule,
|
||||||
|
|
||||||
|
SharedSideNavCardsModule
|
||||||
|
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AllSeriesModule { }
|
@ -1,19 +1,12 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
|
||||||
import { LibraryDetailComponent } from './library-detail/library-detail.component';
|
|
||||||
import { SeriesDetailComponent } from './series-detail/series-detail.component';
|
|
||||||
import { UserLoginComponent } from './user-login/user-login.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 { DashboardComponent } from './dashboard/dashboard.component';
|
|
||||||
import { AllSeriesComponent } from './all-series/all-series.component';
|
|
||||||
import { AdminGuard } from './_guards/admin.guard';
|
import { AdminGuard } from './_guards/admin.guard';
|
||||||
import { ThemeTestComponent } from './theme-test/theme-test.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
|
||||||
|
// TODO: Use Prefetching of LazyLoaded Modules
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: '', component: UserLoginComponent},
|
|
||||||
{
|
{
|
||||||
path: 'admin',
|
path: 'admin',
|
||||||
canActivate: [AdminGuard],
|
canActivate: [AdminGuard],
|
||||||
@ -47,40 +40,48 @@ const routes: Routes = [
|
|||||||
loadChildren: () => import('../app/bookmark/bookmark.module').then(m => m.BookmarkModule)
|
loadChildren: () => import('../app/bookmark/bookmark.module').then(m => m.BookmarkModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: 'all-series',
|
||||||
|
loadChildren: () => import('../app/all-series/all-series.module').then(m => m.AllSeriesModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'libraries',
|
||||||
|
loadChildren: () => import('../app/dashboard/dashboard.module').then(m => m.DashboardModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'library',
|
||||||
runGuardsAndResolvers: 'always',
|
runGuardsAndResolvers: 'always',
|
||||||
canActivate: [AuthGuard, LibraryAccessGuard],
|
canActivate: [AuthGuard, LibraryAccessGuard],
|
||||||
children: [
|
children: [
|
||||||
{path: 'library/:id', component: LibraryDetailComponent},
|
|
||||||
{path: 'library/:libraryId/series/:seriesId', component: SeriesDetailComponent},
|
|
||||||
{
|
{
|
||||||
path: 'library/:libraryId/series/:seriesId/manga',
|
path: ':libraryId',
|
||||||
|
pathMatch: 'full',
|
||||||
|
loadChildren: () => import('../app/library-detail/library-detail.module').then(m => m.LibraryDetailModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':libraryId/series/:seriesId',
|
||||||
|
pathMatch: 'full',
|
||||||
|
loadChildren: () => import('../app/series-detail/series-detail.module').then(m => m.SeriesDetailModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':libraryId/series/:seriesId/manga',
|
||||||
loadChildren: () => import('../app/manga-reader/manga-reader.module').then(m => m.MangaReaderModule)
|
loadChildren: () => import('../app/manga-reader/manga-reader.module').then(m => m.MangaReaderModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'library/:libraryId/series/:seriesId/book',
|
path: ':libraryId/series/:seriesId/book',
|
||||||
loadChildren: () => import('../app/book-reader/book-reader.module').then(m => m.BookReaderModule)
|
loadChildren: () => import('../app/book-reader/book-reader.module').then(m => m.BookReaderModule)
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: 'theme',
|
||||||
runGuardsAndResolvers: 'always',
|
loadChildren: () => import('../app/dev-only/dev-only.module').then(m => m.DevOnlyModule)
|
||||||
canActivate: [AuthGuard],
|
|
||||||
children: [
|
|
||||||
{path: 'library', component: DashboardComponent},
|
|
||||||
{path: 'all-series', component: AllSeriesComponent}, // TODO: This might be better as a separate module
|
|
||||||
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{path: 'theme', component: ThemeTestComponent},
|
{path: 'login', loadChildren: () => import('../app/registration/registration.module').then(m => m.RegistrationModule)},
|
||||||
|
{path: '**', loadChildren: () => import('../app/dashboard/dashboard.module').then(m => m.DashboardModule), pathMatch: 'full'},
|
||||||
{path: 'login', component: UserLoginComponent}, // TODO: move this to registration module
|
|
||||||
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled'})],
|
imports: [RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled', preloadingStrategy: PreloadAllModules})],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule]
|
||||||
})
|
})
|
||||||
export class AppRoutingModule { }
|
export class AppRoutingModule { }
|
||||||
|
@ -4,85 +4,30 @@ import { NgModule } from '@angular/core';
|
|||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import {
|
|
||||||
NgbAccordionModule, NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbPopoverModule, NgbRatingModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { NavHeaderComponent } from './nav-header/nav-header.component';
|
|
||||||
import { JwtInterceptor } from './_interceptors/jwt.interceptor';
|
import { JwtInterceptor } from './_interceptors/jwt.interceptor';
|
||||||
import { UserLoginComponent } from './user-login/user-login.component';
|
|
||||||
import { ToastrModule } from 'ngx-toastr';
|
import { ToastrModule } from 'ngx-toastr';
|
||||||
import { ErrorInterceptor } from './_interceptors/error.interceptor';
|
import { ErrorInterceptor } from './_interceptors/error.interceptor';
|
||||||
import { LibraryComponent } from './library/library.component';
|
|
||||||
import { SharedModule } from './shared/shared.module';
|
|
||||||
import { LibraryDetailComponent } from './library-detail/library-detail.component';
|
|
||||||
import { SeriesDetailComponent } from './series-detail/series-detail.component';
|
|
||||||
import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component';
|
|
||||||
import { CarouselModule } from './carousel/carousel.module';
|
|
||||||
|
|
||||||
import { TypeaheadModule } from './typeahead/typeahead.module';
|
|
||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
|
||||||
import { CardsModule } from './cards/cards.module';
|
|
||||||
import { CollectionsModule } from './collections/collections.module';
|
|
||||||
import { ReadingListModule } from './reading-list/reading-list.module';
|
|
||||||
import { SAVER, getSaver } from './shared/_providers/saver.provider';
|
import { SAVER, getSaver } from './shared/_providers/saver.provider';
|
||||||
import { EventsWidgetComponent } from './events-widget/events-widget.component';
|
|
||||||
import { SeriesMetadataDetailComponent } from './series-metadata-detail/series-metadata-detail.component';
|
|
||||||
import { AllSeriesComponent } from './all-series/all-series.component';
|
|
||||||
import { RegistrationModule } from './registration/registration.module';
|
|
||||||
import { GroupedTypeaheadComponent } from './grouped-typeahead/grouped-typeahead.component';
|
|
||||||
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
|
||||||
import { PipeModule } from './pipe/pipe.module';
|
|
||||||
import { SidenavModule } from './sidenav/sidenav.module';
|
import { SidenavModule } from './sidenav/sidenav.module';
|
||||||
|
import { NavModule } from './nav/nav.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
NavHeaderComponent,
|
|
||||||
UserLoginComponent,
|
|
||||||
LibraryComponent,
|
|
||||||
LibraryDetailComponent,
|
|
||||||
SeriesDetailComponent,
|
|
||||||
ReviewSeriesModalComponent,
|
|
||||||
DashboardComponent,
|
|
||||||
EventsWidgetComponent,
|
|
||||||
SeriesMetadataDetailComponent,
|
|
||||||
AllSeriesComponent,
|
|
||||||
GroupedTypeaheadComponent,
|
|
||||||
ThemeTestComponent,
|
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
ReactiveFormsModule,
|
|
||||||
FormsModule, // EditCollection Modal
|
|
||||||
|
|
||||||
NgbDropdownModule, // Nav
|
|
||||||
NgbPopoverModule, // Nav Events toggle
|
|
||||||
NgbNavModule,
|
|
||||||
|
|
||||||
NgbRatingModule, // Series Detail & Filter
|
|
||||||
NgbPaginationModule,
|
|
||||||
|
|
||||||
NgbCollapseModule, // Login
|
|
||||||
|
|
||||||
SharedModule,
|
|
||||||
CarouselModule,
|
|
||||||
TypeaheadModule,
|
|
||||||
CardsModule,
|
|
||||||
CollectionsModule,
|
|
||||||
ReadingListModule,
|
|
||||||
RegistrationModule,
|
|
||||||
|
|
||||||
NgbAccordionModule, // ThemeTest Component only
|
|
||||||
PipeModule,
|
|
||||||
|
|
||||||
PipeModule,
|
|
||||||
SidenavModule, // For sidenav
|
|
||||||
|
|
||||||
|
SidenavModule,
|
||||||
|
NavModule,
|
||||||
|
|
||||||
ToastrModule.forRoot({
|
ToastrModule.forRoot({
|
||||||
positionClass: 'toast-bottom-right',
|
positionClass: 'toast-bottom-right',
|
||||||
|
@ -20,11 +20,11 @@ import { animate, state, style, transition, trigger } from '@angular/animations'
|
|||||||
import { Stack } from 'src/app/shared/data-structures/stack';
|
import { Stack } from 'src/app/shared/data-structures/stack';
|
||||||
import { MemberService } from 'src/app/_services/member.service';
|
import { MemberService } from 'src/app/_services/member.service';
|
||||||
import { ReadingDirection } from 'src/app/_models/preferences/reading-direction';
|
import { ReadingDirection } from 'src/app/_models/preferences/reading-direction';
|
||||||
import { ScrollService } from 'src/app/scroll.service';
|
|
||||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||||
import { LibraryService } from 'src/app/_services/library.service';
|
import { LibraryService } from 'src/app/_services/library.service';
|
||||||
import { LibraryType } from 'src/app/_models/library';
|
import { LibraryType } from 'src/app/_models/library';
|
||||||
import { ThemeService } from 'src/app/theme.service';
|
import { ThemeService } from 'src/app/_services/theme.service';
|
||||||
|
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||||
|
|
||||||
|
|
||||||
interface PageStyle {
|
interface PageStyle {
|
||||||
@ -415,7 +415,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
const chapterId = this.route.snapshot.paramMap.get('chapterId');
|
const chapterId = this.route.snapshot.paramMap.get('chapterId');
|
||||||
|
|
||||||
if (libraryId === null || seriesId === null || chapterId === null) {
|
if (libraryId === null || seriesId === null || chapterId === null) {
|
||||||
this.router.navigateByUrl('/library');
|
this.router.navigateByUrl('/libraries');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CardsModule } from '../cards/cards.module';
|
|
||||||
import { SharedModule } from '../shared/shared.module';
|
|
||||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { SidenavModule } from '../sidenav/sidenav.module';
|
|
||||||
import { BookmarkRoutingModule } from './bookmark-routing.module';
|
import { BookmarkRoutingModule } from './bookmark-routing.module';
|
||||||
import { BookmarksComponent } from './bookmarks/bookmarks.component';
|
import { BookmarksComponent } from './bookmarks/bookmarks.component';
|
||||||
|
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -15,10 +12,8 @@ import { BookmarksComponent } from './bookmarks/bookmarks.component';
|
|||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CardsModule,
|
|
||||||
SharedModule,
|
SharedSideNavCardsModule,
|
||||||
SidenavModule,
|
|
||||||
NgbTooltipModule,
|
|
||||||
|
|
||||||
BookmarkRoutingModule
|
BookmarkRoutingModule
|
||||||
]
|
]
|
||||||
|
@ -54,7 +54,6 @@ import { EditSeriesRelationComponent } from './edit-series-relation/edit-series-
|
|||||||
|
|
||||||
MetadataFilterModule,
|
MetadataFilterModule,
|
||||||
|
|
||||||
NgbNavModule,
|
|
||||||
NgbTooltipModule, // Card item
|
NgbTooltipModule, // Card item
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
NgbRatingModule,
|
NgbRatingModule,
|
||||||
|
@ -2,10 +2,9 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CollectionDetailComponent } from './collection-detail/collection-detail.component';
|
import { CollectionDetailComponent } from './collection-detail/collection-detail.component';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { CollectionsRoutingModule } from './collections-routing.module';
|
|
||||||
import { CardsModule } from '../cards/cards.module';
|
|
||||||
import { AllCollectionsComponent } from './all-collections/all-collections.component';
|
import { AllCollectionsComponent } from './all-collections/all-collections.component';
|
||||||
import { SidenavModule } from '../sidenav/sidenav.module';
|
import { CollectionsRoutingModule } from './collections-routing.module';
|
||||||
|
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -17,9 +16,10 @@ import { SidenavModule } from '../sidenav/sidenav.module';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
CardsModule,
|
|
||||||
|
SharedSideNavCardsModule,
|
||||||
|
|
||||||
CollectionsRoutingModule,
|
CollectionsRoutingModule,
|
||||||
SidenavModule
|
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AllCollectionsComponent
|
AllCollectionsComponent
|
||||||
|
21
UI/Web/src/app/dashboard/dashboard-routing.module.ts
Normal file
21
UI/Web/src/app/dashboard/dashboard-routing.module.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
import { AuthGuard } from '../_guards/auth.guard';
|
||||||
|
import { DashboardComponent } from './dashboard.component';
|
||||||
|
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
component: DashboardComponent,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes), ],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class DashboardRoutingModule { }
|
@ -1,3 +1,31 @@
|
|||||||
<app-side-nav-companion-bar>
|
<app-side-nav-companion-bar>
|
||||||
</app-side-nav-companion-bar>
|
</app-side-nav-companion-bar>
|
||||||
<app-library></app-library>
|
<ng-container *ngIf="libraries.length === 0 && !isLoading">
|
||||||
|
<div class="mt-3">
|
||||||
|
<div *ngIf="isAdmin" class="d-flex justify-content-center">
|
||||||
|
<p>There are no libraries setup yet. Configure some in <a routerLink="/admin/dashboard" fragment="libraries">Server settings</a>.</p>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!isAdmin" class="d-flex justify-content-center">
|
||||||
|
<p>You haven't been granted access to any libraries.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
|
||||||
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
|
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||||
|
</ng-template>
|
||||||
|
</app-carousel-reel>
|
||||||
|
|
||||||
|
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series" (sectionClick)="handleSectionClick($event)">
|
||||||
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
|
<app-card-item [entity]="item" [title]="item.seriesName" [suppressLibraryLink]="libraryId !== 0" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||||
|
[supressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
|
||||||
|
</ng-template>
|
||||||
|
</app-carousel-reel>
|
||||||
|
|
||||||
|
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series" (sectionClick)="handleSectionClick($event)">
|
||||||
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
|
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
|
||||||
|
</ng-template>
|
||||||
|
</app-carousel-reel>
|
@ -1,20 +1,180 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { ReplaySubject, Subject } from 'rxjs';
|
||||||
|
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||||
|
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
|
||||||
|
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||||
|
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
|
||||||
|
import { Library } from '../_models/library';
|
||||||
|
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||||
|
import { Series } from '../_models/series';
|
||||||
|
import { SortField } from '../_models/series-filter';
|
||||||
|
import { SeriesGroup } from '../_models/series-group';
|
||||||
|
import { User } from '../_models/user';
|
||||||
|
import { AccountService } from '../_services/account.service';
|
||||||
|
import { ImageService } from '../_services/image.service';
|
||||||
|
import { LibraryService } from '../_services/library.service';
|
||||||
|
import { MessageHubService, EVENTS } from '../_services/message-hub.service';
|
||||||
|
import { SeriesService } from '../_services/series.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
templateUrl: './dashboard.component.html',
|
templateUrl: './dashboard.component.html',
|
||||||
styleUrls: ['./dashboard.component.scss']
|
styleUrls: ['./dashboard.component.scss']
|
||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit {
|
export class DashboardComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, 0, but if non-zero, will limit all API calls to library id
|
||||||
|
*/
|
||||||
|
@Input() libraryId: number = 0;
|
||||||
|
|
||||||
constructor(public route: ActivatedRoute, private titleService: Title) {
|
user: User | undefined;
|
||||||
this.titleService.setTitle('Kavita - Dashboard');
|
libraries: Library[] = [];
|
||||||
|
isLoading = false;
|
||||||
|
isAdmin = false;
|
||||||
|
|
||||||
|
recentlyUpdatedSeries: SeriesGroup[] = [];
|
||||||
|
inProgress: Series[] = [];
|
||||||
|
recentlyAddedSeries: Series[] = [];
|
||||||
|
|
||||||
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We use this Replay subject to slow the amount of times we reload the UI
|
||||||
|
*/
|
||||||
|
private loadRecentlyAdded$: ReplaySubject<void> = new ReplaySubject<void>();
|
||||||
|
|
||||||
|
constructor(public accountService: AccountService, private libraryService: LibraryService,
|
||||||
|
private seriesService: SeriesService, private router: Router,
|
||||||
|
private titleService: Title, public imageService: ImageService,
|
||||||
|
private messageHub: MessageHubService) {
|
||||||
|
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
|
||||||
|
if (res.event === EVENTS.SeriesAdded) {
|
||||||
|
const seriesAddedEvent = res.payload as SeriesAddedEvent;
|
||||||
|
|
||||||
|
this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
|
||||||
|
this.recentlyAddedSeries.unshift(series);
|
||||||
|
});
|
||||||
|
} else if (res.event === EVENTS.SeriesRemoved) {
|
||||||
|
const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
|
||||||
|
|
||||||
|
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||||
|
this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||||
|
this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
||||||
|
} else if (res.event === EVENTS.ScanSeries) {
|
||||||
|
// We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time.
|
||||||
|
this.loadRecentlyAdded$.next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loadRecentlyAdded$.pipe(debounceTime(1000), takeUntil(this.onDestroy)).subscribe(() => this.loadRecentlyAdded());
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.titleService.setTitle('Kavita - Dashboard');
|
||||||
|
this.isLoading = true;
|
||||||
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
|
this.user = user;
|
||||||
|
if (this.user) {
|
||||||
|
this.isAdmin = this.accountService.hasAdminRole(this.user);
|
||||||
|
this.libraryService.getLibrariesForMember().pipe(take(1)).subscribe(libraries => {
|
||||||
|
this.libraries = libraries;
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.reloadSeries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.onDestroy.next();
|
||||||
|
this.onDestroy.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadSeries() {
|
||||||
|
this.loadOnDeck();
|
||||||
|
this.loadRecentlyAdded();
|
||||||
|
this.loadRecentlyAddedSeries();
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadInProgress(series: Series | boolean) {
|
||||||
|
if (series === true || series === false) {
|
||||||
|
if (!series) {return;}
|
||||||
|
}
|
||||||
|
// If the update to Series doesn't affect the requirement to be in this stream, then ignore update request
|
||||||
|
const seriesObj = (series as Series);
|
||||||
|
if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadOnDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOnDeck() {
|
||||||
|
let api = this.seriesService.getOnDeck();
|
||||||
|
if (this.libraryId > 0) {
|
||||||
|
api = this.seriesService.getOnDeck(this.libraryId);
|
||||||
|
}
|
||||||
|
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||||
|
this.inProgress = updatedSeries.result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRecentlyAddedSeries() {
|
||||||
|
let api = this.seriesService.getRecentlyAdded();
|
||||||
|
if (this.libraryId > 0) {
|
||||||
|
api = this.seriesService.getRecentlyAdded(this.libraryId);
|
||||||
|
}
|
||||||
|
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||||
|
this.recentlyAddedSeries = updatedSeries.result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
loadRecentlyAdded() {
|
||||||
|
let api = this.seriesService.getRecentlyUpdatedSeries();
|
||||||
|
if (this.libraryId > 0) {
|
||||||
|
api = this.seriesService.getRecentlyUpdatedSeries();
|
||||||
|
}
|
||||||
|
api.pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
||||||
|
this.recentlyUpdatedSeries = updatedSeries.filter(group => {
|
||||||
|
if (this.libraryId === 0) return true;
|
||||||
|
return group.libraryId === this.libraryId;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRecentlyAddedChapterClick(item: RecentlyAddedItem) {
|
||||||
|
this.router.navigate(['library', item.libraryId, 'series', item.seriesId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSectionClick(sectionTitle: string) {
|
||||||
|
if (sectionTitle.toLowerCase() === 'recently updated series') {
|
||||||
|
const params: any = {};
|
||||||
|
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
|
||||||
|
params[FilterQueryParam.Page] = 1;
|
||||||
|
this.router.navigate(['all-series'], {queryParams: params});
|
||||||
|
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
||||||
|
const params: any = {};
|
||||||
|
params[FilterQueryParam.ReadStatus] = 'true,false,false';
|
||||||
|
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
|
||||||
|
params[FilterQueryParam.Page] = 1;
|
||||||
|
this.router.navigate(['all-series'], {queryParams: params});
|
||||||
|
}else if (sectionTitle.toLowerCase() === 'newly added series') {
|
||||||
|
const params: any = {};
|
||||||
|
params[FilterQueryParam.SortBy] = SortField.Created + ',false'; // sort by created, desc
|
||||||
|
params[FilterQueryParam.Page] = 1;
|
||||||
|
this.router.navigate(['all-series'], {queryParams: params});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromArray(arr: Array<any>, element: any) {
|
||||||
|
const index = arr.indexOf(element);
|
||||||
|
if (index >= 0) {
|
||||||
|
arr.splice(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
23
UI/Web/src/app/dashboard/dashboard.module.ts
Normal file
23
UI/Web/src/app/dashboard/dashboard.module.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { DashboardRoutingModule } from './dashboard-routing.module';
|
||||||
|
|
||||||
|
import { CarouselModule } from '../carousel/carousel.module';
|
||||||
|
import { DashboardComponent } from './dashboard.component';
|
||||||
|
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [DashboardComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
|
||||||
|
CarouselModule,
|
||||||
|
|
||||||
|
SharedSideNavCardsModule,
|
||||||
|
|
||||||
|
DashboardRoutingModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class DashboardModule { }
|
18
UI/Web/src/app/dev-only/dev-only-routing.module.ts
Normal file
18
UI/Web/src/app/dev-only/dev-only-routing.module.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
||||||
|
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ThemeTestComponent,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes), ],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class DevOnlyRoutingModule { }
|
37
UI/Web/src/app/dev-only/dev-only.module.ts
Normal file
37
UI/Web/src/app/dev-only/dev-only.module.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { CardsModule } from '../cards/cards.module';
|
||||||
|
import { TypeaheadModule } from '../typeahead/typeahead.module';
|
||||||
|
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
|
import { DevOnlyRoutingModule } from './dev-only-routing.module';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module contains components that aren't meant to ship with main code. They are there to test things out. This module may be deleted in future updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
ThemeTestComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
|
||||||
|
|
||||||
|
TypeaheadModule,
|
||||||
|
CardsModule,
|
||||||
|
NgbAccordionModule,
|
||||||
|
NgbNavModule,
|
||||||
|
|
||||||
|
|
||||||
|
SharedModule,
|
||||||
|
PipeModule,
|
||||||
|
|
||||||
|
DevOnlyRoutingModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class DevOnlyModule { }
|
@ -1,11 +1,11 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
|
import { TagBadgeCursor } from '../../shared/tag-badge/tag-badge.component';
|
||||||
import { ThemeService } from '../theme.service';
|
import { ThemeService } from '../../_services/theme.service';
|
||||||
import { MangaFormat } from '../_models/manga-format';
|
import { MangaFormat } from '../../_models/manga-format';
|
||||||
import { Person, PersonRole } from '../_models/person';
|
import { Person, PersonRole } from '../../_models/person';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../../_models/series';
|
||||||
import { NavService } from '../_services/nav.service';
|
import { NavService } from '../../_services/nav.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-theme-test',
|
selector: 'app-theme-test',
|
@ -0,0 +1,26 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
import { AuthGuard } from '../_guards/auth.guard';
|
||||||
|
import { LibraryAccessGuard } from '../_guards/library-access.guard';
|
||||||
|
import { LibraryDetailComponent } from './library-detail.component';
|
||||||
|
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: ':libraryId',
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
|
canActivate: [AuthGuard, LibraryAccessGuard],
|
||||||
|
component: LibraryDetailComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: LibraryDetailComponent
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes), ],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class LibraryDetailRoutingModule { }
|
@ -10,7 +10,7 @@
|
|||||||
<a ngbNavLink>{{tab.title | sentenceCase}}</a>
|
<a ngbNavLink>{{tab.title | sentenceCase}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<ng-container *ngIf="tab.title === 'Recommended'">
|
<ng-container *ngIf="tab.title === 'Recommended'">
|
||||||
<app-library [libraryId]="libraryId"></app-library>
|
<app-library-recommended [libraryId]="libraryId"></app-library-recommended>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="tab.title === 'Library'">
|
<ng-container *ngIf="tab.title === 'Library'">
|
||||||
<app-card-detail-layout
|
<app-card-detail-layout
|
||||||
|
@ -2,9 +2,8 @@ import { Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angul
|
|||||||
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 { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { debounceTime, take, takeUntil, takeWhile } from 'rxjs/operators';
|
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||||
import { FilterSettings } from '../metadata-filter/filter-settings';
|
|
||||||
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||||
import { Library } from '../_models/library';
|
import { Library } from '../_models/library';
|
||||||
@ -18,6 +17,7 @@ import { EVENTS, MessageHubService } from '../_services/message-hub.service';
|
|||||||
import { SeriesService } from '../_services/series.service';
|
import { SeriesService } from '../_services/series.service';
|
||||||
import { NavService } from '../_services/nav.service';
|
import { NavService } from '../_services/nav.service';
|
||||||
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||||
|
import { FilterSettings } from '../metadata-filter/filter-settings';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-library-detail',
|
selector: 'app-library-detail',
|
||||||
@ -87,8 +87,9 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
||||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||||
private utilityService: UtilityService, public navService: NavService, private filterUtilityService: FilterUtilitiesService) {
|
private utilityService: UtilityService, public navService: NavService, private filterUtilityService: FilterUtilitiesService) {
|
||||||
const routeId = this.route.snapshot.paramMap.get('id');
|
const routeId = this.route.snapshot.paramMap.get('libraryId');
|
||||||
if (routeId === null) {
|
if (routeId === null) {
|
||||||
|
console.log('Redirecting due to not seeing libraryId in route');
|
||||||
this.router.navigateByUrl('/libraries');
|
this.router.navigateByUrl('/libraries');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
28
UI/Web/src/app/library-detail/library-detail.module.ts
Normal file
28
UI/Web/src/app/library-detail/library-detail.module.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { LibraryDetailComponent } from './library-detail.component';
|
||||||
|
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
|
import { LibraryDetailRoutingModule } from './library-detail-routing.module';
|
||||||
|
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
|
||||||
|
import { LibraryRecommendedComponent } from './library-recommended/library-recommended.component';
|
||||||
|
import { CarouselModule } from '../carousel/carousel.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [LibraryDetailComponent, LibraryRecommendedComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
|
||||||
|
NgbNavModule,
|
||||||
|
|
||||||
|
CarouselModule, // because this is heavy, we might want recommended in a new url
|
||||||
|
|
||||||
|
PipeModule,
|
||||||
|
SharedSideNavCardsModule,
|
||||||
|
|
||||||
|
LibraryDetailRoutingModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LibraryDetailModule { }
|
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
<ng-container *ngIf="onDeck$ | async as onDeck">
|
||||||
|
<app-carousel-reel [items]="onDeck" title="On Deck">
|
||||||
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
|
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||||
|
</ng-template>
|
||||||
|
</app-carousel-reel>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="quickReads$ | async as quickReads">
|
||||||
|
<app-carousel-reel [items]="quickReads" title="Quick Reads">
|
||||||
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
|
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||||
|
</ng-template>
|
||||||
|
</app-carousel-reel>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="highlyRated$ | async as highlyRated">
|
||||||
|
<app-carousel-reel [items]="highlyRated" title="Highly Rated">
|
||||||
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
|
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||||
|
</ng-template>
|
||||||
|
</app-carousel-reel>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="rediscover$ | async as rediscover">
|
||||||
|
<app-carousel-reel [items]="rediscover" title="Rediscover">
|
||||||
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
|
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||||
|
</ng-template>
|
||||||
|
</app-carousel-reel>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="genre$ | async as genre">
|
||||||
|
<ng-container *ngIf="moreIn$ | async as moreIn">
|
||||||
|
<app-carousel-reel [items]="moreIn" title="More In {{genre.title}}">
|
||||||
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
|
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||||
|
</ng-template>
|
||||||
|
</app-carousel-reel>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
@ -0,0 +1,65 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { map, Observable, shareReplay } from 'rxjs';
|
||||||
|
import { Genre } from 'src/app/_models/genre';
|
||||||
|
import { Series } from 'src/app/_models/series';
|
||||||
|
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||||
|
import { RecommendationService } from 'src/app/_services/recommendation.service';
|
||||||
|
import { SeriesService } from 'src/app/_services/series.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-library-recommended',
|
||||||
|
templateUrl: './library-recommended.component.html',
|
||||||
|
styleUrls: ['./library-recommended.component.scss']
|
||||||
|
})
|
||||||
|
export class LibraryRecommendedComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() libraryId: number = 0;
|
||||||
|
|
||||||
|
quickReads$!: Observable<Series[]>;
|
||||||
|
highlyRated$!: Observable<Series[]>;
|
||||||
|
onDeck$!: Observable<Series[]>;
|
||||||
|
rediscover$!: Observable<Series[]>;
|
||||||
|
|
||||||
|
moreIn$!: Observable<Series[]>;
|
||||||
|
genre: string = '';
|
||||||
|
genre$!: Observable<Genre>;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(private recommendationService: RecommendationService, private seriesService: SeriesService, private metadataService: MetadataService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
|
||||||
|
this.quickReads$ = this.recommendationService.getQuickReads(this.libraryId)
|
||||||
|
.pipe(map(p => p.result), shareReplay());
|
||||||
|
|
||||||
|
this.highlyRated$ = this.recommendationService.getHighlyRated(this.libraryId)
|
||||||
|
.pipe(map(p => p.result), shareReplay());
|
||||||
|
|
||||||
|
this.rediscover$ = this.recommendationService.getRediscover(this.libraryId)
|
||||||
|
.pipe(map(p => p.result), shareReplay());
|
||||||
|
|
||||||
|
this.onDeck$ = this.seriesService.getOnDeck(this.libraryId)
|
||||||
|
.pipe(map(p => p.result), shareReplay());
|
||||||
|
|
||||||
|
this.genre$ = this.metadataService.getAllGenres([this.libraryId]).pipe(map(genres => genres[Math.floor(Math.random() * genres.length)]), shareReplay());
|
||||||
|
this.genre$.subscribe(genre => {
|
||||||
|
this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id).pipe(map(p => p.result), shareReplay());
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
reloadInProgress(series: Series | boolean) {
|
||||||
|
if (series === true || series === false) {
|
||||||
|
if (!series) {return;}
|
||||||
|
}
|
||||||
|
// If the update to Series doesn't affect the requirement to be in this stream, then ignore update request
|
||||||
|
const seriesObj = (series as Series);
|
||||||
|
if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//this.loadOnDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,29 +0,0 @@
|
|||||||
<ng-container *ngIf="libraries.length === 0 && !isLoading">
|
|
||||||
<div class="mt-3">
|
|
||||||
<div *ngIf="isAdmin" class="d-flex justify-content-center">
|
|
||||||
<p>There are no libraries setup yet. Configure some in <a routerLink="/admin/dashboard" fragment="libraries">Server settings</a>.</p>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="!isAdmin" class="d-flex justify-content-center">
|
|
||||||
<p>You haven't been granted access to any libraries.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
|
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
|
||||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
|
||||||
</ng-template>
|
|
||||||
</app-carousel-reel>
|
|
||||||
|
|
||||||
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series" (sectionClick)="handleSectionClick($event)">
|
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
|
||||||
<app-card-item [entity]="item" [title]="item.seriesName" [suppressLibraryLink]="libraryId !== 0" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
|
||||||
[supressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
|
|
||||||
</ng-template>
|
|
||||||
</app-carousel-reel>
|
|
||||||
|
|
||||||
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series" (sectionClick)="handleSectionClick($event)">
|
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
|
||||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
|
|
||||||
</ng-template>
|
|
||||||
</app-carousel-reel>
|
|
@ -1,180 +0,0 @@
|
|||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
|
||||||
import { Title } from '@angular/platform-browser';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { ReplaySubject, Subject } from 'rxjs';
|
|
||||||
import { debounceTime, filter, take, takeUntil } from 'rxjs/operators';
|
|
||||||
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
|
|
||||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
|
||||||
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
|
|
||||||
import { Library } from '../_models/library';
|
|
||||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
|
||||||
import { Series } from '../_models/series';
|
|
||||||
import { SortField } from '../_models/series-filter';
|
|
||||||
import { SeriesGroup } from '../_models/series-group';
|
|
||||||
import { User } from '../_models/user';
|
|
||||||
import { AccountService } from '../_services/account.service';
|
|
||||||
import { ImageService } from '../_services/image.service';
|
|
||||||
import { LibraryService } from '../_services/library.service';
|
|
||||||
import { EVENTS, MessageHubService } from '../_services/message-hub.service';
|
|
||||||
import { SeriesService } from '../_services/series.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-library',
|
|
||||||
templateUrl: './library.component.html',
|
|
||||||
styleUrls: ['./library.component.scss']
|
|
||||||
})
|
|
||||||
export class LibraryComponent implements OnInit, OnDestroy {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* By default, 0, but if non-zero, will limit all API calls to library id
|
|
||||||
*/
|
|
||||||
@Input() libraryId: number = 0;
|
|
||||||
|
|
||||||
user: User | undefined;
|
|
||||||
libraries: Library[] = [];
|
|
||||||
isLoading = false;
|
|
||||||
isAdmin = false;
|
|
||||||
|
|
||||||
recentlyUpdatedSeries: SeriesGroup[] = [];
|
|
||||||
inProgress: Series[] = [];
|
|
||||||
recentlyAddedSeries: Series[] = [];
|
|
||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We use this Replay subject to slow the amount of times we reload the UI
|
|
||||||
*/
|
|
||||||
private loadRecentlyAdded$: ReplaySubject<void> = new ReplaySubject<void>();
|
|
||||||
|
|
||||||
constructor(public accountService: AccountService, private libraryService: LibraryService,
|
|
||||||
private seriesService: SeriesService, private router: Router,
|
|
||||||
private titleService: Title, public imageService: ImageService,
|
|
||||||
private messageHub: MessageHubService) {
|
|
||||||
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
|
|
||||||
if (res.event === EVENTS.SeriesAdded) {
|
|
||||||
const seriesAddedEvent = res.payload as SeriesAddedEvent;
|
|
||||||
|
|
||||||
this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
|
|
||||||
this.recentlyAddedSeries.unshift(series);
|
|
||||||
});
|
|
||||||
} else if (res.event === EVENTS.SeriesRemoved) {
|
|
||||||
const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
|
|
||||||
|
|
||||||
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
|
|
||||||
this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
|
|
||||||
this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
|
||||||
} else if (res.event === EVENTS.ScanSeries) {
|
|
||||||
// We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time.
|
|
||||||
this.loadRecentlyAdded$.next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadRecentlyAdded$.pipe(debounceTime(1000), takeUntil(this.onDestroy)).subscribe(() => this.loadRecentlyAdded());
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.titleService.setTitle('Kavita - Dashboard');
|
|
||||||
this.isLoading = true;
|
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
|
||||||
this.user = user;
|
|
||||||
if (this.user) {
|
|
||||||
this.isAdmin = this.accountService.hasAdminRole(this.user);
|
|
||||||
this.libraryService.getLibrariesForMember().pipe(take(1)).subscribe(libraries => {
|
|
||||||
this.libraries = libraries;
|
|
||||||
this.isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reloadSeries();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.onDestroy.next();
|
|
||||||
this.onDestroy.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadSeries() {
|
|
||||||
this.loadOnDeck();
|
|
||||||
this.loadRecentlyAdded();
|
|
||||||
this.loadRecentlyAddedSeries();
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadInProgress(series: Series | boolean) {
|
|
||||||
if (series === true || series === false) {
|
|
||||||
if (!series) {return;}
|
|
||||||
}
|
|
||||||
// If the update to Series doesn't affect the requirement to be in this stream, then ignore update request
|
|
||||||
const seriesObj = (series as Series);
|
|
||||||
if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadOnDeck();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadOnDeck() {
|
|
||||||
let api = this.seriesService.getOnDeck();
|
|
||||||
if (this.libraryId > 0) {
|
|
||||||
api = this.seriesService.getOnDeck(this.libraryId);
|
|
||||||
}
|
|
||||||
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
|
||||||
this.inProgress = updatedSeries.result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadRecentlyAddedSeries() {
|
|
||||||
let api = this.seriesService.getRecentlyAdded();
|
|
||||||
if (this.libraryId > 0) {
|
|
||||||
api = this.seriesService.getRecentlyAdded(this.libraryId);
|
|
||||||
}
|
|
||||||
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
|
||||||
this.recentlyAddedSeries = updatedSeries.result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
loadRecentlyAdded() {
|
|
||||||
let api = this.seriesService.getRecentlyUpdatedSeries();
|
|
||||||
if (this.libraryId > 0) {
|
|
||||||
api = this.seriesService.getRecentlyUpdatedSeries();
|
|
||||||
}
|
|
||||||
api.pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
|
||||||
this.recentlyUpdatedSeries = updatedSeries.filter(group => {
|
|
||||||
if (this.libraryId === 0) return true;
|
|
||||||
return group.libraryId === this.libraryId;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRecentlyAddedChapterClick(item: RecentlyAddedItem) {
|
|
||||||
this.router.navigate(['library', item.libraryId, 'series', item.seriesId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSectionClick(sectionTitle: string) {
|
|
||||||
if (sectionTitle.toLowerCase() === 'recently updated series') {
|
|
||||||
const params: any = {};
|
|
||||||
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
|
|
||||||
params[FilterQueryParam.Page] = 1;
|
|
||||||
this.router.navigate(['all-series'], {queryParams: params});
|
|
||||||
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
|
||||||
const params: any = {};
|
|
||||||
params[FilterQueryParam.ReadStatus] = 'true,false,false';
|
|
||||||
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
|
|
||||||
params[FilterQueryParam.Page] = 1;
|
|
||||||
this.router.navigate(['all-series'], {queryParams: params});
|
|
||||||
}else if (sectionTitle.toLowerCase() === 'newly added series') {
|
|
||||||
const params: any = {};
|
|
||||||
params[FilterQueryParam.SortBy] = SortField.Created + ',false'; // sort by created, desc
|
|
||||||
params[FilterQueryParam.Page] = 1;
|
|
||||||
this.router.navigate(['all-series'], {queryParams: params});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromArray(arr: Array<any>, element: any) {
|
|
||||||
const index = arr.indexOf(element);
|
|
||||||
if (index >= 0) {
|
|
||||||
arr.splice(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,8 @@
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
|
import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
|
||||||
import { BehaviorSubject, fromEvent, merge, ReplaySubject, Subject } from 'rxjs';
|
import { BehaviorSubject, fromEvent, ReplaySubject, Subject } from 'rxjs';
|
||||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||||
import { ScrollService } from 'src/app/scroll.service';
|
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||||
import { ReaderService } from '../../_services/reader.service';
|
import { ReaderService } from '../../_services/reader.service';
|
||||||
import { PAGING_DIRECTION } from '../_models/reader-enums';
|
import { PAGING_DIRECTION } from '../_models/reader-enums';
|
||||||
import { WebtoonImage } from '../_models/webtoon-image';
|
import { WebtoonImage } from '../_models/webtoon-image';
|
||||||
|
@ -54,10 +54,6 @@ img {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// canvas {
|
|
||||||
// //position: absolute; // JOE: Not sure why we have this, but it breaks the renderer
|
|
||||||
// }
|
|
||||||
|
|
||||||
.reader {
|
.reader {
|
||||||
background-color: var(--manga-reader-bg-color);
|
background-color: var(--manga-reader-bg-color);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -2,16 +2,15 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { BehaviorSubject, Subject } from 'rxjs';
|
import { BehaviorSubject, Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component';
|
import { ConfirmConfig } from 'src/app/shared/confirm-dialog/_models/confirm-config';
|
||||||
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||||
import { UpdateVersionEvent } from '../_models/events/update-version-event';
|
import { UpdateNotificationModalComponent } from 'src/app/shared/update-notification/update-notification-modal.component';
|
||||||
import { User } from '../_models/user';
|
import { ErrorEvent } from 'src/app/_models/events/error-event';
|
||||||
import { AccountService } from '../_services/account.service';
|
import { NotificationProgressEvent } from 'src/app/_models/events/notification-progress-event';
|
||||||
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
|
import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event';
|
||||||
import { ErrorEvent } from '../_models/events/error-event';
|
import { User } from 'src/app/_models/user';
|
||||||
import { ConfirmService } from '../shared/confirm.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
|
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||||
import { ServerService } from '../_services/server.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nav-events-toggle',
|
selector: 'app-nav-events-toggle',
|
@ -1,10 +1,9 @@
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||||
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewChild } from '@angular/core';
|
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { BehaviorSubject, Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
import { KEY_CODES } from '../../shared/_services/utility.service';
|
||||||
import { SearchResultGroup } from '../_models/search/search-result-group';
|
import { SearchResultGroup } from '../../_models/search/search-result-group';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-grouped-typeahead',
|
selector: 'app-grouped-typeahead',
|
@ -2,7 +2,7 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
||||||
<a class="side-nav-toggle" *ngIf="navService?.sideNavVisibility$ | async" (click)="hideSideNav()"><i class="fas fa-bars"></i></a>
|
<a class="side-nav-toggle" *ngIf="navService?.sideNavVisibility$ | async" (click)="hideSideNav()"><i class="fas fa-bars"></i></a>
|
||||||
<a class="navbar-brand dark-exempt" routerLink="/library" routerLinkActive="active"><img class="logo" src="../../assets/images/logo.png" alt="kavita icon" aria-hidden="true"/><span class="d-none d-md-inline"> Kavita</span></a>
|
<a class="navbar-brand dark-exempt" routerLink="/libraries" routerLinkActive="active"><img class="logo" src="../../assets/images/logo.png" alt="kavita icon" aria-hidden="true"/><span class="d-none d-md-inline"> Kavita</span></a>
|
||||||
<ul class="navbar-nav col me-auto">
|
<ul class="navbar-nav col me-auto">
|
||||||
|
|
||||||
<div class="nav-item" *ngIf="(accountService.currentUser$ | async) as user">
|
<div class="nav-item" *ngIf="(accountService.currentUser$ | async) as user">
|
@ -1,20 +1,20 @@
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { fromEvent, Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
import { ScrollService } from '../scroll.service';
|
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||||
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
|
import { FilterQueryParam } from '../../shared/_services/filter-utilities.service';
|
||||||
import { CollectionTag } from '../_models/collection-tag';
|
import { CollectionTag } from '../../_models/collection-tag';
|
||||||
import { Library } from '../_models/library';
|
import { Library } from '../../_models/library';
|
||||||
import { PersonRole } from '../_models/person';
|
import { PersonRole } from '../../_models/person';
|
||||||
import { ReadingList } from '../_models/reading-list';
|
import { ReadingList } from '../../_models/reading-list';
|
||||||
import { SearchResult } from '../_models/search-result';
|
import { SearchResult } from '../../_models/search-result';
|
||||||
import { SearchResultGroup } from '../_models/search/search-result-group';
|
import { SearchResultGroup } from '../../_models/search/search-result-group';
|
||||||
import { AccountService } from '../_services/account.service';
|
import { AccountService } from '../../_services/account.service';
|
||||||
import { ImageService } from '../_services/image.service';
|
import { ImageService } from '../../_services/image.service';
|
||||||
import { LibraryService } from '../_services/library.service';
|
import { LibraryService } from '../../_services/library.service';
|
||||||
import { NavService } from '../_services/nav.service';
|
import { NavService } from '../../_services/nav.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nav-header',
|
selector: 'app-nav-header',
|
37
UI/Web/src/app/nav/nav.module.ts
Normal file
37
UI/Web/src/app/nav/nav.module.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { EventsWidgetComponent } from './events-widget/events-widget.component';
|
||||||
|
import { GroupedTypeaheadComponent } from './grouped-typeahead/grouped-typeahead.component';
|
||||||
|
import { NavHeaderComponent } from './nav-header/nav-header.component';
|
||||||
|
import { NgbDropdownModule, NgbPopoverModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
|
import { TypeaheadModule } from '../typeahead/typeahead.module';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
NavHeaderComponent,
|
||||||
|
EventsWidgetComponent,
|
||||||
|
GroupedTypeaheadComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgbPopoverModule,
|
||||||
|
NgbNavModule,
|
||||||
|
|
||||||
|
SharedModule, // app image, series-format
|
||||||
|
PipeModule,
|
||||||
|
TypeaheadModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
NavHeaderComponent,
|
||||||
|
SharedModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class NavModule { }
|
@ -2,17 +2,16 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { DragableOrderedListComponent } from './dragable-ordered-list/dragable-ordered-list.component';
|
import { DragableOrderedListComponent } from './dragable-ordered-list/dragable-ordered-list.component';
|
||||||
import { ReadingListDetailComponent } from './reading-list-detail/reading-list-detail.component';
|
import { ReadingListDetailComponent } from './reading-list-detail/reading-list-detail.component';
|
||||||
import { ReadingListRoutingModule } from './reading-list.router.module';
|
import { ReadingListRoutingModule } from './reading-list-routing.module';
|
||||||
import {DragDropModule} from '@angular/cdk/drag-drop';
|
import {DragDropModule} from '@angular/cdk/drag-drop';
|
||||||
import { AddToListModalComponent } from './_modals/add-to-list-modal/add-to-list-modal.component';
|
import { AddToListModalComponent } from './_modals/add-to-list-modal/add-to-list-modal.component';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { CardsModule } from '../cards/cards.module';
|
|
||||||
import { ReadingListsComponent } from './reading-lists/reading-lists.component';
|
import { ReadingListsComponent } from './reading-lists/reading-lists.component';
|
||||||
import { EditReadingListModalComponent } from './_modals/edit-reading-list-modal/edit-reading-list-modal.component';
|
import { EditReadingListModalComponent } from './_modals/edit-reading-list-modal/edit-reading-list-modal.component';
|
||||||
import { PipeModule } from '../pipe/pipe.module';
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { SidenavModule } from '../sidenav/sidenav.module';
|
|
||||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -26,14 +25,15 @@ import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ReadingListRoutingModule,
|
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
CardsModule,
|
NgbNavModule,
|
||||||
|
|
||||||
PipeModule,
|
PipeModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
SidenavModule,
|
SharedSideNavCardsModule,
|
||||||
NgbNavModule
|
|
||||||
|
ReadingListRoutingModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AddToListModalComponent,
|
AddToListModalComponent,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ThemeService } from 'src/app/theme.service';
|
import { ThemeService } from 'src/app/_services/theme.service';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
import { NavService } from 'src/app/_services/nav.service';
|
import { NavService } from 'src/app/_services/nav.service';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ThemeService } from 'src/app/theme.service';
|
import { ThemeService } from 'src/app/_services/theme.service';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ConfirmEmailComponent } from './confirm-email/confirm-email.component';
|
import { ConfirmEmailComponent } from './confirm-email/confirm-email.component';
|
||||||
import { RegistrationRoutingModule } from './registration.router.module';
|
import { RegistrationRoutingModule } from './registration.router.module';
|
||||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbCollapseModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { SplashContainerComponent } from './splash-container/splash-container.component';
|
import { SplashContainerComponent } from './splash-container/splash-container.component';
|
||||||
import { RegisterComponent } from './register/register.component';
|
import { RegisterComponent } from './register/register.component';
|
||||||
@ -10,6 +10,7 @@ import { AddEmailToAccountMigrationModalComponent } from './add-email-to-account
|
|||||||
import { ConfirmMigrationEmailComponent } from './confirm-migration-email/confirm-migration-email.component';
|
import { ConfirmMigrationEmailComponent } from './confirm-migration-email/confirm-migration-email.component';
|
||||||
import { ResetPasswordComponent } from './reset-password/reset-password.component';
|
import { ResetPasswordComponent } from './reset-password/reset-password.component';
|
||||||
import { ConfirmResetPasswordComponent } from './confirm-reset-password/confirm-reset-password.component';
|
import { ConfirmResetPasswordComponent } from './confirm-reset-password/confirm-reset-password.component';
|
||||||
|
import { UserLoginComponent } from './user-login/user-login.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +22,8 @@ import { ConfirmResetPasswordComponent } from './confirm-reset-password/confirm-
|
|||||||
AddEmailToAccountMigrationModalComponent,
|
AddEmailToAccountMigrationModalComponent,
|
||||||
ConfirmMigrationEmailComponent,
|
ConfirmMigrationEmailComponent,
|
||||||
ResetPasswordComponent,
|
ResetPasswordComponent,
|
||||||
ConfirmResetPasswordComponent
|
ConfirmResetPasswordComponent,
|
||||||
|
UserLoginComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -5,8 +5,17 @@ import { ConfirmMigrationEmailComponent } from './confirm-migration-email/confir
|
|||||||
import { ConfirmResetPasswordComponent } from './confirm-reset-password/confirm-reset-password.component';
|
import { ConfirmResetPasswordComponent } from './confirm-reset-password/confirm-reset-password.component';
|
||||||
import { RegisterComponent } from './register/register.component';
|
import { RegisterComponent } from './register/register.component';
|
||||||
import { ResetPasswordComponent } from './reset-password/reset-password.component';
|
import { ResetPasswordComponent } from './reset-password/reset-password.component';
|
||||||
|
import { UserLoginComponent } from './user-login/user-login.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: UserLoginComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
component: UserLoginComponent
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'confirm-email',
|
path: 'confirm-email',
|
||||||
component: ConfirmEmailComponent,
|
component: ConfirmEmailComponent,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { MemberService } from '../_services/member.service';
|
import { MemberService } from '../../_services/member.service';
|
||||||
import { UserLoginComponent } from './user-login.component';
|
import { UserLoginComponent } from './user-login.component';
|
||||||
|
|
||||||
xdescribe('UserLoginComponent', () => {
|
xdescribe('UserLoginComponent', () => {
|
@ -4,14 +4,14 @@ import { Router } from '@angular/router';
|
|||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { SettingsService } from '../admin/settings.service';
|
import { SettingsService } from '../../admin/settings.service';
|
||||||
import { AddEmailToAccountMigrationModalComponent } from '../registration/add-email-to-account-migration-modal/add-email-to-account-migration-modal.component';
|
import { AddEmailToAccountMigrationModalComponent } from '../add-email-to-account-migration-modal/add-email-to-account-migration-modal.component';
|
||||||
import { User } from '../_models/user';
|
import { User } from '../../_models/user';
|
||||||
import { AccountService } from '../_services/account.service';
|
import { AccountService } from '../../_services/account.service';
|
||||||
import { MemberService } from '../_services/member.service';
|
import { MemberService } from '../../_services/member.service';
|
||||||
import { NavService } from '../_services/nav.service';
|
import { NavService } from '../../_services/nav.service';
|
||||||
|
|
||||||
|
|
||||||
// TODO: Move this into registration module
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-login',
|
selector: 'app-user-login',
|
||||||
templateUrl: './user-login.component.html',
|
templateUrl: './user-login.component.html',
|
||||||
@ -48,7 +48,7 @@ export class UserLoginComponent implements OnInit {
|
|||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
this.navService.showSideNav();
|
this.navService.showSideNav();
|
||||||
this.router.navigateByUrl('/library');
|
this.router.navigateByUrl('/libraries');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ export class UserLoginComponent implements OnInit {
|
|||||||
localStorage.setItem('kavita--auth-intersection-url', '');
|
localStorage.setItem('kavita--auth-intersection-url', '');
|
||||||
this.router.navigateByUrl(pageResume);
|
this.router.navigateByUrl(pageResume);
|
||||||
} else {
|
} else {
|
||||||
this.router.navigateByUrl('/library');
|
this.router.navigateByUrl('/libraries');
|
||||||
}
|
}
|
||||||
}, err => {
|
}, err => {
|
||||||
if (err.error === 'You are missing an email on your account. Please wait while we migrate your account.') {
|
if (err.error === 'You are missing an email on your account. Please wait while we migrate your account.') {
|
17
UI/Web/src/app/series-detail/series-detail-routing.module.ts
Normal file
17
UI/Web/src/app/series-detail/series-detail-routing.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
import { SeriesDetailComponent } from './series-detail.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: SeriesDetailComponent
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes), ],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class SeriesDetailRoutingModule { }
|
@ -13,7 +13,7 @@ import { ConfirmService } from '../shared/confirm.service';
|
|||||||
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
|
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
|
||||||
import { DownloadService } from '../shared/_services/download.service';
|
import { DownloadService } from '../shared/_services/download.service';
|
||||||
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||||
import { ReviewSeriesModalComponent } from '../_modals/review-series-modal/review-series-modal.component';
|
import { ReviewSeriesModalComponent } from './review-series-modal/review-series-modal.component';
|
||||||
import { Chapter } from '../_models/chapter';
|
import { Chapter } from '../_models/chapter';
|
||||||
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
|
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
|
||||||
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
|
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
|
||||||
@ -33,8 +33,8 @@ import { ReaderService } from '../_services/reader.service';
|
|||||||
import { ReadingListService } from '../_services/reading-list.service';
|
import { ReadingListService } from '../_services/reading-list.service';
|
||||||
import { SeriesService } from '../_services/series.service';
|
import { SeriesService } from '../_services/series.service';
|
||||||
import { NavService } from '../_services/nav.service';
|
import { NavService } from '../_services/nav.service';
|
||||||
import { RelationKind } from '../_models/series-detail/relation-kind';
|
|
||||||
import { RelatedSeries } from '../_models/series-detail/related-series';
|
import { RelatedSeries } from '../_models/series-detail/related-series';
|
||||||
|
import { RelationKind } from '../_models/series-detail/relation-kind';
|
||||||
|
|
||||||
interface RelatedSeris {
|
interface RelatedSeris {
|
||||||
series: Series;
|
series: Series;
|
||||||
|
38
UI/Web/src/app/series-detail/series-detail.module.ts
Normal file
38
UI/Web/src/app/series-detail/series-detail.module.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SeriesDetailRoutingModule } from './series-detail-routing.module';
|
||||||
|
import { NgbCollapseModule, NgbNavModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { SeriesDetailComponent } from './series-detail.component';
|
||||||
|
import { SeriesMetadataDetailComponent } from './series-metadata-detail/series-metadata-detail.component';
|
||||||
|
import { ReviewSeriesModalComponent } from './review-series-modal/review-series-modal.component';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { TypeaheadModule } from '../typeahead/typeahead.module';
|
||||||
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
SeriesDetailComponent,
|
||||||
|
ReviewSeriesModalComponent,
|
||||||
|
SeriesMetadataDetailComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule, // Review Series Modal
|
||||||
|
|
||||||
|
NgbCollapseModule, // Series Metadata
|
||||||
|
NgbNavModule,
|
||||||
|
NgbRatingModule,
|
||||||
|
|
||||||
|
TypeaheadModule,
|
||||||
|
PipeModule,
|
||||||
|
SharedModule, // person badge, badge expander (these 2 can be their own module)
|
||||||
|
SharedSideNavCardsModule,
|
||||||
|
|
||||||
|
SeriesDetailRoutingModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SeriesDetailModule { }
|
@ -1,13 +1,13 @@
|
|||||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
|
import { TagBadgeCursor } from '../../shared/tag-badge/tag-badge.component';
|
||||||
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
|
import { FilterQueryParam } from '../../shared/_services/filter-utilities.service';
|
||||||
import { UtilityService } from '../shared/_services/utility.service';
|
import { UtilityService } from '../../shared/_services/utility.service';
|
||||||
import { MangaFormat } from '../_models/manga-format';
|
import { MangaFormat } from '../../_models/manga-format';
|
||||||
import { ReadingList } from '../_models/reading-list';
|
import { ReadingList } from '../../_models/reading-list';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../../_models/series';
|
||||||
import { SeriesMetadata } from '../_models/series-metadata';
|
import { SeriesMetadata } from '../../_models/series-metadata';
|
||||||
import { MetadataService } from '../_services/metadata.service';
|
import { MetadataService } from '../../_services/metadata.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-series-metadata-detail',
|
selector: 'app-series-metadata-detail',
|
@ -0,0 +1,23 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CardsModule } from '../cards/cards.module';
|
||||||
|
import { SidenavModule } from '../sidenav/sidenav.module';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports SideNavModule and CardsModule
|
||||||
|
*/
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
|
||||||
|
CardsModule,
|
||||||
|
SidenavModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CardsModule,
|
||||||
|
SidenavModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SharedSideNavCardsModule { }
|
@ -1,8 +1,10 @@
|
|||||||
|
import { HttpParams } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { Chapter } from 'src/app/_models/chapter';
|
import { Chapter } from 'src/app/_models/chapter';
|
||||||
import { LibraryType } from 'src/app/_models/library';
|
import { LibraryType } from 'src/app/_models/library';
|
||||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||||
|
import { PaginatedResult } from 'src/app/_models/pagination';
|
||||||
import { Series } from 'src/app/_models/series';
|
import { Series } from 'src/app/_models/series';
|
||||||
import { SeriesFilter, SortField } from 'src/app/_models/series-filter';
|
import { SeriesFilter, SortField } from 'src/app/_models/series-filter';
|
||||||
import { Volume } from 'src/app/_models/volume';
|
import { Volume } from 'src/app/_models/volume';
|
||||||
@ -201,4 +203,30 @@ export class UtilityService {
|
|||||||
private isObject(object: any) {
|
private isObject(object: any) {
|
||||||
return object != null && typeof object === 'object';
|
return object != null && typeof object === 'object';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addPaginationIfExists(params: HttpParams, pageNum?: number, itemsPerPage?: number) {
|
||||||
|
if (pageNum !== null && pageNum !== undefined && itemsPerPage !== null && itemsPerPage !== undefined) {
|
||||||
|
params = params.append('pageNumber', pageNum + '');
|
||||||
|
params = params.append('pageSize', itemsPerPage + '');
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPaginatedResult(response: any, paginatedVariable: PaginatedResult<any[]> | undefined = undefined) {
|
||||||
|
if (paginatedVariable === undefined) {
|
||||||
|
paginatedVariable = new PaginatedResult();
|
||||||
|
}
|
||||||
|
if (response.body === null) {
|
||||||
|
paginatedVariable.result = [];
|
||||||
|
} else {
|
||||||
|
paginatedVariable.result = response.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageHeader = response.headers?.get('Pagination');
|
||||||
|
if (pageHeader !== null) {
|
||||||
|
paginatedVariable.pagination = JSON.parse(pageHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
return paginatedVariable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,21 +38,24 @@ import { PipeModule } from '../pipe/pipe.module';
|
|||||||
RouterModule,
|
RouterModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
NgbTooltipModule, // RegisterMemberComponent
|
NgbTooltipModule, // TODO: Validate if we still need this
|
||||||
PipeModule,
|
PipeModule,
|
||||||
NgCircleProgressModule.forRoot(),
|
NgCircleProgressModule.forRoot(),
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ReadMoreComponent, // Used globably
|
ReadMoreComponent, // Used globably
|
||||||
DrawerComponent, // Can be replaced with boostrap offscreen canvas (v5)
|
DrawerComponent, // Can be replaced with boostrap offscreen canvas (v5) (also used in book reader and series metadata filter)
|
||||||
ShowIfScrollbarDirective, // Used book reader only?
|
|
||||||
A11yClickDirective, // Used globally
|
A11yClickDirective, // Used globally
|
||||||
SeriesFormatComponent, // Used globally
|
SeriesFormatComponent, // Used globally
|
||||||
TagBadgeComponent, // Used globally
|
TagBadgeComponent, // Used globally
|
||||||
CircularLoaderComponent, // Used in Cards only
|
CircularLoaderComponent, // Used in Cards only
|
||||||
|
ImageComponent, // Used globally
|
||||||
|
|
||||||
|
ShowIfScrollbarDirective, // Used book reader only?
|
||||||
|
|
||||||
PersonBadgeComponent, // Used Series Detail
|
PersonBadgeComponent, // Used Series Detail
|
||||||
BadgeExpanderComponent, // Used globally
|
BadgeExpanderComponent, // Used Series Detail/Metadata
|
||||||
ImageComponent // Used globally
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SharedModule { }
|
export class SharedModule { }
|
||||||
|
@ -107,46 +107,46 @@ a {
|
|||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
.side-nav-item {
|
.side-nav-item {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
//display: flex;
|
||||||
justify-content: space-between;
|
//justify-content: space-between;
|
||||||
padding: 15px 10px;
|
padding: 15px 10px;
|
||||||
width: 100%;
|
//width: 100%;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
min-height: 40px;
|
//min-height: 40px;
|
||||||
overflow: hidden;
|
// overflow: hidden;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
||||||
cursor: pointer;
|
//cursor: pointer;
|
||||||
|
|
||||||
.side-nav-text {
|
.side-nav-text {
|
||||||
padding-left: 10px;
|
// padding-left: 10px;
|
||||||
opacity: 1;
|
// opacity: 1;
|
||||||
min-width: 100px;
|
// min-width: 100px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
div {
|
// div {
|
||||||
min-width: 102px;
|
// min-width: 102px;
|
||||||
width: 100%
|
// width: 100%
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&.closed {
|
&.closed {
|
||||||
.side-nav-text {
|
// .side-nav-text {
|
||||||
opacity: 0;
|
// opacity: 0;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.card-actions {
|
.card-actions {
|
||||||
opacity: 0;
|
//opacity: 0;
|
||||||
font-size: inherit
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
// span {
|
||||||
&:last-child {
|
// &:last-child {
|
||||||
flex-grow: 1;
|
// flex-grow: 1;
|
||||||
justify-content: end;
|
// justify-content: end;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,7 +7,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</app-side-nav-item> -->
|
</app-side-nav-item> -->
|
||||||
|
|
||||||
<app-side-nav-item icon="fa-home" title="Home" link="/library/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-home" title="Home" link="/libraries/"></app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-list" title="Collections" link="/collections/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-list" title="Collections" link="/collections/"></app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-list-ol" title="Reading Lists" link="/lists/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-list-ol" title="Reading Lists" link="/lists/"></app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-bookmark" title="Bookmarks" link="/bookmarks/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-bookmark" title="Bookmarks" link="/bookmarks/"></app-side-nav-item>
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
border: var(--side-nav-border-closed);
|
border: var(--side-nav-border-closed);
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-nav-item:first() {
|
.side-nav-item:first {
|
||||||
border-top-left-radius: var(--side-nav-border-radius);
|
border-top-left-radius: var(--side-nav-border-radius);
|
||||||
border-top-right-radius: var(--side-nav-border-radius);
|
border-top-right-radius: var(--side-nav-border-radius);
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-nav-item:first() {
|
.side-nav-item:first {
|
||||||
border-top-left-radius: var(--side-nav-border-radius);
|
border-top-left-radius: var(--side-nav-border-radius);
|
||||||
border-top-right-radius: var(--side-nav-border-radius);
|
border-top-right-radius: var(--side-nav-border-radius);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { distinctUntilChanged, Subject, take, takeUntil } from 'rxjs';
|
import { distinctUntilChanged, Subject, take, takeUntil } from 'rxjs';
|
||||||
import { ThemeService } from 'src/app/theme.service';
|
import { ThemeService } from 'src/app/_services/theme.service';
|
||||||
import { SiteTheme, ThemeProvider } from 'src/app/_models/preferences/site-theme';
|
import { SiteTheme, ThemeProvider } from 'src/app/_models/preferences/site-theme';
|
||||||
import { User } from 'src/app/_models/user';
|
import { User } from 'src/app/_models/user';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
|
@ -9,8 +9,8 @@ import { ApiKeyComponent } from './api-key/api-key.component';
|
|||||||
import { PipeModule } from '../pipe/pipe.module';
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
import { SiteThemeProviderPipe } from './_pipes/site-theme-provider.pipe';
|
import { SiteThemeProviderPipe } from './_pipes/site-theme-provider.pipe';
|
||||||
import { ThemeManagerComponent } from './theme-manager/theme-manager.component';
|
import { ThemeManagerComponent } from './theme-manager/theme-manager.component';
|
||||||
import { SidenavModule } from '../sidenav/sidenav.module';
|
|
||||||
import { ColorPickerModule } from 'ngx-color-picker';
|
import { ColorPickerModule } from 'ngx-color-picker';
|
||||||
|
import { SidenavModule } from '../sidenav/sidenav.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -24,14 +24,18 @@ import { ColorPickerModule } from 'ngx-color-picker';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
|
||||||
NgbAccordionModule,
|
NgbAccordionModule,
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
|
|
||||||
NgxSliderModule,
|
NgxSliderModule,
|
||||||
UserSettingsRoutingModule,
|
ColorPickerModule, // User prefernces background color
|
||||||
|
|
||||||
PipeModule,
|
PipeModule,
|
||||||
SidenavModule,
|
SidenavModule,
|
||||||
ColorPickerModule, // User prefernces background color
|
|
||||||
|
UserSettingsRoutingModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
SiteThemeProviderPipe,
|
SiteThemeProviderPipe,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user