mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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:
parent
b7d88f08d8
commit
00f0ad5a3f
@ -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));
|
||||||
|
@ -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));
|
||||||
|
@ -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("", "")]
|
||||||
|
@ -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")]
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
|
@ -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");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
120
API/Data/MigrateNormalizedEverything.cs
Normal file
120
API/Data/MigrateNormalizedEverything.cs
Normal 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");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,4 +21,4 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
6
UI/Web/package-lock.json
generated
6
UI/Web/package-lock.json
generated
@ -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"
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)"
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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]);
|
||||||
|
@ -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();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -18,7 +18,7 @@ import { TypeaheadModule } from '../typeahead/typeahead.module';
|
|||||||
NgbRatingModule,
|
NgbRatingModule,
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
TypeaheadModule
|
TypeaheadModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
MetadataFilterComponent
|
MetadataFilterComponent
|
||||||
|
@ -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>
|
||||||
|
@ -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">★</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">
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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%);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user