Misc Enhancements (#1525)

* Moved the data connection for the Database out of appsettings.json and hardcoded it. This will allow for more customization and cleaner update process.

* Removed unneeded code

* Updated pdf viewer to 15.0.0 (pdf 2.6), which now supports east-asian fonts

* Fixed up some regex parsing for volumes that have a float number.

* Fixed a bug where the tooltip for Publication Status wouldn't show

* Fixed some weird parsing rules where v1.1 would parse as volume 1 chapter 1

* Fixed a bug where bookmarking button was hidden for admins without bookmark role (due to migration)

* Unified the star rating component in series detail to match metadata filter.

* Fixed a bug in the bulk selection code when using shift selection, where the inverse of what was selected would be toggled.

* Fixed some old code where if on all series page, only English as a language would return. We now return all languages of all libraries.

* Updated api/metadata/languages documentation

* Refactored some bookmark api names: get-bookmarks -> chapter-bookmarks, get-all-bookmarks -> all-bookmarks, get-series-bookmarks -> series-bookmarks, etc.

* Refactored all cases of createSeriesFilter to filterUtiltityService.

Added ability to search for a series on Bookmarks page.

Fixed a bug where people filters wouldn't respect the disable flag froms ettings.

* Cleaned up a bit of the circular downloader code.

* Implemented Russian Parsing

* Fixed an issue where some users that had a missing theme entry wouldn't be able to update their user preferences.

* Refactored normalization to exclude !, thus allowing series with ! to be different from each other.

* Fixed a migration exit case

* Fixed broken unit test
This commit is contained in:
Joseph Milazzo 2022-09-13 18:59:26 -05:00 committed by GitHub
parent b7d88f08d8
commit 00f0ad5a3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 508 additions and 419 deletions

View File

@ -77,6 +77,8 @@ public class ComicParserTests
[InlineData("Bd Fr-Aldebaran-Antares-t6", "Aldebaran-Antares")] [InlineData("Bd Fr-Aldebaran-Antares-t6", "Aldebaran-Antares")]
[InlineData("Tintin - T22 Vol 714 pour Sydney", "Tintin")] [InlineData("Tintin - T22 Vol 714 pour Sydney", "Tintin")]
[InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")] [InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")]
[InlineData("Kebab Том 1 Глава 1", "Kebab")]
[InlineData("Манга Глава 1", "Манга")]
public void ParseComicSeriesTest(string filename, string expected) public void ParseComicSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename));
@ -124,6 +126,9 @@ public class ComicParserTests
[InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "3")] [InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "3")]
[InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "0")] [InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "0")]
[InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "1")] [InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "1")]
// Russian Tests
[InlineData("Kebab Том 1 Глава 3", "1")]
[InlineData("Манга Глава 2", "0")]
public void ParseComicVolumeTest(string filename, string expected) public void ParseComicVolumeTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(filename));
@ -169,6 +174,10 @@ public class ComicParserTests
[InlineData("Batman Beyond 2016 - Chapter 001.cbz", "1")] [InlineData("Batman Beyond 2016 - Chapter 001.cbz", "1")]
[InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "1")] [InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "1")]
[InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "0")] [InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "0")]
[InlineData("Kebab Том 1 Глава 3", "3")]
[InlineData("Манга Глава 2", "2")]
[InlineData("Манга 2 Глава", "2")]
[InlineData("Манга Том 1 2 Глава", "2")]
public void ParseComicChapterTest(string filename, string expected) public void ParseComicChapterTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(filename));

View File

@ -27,7 +27,7 @@ public class MangaParserTests
[InlineData("vol_356-1", "356")] // Mangapy syntax [InlineData("vol_356-1", "356")] // Mangapy syntax
[InlineData("No Volume", "0")] [InlineData("No Volume", "0")]
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")] [InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")]
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1")] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1.1")]
[InlineData("Tonikaku Cawaii [Volume 11].cbz", "11")] [InlineData("Tonikaku Cawaii [Volume 11].cbz", "11")]
[InlineData("[WS]_Ichiban_Ushiro_no_Daimaou_v02_ch10.zip", "2")] [InlineData("[WS]_Ichiban_Ushiro_no_Daimaou_v02_ch10.zip", "2")]
[InlineData("[xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]", "1")] [InlineData("[xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]", "1")]
@ -39,7 +39,6 @@ public class MangaParserTests
[InlineData("Ichinensei_ni_Nacchattara_v02_ch11_[Taruby]_v1.3.zip", "2")] [InlineData("Ichinensei_ni_Nacchattara_v02_ch11_[Taruby]_v1.3.zip", "2")]
[InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")] [InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")]
[InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")] [InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")]
[InlineData("Dorohedoro v12 (2013) (Digital) (LostNerevarine-Empire).cbz", "12")]
[InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")] [InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")]
[InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "0")] [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "0")]
[InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")] [InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")]
@ -73,6 +72,11 @@ public class MangaParserTests
[InlineData("시즌34삽화2", "34")] [InlineData("시즌34삽화2", "34")]
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1巻", "1")] [InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1巻", "1")]
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")] [InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")]
[InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "3.5")]
[InlineData("Kebab Том 1 Глава 3", "1")]
[InlineData("Манга Глава 2", "0")]
[InlineData("Манга Тома 1-4", "1-4")]
[InlineData("Манга Том 1-4", "1-4")]
public void ParseVolumeTest(string filename, string expected) public void ParseVolumeTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
@ -181,6 +185,10 @@ public class MangaParserTests
[InlineData("諌山創] 23", "] ")] [InlineData("諌山創] 23", "] ")]
[InlineData("(一般コミック) [奥浩哉] 09", "")] [InlineData("(一般コミック) [奥浩哉] 09", "")]
[InlineData("Highschool of the Dead - 02", "Highschool of the Dead")] [InlineData("Highschool of the Dead - 02", "Highschool of the Dead")]
[InlineData("Kebab Том 1 Глава 3", "Kebab")]
[InlineData("Манга Глава 2", "Манга")]
[InlineData("Манга Глава 2-2", "Манга")]
[InlineData("Манга Том 1 3-4 Глава", "Манга")]
public void ParseSeriesTest(string filename, string expected) public void ParseSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
@ -195,7 +203,7 @@ public class MangaParserTests
[InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1-8")] [InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1-8")]
[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "0")] [InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "0")]
[InlineData("c001", "1")] [InlineData("c001", "1")]
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", "12")] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", "0")]
[InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")] [InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")]
[InlineData("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")] [InlineData("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")]
[InlineData("Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip", "0-6")] [InlineData("Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip", "0-6")]
@ -233,8 +241,7 @@ public class MangaParserTests
[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter.rar", "0")] [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter.rar", "0")]
[InlineData("Beelzebub_153b_RHS.zip", "153.5")] [InlineData("Beelzebub_153b_RHS.zip", "153.5")]
[InlineData("Beelzebub_150-153b_RHS.zip", "150-153.5")] [InlineData("Beelzebub_150-153b_RHS.zip", "150-153.5")]
[InlineData("Transferred to another world magical swordsman v1.1", "1")] [InlineData("Transferred to another world magical swordsman v1.1", "0")]
[InlineData("Transferred to another world magical swordsman v1.2", "2")]
[InlineData("Kiss x Sis - Ch.15 - The Angst of a 15 Year Old Boy.cbz", "15")] [InlineData("Kiss x Sis - Ch.15 - The Angst of a 15 Year Old Boy.cbz", "15")]
[InlineData("Kiss x Sis - Ch.12 - 1 , 2 , 3P!.cbz", "12")] [InlineData("Kiss x Sis - Ch.12 - 1 , 2 , 3P!.cbz", "12")]
[InlineData("Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1", "1")] [InlineData("Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1", "1")]
@ -259,6 +266,11 @@ public class MangaParserTests
[InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")] [InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")]
[InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")] [InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")]
[InlineData("[ハレム] SMごっこ 10", "10")] [InlineData("[ハレム] SMごっこ 10", "10")]
[InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "0")]
[InlineData("Kebab Том 1 Глава 3", "3")]
[InlineData("Манга Глава 2", "2")]
[InlineData("Манга 2 Глава", "2")]
[InlineData("Манга Том 1 2 Глава", "2")]
public void ParseChaptersTest(string filename, string expected) public void ParseChaptersTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));

View File

@ -162,7 +162,7 @@ public class ParserTests
[InlineData("Darker Than_Black", "darkerthanblack")] [InlineData("Darker Than_Black", "darkerthanblack")]
[InlineData("Citrus", "citrus")] [InlineData("Citrus", "citrus")]
[InlineData("Citrus+", "citrus+")] [InlineData("Citrus+", "citrus+")]
[InlineData("Again!!!!", "again")] [InlineData("Again", "again")]
[InlineData("카비타", "카비타")] [InlineData("카비타", "카비타")]
[InlineData("06", "06")] [InlineData("06", "06")]
[InlineData("", "")] [InlineData("", "")]

View File

