diff --git a/API.Tests/Parser/ComicParserTests.cs b/API.Tests/Parser/ComicParserTests.cs index 90d325fa1..9a2c576c9 100644 --- a/API.Tests/Parser/ComicParserTests.cs +++ b/API.Tests/Parser/ComicParserTests.cs @@ -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)); diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index e2aa78c6e..c482bcabd 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -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)); diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs index df5e0c2d7..6760d41d5 100644 --- a/API.Tests/Parser/ParserTest.cs +++ b/API.Tests/Parser/ParserTest.cs @@ -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("", "")] diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index 7aee25c30..ffc0afa64 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -115,8 +115,9 @@ public class MetadataController : BaseApiController } /// - /// Fetches all age ratings from the instance + /// Fetches all age languages from the libraries passed (or if none passed, all in the server) /// + /// This does not perform RBS for the user if they have Library access due to the non-sensitive nature of languages /// String separated libraryIds or null for all ratings /// [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() - { - new () - { - Title = englishTag.DisplayName, - IsoCode = englishTag.IetfLanguageTag - } - }); + + return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync()); } [HttpGet("all-languages")] diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 110b1a2a1..2fa6f22e4 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -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 /// /// /// - [HttpGet("get-bookmarks")] + [HttpGet("chapter-bookmarks")] public async Task>> GetBookmarks(int chapterId) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); @@ -540,13 +541,15 @@ public class ReaderController : BaseApiController /// /// Returns a list of all bookmarked pages for a User /// + /// Only supports SeriesNameQuery /// - [HttpGet("get-all-bookmarks")] - public async Task>> GetAllBookmarks() + [HttpPost("all-bookmarks")] + public async Task>> GetAllBookmarks(FilterDto filterDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); if (user.Bookmarks == null) return Ok(Array.Empty()); - return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id)); + + return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id, filterDto)); } /// @@ -629,7 +632,7 @@ public class ReaderController : BaseApiController /// /// /// - [HttpGet("get-volume-bookmarks")] + [HttpGet("volume-bookmarks")] public async Task>> GetBookmarksForVolume(int volumeId) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); @@ -642,7 +645,7 @@ public class ReaderController : BaseApiController /// /// /// - [HttpGet("get-series-bookmarks")] + [HttpGet("series-bookmarks")] public async Task>> GetBookmarksForSeries(int seriesId) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 43011af63..58c3f4828 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -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; diff --git a/API/DTOs/Stats/ServerInfoDto.cs b/API/DTOs/Stats/ServerInfoDto.cs index ecfce3a16..1d56c02a1 100644 --- a/API/DTOs/Stats/ServerInfoDto.cs +++ b/API/DTOs/Stats/ServerInfoDto.cs @@ -118,5 +118,4 @@ public class ServerInfoDto /// /// Introduced in v0.5.4 public bool UsingSeriesRelationships { get; set; } - } diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 804f0b2ef..2ec3a79bb 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -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 /// [Required] public ReadingDirection BookReaderReadingDirection { get; set; } + /// /// UI Site Global Setting: The UI theme the user should use. /// /// Should default to Dark - [Required] public SiteTheme Theme { get; set; } [Required] public string BookReaderThemeName { get; set; } diff --git a/API/Data/MigrateCoverImages.cs b/API/Data/MigrateCoverImages.cs deleted file mode 100644 index faf473edf..000000000 --- a/API/Data/MigrateCoverImages.cs +++ /dev/null @@ -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; - -/// -/// A data structure to migrate Cover Images from byte[] to files. -/// -internal class CoverMigration -{ - public string Id { get; set; } - public byte[] CoverImage { get; set; } - public string ParentId { get; set; } -} - -/// -/// In v0.4.6, Cover Images were migrated from byte[] in the DB to external files. This migration handles that work. -/// -public static class MigrateCoverImages -{ - private static readonly ChapterSortComparerZeroFirst ChapterSortComparerForInChapterSorting = new (); - - /// - /// Run first. Will extract byte[]s from DB and write them to the cover directory. - /// - 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); - } - } - } - - /// - /// Run after . Will update the DB with names of files that were extracted. - /// - /// - 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"); - } - -} diff --git a/API/Data/MigrateNormalizedEverything.cs b/API/Data/MigrateNormalizedEverything.cs new file mode 100644 index 000000000..b6abdcba7 --- /dev/null +++ b/API/Data/MigrateNormalizedEverything.cs @@ -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; + +/// +/// v0.6.0 introduced a change in how Normalization works and hence every normalized field needs to be re-calculated +/// +public static class MigrateNormalizedEverything +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger 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"); + + } + +} diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index b967cece8..410c3b81b 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -44,6 +44,7 @@ public interface ILibraryRepository IEnumerable GetJumpBarAsync(int libraryId); Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds); Task> GetAllLanguagesForLibrariesAsync(List libraryIds); + Task> GetAllLanguagesForLibrariesAsync(); IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds); Task DoAnySeriesFoldersMatch(IEnumerable folders); Library GetLibraryByFolder(string folder); @@ -311,6 +312,26 @@ public class LibraryRepository : ILibraryRepository .ToList(); } + public async Task> 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 GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds) { return _context.Series diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index e02f414f4..fdfccf95f 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -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> GetBookmarkDtosForSeries(int userId, int seriesId); Task> GetBookmarkDtosForVolume(int userId, int volumeId); Task> GetBookmarkDtosForChapter(int userId, int chapterId); - Task> GetAllBookmarkDtos(int userId); + Task> GetAllBookmarkDtos(int userId, FilterDto filter); Task> GetAllBookmarksAsync(); Task GetBookmarkForPage(int page, int chapterId, int userId); Task GetBookmarkAsync(int bookmarkId); @@ -309,12 +311,63 @@ public class UserRepository : IUserRepository .ToListAsync(); } - public async Task> GetAllBookmarkDtos(int userId) + /// + /// Get all bookmarks for the user + /// + /// + /// Only supports SeriesNameQuery + /// + public async Task> 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(_mapper.ConfigurationProvider) .ToListAsync(); } diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index c7836f057..e11e74142 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -59,7 +59,6 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddSqLite(config, env); - services.AddLogging(config); services.AddSignalR(opt => opt.EnableDetailedErrors = true); } @@ -68,18 +67,9 @@ public static class ApplicationServiceExtensions { services.AddDbContext(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); - }); - } } diff --git a/API/Program.cs b/API/Program.cs index 69527caef..4afecbced 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -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()).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()).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"); diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 15e809d02..07f515724 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -58,7 +58,7 @@ public static class Parser private static readonly Regex CoverImageRegex = new Regex(@"(? @@ -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( @"(?.*)(\b|_)(?!\[)(vol\.?)(?\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( - @"(?.*)(\b|_)(?!\[)v(?\d+(-\d+)?)(?!\])", + @"(?.*)(\b|_)(?!\[)v(?" + 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( @"(?\d+(?:(\-)\d+)?)巻", MatchOptions, RegexTimeout), + // Russian Volume: Том n -> Volume n, Тома n -> Volume + new Regex( + @"Том(а?)(\.?)(\s|_)?(?\d+(?:(\-)\d+)?)", + MatchOptions, RegexTimeout), + // Russian Volume: n Том -> Volume n + new Regex( + @"(\s|_)?(?\d+(?:(\-)\d+)?)(\s|_)Том(а?)", + MatchOptions, RegexTimeout), }; private static readonly Regex[] MangaSeriesRegex = new[] { + // Russian Volume: Том n -> Volume n, Тома n -> Volume + new Regex( + @"(?.+?)Том(а?)(\.?)(\s|_)?(?\d+(?:(\-)\d+)?)", + MatchOptions, RegexTimeout), + // Russian Volume: n Том -> Volume n + new Regex( + @"(?.+?)(\s|_)?(?\d+(?:(\-)\d+)?)(\s|_)Том(а?)", + MatchOptions, RegexTimeout), + // Russian Chapter: n Главa -> Chapter n + new Regex( + @"(?.+?)(?!Том)(?\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)", + MatchOptions, RegexTimeout), + // Russian Chapter: Главы n -> Chapter n + new Regex( + @"(?.+?)(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?\d+(?:.\d+|-\d+)?)", + MatchOptions, RegexTimeout), // Grand Blue Dreaming - SP02 new Regex( @"(?.*)(\b|_|-|\s)(?:sp)\d", @@ -280,10 +307,27 @@ public static class Parser new Regex( @"(?.+?)第(?\d+(?:(\-)\d+)?)巻", MatchOptions, RegexTimeout), + }; private static readonly Regex[] ComicSeriesRegex = new[] { + // Russian Volume: Том n -> Volume n, Тома n -> Volume + new Regex( + @"(?.+?)Том(а?)(\.?)(\s|_)?(?\d+(?:(\-)\d+)?)", + MatchOptions, RegexTimeout), + // Russian Volume: n Том -> Volume n + new Regex( + @"(?.+?)(\s|_)?(?\d+(?:(\-)\d+)?)(\s|_)Том(а?)", + MatchOptions, RegexTimeout), + // Russian Chapter: n Главa -> Chapter n + new Regex( + @"(?.+?)(?!Том)(?\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)", + MatchOptions, RegexTimeout), + // Russian Chapter: Главы n -> Chapter n + new Regex( + @"(?.+?)(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?\d+(?:.\d+|-\d+)?)", + MatchOptions, RegexTimeout), // Tintin - T22 Vol 714 pour Sydney new Regex( @"(?.+?)\s?(\b|_|-)\s?((vol|tome|t)\.?)(?\d+(-\d+)?)", @@ -380,6 +424,14 @@ public static class Parser new Regex( @"(?\d+(?:(\-)\d+)?)巻", MatchOptions, RegexTimeout), + // Russian Volume: Том n -> Volume n, Тома n -> Volume + new Regex( + @"Том(а?)(\.?)(\s|_)?(?\d+(?:(\-)\d+)?)", + MatchOptions, RegexTimeout), + // Russian Volume: n Том -> Volume n + new Regex( + @"(\s|_)?(?\d+(?:(\-)\d+)?)(\s|_)Том(а?)", + MatchOptions, RegexTimeout), }; private static readonly Regex[] ComicChapterRegex = new[] @@ -417,11 +469,18 @@ public static class Parser @"^(?.+?)(?:vol\.?\d+)\s#(?\d+)", MatchOptions, RegexTimeout), + // Russian Chapter: Главы n -> Chapter n + new Regex( + @"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?\d+(?:.\d+|-\d+)?)", + MatchOptions, RegexTimeout), + // Russian Chapter: n Главa -> Chapter n + new Regex( + @"(?!Том)(?\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( @"^(?.+?)(?: (?\d+))", MatchOptions, RegexTimeout), - // Saga 001 (2012) (Digital) (Empire-Zone) new Regex( @"(?.+?)(?: |_)(c? ?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)\s\(\d{4}", @@ -438,7 +497,6 @@ public static class Parser new Regex( @"^(?.+?)-(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+\.(?\d+(?:.\d+|-\d+)?)", + @"v\d+\.(\s|_)(?\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)(?.*)\s?(?\d+(?:\.?[\d-]+)?)", MatchOptions, RegexTimeout), + // Russian Chapter: Главы n -> Chapter n + new Regex( + @"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?\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)(?.+?)(?\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( - @"第?(?\d+(?:.\d+|-\d+)?)話", + @"第?(?\d+(?:\.\d+|-\d+)?)話", + MatchOptions, RegexTimeout), + // Russian Chapter: n Главa -> Chapter n + new Regex( + @"(?!Том)(?\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); } } diff --git a/API/Startup.cs b/API/Startup.cs index 277613af8..905322159 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -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); diff --git a/API/config/appsettings.Development.json b/API/config/appsettings.Development.json index 2e7b8afc6..2bb2debc0 100644 --- a/API/config/appsettings.Development.json +++ b/API/config/appsettings.Development.json @@ -1,7 +1,4 @@ { - "ConnectionStrings": { - "DefaultConnection": "Data source=config//kavita.db" - }, "TokenKey": "super secret unguessable key", "Port": 5000 } diff --git a/API/config/appsettings.json b/API/config/appsettings.json index a2d7fb053..be6c0b319 100644 --- a/API/config/appsettings.json +++ b/API/config/appsettings.json @@ -1,7 +1,4 @@ { - "ConnectionStrings": { - "DefaultConnection": "Data source=config/kavita.db" - }, "TokenKey": "super secret unguessable key", "Port": 5000 } diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index e626f56cc..5457c4e7b 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -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(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; - } - - /// - /// This should NEVER be called except by MigrateConfigFiles - /// - /// - /// - 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 */ - } - } } diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 3b9a6de21..8882331f8 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -21,4 +21,4 @@ - \ No newline at end of file + diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 0c437f9ba..66b2b2840 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -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" diff --git a/UI/Web/package.json b/UI/Web/package.json index 53a664d47..a71a77182 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -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", diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 6b38dbaa4..8ed905324 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -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, diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 516d04c9e..ddabc0ab8 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -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(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(this.baseUrl + 'reader/all-bookmarks', data); } getBookmarks(chapterId: number) { - return this.httpClient.get(this.baseUrl + 'reader/get-bookmarks?chapterId=' + chapterId); + return this.httpClient.get(this.baseUrl + 'reader/chapter-bookmarks?chapterId=' + chapterId); } getBookmarksForVolume(volumeId: number) { - return this.httpClient.get(this.baseUrl + 'reader/get-volume-bookmarks?volumeId=' + volumeId); + return this.httpClient.get(this.baseUrl + 'reader/volume-bookmarks?volumeId=' + volumeId); } getBookmarksForSeries(seriesId: number) { - return this.httpClient.get(this.baseUrl + 'reader/get-series-bookmarks?seriesId=' + seriesId); + return this.httpClient.get(this.baseUrl + 'reader/series-bookmarks?seriesId=' + seriesId); } clearBookmarks(seriesId: number) { diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index cc9c4ef60..a5a5f26c7 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -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 = new PaginatedResult(); paginatedSeriesForTagsResults: PaginatedResult = new PaginatedResult(); - 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>(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>(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> { - 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(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; - } } diff --git a/UI/Web/src/app/all-series/all-series.component.ts b/UI/Web/src/app/all-series/all-series.component.ts index 57ca454cd..541feac3a 100644 --- a/UI/Web/src/app/all-series/all-series.component.ts +++ b/UI/Web/src/app/all-series/all-series.component.ts @@ -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 { diff --git a/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html index 943c73077..a43e90936 100644 --- a/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html +++ b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html @@ -1,4 +1,4 @@ - +

