mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-08-11 09:13:42 -04:00
Recently Added Chapter Feedback (#1020)
* Added an alt implementation which shows Recently Added chapters. No extra grouping is performed if multiple chapters per volume. * Started working on a grouping implementation for series. * Disabled the code for bookmarks cleanup as there is some critical issue in there. * Implemented a Series Group activity stream which shows recently updated series and providers a count badge showing how many new chapters/volumes were added in that series. * Removed the bookmark disabling code * Cleaned up code * One more code cleanup
This commit is contained in:
parent
0ce0ee39e0
commit
730624d364
@ -236,8 +236,15 @@ namespace API.Controllers
|
|||||||
return Ok(series);
|
return Ok(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("recently-added-chapters")]
|
[HttpPost("recently-updated-series")]
|
||||||
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
|
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
|
||||||
|
{
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
|
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("recently-added-chapters")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChaptersAlt()
|
||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyAddedChapters(userId));
|
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyAddedChapters(userId));
|
||||||
|
32
API/DTOs/GroupedSeriesDto.cs
Normal file
32
API/DTOs/GroupedSeriesDto.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
|
namespace API.DTOs;
|
||||||
|
/// <summary>
|
||||||
|
/// This is a representation of a Series with some amount of underlying files within it. This is used for Recently Updated Series section
|
||||||
|
/// </summary>
|
||||||
|
public class GroupedSeriesDto
|
||||||
|
{
|
||||||
|
public string SeriesName { get; set; }
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
public int LibraryId { get; set; }
|
||||||
|
public LibraryType LibraryType { get; set; }
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Chapter Id if this is a chapter. Not guaranteed to be set.
|
||||||
|
/// </summary>
|
||||||
|
public int ChapterId { get; set; } = 0;
|
||||||
|
/// <summary>
|
||||||
|
/// Volume Id if this is a chapter. Not guaranteed to be set.
|
||||||
|
/// </summary>
|
||||||
|
public int VolumeId { get; set; } = 0;
|
||||||
|
/// <summary>
|
||||||
|
/// This is used only on the UI. It is just index of being added.
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
public MangaFormat Format { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of items that are updated. This provides a sort of grouping when multiple chapters are added per Volume/Series
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get; set; }
|
||||||
|
}
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.Data.Migrations;
|
||||||
using API.Data.Scanner;
|
using API.Data.Scanner;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.CollectionTags;
|
using API.DTOs.CollectionTags;
|
||||||
@ -21,6 +22,26 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace API.Data.Repositories;
|
namespace API.Data.Repositories;
|
||||||
|
|
||||||
|
internal class RecentlyAddedSeries
|
||||||
|
{
|
||||||
|
public int LibraryId { get; set; }
|
||||||
|
public LibraryType LibraryType { get; set; }
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
public string SeriesName { get; set; }
|
||||||
|
public Series Series { get; set; }
|
||||||
|
public IList<Chapter> Chapters { get; set; } // I don't know if I need this
|
||||||
|
public Chapter Chapter { get; set; } // for Alt implementation
|
||||||
|
public MangaFormat Format { get; set; }
|
||||||
|
public int ChapterId { get; set; } // for Alt implementation
|
||||||
|
public int VolumeId { get; set; } // for Alt implementation
|
||||||
|
public string ChapterNumber { get; set; }
|
||||||
|
public string ChapterRange { get; set; }
|
||||||
|
public string ChapterTitle { get; set; }
|
||||||
|
public bool IsSpecial { get; set; }
|
||||||
|
public int VolumeNumber { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public interface ISeriesRepository
|
public interface ISeriesRepository
|
||||||
{
|
{
|
||||||
void Attach(Series series);
|
void Attach(Series series);
|
||||||
@ -73,6 +94,7 @@ public interface ISeriesRepository
|
|||||||
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
|
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
|
||||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
||||||
Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
||||||
|
Task<IList<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId);
|
||||||
Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId);
|
Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,7 +368,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This returns a dictonary mapping seriesId -> list of chapters back for each series id passed
|
/// This returns a dictionary mapping seriesId -> list of chapters back for each series id passed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seriesIds"></param>
|
/// <param name="seriesIds"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@ -453,7 +475,6 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
allPeopleIds.AddRange(filter.Publisher);
|
allPeopleIds.AddRange(filter.Publisher);
|
||||||
allPeopleIds.AddRange(filter.CoverArtist);
|
allPeopleIds.AddRange(filter.CoverArtist);
|
||||||
allPeopleIds.AddRange(filter.Translators);
|
allPeopleIds.AddRange(filter.Translators);
|
||||||
//allPeopleIds.AddRange(filter.Artist);
|
|
||||||
|
|
||||||
hasPeopleFilter = allPeopleIds.Count > 0;
|
hasPeopleFilter = allPeopleIds.Count > 0;
|
||||||
hasGenresFilter = filter.Genres.Count > 0;
|
hasGenresFilter = filter.Genres.Count > 0;
|
||||||
@ -808,7 +829,116 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string RecentlyAddedItemTitle(RecentlyAddedSeries item)
|
||||||
|
{
|
||||||
|
switch (item.LibraryType)
|
||||||
|
{
|
||||||
|
case LibraryType.Book:
|
||||||
|
return string.Empty;
|
||||||
|
case LibraryType.Comic:
|
||||||
|
return "Issue";
|
||||||
|
case LibraryType.Manga:
|
||||||
|
default:
|
||||||
|
return "Chapter";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show all recently added chapters. Provide some mapping for chapter 0 -> Volume 1
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public async Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId)
|
public async Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId)
|
||||||
|
{
|
||||||
|
var ret = await GetRecentlyAddedChaptersQuery(userId);
|
||||||
|
|
||||||
|
var items = new List<RecentlyAddedItemDto>();
|
||||||
|
foreach (var item in ret)
|
||||||
|
{
|
||||||
|
var dto = new RecentlyAddedItemDto()
|
||||||
|
{
|
||||||
|
LibraryId = item.LibraryId,
|
||||||
|
LibraryType = item.LibraryType,
|
||||||
|
SeriesId = item.SeriesId,
|
||||||
|
SeriesName = item.SeriesName,
|
||||||
|
Created = item.Created,
|
||||||
|
Id = items.Count,
|
||||||
|
Format = item.Format
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add title and Volume/Chapter Id
|
||||||
|
var chapterTitle = RecentlyAddedItemTitle(item);
|
||||||
|
string title;
|
||||||
|
if (item.ChapterNumber.Equals(Parser.Parser.DefaultChapter))
|
||||||
|
{
|
||||||
|
if ((item.VolumeNumber + string.Empty).Equals(Parser.Parser.DefaultChapter))
|
||||||
|
{
|
||||||
|
title = item.ChapterTitle;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
title = "Volume " + item.VolumeNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
dto.VolumeId = item.VolumeId;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
title = item.IsSpecial
|
||||||
|
? item.ChapterRange
|
||||||
|
: $"{chapterTitle} {item.ChapterRange}";
|
||||||
|
dto.ChapterId = item.ChapterId;
|
||||||
|
}
|
||||||
|
|
||||||
|
dto.Title = title;
|
||||||
|
items.Add(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return items;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return recently updated series, regardless of read progress, and group the number of volume or chapters added.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">Used to ensure user has access to libraries</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<IList<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId)
|
||||||
|
{
|
||||||
|
var ret = await GetRecentlyAddedChaptersQuery(userId);
|
||||||
|
|
||||||
|
|
||||||
|
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
|
||||||
|
var index = 0;
|
||||||
|
foreach (var item in ret)
|
||||||
|
{
|
||||||
|
if (seriesMap.ContainsKey(item.SeriesName))
|
||||||
|
{
|
||||||
|
seriesMap[item.SeriesName].Count += 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
seriesMap[item.SeriesName] = new GroupedSeriesDto()
|
||||||
|
{
|
||||||
|
LibraryId = item.LibraryId,
|
||||||
|
LibraryType = item.LibraryType,
|
||||||
|
SeriesId = item.SeriesId,
|
||||||
|
SeriesName = item.SeriesName,
|
||||||
|
Created = item.Created,
|
||||||
|
Id = index,
|
||||||
|
Format = item.Format,
|
||||||
|
Count = 1
|
||||||
|
};
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return seriesMap.Values.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId)
|
||||||
{
|
{
|
||||||
var libraries = await _context.AppUser
|
var libraries = await _context.AppUser
|
||||||
.Where(u => u.Id == userId)
|
.Where(u => u.Id == userId)
|
||||||
@ -817,141 +947,33 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
var libraryIds = libraries.Select(l => l.LibraryId).ToList();
|
var libraryIds = libraries.Select(l => l.LibraryId).ToList();
|
||||||
|
|
||||||
var withinLastWeek = DateTime.Now - TimeSpan.FromDays(12);
|
var withinLastWeek = DateTime.Now - TimeSpan.FromDays(12);
|
||||||
|
var ret = await _context.Chapter
|
||||||
var ret = await _context.Series
|
.Where(c => c.Created >= withinLastWeek)
|
||||||
.Where(s => libraryIds.Contains(s.LibraryId) && s.LastModified >= withinLastWeek)
|
.AsNoTracking()
|
||||||
.Include(s => s.Volumes)
|
.Include(c => c.Volume)
|
||||||
.ThenInclude(v => v.Chapters)
|
.ThenInclude(v => v.Series)
|
||||||
.Select(s => new
|
.ThenInclude(s => s.Library)
|
||||||
|
.OrderByDescending(c => c.Created)
|
||||||
|
.Select(c => new RecentlyAddedSeries()
|
||||||
{
|
{
|
||||||
s.LibraryId,
|
LibraryId = c.Volume.Series.LibraryId,
|
||||||
LibraryType = s.Library.Type,
|
LibraryType = c.Volume.Series.Library.Type,
|
||||||
s.Created,
|
Created = c.Created,
|
||||||
SeriesId = s.Id,
|
SeriesId = c.Volume.Series.Id,
|
||||||
SeriesName = s.Name,
|
SeriesName = c.Volume.Series.Name,
|
||||||
Series = s,
|
Series = c.Volume.Series,
|
||||||
Chapters = s.Volumes.SelectMany(v => v.Chapters)
|
VolumeId = c.VolumeId,
|
||||||
|
ChapterId = c.Id,
|
||||||
|
Format = c.Volume.Series.Format,
|
||||||
|
ChapterNumber = c.Number,
|
||||||
|
ChapterRange = c.Range,
|
||||||
|
IsSpecial = c.IsSpecial,
|
||||||
|
VolumeNumber = c.Volume.Number,
|
||||||
|
ChapterTitle = c.Title
|
||||||
})
|
})
|
||||||
.Take(50)
|
.Take(50)
|
||||||
.AsNoTracking()
|
.Where(c => c.Created >= withinLastWeek && libraryIds.Contains(c.LibraryId))
|
||||||
.AsSplitQuery()
|
|
||||||
.OrderByDescending(item => item.Created)
|
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
return ret;
|
||||||
var items = new List<RecentlyAddedItemDto>();
|
|
||||||
foreach (var series in ret)
|
|
||||||
{
|
|
||||||
if (items.Count >= 50) return items;
|
|
||||||
var chaptersThatMeetCutoff = series.Chapters.Where(c => c.Created >= withinLastWeek)
|
|
||||||
.OrderByDescending(c => c.Created);
|
|
||||||
var chapterMap = chaptersThatMeetCutoff.GroupBy(c => c.VolumeId)
|
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
|
||||||
|
|
||||||
foreach (var (volumeId, chapters) in chapterMap)
|
|
||||||
{
|
|
||||||
// If a single chapter
|
|
||||||
if (chapters.Count == 1)
|
|
||||||
{
|
|
||||||
// Create a chapter ReadingListItemDto
|
|
||||||
var chapterTitle = "Chapter";
|
|
||||||
switch (series.LibraryType)
|
|
||||||
{
|
|
||||||
case LibraryType.Book:
|
|
||||||
chapterTitle = "";
|
|
||||||
break;
|
|
||||||
case LibraryType.Comic:
|
|
||||||
chapterTitle = "Issue";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If chapter is 0, then it means it's really a volume, so show it that way
|
|
||||||
var firstChapter = chapters.First();
|
|
||||||
string title;
|
|
||||||
if (firstChapter.Number.Equals(Parser.Parser.DefaultChapter))
|
|
||||||
{
|
|
||||||
title = "Volume " + series.Series.Volumes.FirstOrDefault(v => v.Id == volumeId)?.Number;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
title = chapters.First().IsSpecial
|
|
||||||
? chapters.FirstOrDefault()?.Range
|
|
||||||
: $"{chapterTitle} {chapters.FirstOrDefault()?.Range}";
|
|
||||||
}
|
|
||||||
|
|
||||||
items.Add(new RecentlyAddedItemDto()
|
|
||||||
{
|
|
||||||
LibraryId = series.LibraryId,
|
|
||||||
LibraryType = series.LibraryType,
|
|
||||||
SeriesId = series.SeriesId,
|
|
||||||
SeriesName = series.SeriesName,
|
|
||||||
Created = chapters.Max(c => c.Created),
|
|
||||||
Title = title,
|
|
||||||
ChapterId = firstChapter.Id,
|
|
||||||
Id = items.Count,
|
|
||||||
Format = series.Series.Format
|
|
||||||
});
|
|
||||||
if (items.Count >= 50) return items;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Multiple chapters, so let's show as a volume
|
|
||||||
var volumeNumber = series.Series.Volumes.FirstOrDefault(v => v.Id == volumeId)?.Number;
|
|
||||||
if (volumeNumber == 0)
|
|
||||||
{
|
|
||||||
var volumeChapters = chapters.Where(c => c.Created >= withinLastWeek).ToList();
|
|
||||||
foreach (var chap in volumeChapters)
|
|
||||||
{
|
|
||||||
// Create a chapter ReadingListItemDto
|
|
||||||
var chapterTitle = "Chapter";
|
|
||||||
switch (series.LibraryType)
|
|
||||||
{
|
|
||||||
case LibraryType.Book:
|
|
||||||
chapterTitle = "";
|
|
||||||
break;
|
|
||||||
case LibraryType.Comic:
|
|
||||||
chapterTitle = "Issue";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var title = volumeChapters.First().IsSpecial
|
|
||||||
? volumeChapters.FirstOrDefault()?.Range
|
|
||||||
: $"{chapterTitle} {volumeChapters.FirstOrDefault()?.Range}";
|
|
||||||
items.Add(new RecentlyAddedItemDto()
|
|
||||||
{
|
|
||||||
LibraryId = series.LibraryId,
|
|
||||||
LibraryType = series.LibraryType,
|
|
||||||
SeriesId = series.SeriesId,
|
|
||||||
SeriesName = series.SeriesName,
|
|
||||||
Created = chap.Created,
|
|
||||||
Title = title,
|
|
||||||
ChapterId = chap.Id,
|
|
||||||
Id = items.Count,
|
|
||||||
Format = series.Series.Format
|
|
||||||
});
|
|
||||||
if (items.Count >= 50) return items;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a volume ReadingListItemDto
|
|
||||||
var theVolume = series.Series.Volumes.First(v => v.Id == volumeId);
|
|
||||||
items.Add(new RecentlyAddedItemDto()
|
|
||||||
{
|
|
||||||
LibraryId = series.LibraryId,
|
|
||||||
LibraryType = series.LibraryType,
|
|
||||||
SeriesId = series.SeriesId,
|
|
||||||
SeriesName = series.SeriesName,
|
|
||||||
Created = chapters.Max(c => c.Created),
|
|
||||||
Title = "Volume " + theVolume.Number,
|
|
||||||
VolumeId = theVolume.Id,
|
|
||||||
Id = items.Count,
|
|
||||||
Format = series.Series.Format
|
|
||||||
});
|
|
||||||
if (items.Count >= 50) return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,13 @@ namespace API.Entities
|
|||||||
/// Original Name on disk. Not exposed to UI.
|
/// Original Name on disk. Not exposed to UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string OriginalName { get; set; }
|
public string OriginalName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Time of creation
|
||||||
|
/// </summary>
|
||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Whenever a modification occurs. Ie) New volumes, removed volumes, title update, etc
|
||||||
|
/// </summary>
|
||||||
public DateTime LastModified { get; set; }
|
public DateTime LastModified { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Absolute path to the (managed) image file
|
/// Absolute path to the (managed) image file
|
||||||
|
@ -964,6 +964,7 @@ namespace API.Parser
|
|||||||
|
|
||||||
public static string Normalize(string name)
|
public static string Normalize(string name)
|
||||||
{
|
{
|
||||||
|
// TODO: This can eat upto 100MB on a file scan. Look into optimizing
|
||||||
var normalized = NormalizeRegex.Replace(name, string.Empty).ToLower();
|
var normalized = NormalizeRegex.Replace(name, string.Empty).ToLower();
|
||||||
return string.IsNullOrEmpty(normalized) ? name : normalized;
|
return string.IsNullOrEmpty(normalized) ? name : normalized;
|
||||||
}
|
}
|
||||||
|
@ -199,8 +199,6 @@ namespace API.Services.Tasks
|
|||||||
_directoryService.FileSystem.Directory.Delete(directory, false);
|
_directoryService.FileSystem.Directory.Delete(directory, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,5 +9,5 @@ export interface RecentlyAddedItem {
|
|||||||
libraryType: LibraryType;
|
libraryType: LibraryType;
|
||||||
volumeId: number;
|
volumeId: number;
|
||||||
chapterId: number;
|
chapterId: number;
|
||||||
id: number; // This is UI only
|
id: number; // This is UI only, sent from backend but has no relation to any entity
|
||||||
}
|
}
|
14
UI/Web/src/app/_models/series-group.ts
Normal file
14
UI/Web/src/app/_models/series-group.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { LibraryType } from "./library";
|
||||||
|
|
||||||
|
export interface SeriesGroup {
|
||||||
|
seriesId: number;
|
||||||
|
seriesName: string;
|
||||||
|
created: string;
|
||||||
|
title: string;
|
||||||
|
libraryId: number;
|
||||||
|
libraryType: LibraryType;
|
||||||
|
volumeId: number;
|
||||||
|
chapterId: number;
|
||||||
|
id: number; // This is UI only, sent from backend but has no relation to any entity
|
||||||
|
count: number;
|
||||||
|
}
|
@ -9,6 +9,7 @@ import { PaginatedResult } from '../_models/pagination';
|
|||||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../_models/series';
|
||||||
import { SeriesFilter } from '../_models/series-filter';
|
import { SeriesFilter } from '../_models/series-filter';
|
||||||
|
import { SeriesGroup } from '../_models/series-group';
|
||||||
import { SeriesMetadata } from '../_models/series-metadata';
|
import { SeriesMetadata } from '../_models/series-metadata';
|
||||||
import { Volume } from '../_models/volume';
|
import { Volume } from '../_models/volume';
|
||||||
import { ImageService } from './image.service';
|
import { ImageService } from './image.service';
|
||||||
@ -123,13 +124,11 @@ export class SeriesService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRecentlyUpdatedSeries() {
|
||||||
|
return this.httpClient.post<SeriesGroup[]>(this.baseUrl + 'series/recently-updated-series', {});
|
||||||
|
}
|
||||||
getRecentlyAddedChapters() {
|
getRecentlyAddedChapters() {
|
||||||
return this.httpClient.post<RecentlyAddedItem[]>(this.baseUrl + 'series/recently-added-chapters', {}).pipe(
|
return this.httpClient.post<RecentlyAddedItem[]>(this.baseUrl + 'series/recently-added-chapters', {});
|
||||||
map(items => {
|
|
||||||
items.forEach((item, i) => item.id = i);
|
|
||||||
return items;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||||
@ -144,9 +143,6 @@ export class SeriesService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// getContinueReading(libraryId: number = 0) {
|
|
||||||
// return this.httpClient.get<InProgressChapter[]>(this.baseUrl + 'series/continue-reading?libraryId=' + libraryId);
|
|
||||||
// }
|
|
||||||
|
|
||||||
refreshMetadata(series: Series) {
|
refreshMetadata(series: Series) {
|
||||||
return this.httpClient.post(this.baseUrl + 'series/refresh-metadata', {libraryId: series.libraryId, seriesId: series.id});
|
return this.httpClient.post(this.baseUrl + 'series/refresh-metadata', {libraryId: series.libraryId, seriesId: series.id});
|
||||||
|
@ -22,6 +22,10 @@
|
|||||||
<div class="bulk-mode {{bulkSelectionService.hasSelections() ? 'always-show' : ''}}" (click)="handleSelection($event)" *ngIf="allowSelection">
|
<div class="bulk-mode {{bulkSelectionService.hasSelections() ? 'always-show' : ''}}" (click)="handleSelection($event)" *ngIf="allowSelection">
|
||||||
<input type="checkbox" attr.aria-labelledby="{{title}}_{{entity?.id}}" [ngModel]="selected" [ngModelOptions]="{standalone: true}">
|
<input type="checkbox" attr.aria-labelledby="{{title}}_{{entity?.id}}" [ngModel]="selected" [ngModelOptions]="{standalone: true}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="count" *ngIf="count > 1">
|
||||||
|
<span class="badge badge-primary">{{count}}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
|
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
|
||||||
|
@ -118,6 +118,12 @@ $image-width: 160px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
|
.count {
|
||||||
|
top: 5px;
|
||||||
|
right: 10px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-actions {
|
.card-actions {
|
||||||
|
@ -68,6 +68,10 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
* This will supress the cannot read archive warning when total pages is 0
|
* This will supress the cannot read archive warning when total pages is 0
|
||||||
*/
|
*/
|
||||||
@Input() supressArchiveWarning: boolean = false;
|
@Input() supressArchiveWarning: boolean = false;
|
||||||
|
/**
|
||||||
|
* The number of updates/items within the card. If less than 2, will not be shown.
|
||||||
|
*/
|
||||||
|
@Input() count: number = 0;
|
||||||
/**
|
/**
|
||||||
* Event emitted when item is clicked
|
* Event emitted when item is clicked
|
||||||
*/
|
*/
|
||||||
|
@ -11,6 +11,20 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</app-carousel-reel>
|
</app-carousel-reel>
|
||||||
|
|
||||||
|
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series">
|
||||||
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
|
<app-card-item [entity]="item" [title]="item.seriesName" [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]="recentlyAdded" title="Recently Added Series" (sectionClick)="handleSectionClick($event)">
|
||||||
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
|
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="loadRecentlyAdded()"></app-series-card>
|
||||||
|
</ng-template>
|
||||||
|
</app-carousel-reel> -->
|
||||||
|
|
||||||
<app-carousel-reel [items]="recentlyAddedChapters" title="Recently Added">
|
<app-carousel-reel [items]="recentlyAddedChapters" title="Recently Added">
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
<app-card-item [entity]="item" [title]="item.title" [subtitle]="item.seriesName" [imageUrl]="imageService.getRecentlyAddedItem(item)"
|
<app-card-item [entity]="item" [title]="item.title" [subtitle]="item.seriesName" [imageUrl]="imageService.getRecentlyAddedItem(item)"
|
||||||
@ -18,12 +32,6 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</app-carousel-reel>
|
</app-carousel-reel>
|
||||||
|
|
||||||
<app-carousel-reel [items]="recentlyAdded" title="Recently Added Series" (sectionClick)="handleSectionClick($event)">
|
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
|
||||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="loadRecentlyAdded()"></app-series-card>
|
|
||||||
</ng-template>
|
|
||||||
</app-carousel-reel>
|
|
||||||
|
|
||||||
<app-carousel-reel [items]="libraries" title="Libraries" (sectionClick)="handleSectionClick($event)">
|
<app-carousel-reel [items]="libraries" title="Libraries" (sectionClick)="handleSectionClick($event)">
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
<app-library-card [data]="item"></app-library-card>
|
<app-library-card [data]="item"></app-library-card>
|
||||||
|
@ -8,6 +8,7 @@ import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
|
|||||||
import { Library } from '../_models/library';
|
import { Library } from '../_models/library';
|
||||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../_models/series';
|
||||||
|
import { SeriesGroup } from '../_models/series-group';
|
||||||
import { User } from '../_models/user';
|
import { User } from '../_models/user';
|
||||||
import { AccountService } from '../_services/account.service';
|
import { AccountService } from '../_services/account.service';
|
||||||
import { ImageService } from '../_services/image.service';
|
import { ImageService } from '../_services/image.service';
|
||||||
@ -28,6 +29,7 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||||||
isAdmin = false;
|
isAdmin = false;
|
||||||
|
|
||||||
recentlyAdded: Series[] = [];
|
recentlyAdded: Series[] = [];
|
||||||
|
recentlyUpdatedSeries: SeriesGroup[] = [];
|
||||||
recentlyAddedChapters: RecentlyAddedItem[] = [];
|
recentlyAddedChapters: RecentlyAddedItem[] = [];
|
||||||
inProgress: Series[] = [];
|
inProgress: Series[] = [];
|
||||||
|
|
||||||
@ -45,10 +47,14 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||||||
this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
|
this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
|
||||||
this.recentlyAdded.unshift(series);
|
this.recentlyAdded.unshift(series);
|
||||||
});
|
});
|
||||||
|
this.loadRecentlyAddedChapters();
|
||||||
} else if (res.event === EVENTS.SeriesRemoved) {
|
} else if (res.event === EVENTS.SeriesRemoved) {
|
||||||
const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
|
const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
|
||||||
this.recentlyAdded = this.recentlyAdded.filter(item => item.id != seriesRemovedEvent.seriesId);
|
this.recentlyAdded = this.recentlyAdded.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||||
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
|
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||||
|
|
||||||
|
this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
||||||
|
this.recentlyAddedChapters = this.recentlyAddedChapters.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -105,6 +111,10 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadRecentlyAddedChapters() {
|
loadRecentlyAddedChapters() {
|
||||||
|
this.seriesService.getRecentlyUpdatedSeries().pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
||||||
|
this.recentlyUpdatedSeries = updatedSeries;
|
||||||
|
});
|
||||||
|
|
||||||
this.seriesService.getRecentlyAddedChapters().pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
this.seriesService.getRecentlyAddedChapters().pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
||||||
this.recentlyAddedChapters = updatedSeries;
|
this.recentlyAddedChapters = updatedSeries;
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user