@ -115,8 +115,9 @@ public class MetadataController : BaseApiController
} }
/// <summary> /// <summary>
/// Fetches all age ratings from the instance /// Fetches all age languages from the libraries passed (or if none passed, all in the server)
/// </summary> /// </summary>
/// <remarks>This does not perform RBS for the user if they have Library access due to the non-sensitive nature of languages</remarks>
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param> /// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
/// <returns></returns> /// <returns></returns>
[HttpGet("languages")] [HttpGet("languages")]
@ -128,15 +129,8 @@ public class MetadataController : BaseApiController
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids)); return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
} }
var englishTag = CultureInfo.GetCultureInfo("en");
return Ok(new List<LanguageDto>() return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync());
{
new ()
{
Title = englishTag.DisplayName,
IsoCode = englishTag.IetfLanguageTag
}
});
} }
[HttpGet("all-languages")] [HttpGet("all-languages")]

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using API.Data; using API.Data;
using API.Data.Repositories; using API.Data.Repositories;
using API.DTOs; using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Reader; using API.DTOs.Reader;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
@ -529,7 +530,7 @@ public class ReaderController : BaseApiController
/// </summary> /// </summary>
/// <param name="chapterId"></param> /// <param name="chapterId"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("get-bookmarks")] [HttpGet("chapter-bookmarks")]
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId) public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
{ {
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
@ -540,13 +541,15 @@ public class ReaderController : BaseApiController
/// <summary> /// <summary>
/// Returns a list of all bookmarked pages for a User /// Returns a list of all bookmarked pages for a User
/// </summary> /// </summary>
/// <param name="filterDto">Only supports SeriesNameQuery</param>
/// <returns></returns> /// <returns></returns>
[HttpGet("get-all-bookmarks")] [HttpPost("all-bookmarks")]
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks() public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks(FilterDto filterDto)
{ {
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>()); if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id));
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id, filterDto));
} }
/// <summary> /// <summary>
@ -629,7 +632,7 @@ public class ReaderController : BaseApiController
/// </summary> /// </summary>
/// <param name="volumeId"></param> /// <param name="volumeId"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("get-volume-bookmarks")] [HttpGet("volume-bookmarks")]
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId) public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
{ {
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
@ -642,7 +645,7 @@ public class ReaderController : BaseApiController
/// </summary> /// </summary>
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("get-series-bookmarks")] [HttpGet("series-bookmarks")]
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId) public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
{ {
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);

View File

@ -78,6 +78,8 @@ public class UsersController : BaseApiController
AppUserIncludes.UserPreferences); AppUserIncludes.UserPreferences);
var existingPreferences = user.UserPreferences; var existingPreferences = user.UserPreferences;
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection; existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
existingPreferences.ScalingOption = preferencesDto.ScalingOption; existingPreferences.ScalingOption = preferencesDto.ScalingOption;
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption; existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
@ -92,7 +94,6 @@ public class UsersController : BaseApiController
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize; existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate; existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection; existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName; existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode; existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode; existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;

View File

@ -118,5 +118,4 @@ public class ServerInfoDto
/// </summary> /// </summary>
/// <remarks>Introduced in v0.5.4</remarks> /// <remarks>Introduced in v0.5.4</remarks>
public bool UsingSeriesRelationships { get; set; } public bool UsingSeriesRelationships { get; set; }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Data;
using API.DTOs.Theme; using API.DTOs.Theme;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
@ -83,11 +84,11 @@ public class UserPreferencesDto
/// </summary> /// </summary>
[Required] [Required]
public ReadingDirection BookReaderReadingDirection { get; set; } public ReadingDirection BookReaderReadingDirection { get; set; }
/// <summary> /// <summary>
/// UI Site Global Setting: The UI theme the user should use. /// UI Site Global Setting: The UI theme the user should use.
/// </summary> /// </summary>
/// <remarks>Should default to Dark</remarks> /// <remarks>Should default to Dark</remarks>
[Required]
public SiteTheme Theme { get; set; } public SiteTheme Theme { get; set; }
[Required] [Required]
public string BookReaderThemeName { get; set; } public string BookReaderThemeName { get; set; }

View File

@ -1,181 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
using API.Helpers;
using API.Services;
using Microsoft.EntityFrameworkCore;
namespace API.Data;
/// <summary>
/// A data structure to migrate Cover Images from byte[] to files.
/// </summary>
internal class CoverMigration
{
public string Id { get; set; }
public byte[] CoverImage { get; set; }
public string ParentId { get; set; }
}
/// <summary>
/// In v0.4.6, Cover Images were migrated from byte[] in the DB to external files. This migration handles that work.
/// </summary>
public static class MigrateCoverImages
{
private static readonly ChapterSortComparerZeroFirst ChapterSortComparerForInChapterSorting = new ();
/// <summary>
/// Run first. Will extract byte[]s from DB and write them to the cover directory.
/// </summary>
public static void ExtractToImages(DbContext context, IDirectoryService directoryService, IImageService imageService)
{
Console.WriteLine("Migrating Cover Images to disk. Expect delay.");
directoryService.ExistOrCreate(directoryService.CoverImageDirectory);
Console.WriteLine("Extracting cover images for Series");
var lockedSeries = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From Series Where CoverImage IS NOT NULL", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1],
ParentId = "0"
});
foreach (var series in lockedSeries)
{
if (series.CoverImage == null || !series.CoverImage.Any()) continue;
if (File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetSeriesFormat(int.Parse(series.Id))}.png"))) continue;
try
{
var stream = new MemoryStream(series.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, ImageService.GetSeriesFormat(int.Parse(series.Id)), directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Extracting cover images for Chapters");
var chapters = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage, VolumeId From Chapter Where CoverImage IS NOT NULL;", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1],
ParentId = x[2] + string.Empty
});
foreach (var chapter in chapters)
{
if (chapter.CoverImage == null || !chapter.CoverImage.Any()) continue;
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}.png"))) continue;
try
{
var stream = new MemoryStream(chapter.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}", directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Extracting cover images for Collection Tags");
var tags = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From CollectionTag Where CoverImage IS NOT NULL;", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1] ,
ParentId = "0"
});
foreach (var tag in tags)
{
if (tag.CoverImage == null || !tag.CoverImage.Any()) continue;
if (directoryService.FileSystem.File.Exists(Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}.png"))) continue;
try
{
var stream = new MemoryStream(tag.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}", directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
/// <summary>
/// Run after <see cref="ExtractToImages"/>. Will update the DB with names of files that were extracted.
/// </summary>
/// <param name="context"></param>
public static async Task UpdateDatabaseWithImages(DataContext context, IDirectoryService directoryService)
{
Console.WriteLine("Updating Series entities");
var seriesCovers = await context.Series.Where(s => !string.IsNullOrEmpty(s.CoverImage)).ToListAsync();
foreach (var series in seriesCovers)
{
if (!directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetSeriesFormat(series.Id)}.png"))) continue;
series.CoverImage = $"{ImageService.GetSeriesFormat(series.Id)}.png";
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Chapter entities");
var chapters = await context.Chapter.ToListAsync();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var chapter in chapters)
{
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png")))
{
chapter.CoverImage = $"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Volume entities");
var volumes = await context.Volume.Include(v => v.Chapters).ToListAsync();
foreach (var volume in volumes)
{
var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerForInChapterSorting);
if (firstChapter == null) continue;
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png")))
{
volume.CoverImage = $"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Collection Tag entities");
var tags = await context.CollectionTag.ToListAsync();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var tag in tags)
{
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetCollectionTagFormat(tag.Id)}.png")))
{
tag.CoverImage = $"{ImageService.GetCollectionTagFormat(tag.Id)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Cover Image Migration completed");
}
}

View File

@ -0,0 +1,120 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data;
/// <summary>
/// v0.6.0 introduced a change in how Normalization works and hence every normalized field needs to be re-calculated
/// </summary>
public static class MigrateNormalizedEverything
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
// if current version is > 0.5.6.3, then we can exit and not perform
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (Version.Parse(settings.InstallVersion) > new Version(0, 5, 6, 3))
{
return;
}
logger.LogCritical("Running MigrateNormalizedEverything migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database");
logger.LogInformation("Updating Normalization on Series...");
foreach (var series in await dataContext.Series.ToListAsync())
{
series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName ?? string.Empty);
series.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name ?? string.Empty);
logger.LogInformation("Updated Series: {SeriesName}", series.Name);
unitOfWork.SeriesRepository.Update(series);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on Series...Done");
// Genres
logger.LogInformation("Updating Normalization on Genres...");
foreach (var genre in await dataContext.Genre.ToListAsync())
{
genre.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(genre.Title ?? string.Empty);
logger.LogInformation("Updated Genre: {Genre}", genre.Title);
unitOfWork.GenreRepository.Attach(genre);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on Genres...Done");
// Tags
logger.LogInformation("Updating Normalization on Tags...");
foreach (var tag in await dataContext.Tag.ToListAsync())
{
tag.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(tag.Title ?? string.Empty);
logger.LogInformation("Updated Tag: {Tag}", tag.Title);
unitOfWork.TagRepository.Attach(tag);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on Tags...Done");
// People
logger.LogInformation("Updating Normalization on People...");
foreach (var person in await dataContext.Person.ToListAsync())
{
person.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(person.Name ?? string.Empty);
logger.LogInformation("Updated Person: {Person}", person.Name);
unitOfWork.PersonRepository.Attach(person);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on People...Done");
// Collections
logger.LogInformation("Updating Normalization on Collections...");
foreach (var collection in await dataContext.CollectionTag.ToListAsync())
{
collection.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(collection.Title ?? string.Empty);
logger.LogInformation("Updated Collection: {Collection}", collection.Title);
unitOfWork.CollectionTagRepository.Update(collection);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on Collections...Done");
// Reading Lists
logger.LogInformation("Updating Normalization on Reading Lists...");
foreach (var readingList in await dataContext.ReadingList.ToListAsync())
{
readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title ?? string.Empty);
logger.LogInformation("Updated Reading List: {ReadingList}", readingList.Title);
unitOfWork.ReadingListRepository.Update(readingList);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on Reading Lists...Done");
logger.LogInformation("MigrateNormalizedEverything migration finished");
}
}

View File

@ -44,6 +44,7 @@ public interface ILibraryRepository
IEnumerable<JumpKeyDto> GetJumpBarAsync(int libraryId); IEnumerable<JumpKeyDto> GetJumpBarAsync(int libraryId);
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<LanguageDto>> GetAllLanguagesForLibrariesAsync();
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds); IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders); Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders);
Library GetLibraryByFolder(string folder); Library GetLibraryByFolder(string folder);
@ -311,6 +312,26 @@ public class LibraryRepository : ILibraryRepository
.ToList(); .ToList();
} }
public async Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync()
{
var ret = await _context.Series
.Select(s => s.Metadata.Language)
.AsSplitQuery()
.AsNoTracking()
.Distinct()
.ToListAsync();
return ret
.Where(s => !string.IsNullOrEmpty(s))
.Select(s => new LanguageDto()
{
Title = CultureInfo.GetCultureInfo(s).DisplayName,
IsoCode = s
})
.OrderBy(s => s.Title)
.ToList();
}
public IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds) public IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
{ {
return _context.Series return _context.Series

View File

@ -5,12 +5,14 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Constants; using API.Constants;
using API.DTOs; using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Reader; using API.DTOs.Reader;
using API.Entities; using API.Entities;
using AutoMapper; using AutoMapper;
using AutoMapper.QueryableExtensions; using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SixLabors.ImageSharp.PixelFormats;
namespace API.Data.Repositories; namespace API.Data.Repositories;
@ -44,7 +46,7 @@ public interface IUserRepository
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId); Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId); Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId); Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId);
Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId); Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId, FilterDto filter);
Task<IEnumerable<AppUserBookmark>> GetAllBookmarksAsync(); Task<IEnumerable<AppUserBookmark>> GetAllBookmarksAsync();
Task<AppUserBookmark> GetBookmarkForPage(int page, int chapterId, int userId); Task<AppUserBookmark> GetBookmarkForPage(int page, int chapterId, int userId);
Task<AppUserBookmark> GetBookmarkAsync(int bookmarkId); Task<AppUserBookmark> GetBookmarkAsync(int bookmarkId);
@ -309,12 +311,63 @@ public class UserRepository : IUserRepository
.ToListAsync(); .ToListAsync();
} }
public async Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId) /// <summary>
/// Get all bookmarks for the user
/// </summary>
/// <param name="userId"></param>
/// <param name="filter">Only supports SeriesNameQuery</param>
/// <returns></returns>
public async Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId, FilterDto filter)
{ {
return await _context.AppUserBookmark var query = _context.AppUserBookmark
.Where(x => x.AppUserId == userId) .Where(x => x.AppUserId == userId)
.OrderBy(x => x.Page) .OrderBy(x => x.Page)
.AsNoTracking() .AsNoTracking();
if (!string.IsNullOrEmpty(filter.SeriesNameQuery))
{
var seriesNameQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(filter.SeriesNameQuery);
var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, (bookmark, series) => new
{
bookmark,
series
})
.Where(o => EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%")
);
// This doesn't work on bookmarks themselves, only the series. For now, I don't think there is much value add
// if (filter.SortOptions != null)
// {
// if (filter.SortOptions.IsAscending)
// {
// filterSeriesQuery = filter.SortOptions.SortField switch
// {
// SortField.SortName => filterSeriesQuery.OrderBy(s => s.series.SortName),
// SortField.CreatedDate => filterSeriesQuery.OrderBy(s => s.bookmark.Created),
// SortField.LastModifiedDate => filterSeriesQuery.OrderBy(s => s.bookmark.LastModified),
// _ => filterSeriesQuery
// };
// }
// else
// {
// filterSeriesQuery = filter.SortOptions.SortField switch
// {
// SortField.SortName => filterSeriesQuery.OrderByDescending(s => s.series.SortName),
// SortField.CreatedDate => filterSeriesQuery.OrderByDescending(s => s.bookmark.Created),
// SortField.LastModifiedDate => filterSeriesQuery.OrderByDescending(s => s.bookmark.LastModified),
// _ => filterSeriesQuery
// };
// }
// }
query = filterSeriesQuery.Select(o => o.bookmark);
}
return await query
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider) .ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync(); .ToListAsync();
} }