Bookmarks

@@ -8,9 +8,10 @@ [] = []; + pagination!: Pagination; + filter: SeriesFilter | undefined = undefined; + filterSettings: FilterSettings = new FilterSettings(); + filterOpen: EventEmitter = new EventEmitter(); + filterActive: boolean = false; + filterActiveCheck!: SeriesFilter; + trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`; refresh: EventEmitter = 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(); + } + } diff --git a/UI/Web/src/app/cards/bulk-selection.service.ts b/UI/Web/src/app/cards/bulk-selection.service.ts index 326aba869..c6bf26e5f 100644 --- a/UI/Web/src/app/cards/bulk-selection.service.ts +++ b/UI/Web/src/app/cards/bulk-selection.service.ts @@ -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[]>(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]); diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index 2326b4521..e86cd8b4f 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -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(); } diff --git a/UI/Web/src/app/cards/cards.module.ts b/UI/Web/src/app/cards/cards.module.ts index 87c4f36ff..4b86e1fbb 100644 --- a/UI/Web/src/app/cards/cards.module.ts +++ b/UI/Web/src/app/cards/cards.module.ts @@ -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 diff --git a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html index d38ead742..8c8ecc8c1 100644 --- a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html +++ b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html @@ -31,7 +31,7 @@
- + {{pubStatus}} diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts index abf55b550..8c5b270b5 100644 --- a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts @@ -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(); diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index eb62bacae..9d28d516f 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -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(); } diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index f5ef247bf..ab0512e63 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -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; diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index 8138e8152..7284b8c47 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -99,7 +99,7 @@
+ [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.CoverArtist) || filterSettings.peopleDisabled"> {{item.name}} @@ -114,7 +114,7 @@
+ [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Writer) || filterSettings.peopleDisabled"> {{item.name}} @@ -129,7 +129,7 @@
+ [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Publisher) || filterSettings.peopleDisabled"> {{item.name}} @@ -144,7 +144,7 @@
+ [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Penciller) || filterSettings.peopleDisabled"> {{item.name}} @@ -159,7 +159,7 @@
+ [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Letterer) || filterSettings.peopleDisabled"> {{item.name}} @@ -174,7 +174,7 @@
+ [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Inker) || filterSettings.peopleDisabled"> {{item.name}} @@ -189,7 +189,7 @@
+ [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Editor) || filterSettings.peopleDisabled"> {{item.name}} @@ -204,7 +204,7 @@
+ [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Colorist) || filterSettings.peopleDisabled"> {{item.name}} @@ -219,7 +219,7 @@
+ [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Character) || filterSettings.peopleDisabled"> {{item.name}} @@ -234,7 +234,7 @@
+ [reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Translator) || filterSettings.peopleDisabled"> {{item.name}} diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts index 0ebeade4e..8579ef60d 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts @@ -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); diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.module.ts b/UI/Web/src/app/metadata-filter/metadata-filter.module.ts index 5cea6bdc8..bf2582104 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.module.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.module.ts @@ -18,7 +18,7 @@ import { TypeaheadModule } from '../typeahead/typeahead.module'; NgbRatingModule, NgbCollapseModule, SharedModule, - TypeaheadModule + TypeaheadModule, ], exports: [ MetadataFilterComponent diff --git a/UI/Web/src/app/nav/events-widget/events-widget.component.html b/UI/Web/src/app/nav/events-widget/events-widget.component.html index 6b2f99984..a456943bb 100644 --- a/UI/Web/src/app/nav/events-widget/events-widget.component.html +++ b/UI/Web/src/app/nav/events-widget/events-widget.component.html @@ -66,7 +66,7 @@
  • - + 10% downloaded diff --git a/UI/Web/src/app/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/series-detail.component.html index ddc18f591..e43fcbd76 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/series-detail.component.html @@ -97,12 +97,16 @@
    - - + + + + + +
  • - +
    diff --git a/UI/Web/src/app/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/series-detail.component.ts index 16f59d084..41c6bdc4d 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -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'; diff --git a/UI/Web/src/app/series-detail/series-detail.module.ts b/UI/Web/src/app/series-detail/series-detail.module.ts index 975942e2d..e0ae26dd5 100644 --- a/UI/Web/src/app/series-detail/series-detail.module.ts +++ b/UI/Web/src/app/series-detail/series-detail.module.ts @@ -12,7 +12,6 @@ import { ReactiveFormsModule } from '@angular/forms'; import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module'; - @NgModule({ declarations: [ SeriesDetailComponent, diff --git a/UI/Web/src/app/shared/_services/filter-utilities.service.ts b/UI/Web/src/app/shared/_services/filter-utilities.service.ts index 865a8ddd2..2fc6b144d 100644 --- a/UI/Web/src/app/shared/_services/filter-utilities.service.ts +++ b/UI/Web/src/app/shared/_services/filter-utilities.service.ts @@ -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; + } } diff --git a/UI/Web/src/app/shared/circular-loader/circular-loader.component.html b/UI/Web/src/app/shared/circular-loader/circular-loader.component.html index b79c748e7..b703d6a86 100644 --- a/UI/Web/src/app/shared/circular-loader/circular-loader.component.html +++ b/UI/Web/src/app/shared/circular-loader/circular-loader.component.html @@ -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" >
    diff --git a/UI/Web/src/app/shared/circular-loader/circular-loader.component.scss b/UI/Web/src/app/shared/circular-loader/circular-loader.component.scss index d4cabba66..817949cd8 100644 --- a/UI/Web/src/app/shared/circular-loader/circular-loader.component.scss +++ b/UI/Web/src/app/shared/circular-loader/circular-loader.component.scss @@ -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; } \ No newline at end of file diff --git a/UI/Web/src/app/shared/circular-loader/circular-loader.component.ts b/UI/Web/src/app/shared/circular-loader/circular-loader.component.ts index f43e93891..9e630b608 100644 --- a/UI/Web/src/app/shared/circular-loader/circular-loader.component.ts +++ b/UI/Web/src/app/shared/circular-loader/circular-loader.component.ts @@ -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 diff --git a/UI/Web/src/app/want-to-read/want-to-read/want-to-read.component.ts b/UI/Web/src/app/want-to-read/want-to-read/want-to-read.component.ts index b873c97e4..7d38a4788 100644 --- a/UI/Web/src/app/want-to-read/want-to-read/want-to-read.component.ts +++ b/UI/Web/src/app/want-to-read/want-to-read/want-to-read.component.ts @@ -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) => { diff --git a/UI/Web/src/index.html b/UI/Web/src/index.html index ced9095a7..992957479 100644 --- a/UI/Web/src/index.html +++ b/UI/Web/src/index.html @@ -14,6 +14,10 @@ + + + + diff --git a/UI/Web/src/styles.scss b/UI/Web/src/styles.scss index dea14a147..9220be28b 100644 --- a/UI/Web/src/styles.scss +++ b/UI/Web/src/styles.scss @@ -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'; diff --git a/UI/Web/src/theme/themes/dark.scss b/UI/Web/src/theme/themes/dark.scss index 706d54aee..06ae21b39 100644 --- a/UI/Web/src/theme/themes/dark.scss +++ b/UI/Web/src/theme/themes/dark.scss @@ -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%); + + }