mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -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/backcover.png", false)]
|
||||
[InlineData("backcover.png", false)]
|
||||
[InlineData("back_cover.png", false)]
|
||||
public void IsCoverImageTest(string inputPath, bool expected)
|
||||
{
|
||||
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<Chunk> GetChunkInfo(int libraryId = 0);
|
||||
Task<IList<SeriesMetadata>> GetSeriesMetadataForIdsAsync(IEnumerable<int> seriesIds);
|
||||
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
||||
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
|
||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||
|
||||
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
|
||||
@ -416,7 +420,9 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Genres)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People);
|
||||
.ThenInclude(m => m.People)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Tags);
|
||||
}
|
||||
|
||||
return await query.SingleOrDefaultAsync();
|
||||
@ -972,9 +978,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
|
||||
public async Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind)
|
||||
{
|
||||
var libraryIds = _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
|
||||
var libraryIds = GetLibraryIdsForUser(userId);
|
||||
var usersSeriesIds = _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Id);
|
||||
@ -995,14 +999,100 @@ public class SeriesRepository : ISeriesRepository
|
||||
.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
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
|
||||
var usersSeriesIds = _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Id);
|
||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
|
||||
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()
|
||||
{
|
||||
@ -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)
|
||||
{
|
||||
return await _context.Series.SelectMany(s =>
|
||||
|
@ -54,7 +54,7 @@ namespace API.Parser
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex BookFileRegex = new Regex(BookFileExtensions,
|
||||
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);
|
||||
|
||||
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]",
|
||||
|
@ -285,14 +285,11 @@ public class SeriesService : ISeriesService
|
||||
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
|
||||
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
|
||||
series.Metadata.Tags.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
// Remove tag
|
||||
series.Metadata.Tags.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
|
@ -7,7 +7,7 @@ import { Preferences } from '../_models/preferences/preferences';
|
||||
import { User } from '../_models/user';
|
||||
import { Router } from '@angular/router';
|
||||
import { MessageHubService } from './message-hub.service';
|
||||
import { ThemeService } from '../theme.service';
|
||||
import { ThemeService } from './theme.service';
|
||||
import { InviteUserResponse } from '../_models/invite-user-response';
|
||||
|
||||
@Injectable({
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Inject, Injectable, OnDestroy } from '@angular/core';
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { ThemeService } from '../theme.service';
|
||||
import { ThemeService } from './theme.service';
|
||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||
import { AccountService } from './account.service';
|
||||
import { NavService } from './nav.service';
|
||||
|
||||
@Injectable({
|
||||
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 { map } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { UtilityService } from '../shared/_services/utility.service';
|
||||
import { Chapter } from '../_models/chapter';
|
||||
import { ChapterMetadata } from '../_models/chapter-metadata';
|
||||
import { CollectionTag } from '../_models/collection-tag';
|
||||
import { PaginatedResult } from '../_models/pagination';
|
||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||
import { Series } from '../_models/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 { SeriesFilter } from '../_models/series-filter';
|
||||
import { SeriesGroup } from '../_models/series-group';
|
||||
@ -27,43 +26,28 @@ export class SeriesService {
|
||||
paginatedResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>();
|
||||
paginatedSeriesForTagsResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>();
|
||||
|
||||
constructor(private httpClient: HttpClient, private imageService: ImageService) { }
|
||||
|
||||
_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;
|
||||
}
|
||||
constructor(private httpClient: HttpClient, private imageService: ImageService, private utilityService: UtilityService) { }
|
||||
|
||||
getAllSeries(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||
let params = new HttpParams();
|
||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
const data = this.createSeriesFilter(filter);
|
||||
|
||||
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all', data, {observe: 'response', params}).pipe(
|
||||
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) {
|
||||
let params = new HttpParams();
|
||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
const data = this.createSeriesFilter(filter);
|
||||
|
||||
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
|
||||
map((response: any) => {
|
||||
return this._cachePaginatedResults(response, this.paginatedResults);
|
||||
return this.utilityService.createPaginatedResult(response, this.paginatedResults);
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -123,11 +107,11 @@ export class SeriesService {
|
||||
getRecentlyAdded(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||
const data = this.createSeriesFilter(filter);
|
||||
let params = new HttpParams();
|
||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
|
||||
return this.httpClient.post<Series[]>(this.baseUrl + 'series/recently-added?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
|
||||
map(response => {
|
||||
return this._cachePaginatedResults(response, new PaginatedResult<Series[]>());
|
||||
return this.utilityService.createPaginatedResult(response, new PaginatedResult<Series[]>());
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -140,11 +124,11 @@ export class SeriesService {
|
||||
const data = this.createSeriesFilter(filter);
|
||||
|
||||
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(
|
||||
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) {
|
||||
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(
|
||||
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);
|
||||
}
|
||||
|
||||
_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) {
|
||||
if (filter !== undefined) return filter;
|
||||
|
@ -4,11 +4,10 @@ import { Inject, Injectable, OnDestroy, Renderer2, RendererFactory2, SecurityCon
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { map, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { ConfirmService } from './shared/confirm.service';
|
||||
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 { EVENTS, MessageHubService } from './_services/message-hub.service';
|
||||
import { ConfirmService } from '../shared/confirm.service';
|
||||
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
||||
import { SiteTheme, ThemeProvider } from '../_models/preferences/site-theme';
|
||||
import { EVENTS, MessageHubService } from './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 { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||
import { Library } from '../_models/library';
|
||||
import { Pagination } from '../_models/pagination';
|
||||
import { Series } from '../_models/series';
|
||||
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 { EVENTS, Message, MessageHubService } from '../_services/message-hub.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 { Routes, RouterModule } 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 { Routes, RouterModule, PreloadAllModules } from '@angular/router';
|
||||
import { AuthGuard } from './_guards/auth.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 { 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: Use Prefetching of LazyLoaded Modules
|
||||
const routes: Routes = [
|
||||
{path: '', component: UserLoginComponent},
|
||||
{
|
||||
path: 'admin',
|
||||
canActivate: [AdminGuard],
|
||||
@ -47,40 +40,48 @@ const routes: Routes = [
|
||||
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',
|
||||
canActivate: [AuthGuard, LibraryAccessGuard],
|
||||
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)
|
||||
},
|
||||
{
|
||||
path: 'library/:libraryId/series/:seriesId/book',
|
||||
path: ':libraryId/series/:seriesId/book',
|
||||
loadChildren: () => import('../app/book-reader/book-reader.module').then(m => m.BookReaderModule)
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{path: 'library', component: DashboardComponent},
|
||||
{path: 'all-series', component: AllSeriesComponent}, // TODO: This might be better as a separate module
|
||||
|
||||
]
|
||||
path: 'theme',
|
||||
loadChildren: () => import('../app/dev-only/dev-only.module').then(m => m.DevOnlyModule)
|
||||
},
|
||||
{path: 'theme', component: ThemeTestComponent},
|
||||
|
||||
{path: 'login', component: UserLoginComponent}, // TODO: move this to registration module
|
||||
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
|
||||
{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'},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled'})],
|
||||
imports: [RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled', preloadingStrategy: PreloadAllModules})],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
@ -4,85 +4,30 @@ import { NgModule } from '@angular/core';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
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 { UserLoginComponent } from './user-login/user-login.component';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
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 { 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 { NavModule } from './nav/nav.module';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
NavHeaderComponent,
|
||||
UserLoginComponent,
|
||||
LibraryComponent,
|
||||
LibraryDetailComponent,
|
||||
SeriesDetailComponent,
|
||||
ReviewSeriesModalComponent,
|
||||
DashboardComponent,
|
||||
EventsWidgetComponent,
|
||||
SeriesMetadataDetailComponent,
|
||||
AllSeriesComponent,
|
||||
GroupedTypeaheadComponent,
|
||||
ThemeTestComponent,
|
||||
|
||||
],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
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({
|
||||
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 { MemberService } from 'src/app/_services/member.service';
|
||||
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 { LibraryService } from 'src/app/_services/library.service';
|
||||
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 {
|
||||
@ -415,7 +415,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const chapterId = this.route.snapshot.paramMap.get('chapterId');
|
||||
|
||||
if (libraryId === null || seriesId === null || chapterId === null) {
|
||||
this.router.navigateByUrl('/library');
|
||||
this.router.navigateByUrl('/libraries');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
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 { 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: [
|
||||
CommonModule,
|
||||
CardsModule,
|
||||
SharedModule,
|
||||
SidenavModule,
|
||||
NgbTooltipModule,
|
||||
|
||||
SharedSideNavCardsModule,
|
||||
|
||||
BookmarkRoutingModule
|
||||
]
|
||||
|
@ -54,7 +54,6 @@ import { EditSeriesRelationComponent } from './edit-series-relation/edit-series-
|
||||
|
||||
MetadataFilterModule,
|
||||
|
||||
NgbNavModule,
|
||||
NgbTooltipModule, // Card item
|
||||
NgbCollapseModule,
|
||||
NgbRatingModule,
|
||||
|
@ -2,10 +2,9 @@ import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CollectionDetailComponent } from './collection-detail/collection-detail.component';
|
||||
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 { 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: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CardsModule,
|
||||
|
||||
SharedSideNavCardsModule,
|
||||
|
||||
CollectionsRoutingModule,
|
||||
SidenavModule
|
||||
],
|
||||
exports: [
|
||||
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-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 { 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({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
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) {
|
||||
this.titleService.setTitle('Kavita - Dashboard');
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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 { ToastrService } from 'ngx-toastr';
|
||||
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
|
||||
import { ThemeService } from '../theme.service';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
import { Person, PersonRole } from '../_models/person';
|
||||
import { Series } from '../_models/series';
|
||||
import { NavService } from '../_services/nav.service';
|
||||
import { TagBadgeCursor } from '../../shared/tag-badge/tag-badge.component';
|
||||
import { ThemeService } from '../../_services/theme.service';
|
||||
import { MangaFormat } from '../../_models/manga-format';
|
||||
import { Person, PersonRole } from '../../_models/person';
|
||||
import { Series } from '../../_models/series';
|
||||
import { NavService } from '../../_services/nav.service';
|
||||
|
||||
@Component({
|
||||
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>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngIf="tab.title === 'Recommended'">
|
||||
<app-library [libraryId]="libraryId"></app-library>
|
||||
<app-library-recommended [libraryId]="libraryId"></app-library-recommended>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.title === 'Library'">
|
||||
<app-card-detail-layout
|
||||
|
@ -2,9 +2,8 @@ import { Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angul
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
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 { FilterSettings } from '../metadata-filter/filter-settings';
|
||||
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||
import { Library } from '../_models/library';
|
||||
@ -18,6 +17,7 @@ import { EVENTS, MessageHubService } from '../_services/message-hub.service';
|
||||
import { SeriesService } from '../_services/series.service';
|
||||
import { NavService } from '../_services/nav.service';
|
||||
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||
import { FilterSettings } from '../metadata-filter/filter-settings';
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-detail',
|
||||
@ -87,8 +87,9 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||
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) {
|
||||
console.log('Redirecting due to not seeing libraryId in route');
|
||||
this.router.navigateByUrl('/libraries');
|
||||
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 { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
|
||||
import { BehaviorSubject, fromEvent, merge, ReplaySubject, Subject } from 'rxjs';
|
||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||
import { ScrollService } from 'src/app/scroll.service';
|
||||
import { BehaviorSubject, fromEvent, ReplaySubject, Subject } from 'rxjs';
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||
import { ReaderService } from '../../_services/reader.service';
|
||||
import { PAGING_DIRECTION } from '../_models/reader-enums';
|
||||
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 {
|
||||
background-color: var(--manga-reader-bg-color);
|
||||
overflow: auto;
|
||||
|
@ -2,16 +2,15 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component';
|
||||
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
||||
import { UpdateVersionEvent } from '../_models/events/update-version-event';
|
||||
import { User } from '../_models/user';
|
||||
import { AccountService } from '../_services/account.service';
|
||||
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
|
||||
import { ErrorEvent } from '../_models/events/error-event';
|
||||
import { ConfirmService } from '../shared/confirm.service';
|
||||
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
|
||||
import { ServerService } from '../_services/server.service';
|
||||
import { ConfirmConfig } from 'src/app/shared/confirm-dialog/_models/confirm-config';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { UpdateNotificationModalComponent } from 'src/app/shared/update-notification/update-notification-modal.component';
|
||||
import { ErrorEvent } from 'src/app/_models/events/error-event';
|
||||
import { NotificationProgressEvent } from 'src/app/_models/events/notification-progress-event';
|
||||
import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav-events-toggle',
|
@ -1,10 +1,9 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||
import { SearchResultGroup } from '../_models/search/search-result-group';
|
||||
import { KEY_CODES } from '../../shared/_services/utility.service';
|
||||
import { SearchResultGroup } from '../../_models/search/search-result-group';
|
||||
|
||||
@Component({
|
||||
selector: 'app-grouped-typeahead',
|
@ -2,7 +2,7 @@
|
||||
<div class="container-fluid">
|
||||
<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="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">
|
||||
|
||||
<div class="nav-item" *ngIf="(accountService.currentUser$ | async) as user">
|
@ -1,20 +1,20 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { fromEvent, Subject } from 'rxjs';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { ScrollService } from '../scroll.service';
|
||||
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
|
||||
import { CollectionTag } from '../_models/collection-tag';
|
||||
import { Library } from '../_models/library';
|
||||
import { PersonRole } from '../_models/person';
|
||||
import { ReadingList } from '../_models/reading-list';
|
||||
import { SearchResult } from '../_models/search-result';
|
||||
import { SearchResultGroup } from '../_models/search/search-result-group';
|
||||
import { AccountService } from '../_services/account.service';
|
||||
import { ImageService } from '../_services/image.service';
|
||||
import { LibraryService } from '../_services/library.service';
|
||||
import { NavService } from '../_services/nav.service';
|
||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||
import { FilterQueryParam } from '../../shared/_services/filter-utilities.service';
|
||||
import { CollectionTag } from '../../_models/collection-tag';
|
||||
import { Library } from '../../_models/library';
|
||||
import { PersonRole } from '../../_models/person';
|
||||
import { ReadingList } from '../../_models/reading-list';
|
||||
import { SearchResult } from '../../_models/search-result';
|
||||
import { SearchResultGroup } from '../../_models/search/search-result-group';
|
||||
import { AccountService } from '../../_services/account.service';
|
||||
import { ImageService } from '../../_services/image.service';
|
||||
import { LibraryService } from '../../_services/library.service';
|
||||
import { NavService } from '../../_services/nav.service';
|
||||
|
||||
@Component({
|
||||
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 { DragableOrderedListComponent } from './dragable-ordered-list/dragable-ordered-list.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 { AddToListModalComponent } from './_modals/add-to-list-modal/add-to-list-modal.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { CardsModule } from '../cards/cards.module';
|
||||
import { ReadingListsComponent } from './reading-lists/reading-lists.component';
|
||||
import { EditReadingListModalComponent } from './_modals/edit-reading-list-modal/edit-reading-list-modal.component';
|
||||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SidenavModule } from '../sidenav/sidenav.module';
|
||||
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: [
|
||||
CommonModule,
|
||||
ReadingListRoutingModule,
|
||||
ReactiveFormsModule,
|
||||
DragDropModule,
|
||||
CardsModule,
|
||||
NgbNavModule,
|
||||
|
||||
PipeModule,
|
||||
SharedModule,
|
||||
SidenavModule,
|
||||
NgbNavModule
|
||||
SharedSideNavCardsModule,
|
||||
|
||||
ReadingListRoutingModule,
|
||||
],
|
||||
exports: [
|
||||
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 { ActivatedRoute, Router } from '@angular/router';
|
||||
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 { NavService } from 'src/app/_services/nav.service';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
|
@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ConfirmEmailComponent } from './confirm-email/confirm-email.component';
|
||||
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 { SplashContainerComponent } from './splash-container/splash-container.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 { ResetPasswordComponent } from './reset-password/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,
|
||||
ConfirmMigrationEmailComponent,
|
||||
ResetPasswordComponent,
|
||||
ConfirmResetPasswordComponent
|
||||
ConfirmResetPasswordComponent,
|
||||
UserLoginComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -5,8 +5,17 @@ import { ConfirmMigrationEmailComponent } from './confirm-migration-email/confir
|
||||
import { ConfirmResetPasswordComponent } from './confirm-reset-password/confirm-reset-password.component';
|
||||
import { RegisterComponent } from './register/register.component';
|
||||
import { ResetPasswordComponent } from './reset-password/reset-password.component';
|
||||
import { UserLoginComponent } from './user-login/user-login.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: UserLoginComponent
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
component: UserLoginComponent
|
||||
},
|
||||
{
|
||||
path: 'confirm-email',
|
||||
component: ConfirmEmailComponent,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { of } from 'rxjs';
|
||||
import { MemberService } from '../_services/member.service';
|
||||
import { MemberService } from '../../_services/member.service';
|
||||
import { UserLoginComponent } from './user-login.component';
|
||||
|
||||
xdescribe('UserLoginComponent', () => {
|
@ -4,14 +4,14 @@ import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { SettingsService } from '../admin/settings.service';
|
||||
import { AddEmailToAccountMigrationModalComponent } from '../registration/add-email-to-account-migration-modal/add-email-to-account-migration-modal.component';
|
||||
import { User } from '../_models/user';
|
||||
import { AccountService } from '../_services/account.service';
|
||||
import { MemberService } from '../_services/member.service';
|
||||
import { NavService } from '../_services/nav.service';
|
||||
import { SettingsService } from '../../admin/settings.service';
|
||||
import { AddEmailToAccountMigrationModalComponent } from '../add-email-to-account-migration-modal/add-email-to-account-migration-modal.component';
|
||||
import { User } from '../../_models/user';
|
||||
import { AccountService } from '../../_services/account.service';
|
||||
import { MemberService } from '../../_services/member.service';
|
||||
import { NavService } from '../../_services/nav.service';
|
||||
|
||||
|
||||
// TODO: Move this into registration module
|
||||
@Component({
|
||||
selector: 'app-user-login',
|
||||
templateUrl: './user-login.component.html',
|
||||
@ -48,7 +48,7 @@ export class UserLoginComponent implements OnInit {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
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', '');
|
||||
this.router.navigateByUrl(pageResume);
|
||||
} else {
|
||||
this.router.navigateByUrl('/library');
|
||||
this.router.navigateByUrl('/libraries');
|
||||
}
|
||||
}, err => {
|
||||
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 { DownloadService } from '../shared/_services/download.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 { ScanSeriesEvent } from '../_models/events/scan-series-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 { SeriesService } from '../_services/series.service';
|
||||
import { NavService } from '../_services/nav.service';
|
||||
import { RelationKind } from '../_models/series-detail/relation-kind';
|
||||
import { RelatedSeries } from '../_models/series-detail/related-series';
|
||||
import { RelationKind } from '../_models/series-detail/relation-kind';
|
||||
|
||||
interface RelatedSeris {
|
||||
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 { Router } from '@angular/router';
|
||||
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
|
||||
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
|
||||
import { UtilityService } from '../shared/_services/utility.service';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
import { ReadingList } from '../_models/reading-list';
|
||||
import { Series } from '../_models/series';
|
||||
import { SeriesMetadata } from '../_models/series-metadata';
|
||||
import { MetadataService } from '../_services/metadata.service';
|
||||
import { TagBadgeCursor } from '../../shared/tag-badge/tag-badge.component';
|
||||
import { FilterQueryParam } from '../../shared/_services/filter-utilities.service';
|
||||
import { UtilityService } from '../../shared/_services/utility.service';
|
||||
import { MangaFormat } from '../../_models/manga-format';
|
||||
import { ReadingList } from '../../_models/reading-list';
|
||||
import { Series } from '../../_models/series';
|
||||
import { SeriesMetadata } from '../../_models/series-metadata';
|
||||
import { MetadataService } from '../../_services/metadata.service';
|
||||
|
||||
@Component({
|
||||
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 { ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { PaginatedResult } from 'src/app/_models/pagination';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { SeriesFilter, SortField } from 'src/app/_models/series-filter';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
@ -201,4 +203,30 @@ export class UtilityService {
|
||||
private isObject(object: any) {
|
||||
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,
|
||||
ReactiveFormsModule,
|
||||
NgbCollapseModule,
|
||||
NgbTooltipModule, // RegisterMemberComponent
|
||||
NgbTooltipModule, // TODO: Validate if we still need this
|
||||
PipeModule,
|
||||
NgCircleProgressModule.forRoot(),
|
||||
],
|
||||
exports: [
|
||||
ReadMoreComponent, // Used globably
|
||||
DrawerComponent, // Can be replaced with boostrap offscreen canvas (v5)
|
||||
ShowIfScrollbarDirective, // Used book reader only?
|
||||
DrawerComponent, // Can be replaced with boostrap offscreen canvas (v5) (also used in book reader and series metadata filter)
|
||||
A11yClickDirective, // Used globally
|
||||
SeriesFormatComponent, // Used globally
|
||||
TagBadgeComponent, // Used globally
|
||||
CircularLoaderComponent, // Used in Cards only
|
||||
ImageComponent, // Used globally
|
||||
|
||||
ShowIfScrollbarDirective, // Used book reader only?
|
||||
|
||||
PersonBadgeComponent, // Used Series Detail
|
||||
BadgeExpanderComponent, // Used globally
|
||||
ImageComponent // Used globally
|
||||
BadgeExpanderComponent, // Used Series Detail/Metadata
|
||||
|
||||
],
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
@ -107,46 +107,46 @@ a {
|
||||
@media (max-width: 576px) {
|
||||
.side-nav-item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
//display: flex;
|
||||
//justify-content: space-between;
|
||||
padding: 15px 10px;
|
||||
width: 100%;
|
||||
//width: 100%;
|
||||
height: 70px;
|
||||
min-height: 40px;
|
||||
overflow: hidden;
|
||||
//min-height: 40px;
|
||||
// overflow: hidden;
|
||||
font-size: 1rem;
|
||||
|
||||
cursor: pointer;
|
||||
//cursor: pointer;
|
||||
|
||||
.side-nav-text {
|
||||
padding-left: 10px;
|
||||
opacity: 1;
|
||||
min-width: 100px;
|
||||
// padding-left: 10px;
|
||||
// opacity: 1;
|
||||
// min-width: 100px;
|
||||
width: 100%;
|
||||
|
||||
div {
|
||||
min-width: 102px;
|
||||
width: 100%
|
||||
}
|
||||
// div {
|
||||
// min-width: 102px;
|
||||
// width: 100%
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
&.closed {
|
||||
.side-nav-text {
|
||||
opacity: 0;
|
||||
}
|
||||
// .side-nav-text {
|
||||
// opacity: 0;
|
||||
// }
|
||||
|
||||
.card-actions {
|
||||
opacity: 0;
|
||||
font-size: inherit
|
||||
//opacity: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
&:last-child {
|
||||
flex-grow: 1;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
// span {
|
||||
// &:last-child {
|
||||
// flex-grow: 1;
|
||||
// justify-content: end;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
</ng-container>
|
||||
</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-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>
|
||||
|
@ -25,7 +25,7 @@
|
||||
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-right-radius: var(--side-nav-border-radius);
|
||||
}
|
||||
@ -52,7 +52,7 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.side-nav-item:first() {
|
||||
.side-nav-item:first {
|
||||
border-top-left-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 { ToastrService } from 'ngx-toastr';
|
||||
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 { User } from 'src/app/_models/user';
|
||||
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 { SiteThemeProviderPipe } from './_pipes/site-theme-provider.pipe';
|
||||
import { ThemeManagerComponent } from './theme-manager/theme-manager.component';
|
||||
import { SidenavModule } from '../sidenav/sidenav.module';
|
||||
import { ColorPickerModule } from 'ngx-color-picker';
|
||||
import { SidenavModule } from '../sidenav/sidenav.module';
|
||||
|
||||
|
||||
|
||||
@ -24,14 +24,18 @@ import { ColorPickerModule } from 'ngx-color-picker';
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
|
||||
NgbAccordionModule,
|
||||
NgbNavModule,
|
||||
NgbTooltipModule,
|
||||
|
||||
NgxSliderModule,
|
||||
UserSettingsRoutingModule,
|
||||
ColorPickerModule, // User prefernces background color
|
||||
|
||||
PipeModule,
|
||||
SidenavModule,
|
||||
ColorPickerModule, // User prefernces background color
|
||||
|
||||
UserSettingsRoutingModule,
|
||||
],
|
||||
exports: [
|
||||
SiteThemeProviderPipe,
|
||||
|
Loading…
x
Reference in New Issue
Block a user