View File

@ -59,7 +59,6 @@ public static class ApplicationServiceExtensions
services.AddScoped<IEventHub, EventHub>(); services.AddScoped<IEventHub, EventHub>();
services.AddSqLite(config, env); services.AddSqLite(config, env);
services.AddLogging(config);
services.AddSignalR(opt => opt.EnableDetailedErrors = true); services.AddSignalR(opt => opt.EnableDetailedErrors = true);
} }
@ -68,18 +67,9 @@ public static class ApplicationServiceExtensions
{ {
services.AddDbContext<DataContext>(options => services.AddDbContext<DataContext>(options =>
{ {
options.UseSqlite(config.GetConnectionString("DefaultConnection")); options.UseSqlite("Data source=config/kavita.db");
options.EnableDetailedErrors(); options.EnableDetailedErrors();
options.EnableSensitiveDataLogging(env.IsDevelopment()); options.EnableSensitiveDataLogging(env.IsDevelopment());
}); });
} }
private static void AddLogging(this IServiceCollection services, IConfiguration config)
{
services.AddLogging(loggingBuilder =>
{
var loggingSection = config.GetSection("Logging");
loggingBuilder.AddFile(loggingSection);
});
}
} }

View File

@ -37,7 +37,6 @@ public class Program
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
Console.OutputEncoding = System.Text.Encoding.UTF8; Console.OutputEncoding = System.Text.Encoding.UTF8;
var isDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker;
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.WriteTo.Console() .WriteTo.Console()
.CreateBootstrapLogger(); .CreateBootstrapLogger();
@ -87,7 +86,8 @@ public class Program
await Seed.SeedThemes(context); await Seed.SeedThemes(context);
await Seed.SeedUserApiKeys(context); await Seed.SeedUserApiKeys(context);
// NOTE: This check is from v0.4.8 (Nov 04, 2021). We can likely remove this
var isDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker;
if (isDocker && new FileInfo("data/appsettings.json").Exists) if (isDocker && new FileInfo("data/appsettings.json").Exists)
{ {
logger.LogCritical("WARNING! Mount point is incorrect, nothing here will persist. Please change your container mount from /kavita/data to /kavita/config"); logger.LogCritical("WARNING! Mount point is incorrect, nothing here will persist. Please change your container mount from /kavita/data to /kavita/config");

View File

@ -58,7 +58,7 @@ public static class Parser
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)(?<!back)(?<!back_)(?<!back-)(cover|folder)(?![\w\d])", private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)(?<!back)(?<!back_)(?<!back-)(cover|folder)(?![\w\d])",
MatchOptions, RegexTimeout); MatchOptions, RegexTimeout);
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]", private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+!]",
MatchOptions, RegexTimeout); MatchOptions, RegexTimeout);
/// <summary> /// <summary>
@ -67,6 +67,8 @@ public static class Parser
private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+", private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+",
MatchOptions, RegexTimeout); MatchOptions, RegexTimeout);
private const string Number = @"\d+(\.\d)?";
private const string NumberRange = Number + @"(-" + Number + @")?";
private static readonly Regex[] MangaVolumeRegex = new[] private static readonly Regex[] MangaVolumeRegex = new[]
{ {
@ -78,9 +80,10 @@ public static class Parser
new Regex( new Regex(
@"(?<Series>.*)(\b|_)(?!\[)(vol\.?)(?<Volume>\d+(-\d+)?)(?!\])", @"(?<Series>.*)(\b|_)(?!\[)(vol\.?)(?<Volume>\d+(-\d+)?)(?!\])",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// TODO: In .NET 7, update this to use raw literal strings and apply the NumberRange everywhere
// Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17 // Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17
new Regex( new Regex(
@"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>\d+(-\d+)?)(?!\])", @"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>" + NumberRange + @")(?!\])",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Kodomo no Jikan vol. 10, [dmntsf.net] One Piece - Digital Colored Comics Vol. 20.5-21.5 Ch. 177 // Kodomo no Jikan vol. 10, [dmntsf.net] One Piece - Digital Colored Comics Vol. 20.5-21.5 Ch. 177
new Regex( new Regex(
@ -130,10 +133,34 @@ public static class Parser
new Regex( new Regex(
@"(?<Volume>\d+(?:(\-)\d+)?)巻", @"(?<Volume>\d+(?:(\-)\d+)?)巻",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: n Том -> Volume n
new Regex(
@"(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
MatchOptions, RegexTimeout),
}; };
private static readonly Regex[] MangaSeriesRegex = new[] private static readonly Regex[] MangaSeriesRegex = new[]
{ {
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: n Том -> Volume n
new Regex(
@"(?<Series>.+?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
MatchOptions, RegexTimeout),
// Russian Chapter: n Главa -> Chapter n
new Regex(
@"(?<Series>.+?)(?!Том)(?<!Том\.)\s\d+(\s|_)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)",
MatchOptions, RegexTimeout),
// Russian Chapter: Главы n -> Chapter n
new Regex(
@"(?<Series>.+?)(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, RegexTimeout),
// Grand Blue Dreaming - SP02 // Grand Blue Dreaming - SP02
new Regex( new Regex(
@"(?<Series>.*)(\b|_|-|\s)(?:sp)\d", @"(?<Series>.*)(\b|_|-|\s)(?:sp)\d",
@ -280,10 +307,27 @@ public static class Parser
new Regex( new Regex(
@"(?<Series>.+?)第(?<Volume>\d+(?:(\-)\d+)?)巻", @"(?<Series>.+?)第(?<Volume>\d+(?:(\-)\d+)?)巻",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
}; };
private static readonly Regex[] ComicSeriesRegex = new[] private static readonly Regex[] ComicSeriesRegex = new[]
{ {
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: n Том -> Volume n
new Regex(
@"(?<Series>.+?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
MatchOptions, RegexTimeout),
// Russian Chapter: n Главa -> Chapter n
new Regex(
@"(?<Series>.+?)(?!Том)(?<!Том\.)\s\d+(\s|_)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)",
MatchOptions, RegexTimeout),
// Russian Chapter: Главы n -> Chapter n
new Regex(
@"(?<Series>.+?)(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, RegexTimeout),
// Tintin - T22 Vol 714 pour Sydney // Tintin - T22 Vol 714 pour Sydney
new Regex( new Regex(
@"(?<Series>.+?)\s?(\b|_|-)\s?((vol|tome|t)\.?)(?<Volume>\d+(-\d+)?)", @"(?<Series>.+?)\s?(\b|_|-)\s?((vol|tome|t)\.?)(?<Volume>\d+(-\d+)?)",
@ -380,6 +424,14 @@ public static class Parser
new Regex( new Regex(
@"(?<Volume>\d+(?:(\-)\d+)?)巻", @"(?<Volume>\d+(?:(\-)\d+)?)巻",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: n Том -> Volume n
new Regex(
@"(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
MatchOptions, RegexTimeout),
}; };
private static readonly Regex[] ComicChapterRegex = new[] private static readonly Regex[] ComicChapterRegex = new[]
@ -417,11 +469,18 @@ public static class Parser
@"^(?<Series>.+?)(?:vol\.?\d+)\s#(?<Chapter>\d+)", @"^(?<Series>.+?)(?:vol\.?\d+)\s#(?<Chapter>\d+)",
MatchOptions, MatchOptions,
RegexTimeout), RegexTimeout),
// Russian Chapter: Главы n -> Chapter n
new Regex(
@"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, RegexTimeout),
// Russian Chapter: n Главa -> Chapter n
new Regex(
@"(?!Том)(?<!Том\.)\s\d+(\s|_)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)",
MatchOptions, RegexTimeout),
// Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) // Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
new Regex( new Regex(
@"^(?<Series>.+?)(?: (?<Chapter>\d+))", @"^(?<Series>.+?)(?: (?<Chapter>\d+))",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Saga 001 (2012) (Digital) (Empire-Zone) // Saga 001 (2012) (Digital) (Empire-Zone)
new Regex( new Regex(
@"(?<Series>.+?)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)\s\(\d{4}", @"(?<Series>.+?)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)\s\(\d{4}",
@ -438,7 +497,6 @@ public static class Parser
new Regex( new Regex(
@"^(?<Series>.+?)-(chapter-)?(?<Chapter>\d+)", @"^(?<Series>.+?)-(chapter-)?(?<Chapter>\d+)",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
}; };
private static readonly Regex[] ReleaseGroupRegex = new[] private static readonly Regex[] ReleaseGroupRegex = new[]
@ -459,7 +517,7 @@ public static class Parser
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
new Regex( new Regex(
@"v\d+\.(?<Chapter>\d+(?:.\d+|-\d+)?)", @"v\d+\.(\s|_)(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz (Rare case, if causes issue remove) // Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz (Rare case, if causes issue remove)
new Regex( new Regex(
@ -469,6 +527,10 @@ public static class Parser
new Regex( new Regex(
@"^(?!Vol)(?<Series>.*)\s?(?<!vol\. )\sChapter\s(?<Chapter>\d+(?:\.?[\d-]+)?)", @"^(?!Vol)(?<Series>.*)\s?(?<!vol\. )\sChapter\s(?<Chapter>\d+(?:\.?[\d-]+)?)",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Russian Chapter: Главы n -> Chapter n
new Regex(
@"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, RegexTimeout),
// Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz
new Regex( new Regex(
@"^(?!Vol)(?<Series>.+?)(?<!Vol)(?<!Vol.)\s(\d\s)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", @"^(?!Vol)(?<Series>.+?)(?<!Vol)(?<!Vol.)\s(\d\s)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)",
@ -503,9 +565,14 @@ public static class Parser
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Korean Chapter: 第10話 -> Chapter n, [ハレム]ナナとカオル 高校生のSMごっこ 第1話 // Korean Chapter: 第10話 -> Chapter n, [ハレム]ナナとカオル 高校生のSMごっこ 第1話
new Regex( new Regex(
@"第?(?<Chapter>\d+(?:.\d+|-\d+)?)話", @"第?(?<Chapter>\d+(?:\.\d+|-\d+)?)話",
MatchOptions, RegexTimeout),
// Russian Chapter: n Главa -> Chapter n
new Regex(
@"(?!Том)(?<!Том\.)\s\d+(\s|_)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
}; };
private static readonly Regex[] MangaEditionRegex = { private static readonly Regex[] MangaEditionRegex = {
// Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
new Regex( new Regex(
@ -760,12 +827,10 @@ public static class Parser
var matches = regex.Matches(filename); var matches = regex.Matches(filename);
foreach (Match match in matches) foreach (Match match in matches)
{ {
if (match.Groups["Chapter"].Success && match.Groups["Chapter"] != Match.Empty) if (!match.Groups["Chapter"].Success || match.Groups["Chapter"] == Match.Empty) continue;
{ var value = match.Groups["Chapter"].Value;
var value = match.Groups["Chapter"].Value; var hasPart = match.Groups["Part"].Success;
var hasPart = match.Groups["Part"].Success; return FormatValue(value, hasPart);
return FormatValue(value, hasPart);
}
} }
} }

View File

@ -5,7 +5,6 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Constants;
using API.Data; using API.Data;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
@ -18,12 +17,10 @@ using API.Services.Tasks;
using API.SignalR; using API.SignalR;
using Hangfire; using Hangfire;
using Hangfire.MemoryStorage; using Hangfire.MemoryStorage;
using Hangfire.Storage.SQLite;
using Kavita.Common; using Kavita.Common;
using Kavita.Common.EnvironmentInfo; using Kavita.Common.EnvironmentInfo;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -193,8 +190,8 @@ public class Startup
await MigrateRemoveExtraThemes.Migrate(unitOfWork, themeService); await MigrateRemoveExtraThemes.Migrate(unitOfWork, themeService);
// Only needed for v0.5.5.x and v0.5.6 // only needed for v0.5.4 and v0.6.0
await MigrateNormalizedLocalizedName.Migrate(unitOfWork, dataContext, logger); await MigrateNormalizedEverything.Migrate(unitOfWork, dataContext, logger);
// Update the version in the DB after all migrations are run // Update the version in the DB after all migrations are run
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);

View File

@ -1,7 +1,4 @@
{ {
"ConnectionStrings": {
"DefaultConnection": "Data source=config//kavita.db"
},
"TokenKey": "super secret unguessable key", "TokenKey": "super secret unguessable key",
"Port": 5000 "Port": 5000
} }

View File

@ -1,7 +1,4 @@
{ {
"ConnectionStrings": {
"DefaultConnection": "Data source=config/kavita.db"
},
"TokenKey": "super secret unguessable key", "TokenKey": "super secret unguessable key",
"Port": 5000 "Port": 5000
} }

View File

@ -29,13 +29,6 @@ public static class Configuration
set => SetJwtToken(GetAppSettingFilename(), value); set => SetJwtToken(GetAppSettingFilename(), value);
} }
public static string DatabasePath
{
get => GetDatabasePath(GetAppSettingFilename());
set => SetDatabasePath(GetAppSettingFilename(), value);
}
private static string GetAppSettingFilename() private static string GetAppSettingFilename()
{ {
if (!string.IsNullOrEmpty(AppSettingsFilename)) if (!string.IsNullOrEmpty(AppSettingsFilename))
@ -191,52 +184,4 @@ public static class Configuration
/* Swallow Exception */ /* Swallow Exception */
} }
} }
private static string GetDatabasePath(string filePath)
{
const string defaultFile = "config/kavita.db";
try
{
var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
if (jsonObj.TryGetProperty("ConnectionStrings", out JsonElement tokenElement))
{
foreach (var property in tokenElement.EnumerateObject())
{
if (!property.Name.Equals("DefaultConnection")) continue;
return property.Value.GetString();
}
}
}
catch (Exception ex)
{
Console.WriteLine("Error writing app settings: " + ex.Message);
}
return defaultFile;
}
/// <summary>
/// This should NEVER be called except by MigrateConfigFiles
/// </summary>
/// <param name="filePath"></param>
/// <param name="updatedPath"></param>
private static void SetDatabasePath(string filePath, string updatedPath)
{
try
{
var existingString = GetDatabasePath(filePath);
var json = File.ReadAllText(filePath)
.Replace(existingString,
"Data source=" + updatedPath);
File.WriteAllText(filePath, json);
}
catch (Exception)
{
/* Swallow Exception */
}
}
} }

View File

@ -21,4 +21,4 @@
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -12650,9 +12650,9 @@
} }
}, },
"ngx-extended-pdf-viewer": { "ngx-extended-pdf-viewer": {
"version": "14.5.3", "version": "15.0.2",
"resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-14.5.3.tgz", "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-15.0.2.tgz",
"integrity": "sha512-9pqnbonKcu/6SIwPe3yCfHzsO1fgO7qIwETHD7UuS2kAG5GM7VkEwrqMoF7qsZ0Lq/rkqFBcGsS4GYW5JK+oEQ==", "integrity": "sha512-3cuJ87hqod8b/DiIjLNCYxLZYkfi+bm0PsjMFw4GnGfjKB7QJv0p/+KvrCdD68k18Aim5Sd5BMZhF2pHelp1mw==",
"requires": { "requires": {
"lodash.deburr": "^4.1.0", "lodash.deburr": "^4.1.0",
"tslib": "^2.3.0" "tslib": "^2.3.0"

View File

@ -39,7 +39,7 @@
"lazysizes": "^5.3.2", "lazysizes": "^5.3.2",
"ng-circle-progress": "^1.6.0", "ng-circle-progress": "^1.6.0",
"ngx-color-picker": "^12.0.0", "ngx-color-picker": "^12.0.0",
"ngx-extended-pdf-viewer": "^14.5.2", "ngx-extended-pdf-viewer": "^15.0.0",
"ngx-file-drop": "^14.0.1", "ngx-file-drop": "^14.0.1",
"ngx-infinite-scroll": "^13.0.2", "ngx-infinite-scroll": "^13.0.2",
"ngx-toastr": "^14.2.1", "ngx-toastr": "^14.2.1",

View File

@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
import { Chapter } from '../_models/chapter'; import { Chapter } from '../_models/chapter';
import { CollectionTag } from '../_models/collection-tag'; import { CollectionTag } from '../_models/collection-tag';
import { Library } from '../_models/library'; import { Library } from '../_models/library';
import { MangaFormat } from '../_models/manga-format';
import { ReadingList } from '../_models/reading-list'; import { ReadingList } from '../_models/reading-list';
import { Series } from '../_models/series'; import { Series } from '../_models/series';
import { Volume } from '../_models/volume'; import { Volume } from '../_models/volume';
@ -271,13 +270,13 @@ export class ActionFactoryService {
action: Action.MarkAsRead, action: Action.MarkAsRead,
title: 'Mark as Read', title: 'Mark as Read',
callback: this.dummyCallback, callback: this.dummyCallback,
requiresAdmin: false requiresAdmin: false
}, },
{ {
action: Action.MarkAsUnread, action: Action.MarkAsUnread,
title: 'Mark as Unread', title: 'Mark as Unread',
callback: this.dummyCallback, callback: this.dummyCallback,
requiresAdmin: false requiresAdmin: false
}, },
{ {
action: Action.AddToReadingList, action: Action.AddToReadingList,

View File

@ -1,4 +1,4 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -10,6 +10,9 @@ import { MangaFormat } from '../_models/manga-format';
import { BookmarkInfo } from '../_models/manga-reader/bookmark-info'; import { BookmarkInfo } from '../_models/manga-reader/bookmark-info';
import { PageBookmark } from '../_models/page-bookmark'; import { PageBookmark } from '../_models/page-bookmark';
import { ProgressBookmark } from '../_models/progress-bookmark'; import { ProgressBookmark } from '../_models/progress-bookmark';
import { SeriesFilter } from '../_models/series-filter';
import { UtilityService } from '../shared/_services/utility.service';
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
export const CHAPTER_ID_DOESNT_EXIST = -1; export const CHAPTER_ID_DOESNT_EXIST = -1;
export const CHAPTER_ID_NOT_FETCHED = -2; export const CHAPTER_ID_NOT_FETCHED = -2;
@ -24,7 +27,9 @@ export class ReaderService {
// Override background color for reader and restore it onDestroy // Override background color for reader and restore it onDestroy
private originalBodyColor!: string; private originalBodyColor!: string;
constructor(private httpClient: HttpClient, private router: Router, private location: Location) { } constructor(private httpClient: HttpClient, private router: Router,
private location: Location, private utilityService: UtilityService,
private filterUtilitySerivce: FilterUtilitiesService) { }
getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat) { getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat) {
if (format === undefined) format = MangaFormat.ARCHIVE; if (format === undefined) format = MangaFormat.ARCHIVE;
@ -50,20 +55,24 @@ export class ReaderService {
return this.httpClient.post(this.baseUrl + 'reader/unbookmark', {seriesId, volumeId, chapterId, page}); return this.httpClient.post(this.baseUrl + 'reader/unbookmark', {seriesId, volumeId, chapterId, page});
} }
getAllBookmarks() { getAllBookmarks(filter: SeriesFilter | undefined) {
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-all-bookmarks'); let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, undefined, undefined);
const data = this.filterUtilitySerivce.createSeriesFilter(filter);
return this.httpClient.post<PageBookmark[]>(this.baseUrl + 'reader/all-bookmarks', data);
} }
getBookmarks(chapterId: number) { getBookmarks(chapterId: number) {
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-bookmarks?chapterId=' + chapterId); return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/chapter-bookmarks?chapterId=' + chapterId);
} }
getBookmarksForVolume(volumeId: number) { getBookmarksForVolume(volumeId: number) {
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-volume-bookmarks?volumeId=' + volumeId); return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/volume-bookmarks?volumeId=' + volumeId);
} }
getBookmarksForSeries(seriesId: number) { getBookmarksForSeries(seriesId: number) {
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-series-bookmarks?seriesId=' + seriesId); return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/series-bookmarks?seriesId=' + seriesId);
} }
clearBookmarks(seriesId: number) { clearBookmarks(seriesId: number) {

View File

@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
import { UtilityService } from '../shared/_services/utility.service'; import { UtilityService } from '../shared/_services/utility.service';
import { Chapter } from '../_models/chapter'; import { Chapter } from '../_models/chapter';
import { ChapterMetadata } from '../_models/chapter-metadata'; import { ChapterMetadata } from '../_models/chapter-metadata';
@ -26,12 +27,13 @@ export class SeriesService {
paginatedResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>(); paginatedResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>();
paginatedSeriesForTagsResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>(); paginatedSeriesForTagsResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>();
constructor(private httpClient: HttpClient, private imageService: ImageService, private utilityService: UtilityService) { } constructor(private httpClient: HttpClient, private imageService: ImageService,
private utilityService: UtilityService, private filterUtilitySerivce: FilterUtilitiesService) { }
getAllSeries(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) { getAllSeries(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
let params = new HttpParams(); let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
const data = this.createSeriesFilter(filter); const data = this.filterUtilitySerivce.createSeriesFilter(filter);
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all', data, {observe: 'response', params}).pipe( return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all', data, {observe: 'response', params}).pipe(
map((response: any) => { map((response: any) => {
@ -43,7 +45,7 @@ export class SeriesService {
getSeriesForLibrary(libraryId: number, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) { getSeriesForLibrary(libraryId: number, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
let params = new HttpParams(); let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
const data = this.createSeriesFilter(filter); const data = this.filterUtilitySerivce.createSeriesFilter(filter);
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series?libraryId=' + libraryId, data, {observe: 'response', params}).pipe( return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
map((response: any) => { map((response: any) => {
@ -109,7 +111,7 @@ export class SeriesService {
} }
getRecentlyAdded(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) { getRecentlyAdded(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
const data = this.createSeriesFilter(filter); const data = this.filterUtilitySerivce.createSeriesFilter(filter);
let params = new HttpParams(); let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
@ -125,7 +127,7 @@ export class SeriesService {
} }
getWantToRead(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter): Observable<PaginatedResult<Series[]>> { getWantToRead(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter): Observable<PaginatedResult<Series[]>> {
const data = this.createSeriesFilter(filter); const data = this.filterUtilitySerivce.createSeriesFilter(filter);
let params = new HttpParams(); let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
@ -137,7 +139,7 @@ export class SeriesService {
} }
getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) { getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
const data = this.createSeriesFilter(filter); const data = this.filterUtilitySerivce.createSeriesFilter(filter);
let params = new HttpParams(); let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
@ -204,41 +206,4 @@ export class SeriesService {
getSeriesDetail(seriesId: number) { getSeriesDetail(seriesId: number) {
return this.httpClient.get<SeriesDetail>(this.baseUrl + 'series/series-detail?seriesId=' + seriesId); return this.httpClient.get<SeriesDetail>(this.baseUrl + 'series/series-detail?seriesId=' + seriesId);
} }
createSeriesFilter(filter?: SeriesFilter) {
if (filter !== undefined) return filter;
const data: SeriesFilter = {
formats: [],
libraries: [],
genres: [],
writers: [],
artists: [],
penciller: [],
inker: [],
colorist: [],
letterer: [],
coverArtist: [],
editor: [],
publisher: [],
character: [],
translators: [],
collectionTags: [],
rating: 0,
readStatus: {
read: true,
inProgress: true,
notRead: true
},
sortOptions: null,
ageRating: [],
tags: [],
languages: [],
publicationStatus: [],
seriesNameQuery: '',
};
return data;
}
} }

View File

@ -13,7 +13,6 @@ import { Series } from '../_models/series';
import { FilterEvent, SeriesFilter } from '../_models/series-filter'; import { FilterEvent, SeriesFilter } from '../_models/series-filter';
import { Action } from '../_services/action-factory.service'; import { Action } from '../_services/action-factory.service';
import { ActionService } from '../_services/action.service'; import { ActionService } from '../_services/action.service';
import { LibraryService } from '../_services/library.service';
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service'; import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
import { SeriesService } from '../_services/series.service'; import { SeriesService } from '../_services/series.service';
@ -86,14 +85,14 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
private titleService: Title, private actionService: ActionService, private titleService: Title, private actionService: ActionService,
public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
private utilityService: UtilityService, private route: ActivatedRoute, private utilityService: UtilityService, private route: ActivatedRoute,
private filterUtilityService: FilterUtilitiesService, private libraryService: LibraryService) { private filterUtilityService: FilterUtilitiesService) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.titleService.setTitle('Kavita - All Series'); this.titleService.setTitle('Kavita - All Series');
this.pagination = this.filterUtilityService.pagination(this.route.snapshot); this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot); [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
this.filterActiveCheck = this.seriesService.createSeriesFilter(); this.filterActiveCheck = this.filterUtilityService.createSeriesFilter();
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -1,4 +1,4 @@
<app-side-nav-companion-bar [hasFilter]="false"> <app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<h2 title> <h2 title>
Bookmarks Bookmarks
</h2> </h2>
@ -8,9 +8,10 @@
<app-card-detail-layout <app-card-detail-layout
[isLoading]="loadingBookmarks" [isLoading]="loadingBookmarks"
[items]="series" [items]="series"
[filterSettings]="filterSettings"
[trackByIdentity]="trackByIdentity" [trackByIdentity]="trackByIdentity"
[refresh]="refresh" [refresh]="refresh"
(applyFilter)="updateFilter($event)"
> >
<ng-template #cardItem let-item let-position="idx"> <ng-template #cardItem let-item let-position="idx">
<app-card-item [entity]="item" (reload)="loadBookmarks()" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)" <app-card-item [entity]="item" (reload)="loadBookmarks()" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)"

View File

@ -1,13 +1,17 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { take, Subject } from 'rxjs'; import { take, Subject } from 'rxjs';
import { BulkSelectionService } from 'src/app/cards/bulk-selection.service'; import { BulkSelectionService } from 'src/app/cards/bulk-selection.service';
import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
import { ConfirmService } from 'src/app/shared/confirm.service'; import { ConfirmService } from 'src/app/shared/confirm.service';
import { DownloadService } from 'src/app/shared/_services/download.service'; import { DownloadService } from 'src/app/shared/_services/download.service';
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
import { KEY_CODES } from 'src/app/shared/_services/utility.service'; import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { PageBookmark } from 'src/app/_models/page-bookmark'; import { PageBookmark } from 'src/app/_models/page-bookmark';
import { Pagination } from 'src/app/_models/pagination';
import { Series } from 'src/app/_models/series'; import { Series } from 'src/app/_models/series';
import { FilterEvent, SeriesFilter } from 'src/app/_models/series-filter';
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service'; import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { ReaderService } from 'src/app/_services/reader.service'; import { ReaderService } from 'src/app/_services/reader.service';
@ -29,6 +33,13 @@ export class BookmarksComponent implements OnInit, OnDestroy {
clearingSeries: {[id: number]: boolean} = {}; clearingSeries: {[id: number]: boolean} = {};
actions: ActionItem<Series>[] = []; actions: ActionItem<Series>[] = [];
pagination!: Pagination;
filter: SeriesFilter | undefined = undefined;
filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActive: boolean = false;
filterActiveCheck!: SeriesFilter;
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`; trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
refresh: EventEmitter<void> = new EventEmitter(); refresh: EventEmitter<void> = new EventEmitter();
@ -38,12 +49,25 @@ export class BookmarksComponent implements OnInit, OnDestroy {
private downloadService: DownloadService, private toastr: ToastrService, private downloadService: DownloadService, private toastr: ToastrService,
private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService, private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService,
public imageService: ImageService, private actionFactoryService: ActionFactoryService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
private router: Router, private readonly cdRef: ChangeDetectorRef) { } private router: Router, private readonly cdRef: ChangeDetectorRef,
private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute) {
this.filterSettings.ageRatingDisabled = true;
this.filterSettings.collectionDisabled = true;
this.filterSettings.formatDisabled = true;
this.filterSettings.genresDisabled = true;
this.filterSettings.languageDisabled = true;
this.filterSettings.libraryDisabled = true;
this.filterSettings.peopleDisabled = true;
this.filterSettings.publicationStatusDisabled = true;
this.filterSettings.ratingDisabled = true;
this.filterSettings.readProgressDisabled = true;
this.filterSettings.tagsDisabled = true;
this.filterSettings.sortDisabled = true;
}
ngOnInit(): void { ngOnInit(): void {
this.loadBookmarks();
this.actions = this.actionFactoryService.getBookmarkActions(this.handleAction.bind(this)); this.actions = this.actionFactoryService.getBookmarkActions(this.handleAction.bind(this));
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
} }
ngOnDestroy() { ngOnDestroy() {
@ -111,9 +135,15 @@ export class BookmarksComponent implements OnInit, OnDestroy {
} }
loadBookmarks() { loadBookmarks() {
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
if (this.filter == undefined) {
this.filter = this.filterUtilityService.createSeriesFilter();
this.cdRef.markForCheck();
}
this.loadingBookmarks = true; this.loadingBookmarks = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.readerService.getAllBookmarks().pipe(take(1)).subscribe(bookmarks => {
this.readerService.getAllBookmarks(this.filter).pipe(take(1)).subscribe(bookmarks => {
this.bookmarks = bookmarks; this.bookmarks = bookmarks;
this.seriesIds = {}; this.seriesIds = {};
this.bookmarks.forEach(bmk => { this.bookmarks.forEach(bmk => {
@ -174,4 +204,11 @@ export class BookmarksComponent implements OnInit, OnDestroy {
}); });
} }
updateFilter(data: FilterEvent) {
this.filter = data.filter;
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter);
this.loadBookmarks();
}
} }

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRoute, NavigationStart, Router } from '@angular/router'; import { NavigationStart, Router } from '@angular/router';
import { ReplaySubject } from 'rxjs'; import { ReplaySubject } from 'rxjs';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service'; import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
@ -23,7 +23,6 @@ export class BulkSelectionService {
private selectedCards: { [key: string]: {[key: number]: boolean} } = {}; private selectedCards: { [key: string]: {[key: number]: boolean} } = {};
private dataSourceMax: { [key: string]: number} = {}; private dataSourceMax: { [key: string]: number} = {};
public isShiftDown: boolean = false; public isShiftDown: boolean = false;
private activeRoute: string = '';
private actionsSource = new ReplaySubject<ActionItem<any>[]>(1); private actionsSource = new ReplaySubject<ActionItem<any>[]>(1);
public actions$ = this.actionsSource.asObservable(); public actions$ = this.actionsSource.asObservable();
@ -34,14 +33,13 @@ export class BulkSelectionService {
*/ */
public selections$ = this.selectionsSource.asObservable(); public selections$ = this.selectionsSource.asObservable();
constructor(private router: Router, private actionFactory: ActionFactoryService, private route: ActivatedRoute) { constructor(router: Router, private actionFactory: ActionFactoryService) {
router.events router.events
.pipe(filter(event => event instanceof NavigationStart)) .pipe(filter(event => event instanceof NavigationStart))
.subscribe((event) => { .subscribe(() => {
this.deselectAll(); this.deselectAll();
this.dataSourceMax = {}; this.dataSourceMax = {};
this.prevIndex = 0; this.prevIndex = 0;
this.activeRoute = this.router.url;
}); });
} }
@ -53,7 +51,7 @@ export class BulkSelectionService {
this.debugLog('Selecting ' + dataSource + ' cards from ' + this.prevIndex + ' to ' + index); this.debugLog('Selecting ' + dataSource + ' cards from ' + this.prevIndex + ' to ' + index);
this.selectCards(dataSource, this.prevIndex, index, !wasSelected); this.selectCards(dataSource, this.prevIndex, index, !wasSelected);
} else { } else {
const isForwardSelection = index < this.prevIndex; const isForwardSelection = index > this.prevIndex;
if (isForwardSelection) { if (isForwardSelection) {
this.debugLog('Selecting ' + this.prevDataSource + ' cards from ' + this.prevIndex + ' to ' + this.dataSourceMax[this.prevDataSource]); this.debugLog('Selecting ' + this.prevDataSource + ' cards from ' + this.prevIndex + ' to ' + this.dataSourceMax[this.prevDataSource]);

View File

@ -5,6 +5,7 @@ import { Router } from '@angular/router';
import { VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller'; import { VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { FilterSettings } from 'src/app/metadata-filter/filter-settings'; import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { JumpKey } from 'src/app/_models/jumpbar/jump-key'; import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
import { Library } from 'src/app/_models/library'; import { Library } from 'src/app/_models/library';
@ -71,10 +72,10 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
return Breakpoint; return Breakpoint;
} }
constructor(private seriesService: SeriesService, public utilityService: UtilityService, constructor(private filterUtilitySerivce: FilterUtilitiesService, public utilityService: UtilityService,
@Inject(DOCUMENT) private document: Document, private changeDetectionRef: ChangeDetectorRef, @Inject(DOCUMENT) private document: Document, private changeDetectionRef: ChangeDetectorRef,
private jumpbarService: JumpbarService, private router: Router) { private jumpbarService: JumpbarService, private router: Router) {
this.filter = this.seriesService.createSeriesFilter(); this.filter = this.filterUtilitySerivce.createSeriesFilter();
this.changeDetectionRef.markForCheck(); this.changeDetectionRef.markForCheck();
} }

View File

@ -29,7 +29,6 @@ import { DownloadIndicatorComponent } from './download-indicator/download-indica
@NgModule({ @NgModule({
declarations: [ declarations: [
CardItemComponent, CardItemComponent,
@ -68,7 +67,6 @@ import { DownloadIndicatorComponent } from './download-indicator/download-indica
VirtualScrollerModule, VirtualScrollerModule,
NgbOffcanvasModule, // Series Detail, action of cards NgbOffcanvasModule, // Series Detail, action of cards
NgbNavModule, //Series Detail NgbNavModule, //Series Detail
NgbPaginationModule, // EditCollectionTagsComponent NgbPaginationModule, // EditCollectionTagsComponent

View File

@ -31,7 +31,7 @@
<ng-container> <ng-container>
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2"> <div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus"> <ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus">
<app-icon-and-title label="Publication" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === 'Ongoing' ? 'empty' : 'end'}}" (click)="handleGoTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)" title="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})"> <app-icon-and-title label="Publication" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === 'Ongoing' ? 'empty' : 'end'}}" (click)="handleGoTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)" ngbTooltip="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})">
{{pubStatus}} {{pubStatus}}
</app-icon-and-title> </app-icon-and-title>
</ng-container> </ng-container>

View File

@ -140,7 +140,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy, AfterConten
this.seriesPagination = this.filterUtilityService.pagination(this.route.snapshot); this.seriesPagination = this.filterUtilityService.pagination(this.route.snapshot);
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot); [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
this.filterSettings.presets.collectionTags = [tagId]; this.filterSettings.presets.collectionTags = [tagId];
this.filterActiveCheck = this.seriesService.createSeriesFilter(); this.filterActiveCheck = this.filterUtilityService.createSeriesFilter();
this.filterActiveCheck.collectionTags = [tagId]; this.filterActiveCheck.collectionTags = [tagId];
this.cdRef.markForCheck(); this.cdRef.markForCheck();

View File

@ -134,7 +134,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot); [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId]; if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId];
// Setup filterActiveCheck to check filter against // Setup filterActiveCheck to check filter against
this.filterActiveCheck = this.seriesService.createSeriesFilter(); this.filterActiveCheck = this.filterUtilityService.createSeriesFilter();
this.filterActiveCheck.libraries = [this.libraryId]; this.filterActiveCheck.libraries = [this.libraryId];
this.filterSettings.libraryDisabled = true; this.filterSettings.libraryDisabled = true;
@ -230,7 +230,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
loadPage() { loadPage() {
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards // The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
if (this.filter == undefined) { if (this.filter == undefined) {
this.filter = this.seriesService.createSeriesFilter(); this.filter = this.filterUtilityService.createSeriesFilter();
this.filter.libraries.push(this.libraryId); this.filter.libraries.push(this.libraryId);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }

View File

@ -468,7 +468,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
this.user = user; this.user = user;
this.hasBookmarkRights = this.accountService.hasBookmarkRole(user); this.hasBookmarkRights = this.accountService.hasBookmarkRole(user) || this.accountService.hasAdminRole(user);
this.readingDirection = this.user.preferences.readingDirection; this.readingDirection = this.user.preferences.readingDirection;
this.scalingOption = this.user.preferences.scalingOption; this.scalingOption = this.user.preferences.scalingOption;
this.pageSplitOption = this.user.preferences.pageSplitOption; this.pageSplitOption = this.user.preferences.pageSplitOption;

View File

@ -99,7 +99,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="cover-artist" class="form-label">Cover Artists</label> <label for="cover-artist" class="form-label">Cover Artists</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)" <app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.CoverArtist)"> [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.CoverArtist) || filterSettings.peopleDisabled">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -114,7 +114,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="writers" class="form-label">Writers</label> <label for="writers" class="form-label">Writers</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)" <app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Writer)"> [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Writer) || filterSettings.peopleDisabled">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -129,7 +129,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="publisher" class="form-label">Publisher</label> <label for="publisher" class="form-label">Publisher</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)" <app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Publisher)"> [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Publisher) || filterSettings.peopleDisabled">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -144,7 +144,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="penciller" class="form-label">Penciller</label> <label for="penciller" class="form-label">Penciller</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)" <app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Penciller)"> [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Penciller) || filterSettings.peopleDisabled">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -159,7 +159,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="letterer" class="form-label">Letterer</label> <label for="letterer" class="form-label">Letterer</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)" <app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Letterer)"> [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Letterer) || filterSettings.peopleDisabled">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -174,7 +174,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="inker" class="form-label">Inker</label> <label for="inker" class="form-label">Inker</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)" <app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Inker)"> [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Inker) || filterSettings.peopleDisabled">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -189,7 +189,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="editor" class="form-label">Editor</label> <label for="editor" class="form-label">Editor</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)" <app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Editor)"> [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Editor) || filterSettings.peopleDisabled">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -204,7 +204,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="colorist" class="form-label">Colorist</label> <label for="colorist" class="form-label">Colorist</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)" <app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Colorist)"> [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Colorist) || filterSettings.peopleDisabled">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -219,7 +219,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="character" class="form-label">Character</label> <label for="character" class="form-label">Character</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)" <app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Character)"> [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Character) || filterSettings.peopleDisabled">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>
@ -234,7 +234,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="translators" class="form-label">Translators</label> <label for="translators" class="form-label">Translators</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)" <app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Translator)"> [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Translator) || filterSettings.peopleDisabled">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} {{item.name}}
</ng-template> </ng-template>

View File

@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Ev
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'; import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
import { distinctUntilChanged, forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs'; import { distinctUntilChanged, forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs';
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
import { UtilityService } from '../shared/_services/utility.service'; import { UtilityService } from '../shared/_services/utility.service';
import { TypeaheadSettings } from '../typeahead/typeahead-settings'; import { TypeaheadSettings } from '../typeahead/typeahead-settings';
import { CollectionTag } from '../_models/collection-tag'; import { CollectionTag } from '../_models/collection-tag';
@ -17,7 +18,6 @@ import { Tag } from '../_models/tag';
import { CollectionTagService } from '../_services/collection-tag.service'; import { CollectionTagService } from '../_services/collection-tag.service';
import { LibraryService } from '../_services/library.service'; import { LibraryService } from '../_services/library.service';
import { MetadataService } from '../_services/metadata.service'; import { MetadataService } from '../_services/metadata.service';
import { SeriesService } from '../_services/series.service';
import { ToggleService } from '../_services/toggle.service'; import { ToggleService } from '../_services/toggle.service';
import { FilterSettings } from './filter-settings'; import { FilterSettings } from './filter-settings';
@ -86,9 +86,9 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
return SortField; return SortField;
} }
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService, constructor(private libraryService: LibraryService, private metadataService: MetadataService, private utilityService: UtilityService,
private utilityService: UtilityService, private collectionTagService: CollectionTagService, public toggleService: ToggleService, private collectionTagService: CollectionTagService, public toggleService: ToggleService,
private readonly cdRef: ChangeDetectorRef) { private readonly cdRef: ChangeDetectorRef, private filterUtilitySerivce: FilterUtilitiesService) {
} }
ngOnInit(): void { ngOnInit(): void {
@ -105,7 +105,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
}); });
} }
this.filter = this.seriesService.createSeriesFilter(); this.filter = this.filterUtilitySerivce.createSeriesFilter();
this.readProgressGroup = new FormGroup({ this.readProgressGroup = new FormGroup({
read: new FormControl({value: this.filter.readStatus.read, disabled: this.filterSettings.readProgressDisabled}, []), read: new FormControl({value: this.filter.readStatus.read, disabled: this.filterSettings.readProgressDisabled}, []),
notRead: new FormControl({value: this.filter.readStatus.notRead, disabled: this.filterSettings.readProgressDisabled}, []), notRead: new FormControl({value: this.filter.readStatus.notRead, disabled: this.filterSettings.readProgressDisabled}, []),
@ -601,7 +601,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
} }
clear() { clear() {
this.filter = this.seriesService.createSeriesFilter(); this.filter = this.filterUtilitySerivce.createSeriesFilter();
this.readProgressGroup.get('read')?.setValue(true); this.readProgressGroup.get('read')?.setValue(true);
this.readProgressGroup.get('notRead')?.setValue(true); this.readProgressGroup.get('notRead')?.setValue(true);
this.readProgressGroup.get('inProgress')?.setValue(true); this.readProgressGroup.get('inProgress')?.setValue(true);

View File

@ -18,7 +18,7 @@ import { TypeaheadModule } from '../typeahead/typeahead.module';
NgbRatingModule, NgbRatingModule,
NgbCollapseModule, NgbCollapseModule,
SharedModule, SharedModule,
TypeaheadModule TypeaheadModule,
], ],
exports: [ exports: [
MetadataFilterComponent MetadataFilterComponent

View File

@ -66,7 +66,7 @@
<li class="list-group-item dark-menu-item"> <li class="list-group-item dark-menu-item">
<div class="d-inline-flex"> <div class="d-inline-flex">
<span class="download"> <span class="download">
<app-circular-loader [currentValue]="25" [maxValue]="100" fontSize="16px" [showIcon]="true" width="25px" height="unset" [center]="false"></app-circular-loader> <app-circular-loader [currentValue]="25" fontSize="16px" [showIcon]="true" width="25px" height="unset" [center]="false"></app-circular-loader>
<span class="visually-hidden" role="status"> <span class="visually-hidden" role="status">
10% downloaded 10% downloaded
</span> </span>

View File

@ -97,12 +97,16 @@
</button> </button>
</div> </div>
<div class="col-auto ms-2"> <div class="col-auto ms-2">
<ngb-rating class="rating-star" [(rate)]="series.userRating" (rateChange)="updateRating($event)" (click)="promptToReview()"></ngb-rating> <ngb-rating class="rating-star" [(rate)]="series.userRating" (rateChange)="updateRating($event)" (click)="promptToReview()" [resettable]="false">
<button *ngIf="series.userRating || series.userRating" class="btn btn-sm btn-icon" (click)="openReviewModal(true)" placement="bottom" ngbTooltip="Edit Review" aria-label="Edit Review"><i class="fa fa-pen" aria-hidden="true"></i></button> <ng-template let-fill="fill" let-index="index">
<span class="star" [class.filled]="(index < series.userRating) && series.userRating > 0">&#9733;</span>
</ng-template>
</ngb-rating>
<button *ngIf="series.userRating || series.userReview" class="btn btn-sm btn-icon" (click)="openReviewModal(true)" placement="bottom" ngbTooltip="Edit Review" aria-label="Edit Review"><i class="fa fa-pen" aria-hidden="true"></i></button>
</div> </div>
</div> </div>
<div class="row g-0"> <div class="row g-0">
<!-- TODO: This will be the first of reviews section. Reviews will show your plus other peoples reviews in media cards like Plex does and this will be below metadata --> <!-- TODO: This will be the first of reviews section. Reviews will show your plus other peoples reviews in media cards like Plex does and this will be below metadata. NOTE: We need to clean the reviews in case any html is in there.-->
<app-read-more class="user-review {{userReview ? 'mt-1' : ''}}" [text]="series.userReview || ''" [maxLength]="250"></app-read-more> <app-read-more class="user-review {{userReview ? 'mt-1' : ''}}" [text]="series.userReview || ''" [maxLength]="250"></app-read-more>
</div> </div>
<div *ngIf="seriesMetadata" class="mt-2"> <div *ngIf="seriesMetadata" class="mt-2">

View File

@ -3,7 +3,7 @@ import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { forkJoin, Subject, tap } from 'rxjs'; import { forkJoin, Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators'; import { take, takeUntil } from 'rxjs/operators';
import { BulkSelectionService } from '../cards/bulk-selection.service'; import { BulkSelectionService } from '../cards/bulk-selection.service';
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component'; import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';

View File

@ -12,7 +12,6 @@ import { ReactiveFormsModule } from '@angular/forms';
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module'; import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
SeriesDetailComponent, SeriesDetailComponent,

View File

@ -42,7 +42,7 @@ export enum FilterQueryParam {
}) })
export class FilterUtilitiesService { export class FilterUtilitiesService {
constructor(private route: ActivatedRoute, private seriesService: SeriesService) { } constructor() { }
/** /**
* Updates the window location with a custom url based on filter and pagination objects * Updates the window location with a custom url based on filter and pagination objects
@ -145,7 +145,7 @@ export class FilterUtilitiesService {
* @returns The Preset filter and if something was set within * @returns The Preset filter and if something was set within
*/ */
filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot): [SeriesFilter, boolean] { filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot): [SeriesFilter, boolean] {
const filter = this.seriesService.createSeriesFilter(); const filter = this.createSeriesFilter();
let anyChanged = false; let anyChanged = false;
const format = snapshot.queryParamMap.get(FilterQueryParam.Format); const format = snapshot.queryParamMap.get(FilterQueryParam.Format);
@ -305,4 +305,39 @@ export class FilterUtilitiesService {
return [filter, false]; // anyChanged. Testing out if having a filter active but keep drawer closed by default works better return [filter, false]; // anyChanged. Testing out if having a filter active but keep drawer closed by default works better
} }
createSeriesFilter(filter?: SeriesFilter) {
if (filter !== undefined) return filter;
const data: SeriesFilter = {
formats: [],
libraries: [],
genres: [],
writers: [],
artists: [],
penciller: [],
inker: [],
colorist: [],
letterer: [],
coverArtist: [],
editor: [],
publisher: [],
character: [],
translators: [],
collectionTags: [],
rating: 0,
readStatus: {
read: true,
inProgress: true,
notRead: true
},
sortOptions: null,
ageRating: [],
tags: [],
languages: [],
publicationStatus: [],
seriesNameQuery: '',
};
return data;
}
} }

View File

@ -11,7 +11,7 @@
[space] = "0" [space] = "0"
[backgroundPadding]="0" [backgroundPadding]="0"
outerStrokeLinecap="butt" outerStrokeLinecap="butt"
[outerStrokeColor]="'#4ac694'" [outerStrokeColor]="outerStrokeColor"
[innerStrokeColor]="innerStrokeColor" [innerStrokeColor]="innerStrokeColor"
titleFontSize= "24" titleFontSize= "24"
unitsFontSize= "24" unitsFontSize= "24"
@ -21,7 +21,7 @@
[startFromZero]="false" [startFromZero]="false"
[responsive]="true" [responsive]="true"
[backgroundOpacity]="0.5" [backgroundOpacity]="0.5"
[backgroundColor]="'#000'" [backgroundColor]="backgroundColor"
></circle-progress> ></circle-progress>
</div> </div>
</ng-container> </ng-container>

View File

@ -1,13 +1,14 @@
.number { .number {
position: absolute; position: absolute;
top:50%; top: 50%;
left:50%; left: 50%;
font-size:18px; font-size: 18px;
} }
.indicator { .indicator {
font-weight:500; font-weight: 500;
z-index:10; margin-left: 2px;
z-index: 10;
color: var(--primary-color); color: var(--primary-color);
animation: MoveUpDown 1s linear infinite; animation: MoveUpDown 1s linear infinite;
} }

View File

@ -9,10 +9,23 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
export class CircularLoaderComponent { export class CircularLoaderComponent {
@Input() currentValue: number = 0; @Input() currentValue: number = 0;
@Input() maxValue: number = 0; /**
* If an animation should be used
*/
@Input() animation: boolean = true; @Input() animation: boolean = true;
/**
* Color of an inner bar
*/
@Input() innerStrokeColor: string = 'transparent'; @Input() innerStrokeColor: string = 'transparent';
/**
* Color of the Downloader bar
*/
@Input() outerStrokeColor: string = '#4ac694';
@Input() backgroundColor: string = '#000';
@Input() fontSize: string = '36px'; @Input() fontSize: string = '36px';
/**
* Show the icon inside the downloader
*/
@Input() showIcon: boolean = true; @Input() showIcon: boolean = true;
/** /**
* The width in pixels of the loader * The width in pixels of the loader

View File

@ -85,7 +85,7 @@ export class WantToReadComponent implements OnInit, OnDestroy {
this.seriesPagination = this.filterUtilityService.pagination(this.route.snapshot); this.seriesPagination = this.filterUtilityService.pagination(this.route.snapshot);
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot); [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
this.filterActiveCheck = this.seriesService.createSeriesFilter(); this.filterActiveCheck = this.filterUtilityService.createSeriesFilter();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => { this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => {

View File

@ -14,6 +14,10 @@
<meta name="msapplication-config" content="assets/icons/browserconfig.xml"> <meta name="msapplication-config" content="assets/icons/browserconfig.xml">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
</head> </head>
<body class="mat-typography" theme="dark"> <body class="mat-typography" theme="dark">
<app-root></app-root> <app-root></app-root>

View File

@ -1,5 +1,6 @@
@use '../node_modules/swiper/swiper.scss' as swiper; @use '../node_modules/swiper/swiper.scss' as swiper;
// Import themes which define the css variables we use to customize the app // Import themes which define the css variables we use to customize the app
@import './theme/themes/dark'; @import './theme/themes/dark';

View File

@ -240,4 +240,6 @@
/* List Card Item */ /* List Card Item */
--card-list-item-bg-color: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%); --card-list-item-bg-color: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
} }