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("Tintin - T22 Vol 714 pour Sydney", "Tintin")]
|
||||
[InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")]
|
||||
[InlineData("Kebab Том 1 Глава 1", "Kebab")]
|
||||
[InlineData("Манга Глава 1", "Манга")]
|
||||
public void ParseComicSeriesTest(string filename, string expected)
|
||||
{
|
||||
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("Adventure Time (2012)/Adventure Time #1 (2012)", "0")]
|
||||
[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)
|
||||
{
|
||||
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("Adventure Time (2012)/Adventure Time #1 (2012)", "1")]
|
||||
[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)
|
||||
{
|
||||
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("No Volume", "0")]
|
||||
[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("[WS]_Ichiban_Ushiro_no_Daimaou_v02_ch10.zip", "2")]
|
||||
[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("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")]
|
||||
[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_DKThias_Chapter11v2.zip", "0")]
|
||||
[InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")]
|
||||
@ -73,6 +72,11 @@ public class MangaParserTests
|
||||
[InlineData("시즌34삽화2", "34")]
|
||||
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1巻", "1")]
|
||||
[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)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
|
||||
@ -181,6 +185,10 @@ public class MangaParserTests
|
||||
[InlineData("諌山創] 進撃の巨人 第23巻", "諌山創] 進撃の巨人")]
|
||||
[InlineData("(一般コミック) [奥浩哉] いぬやしき 第09巻", "いぬやしき")]
|
||||
[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)
|
||||
{
|
||||
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("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "0")]
|
||||
[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("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")]
|
||||
[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("Beelzebub_153b_RHS.zip", "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.2", "2")]
|
||||
[InlineData("Transferred to another world magical swordsman v1.1", "0")]
|
||||
[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("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("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")]
|
||||
[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)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
||||
|
@ -162,7 +162,7 @@ public class ParserTests
|
||||
[InlineData("Darker Than_Black", "darkerthanblack")]
|
||||
[InlineData("Citrus", "citrus")]
|
||||
[InlineData("Citrus+", "citrus+")]
|
||||
[InlineData("Again!!!!", "again")]
|
||||
[InlineData("Again", "again")]
|
||||
[InlineData("카비타", "카비타")]
|
||||
[InlineData("06", "06")]
|
||||
[InlineData("", "")]
|
||||
|
@ -115,8 +115,9 @@ public class MetadataController : BaseApiController
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
/// <returns></returns>
|
||||
[HttpGet("languages")]
|
||||
@ -128,15 +129,8 @@ public class MetadataController : BaseApiController
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
|
||||
}
|
||||
|
||||
var englishTag = CultureInfo.GetCultureInfo("en");
|
||||
return Ok(new List<LanguageDto>()
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Title = englishTag.DisplayName,
|
||||
IsoCode = englishTag.IetfLanguageTag
|
||||
}
|
||||
});
|
||||
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync());
|
||||
}
|
||||
|
||||
[HttpGet("all-languages")]
|
||||
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
@ -529,7 +530,7 @@ public class ReaderController : BaseApiController
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("get-bookmarks")]
|
||||
[HttpGet("chapter-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
@ -540,13 +541,15 @@ public class ReaderController : BaseApiController
|
||||
/// <summary>
|
||||
/// Returns a list of all bookmarked pages for a User
|
||||
/// </summary>
|
||||
/// <param name="filterDto">Only supports SeriesNameQuery</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("get-all-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks()
|
||||
[HttpPost("all-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks(FilterDto filterDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
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>
|
||||
@ -629,7 +632,7 @@ public class ReaderController : BaseApiController
|
||||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("get-volume-bookmarks")]
|
||||
[HttpGet("volume-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
@ -642,7 +645,7 @@ public class ReaderController : BaseApiController
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("get-series-bookmarks")]
|
||||
[HttpGet("series-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
|
@ -78,6 +78,8 @@ public class UsersController : BaseApiController
|
||||
AppUserIncludes.UserPreferences);
|
||||
var existingPreferences = user.UserPreferences;
|
||||
|
||||
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||
|
||||
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
|
||||
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
||||
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
|
||||
@ -92,7 +94,6 @@ public class UsersController : BaseApiController
|
||||
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
||||
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
||||
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
||||
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
|
||||
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
|
||||
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
|
||||
|
@ -118,5 +118,4 @@ public class ServerInfoDto
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.5.4</remarks>
|
||||
public bool UsingSeriesRelationships { get; set; }
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Data;
|
||||
using API.DTOs.Theme;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
@ -83,11 +84,11 @@ public class UserPreferencesDto
|
||||
/// </summary>
|
||||
[Required]
|
||||
public ReadingDirection BookReaderReadingDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// UI Site Global Setting: The UI theme the user should use.
|
||||
/// </summary>
|
||||
/// <remarks>Should default to Dark</remarks>
|
||||
[Required]
|
||||
public SiteTheme Theme { get; set; }
|
||||
[Required]
|
||||
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);
|
||||
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync();
|
||||
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders);
|
||||
Library GetLibraryByFolder(string folder);
|
||||
@ -311,6 +312,26 @@ public class LibraryRepository : ILibraryRepository
|
||||
.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)
|
||||
{
|
||||
return _context.Series
|
||||
|
@ -5,12 +5,14 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
@ -44,7 +46,7 @@ public interface IUserRepository
|
||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
|
||||
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<AppUserBookmark> GetBookmarkForPage(int page, int chapterId, int userId);
|
||||
Task<AppUserBookmark> GetBookmarkAsync(int bookmarkId);
|
||||
@ -309,12 +311,63 @@ public class UserRepository : IUserRepository
|
||||
.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)
|
||||
.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)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
@ -59,7 +59,6 @@ public static class ApplicationServiceExtensions
|
||||
services.AddScoped<IEventHub, EventHub>();
|
||||
|
||||
services.AddSqLite(config, env);
|
||||
services.AddLogging(config);
|
||||
services.AddSignalR(opt => opt.EnableDetailedErrors = true);
|
||||
}
|
||||
|
||||
@ -68,18 +67,9 @@ public static class ApplicationServiceExtensions
|
||||
{
|
||||
services.AddDbContext<DataContext>(options =>
|
||||
{
|
||||
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
|
||||
options.UseSqlite("Data source=config/kavita.db");
|
||||
options.EnableDetailedErrors();
|
||||
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)
|
||||
{
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
var isDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker;
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Console()
|
||||
.CreateBootstrapLogger();
|
||||
@ -87,7 +86,8 @@ public class Program
|
||||
await Seed.SeedThemes(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)
|
||||
{
|
||||
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])",
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
@ -67,6 +67,8 @@ public static class Parser
|
||||
private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
private const string Number = @"\d+(\.\d)?";
|
||||
private const string NumberRange = Number + @"(-" + Number + @")?";
|
||||
|
||||
private static readonly Regex[] MangaVolumeRegex = new[]
|
||||
{
|
||||
@ -78,9 +80,10 @@ public static class Parser
|
||||
new Regex(
|
||||
@"(?<Series>.*)(\b|_)(?!\[)(vol\.?)(?<Volume>\d+(-\d+)?)(?!\])",
|
||||
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
|
||||
new Regex(
|
||||
@"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>\d+(-\d+)?)(?!\])",
|
||||
@"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>" + NumberRange + @")(?!\])",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Kodomo no Jikan vol. 10, [dmntsf.net] One Piece - Digital Colored Comics Vol. 20.5-21.5 Ch. 177
|
||||
new Regex(
|
||||
@ -130,10 +133,34 @@ public static class Parser
|
||||
new Regex(
|
||||
@"(?<Volume>\d+(?:(\-)\d+)?)巻",
|
||||
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[]
|
||||
{
|
||||
// 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
|
||||
new Regex(
|
||||
@"(?<Series>.*)(\b|_|-|\s)(?:sp)\d",
|
||||
@ -280,10 +307,27 @@ public static class Parser
|
||||
new Regex(
|
||||
@"(?<Series>.+?)第(?<Volume>\d+(?:(\-)\d+)?)巻",
|
||||
MatchOptions, RegexTimeout),
|
||||
|
||||
};
|
||||
|
||||
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
|
||||
new Regex(
|
||||
@"(?<Series>.+?)\s?(\b|_|-)\s?((vol|tome|t)\.?)(?<Volume>\d+(-\d+)?)",
|
||||
@ -380,6 +424,14 @@ public static class Parser
|
||||
new Regex(
|
||||
@"(?<Volume>\d+(?:(\-)\d+)?)巻",
|
||||
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[]
|
||||
@ -417,11 +469,18 @@ public static class Parser
|
||||
@"^(?<Series>.+?)(?:vol\.?\d+)\s#(?<Chapter>\d+)",
|
||||
MatchOptions,
|
||||
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)
|
||||
new Regex(
|
||||
@"^(?<Series>.+?)(?: (?<Chapter>\d+))",
|
||||
MatchOptions, RegexTimeout),
|
||||
|
||||
// Saga 001 (2012) (Digital) (Empire-Zone)
|
||||
new Regex(
|
||||
@"(?<Series>.+?)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)\s\(\d{4}",
|
||||
@ -438,7 +497,6 @@ public static class Parser
|
||||
new Regex(
|
||||
@"^(?<Series>.+?)-(chapter-)?(?<Chapter>\d+)",
|
||||
MatchOptions, RegexTimeout),
|
||||
|
||||
};
|
||||
|
||||
private static readonly Regex[] ReleaseGroupRegex = new[]
|
||||
@ -459,7 +517,7 @@ public static class Parser
|
||||
MatchOptions, RegexTimeout),
|
||||
// [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
|
||||
new Regex(
|
||||
@"v\d+\.(?<Chapter>\d+(?:.\d+|-\d+)?)",
|
||||
@"v\d+\.(\s|_)(?<Chapter>\d+(?:.\d+|-\d+)?)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz (Rare case, if causes issue remove)
|
||||
new Regex(
|
||||
@ -469,6 +527,10 @@ public static class Parser
|
||||
new Regex(
|
||||
@"^(?!Vol)(?<Series>.*)\s?(?<!vol\. )\sChapter\s(?<Chapter>\d+(?:\.?[\d-]+)?)",
|
||||
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
|
||||
new Regex(
|
||||
@"^(?!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),
|
||||
// Korean Chapter: 第10話 -> Chapter n, [ハレム]ナナとカオル ~高校生のSMごっこ~ 第1話
|
||||
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),
|
||||
};
|
||||
|
||||
private static readonly Regex[] MangaEditionRegex = {
|
||||
// Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
|
||||
new Regex(
|
||||
@ -760,12 +827,10 @@ public static class Parser
|
||||
var matches = regex.Matches(filename);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
if (match.Groups["Chapter"].Success && match.Groups["Chapter"] != Match.Empty)
|
||||
{
|
||||
var value = match.Groups["Chapter"].Value;
|
||||
var hasPart = match.Groups["Part"].Success;
|
||||
return FormatValue(value, hasPart);
|
||||
}
|
||||
if (!match.Groups["Chapter"].Success || match.Groups["Chapter"] == Match.Empty) continue;
|
||||
var value = match.Groups["Chapter"].Value;
|
||||
var hasPart = match.Groups["Part"].Success;
|
||||
return FormatValue(value, hasPart);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
@ -18,12 +17,10 @@ using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Hangfire.MemoryStorage;
|
||||
using Hangfire.Storage.SQLite;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -193,8 +190,8 @@ public class Startup
|
||||
|
||||
await MigrateRemoveExtraThemes.Migrate(unitOfWork, themeService);
|
||||
|
||||
// Only needed for v0.5.5.x and v0.5.6
|
||||
await MigrateNormalizedLocalizedName.Migrate(unitOfWork, dataContext, logger);
|
||||
// only needed for v0.5.4 and v0.6.0
|
||||
await MigrateNormalizedEverything.Migrate(unitOfWork, dataContext, logger);
|
||||
|
||||
// Update the version in the DB after all migrations are run
|
||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
|
@ -1,7 +1,4 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Data source=config//kavita.db"
|
||||
},
|
||||
"TokenKey": "super secret unguessable key",
|
||||
"Port": 5000
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Data source=config/kavita.db"
|
||||
},
|
||||
"TokenKey": "super secret unguessable key",
|
||||
"Port": 5000
|
||||
}
|
||||
|
@ -29,13 +29,6 @@ public static class Configuration
|
||||
set => SetJwtToken(GetAppSettingFilename(), value);
|
||||
}
|
||||
|
||||
|
||||
public static string DatabasePath
|
||||
{
|
||||
get => GetDatabasePath(GetAppSettingFilename());
|
||||
set => SetDatabasePath(GetAppSettingFilename(), value);
|
||||
}
|
||||
|
||||
private static string GetAppSettingFilename()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(AppSettingsFilename))
|
||||
@ -191,52 +184,4 @@ public static class Configuration
|
||||
/* 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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6
UI/Web/package-lock.json
generated
6
UI/Web/package-lock.json
generated
@ -12650,9 +12650,9 @@
|
||||
}
|
||||
},
|
||||
"ngx-extended-pdf-viewer": {
|
||||
"version": "14.5.3",
|
||||
"resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-14.5.3.tgz",
|
||||
"integrity": "sha512-9pqnbonKcu/6SIwPe3yCfHzsO1fgO7qIwETHD7UuS2kAG5GM7VkEwrqMoF7qsZ0Lq/rkqFBcGsS4GYW5JK+oEQ==",
|
||||
"version": "15.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-15.0.2.tgz",
|
||||
"integrity": "sha512-3cuJ87hqod8b/DiIjLNCYxLZYkfi+bm0PsjMFw4GnGfjKB7QJv0p/+KvrCdD68k18Aim5Sd5BMZhF2pHelp1mw==",
|
||||
"requires": {
|
||||
"lodash.deburr": "^4.1.0",
|
||||
"tslib": "^2.3.0"
|
||||
|
@ -39,7 +39,7 @@
|
||||
"lazysizes": "^5.3.2",
|
||||
"ng-circle-progress": "^1.6.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-infinite-scroll": "^13.0.2",
|
||||
"ngx-toastr": "^14.2.1",
|
||||
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
|
||||
import { Chapter } from '../_models/chapter';
|
||||
import { CollectionTag } from '../_models/collection-tag';
|
||||
import { Library } from '../_models/library';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
import { ReadingList } from '../_models/reading-list';
|
||||
import { Series } from '../_models/series';
|
||||
import { Volume } from '../_models/volume';
|
||||
@ -271,13 +270,13 @@ export class ActionFactoryService {
|
||||
action: Action.MarkAsRead,
|
||||
title: 'Mark as Read',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false
|
||||
requiresAdmin: false
|
||||
},
|
||||
{
|
||||
action: Action.MarkAsUnread,
|
||||
title: 'Mark as Unread',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false
|
||||
requiresAdmin: false
|
||||
},
|
||||
{
|
||||
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 { Location } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
@ -10,6 +10,9 @@ import { MangaFormat } from '../_models/manga-format';
|
||||
import { BookmarkInfo } from '../_models/manga-reader/bookmark-info';
|
||||
import { PageBookmark } from '../_models/page-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_NOT_FETCHED = -2;
|
||||
@ -24,7 +27,9 @@ export class ReaderService {
|
||||
// Override background color for reader and restore it onDestroy
|
||||
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) {
|
||||
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});
|
||||
}
|
||||
|
||||
getAllBookmarks() {
|
||||
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-all-bookmarks');
|
||||
getAllBookmarks(filter: SeriesFilter | undefined) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
|
@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||
import { UtilityService } from '../shared/_services/utility.service';
|
||||
import { Chapter } from '../_models/chapter';
|
||||
import { ChapterMetadata } from '../_models/chapter-metadata';
|
||||
@ -26,12 +27,13 @@ export class SeriesService {
|
||||
paginatedResults: 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) {
|
||||
let params = new HttpParams();
|
||||
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(
|
||||
map((response: any) => {
|
||||
@ -43,7 +45,7 @@ export class SeriesService {
|
||||
getSeriesForLibrary(libraryId: number, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||
let params = new HttpParams();
|
||||
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(
|
||||
map((response: any) => {
|
||||
@ -109,7 +111,7 @@ export class SeriesService {
|
||||
}
|
||||
|
||||
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();
|
||||
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
|
||||
@ -125,7 +127,7 @@ export class SeriesService {
|
||||
}
|
||||
|
||||
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();
|
||||
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
@ -137,7 +139,7 @@ export class SeriesService {
|
||||
}
|
||||
|
||||
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();
|
||||
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
@ -204,41 +206,4 @@ export class SeriesService {
|
||||
getSeriesDetail(seriesId: number) {
|
||||
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 { Action } from '../_services/action-factory.service';
|
||||
import { ActionService } from '../_services/action.service';
|
||||
import { LibraryService } from '../_services/library.service';
|
||||
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
|
||||
import { SeriesService } from '../_services/series.service';
|
||||
|
||||
@ -86,14 +85,14 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
||||
private titleService: Title, private actionService: ActionService,
|
||||
public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||
private utilityService: UtilityService, private route: ActivatedRoute,
|
||||
private filterUtilityService: FilterUtilitiesService, private libraryService: LibraryService) {
|
||||
private filterUtilityService: FilterUtilitiesService) {
|
||||
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.titleService.setTitle('Kavita - All Series');
|
||||
|
||||
this.pagination = this.filterUtilityService.pagination(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 {
|
||||
|
@ -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>
|
||||
Bookmarks
|
||||
</h2>
|
||||
@ -8,9 +8,10 @@
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingBookmarks"
|
||||
[items]="series"
|
||||
[filterSettings]="filterSettings"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[refresh]="refresh"
|
||||
|
||||
(applyFilter)="updateFilter($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<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 { Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take, Subject } from 'rxjs';
|
||||
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 { 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 { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||
import { Pagination } from 'src/app/_models/pagination';
|
||||
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 { ImageService } from 'src/app/_services/image.service';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
@ -29,6 +33,13 @@ export class BookmarksComponent implements OnInit, OnDestroy {
|
||||
clearingSeries: {[id: number]: boolean} = {};
|
||||
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}`;
|
||||
refresh: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
@ -38,12 +49,25 @@ export class BookmarksComponent implements OnInit, OnDestroy {
|
||||
private downloadService: DownloadService, private toastr: ToastrService,
|
||||
private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService,
|
||||
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 {
|
||||
this.loadBookmarks();
|
||||
|
||||
this.actions = this.actionFactoryService.getBookmarkActions(this.handleAction.bind(this));
|
||||
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@ -111,9 +135,15 @@ export class BookmarksComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
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.cdRef.markForCheck();
|
||||
this.readerService.getAllBookmarks().pipe(take(1)).subscribe(bookmarks => {
|
||||
|
||||
this.readerService.getAllBookmarks(this.filter).pipe(take(1)).subscribe(bookmarks => {
|
||||
this.bookmarks = bookmarks;
|
||||
this.seriesIds = {};
|
||||
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 { ActivatedRoute, NavigationStart, Router } from '@angular/router';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NavigationStart, Router } from '@angular/router';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
|
||||
@ -23,7 +23,6 @@ export class BulkSelectionService {
|
||||
private selectedCards: { [key: string]: {[key: number]: boolean} } = {};
|
||||
private dataSourceMax: { [key: string]: number} = {};
|
||||
public isShiftDown: boolean = false;
|
||||
private activeRoute: string = '';
|
||||
|
||||
private actionsSource = new ReplaySubject<ActionItem<any>[]>(1);
|
||||
public actions$ = this.actionsSource.asObservable();
|
||||
@ -34,14 +33,13 @@ export class BulkSelectionService {
|
||||
*/
|
||||
public selections$ = this.selectionsSource.asObservable();
|
||||
|
||||
constructor(private router: Router, private actionFactory: ActionFactoryService, private route: ActivatedRoute) {
|
||||
constructor(router: Router, private actionFactory: ActionFactoryService) {
|
||||
router.events
|
||||
.pipe(filter(event => event instanceof NavigationStart))
|
||||
.subscribe((event) => {
|
||||
.subscribe(() => {
|
||||
this.deselectAll();
|
||||
this.dataSourceMax = {};
|
||||
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.selectCards(dataSource, this.prevIndex, index, !wasSelected);
|
||||
} else {
|
||||
const isForwardSelection = index < this.prevIndex;
|
||||
const isForwardSelection = index > this.prevIndex;
|
||||
|
||||
if (isForwardSelection) {
|
||||
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 { Subject } from 'rxjs';
|
||||
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 { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
@ -71,10 +72,10 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
||||
constructor(private seriesService: SeriesService, public utilityService: UtilityService,
|
||||
constructor(private filterUtilitySerivce: FilterUtilitiesService, public utilityService: UtilityService,
|
||||
@Inject(DOCUMENT) private document: Document, private changeDetectionRef: ChangeDetectorRef,
|
||||
private jumpbarService: JumpbarService, private router: Router) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.filter = this.filterUtilitySerivce.createSeriesFilter();
|
||||
this.changeDetectionRef.markForCheck();
|
||||
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ import { DownloadIndicatorComponent } from './download-indicator/download-indica
|
||||
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CardItemComponent,
|
||||
@ -68,7 +67,6 @@ import { DownloadIndicatorComponent } from './download-indicator/download-indica
|
||||
|
||||
VirtualScrollerModule,
|
||||
|
||||
|
||||
NgbOffcanvasModule, // Series Detail, action of cards
|
||||
NgbNavModule, //Series Detail
|
||||
NgbPaginationModule, // EditCollectionTagsComponent
|
||||
|
@ -31,7 +31,7 @@
|
||||
<ng-container>
|
||||
<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">
|
||||
<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}}
|
||||
</app-icon-and-title>
|
||||
</ng-container>
|
||||
|
@ -140,7 +140,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy, AfterConten
|
||||
this.seriesPagination = this.filterUtilityService.pagination(this.route.snapshot);
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
|
||||
this.filterSettings.presets.collectionTags = [tagId];
|
||||
this.filterActiveCheck = this.seriesService.createSeriesFilter();
|
||||
this.filterActiveCheck = this.filterUtilityService.createSeriesFilter();
|
||||
this.filterActiveCheck.collectionTags = [tagId];
|
||||
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);
|
||||
if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId];
|
||||
// Setup filterActiveCheck to check filter against
|
||||
this.filterActiveCheck = this.seriesService.createSeriesFilter();
|
||||
this.filterActiveCheck = this.filterUtilityService.createSeriesFilter();
|
||||
this.filterActiveCheck.libraries = [this.libraryId];
|
||||
|
||||
this.filterSettings.libraryDisabled = true;
|
||||
@ -230,7 +230,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
loadPage() {
|
||||
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
|
||||
if (this.filter == undefined) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.filter = this.filterUtilityService.createSeriesFilter();
|
||||
this.filter.libraries.push(this.libraryId);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
@ -468,7 +468,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
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.scalingOption = this.user.preferences.scalingOption;
|
||||
this.pageSplitOption = this.user.preferences.pageSplitOption;
|
||||
|
@ -99,7 +99,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="cover-artist" class="form-label">Cover Artists</label>
|
||||
<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">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
@ -114,7 +114,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="writers" class="form-label">Writers</label>
|
||||
<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">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
@ -129,7 +129,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="publisher" class="form-label">Publisher</label>
|
||||
<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">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
@ -144,7 +144,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="penciller" class="form-label">Penciller</label>
|
||||
<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">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
@ -159,7 +159,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="letterer" class="form-label">Letterer</label>
|
||||
<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">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
@ -174,7 +174,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="inker" class="form-label">Inker</label>
|
||||
<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">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
@ -189,7 +189,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="editor" class="form-label">Editor</label>
|
||||
<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">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
@ -204,7 +204,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="colorist" class="form-label">Colorist</label>
|
||||
<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">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
@ -219,7 +219,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="character" class="form-label">Character</label>
|
||||
<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">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
@ -234,7 +234,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="translators" class="form-label">Translators</label>
|
||||
<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">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
|
@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Ev
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||
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 { TypeaheadSettings } from '../typeahead/typeahead-settings';
|
||||
import { CollectionTag } from '../_models/collection-tag';
|
||||
@ -17,7 +18,6 @@ import { Tag } from '../_models/tag';
|
||||
import { CollectionTagService } from '../_services/collection-tag.service';
|
||||
import { LibraryService } from '../_services/library.service';
|
||||
import { MetadataService } from '../_services/metadata.service';
|
||||
import { SeriesService } from '../_services/series.service';
|
||||
import { ToggleService } from '../_services/toggle.service';
|
||||
import { FilterSettings } from './filter-settings';
|
||||
|
||||
@ -86,9 +86,9 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
return SortField;
|
||||
}
|
||||
|
||||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
|
||||
private utilityService: UtilityService, private collectionTagService: CollectionTagService, public toggleService: ToggleService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private utilityService: UtilityService,
|
||||
private collectionTagService: CollectionTagService, public toggleService: ToggleService,
|
||||
private readonly cdRef: ChangeDetectorRef, private filterUtilitySerivce: FilterUtilitiesService) {
|
||||
}
|
||||
|
||||
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({
|
||||
read: new FormControl({value: this.filter.readStatus.read, 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() {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.filter = this.filterUtilitySerivce.createSeriesFilter();
|
||||
this.readProgressGroup.get('read')?.setValue(true);
|
||||
this.readProgressGroup.get('notRead')?.setValue(true);
|
||||
this.readProgressGroup.get('inProgress')?.setValue(true);
|
||||
|
@ -18,7 +18,7 @@ import { TypeaheadModule } from '../typeahead/typeahead.module';
|
||||
NgbRatingModule,
|
||||
NgbCollapseModule,
|
||||
SharedModule,
|
||||
TypeaheadModule
|
||||
TypeaheadModule,
|
||||
],
|
||||
exports: [
|
||||
MetadataFilterComponent
|
||||
|
@ -66,7 +66,7 @@
|
||||
<li class="list-group-item dark-menu-item">
|
||||
<div class="d-inline-flex">
|
||||
<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">
|
||||
10% downloaded
|
||||
</span>
|
||||
|
@ -97,12 +97,16 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto ms-2">
|
||||
<ngb-rating class="rating-star" [(rate)]="series.userRating" (rateChange)="updateRating($event)" (click)="promptToReview()"></ngb-rating>
|
||||
<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>
|
||||
<ngb-rating class="rating-star" [(rate)]="series.userRating" (rateChange)="updateRating($event)" (click)="promptToReview()" [resettable]="false">
|
||||
<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 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>
|
||||
</div>
|
||||
<div *ngIf="seriesMetadata" class="mt-2">
|
||||
|
@ -3,7 +3,7 @@ import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { forkJoin, Subject, tap } from 'rxjs';
|
||||
import { forkJoin, Subject } from 'rxjs';
|
||||
import { take, takeUntil } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||
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';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
SeriesDetailComponent,
|
||||
|
@ -42,7 +42,7 @@ export enum FilterQueryParam {
|
||||
})
|
||||
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
|
||||
@ -145,7 +145,7 @@ export class FilterUtilitiesService {
|
||||
* @returns The Preset filter and if something was set within
|
||||
*/
|
||||
filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot): [SeriesFilter, boolean] {
|
||||
const filter = this.seriesService.createSeriesFilter();
|
||||
const filter = this.createSeriesFilter();
|
||||
let anyChanged = false;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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"
|
||||
[backgroundPadding]="0"
|
||||
outerStrokeLinecap="butt"
|
||||
[outerStrokeColor]="'#4ac694'"
|
||||
[outerStrokeColor]="outerStrokeColor"
|
||||
[innerStrokeColor]="innerStrokeColor"
|
||||
titleFontSize= "24"
|
||||
unitsFontSize= "24"
|
||||
@ -21,7 +21,7 @@
|
||||
[startFromZero]="false"
|
||||
[responsive]="true"
|
||||
[backgroundOpacity]="0.5"
|
||||
[backgroundColor]="'#000'"
|
||||
[backgroundColor]="backgroundColor"
|
||||
></circle-progress>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -1,13 +1,14 @@
|
||||
.number {
|
||||
position: absolute;
|
||||
top:50%;
|
||||
left:50%;
|
||||
font-size:18px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
font-weight:500;
|
||||
z-index:10;
|
||||
font-weight: 500;
|
||||
margin-left: 2px;
|
||||
z-index: 10;
|
||||
color: var(--primary-color);
|
||||
animation: MoveUpDown 1s linear infinite;
|
||||
}
|
@ -9,10 +9,23 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
export class CircularLoaderComponent {
|
||||
|
||||
@Input() currentValue: number = 0;
|
||||
@Input() maxValue: number = 0;
|
||||
/**
|
||||
* If an animation should be used
|
||||
*/
|
||||
@Input() animation: boolean = true;
|
||||
/**
|
||||
* Color of an inner bar
|
||||
*/
|
||||
@Input() innerStrokeColor: string = 'transparent';
|
||||
/**
|
||||
* Color of the Downloader bar
|
||||
*/
|
||||
@Input() outerStrokeColor: string = '#4ac694';
|
||||
@Input() backgroundColor: string = '#000';
|
||||
@Input() fontSize: string = '36px';
|
||||
/**
|
||||
* Show the icon inside the downloader
|
||||
*/
|
||||
@Input() showIcon: boolean = true;
|
||||
/**
|
||||
* 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.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.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => {
|
||||
|
@ -14,6 +14,10 @@
|
||||
<meta name="msapplication-config" content="assets/icons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<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>
|
||||
<body class="mat-typography" theme="dark">
|
||||
<app-root></app-root>
|
||||
|
@ -1,5 +1,6 @@
|
||||
@use '../node_modules/swiper/swiper.scss' as swiper;
|
||||
|
||||
|
||||
// Import themes which define the css variables we use to customize the app
|
||||
@import './theme/themes/dark';
|
||||
|
||||
|
@ -240,4 +240,6 @@
|
||||
/* 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%);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user