diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index 5848c74ba..6037e0715 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/API.Tests/Services/BookServiceTests.cs @@ -1,10 +1,12 @@ using System.IO; using System.IO.Abstractions; +using System.Threading.Tasks; using API.Entities.Enums; using API.Services; using API.Services.Tasks.Scanner.Parser; using Microsoft.Extensions.Logging; using NSubstitute; +using VersOne.Epub; using Xunit; namespace API.Tests.Services; @@ -142,4 +144,19 @@ public class BookServiceTests Assert.Equal(parserInfo.Title, comicInfo.Title); Assert.Equal(parserInfo.Series, comicInfo.Title); } + + /// + /// Tests that the ./ rewrite hack works as expected + /// + [Fact] + public async Task ShouldBeAbleToLookUpImage() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var filePath = Path.Join(testDirectory, "Relative Key Test File.epub"); + + var result = await _bookService.GetResourceAsync(filePath, "./images/titlepage800.png"); + + Assert.True(result.IsSuccess); + Assert.Equal("image/png", result.ContentType); + } } diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index db310d957..ed082cbed 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -633,7 +633,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest await readerService.MarkChaptersAsRead(user, s.Id, new List() {c}); await unitOfWork.CommitAsync(); - var chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); + var chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id, 1); await unitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); Assert.NotNull(chapter); @@ -644,7 +644,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest unitOfWork.ChapterRepository.Update(c); await unitOfWork.CommitAsync(); - chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); + chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id, 1); await unitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); Assert.NotNull(chapter); Assert.Equal(2, chapter.PagesRead); @@ -655,7 +655,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest new DirectoryService(Substitute.For>(), new MockFileSystem())); await cleanupService.EnsureChapterProgressIsCapped(); - chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); + chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id, 1); await unitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); Assert.NotNull(chapter); diff --git a/API.Tests/Services/SiteThemeServiceTests.cs b/API.Tests/Services/SiteThemeServiceTests.cs index c936b8d89..327d82060 100644 --- a/API.Tests/Services/SiteThemeServiceTests.cs +++ b/API.Tests/Services/SiteThemeServiceTests.cs @@ -33,7 +33,7 @@ public class SiteThemeServiceTest(ITestOutputHelper outputHelper): AbstractDbTes var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var siteThemeService = new ThemeService(ds, unitOfWork, _messageHub, Substitute.For(), + var siteThemeService = new ThemeService(ds, unitOfWork, _messageHub, Substitute.For>(), Substitute.For()); context.SiteTheme.Add(new SiteTheme() @@ -62,7 +62,7 @@ public class SiteThemeServiceTest(ITestOutputHelper outputHelper): AbstractDbTes var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var siteThemeService = new ThemeService(ds, unitOfWork, _messageHub, Substitute.For(), + var siteThemeService = new ThemeService(ds, unitOfWork, _messageHub, Substitute.For>(), Substitute.For()); context.SiteTheme.Add(new SiteTheme() @@ -91,7 +91,7 @@ public class SiteThemeServiceTest(ITestOutputHelper outputHelper): AbstractDbTes var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var siteThemeService = new ThemeService(ds, unitOfWork, _messageHub, Substitute.For(), + var siteThemeService = new ThemeService(ds, unitOfWork, _messageHub, Substitute.For>(), Substitute.For()); context.SiteTheme.Add(new SiteTheme() diff --git a/API.Tests/Services/Test Data/BookService/Relative Key Test File.epub b/API.Tests/Services/Test Data/BookService/Relative Key Test File.epub new file mode 100644 index 000000000..2b58afec1 Binary files /dev/null and b/API.Tests/Services/Test Data/BookService/Relative Key Test File.epub differ diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 0b0a17160..2277f438c 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -17,7 +16,6 @@ using API.Errors; using API.Extensions; using API.Helpers.Builders; using API.Services; -using API.Services.Plus; using API.SignalR; using AutoMapper; using Hangfire; @@ -29,7 +27,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SharpCompress; namespace API.Controllers; @@ -53,7 +50,6 @@ public class AccountController : BaseApiController private readonly IEmailService _emailService; private readonly IEventHub _eventHub; private readonly ILocalizationService _localizationService; - private readonly IOidcService _oidcService; /// public AccountController(UserManager userManager, @@ -62,8 +58,7 @@ public class AccountController : BaseApiController ILogger logger, IMapper mapper, IAccountService accountService, IEmailService emailService, IEventHub eventHub, - ILocalizationService localizationService, - IOidcService oidcService) + ILocalizationService localizationService) { _userManager = userManager; _signInManager = signInManager; @@ -75,7 +70,6 @@ public class AccountController : BaseApiController _emailService = emailService; _eventHub = eventHub; _localizationService = localizationService; - _oidcService = oidcService; } /// @@ -197,11 +191,7 @@ public class AccountController : BaseApiController var result = await _userManager.CreateAsync(user, registerDto.Password); if (!result.Succeeded) return BadRequest(result.Errors); - // Assign default streams - _accountService.AddDefaultStreamsToUser(user); - - // Assign default reading profile - await _accountService.AddDefaultReadingProfileToUser(user); + await _accountService.SeedUser(user); var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen")); @@ -534,6 +524,11 @@ public class AccountController : BaseApiController return Ok(); } + /// + /// Change the Age Rating restriction for the user + /// + /// + /// [HttpPost("update/age-restriction")] public async Task UpdateAgeRestriction(UpdateAgeRestrictionDto dto) { @@ -745,11 +740,7 @@ public class AccountController : BaseApiController var result = await _userManager.CreateAsync(user, AccountService.DefaultPassword); if (!result.Succeeded) return BadRequest(result.Errors); - // Assign default streams - _accountService.AddDefaultStreamsToUser(user); - - // Assign default reading profile - await _accountService.AddDefaultReadingProfileToUser(user); + await _accountService.SeedUser(user); // Assign Roles var roles = dto.Roles; diff --git a/API/Controllers/AnnotationController.cs b/API/Controllers/AnnotationController.cs index f6fcfbb87..5da784dc3 100644 --- a/API/Controllers/AnnotationController.cs +++ b/API/Controllers/AnnotationController.cs @@ -45,6 +45,17 @@ public class AnnotationController : BaseApiController return Ok(await _unitOfWork.UserRepository.GetAnnotations(User.GetUserId(), chapterId)); } + /// + /// Returns all annotations by Series + /// + /// + /// + [HttpGet("all-for-series")] + public async Task> GetAnnotationsBySeries(int seriesId) + { + return Ok(await _unitOfWork.UserRepository.GetAnnotationDtosBySeries(User.GetUserId(), seriesId)); + } + /// /// Returns the Annotation by Id. User must have access to annotation. /// diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index 2fcf8567a..c41ad4077 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -111,19 +111,16 @@ public class BookController : BaseApiController public async Task GetBookPageResources(int chapterId, [FromQuery] string file) { if (chapterId <= 0) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist")); - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + + var chapter = await _cacheService.Ensure(chapterId); if (chapter == null) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist")); - using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.LenientBookReaderOptions); - var key = BookService.CoalesceKeyForAnyFile(book, file); + var cachedFilePath = Path.Join(_cacheService.GetCachePath(chapterId), Path.GetFileName(chapter.Files.ElementAt(0).FilePath)); + var result = await _bookService.GetResourceAsync(cachedFilePath, file); - if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest(await _localizationService.Get("en", "file-missing")); + if (!result.IsSuccess) return BadRequest(await _localizationService.Translate(User.GetUserId(), result.ErrorMessage)); - var bookFile = book.Content.AllFiles.GetLocalFileRefByKey(key); - var content = await bookFile.ReadContentAsBytesAsync(); - - var contentType = BookService.GetContentType(bookFile.ContentType); - return File(content, contentType, $"{chapterId}-{file}"); + return File(result.Content, result.ContentType, $"{chapterId}-{file}"); } /// diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs index 94535d499..4ddbd86a0 100644 --- a/API/Controllers/ChapterController.cs +++ b/API/Controllers/ChapterController.cs @@ -51,9 +51,7 @@ public class ChapterController : BaseApiController [HttpGet] public async Task> GetChapter(int chapterId) { - var chapter = - await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, - ChapterIncludes.People | ChapterIncludes.Files); + var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, User.GetUserId()); return Ok(chapter); } diff --git a/API/Controllers/ColorScapeController.cs b/API/Controllers/ColorScapeController.cs index 04827658d..bc9293b7a 100644 --- a/API/Controllers/ColorScapeController.cs +++ b/API/Controllers/ColorScapeController.cs @@ -50,7 +50,7 @@ public class ColorScapeController : BaseApiController [HttpGet("chapter")] public async Task> GetColorScapeForChapter(int id) { - var entity = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(id); + var entity = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(id, User.GetUserId()); return GetColorSpaceDto(entity); } diff --git a/API/Controllers/FallbackController.cs b/API/Controllers/FallbackController.cs index 0c925476f..9aff82027 100644 --- a/API/Controllers/FallbackController.cs +++ b/API/Controllers/FallbackController.cs @@ -20,8 +20,13 @@ public class FallbackController : Controller _taskScheduler = taskScheduler; } - public PhysicalFileResult Index() + public IActionResult Index() { + if (HttpContext.Request.Path.StartsWithSegments("/api")) + { + return NotFound(); + } + return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML"); } } diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 0fd9c437a..e59620340 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -18,6 +18,7 @@ using API.DTOs.Filtering.v2; using API.DTOs.OPDS; using API.DTOs.Person; using API.DTOs.Progress; +using API.DTOs.ReadingLists; using API.DTOs.Search; using API.Entities; using API.Entities.Enums; @@ -105,33 +106,32 @@ public class OpdsController : BaseApiController private readonly XmlSerializer _xmlSerializer; private readonly XmlSerializer _xmlOpenSearchSerializer; - private readonly FilterDto _filterDto = new FilterDto() + private readonly FilterDto _filterDto = new() { - Formats = new List(), - Character = new List(), - Colorist = new List(), - Editor = new List(), - Genres = new List(), - Inker = new List(), - Languages = new List(), - Letterer = new List(), - Penciller = new List(), - Libraries = new List(), - Publisher = new List(), + Formats = [], + Character = [], + Colorist = [], + Editor = [], + Genres = [], + Inker = [], + Languages = [], + Letterer = [], + Penciller = [], + Libraries = [], + Publisher = [], Rating = 0, - Tags = new List(), - Translators = new List(), - Writers = new List(), - AgeRating = new List(), - CollectionTags = new List(), - CoverArtist = new List(), + Tags = [], + Translators = [], + Writers = [], + AgeRating = [], + CollectionTags = [], + CoverArtist = [], ReadStatus = new ReadStatus(), SortOptions = null, - PublicationStatus = new List() + PublicationStatus = [] }; - private readonly FilterV2Dto _filterV2Dto = new FilterV2Dto(); - private readonly ChapterSortComparerDefaultLast _chapterSortComparerDefaultLast = ChapterSortComparerDefaultLast.Default; + private readonly FilterV2Dto _filterV2Dto = new(); private const int PageSize = 20; public const string UserId = nameof(UserId); @@ -187,10 +187,10 @@ public class OpdsController : BaseApiController { Text = await _localizationService.Translate(userId, "browse-on-deck") }, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/on-deck"), - } + ] }); break; case DashboardStreamType.NewlyAdded: @@ -202,10 +202,10 @@ public class OpdsController : BaseApiController { Text = await _localizationService.Translate(userId, "browse-recently-added") }, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/recently-added"), - } + ] }); break; case DashboardStreamType.RecentlyUpdated: @@ -217,10 +217,10 @@ public class OpdsController : BaseApiController { Text = await _localizationService.Translate(userId, "browse-recently-updated") }, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/recently-updated"), - } + ] }); break; case DashboardStreamType.MoreInGenre: @@ -235,10 +235,10 @@ public class OpdsController : BaseApiController { Text = await _localizationService.Translate(userId, "browse-more-in-genre", randomGenre.Title) }, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/more-in-genre?genreId={randomGenre.Id}"), - } + ] }); break; case DashboardStreamType.SmartFilter: @@ -269,10 +269,10 @@ public class OpdsController : BaseApiController { Text = await _localizationService.Translate(userId, "browse-reading-lists") }, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/reading-list"), - } + ] }); feed.Entries.Add(new FeedEntry() { @@ -282,10 +282,10 @@ public class OpdsController : BaseApiController { Text = await _localizationService.Translate(userId, "browse-want-to-read") }, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/want-to-read"), - } + ] }); feed.Entries.Add(new FeedEntry() { @@ -295,10 +295,10 @@ public class OpdsController : BaseApiController { Text = await _localizationService.Translate(userId, "browse-libraries") }, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/libraries"), - } + ] }); feed.Entries.Add(new FeedEntry() { @@ -308,10 +308,10 @@ public class OpdsController : BaseApiController { Text = await _localizationService.Translate(userId, "browse-collections") }, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections"), - } + ] }); if ((_unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(userId)).Any()) @@ -324,30 +324,13 @@ public class OpdsController : BaseApiController { Text = await _localizationService.Translate(userId, "browse-smart-filters") }, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/smart-filters"), - } + ] }); } - // if ((await _unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId)).Any()) - // { - // feed.Entries.Add(new FeedEntry() - // { - // Id = "allExternalSources", - // Title = await _localizationService.Translate(userId, "external-sources"), - // Content = new FeedEntryContent() - // { - // Text = await _localizationService.Translate(userId, "browse-external-sources") - // }, - // Links = new List() - // { - // CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/external-sources"), - // } - // }); - // } - return CreateXmlResult(SerializeXml(feed)); } @@ -396,12 +379,12 @@ public class OpdsController : BaseApiController [HttpGet("{apiKey}/smart-filters")] [Produces("application/xml")] - public async Task GetSmartFilters(string apiKey) + public async Task GetSmartFilters(string apiKey, [FromQuery] int pageNumber = 0) { var userId = GetUserIdFromContext(); var (_, prefix) = await GetPrefix(); - var filters = _unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(userId); + var filters = await _unitOfWork.AppUserSmartFilterRepository.GetPagedDtosByUserIdAsync(userId, GetUserParams(pageNumber)); var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters"), $"{apiKey}/smart-filters", apiKey, prefix); SetFeedId(feed, "smartFilters"); @@ -419,40 +402,10 @@ public class OpdsController : BaseApiController }); } + AddPagination(feed, filters, $"{prefix}{apiKey}/smart-filters"); return CreateXmlResult(SerializeXml(feed)); } - [HttpGet("{apiKey}/external-sources")] - [Produces("application/xml")] - public async Task GetExternalSources(string apiKey) - { - // NOTE: This doesn't seem possible in OPDS v2.1 due to the resulting stream using relative links and most apps resolve against source url. Even using full paths doesn't work - var userId = GetUserIdFromContext(); - var (_, prefix) = await GetPrefix(); - - var externalSources = await _unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId); - var feed = CreateFeed(await _localizationService.Translate(userId, "external-sources"), $"{apiKey}/external-sources", apiKey, prefix); - SetFeedId(feed, "externalSources"); - foreach (var externalSource in externalSources) - { - var opdsUrl = $"{externalSource.Host}api/opds/{externalSource.ApiKey}"; - feed.Entries.Add(new FeedEntry() - { - Id = externalSource.Id.ToString(), - Title = externalSource.Name, - Summary = externalSource.Host, - Links = new List() - { - CreateLink(FeedLinkRelation.Start, FeedLinkType.AtomNavigation, opdsUrl), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{opdsUrl}/favicon") - } - }); - } - - return CreateXmlResult(SerializeXml(feed)); - } - - [HttpGet("{apiKey}/libraries")] [Produces("application/xml")] public async Task GetLibraries(string apiKey) @@ -463,7 +416,7 @@ public class OpdsController : BaseApiController SetFeedId(feed, "libraries"); // Ensure libraries follow SideNav order - var userSideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreams(userId, false); + var userSideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreams(userId); foreach (var library in userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library).Select(sideNavStream => sideNavStream.Library)) { feed.Entries.Add(new FeedEntry() @@ -506,14 +459,14 @@ public class OpdsController : BaseApiController [HttpGet("{apiKey}/collections")] [Produces("application/xml")] - public async Task GetCollections(string apiKey) + public async Task GetCollections(string apiKey, [FromQuery] int pageNumber = 0) { var userId = GetUserIdFromContext(); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null) return Unauthorized(); - var tags = await _unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(user.Id, true); + var tags = await _unitOfWork.CollectionTagRepository.GetCollectionDtosPagedAsync(user.Id, GetUserParams(pageNumber), true); var (baseUrl, prefix) = await GetPrefix(); var feed = CreateFeed(await _localizationService.Translate(userId, "collections"), $"{apiKey}/collections", apiKey, prefix); @@ -536,6 +489,7 @@ public class OpdsController : BaseApiController ] })); + AddPagination(feed, tags, $"{prefix}{apiKey}/collections"); return CreateXmlResult(SerializeXml(feed)); } @@ -609,14 +563,6 @@ public class OpdsController : BaseApiController return CreateXmlResult(SerializeXml(feed)); } - private static UserParams GetUserParams(int pageNumber) - { - return new UserParams() - { - PageNumber = pageNumber, - PageSize = PageSize - }; - } [HttpGet("{apiKey}/reading-list/{readingListId}")] [Produces("application/xml")] @@ -645,13 +591,22 @@ public class OpdsController : BaseApiController var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{apiKey}/reading-list/{readingListId}", apiKey, prefix); SetFeedId(feed, $"reading-list-{readingListId}"); - var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId); + + var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId, GetUserParams(pageNumber))).ToList(); + + // Check if there is reading progress or not, if so, inject a "continue-reading" item + var firstReadReadingListItem = items.FirstOrDefault(i => i.PagesRead > 0); + if (firstReadReadingListItem != null) + { + await AddContinueReadingPoint(apiKey, firstReadReadingListItem, userId, feed, prefix, baseUrl); + } + foreach (var item in items) { - var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(item.ChapterId); + var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(item.ChapterId, userId); // If there is only one file underneath, add a direct acquisition link, otherwise add a subsection - if (chapterDto != null && chapterDto.Files.Count == 1) + if (chapterDto is {Files.Count: 1}) { var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(item.SeriesId, userId); feed.Entries.Add(await CreateChapterWithFile(userId, item.SeriesId, item.VolumeId, item.ChapterId, @@ -668,6 +623,29 @@ public class OpdsController : BaseApiController return CreateXmlResult(SerializeXml(feed)); } + private async Task AddContinueReadingPoint(string apiKey, int seriesId, ChapterDto chapterDto, int userId, + Feed feed, string prefix, string baseUrl) + { + var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + if (chapterDto is {Files.Count: 1}) + { + feed.Entries.Add(await CreateContinueReadingFromFile(userId, seriesId, chapterDto.VolumeId, chapterDto.Id, + chapterDto.Files.First(), series!, chapterDto, apiKey, prefix, baseUrl)); + } + } + + private async Task AddContinueReadingPoint(string apiKey, ReadingListItemDto firstReadReadingListItem, int userId, + Feed feed, string prefix, string baseUrl) + { + var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(firstReadReadingListItem.ChapterId, userId); + var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(firstReadReadingListItem.SeriesId, userId); + if (chapterDto is {Files.Count: 1}) + { + feed.Entries.Add(await CreateContinueReadingFromFile(userId, firstReadReadingListItem.SeriesId, firstReadReadingListItem.VolumeId, firstReadReadingListItem.ChapterId, + chapterDto.Files.First(), series!, chapterDto, apiKey, prefix, baseUrl)); + } + } + [HttpGet("{apiKey}/libraries/{libraryId}")] [Produces("application/xml")] public async Task GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = 0) @@ -684,14 +662,14 @@ public class OpdsController : BaseApiController var filter = new FilterV2Dto { - Statements = new List() { + Statements = [ new () { Comparison = FilterComparison.Equal, Field = FilterField.Libraries, Value = libraryId + string.Empty } - } + ] }; var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(pageNumber), filter); @@ -735,6 +713,7 @@ public class OpdsController : BaseApiController var userId = GetUserIdFromContext(); var (baseUrl, prefix) = await GetPrefix(); var genre = await _unitOfWork.GenreRepository.GetGenreById(genreId); + if (genre == null) return BadRequest(await _localizationService.Translate(userId, "genre-doesnt-exist")); var seriesDtos = await _unitOfWork.SeriesRepository.GetMoreIn(userId, 0, genreId, GetUserParams(pageNumber)); var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.Id)); @@ -750,6 +729,13 @@ public class OpdsController : BaseApiController return CreateXmlResult(SerializeXml(feed)); } + /// + /// Returns recently updated series. While pagination is avaible, total amount of pages is not due to implementation + /// details + /// + /// + /// + /// [HttpGet("{apiKey}/recently-updated")] [Produces("application/xml")] public async Task GetRecentlyUpdated(string apiKey, [FromQuery] int pageNumber = 1) @@ -832,7 +818,7 @@ public class OpdsController : BaseApiController return BadRequest(await _localizationService.Translate(userId, "query-required")); } query = query.Replace(@"%", string.Empty); - // Get libraries user has access to + var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList(); if (libraries.Count == 0) return BadRequest(await _localizationService.Translate(userId, "libraries-restricted")); @@ -855,15 +841,15 @@ public class OpdsController : BaseApiController Id = collection.Id.ToString(), Title = collection.Title, Summary = collection.Summary, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections/{collection.Id}"), CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionId={collection.Id}&apiKey={apiKey}"), CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionId={collection.Id}&apiKey={apiKey}") - } + ] }); } @@ -874,14 +860,14 @@ public class OpdsController : BaseApiController Id = readingListDto.Id.ToString(), Title = readingListDto.Title, Summary = readingListDto.Summary, - Links = new List() - { + Links = + [ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/reading-list/{readingListDto.Id}"), - } + ] }); } - // TODO: Search should allow Chapters/Files and more + feed.Total = feed.Entries.Count; return CreateXmlResult(SerializeXml(feed)); } @@ -926,20 +912,30 @@ public class OpdsController : BaseApiController SetFeedId(feed, $"series-{series.Id}"); feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesId}&apiKey={apiKey}")); + + // Check if there is reading progress or not, if so, inject a "continue-reading" item + var anyUserProgress = await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId); + if (anyUserProgress) + { + var chapterDto = await _readerService.GetContinuePoint(seriesId, userId); + await AddContinueReadingPoint(apiKey, seriesId, chapterDto, userId, feed, prefix, baseUrl); + } + + var chapterDict = new Dictionary(); var fileDict = new Dictionary(); + var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId); foreach (var volume in seriesDetail.Volumes) { - var chaptersForVolume = await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id, ChapterIncludes.Files | ChapterIncludes.People); + var chaptersForVolume = await _unitOfWork.ChapterRepository.GetChapterDtosAsync(volume.Id, userId); - foreach (var chapter in chaptersForVolume) + foreach (var chapterDto in chaptersForVolume) { - var chapterId = chapter.Id; + var chapterId = chapterDto.Id; if (!chapterDict.TryAdd(chapterId, 0)) continue; - var chapterDto = _mapper.Map(chapter); - foreach (var mangaFile in chapter.Files) + foreach (var mangaFile in chapterDto.Files) { // If a chapter has multiple files that are within one chapter, this dict prevents duplicate key exception if (!fileDict.TryAdd(mangaFile.Id, 0)) continue; @@ -999,25 +995,31 @@ public class OpdsController : BaseApiController return NotFound(); } - var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId, VolumeIncludes.Chapters); if (volume == null) { return NotFound(); } - var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ", + var feed = CreateFeed($"{series.Name} - Volume {volume!.Name}", $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix); - SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{_seriesService.FormatChapterName(userId, libraryType)}s"); + SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}"); - foreach (var chapterId in volume.Chapters.Select(c => c.Id)) + var chapterDtos = await _unitOfWork.ChapterRepository.GetChapterDtoByIdsAsync(volume.Chapters.Select(c => c.Id), userId); + + // Check if there is reading progress or not, if so, inject a "continue-reading" item + var firstChapterWithProgress = chapterDtos.FirstOrDefault(c => c.PagesRead > 0); + if (firstChapterWithProgress != null) { - var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, ChapterIncludes.Files | ChapterIncludes.People); - if (chapterDto == null) continue; + var chapterDto = await _readerService.GetContinuePoint(seriesId, userId); + await AddContinueReadingPoint(apiKey, seriesId, chapterDto, userId, feed, prefix, baseUrl); + } + foreach (var chapterDto in chapterDtos) + { foreach (var mangaFile in chapterDto.Files) { - feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapterId, mangaFile, series, chapterDto!, apiKey, prefix, baseUrl)); + feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapterDto.Id, mangaFile, series, chapterDto!, apiKey, prefix, baseUrl)); } } @@ -1032,14 +1034,17 @@ public class OpdsController : BaseApiController var (baseUrl, prefix) = await GetPrefix(); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + if (series == null) return BadRequest(await _localizationService.Translate(userId, "series-doesnt-exist")); + var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, ChapterIncludes.Files | ChapterIncludes.People); + var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, userId); if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "chapter-doesnt-exist")); var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId); - var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s", + var chapterName = await _seriesService.FormatChapterName(userId, libraryType); + var feed = CreateFeed( $"{series.Name} - Volume {volume!.Name} - {chapterName} {chapterId}", $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey, prefix); SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{_seriesService.FormatChapterName(userId, libraryType)}-{chapterId}-files"); @@ -1197,6 +1202,17 @@ public class OpdsController : BaseApiController }; } + private async Task CreateContinueReadingFromFile(int userId, int seriesId, int volumeId, int chapterId, + MangaFileDto mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl) + { + var entry = await CreateChapterWithFile(userId, seriesId, volumeId, chapterId, mangaFile, series, chapter, + apiKey, prefix, baseUrl); + + entry.Title = await _localizationService.Translate(userId, "opds-continue-reading-title", entry.Title); + + return entry; + } + private async Task CreateChapterWithFile(int userId, int seriesId, int volumeId, int chapterId, MangaFileDto mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl) { @@ -1216,6 +1232,7 @@ public class OpdsController : BaseApiController { var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty); SeriesService.RenameVolumeName(volume, libraryType, volumeLabel); + if (!volume.IsLooseLeaf()) { title += $" - {volume.Name}"; @@ -1231,10 +1248,10 @@ public class OpdsController : BaseApiController } // Chunky requires a file at the end. Our API ignores this - var accLink = - CreateLink(FeedLinkRelation.Acquisition, fileType, + var accLink = CreateLink(FeedLinkRelation.Acquisition, fileType, $"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}", filename); + accLink.TotalPages = chapter.Pages; var entry = new FeedEntry() @@ -1269,6 +1286,9 @@ public class OpdsController : BaseApiController entry.Links.Add(await CreatePageStreamLink(series.LibraryId, seriesId, volumeId, chapterId, mangaFile, apiKey, prefix)); } + // Patch in reading status on the item (as OPDS is seriously lacking) + entry.Title = $"{GetReadingProgressIcon(chapter.PagesRead, chapter.Pages)} {entry.Title}"; + return entry; } @@ -1306,12 +1326,27 @@ public class OpdsController : BaseApiController // Save progress for the user (except Panels, they will use a direct connection) var userAgent = Request.Headers["User-Agent"].ToString(); + + + if (!userAgent.StartsWith("Panels", StringComparison.InvariantCultureIgnoreCase) || !saveProgress) { + // Kavita expects 0-N for progress, KOReader doesn't respect the OPDS-PS spec and does some wierd stuff + // https://github.com/Kareadita/Kavita/pull/4014#issuecomment-3313677492 + var koreaderOffset = 0; + if (userAgent.StartsWith("Koreader", StringComparison.InvariantCultureIgnoreCase)) + { + var totalPages = await _unitOfWork.ChapterRepository.GetChapterTotalPagesAsync(chapterId); + if (totalPages - pageNumber < 2) + { + koreaderOffset = 1; + } + } + await _readerService.SaveReadingProgress(new ProgressDto() { ChapterId = chapterId, - PageNum = pageNumber, + PageNum = pageNumber + koreaderOffset, SeriesId = seriesId, VolumeId = volumeId, LibraryId =libraryId @@ -1322,7 +1357,7 @@ public class OpdsController : BaseApiController } catch (Exception) { - _cacheService.CleanupChapters(new []{ chapterId }); + _cacheService.CleanupChapters([chapterId]); throw; } } @@ -1334,6 +1369,7 @@ public class OpdsController : BaseApiController var userId = GetUserIdFromContext(); var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico"); if (files.Length == 0) return BadRequest(await _localizationService.Translate(userId, "favicon-doesnt-exist")); + var path = files[0]; var content = await _directoryService.ReadFileAsync(path); var format = Path.GetExtension(path); @@ -1452,8 +1488,35 @@ public class OpdsController : BaseApiController } } + private static UserParams GetUserParams(int pageNumber) + { + return new UserParams() + { + PageNumber = pageNumber, + PageSize = PageSize + }; + } + private static string RemoveInvalidXmlChars(string input) { return new string(input.Where(XmlConvert.IsXmlChar).ToArray()); } + + private static string GetReadingProgressIcon(int pagesRead, int totalPages) + { + if (pagesRead == 0) return "⭘"; + + var percentageRead = (double)pagesRead / totalPages; + + return percentageRead switch + { + // 100% + >= 1.0 => "⬤", + // > 50% and < 100% + > 0.5 => "◕", + // > 25% and <= 50% + > 0.25 => "◑", + _ => "◔" + }; + } } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 09790c17b..6281c2888 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -516,7 +516,7 @@ public class ReaderController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); if (user == null) return Unauthorized(); - user.Progresses ??= new List(); + user.Progresses ??= []; var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); foreach (var volume in volumes) @@ -566,9 +566,11 @@ public class ReaderController : BaseApiController public async Task SaveProgress(ProgressDto progressDto) { var userId = User.GetUserId(); - if (!await _readerService.SaveReadingProgress(progressDto, userId)) - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress")); + if (!await _readerService.SaveReadingProgress(progressDto, userId)) + { + return BadRequest(await _localizationService.Translate(userId, "generic-read-progress")); + } return Ok(true); } @@ -627,7 +629,7 @@ public class ReaderController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); if (user == null) return Unauthorized(); - if (user.Bookmarks == null) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); + if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); try { @@ -667,7 +669,7 @@ public class ReaderController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); if (user == null) return Unauthorized(); - if (user.Bookmarks == null) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); + if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); try { @@ -882,7 +884,7 @@ public class ReaderController : BaseApiController { var userId = User.GetUserId(); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId); + var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, userId); if (series == null || chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); // Patch in the reading progress diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index ae9c7ddd8..e33ecebe1 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -180,7 +180,7 @@ public class SeriesController : BaseApiController [HttpGet("chapter")] public async Task> GetChapter(int chapterId) { - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId); + var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, User.GetUserId()); if (chapter == null) return NoContent(); return Ok(await _unitOfWork.ChapterRepository.AddChapterModifiers(User.GetUserId(), chapter)); } diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 5a9ad49f8..340116d18 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -71,6 +71,9 @@ public class SettingsController : BaseApiController public async Task> GetSettings() { var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + + // Do not send OIDC secret to user + settingsDto.OidcConfig.Secret = "*".Repeat(settingsDto.OidcConfig.Secret.Length); return Ok(settingsDto); } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index abf8468f0..f67b90a43 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -109,6 +109,7 @@ public class UsersController : BaseApiController existingPreferences.NoTransitions = preferencesDto.NoTransitions; existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships; existingPreferences.ShareReviews = preferencesDto.ShareReviews; + existingPreferences.ColorScapeEnabled = preferencesDto.ColorScapeEnabled; existingPreferences.BookReaderHighlightSlots = preferencesDto.BookReaderHighlightSlots; if (await _licenseService.HasActiveLicense()) diff --git a/API/DTOs/OPDS/Feed.cs b/API/DTOs/OPDS/Feed.cs index 5f4c4b115..e10465844 100644 --- a/API/DTOs/OPDS/Feed.cs +++ b/API/DTOs/OPDS/Feed.cs @@ -4,11 +4,6 @@ using System.Xml.Serialization; namespace API.DTOs.OPDS; -// TODO: OPDS Dtos are internal state, shouldn't be in DTO directory - -/// -/// -/// [XmlRoot("feed", Namespace = "http://www.w3.org/2005/Atom")] public sealed record Feed { @@ -41,10 +36,10 @@ public sealed record Feed public int? StartIndex { get; set; } = null; [XmlElement("link")] - public List Links { get; set; } = new List() ; + public List Links { get; set; } = []; [XmlElement("entry")] - public List Entries { get; set; } = new List(); + public List Entries { get; set; } = []; public bool ShouldSerializeTotal() { diff --git a/API/DTOs/Reader/AnnotationDto.cs b/API/DTOs/Reader/AnnotationDto.cs index 911e3ab47..b73fa3baa 100644 --- a/API/DTOs/Reader/AnnotationDto.cs +++ b/API/DTOs/Reader/AnnotationDto.cs @@ -49,6 +49,7 @@ public sealed record AnnotationDto /// public int SelectedSlotIndex { get; set; } + public required int ChapterId { get; set; } public required int VolumeId { get; set; } public required int SeriesId { get; set; } diff --git a/API/DTOs/Reader/BookResourceResultDto.cs b/API/DTOs/Reader/BookResourceResultDto.cs new file mode 100644 index 000000000..9935341d9 --- /dev/null +++ b/API/DTOs/Reader/BookResourceResultDto.cs @@ -0,0 +1,16 @@ +namespace API.DTOs.Reader; + +public sealed record BookResourceResultDto +{ + public bool IsSuccess { get; init; } + public string ErrorMessage { get; init; } + public byte[] Content { get; init; } + public string ContentType { get; init; } + public string FileName { get; init; } + + public static BookResourceResultDto Success(byte[] content, string contentType, string fileName) => + new() { IsSuccess = true, Content = content, ContentType = contentType, FileName = fileName }; + + public static BookResourceResultDto Error(string errorMessage) => + new() { IsSuccess = false, ErrorMessage = errorMessage }; +} diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index b7fd625f4..f4c72bfe8 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -37,6 +37,9 @@ public sealed record UserPreferencesDto /// [Required] public string Locale { get; set; } + /// + [Required] + public bool ColorScapeEnabled { get; set; } = true; /// public bool AniListScrobblingEnabled { get; set; } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index a455cc32f..36a4526af 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -139,6 +139,9 @@ public sealed class DataContext : IdentityDbContext() .Property(b => b.AllowAutomaticWebtoonReaderDetection) .HasDefaultValue(true); + builder.Entity() + .Property(b => b.ColorScapeEnabled) + .HasDefaultValue(true); builder.Entity() .Property(b => b.AllowScrobbling) diff --git a/API/Data/Migrations/20250919114119_ColorScapeSetting.Designer.cs b/API/Data/Migrations/20250919114119_ColorScapeSetting.Designer.cs new file mode 100644 index 000000000..67c146f48 --- /dev/null +++ b/API/Data/Migrations/20250919114119_ColorScapeSetting.Designer.cs @@ -0,0 +1,3854 @@ +// +using System; +using System.Collections.Generic; +using API.Data; +using API.Entities.MetadataMatching; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250919114119_ColorScapeSetting")] + partial class ColorScapeSetting + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.7"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("HasRunScrobbleEventGeneration") + .HasColumnType("INTEGER"); + + b.Property("IdentityProvider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("MalAccessToken") + .HasColumnType("TEXT"); + + b.Property("MalUserName") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OidcId") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventGenerationRan") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserAnnotation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("ContainsSpoiler") + .HasColumnType("INTEGER"); + + b.Property("Context") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingXPath") + .HasColumnType("TEXT"); + + b.Property("HighlightCount") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SelectedSlotIndex") + .HasColumnType("INTEGER"); + + b.Property("SelectedText") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("XPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.ToTable("AppUserAnnotation"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("ImageOffset") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("XPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserChapterRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("MissingSeriesFromSource") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("SourceUrl") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TotalSourceCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserCollection"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(4); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserDashboardStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Host") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserExternalSource"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AniListScrobblingEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderHighlightSlots") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + b.Property("ColorScapeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("en"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.Property("WantToReadSync") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("DisableWidthOverride") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("LibraryIds") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("SeriesIds") + .HasColumnType("TEXT"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("WidthOverride") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserReadingProfiles"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SelectedText") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AverageExternalRating") + .HasColumnType("REAL"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("ISBNLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("KPlusOverrides") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ReleaseDateLocked") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("REAL"); + + b.Property("SortOrderLocked") + .HasColumnType("INTEGER"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TitleNameLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DeliveryStatus") + .HasColumnType("TEXT"); + + b.Property("EmailTemplate") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SendDate") + .HasColumnType("TEXT"); + + b.Property("Sent") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); + + b.ToTable("EmailHistory"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("RanAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ManualMigrationHistory"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowMetadataMatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EnableMetadata") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("RemovePrefixForSortName") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Pattern") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryExcludePattern"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FileTypeGroup") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryFileTypeGroup"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("KoreaderHash") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ProviderUrl") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalRating"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("ExternalRecommendation"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalReview"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("INTEGER"); + + b.Property("CbrId") + .HasColumnType("INTEGER"); + + b.Property("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("ValidUntilUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastChecked") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBlacklist"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("KPlusOverrides") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DestinationType") + .HasColumnType("INTEGER"); + + b.Property("DestinationValue") + .HasColumnType("TEXT"); + + b.Property("ExcludeFromSource") + .HasColumnType("INTEGER"); + + b.Property("MetadataSettingsId") + .HasColumnType("INTEGER"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.Property("SourceValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MetadataSettingsId"); + + b.ToTable("MetadataFieldMapping"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRatingMappings") + .HasColumnType("TEXT"); + + b.Property("Blacklist") + .HasColumnType("TEXT"); + + b.Property("EnableChapterCoverImage") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterPublisher") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterReleaseDate") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterTitle") + .HasColumnType("INTEGER"); + + b.Property("EnableCoverImage") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("EnableExtendedMetadataProcessing") + .HasColumnType("INTEGER"); + + b.Property("EnableGenres") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalizedName") + .HasColumnType("INTEGER"); + + b.Property("EnablePeople") + .HasColumnType("INTEGER"); + + b.Property("EnablePublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("EnableRelationships") + .HasColumnType("INTEGER"); + + b.Property("EnableStartDate") + .HasColumnType("INTEGER"); + + b.Property("EnableSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableTags") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FirstLastPeopleNaming") + .HasColumnType("INTEGER"); + + b.Property("Overrides") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("PersonRoles") + .HasColumnType("TEXT"); + + b.Property("Whitelist") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("ChapterPeople"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("Asin") + .HasColumnType("TEXT"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("HardcoverId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Alias") + .HasColumnType("TEXT"); + + b.Property("NormalizedAlias") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PersonId"); + + b.ToTable("PersonAlias"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.Property("SeriesMetadataId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.HasKey("SeriesMetadataId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsErrored") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DontMatch") + .HasColumnType("INTEGER"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsBlacklisted") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("LowestFolderPath") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("CompatibleVersion") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("GitHubPath") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PreviewUrls") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ShaHash") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LookupName") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.Property("CollectionsId") + .HasColumnType("INTEGER"); + + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionsId", "ItemsId"); + + b.HasIndex("ItemsId"); + + b.ToTable("AppUserCollectionSeries"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.Property("ExternalRatingsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRatingExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.Property("ExternalRecommendationsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRecommendationExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalReviewExternalSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserAnnotation", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Annotations") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ChapterRatings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Ratings") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Collections") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingProfiles") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalRatings") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalReviews") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") + .WithMany("FieldMappings") + .HasForeignKey("MetadataSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("People") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("ChapterPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("Aliases") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("SeriesMetadataPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") + .WithMany("People") + .HasForeignKey("SeriesMetadataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.HasOne("API.Entities.AppUserCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Annotations"); + + b.Navigation("Bookmarks"); + + b.Navigation("ChapterRatings"); + + b.Navigation("Collections"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ReadingProfiles"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences") + .IsRequired(); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("ExternalRatings"); + + b.Navigation("ExternalReviews"); + + b.Navigation("Files"); + + b.Navigation("People"); + + b.Navigation("Ratings"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Navigation("FieldMappings"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Navigation("Aliases"); + + b.Navigation("ChapterPeople"); + + b.Navigation("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("ExternalSeriesMetadata"); + + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20250919114119_ColorScapeSetting.cs b/API/Data/Migrations/20250919114119_ColorScapeSetting.cs new file mode 100644 index 000000000..445ada7a3 --- /dev/null +++ b/API/Data/Migrations/20250919114119_ColorScapeSetting.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class ColorScapeSetting : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ColorScapeEnabled", + table: "AppUserPreferences", + type: "INTEGER", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ColorScapeEnabled", + table: "AppUserPreferences"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index ef16fe3ec..31188f691 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -551,6 +551,11 @@ namespace API.Data.Migrations b.Property("CollapseSeriesRelationships") .HasColumnType("INTEGER"); + b.Property("ColorScapeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + b.Property("EmulateBook") .HasColumnType("INTEGER"); diff --git a/API/Data/Repositories/AppUserSmartFilterRepository.cs b/API/Data/Repositories/AppUserSmartFilterRepository.cs index c7f981daa..5b6ff98f1 100644 --- a/API/Data/Repositories/AppUserSmartFilterRepository.cs +++ b/API/Data/Repositories/AppUserSmartFilterRepository.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using API.DTOs.Dashboard; using API.Entities; +using API.Helpers; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; @@ -16,6 +17,7 @@ public interface IAppUserSmartFilterRepository void Attach(AppUserSmartFilter filter); void Delete(AppUserSmartFilter filter); IEnumerable GetAllDtosByUserId(int userId); + Task> GetPagedDtosByUserIdAsync(int userId, UserParams userParams); Task GetById(int smartFilterId); } @@ -54,6 +56,15 @@ public class AppUserSmartFilterRepository : IAppUserSmartFilterRepository .AsEnumerable(); } + public Task> GetPagedDtosByUserIdAsync(int userId, UserParams userParams) + { + var filters = _context.AppUserSmartFilter + .Where(f => f.AppUserId == userId) + .ProjectTo(_mapper.ConfigurationProvider); + + return PagedList.CreateAsync(filters, userParams); + } + public async Task GetById(int smartFilterId) { return await _context.AppUserSmartFilter diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index 81a140f5b..6f86a1afb 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -40,10 +40,12 @@ public interface IChapterRepository Task GetChapterInfoDtoAsync(int chapterId); Task GetChapterTotalPagesAsync(int chapterId); Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); - Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); + Task GetChapterDtoAsync(int chapterId, int userId); + Task> GetChapterDtoByIdsAsync(IEnumerable chapterIds, int userId); Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); Task> GetFilesForChapterAsync(int chapterId); Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None); + Task> GetChapterDtosAsync(int volumeId, int userId); Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds); Task GetChapterCoverImageAsync(int chapterId); Task> GetAllCoverImagesAsync(); @@ -153,18 +155,39 @@ public class ChapterRepository : IChapterRepository .Select(c => c.Pages) .FirstOrDefaultAsync(); } - public async Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) + public async Task GetChapterDtoAsync(int chapterId, int userId) { var chapter = await _context.Chapter - .Includes(includes) + .Includes(ChapterIncludes.Files | ChapterIncludes.People) .ProjectTo(_mapper.ConfigurationProvider) - .AsNoTracking() .AsSplitQuery() .FirstOrDefaultAsync(c => c.Id == chapterId); + if (userId > 0 && chapter != null) + { + await AddChapterModifiers(userId, chapter); + } + return chapter; } + public async Task> GetChapterDtoByIdsAsync(IEnumerable chapterIds, int userId) + { + var chapters = await _context.Chapter + .Where(c => chapterIds.Contains(c.Id)) + .Includes(ChapterIncludes.Files | ChapterIncludes.People) + .ProjectTo(_mapper.ConfigurationProvider) + .AsSplitQuery() + .ToListAsync() ; + + foreach (var chapter in chapters) + { + await AddChapterModifiers(userId, chapter); + } + + return chapters; + } + public async Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) { var chapter = await _context.Chapter @@ -218,6 +241,28 @@ public class ChapterRepository : IChapterRepository .ToListAsync(); } + /// + /// Returns Chapters for a volume id with Progress + /// + /// + /// + public async Task> GetChapterDtosAsync(int volumeId, int userId) + { + var chapts = await _context.Chapter + .Where(c => c.VolumeId == volumeId) + .Includes(ChapterIncludes.Files | ChapterIncludes.People) + .OrderBy(c => c.SortOrder) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + + foreach (var chapter in chapts) + { + await AddChapterModifiers(userId, chapter); + } + + return chapts; + } + /// /// Returns the cover image for a chapter id. /// diff --git a/API/Data/Repositories/CollectionTagRepository.cs b/API/Data/Repositories/CollectionTagRepository.cs index 562f59e91..41b29686f 100644 --- a/API/Data/Repositories/CollectionTagRepository.cs +++ b/API/Data/Repositories/CollectionTagRepository.cs @@ -9,6 +9,7 @@ using API.Entities.Enums; using API.Extensions; using API.Extensions.QueryExtensions; using API.Extensions.QueryExtensions.Filtering; +using API.Helpers; using API.Services.Plus; using AutoMapper; using AutoMapper.QueryableExtensions; @@ -49,6 +50,7 @@ public interface ICollectionTagRepository /// /// Task> GetCollectionDtosAsync(int userId, bool includePromoted = false); + Task> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false); Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false); Task> GetAllCoverImagesAsync(); @@ -117,6 +119,18 @@ public class CollectionTagRepository : ICollectionTagRepository .ToListAsync(); } + public async Task> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false) + { + var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var collections = _context.AppUserCollection + .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) + .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) + .OrderBy(uc => uc.Title) + .ProjectTo(_mapper.ConfigurationProvider); + + return await PagedList.CreateAsync(collections, userParams); + } + public async Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index d3baa4de6..7f705e8ae 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -26,8 +26,8 @@ public interface IGenreRepository Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, QueryContext context = QueryContext.None); Task GetCountAsync(); - Task GetRandomGenre(); - Task GetGenreById(int id); + Task GetRandomGenre(); + Task GetGenreById(int id); Task> GetAllGenresNotInListAsync(ICollection genreNames); Task> GetBrowseableGenre(int userId, UserParams userParams); } @@ -79,7 +79,7 @@ public class GenreRepository : IGenreRepository return await _context.Genre.CountAsync(); } - public async Task GetRandomGenre() + public async Task GetRandomGenre() { var genreCount = await GetCountAsync(); if (genreCount == 0) return null; @@ -92,7 +92,7 @@ public class GenreRepository : IGenreRepository .FirstOrDefaultAsync(); } - public async Task GetGenreById(int id) + public async Task GetGenreById(int id) { return await _context.Genre .Where(g => g.Id == id) diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 6992b2950..94a2198e4 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -31,7 +31,7 @@ public interface IReadingListRepository { Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true); Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None); - Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId); + Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null); Task GetReadingListDtoByIdAsync(int readingListId, int userId); Task> AddReadingProgressModifiers(int userId, IList items); Task GetReadingListDtoByTitleAsync(int userId, string title); @@ -357,11 +357,11 @@ public class ReadingListRepository : IReadingListRepository .SingleOrDefaultAsync(); } - public async Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId) + public async Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null) { var userLibraries = _context.Library.GetUserLibraries(userId); - var items = await _context.ReadingListItem + var query = _context.ReadingListItem .Where(s => s.ReadingListId == readingListId) .Join(_context.Chapter, s => s.ChapterId, chapter => chapter.Id, (data, chapter) => new { @@ -431,9 +431,17 @@ public class ReadingListRepository : IReadingListRepository }) .Where(o => userLibraries.Contains(o.LibraryId)) .OrderBy(rli => rli.Order) - .AsSplitQuery() - .AsNoTracking() - .ToListAsync(); + .AsSplitQuery(); + + if (userParams != null) + { + query = query + .Skip(userParams.PageNumber * userParams.PageSize) + .Take(userParams.PageSize); + } + + + var items = await query.ToListAsync(); foreach (var item in items) { diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 9f101a43e..0a446179a 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -29,20 +29,20 @@ namespace API.Data.Repositories; public enum AppUserIncludes { None = 1, - Progress = 2, - Bookmarks = 4, - ReadingLists = 8, - Ratings = 16, - UserPreferences = 32, - WantToRead = 64, - ReadingListsWithItems = 128, - Devices = 256, - ScrobbleHolds = 512, - SmartFilters = 1024, - DashboardStreams = 2048, - SideNavStreams = 4096, - ExternalSources = 8192, - Collections = 16384, // 2^14 + Progress = 1 << 1, + Bookmarks = 1 << 2, + ReadingLists = 1 << 3, + Ratings = 1 << 4, + UserPreferences = 1 << 5, + WantToRead = 1 << 6, + ReadingListsWithItems = 1 << 7, + Devices = 1 << 8, + ScrobbleHolds = 1 << 9, + SmartFilters = 1 << 10, + DashboardStreams = 1 << 11, + SideNavStreams = 1 << 12, + ExternalSources = 1 << 13, + Collections = 1 << 14, ChapterRatings = 1 << 15, } @@ -118,6 +118,7 @@ public interface IUserRepository Task GetByOidcId(string? oidcId, AppUserIncludes includes = AppUserIncludes.None); Task GetAnnotationDtoById(int userId, int annotationId); + Task> GetAnnotationDtosBySeries(int userId, int seriesId); } public class UserRepository : IUserRepository @@ -612,6 +613,14 @@ public class UserRepository : IUserRepository .FirstOrDefaultAsync(); } + public async Task> GetAnnotationDtosBySeries(int userId, int seriesId) + { + return await _context.AppUserAnnotation + .Where(a => a.AppUserId == userId && a.SeriesId == seriesId) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + } + public async Task> GetAdminUsersAsync() { @@ -629,6 +638,7 @@ public class UserRepository : IUserRepository var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); if (user == null) return ArraySegment.Empty; + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (_userManager == null) { // userManager is null on Unit Tests only diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index aa6195143..86a68afd0 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -169,6 +169,10 @@ public class AppUserPreferences /// UI Site Global Setting: The language locale that should be used for the user /// public string Locale { get; set; } + /// + /// UI Site Global Setting: Should Kavita render ColorScape gradients + /// + public bool ColorScapeEnabled { get; set; } = true; #endregion #region KavitaPlus diff --git a/API/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs index a8d943b2d..2e2bd235b 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -12,7 +12,8 @@ public enum LibraryType /// /// Uses Comic regex for filename parsing /// - [Description("Comic (Legacy)")] + /// This was the original implementation and is much more flexible + [Description("Comic (Flexible)")] Comic = 1, /// /// Uses Manga regex for filename parsing also uses epub metadata diff --git a/API/Extensions/StringExtensions.cs b/API/Extensions/StringExtensions.cs index fc005b06f..78a090e42 100644 --- a/API/Extensions/StringExtensions.cs +++ b/API/Extensions/StringExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using System.Text.RegularExpressions; namespace API.Extensions; @@ -81,4 +82,15 @@ public static class StringExtensions return input[0] + new string('*', atIdx - 1) + input[atIdx..]; } + /// + /// Repeat returns a string that is equal to the original string repeat n times + /// + /// String to repeat + /// Amount of times to repeat + /// + public static string Repeat(this string? input, int n) + { + return string.IsNullOrEmpty(input) ? string.Empty : string.Concat(Enumerable.Repeat(input, n)); + } + } diff --git a/API/Helpers/AnnotationHelper.cs b/API/Helpers/AnnotationHelper.cs index ff428028d..3b2fb4277 100644 --- a/API/Helpers/AnnotationHelper.cs +++ b/API/Helpers/AnnotationHelper.cs @@ -43,11 +43,13 @@ public static partial class AnnotationHelper var originalText = elem.InnerText; // Calculate positions and sort by start position + var normalizedOriginalText = NormalizeWhitespace(originalText); + var sortedAnnotations = elementAnnotations .Select(a => new { Annotation = a, - StartPos = originalText.IndexOf(a.SelectedText, StringComparison.Ordinal) + StartPos = normalizedOriginalText.IndexOf(NormalizeWhitespace(a.SelectedText), StringComparison.Ordinal) }) .Where(a => a.StartPos >= 0) .OrderBy(a => a.StartPos) @@ -79,9 +81,10 @@ public static partial class AnnotationHelper elem.AppendChild(HtmlNode.CreateNode(originalText.Substring(currentPos))); } } - catch (Exception) + catch (Exception ex) { /* Swallow */ + return; } } } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 864dda0db..2452c38a7 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -388,7 +388,7 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.OwnerUsername, opt => opt.MapFrom(src => src.AppUser.UserName)) - .ForMember(dest => dest.OwnerUserId, opt => opt.MapFrom(src => src.AppUserId)); + .ForMember(dest => dest.OwnerUserId, opt => opt.MapFrom(src => src.AppUserId)) ; CreateMap(); } diff --git a/API/Helpers/PagedList.cs b/API/Helpers/PagedList.cs index 44d8a5082..4a2268e47 100644 --- a/API/Helpers/PagedList.cs +++ b/API/Helpers/PagedList.cs @@ -23,6 +23,11 @@ public class PagedList : List public int PageSize { get; set; } public int TotalCount { get; set; } + public static async Task> CreateAsync(IQueryable source, UserParams userParams) + { + return await CreateAsync(source, userParams.PageNumber, userParams.PageSize); + } + public static async Task> CreateAsync(IQueryable source, int pageNumber, int pageSize) { // NOTE: OrderBy warning being thrown here even if query has the orderby statement diff --git a/API/I18N/en.json b/API/I18N/en.json index bf07c1140..96366059e 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -160,6 +160,7 @@ "generic-user-pref": "There was an issue saving preferences", "opds-disabled": "OPDS is not enabled on this server", + "opds-continue-reading-title": "Continue Reading from: {0}", "on-deck": "On Deck", "browse-on-deck": "Browse On Deck", "recently-added": "Recently Added", @@ -239,6 +240,7 @@ "backup": "Backup", "update-yearly-stats": "Update Yearly Stats", - "generated-reading-profile-name": "Generated from {0}" + "generated-reading-profile-name": "Generated from {0}", + "genre-doesnt-exist": "Genre doesn't exist" } diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs index 8cef587e7..3f99dedb7 100644 --- a/API/Services/AccountService.cs +++ b/API/Services/AccountService.cs @@ -54,6 +54,12 @@ public interface IAccountService /// Does NOT commit Task UpdateLibrariesForUser(AppUser user, IList librariesIds, bool hasAdminRole); Task> UpdateRolesForUser(AppUser user, IList roles); + /// + /// Seeds all information necessary for a new user + /// + /// + /// + Task SeedUser(AppUser user); void AddDefaultStreamsToUser(AppUser user); Task AddDefaultReadingProfileToUser(AppUser user); } @@ -266,6 +272,17 @@ public partial class AccountService : IAccountService return []; } + public async Task SeedUser(AppUser user) + { + AddDefaultStreamsToUser(user); + AddDefaultHighlightSlotsToUser(user); + await AddDefaultReadingProfileToUser(user); // Commits + } + + /// + /// Assign default streams + /// + /// public void AddDefaultStreamsToUser(AppUser user) { foreach (var newStream in Seed.DefaultStreams.Select(_mapper.Map)) @@ -279,6 +296,18 @@ public partial class AccountService : IAccountService } } + private void AddDefaultHighlightSlotsToUser(AppUser user) + { + if (user.UserPreferences.BookReaderHighlightSlots.Any()) return; + + user.UserPreferences.BookReaderHighlightSlots = Seed.DefaultHighlightSlots.ToList(); + _unitOfWork.UserRepository.Update(user); + } + + /// + /// Assign default reading profile + /// + /// public async Task AddDefaultReadingProfileToUser(AppUser user) { var profile = new AppUserReadingProfileBuilder(user.Id) diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 7b2e2ac79..756068ab5 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -63,6 +63,8 @@ public interface IBookService Task> CreateKeyToPageMappingAsync(EpubBookRef book); Task?> GetWordCountsPerPage(string bookFilePath); Task CopyImageToTempFromBook(int chapterId, BookmarkDto bookmarkDto, string cachedBookPath); + Task GetResourceAsync(string bookFilePath, string requestedKey); + } public partial class BookService : IBookService @@ -315,7 +317,7 @@ public partial class BookService : IBookService /// /// /// - private static void InjectPTOCBookmarks(HtmlDocument doc, List ptocBookmarks) + private void InjectPTOCBookmarks(HtmlDocument doc, List ptocBookmarks) { if (ptocBookmarks.Count == 0) return; @@ -333,8 +335,9 @@ public partial class BookService : IBookService elem.PrependChild(HtmlNode.CreateNode( $"")); } - catch (Exception) + catch (Exception ex) { + _logger.LogWarning(ex, "Failed to inject a text (ptoc) bookmark into file"); // Swallow } } @@ -1075,6 +1078,27 @@ public partial class BookService : IBookService throw new KavitaException($"Page {bookmarkDto.Page} not found in epub"); } + /// + /// Attempts to resolve a requested key path with some hacks to attempt to handle incorrect metadata + /// + /// + /// + /// + public async Task GetResourceAsync(string bookFilePath, string requestedKey) + { + using var book = await EpubReader.OpenBookAsync(bookFilePath, LenientBookReaderOptions); + var key = CoalesceKeyForAnyFile(book, requestedKey); + + if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) + return BookResourceResultDto.Error("file-missing"); + + var bookFile = book.Content.AllFiles.GetLocalFileRefByKey(key); + var content = await bookFile.ReadContentAsBytesAsync(); + var contentType = GetContentType(bookFile.ContentType); + + return BookResourceResultDto.Success(content, contentType, requestedKey); + } + /// /// Parses out Title from book. Chapters and Volumes will always be "0". If there is any exception reading book (malformed books) @@ -1219,7 +1243,7 @@ public partial class BookService : IBookService /// Body element from the epub /// Epub mappings /// Page number we are loading - /// Ptoc Bookmarks to tie against + /// Ptoc (Text) Bookmarks to tie against /// private async Task ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary mappings, int page, List ptocBookmarks, List annotations) @@ -1228,7 +1252,6 @@ public partial class BookService : IBookService RewriteAnchors(page, doc, mappings); - // TODO: Pass bookmarks here for state management ScopeImages(doc, book, apiBase); InjectImages(doc, book, apiBase); @@ -1285,13 +1308,13 @@ public partial class BookService : IBookService var cleanedKey = CleanContentKeys(key); if (book.Content.AllFiles.ContainsLocalFileRefWithKey(cleanedKey)) return cleanedKey; - // TODO: Figure this out - // Fallback to searching for key (bad epub metadata) - // var correctedKey = book.Content.AllFiles.Keys.SingleOrDefault(s => s.EndsWith(key)); - // if (!string.IsNullOrEmpty(correctedKey)) - // { - // key = correctedKey; - // } + // Correct relative paths ./ + if (key.StartsWith("./")) + { + var nonPathKey = key.Replace("./", string.Empty); + var correctedKey = book.Content.AllFiles.Local.SingleOrDefault(s => s.Key == nonPathKey); + if (correctedKey != null) return correctedKey.Key; + } return key; } @@ -1329,8 +1352,8 @@ public partial class BookService : IBookService var tocPage = book.Content.Html.Local.Select(s => s.Key) .FirstOrDefault(k => k.Equals("TOC.XHTML", StringComparison.InvariantCultureIgnoreCase) || k.Equals("NAVIGATION.XHTML", StringComparison.InvariantCultureIgnoreCase)); - if (string.IsNullOrEmpty(tocPage)) return chaptersList; + if (string.IsNullOrEmpty(tocPage)) return chaptersList; if (!book.Content.Html.TryGetLocalFileRefByKey(tocPage, out var file)) return chaptersList; var content = await file.ReadContentAsync(); diff --git a/API/Services/KoreaderService.cs b/API/Services/KoreaderService.cs index 8300199f0..b2656618c 100644 --- a/API/Services/KoreaderService.cs +++ b/API/Services/KoreaderService.cs @@ -47,7 +47,7 @@ public class KoreaderService : IKoreaderService var userProgressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); if (userProgressDto == null) { - var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId); + var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId, userId); if (chapterDto == null) throw new KavitaException(await _localizationService.Translate(userId, "chapter-doesnt-exist")); var volumeDto = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapterDto.VolumeId); diff --git a/API/Services/OidcService.cs b/API/Services/OidcService.cs index e19663be1..3473fdb02 100644 --- a/API/Services/OidcService.cs +++ b/API/Services/OidcService.cs @@ -333,8 +333,7 @@ public class OidcService(ILogger logger, UserManager userM user.OidcId = externalId; user.IdentityProvider = IdentityProvider.OpenIdConnect; - accountService.AddDefaultStreamsToUser(user); - await accountService.AddDefaultReadingProfileToUser(user); + await accountService.SeedUser(user); await SyncUserSettings(request, settings, claimsPrincipal, user); await SetDefaults(settings, user); diff --git a/API/Services/SettingsService.cs b/API/Services/SettingsService.cs index 0cf07c7a0..02018dc67 100644 --- a/API/Services/SettingsService.cs +++ b/API/Services/SettingsService.cs @@ -598,11 +598,17 @@ public class SettingsService : ISettingsService updateSettingsDto.OidcConfig.RolesClaim = ClaimTypes.Role; } + var currentConfig = JsonSerializer.Deserialize(setting.Value)!; + + // Patch Oidc Secret back in if not changed + if ("*".Repeat(currentConfig.Secret.Length) == updateSettingsDto.OidcConfig.Secret) + { + updateSettingsDto.OidcConfig.Secret = currentConfig.Secret; + } + var newValue = JsonSerializer.Serialize(updateSettingsDto.OidcConfig); if (setting.Value == newValue) return false; - var currentConfig = JsonSerializer.Deserialize(setting.Value)!; - if (currentConfig.Authority != updateSettingsDto.OidcConfig.Authority) { if (!await IsValidAuthority(updateSettingsDto.OidcConfig.Authority + string.Empty)) diff --git a/API/Services/TachiyomiService.cs b/API/Services/TachiyomiService.cs index 7cba28695..dadcfdaa5 100644 --- a/API/Services/TachiyomiService.cs +++ b/API/Services/TachiyomiService.cs @@ -95,7 +95,7 @@ public class TachiyomiService : ITachiyomiService } // There is progress, we now need to figure out the highest volume or chapter and return that. - var prevChapter = (await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId))!; + var prevChapter = (await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId, userId))!; var volumeWithProgress = (await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId))!; // We only encode for single-file volumes diff --git a/API/Services/Tasks/SiteThemeService.cs b/API/Services/Tasks/SiteThemeService.cs index 3dca14ab9..3c1be35f0 100644 --- a/API/Services/Tasks/SiteThemeService.cs +++ b/API/Services/Tasks/SiteThemeService.cs @@ -77,7 +77,6 @@ public class ThemeService : IThemeService private readonly IDirectoryService _directoryService; private readonly IUnitOfWork _unitOfWork; private readonly IEventHub _eventHub; - private readonly IFileService _fileService; private readonly ILogger _logger; private readonly Markdown _markdown = new(); private readonly IMemoryCache _cache; @@ -91,12 +90,11 @@ public class ThemeService : IThemeService private const string GithubReadme = "https://raw.githubusercontent.com/Kareadita/Themes/main/README.md"; public ThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork, - IEventHub eventHub, IFileService fileService, ILogger logger, IMemoryCache cache) + IEventHub eventHub, ILogger logger, IMemoryCache cache) { _directoryService = directoryService; _unitOfWork = unitOfWork; _eventHub = eventHub; - _fileService = fileService; _logger = logger; _cache = cache; diff --git a/API/Startup.cs b/API/Startup.cs index d803b990e..fad79ceea 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -400,11 +400,13 @@ public class Startup app.UseStaticFiles(new StaticFileOptions { // bcmap files needed for PDF reader localizations (https://github.com/Kareadita/Kavita/issues/2970) + // ftl files are needed for PDF zoom options (https://github.com/Kareadita/Kavita/issues/3995) ContentTypeProvider = new FileExtensionContentTypeProvider { Mappings = { - [".bcmap"] = "application/octet-stream" + [".bcmap"] = "application/octet-stream", + [".ftl"] = "text/plain" } }, HttpsCompression = HttpsCompressionMode.Compress, diff --git a/README.md b/README.md index 227397a79..aa23a43db 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,14 @@ your reading collection with your friends and family! - First class responsive readers that work great on any device (phone, tablet, desktop) - Customizable theming support: [Theme Repo](https://github.com/Kareadita/Themes) and [Documentation](https://wiki.kavitareader.com/guides/themes) - External metadata integration and scrobbling for read status, ratings, and reviews (available via [Kavita+](https://wiki.kavitareader.com/kavita+)) -- Rich Metadata support with filtering and searching +- Rich Metadata support with filtering, searching, and smart filters - Ways to group reading material: Collections, Reading Lists (CBL Import), Want to Read -- Ability to manage users with rich Role-based management for age restrictions, abilities within the app, etc +- Ability to manage users with rich Role-based management for age restrictions, abilities within the app, OIDC, etc - Rich web readers supporting webtoon, continuous reading mode (continue without leaving the reader), virtual pages (epub), etc - Ability to customize your dashboard and side nav with smart filters, custom order and visibility toggles -- Full Localization Support -- Ability to download metadata (available via [Kavita+](https://wiki.kavitareader.com/kavita+)) - +- Full Localization Support ([Weblate](https://hosted.weblate.org/engage/kavita/)) +- Ability to download metadata, reviews, ratings, and more (available via [Kavita+](https://wiki.kavitareader.com/kavita+)) +- Epub-based Annotation/Highlight support ## Support [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://discord.gg/eczRp9eeem) diff --git a/UI/Web/angular.json b/UI/Web/angular.json index 6c24c56b0..b0c30b5b1 100644 --- a/UI/Web/angular.json +++ b/UI/Web/angular.json @@ -91,6 +91,14 @@ "maximumError": "30kb" } ] + }, + "proxy": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.proxy.ts" + } + ] } }, "defaultConfiguration": "" @@ -106,6 +114,9 @@ "configurations": { "production": { "buildTarget": "kavita-webui:build:production" + }, + "proxy": { + "buildTarget": "kavita-webui:build:proxy" } } }, diff --git a/UI/Web/package.json b/UI/Web/package.json index 3b89df4a7..4a4708e33 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -4,8 +4,10 @@ "scripts": { "ng": "ng", "start": "npm run cache-locale && ng serve --host 0.0.0.0", + "start-proxy": "npm run cache-locale && ng serve --configuration proxy --host 0.0.0.0 --proxy-config proxy.conf.json", "build": "npm run cache-locale && ng build", "build-backend": "ng build && rm -r ../../API/wwwroot/* && cp -r dist/browser/* ../../API/wwwroot", + "build-backend-prod": "ng build --configuration production && rm -r ../../API/wwwroot/* && cp -r dist/browser/* ../../API/wwwroot", "minify-langs": "node minify-json.js", "cache-locale": "node hash-localization.js", "cache-locale-prime": "node hash-localization-prime.js", diff --git a/UI/Web/proxy.conf.json b/UI/Web/proxy.conf.json new file mode 100644 index 000000000..4d787f811 --- /dev/null +++ b/UI/Web/proxy.conf.json @@ -0,0 +1,37 @@ +{ + "/api": { + "target": "http://localhost:5000", + "secure": false, + "changeOrigin": true, + "logLevel": "debug" + }, + "/hubs": { + "target": "http://localhost:5000", + "secure": false, + "changeOrigin": true, + "logLevel": "debug", + "ws": true + }, + "/oidc/login": { + "target": "http://localhost:5000", + "secure": false, + "changeOrigin": true, + "logLevel": "debug" + }, + "/oidc/logout": { + "target": "http://localhost:5000", + "secure": false, + "changeOrigin": true, + "logLevel": "debug" + }, + "/signin-oidc": { + "target": "http://localhost:5000", + "secure": false, + "changeOrigin": true + }, + "/signout-callback-oidc": { + "target": "http://localhost:5000", + "secure": false, + "changeOrigin": true + } +} diff --git a/UI/Web/src/app/_guards/auth.guard.ts b/UI/Web/src/app/_guards/auth.guard.ts index 5422a7fb8..bc037a081 100644 --- a/UI/Web/src/app/_guards/auth.guard.ts +++ b/UI/Web/src/app/_guards/auth.guard.ts @@ -11,16 +11,16 @@ import {APP_BASE_HREF} from "@angular/common"; providedIn: 'root' }) export class AuthGuard implements CanActivate { + private accountService = inject(AccountService); + private router = inject(Router); + private toastr = inject(ToastrService); + private translocoService = inject(TranslocoService); + public static urlKey: string = 'kavita--auth-intersection-url'; baseURL = inject(APP_BASE_HREF); - constructor(private accountService: AccountService, - private router: Router, - private toastr: ToastrService, - private translocoService: TranslocoService) {} - canActivate(): Observable { return this.accountService.currentUser$.pipe(take(1), map((user) => { diff --git a/UI/Web/src/app/_guards/library-access.guard.ts b/UI/Web/src/app/_guards/library-access.guard.ts index 8d10699f7..d1054de33 100644 --- a/UI/Web/src/app/_guards/library-access.guard.ts +++ b/UI/Web/src/app/_guards/library-access.guard.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable, of } from 'rxjs'; import { MemberService } from '../_services/member.service'; @@ -7,8 +7,8 @@ import { MemberService } from '../_services/member.service'; providedIn: 'root' }) export class LibraryAccessGuard implements CanActivate { + private memberService = inject(MemberService); - constructor(private memberService: MemberService) {} canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const libraryId = parseInt(state.url.split('library/')[1], 10); diff --git a/UI/Web/src/app/_interceptors/error.interceptor.ts b/UI/Web/src/app/_interceptors/error.interceptor.ts index 834179396..555445bae 100644 --- a/UI/Web/src/app/_interceptors/error.interceptor.ts +++ b/UI/Web/src/app/_interceptors/error.interceptor.ts @@ -11,13 +11,14 @@ import {APP_BASE_HREF} from "@angular/common"; @Injectable() export class ErrorInterceptor implements HttpInterceptor { + private router = inject(Router); + private toastr = inject(ToastrService); + private accountService = inject(AccountService); + private translocoService = inject(TranslocoService); + baseURL = inject(APP_BASE_HREF); - constructor(private router: Router, private toastr: ToastrService, - private accountService: AccountService, - private translocoService: TranslocoService) {} - intercept(request: HttpRequest, next: HttpHandler): Observable> { return next.handle(request).pipe( diff --git a/UI/Web/src/app/_interceptors/jwt.interceptor.ts b/UI/Web/src/app/_interceptors/jwt.interceptor.ts index d9cd47327..5d39b950d 100644 --- a/UI/Web/src/app/_interceptors/jwt.interceptor.ts +++ b/UI/Web/src/app/_interceptors/jwt.interceptor.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; import {Observable, switchMap} from 'rxjs'; import { AccountService } from '../_services/account.service'; @@ -6,8 +6,8 @@ import { take } from 'rxjs/operators'; @Injectable() export class JwtInterceptor implements HttpInterceptor { + private accountService = inject(AccountService); - constructor(private accountService: AccountService) {} intercept(request: HttpRequest, next: HttpHandler): Observable> { const user = this.accountService.currentUserSignal(); diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index f68a33504..8a98a2c92 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -14,6 +14,7 @@ export interface Preferences { shareReviews: boolean; locale: string; bookReaderHighlightSlots: HighlightSlot[]; + colorScapeEnabled: boolean; // Kavita+ aniListScrobblingEnabled: boolean; diff --git a/UI/Web/src/app/_pipes/default-date.pipe.ts b/UI/Web/src/app/_pipes/default-date.pipe.ts index 7cd541e0b..c5c44a04b 100644 --- a/UI/Web/src/app/_pipes/default-date.pipe.ts +++ b/UI/Web/src/app/_pipes/default-date.pipe.ts @@ -1,4 +1,4 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform, inject } from '@angular/core'; import {TranslocoService} from "@jsverse/transloco"; @Pipe({ @@ -7,9 +7,8 @@ import {TranslocoService} from "@jsverse/transloco"; standalone: true }) export class DefaultDatePipe implements PipeTransform { + private translocoService = inject(TranslocoService); - constructor(private translocoService: TranslocoService) { - } transform(value: any, replacementString = 'default-date-pipe.never'): string { if (value === null || value === undefined || value === '' || value === Infinity || Number.isNaN(value) || value === '1/1/01') { return this.translocoService.translate(replacementString); diff --git a/UI/Web/src/app/_pipes/language-name.pipe.ts b/UI/Web/src/app/_pipes/language-name.pipe.ts index 697554bd3..49589e5d3 100644 --- a/UI/Web/src/app/_pipes/language-name.pipe.ts +++ b/UI/Web/src/app/_pipes/language-name.pipe.ts @@ -1,4 +1,4 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform, inject } from '@angular/core'; import { map, Observable } from 'rxjs'; import { MetadataService } from '../_services/metadata.service'; import {shareReplay} from "rxjs/operators"; @@ -8,8 +8,8 @@ import {shareReplay} from "rxjs/operators"; standalone: true }) export class LanguageNamePipe implements PipeTransform { + private metadataService = inject(MetadataService); - constructor(private metadataService: MetadataService) {} transform(isoCode: string): Observable { return this.metadataService.getLanguageNameForCode(isoCode).pipe(shareReplay()); diff --git a/UI/Web/src/app/_pipes/library-type-subtitle.pipe.ts b/UI/Web/src/app/_pipes/library-type-subtitle.pipe.ts new file mode 100644 index 000000000..db9e53440 --- /dev/null +++ b/UI/Web/src/app/_pipes/library-type-subtitle.pipe.ts @@ -0,0 +1,30 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {LibraryType} from "../_models/library/library"; +import {translate} from "@jsverse/transloco"; + +@Pipe({ + name: 'libraryTypeSubtitle' +}) +export class LibraryTypeSubtitlePipe implements PipeTransform { + + transform(value: LibraryType | null | undefined): string { + if (value === null || value === undefined) return ''; + + switch (value) { + case LibraryType.Manga: + return translate('library-type-subtitle-pipe.manga'); + case LibraryType.Comic: + return translate('library-type-subtitle-pipe.comic'); + case LibraryType.Book: + return translate('library-type-subtitle-pipe.book'); + case LibraryType.Images: + return translate('library-type-subtitle-pipe.image'); + case LibraryType.LightNovel: + return translate('library-type-subtitle-pipe.lightNovel'); + case LibraryType.ComicVine: + return translate('library-type-subtitle-pipe.comicVine'); + + } + } + +} diff --git a/UI/Web/src/app/_pipes/manga-format.pipe.ts b/UI/Web/src/app/_pipes/manga-format.pipe.ts index 60672271b..9a1e073b7 100644 --- a/UI/Web/src/app/_pipes/manga-format.pipe.ts +++ b/UI/Web/src/app/_pipes/manga-format.pipe.ts @@ -1,4 +1,4 @@ -import {Pipe, PipeTransform} from '@angular/core'; +import { Pipe, PipeTransform, inject } from '@angular/core'; import { MangaFormat } from '../_models/manga-format'; import {TranslocoService} from "@jsverse/transloco"; @@ -10,8 +10,8 @@ import {TranslocoService} from "@jsverse/transloco"; standalone: true }) export class MangaFormatPipe implements PipeTransform { + private translocoService = inject(TranslocoService); - constructor(private translocoService: TranslocoService) {} transform(format: MangaFormat): string { switch (format) { diff --git a/UI/Web/src/app/_pipes/publication-status.pipe.ts b/UI/Web/src/app/_pipes/publication-status.pipe.ts index 98a62a2b6..56597106b 100644 --- a/UI/Web/src/app/_pipes/publication-status.pipe.ts +++ b/UI/Web/src/app/_pipes/publication-status.pipe.ts @@ -1,4 +1,4 @@ -import {Pipe, PipeTransform} from '@angular/core'; +import { Pipe, PipeTransform, inject } from '@angular/core'; import { PublicationStatus } from '../_models/metadata/publication-status'; import {TranslocoService} from "@jsverse/transloco"; @@ -7,7 +7,8 @@ import {TranslocoService} from "@jsverse/transloco"; standalone: true }) export class PublicationStatusPipe implements PipeTransform { - constructor(private translocoService: TranslocoService) {} + private translocoService = inject(TranslocoService); + transform(value: PublicationStatus): string { switch (value) { diff --git a/UI/Web/src/app/_pipes/read-time-left.pipe.ts b/UI/Web/src/app/_pipes/read-time-left.pipe.ts index 5dd04dc75..4cce0f53f 100644 --- a/UI/Web/src/app/_pipes/read-time-left.pipe.ts +++ b/UI/Web/src/app/_pipes/read-time-left.pipe.ts @@ -1,4 +1,4 @@ -import {Pipe, PipeTransform} from '@angular/core'; +import { Pipe, PipeTransform, inject } from '@angular/core'; import {TranslocoService} from "@jsverse/transloco"; import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range"; @@ -7,8 +7,8 @@ import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range"; standalone: true }) export class ReadTimeLeftPipe implements PipeTransform { + private readonly translocoService = inject(TranslocoService); - constructor(private readonly translocoService: TranslocoService) {} transform(readingTimeLeft: HourEstimateRange, includeLeftLabel = false): string { const hoursLabel = readingTimeLeft.avgHours > 1 diff --git a/UI/Web/src/app/_pipes/read-time.pipe.ts b/UI/Web/src/app/_pipes/read-time.pipe.ts index 1970b2812..a841f4bb6 100644 --- a/UI/Web/src/app/_pipes/read-time.pipe.ts +++ b/UI/Web/src/app/_pipes/read-time.pipe.ts @@ -1,4 +1,4 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform, inject } from '@angular/core'; import {IHasReadingTime} from "../_models/common/i-has-reading-time"; import {TranslocoService} from "@jsverse/transloco"; @@ -7,7 +7,8 @@ import {TranslocoService} from "@jsverse/transloco"; standalone: true }) export class ReadTimePipe implements PipeTransform { - constructor(private translocoService: TranslocoService) {} + private translocoService = inject(TranslocoService); + transform(readingTime: IHasReadingTime): string { if (readingTime.maxHoursToRead === 0 || readingTime.minHoursToRead === 0) { diff --git a/UI/Web/src/app/_pipes/sort-field.pipe.ts b/UI/Web/src/app/_pipes/sort-field.pipe.ts index d032de9c8..c2464935d 100644 --- a/UI/Web/src/app/_pipes/sort-field.pipe.ts +++ b/UI/Web/src/app/_pipes/sort-field.pipe.ts @@ -1,4 +1,4 @@ -import {Pipe, PipeTransform} from '@angular/core'; +import { Pipe, PipeTransform, inject } from '@angular/core'; import {SortField} from "../_models/metadata/series-filter"; import {TranslocoService} from "@jsverse/transloco"; import {ValidFilterEntity} from "../metadata-filter/filter-settings"; @@ -9,9 +9,8 @@ import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; standalone: true }) export class SortFieldPipe implements PipeTransform { + private translocoService = inject(TranslocoService); - constructor(private translocoService: TranslocoService) { - } transform(value: T, entityType: ValidFilterEntity): string { diff --git a/UI/Web/src/app/_pipes/time-ago.pipe.ts b/UI/Web/src/app/_pipes/time-ago.pipe.ts index 9940d4bb7..0c2dca0fa 100644 --- a/UI/Web/src/app/_pipes/time-ago.pipe.ts +++ b/UI/Web/src/app/_pipes/time-ago.pipe.ts @@ -1,4 +1,4 @@ -import {ChangeDetectorRef, NgZone, OnDestroy, Pipe, PipeTransform} from '@angular/core'; +import { ChangeDetectorRef, NgZone, OnDestroy, Pipe, PipeTransform, inject } from '@angular/core'; import {TranslocoService} from "@jsverse/transloco"; /** @@ -34,10 +34,12 @@ and modified standalone: true }) export class TimeAgoPipe implements PipeTransform, OnDestroy { + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private ngZone = inject(NgZone); + private translocoService = inject(TranslocoService); + private timer: number | null = null; - constructor(private readonly changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, - private translocoService: TranslocoService) {} transform(value: string | Date | null) { if (value === '' || value === null || value === undefined || (typeof value === 'string' && value.split('T')[0] === '0001-01-01')) { diff --git a/UI/Web/src/app/_resolvers/reading-profile.resolver.ts b/UI/Web/src/app/_resolvers/reading-profile.resolver.ts index 1d28adf95..6ba9daef5 100644 --- a/UI/Web/src/app/_resolvers/reading-profile.resolver.ts +++ b/UI/Web/src/app/_resolvers/reading-profile.resolver.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; import {Observable} from 'rxjs'; import {ReadingProfileService} from "../_services/reading-profile.service"; @@ -7,8 +7,8 @@ import {ReadingProfileService} from "../_services/reading-profile.service"; providedIn: 'root' }) export class ReadingProfileResolver implements Resolve { + private readingProfileService = inject(ReadingProfileService); - constructor(private readingProfileService: ReadingProfileService) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { // Extract seriesId from route params or parent route diff --git a/UI/Web/src/app/_resolvers/url-filter.resolver.ts b/UI/Web/src/app/_resolvers/url-filter.resolver.ts index 16bc5c752..9dbf786e0 100644 --- a/UI/Web/src/app/_resolvers/url-filter.resolver.ts +++ b/UI/Web/src/app/_resolvers/url-filter.resolver.ts @@ -1,4 +1,4 @@ -import {Injectable} from "@angular/core"; +import { Injectable, inject } from "@angular/core"; import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from "@angular/router"; import {Observable, of} from "rxjs"; import {FilterV2} from "../_models/metadata/v2/filter-v2"; @@ -12,8 +12,8 @@ import {FilterUtilitiesService} from "../shared/_services/filter-utilities.servi providedIn: 'root' }) export class UrlFilterResolver implements Resolve { + private filterUtilitiesService = inject(FilterUtilitiesService); - constructor(private filterUtilitiesService: FilterUtilitiesService) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { if (!state.url.includes('?')) return of(null); diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index 90a890597..499fb81ed 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -7,7 +7,6 @@ import {Preferences} from '../_models/preferences/preferences'; import {User} from '../_models/user'; import {Router} from '@angular/router'; import {EVENTS, MessageHubService} from './message-hub.service'; -import {ThemeService} from './theme.service'; import {InviteUserResponse} from '../_models/auth/invite-user-response'; import {UserUpdateEvent} from '../_models/events/user-update-event'; import {AgeRating} from '../_models/metadata/age-rating'; @@ -51,7 +50,6 @@ export class AccountService { private readonly httpClient = inject(HttpClient); private readonly router = inject(Router); private readonly messageHub = inject(MessageHubService); - private readonly themeService = inject(ThemeService); baseUrl = environment.apiUrl; userKey = 'kavita-user'; @@ -226,14 +224,6 @@ export class AccountService { if (user) { localStorage.setItem(this.userKey, JSON.stringify(user)); localStorage.setItem(AccountService.lastLoginKey, user.username); - - if (user.preferences && user.preferences.theme) { - this.themeService.setTheme(user.preferences.theme.name); - } else { - this.themeService.setTheme(this.themeService.defaultTheme); - } - } else { - this.themeService.setTheme(this.themeService.defaultTheme); } this.currentUser = user; diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 8bd667b60..4d0cf9384 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {map, Observable, shareReplay} from 'rxjs'; import {Chapter} from '../_models/chapter'; import {UserCollection} from '../_models/collection-tag'; @@ -180,6 +180,9 @@ export type ActionableEntity = Volume | Series | Chapter | ReadingList | UserCol providedIn: 'root', }) export class ActionFactoryService { + private accountService = inject(AccountService); + private deviceService = inject(DeviceService); + private libraryActions: Array> = []; private seriesActions: Array> = []; private volumeActions: Array> = []; @@ -192,7 +195,7 @@ export class ActionFactoryService { private smartFilterActions: Array> = []; private sideNavHomeActions: Array> = []; - constructor(private accountService: AccountService, private deviceService: DeviceService) { + constructor() { this.accountService.currentUser$.subscribe((_) => { this._resetActions(); }); diff --git a/UI/Web/src/app/_services/annotation.service.ts b/UI/Web/src/app/_services/annotation.service.ts index cb18eb0e8..d9fb3c9f7 100644 --- a/UI/Web/src/app/_services/annotation.service.ts +++ b/UI/Web/src/app/_services/annotation.service.ts @@ -73,6 +73,10 @@ export class AnnotationService { })); } + getAnnotationsForSeries(seriesId: number) { + return this.httpClient.get>(this.baseUrl + 'annotation/all-for-series?seriesId=' + seriesId); + } + createAnnotation(data: Annotation) { return this.httpClient.post(this.baseUrl + 'annotation/create', data).pipe( diff --git a/UI/Web/src/app/_services/chapter.service.ts b/UI/Web/src/app/_services/chapter.service.ts index 6a6f7a600..7b338c268 100644 --- a/UI/Web/src/app/_services/chapter.service.ts +++ b/UI/Web/src/app/_services/chapter.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {environment} from "../../environments/environment"; import {HttpClient} from "@angular/common/http"; import {Chapter} from "../_models/chapter"; @@ -9,11 +9,11 @@ import {ChapterDetailPlus} from "../_models/chapter-detail-plus"; providedIn: 'root' }) export class ChapterService { + private httpClient = inject(HttpClient); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } - getChapterMetadata(chapterId: number) { return this.httpClient.get(this.baseUrl + 'chapter?chapterId=' + chapterId); } diff --git a/UI/Web/src/app/_services/collection-tag.service.ts b/UI/Web/src/app/_services/collection-tag.service.ts index df668f13a..5b13ef052 100644 --- a/UI/Web/src/app/_services/collection-tag.service.ts +++ b/UI/Web/src/app/_services/collection-tag.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {environment} from 'src/environments/environment'; import {UserCollection} from '../_models/collection-tag'; import {TextResonse} from '../_types/text-response'; @@ -12,11 +12,12 @@ import {AccountService} from "./account.service"; providedIn: 'root' }) export class CollectionTagService { + private httpClient = inject(HttpClient); + private accountService = inject(AccountService); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient, private accountService: AccountService) { } - allCollections(ownedOnly = false) { return this.httpClient.get(this.baseUrl + 'collection?ownedOnly=' + ownedOnly); } diff --git a/UI/Web/src/app/_services/colorscape.service.ts b/UI/Web/src/app/_services/colorscape.service.ts index 73228467b..046f3d04f 100644 --- a/UI/Web/src/app/_services/colorscape.service.ts +++ b/UI/Web/src/app/_services/colorscape.service.ts @@ -2,7 +2,7 @@ import {inject, Injectable} from '@angular/core'; import {DOCUMENT} from '@angular/common'; import {BehaviorSubject, filter, take, tap, timer} from 'rxjs'; import {NavigationEnd, Router} from "@angular/router"; -import {environment} from "../../environments/environment"; +import {AccountService} from "./account.service"; interface ColorSpace { primary: string; @@ -33,10 +33,10 @@ const colorScapeSelector = 'colorscape'; export class ColorscapeService { private readonly document = inject(DOCUMENT); private readonly router = inject(Router); + private readonly accountService = inject(AccountService); private colorSubject = new BehaviorSubject(null); private colorSeedSubject = new BehaviorSubject<{primary: string, complementary: string | null} | null>(null); - public readonly colors$ = this.colorSubject.asObservable(); private minDuration = 1000; // minimum duration private maxDuration = 4000; // maximum duration @@ -52,6 +52,7 @@ export class ColorscapeService { tap(() => this.checkAndResetColorscapeAfterDelay()) ).subscribe(); + } /** @@ -175,7 +176,7 @@ export class ColorscapeService { * @param complementaryColor */ setColorScape(primaryColor: string, complementaryColor: string | null = null) { - if (this.getCssVariable('--colorscape-enabled') === 'false') { + if (this.accountService.currentUserSignal()?.preferences?.colorScapeEnabled === false || this.getCssVariable('--colorscape-enabled') === 'false') { return; } diff --git a/UI/Web/src/app/_services/dashboard.service.ts b/UI/Web/src/app/_services/dashboard.service.ts index 493fae370..d0b41104f 100644 --- a/UI/Web/src/app/_services/dashboard.service.ts +++ b/UI/Web/src/app/_services/dashboard.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {TextResonse} from "../_types/text-response"; import {HttpClient} from "@angular/common/http"; import {environment} from "../../environments/environment"; @@ -8,8 +8,9 @@ import {DashboardStream} from "../_models/dashboard/dashboard-stream"; providedIn: 'root' }) export class DashboardService { + private httpClient = inject(HttpClient); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } getDashboardStreams(visibleOnly = true) { return this.httpClient.get>(this.baseUrl + 'stream/dashboard?visibleOnly=' + visibleOnly); diff --git a/UI/Web/src/app/_services/device.service.ts b/UI/Web/src/app/_services/device.service.ts index 496abf9c2..0d016db6c 100644 --- a/UI/Web/src/app/_services/device.service.ts +++ b/UI/Web/src/app/_services/device.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { ReplaySubject, shareReplay, tap } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Device } from '../_models/device/device'; @@ -11,6 +11,9 @@ import { AccountService } from './account.service'; providedIn: 'root' }) export class DeviceService { + private httpClient = inject(HttpClient); + private accountService = inject(AccountService); + baseUrl = environment.apiUrl; @@ -18,7 +21,7 @@ export class DeviceService { public devices$ = this.devicesSource.asObservable().pipe(shareReplay()); - constructor(private httpClient: HttpClient, private accountService: AccountService) { + constructor() { // Ensure we are authenticated before we make an authenticated api call. this.accountService.currentUser$.subscribe(user => { if (!user) { diff --git a/UI/Web/src/app/_services/email.service.ts b/UI/Web/src/app/_services/email.service.ts index 5afb62ca7..1523fb52a 100644 --- a/UI/Web/src/app/_services/email.service.ts +++ b/UI/Web/src/app/_services/email.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {environment} from "../../environments/environment"; import {HttpClient} from "@angular/common/http"; import {EmailHistory} from "../_models/email-history"; @@ -7,8 +7,9 @@ import {EmailHistory} from "../_models/email-history"; providedIn: 'root' }) export class EmailService { + private httpClient = inject(HttpClient); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } getEmailHistory() { return this.httpClient.get(`${this.baseUrl}email/all`); diff --git a/UI/Web/src/app/_services/epub-reader-menu.service.ts b/UI/Web/src/app/_services/epub-reader-menu.service.ts index 37a58fa69..529b91386 100644 --- a/UI/Web/src/app/_services/epub-reader-menu.service.ts +++ b/UI/Web/src/app/_services/epub-reader-menu.service.ts @@ -44,6 +44,23 @@ export class EpubReaderMenuService { (ref.componentInstance as ViewEditAnnotationDrawerComponent).mode.set(AnnotationMode.Create); this.isDrawerOpen.set(true); + + // Set CSS variable for drawer height + setTimeout(() => { + var drawerElement = document.querySelector('view-edit-annotation-drawer, app-view-edit-annotation-drawer'); + if (!drawerElement) return; + var setDrawerHeightVar = function() { + if (!drawerElement) return; + var height = (drawerElement as HTMLElement).offsetHeight; + document.documentElement.style.setProperty('--drawer-height', height + 'px'); + }; + setDrawerHeightVar(); + var resizeObserver = new window.ResizeObserver(function() { + setDrawerHeightVar(); + }); + resizeObserver.observe(drawerElement as HTMLElement); + // Optionally store observer for cleanup if needed + }, 0); } @@ -156,6 +173,23 @@ export class EpubReaderMenuService { ref.dismissed.subscribe(() => this.setDrawerClosed()); this.isDrawerOpen.set(true); + + // Set CSS variable for drawer height + setTimeout(() => { + var drawerElement = document.querySelector('view-edit-annotation-drawer, app-view-edit-annotation-drawer'); + if (!drawerElement) return; + var setDrawerHeightVar = function() { + if (!drawerElement) return; + var height = (drawerElement as HTMLElement).offsetHeight; + document.documentElement.style.setProperty('--drawer-height', height + 'px'); + }; + setDrawerHeightVar(); + var resizeObserver = new window.ResizeObserver(function() { + setDrawerHeightVar(); + }); + resizeObserver.observe(drawerElement as HTMLElement); + // Optionally store observer for cleanup if needed + }, 0); } closeAll() { diff --git a/UI/Web/src/app/_services/epub-reader-settings.service.ts b/UI/Web/src/app/_services/epub-reader-settings.service.ts index 75109b4c9..cfdb5f8c5 100644 --- a/UI/Web/src/app/_services/epub-reader-settings.service.ts +++ b/UI/Web/src/app/_services/epub-reader-settings.service.ts @@ -9,7 +9,7 @@ import {ReadingProfile, ReadingProfileKind} from "../_models/preferences/reading import {BookService, FontFamily} from "../book-reader/_services/book.service"; import {ThemeService} from './theme.service'; import {ReadingProfileService} from "./reading-profile.service"; -import {debounceTime, distinctUntilChanged, filter, skip, tap} from "rxjs/operators"; +import {debounceTime, distinctUntilChanged, filter, tap} from "rxjs/operators"; import {BookTheme} from "../_models/preferences/book-theme"; import {DOCUMENT} from "@angular/common"; import {translate} from "@jsverse/transloco"; @@ -335,6 +335,13 @@ export class EpubReaderSettingsService { ? WritingStyle.Vertical : WritingStyle.Horizontal; + // Default back to Col 1 in this case + if (newStyle === WritingStyle.Vertical ) { + if (this._layoutMode() === BookPageLayoutMode.Column2) { + this.updateLayoutMode(BookPageLayoutMode.Column1); + } + } + this._writingStyle.set(newStyle); this.settingsForm.get('bookReaderWritingStyle')!.setValue(newStyle); } @@ -554,6 +561,14 @@ export class EpubReaderSettingsService { takeUntilDestroyed(this.destroyRef) ).subscribe((layoutMode: BookPageLayoutMode) => { this.isUpdatingFromForm = true; + + if (this.writingStyle() === WritingStyle.Vertical && layoutMode === BookPageLayoutMode.Column2) { + this.toastr.info(translate('book-reader.forced-vertical-switch')); + this.isUpdatingFromForm = false; + return; + } + + this._layoutMode.set(layoutMode); this.isUpdatingFromForm = false; }); diff --git a/UI/Web/src/app/_services/external-source.service.ts b/UI/Web/src/app/_services/external-source.service.ts index 5fbc5a397..3f974b430 100644 --- a/UI/Web/src/app/_services/external-source.service.ts +++ b/UI/Web/src/app/_services/external-source.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {environment} from "../../environments/environment"; import { HttpClient } from "@angular/common/http"; import {ExternalSource} from "../_models/sidenav/external-source"; @@ -9,9 +9,10 @@ import {map} from "rxjs/operators"; providedIn: 'root' }) export class ExternalSourceService { + private httpClient = inject(HttpClient); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } getExternalSources() { return this.httpClient.get>(this.baseUrl + 'stream/external-sources'); diff --git a/UI/Web/src/app/_services/filter.service.ts b/UI/Web/src/app/_services/filter.service.ts index 2b9681e90..f5c50cf23 100644 --- a/UI/Web/src/app/_services/filter.service.ts +++ b/UI/Web/src/app/_services/filter.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {FilterV2} from "../_models/metadata/v2/filter-v2"; import {environment} from "../../environments/environment"; import {HttpClient} from "@angular/common/http"; @@ -8,9 +8,10 @@ import {SmartFilter} from "../_models/metadata/v2/smart-filter"; providedIn: 'root' }) export class FilterService { + private httpClient = inject(HttpClient); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } saveFilter(filter: FilterV2) { return this.httpClient.post(this.baseUrl + 'filter/update', filter); diff --git a/UI/Web/src/app/_services/image.service.ts b/UI/Web/src/app/_services/image.service.ts index 8c559b726..7af276266 100644 --- a/UI/Web/src/app/_services/image.service.ts +++ b/UI/Web/src/app/_services/image.service.ts @@ -9,6 +9,9 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; providedIn: 'root' }) export class ImageService { + private accountService = inject(AccountService); + private themeService = inject(ThemeService); + private readonly destroyRef = inject(DestroyRef); baseUrl = environment.apiUrl; apiKey: string = ''; @@ -20,7 +23,7 @@ export class ImageService { public nextChapterImage = 'assets/images/image-placeholder.dark-min.png'; public noPersonImage = 'assets/images/error-person-missing.dark.min.png'; - constructor(private accountService: AccountService, private themeService: ThemeService) { + constructor() { this.themeService.currentTheme$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(theme => { if (this.themeService.isDarkTheme()) { this.placeholderImage = 'assets/images/image-placeholder.dark-min.png'; diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index 5d3a89f0a..7a9d93ed3 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -1,5 +1,5 @@ import {HttpClient} from '@angular/common/http'; -import {DestroyRef, Injectable} from '@angular/core'; +import { DestroyRef, Injectable, inject } from '@angular/core'; import {of} from 'rxjs'; import {filter, map, tap} from 'rxjs/operators'; import {environment} from 'src/environments/environment'; @@ -14,13 +14,17 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; providedIn: 'root' }) export class LibraryService { + private httpClient = inject(HttpClient); + private readonly messageHub = inject(MessageHubService); + private readonly destroyRef = inject(DestroyRef); + baseUrl = environment.apiUrl; private libraryNames: {[key:number]: string} | undefined = undefined; private libraryTypes: {[key: number]: LibraryType} | undefined = undefined; - constructor(private httpClient: HttpClient, private readonly messageHub: MessageHubService, private readonly destroyRef: DestroyRef) { + constructor() { this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef), filter(e => e.event === EVENTS.LibraryModified), tap((e) => { console.log('LibraryModified event came in, clearing library name cache'); diff --git a/UI/Web/src/app/_services/localization.service.ts b/UI/Web/src/app/_services/localization.service.ts index 7519a9562..0534fdb3c 100644 --- a/UI/Web/src/app/_services/localization.service.ts +++ b/UI/Web/src/app/_services/localization.service.ts @@ -9,6 +9,8 @@ import {TranslocoService} from "@jsverse/transloco"; providedIn: 'root' }) export class LocalizationService { + private httpClient = inject(HttpClient); + private readonly translocoService = inject(TranslocoService); @@ -17,8 +19,6 @@ export class LocalizationService { private readonly localeSubject = new ReplaySubject(1); public readonly locales$ = this.localeSubject.asObservable(); - constructor(private httpClient: HttpClient) { } - getLocales() { return this.httpClient.get(this.baseUrl + 'locale').pipe(tap(locales => { this.localeSubject.next(locales); diff --git a/UI/Web/src/app/_services/member.service.ts b/UI/Web/src/app/_services/member.service.ts index d93098995..ec6aaa031 100644 --- a/UI/Web/src/app/_services/member.service.ts +++ b/UI/Web/src/app/_services/member.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { environment } from 'src/environments/environment'; import { Member } from '../_models/auth/member'; import {UserTokenInfo} from "../_models/kavitaplus/user-token-info"; @@ -8,11 +8,11 @@ import {UserTokenInfo} from "../_models/kavitaplus/user-token-info"; providedIn: 'root' }) export class MemberService { + private httpClient = inject(HttpClient); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } - getMembers(includePending: boolean = false) { return this.httpClient.get(this.baseUrl + 'users?includePending=' + includePending); } diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index fe0702219..acbfe37bc 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -38,6 +38,8 @@ import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; providedIn: 'root' }) export class MetadataService { + private httpClient = inject(HttpClient); + private readonly translocoService = inject(TranslocoService); private readonly libraryService = inject(LibraryService); @@ -47,11 +49,9 @@ export class MetadataService { baseUrl = environment.apiUrl; private validLanguages: Array = []; private ageRatingPipe = new AgeRatingPipe(); - private mangaFormatPipe = new MangaFormatPipe(this.translocoService); + private mangaFormatPipe = new MangaFormatPipe(); private personRolePipe = new PersonRolePipe(); - constructor(private httpClient: HttpClient) { } - getSeriesMetadataFromPlus(seriesId: number, libraryType: LibraryType) { return this.httpClient.get(this.baseUrl + 'metadata/series-detail-plus?seriesId=' + seriesId + '&libraryType=' + libraryType); } diff --git a/UI/Web/src/app/_services/nav.service.ts b/UI/Web/src/app/_services/nav.service.ts index ce9f2df78..4e48527ea 100644 --- a/UI/Web/src/app/_services/nav.service.ts +++ b/UI/Web/src/app/_services/nav.service.ts @@ -1,5 +1,5 @@ import {DOCUMENT} from '@angular/common'; -import {DestroyRef, inject, Inject, Injectable, Renderer2, RendererFactory2, RendererStyleFlags2} from '@angular/core'; +import { DestroyRef, inject, Injectable, Renderer2, RendererFactory2, RendererStyleFlags2 } from '@angular/core'; import {filter, ReplaySubject, take} from 'rxjs'; import {HttpClient} from "@angular/common/http"; import {environment} from "../../environments/environment"; @@ -34,6 +34,9 @@ interface NavItem { providedIn: 'root' }) export class NavService { + private document = inject(DOCUMENT); + private httpClient = inject(HttpClient); + private readonly accountService = inject(AccountService); private readonly router = inject(Router); @@ -103,7 +106,9 @@ export class NavService { private renderer: Renderer2; baseUrl = environment.apiUrl; - constructor(@Inject(DOCUMENT) private document: Document, rendererFactory: RendererFactory2, private httpClient: HttpClient) { + constructor() { + const rendererFactory = inject(RendererFactory2); + this.renderer = rendererFactory.createRenderer(null, null); // To avoid flashing, let's check if we are authenticated before we show diff --git a/UI/Web/src/app/_services/person.service.ts b/UI/Web/src/app/_services/person.service.ts index bfb312d8a..95c1708d1 100644 --- a/UI/Web/src/app/_services/person.service.ts +++ b/UI/Web/src/app/_services/person.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {HttpClient, HttpParams} from "@angular/common/http"; import {environment} from "../../environments/environment"; import {Person, PersonRole} from "../_models/metadata/person"; @@ -17,11 +17,12 @@ import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; providedIn: 'root' }) export class PersonService { + private httpClient = inject(HttpClient); + private utilityService = inject(UtilityService); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient, private utilityService: UtilityService) { } - updatePerson(person: Person) { return this.httpClient.post(this.baseUrl + "person/update", person); } diff --git a/UI/Web/src/app/_services/reading-list.service.ts b/UI/Web/src/app/_services/reading-list.service.ts index 088263a33..1d778f143 100644 --- a/UI/Web/src/app/_services/reading-list.service.ts +++ b/UI/Web/src/app/_services/reading-list.service.ts @@ -1,5 +1,5 @@ import {HttpClient, HttpParams} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {map} from 'rxjs/operators'; import {environment} from 'src/environments/environment'; import {UtilityService} from '../shared/_services/utility.service'; @@ -14,11 +14,12 @@ import {Action, ActionItem} from './action-factory.service'; providedIn: 'root' }) export class ReadingListService { + private httpClient = inject(HttpClient); + private utilityService = inject(UtilityService); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient, private utilityService: UtilityService) { } - getReadingList(readingListId: number) { return this.httpClient.get(this.baseUrl + 'readinglist?readingListId=' + readingListId); } diff --git a/UI/Web/src/app/_services/recommendation.service.ts b/UI/Web/src/app/_services/recommendation.service.ts index b6795416f..692d92a08 100644 --- a/UI/Web/src/app/_services/recommendation.service.ts +++ b/UI/Web/src/app/_services/recommendation.service.ts @@ -1,5 +1,5 @@ import { HttpClient, HttpParams } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { map } from 'rxjs'; import { environment } from 'src/environments/environment'; import { UtilityService } from '../shared/_services/utility.service'; @@ -10,11 +10,12 @@ import { Series } from '../_models/series'; providedIn: 'root' }) export class RecommendationService { + private httpClient = inject(HttpClient); + private utilityService = inject(UtilityService); + private baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient, private utilityService: UtilityService) { } - getQuickReads(libraryId: number, pageNum?: number, itemsPerPage?: number) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); diff --git a/UI/Web/src/app/_services/review.service.ts b/UI/Web/src/app/_services/review.service.ts index b8635bcf8..112c9823d 100644 --- a/UI/Web/src/app/_services/review.service.ts +++ b/UI/Web/src/app/_services/review.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {UserReview} from "../_single-module/review-card/user-review"; import {environment} from "../../environments/environment"; import {HttpClient} from "@angular/common/http"; @@ -8,11 +8,11 @@ import {Rating} from "../_models/rating"; providedIn: 'root' }) export class ReviewService { + private httpClient = inject(HttpClient); + private baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } - deleteReview(seriesId: number, chapterId?: number) { if (chapterId) { return this.httpClient.delete(this.baseUrl + `review/chapter?chapterId=${chapterId}`); diff --git a/UI/Web/src/app/_services/scrobbling.service.ts b/UI/Web/src/app/_services/scrobbling.service.ts index cfc7b34ac..24c25964b 100644 --- a/UI/Web/src/app/_services/scrobbling.service.ts +++ b/UI/Web/src/app/_services/scrobbling.service.ts @@ -1,5 +1,5 @@ import {HttpClient, HttpParams} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {map} from 'rxjs/operators'; import {environment} from 'src/environments/environment'; import {TextResonse} from '../_types/text-response'; @@ -22,12 +22,12 @@ export enum ScrobbleProvider { providedIn: 'root' }) export class ScrobblingService { + private httpClient = inject(HttpClient); + private utilityService = inject(UtilityService); + baseUrl = environment.apiUrl; - - constructor(private httpClient: HttpClient, private utilityService: UtilityService) {} - hasTokenExpired(provider: ScrobbleProvider) { return this.httpClient.get(this.baseUrl + 'scrobbling/token-expired?provider=' + provider, TextResonse) .pipe(map(r => r === "true")); diff --git a/UI/Web/src/app/_services/search.service.ts b/UI/Web/src/app/_services/search.service.ts index 4a95fff99..48115bcdb 100644 --- a/UI/Web/src/app/_services/search.service.ts +++ b/UI/Web/src/app/_services/search.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { of } from 'rxjs'; import { environment } from 'src/environments/environment'; import { SearchResultGroup } from '../_models/search/search-result-group'; @@ -9,11 +9,11 @@ import { Series } from '../_models/series'; providedIn: 'root' }) export class SearchService { + private httpClient = inject(HttpClient); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } - search(term: string, includeChapterAndFiles: boolean = false) { if (term === '') { return of(new SearchResultGroup()); diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 9c436e636..b672f78ad 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -1,5 +1,5 @@ import {HttpClient, HttpParams} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {environment} from 'src/environments/environment'; @@ -26,13 +26,14 @@ import {FilterField} from "../_models/metadata/v2/filter-field"; providedIn: 'root' }) export class SeriesService { + private httpClient = inject(HttpClient); + private utilityService = inject(UtilityService); + baseUrl = environment.apiUrl; paginatedResults: PaginatedResult = new PaginatedResult(); paginatedSeriesForTagsResults: PaginatedResult = new PaginatedResult(); - constructor(private httpClient: HttpClient, private utilityService: UtilityService) { } - getAllSeriesV2(pageNum?: number, itemsPerPage?: number, filter?: FilterV2, context: QueryContext = QueryContext.None) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 4a71e836e..220eeab6c 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { environment } from 'src/environments/environment'; import {ServerInfoSlim} from '../admin/_models/server-info'; import { UpdateVersionEvent } from '../_models/events/update-version-event'; @@ -12,11 +12,11 @@ import {map} from "rxjs/operators"; providedIn: 'root' }) export class ServerService { + private http = inject(HttpClient); + baseUrl = environment.apiUrl; - constructor(private http: HttpClient) { } - getVersion(apiKey: string) { return this.http.get(this.baseUrl + 'plugin/version?apiKey=' + apiKey, TextResonse); } diff --git a/UI/Web/src/app/_services/statistics.service.ts b/UI/Web/src/app/_services/statistics.service.ts index cf80765f2..a805c187f 100644 --- a/UI/Web/src/app/_services/statistics.service.ts +++ b/UI/Web/src/app/_services/statistics.service.ts @@ -1,5 +1,5 @@ import {HttpClient, HttpParams} from '@angular/common/http'; -import {Inject, inject, Injectable} from '@angular/core'; +import {inject, Injectable} from '@angular/core'; import {environment} from 'src/environments/environment'; import {UserReadStatistics} from '../statistics/_models/user-read-statistics'; import {PublicationStatusPipe} from '../_pipes/publication-status.pipe'; @@ -34,13 +34,14 @@ export enum DayOfWeek providedIn: 'root' }) export class StatisticsService { + private httpClient = inject(HttpClient); + private save = inject(SAVER); + baseUrl = environment.apiUrl; translocoService = inject(TranslocoService); - publicationStatusPipe = new PublicationStatusPipe(this.translocoService); - mangaFormatPipe = new MangaFormatPipe(this.translocoService); - - constructor(private httpClient: HttpClient, @Inject(SAVER) private save: Saver) { } + publicationStatusPipe = new PublicationStatusPipe(); + mangaFormatPipe = new MangaFormatPipe(); getUserStatistics(userId: number, libraryIds: Array = []) { const url = `${this.baseUrl}stats/user/${userId}/read`; diff --git a/UI/Web/src/app/_services/theme.service.ts b/UI/Web/src/app/_services/theme.service.ts index 3e186f8ac..681b8662b 100644 --- a/UI/Web/src/app/_services/theme.service.ts +++ b/UI/Web/src/app/_services/theme.service.ts @@ -1,17 +1,9 @@ import {DOCUMENT} from '@angular/common'; -import { HttpClient } from '@angular/common/http'; -import { - DestroyRef, - inject, - Inject, - Injectable, - Renderer2, - RendererFactory2, - SecurityContext -} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {DestroyRef, effect, inject, Injectable, Renderer2, RendererFactory2, SecurityContext} from '@angular/core'; import {DomSanitizer} from '@angular/platform-browser'; import {ToastrService} from 'ngx-toastr'; -import {filter, map, ReplaySubject, take, tap} from 'rxjs'; +import {map, ReplaySubject, take, tap} from 'rxjs'; import {environment} from 'src/environments/environment'; import {ConfirmService} from '../shared/confirm.service'; import {NotificationProgressEvent} from '../_models/events/notification-progress-event'; @@ -23,15 +15,21 @@ import {translate} from "@jsverse/transloco"; import {DownloadableSiteTheme} from "../_models/theme/downloadable-site-theme"; import {NgxFileDropEntry} from "ngx-file-drop"; import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event"; -import {NavigationEnd, Router} from "@angular/router"; import {ColorscapeService} from "./colorscape.service"; import {ColorScape} from "../_models/theme/colorscape"; -import {debounceTime} from "rxjs/operators"; +import {AccountService} from "./account.service"; @Injectable({ providedIn: 'root' }) export class ThemeService { + private document = inject(DOCUMENT); + private httpClient = inject(HttpClient); + private domSanitizer = inject(DomSanitizer); + private confirmService = inject(ConfirmService); + private toastr = inject(ToastrService); + private accountService = inject(AccountService); + private readonly destroyRef = inject(DestroyRef); private readonly colorTransitionService = inject(ColorscapeService); @@ -44,7 +42,7 @@ export class ThemeService { private themesSource = new ReplaySubject(1); public themes$ = this.themesSource.asObservable(); - + private darkModeSource = new ReplaySubject(1); public isDarkMode$ = this.darkModeSource.asObservable(); @@ -57,9 +55,10 @@ export class ThemeService { private baseUrl = environment.apiUrl; - constructor(rendererFactory: RendererFactory2, @Inject(DOCUMENT) private document: Document, private httpClient: HttpClient, - messageHub: MessageHubService, private domSanitizer: DomSanitizer, private confirmService: ConfirmService, private toastr: ToastrService, - private router: Router) { + constructor() { + const rendererFactory = inject(RendererFactory2); + const messageHub = inject(MessageHubService); + this.renderer = rendererFactory.createRenderer(null, null); messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => { @@ -83,8 +82,15 @@ export class ThemeService { }); } + }); - + effect(() => { + const user = this.accountService.currentUserSignal(); + if (user?.preferences && user?.preferences.theme) { + this.setTheme(user.preferences.theme.name); + } else { + this.setTheme(this.defaultTheme); + } }); } diff --git a/UI/Web/src/app/_services/toggle.service.ts b/UI/Web/src/app/_services/toggle.service.ts index 0ad9813e3..1770fc827 100644 --- a/UI/Web/src/app/_services/toggle.service.ts +++ b/UI/Web/src/app/_services/toggle.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {NavigationStart, Router} from '@angular/router'; import {filter, ReplaySubject, take} from 'rxjs'; @@ -13,7 +13,9 @@ export class ToggleService { private toggleStateSource: ReplaySubject = new ReplaySubject(1); public toggleState$ = this.toggleStateSource.asObservable(); - constructor(router: Router) { + constructor() { + const router = inject(Router); + router.events .pipe(filter(event => event instanceof NavigationStart)) .subscribe((event) => { diff --git a/UI/Web/src/app/_services/upload.service.ts b/UI/Web/src/app/_services/upload.service.ts index f2a811161..20f9d2439 100644 --- a/UI/Web/src/app/_services/upload.service.ts +++ b/UI/Web/src/app/_services/upload.service.ts @@ -10,12 +10,12 @@ import {tap} from "rxjs"; providedIn: 'root' }) export class UploadService { + private httpClient = inject(HttpClient); + private baseUrl = environment.apiUrl; private readonly toastr = inject(ToastrService); - constructor(private httpClient: HttpClient) { } - uploadByUrl(url: string) { return this.httpClient.post(this.baseUrl + 'upload/upload-by-url', {url}, TextResonse); diff --git a/UI/Web/src/app/_services/volume.service.ts b/UI/Web/src/app/_services/volume.service.ts index 8c9f9e17e..a74e3bf33 100644 --- a/UI/Web/src/app/_services/volume.service.ts +++ b/UI/Web/src/app/_services/volume.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import {environment} from "../../environments/environment"; import { HttpClient } from "@angular/common/http"; import {Volume} from "../_models/volume"; @@ -8,11 +8,11 @@ import {TextResonse} from "../_types/text-response"; providedIn: 'root' }) export class VolumeService { + private httpClient = inject(HttpClient); + baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } - getVolumeMetadata(volumeId: number) { return this.httpClient.get(this.baseUrl + 'volume?volumeId=' + volumeId); } diff --git a/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.html b/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.html index 2be9b618d..e6a9d6b00 100644 --- a/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.html +++ b/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.html @@ -1 +1 @@ - + diff --git a/UI/Web/src/app/_single-module/annotations-tab/annotations-tab.component.html b/UI/Web/src/app/_single-module/annotations-tab/annotations-tab.component.html index 0135af417..16b318bf3 100644 --- a/UI/Web/src/app/_single-module/annotations-tab/annotations-tab.component.html +++ b/UI/Web/src/app/_single-module/annotations-tab/annotations-tab.component.html @@ -1,9 +1,11 @@
- - -
- -
-
-
+ +
+ @for(item of scroll.viewPortItems; let idx = $index; track item.id) { +
+ +
+ } +
+
diff --git a/UI/Web/src/app/_single-module/annotations-tab/annotations-tab.component.ts b/UI/Web/src/app/_single-module/annotations-tab/annotations-tab.component.ts index cf46374cf..f1a47beb0 100644 --- a/UI/Web/src/app/_single-module/annotations-tab/annotations-tab.component.ts +++ b/UI/Web/src/app/_single-module/annotations-tab/annotations-tab.component.ts @@ -1,23 +1,26 @@ -import {Component, input} from '@angular/core'; -import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component"; +import {ChangeDetectionStrategy, Component, input} from '@angular/core'; import {TranslocoDirective} from "@jsverse/transloco"; import {Annotation} from "../../book-reader/_models/annotations/annotation"; import { AnnotationCardComponent } from "../../book-reader/_components/_annotations/annotation-card/annotation-card.component"; +import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller"; @Component({ selector: 'app-annotations-tab', imports: [ - CarouselReelComponent, TranslocoDirective, - AnnotationCardComponent + AnnotationCardComponent, + VirtualScrollerModule ], templateUrl: './annotations-tab.component.html', - styleUrl: './annotations-tab.component.scss' + styleUrl: './annotations-tab.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush }) export class AnnotationsTabComponent { annotations = input.required(); + scrollingBlock = input.required(); + displaySeries = input(false); } diff --git a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html index ff7cac714..210e89a56 100644 --- a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html +++ b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html @@ -1,20 +1,20 @@ @if (actions.length > 0) { @if ((utilityService.activeBreakpoint$ | async)! <= Breakpoint.Tablet) { - } @else {
-
- +
@@ -45,7 +45,7 @@ }
- +
} diff --git a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.scss b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.scss index 19a986986..34b611b9f 100644 --- a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.scss +++ b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.scss @@ -42,7 +42,3 @@ float: right; padding: var(--bs-dropdown-item-padding-y) 0; } - -.btn { - padding: 5px; -} diff --git a/UI/Web/src/app/_single-module/cover-image/cover-image.component.html b/UI/Web/src/app/_single-module/cover-image/cover-image.component.html index d4ce0a5b1..cd350ff01 100644 --- a/UI/Web/src/app/_single-module/cover-image/cover-image.component.html +++ b/UI/Web/src/app/_single-module/cover-image/cover-image.component.html @@ -1,8 +1,8 @@ @if(mobileSeriesImgBackground === 'true') { - + } @else { - + }
@@ -18,7 +18,7 @@ @if (entity.pagesRead < entity.pages && entity.pagesRead > 0) {
- +
@if (continueTitle !== '') {
diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html index be0457481..4da6289c8 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html @@ -41,7 +41,7 @@

{{t('format-title')}}

- {{format | mangaFormat }} + {{format | mangaFormat }}
} @@ -80,7 +80,7 @@ + [errorImage]="imageService.errorWebLinkImage" /> @@ -94,7 +94,7 @@
- +
@@ -102,7 +102,7 @@
- +
@@ -110,7 +110,7 @@
- +
@@ -119,7 +119,7 @@
- +
@@ -127,7 +127,7 @@
- +
@@ -135,7 +135,7 @@
- +
@@ -143,7 +143,7 @@
- +
@@ -151,7 +151,7 @@
- +
@@ -159,7 +159,7 @@
- +
@@ -167,7 +167,7 @@
- +
@@ -175,7 +175,7 @@
- +
@@ -183,7 +183,7 @@
- +
diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html index 2b271bddf..f346768c5 100644 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html +++ b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html @@ -1,7 +1,7 @@ - +
diff --git a/UI/Web/src/app/announcements/_components/new-update-modal/new-update-modal.component.html b/UI/Web/src/app/announcements/_components/new-update-modal/new-update-modal.component.html index 8f71f69a6..a1afbe0ad 100644 --- a/UI/Web/src/app/announcements/_components/new-update-modal/new-update-modal.component.html +++ b/UI/Web/src/app/announcements/_components/new-update-modal/new-update-modal.component.html @@ -4,7 +4,7 @@
diff --git a/UI/Web/src/app/announcements/_components/update-notification/update-notification-modal.component.ts b/UI/Web/src/app/announcements/_components/update-notification/update-notification-modal.component.ts index 01f7321d6..4517dcffc 100644 --- a/UI/Web/src/app/announcements/_components/update-notification/update-notification-modal.component.ts +++ b/UI/Web/src/app/announcements/_components/update-notification/update-notification-modal.component.ts @@ -1,7 +1,7 @@ -import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, inject, Input, OnInit} from '@angular/core'; import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; import {UpdateVersionEvent} from 'src/app/_models/events/update-version-event'; -import {CommonModule} from "@angular/common"; + import {TranslocoDirective} from "@jsverse/transloco"; import {WikiLink} from "../../../_models/wiki"; import {ChangelogUpdateItemComponent} from "../changelog-update-item/changelog-update-item.component"; @@ -9,20 +9,19 @@ import {ChangelogUpdateItemComponent} from "../changelog-update-item/changelog-u @Component({ selector: 'app-update-notification-modal', - imports: [CommonModule, NgbModalModule, TranslocoDirective, ChangelogUpdateItemComponent], + imports: [NgbModalModule, TranslocoDirective, ChangelogUpdateItemComponent], templateUrl: './update-notification-modal.component.html', styleUrls: ['./update-notification-modal.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class UpdateNotificationModalComponent implements OnInit { + private readonly modal = inject(NgbActiveModal); @Input({required: true}) updateData!: UpdateVersionEvent; updateUrl: string = WikiLink.UpdateNative; // TODO: I think I can remove this and just use NewUpdateModalComponent instead which handles both Nightly/Stable - constructor(public modal: NgbActiveModal) { } - ngOnInit() { if (this.updateData.isDocker) { this.updateUrl = WikiLink.UpdateDocker; diff --git a/UI/Web/src/app/app.component.html b/UI/Web/src/app/app.component.html index ac8c904da..82c83e69d 100644 --- a/UI/Web/src/app/app.component.html +++ b/UI/Web/src/app/app.component.html @@ -3,12 +3,12 @@ @if (currentUser && (navService.navbarVisible$ | async) === true) {
- +
} } - + @let sideNavVisible = navService.sideNavVisibility$ | async; @let sideNavCollapsed = navService.sideNavCollapsed$ | async; @@ -17,9 +17,9 @@
@if (sideNavVisible) { @if(usePreferenceSideNav) { - + } @else { - + } } @@ -29,11 +29,11 @@
- +
} @else { - + }
diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index 485fe30f5..006642d00 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -50,7 +50,10 @@ export class AppComponent implements OnInit { transitionState$!: Observable; - constructor(ratingConfig: NgbRatingConfig, modalConfig: NgbModalConfig) { + constructor() { + const ratingConfig = inject(NgbRatingConfig); + const modalConfig = inject(NgbModalConfig); + modalConfig.fullscreen = 'lg'; diff --git a/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.html b/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.html index 2f1586f91..f92e228e3 100644 --- a/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.html +++ b/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.html @@ -1,5 +1,5 @@ -
+
{{ annotation().ownerUsername }} @@ -7,7 +7,7 @@
{{ annotation().createdUtc | utcToLocaleDate | date: 'shortDate' }}
-
+

{{annotation().selectedText}}

@@ -16,7 +16,7 @@
@let content = annotation().comment; @if (content !== '\"\"') { - + } @else { {{null | defaultValue}} } @@ -24,15 +24,6 @@
diff --git a/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.scss b/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.scss index 0ba4d486a..4e032c440 100644 --- a/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.scss +++ b/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.scss @@ -1,10 +1,16 @@ .annotation-card { - max-width: 400px; + max-width: 25rem; + width: 25rem; + + .card-body { + height: 7.78rem; + overflow: auto; + } } .content-quote { color: var(--drawer-text-color); - max-height: 320px; + max-height: 20rem; overflow: hidden; display: -webkit-box; line-clamp: 2; @@ -12,6 +18,13 @@ -webkit-box-orient: vertical; } +.spoilers { + filter: blur(3px); +} + :host ::ng-deep quill-view { color: var(--drawer-text-color); } +:host ::ng-deep .ql-editor { + padding: 0; +} diff --git a/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.ts b/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.ts index 865bf7a6f..6ec77577a 100644 --- a/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.ts +++ b/UI/Web/src/app/book-reader/_components/_annotations/annotation-card/annotation-card.component.ts @@ -12,7 +12,7 @@ import { import {Annotation} from "../../../_models/annotations/annotation"; import {UtcToLocaleDatePipe} from "../../../../_pipes/utc-to-locale-date.pipe"; import {QuillViewComponent} from "ngx-quill"; -import {DatePipe, NgStyle} from "@angular/common"; +import {DatePipe, NgClass, NgStyle} from "@angular/common"; import {translate, TranslocoDirective} from "@jsverse/transloco"; import {ConfirmService} from "../../../../shared/confirm.service"; import {AnnotationService} from "../../../../_services/annotation.service"; @@ -31,7 +31,8 @@ import {ActivatedRoute, Router, RouterLink} from "@angular/router"; TranslocoDirective, DefaultValuePipe, NgStyle, - RouterLink + RouterLink, + NgClass ], templateUrl: './annotation-card.component.html', styleUrl: './annotation-card.component.scss', @@ -50,6 +51,10 @@ export class AnnotationCardComponent { annotation = model.required(); allowEdit = input(true); showPageLink = input(true); + /** + * If sizes should be forced. Turned of in drawer to account for manual resize + */ + forceSize = input(true); /** * Redirects to the reader with annotation in view */ @@ -59,6 +64,7 @@ export class AnnotationCardComponent { @Output() navigate = new EventEmitter(); titleColor: Signal; + hasClicked = model(false); constructor() { diff --git a/UI/Web/src/app/book-reader/_components/_annotations/highlight-bar/highlight-bar.component.html b/UI/Web/src/app/book-reader/_components/_annotations/highlight-bar/highlight-bar.component.html index 957c4dbab..e9bed0e2d 100644 --- a/UI/Web/src/app/book-reader/_components/_annotations/highlight-bar/highlight-bar.component.html +++ b/UI/Web/src/app/book-reader/_components/_annotations/highlight-bar/highlight-bar.component.html @@ -32,7 +32,7 @@ [selected]="slot.slotNumber === selectedSlotIndex()" (selectPicker)="selectSlot(index, slot)" [editMode]="isEditMode()" - [canChangeEditMode]="canChangeEditMode()" + [canChangeEditMode]="allowEditMode()" (editModeChange)="isEditMode.set($event)" [first]="$first" [last]="$last" @@ -41,12 +41,14 @@
} - @if (desktopLayout()) { + @if (desktopLayout() && allowEditMode()) { } diff --git a/UI/Web/src/app/book-reader/_components/_annotations/highlight-bar/highlight-bar.component.ts b/UI/Web/src/app/book-reader/_components/_annotations/highlight-bar/highlight-bar.component.ts index 905a203d5..8eb8d4f10 100644 --- a/UI/Web/src/app/book-reader/_components/_annotations/highlight-bar/highlight-bar.component.ts +++ b/UI/Web/src/app/book-reader/_components/_annotations/highlight-bar/highlight-bar.component.ts @@ -31,7 +31,7 @@ export class HighlightBarComponent { isCollapsed = model(true); canCollapse = model(true); isEditMode = model(false); - canChangeEditMode = model(true); + allowEditMode = model(true); slots = this.annotationService.slots; @@ -54,7 +54,7 @@ export class HighlightBarComponent { } toggleEditMode() { - if (!this.canChangeEditMode()) return; + if (!this.allowEditMode()) return; const existingEdit = this.isEditMode(); this.isEditMode.set(!existingEdit); diff --git a/UI/Web/src/app/book-reader/_components/_drawers/epub-setting-drawer/epub-setting-drawer.component.html b/UI/Web/src/app/book-reader/_components/_drawers/epub-setting-drawer/epub-setting-drawer.component.html index 390d12d88..828be4605 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/epub-setting-drawer/epub-setting-drawer.component.html +++ b/UI/Web/src/app/book-reader/_components/_drawers/epub-setting-drawer/epub-setting-drawer.component.html @@ -1,20 +1,21 @@
-
+
{{t('title')}}
- +
@let sId = seriesId(); @let rp = readingProfile(); @if (sId && rp) { - + /> }
diff --git a/UI/Web/src/app/book-reader/_components/_drawers/epub-setting-drawer/epub-setting-drawer.component.scss b/UI/Web/src/app/book-reader/_components/_drawers/epub-setting-drawer/epub-setting-drawer.component.scss index 5cec79d06..7e6e64c65 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/epub-setting-drawer/epub-setting-drawer.component.scss +++ b/UI/Web/src/app/book-reader/_components/_drawers/epub-setting-drawer/epub-setting-drawer.component.scss @@ -4,3 +4,12 @@ display: flex; flex-direction: column; } + +.drawer-text i { + color: var(--drawer-text-color); + font-size: 16px; +} + +.drawer-text:hover i { + color: var(--drawer-text-color) +} diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.html b/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.html index ccc26f8dc..a2d72d967 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.html +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.html @@ -1,14 +1,24 @@ + + +
{{t('title')}}
- +
- @if (annotations.length > FilterAfter) { + @if (annotations().length > FilterAfter) {
@@ -20,7 +30,7 @@ } @for(annotation of annotations() | filter: filterList; track annotation.comment + annotation.highlightColor + annotation.containsSpolier) { - + } @empty {

{{t('no-data')}}

diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.scss b/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.scss index 977ab17b5..bb7517052 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.scss +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.scss @@ -6,4 +6,12 @@ flex-direction: column; } +.drawer-text i { + color: var(--drawer-text-color); + font-size: 16px; +} + +.drawer-text:hover i { + color: var(--drawer-text-color) +} diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.ts b/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.ts index e71b1e62c..35cb540d8 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.ts +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component.ts @@ -6,6 +6,10 @@ import {Annotation} from "../../../_models/annotations/annotation"; import {AnnotationService} from "../../../../_services/annotation.service"; import {FilterPipe} from "../../../../_pipes/filter.pipe"; import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; +import { + OffCanvasResizeComponent, + ResizeMode +} from "../../../../shared/_components/off-canvas-resize/off-canvas-resize.component"; @Component({ selector: 'app-view-annotations-drawer', @@ -13,7 +17,8 @@ import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; TranslocoDirective, AnnotationCardComponent, FilterPipe, - ReactiveFormsModule + ReactiveFormsModule, + OffCanvasResizeComponent ], templateUrl: './view-annotations-drawer.component.html', styleUrl: './view-annotations-drawer.component.scss', @@ -30,7 +35,7 @@ export class ViewAnnotationsDrawerComponent { formGroup = new FormGroup({ filter: new FormControl('', []) }); - readonly FilterAfter = 6; + readonly FilterAfter = 4; handleDelete(annotation: Annotation) { this.annotationService.delete(annotation.id).subscribe(); @@ -50,4 +55,7 @@ export class ViewAnnotationsDrawerComponent { return listItem.comment.toLowerCase().indexOf(query) >= 0 || listItem.pageNumber.toString().indexOf(query) >= 0 || (listItem.selectedText ?? '').toLowerCase().indexOf(query) >= 0; } + + protected readonly window = window; + protected readonly ResizeMode = ResizeMode; } diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-bookmarks-drawer/view-bookmark-drawer.component.html b/UI/Web/src/app/book-reader/_components/_drawers/view-bookmarks-drawer/view-bookmark-drawer.component.html index 95cade611..017b6af20 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-bookmarks-drawer/view-bookmark-drawer.component.html +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-bookmarks-drawer/view-bookmark-drawer.component.html @@ -3,7 +3,9 @@
{{t('title')}}
- +
diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-bookmarks-drawer/view-bookmark-drawer.component.scss b/UI/Web/src/app/book-reader/_components/_drawers/view-bookmarks-drawer/view-bookmark-drawer.component.scss index f31bffc12..f8ae0d0c6 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-bookmarks-drawer/view-bookmark-drawer.component.scss +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-bookmarks-drawer/view-bookmark-drawer.component.scss @@ -5,6 +5,15 @@ flex-direction: column; } +.drawer-text i { + color: var(--drawer-text-color); + font-size: 16px; +} + +.drawer-text:hover i { + color: var(--drawer-text-color) +} + .bookmark-item { display: flex; background: var(--bs-dark, #2b2b2b); diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.html b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.html index 7fa69440c..54c148858 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.html +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.html @@ -1,6 +1,13 @@ -
+ + +
@@ -27,7 +34,7 @@ }
-
+
@if (isEditOrCreateMode() && formGroup) {
@@ -41,6 +48,27 @@ {{t('contains-spoilers-label')}}
} + @switch (mode()) { + @case (AnnotationMode.Create) { + + } + @case (AnnotationMode.View) { + @let an = annotation(); + @if (an) { +
+
+ {{ an.ownerUsername }} +
+
{{ an.createdUtc | utcToLocaleDate | date: 'shortDate' }}
+
+ + @if (an.ownerUsername === accountService.currentUserSignal()?.username) { + + } + + } + } + }
@@ -57,42 +85,34 @@ }
-
- @if (annotation() && formGroup) { - -
-
-
-

-
- {{annotation()! | pageChapterLabel}} -
-
- - - -
- @if (isEditOrCreateMode()) { - - - } @else { - - } -
- - @if (mode() === AnnotationMode.Create) { -
- +
+ @if (annotation() && formGroup) { + +
+
+
+

+ {{annotation()! | pageChapterLabel}} +
+
+ + + +
+ @if (isEditOrCreateMode()) { + + } @else { + } +
- - } + + } -
+
diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.scss b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.scss index eb96a89e7..4573511c7 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.scss +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.scss @@ -45,4 +45,9 @@ $green-color: rgba(34, 197, 94); background-color: $green-color; } +::ng-deep .ql-editor { + height: calc(var(--drawer-height) - 278px); + overflow: auto; +} + diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.ts b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.ts index f29eaf532..5cc1e284a 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.ts +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-edit-annotation-drawer/view-edit-annotation-drawer.component.ts @@ -23,13 +23,16 @@ import {HighlightBarComponent} from "../../_annotations/highlight-bar/highlight- import {SlotColorPipe} from "../../../../_pipes/slot-color.pipe"; import {User} from "../../../../_models/user"; import {DomSanitizer, SafeHtml} from "@angular/platform-browser"; -import {DOCUMENT, NgStyle} from "@angular/common"; +import {DatePipe, DOCUMENT, NgStyle} from "@angular/common"; import {SafeHtmlPipe} from "../../../../_pipes/safe-html.pipe"; import {EpubHighlightService} from "../../../../_services/epub-highlight.service"; import {PageChapterLabelPipe} from "../../../../_pipes/page-chapter-label.pipe"; import {UserBreakpoint, UtilityService} from "../../../../shared/_services/utility.service"; import {QuillTheme, QuillWrapperComponent} from "../../quill-wrapper/quill-wrapper.component"; import {ContentChange, QuillViewComponent} from "ngx-quill"; +import {UtcToLocaleDatePipe} from "../../../../_pipes/utc-to-locale-date.pipe"; +import {AccountService} from "../../../../_services/account.service"; +import {OffCanvasResizeComponent, ResizeMode} from "../../../../shared/_components/off-canvas-resize/off-canvas-resize.component"; export enum AnnotationMode { View = 0, @@ -49,13 +52,17 @@ const INIT_HIGHLIGHT_DELAY = 200; NgStyle, PageChapterLabelPipe, QuillWrapperComponent, - QuillViewComponent + QuillViewComponent, + DatePipe, + UtcToLocaleDatePipe, + OffCanvasResizeComponent ], templateUrl: './view-edit-annotation-drawer.component.html', styleUrl: './view-edit-annotation-drawer.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) export class ViewEditAnnotationDrawerComponent implements OnInit { + private readonly activeOffcanvas = inject(NgbActiveOffcanvas); private readonly annotationService = inject(AnnotationService); private readonly destroyRef = inject(DestroyRef); @@ -66,6 +73,7 @@ export class ViewEditAnnotationDrawerComponent implements OnInit { private readonly epubHighlightService = inject(EpubHighlightService); private readonly fb = inject(NonNullableFormBuilder); protected readonly utilityService = inject(UtilityService); + protected readonly accountService = inject(AccountService); @ViewChild('renderTarget', {read: ViewContainerRef}) renderTarget!: ViewContainerRef; @@ -193,6 +201,25 @@ export class ViewEditAnnotationDrawerComponent implements OnInit { hasSpoiler: this.fb.control(false, []), selectedSlotIndex: this.fb.control(0, []), }); + + effect(() => { + const editMode = this.isEditMode(); + if (!editMode) return; + + this.formGroup.valueChanges.pipe( + debounceTime(350), + switchMap(_ => { + const updatedAnnotation = this.annotation(); + if (!updatedAnnotation) return of(); + + updatedAnnotation.containsSpoiler = this.formGroup.get('hasSpoiler')!.value; + updatedAnnotation.comment = JSON.stringify(this.annotationNote); + + return this.annotationService.updateAnnotation(updatedAnnotation); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(); + }); } ngOnInit(){ @@ -203,24 +230,6 @@ export class ViewEditAnnotationDrawerComponent implements OnInit { this.formGroup.get('hasSpoiler')!.setValue(annotation.containsSpoiler); this.formGroup.get('selectedSlotIndex')!.setValue(annotation.selectedSlotIndex); } - - if (!this.isEditMode()) { - return; - } - - this.formGroup.valueChanges.pipe( - debounceTime(350), - switchMap(_ => { - const updatedAnnotation = this.annotation(); - if (!updatedAnnotation) return of(); - - updatedAnnotation.containsSpoiler = this.formGroup.get('hasSpoiler')!.value; - updatedAnnotation.comment = JSON.stringify(this.annotationNote); - - return this.annotationService.updateAnnotation(updatedAnnotation); - }), - takeUntilDestroyed(this.destroyRef) - ).subscribe(); } createAnnotation() { @@ -238,6 +247,15 @@ export class ViewEditAnnotationDrawerComponent implements OnInit { }); } + switchToEditMode() { + if (this.isEditMode()) return; + + const annotation = this.annotation(); + if (annotation == null || annotation.ownerUsername !== this.accountService.currentUserSignal()?.username) return; + + this.mode.set(AnnotationMode.Edit); + } + changeSlotIndex(slotIndex: number) { const annotation = this.annotation(); @@ -326,4 +344,6 @@ export class ViewEditAnnotationDrawerComponent implements OnInit { protected readonly AnnotationMode = AnnotationMode; protected readonly UserBreakpoint = UserBreakpoint; protected readonly QuillTheme = QuillTheme; + protected readonly ResizeMode = ResizeMode; + protected readonly window = window; } diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-toc-drawer/view-toc-drawer.component.html b/UI/Web/src/app/book-reader/_components/_drawers/view-toc-drawer/view-toc-drawer.component.html index d267ca5b8..f5f4af793 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-toc-drawer/view-toc-drawer.component.html +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-toc-drawer/view-toc-drawer.component.html @@ -3,7 +3,9 @@
{{t('title')}}
- +
diff --git a/UI/Web/src/app/book-reader/_components/_drawers/view-toc-drawer/view-toc-drawer.component.scss b/UI/Web/src/app/book-reader/_components/_drawers/view-toc-drawer/view-toc-drawer.component.scss index 5cec79d06..7e6e64c65 100644 --- a/UI/Web/src/app/book-reader/_components/_drawers/view-toc-drawer/view-toc-drawer.component.scss +++ b/UI/Web/src/app/book-reader/_components/_drawers/view-toc-drawer/view-toc-drawer.component.scss @@ -4,3 +4,12 @@ display: flex; flex-direction: column; } + +.drawer-text i { + color: var(--drawer-text-color); + font-size: 16px; +} + +.drawer-text:hover i { + color: var(--drawer-text-color) +} diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html index 0b3d8b9f2..ade824d22 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html @@ -5,7 +5,7 @@
{{t('skip-header')}} - + @if (page() !== undefined) { - + (refreshToC)="refreshPersonalToC()" /> }
@@ -48,7 +47,7 @@ @if (shouldShowBottomActionBar()) {
- +
}
@@ -57,33 +56,36 @@ @if (shouldShowMenu()) {
-
- +
+
+ +
-
+
@if (isLoading()) {
{{ t('loading-book') }}
} @else { - @if (incognitoMode()) { - +
+ @if (incognitoMode()) { + ({{ t('incognito-mode-label') }}) - } - - {{ bookTitle() }} - @if (utilityService.getActiveBreakpoint() >= Breakpoint.Desktop) { - - {{ authorText() }} } + + {{ bookTitle() }} + @if (utilityService.getActiveBreakpoint() >= Breakpoint.Desktop) { + - {{ authorText() }} + } - +
}
-
+
@if (!this.adhocPageHistory.isEmpty()) { @@ -115,50 +120,60 @@ -
- +
- @if (!this.adhocPageHistory.isEmpty()) { - - } + + @if (!this.adhocPageHistory.isEmpty()) { + + } +
-
+
@if(!isLoading()) { - {{t('page-num-label', {page: virtualizedPageNum()})}} / {{virtualizedMaxPages()}} - + + {{t('page-num-label', {page: virtualizedPageNum() + 1})}} / {{virtualizedMaxPages()}} + - {{t('completion-label', {percent: (virtualizedPageNum() / virtualizedMaxPages()) | percent})}} - @if (readingTimeLeftResource.value(); as timeLeft) { - , - - - {{timeLeft! | readTimeLeft:true }} +
+ + {{t('completion-label', {percent: (virtualizedPageNum() / virtualizedMaxPages()) | percent})}} + @if (readingTimeLeftResource.value(); as timeLeft) { + + + + {{timeLeft! | readTimeLeft:true }} - } + } +
} @if (debugMode()) { @let vp = getVirtualPage(); - {{vp[0] * vp[2]}} / {{vp[1] * vp[2]}} + {{vp[0] * vp[2]}} / {{vp[1] * vp[2]}} }
- +
+ +
+
diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss index 9978d1cd4..40b71842f 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss @@ -102,6 +102,11 @@ $action-bar-height: 38px; max-height: $action-bar-height; height: $action-bar-height; + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + padding: 0 0.5rem; + .book-title-text { text-align: center; text-overflow: ellipsis; @@ -129,6 +134,31 @@ $action-bar-height: 38px; } } +@media (min-width: 876px) { + .action-bar { + grid-template-columns: 1fr auto 1fr; + } + + .center-group { + justify-self: center; + } + + .right-group { + justify-self: end; + } +} + +/* Mobile - 2 columns */ +@media (max-width: 875px) { + .action-bar { + grid-template-columns: auto 1fr; + } + + .right-group { + justify-self: end; + } +} + .reader-container { outline: none; // Only the reading section itself shouldn't receive any outline. We use it to shift focus in fullscreen mode overflow: auto; diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index f512fe40f..037ffb004 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -23,7 +23,7 @@ import { import {DOCUMENT, NgClass, NgStyle, NgTemplateOutlet, PercentPipe} from '@angular/common'; import {ActivatedRoute, Router} from '@angular/router'; import {ToastrService} from 'ngx-toastr'; -import {forkJoin, fromEvent, merge, of, switchMap} from 'rxjs'; +import {firstValueFrom, forkJoin, fromEvent, merge, of, switchMap} from 'rxjs'; import {catchError, debounceTime, distinctUntilChanged, filter, take, tap} from 'rxjs/operators'; import {Chapter} from 'src/app/_models/chapter'; import {NavService} from 'src/app/_services/nav.service'; @@ -81,6 +81,8 @@ interface HistoryPoint { scrollPart: string; } +type Container = {left: number, right: number, top: number, bottom: number, width: number, height: number}; + const TOP_OFFSET = -(50 + 10) * 1.5; // px the sticky header takes up // TODO: Do I need this or can I change it with new fixed top height const COLUMN_GAP = 20; // px @@ -296,7 +298,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { pageNumber: this.pageNum(), }), loader: async ({params}) => { - return this.readerService.getTimeLeftForChapter(params.seriesId, params.chapterId).toPromise(); + return firstValueFrom(this.readerService.getTimeLeftForChapter(params.seriesId, params.chapterId)); } }); @@ -449,9 +451,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { const showForVerticalDefault = !isColumnMode && isVerticalLayout; + const showWhenDefaultLayout = !isColumnMode && !isVerticalLayout; + const otherCondition = !immersiveMode || isDrawerOpen || actionBarVisible; - return (baseCondition || showForVerticalDefault) && otherCondition; + return (baseCondition || showForVerticalDefault || showWhenDefaultLayout) && otherCondition; }); @@ -642,7 +646,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } }); - } /** @@ -732,6 +735,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.lastSeenScrollPartPath !== '') { this.saveProgress(); + + if (this.debugMode()) { + this.logSelectedElement(); + } + } } @@ -754,6 +762,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.readerService.disableWakeLock(); + // Remove any debug viewport things + this.document.querySelector('#test')?.remove(); + this.themeService.clearBookTheme(); this.themeService.currentTheme$.pipe(take(1)).subscribe(theme => { @@ -1149,7 +1160,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { async promptForPage() { const promptConfig = {...this.confirmService.defaultPrompt}; promptConfig.header = translate('book-reader.go-to-page'); - promptConfig.content = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages() - 1}); + promptConfig.content = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages()}); const goToPageNum = await this.confirmService.prompt(undefined, promptConfig); @@ -1163,13 +1174,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { const goToPageNum = await this.promptForPage(); if (goToPageNum === null) { return; } - page = parseInt(goToPageNum.trim(), 10); + page = parseInt(goToPageNum.trim(), 10) - 1; // -1 since the UI displays with a +1 } if (page === undefined || this.pageNum() === page) { return; } - if (page > this.maxPages() - 1) { - page = this.maxPages() - 1; + if (page > this.maxPages()) { + page = this.maxPages(); } else if (page < 0) { page = 0; } @@ -1270,10 +1281,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { textColor = 'black'; } + const offSetX = Math.min(32, imgRect.width * 0.05); + const offSetY = Math.min(32, imgRect.height * 0.05); + icon.style.cssText = ` position: absolute; - left: ${relativeX + imgRect.width - 16 * 2}px; - top: ${relativeY + imgRect.height - 16 * 2}px; + left: ${imgRect.width + relativeX - offSetX}px; + top: ${imgRect.height + relativeY - offSetY}px; margin: 0; transform-origin: bottom right; padding-top: 5px; @@ -1396,7 +1410,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { // we need to click the document before arrow keys will scroll down. this.reader.nativeElement.focus(); - this.saveProgress(); + this.scroll(() => this.handleScrollEvent()); // Will set lastSeenXPath and save progress this.isLoading.set(false); this.cdRef.markForCheck(); @@ -1454,7 +1468,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { // afterFrame(() => this.scrollService.scrollTo(0, this.reader.nativeElement)); // }, SCROLL_DELAY); console.log('Scrolling via x axis to 0: ', 0, ' via ', this.reader.nativeElement); - this.scroll(() => this.scrollService.scrollToX(0, this.reader.nativeElement)); + this.scroll(() => this.scrollService.scrollTo(0, this.reader.nativeElement)); return; } @@ -1486,9 +1500,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { return; } - setTimeout(() => { - afterFrame(() => this.scrollService.scrollToX(0, this.bookContentElemRef.nativeElement)); - }, SCROLL_DELAY); + // setTimeout(() => { + // afterFrame(() => this.scrollService.scrollToX(0, this.bookContentElemRef.nativeElement)); + // }, SCROLL_DELAY); console.log('Scrolling via x axis to 0: ', 0, ' via ', this.bookContentElemRef.nativeElement); this.scroll(() => this.scrollService.scrollToX(0, this.bookContentElemRef.nativeElement)); @@ -1750,9 +1764,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { let resumeElement: string | null = null; if (!this.bookContentElemRef || !this.bookContentElemRef.nativeElement) return null; + const container = this.getViewportBoundingRect(); + const intersectingEntries = Array.from(this.bookContentElemRef.nativeElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span')) .filter(element => !element.classList.contains('no-observe')) .filter(entry => { + //return this.isPartiallyContainedIn(container, entry); return this.utilityService.isInViewport(entry, this.topOffset); }); @@ -1936,7 +1953,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.applyWritingStyle(); break; case "layoutMode": - this.applyLayoutMode(res.object as BookPageLayoutMode); + this.applyLayoutMode(res.object as BookPageLayoutMode, true); break; case "readingDirection": // No extra functionality needs to be done @@ -2062,12 +2079,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.cdRef.markForCheck(); } - applyLayoutMode(mode: BookPageLayoutMode) { - //const layoutModeChanged = mode !== this.layoutMode(); // TODO: This functionality wont work on the new signal-based logic - + applyLayoutMode(mode: BookPageLayoutMode, isChange: boolean = false) { this.clearTimeout(this.updateImageSizeTimeout); this.updateImageSizeTimeout = setTimeout( () => { - this.updateImageSizes() + this.updateImageSizes(); + this.injectImageBookmarkIndicators(true); }, 200); this.updateSingleImagePageStyles(); @@ -2084,11 +2100,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.cdRef.markForCheck(); }); - // When I switch layout, I might need to resume the progress point. - // if (mode === BookPageLayoutMode.Default && layoutModeChanged) { - // const lastSelector = this.lastSeenScrollPartPath; - // setTimeout(() => this.scrollTo(lastSelector)); - // } + const lastSelector = this.lastSeenScrollPartPath; + if (isChange && lastSelector !== '') { + setTimeout(() => this.scrollTo(lastSelector), SCROLL_DELAY); + } } applyImmersiveMode(immersiveMode: boolean) { @@ -2289,9 +2304,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { * * NOTE: On Scroll LayoutMode, the height/bottom are not correct */ - getViewportBoundingRect() { + getViewportBoundingRect(): Container { const margin = this.getMargin(); - const [currentVirtualPage, _, pageSize] = this.getVirtualPage(); + //const [currentVirtualPage, _, pageSize] = this.getVirtualPage(); + const pageSize = this.pageWidth(); const visibleBoundingBox = this.bookContentElemRef.nativeElement.getBoundingClientRect(); let bookContentPadding = 20; @@ -2327,10 +2343,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { const viewport = this.getViewportBoundingRect(); // Insert a debug element to help visualize - document.querySelector('#test')?.remove(); + this.document.querySelector('#test')?.remove(); // Create and inject the red rectangle div - const redRect = document.createElement('div'); + const redRect = this.document.createElement('div'); redRect.id = 'test'; redRect.style.position = 'absolute'; redRect.style.left = `${viewport.left}px`; @@ -2342,7 +2358,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { redRect.style.zIndex = '1000'; // Inject into the document - document.body.appendChild(redRect); + this.document.body.appendChild(redRect); } /** @@ -2370,6 +2386,43 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { return margin; } + /** + * A custom is visible method for column modes, that makes sense + * @param container + * @param element + */ + isPartiallyContainedIn(container: Container, element: Element) { + const rect = element.getBoundingClientRect(); + + if ( + rect.top >= container.top && + rect.bottom <= container.bottom && + rect.right <= container.right && + rect.left >= container.left + ) { + return true; + } + + // First element in the top, overflow to the left + const isCloseByTop = Math.abs(rect.top - container.top) <= 0.05 * container.height; + if (isCloseByTop && rect.left < container.left && rect.right < container.right && rect.right > container.left) { + return true; + } + + return false; + } + + logSelectedElement() { + const element = this.getElementFromXPath(this.lastSeenScrollPartPath); + if (element) { + console.log(element); + (element as HTMLElement).style.border = '1px solid red'; + setTimeout(() => { + (element as HTMLElement).style.border = ''; + }, 1_000); + } + } + protected readonly Breakpoint = Breakpoint; protected readonly environment = environment; diff --git a/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.html b/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.html index cb4752461..847d4bc76 100644 --- a/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.html +++ b/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.html @@ -1,4 +1,4 @@ -
+
@if (toolbarItem.value !== undefined) { - + } @else if (toolbarItem.values !== undefined) { - @for (value of toolbarItem.values; track value) { } } @else { - + } diff --git a/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.scss b/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.scss index 72357dad6..43b70ee60 100644 --- a/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.scss +++ b/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.scss @@ -167,4 +167,15 @@ padding-right: 0.9375rem; border-right: 0.0625rem solid var(--input-border-color); } + + .ql-tooltip { + border: 0.0625rem solid var(--primary-color); + background-color: var(--drawer-bg-color); + max-width: 90vw !important; + width: auto !important; + + &.ql-editing { + min-width: 20rem !important; + } + } } diff --git a/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.ts b/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.ts index 7894b069f..9b243a6fe 100644 --- a/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.ts +++ b/UI/Web/src/app/book-reader/_components/quill-wrapper/quill-wrapper.component.ts @@ -1,6 +1,8 @@ import {ChangeDetectionStrategy, Component, computed, EventEmitter, input, OnInit, Output} from '@angular/core'; import {ContentChange, QuillEditorComponent, QuillFormat} from "ngx-quill"; import {FormGroup, ReactiveFormsModule} from "@angular/forms"; +import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; +import {TranslocoDirective} from "@jsverse/transloco"; export enum QuillTheme { Snow = 'snow', @@ -48,6 +50,10 @@ export interface ToolbarItem { * Pass an **empty** array to use the quill defaults */ values?: string[]; + /** + * An optional translation key for a tooltip + */ + tooltipTranslationKey?: string } // There is very little documentation to what values are possible. @@ -77,7 +83,7 @@ const defaultToolbarItems: ToolbarItem[][] = [ ], [ - {key: QuillToolbarKey.Clean}, + {key: QuillToolbarKey.Clean, tooltipTranslationKey: 'clean-tooltip'}, ] ]; @@ -90,6 +96,8 @@ const defaultToolbarItems: ToolbarItem[][] = [ imports: [ QuillEditorComponent, ReactiveFormsModule, + NgbTooltip, + TranslocoDirective, ], templateUrl: './quill-wrapper.component.html', styleUrl: './quill-wrapper.component.scss', diff --git a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html index 7e1f5b682..500645bed 100644 --- a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html +++ b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html @@ -83,7 +83,7 @@
{{t('writing-style-tooltip')}} - +
diff --git a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts index 60f261e51..e20c1cb13 100644 --- a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts +++ b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts @@ -23,7 +23,6 @@ import { import {TranslocoDirective} from "@jsverse/transloco"; import {ReadingProfile, ReadingProfileKind} from "../../../_models/preferences/reading-profiles"; import {BookReadingProfileFormGroup, EpubReaderSettingsService} from "../../../_services/epub-reader-settings.service"; -import {LayoutMode} from "../../../manga-reader/_models/layout-mode"; /** * Used for book reader. Do not use for other components @@ -121,6 +120,9 @@ export class ReaderSettingsComponent implements OnInit { protected currentReadingProfile!: Signal; + protected isVerticalLayout!: Signal; + + async ngOnInit() { this.pageStyles = this.readerSettingsService.pageStyles; this.readingDirectionModel = this.readerSettingsService.readingDirection; diff --git a/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.html b/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.html index b5c70f86c..27eff498f 100644 --- a/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.html +++ b/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.html @@ -5,7 +5,7 @@ @if (bookChapters.length === 0) { @if (loading()) { - + } @else {
{{t('no-data')}} diff --git a/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html b/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html index 68be63da1..9d091a064 100644 --- a/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html +++ b/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html @@ -6,7 +6,7 @@
{{t('series-count', {num: series.length | number})}}
- + @if (filter) { + /> diff --git a/UI/Web/src/app/cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component.ts b/UI/Web/src/app/cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component.ts index da2742260..e28713a13 100644 --- a/UI/Web/src/app/cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component.ts +++ b/UI/Web/src/app/cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component.ts @@ -16,14 +16,14 @@ import {ToastrService} from 'ngx-toastr'; import {UserCollection} from 'src/app/_models/collection-tag'; import {ReadingList} from 'src/app/_models/reading-list'; import {CollectionTagService} from 'src/app/_services/collection-tag.service'; -import {CommonModule} from "@angular/common"; + import {FilterPipe} from "../../../_pipes/filter.pipe"; import {translate, TranslocoDirective} from "@jsverse/transloco"; import {ScrobbleProvider} from "../../../_services/scrobbling.service"; @Component({ selector: 'app-bulk-add-to-collection', - imports: [CommonModule, ReactiveFormsModule, FilterPipe, NgbModalModule, TranslocoDirective], + imports: [ReactiveFormsModule, FilterPipe, NgbModalModule, TranslocoDirective], templateUrl: './bulk-add-to-collection.component.html', styleUrls: ['./bulk-add-to-collection.component.scss'], encapsulation: ViewEncapsulation.None, // This is needed as per the bootstrap modal documentation to get styles to work. diff --git a/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.html b/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.html index b89b20d95..8d48fcf7f 100644 --- a/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.html +++ b/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.html @@ -34,7 +34,7 @@ {{t('promote-tooltip')}} - +
@@ -82,12 +82,11 @@ @if (pagination && series.length !== 0 && pagination.totalPages > 1) {
- + [collectionSize]="pagination.totalItems" />
}
@@ -101,7 +100,7 @@ + (resetClicked)="handleReset()" /> diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html index 42112d5ac..c1d784a9c 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html @@ -40,7 +40,7 @@
- + @if (formControl.errors) { @if (formControl.errors.required) { @@ -60,7 +60,7 @@ @if (editSeriesForm.get('localizedName'); as formControl) {
- +
} @@ -75,7 +75,7 @@
- +
@@ -118,7 +118,7 @@
- + @@ -186,7 +186,7 @@
- + @for (opt of publicationStatuses; track opt.value) { @@ -504,7 +504,7 @@ {{t(tabs[TabID.WebLinks])}}

{{t('web-link-description')}}

- +
} @@ -515,14 +515,14 @@ - +
  • {{t(tabs[TabID.Related])}} - +
  • @@ -689,7 +689,7 @@
    diff --git a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html index 1a2d067a2..a751336d5 100644 --- a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html +++ b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html @@ -4,7 +4,7 @@

    {{t('title')}}

    {{t('item-count', {num: collections.length | number})}}
    - + - + diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html index ea0e85fd1..8653431dd 100644 --- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html +++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html @@ -6,9 +6,9 @@ @if (collectionTag) {

    - + {{collectionTag.title}} - +

    }
    {{t('item-count', {num: series.length})}}
    @@ -23,14 +23,14 @@ @if (summary.length > 0 || collectionTag.source !== ScrobbleProvider.Kavita) {
    - + @if (collectionTag.source !== ScrobbleProvider.Kavita && collectionTag.missingSeriesFromSource !== null && series.length !== collectionTag.totalSourceCount && collectionTag.totalSourceCount > 0) {
    + [ngbTooltip]="collectionTag.source | scrobbleProviderName" tabindex="0" /> {{t('sync-progress', {title: series.length + ' / ' + collectionTag.totalSourceCount})}}
    @@ -39,7 +39,7 @@
    @if (summary.length > 0) {
    - +
    @if (collectionTag.source !== ScrobbleProvider.Kavita) { @@ -55,7 +55,7 @@ - + @if (filter) { + /> @if(!filterActive && series.length === 0) { diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts index 9b3f55240..95f0fe08f 100644 --- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts @@ -1,17 +1,5 @@ import {AsyncPipe, DatePipe, DOCUMENT} from '@angular/common'; -import { - AfterContentChecked, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - DestroyRef, - ElementRef, - EventEmitter, - inject, - Inject, - OnInit, - ViewChild -} from '@angular/core'; +import { AfterContentChecked, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, EventEmitter, inject, OnInit, ViewChild } from '@angular/core'; import {Title} from '@angular/platform-browser'; import {ActivatedRoute, Router} from '@angular/router'; import {NgbModal, NgbOffcanvas, NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; @@ -75,6 +63,8 @@ import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; DatePipe, DefaultDatePipe, ProviderImagePipe, AsyncPipe, ScrobbleProviderNamePipe, PromotedIconComponent] }) export class CollectionDetailComponent implements OnInit, AfterContentChecked { + private document = inject(DOCUMENT); + public readonly imageService = inject(ImageService); public readonly bulkSelectionService = inject(BulkSelectionService); @@ -181,7 +171,7 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked { } } - constructor(@Inject(DOCUMENT) private document: Document) { + constructor() { this.router.routeReuseStrategy.shouldReuseRoute = () => false; const routeId = this.route.snapshot.paramMap.get('id'); diff --git a/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.html b/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.html index dc3caacfe..0e0e026f4 100644 --- a/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.html +++ b/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.html @@ -7,7 +7,7 @@ + [attr.aria-label]="collection.source | scrobbleProviderName" /> }
    } diff --git a/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html b/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html index d8c3dba18..bd0b77831 100644 --- a/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html +++ b/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html @@ -16,7 +16,7 @@ } @empty { @if (isLoading) { - + } @else {

    {{t('nothing-found')}}

    } diff --git a/UI/Web/src/app/dashboard/_components/dashboard.component.html b/UI/Web/src/app/dashboard/_components/dashboard.component.html index e38390131..052b05539 100644 --- a/UI/Web/src/app/dashboard/_components/dashboard.component.html +++ b/UI/Web/src/app/dashboard/_components/dashboard.component.html @@ -1,5 +1,5 @@
    - + @if (libraries$ | async; as libraries) { @@ -23,19 +23,19 @@ @for(stream of streams; track stream.id) { @switch (stream.streamType) { @case (StreamType.OnDeck) { - + } @case (StreamType.RecentlyUpdated) { - + } @case (StreamType.NewlyAdded) { - + } @case (StreamType.SmartFilter) { - + } @case (StreamType.MoreInGenre) { - + } } @@ -45,7 +45,7 @@ + (reload)="reloadStream(item.id)" (dataChanged)="reloadStream(item.id)" /> } @@ -56,7 +56,7 @@ + (reload)="reloadStream(stream.id, true)" (dataChanged)="reloadStream(stream.id)" /> } @@ -69,9 +69,7 @@ - - + [linkUrl]="'/library/' + item.libraryId + '/series/' + item.seriesId" /> } @@ -81,7 +79,7 @@ @if(stream.api | async; as data) { - + } @@ -91,14 +89,14 @@ @if(stream.api | async; as data) { - + } } - +
    diff --git a/UI/Web/src/app/library-detail/library-detail.component.html b/UI/Web/src/app/library-detail/library-detail.component.html index c86261c73..2de45ec60 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.html +++ b/UI/Web/src/app/library-detail/library-detail.component.html @@ -3,15 +3,15 @@

    {{libraryName}} - +

    @if (active.fragment === '') {
    {{t('common.series-count', {num: pagination.totalItems | number})}}
    }
    - - + + @if (filter) { + [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true" /> diff --git a/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.html b/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.html index 3320fef16..25dc7a93c 100644 --- a/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.html +++ b/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.html @@ -1,18 +1,18 @@ - -
    - -  - - -  - -
    -
    +@if (isValid()) { +
    + @if (currentImage) { +  + } + @if (shouldRenderDouble$ | async) { +  + } +
    + } diff --git a/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.ts index 64e321bce..bec64c11e 100644 --- a/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.ts +++ b/UI/Web/src/app/manga-reader/_components/double-renderer-no-cover/double-no-cover-renderer.component.ts @@ -1,15 +1,5 @@ -import { DOCUMENT, NgIf, NgClass, AsyncPipe } from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, DestroyRef, - EventEmitter, - inject, - Inject, - Input, - OnInit, - Output -} from '@angular/core'; +import { DOCUMENT, NgClass, AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, EventEmitter, inject, Input, OnInit, Output } from '@angular/core'; import { Observable, of, map, tap, shareReplay, filter, combineLatest } from 'rxjs'; import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; @@ -30,9 +20,14 @@ import { SafeStylePipe } from '../../../_pipes/safe-style.pipe'; templateUrl: './double-no-cover-renderer.component.html', styleUrls: ['./double-no-cover-renderer.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, NgClass, AsyncPipe, SafeStylePipe] + imports: [NgClass, AsyncPipe, SafeStylePipe] }) export class DoubleNoCoverRendererComponent implements OnInit { + private readonly cdRef = inject(ChangeDetectorRef); + mangaReaderService = inject(MangaReaderService); + private document = inject(DOCUMENT); + readerService = inject(ReaderService); + @Input({required: true}) readerSettings$!: Observable; @Input({required: true}) image$!: Observable; @@ -79,11 +74,6 @@ export class DoubleNoCoverRendererComponent implements OnInit { get FITTING_OPTION() {return FITTING_OPTION;} get LayoutMode() {return LayoutMode;} - - - constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: MangaReaderService, - @Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { } - ngOnInit(): void { this.readerModeClass$ = this.readerSettings$.pipe( map(values => values.readerMode), diff --git a/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.html b/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.html index 3320fef16..25dc7a93c 100644 --- a/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.html +++ b/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.html @@ -1,18 +1,18 @@ - -
    - -  - - -  - -
    -
    +@if (isValid()) { +
    + @if (currentImage) { +  + } + @if (shouldRenderDouble$ | async) { +  + } +
    + } diff --git a/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.ts index 458e8ff3f..6a5e004bb 100644 --- a/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.ts +++ b/UI/Web/src/app/manga-reader/_components/double-renderer/double-renderer.component.ts @@ -1,15 +1,5 @@ -import { DOCUMENT, NgIf, NgClass, AsyncPipe } from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, DestroyRef, - EventEmitter, - inject, - Inject, - Input, - OnInit, - Output -} from '@angular/core'; +import { DOCUMENT, NgClass, AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, EventEmitter, inject, Input, OnInit, Output } from '@angular/core'; import { Observable, of, map, tap, shareReplay, filter, combineLatest } from 'rxjs'; import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; @@ -30,9 +20,14 @@ import { SafeStylePipe } from '../../../_pipes/safe-style.pipe'; templateUrl: './double-renderer.component.html', styleUrls: ['./double-renderer.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, NgClass, AsyncPipe, SafeStylePipe] + imports: [NgClass, AsyncPipe, SafeStylePipe] }) export class DoubleRendererComponent implements OnInit, ImageRenderer { + private readonly cdRef = inject(ChangeDetectorRef); + mangaReaderService = inject(MangaReaderService); + private document = inject(DOCUMENT); + readerService = inject(ReaderService); + @Input({required: true}) readerSettings$!: Observable; @Input({required: true}) image$!: Observable; @@ -78,10 +73,6 @@ export class DoubleRendererComponent implements OnInit, ImageRenderer { protected readonly ReaderMode = ReaderMode; protected readonly LayoutMode = LayoutMode; - - constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: MangaReaderService, - @Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { } - ngOnInit(): void { this.readerModeClass$ = this.readerSettings$.pipe( map(values => values.readerMode), diff --git a/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.html b/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.html index d65b8a4fb..9350ad94e 100644 --- a/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.html +++ b/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.html @@ -1,18 +1,19 @@ - -
    - -  - - -  - -
    -
    +@if (isValid()) { +
    + + @if(leftImage) { +  + } + + @if (shouldRenderDouble$ | async) { +  + } +
    +} diff --git a/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.ts index 39d56229d..0e1347cd2 100644 --- a/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.ts +++ b/UI/Web/src/app/manga-reader/_components/double-reverse-renderer/double-reverse-renderer.component.ts @@ -1,26 +1,16 @@ -import { DOCUMENT, NgIf, NgClass, AsyncPipe } from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, DestroyRef, - EventEmitter, - inject, - Inject, - Input, - OnInit, - Output -} from '@angular/core'; -import { Observable, of, map, tap, shareReplay, filter, combineLatest } from 'rxjs'; -import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; -import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; -import { ReaderService } from 'src/app/_services/reader.service'; -import { LayoutMode } from '../../_models/layout-mode'; -import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums'; -import { ReaderSetting } from '../../_models/reader-setting'; -import { DEBUG_MODES, ImageRenderer } from '../../_models/renderer'; -import { MangaReaderService } from '../../_service/manga-reader.service'; +import {AsyncPipe, DOCUMENT, NgClass} from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, EventEmitter, inject, Input, OnInit, Output } from '@angular/core'; +import {combineLatest, filter, map, Observable, of, shareReplay, tap} from 'rxjs'; +import {PageSplitOption} from 'src/app/_models/preferences/page-split-option'; +import {ReaderMode} from 'src/app/_models/preferences/reader-mode'; +import {ReaderService} from 'src/app/_services/reader.service'; +import {LayoutMode} from '../../_models/layout-mode'; +import {FITTING_OPTION, PAGING_DIRECTION} from '../../_models/reader-enums'; +import {ReaderSetting} from '../../_models/reader-setting'; +import {DEBUG_MODES, ImageRenderer} from '../../_models/renderer'; +import {MangaReaderService} from '../../_service/manga-reader.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import { SafeStylePipe } from '../../../_pipes/safe-style.pipe'; +import {SafeStylePipe} from '../../../_pipes/safe-style.pipe'; /** * This is aimed at manga. Double page renderer but where if we have page = 10, you will see @@ -31,9 +21,14 @@ import { SafeStylePipe } from '../../../_pipes/safe-style.pipe'; templateUrl: './double-reverse-renderer.component.html', styleUrls: ['./double-reverse-renderer.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, NgClass, AsyncPipe, SafeStylePipe] + imports: [NgClass, AsyncPipe, SafeStylePipe] }) export class DoubleReverseRendererComponent implements OnInit, ImageRenderer { + private readonly cdRef = inject(ChangeDetectorRef); + mangaReaderService = inject(MangaReaderService); + private document = inject(DOCUMENT); + readerService = inject(ReaderService); + @Input({required: true}) readerSettings$!: Observable; @@ -81,11 +76,6 @@ export class DoubleReverseRendererComponent implements OnInit, ImageRenderer { get FITTING_OPTION() {return FITTING_OPTION;} get LayoutMode() {return LayoutMode;} - - - constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: MangaReaderService, - @Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { } - ngOnInit(): void { this.readerModeClass$ = this.readerSettings$.pipe( filter(_ => this.isValid()), diff --git a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts index 667c9205a..29a79fd2c 100644 --- a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts +++ b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts @@ -1,23 +1,5 @@ import {AsyncPipe, DOCUMENT} from '@angular/common'; -import { - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, computed, - DestroyRef, effect, - ElementRef, - EventEmitter, - inject, - Inject, Injector, - Input, - OnChanges, - OnDestroy, - OnInit, - Output, - Renderer2, signal, Signal, - SimpleChanges, - ViewChild -} from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, DestroyRef, effect, ElementRef, EventEmitter, inject, Injector, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, signal, Signal, SimpleChanges, ViewChild } from '@angular/core'; import {BehaviorSubject, fromEvent, map, Observable, of, ReplaySubject, tap} from 'rxjs'; import {debounceTime} from 'rxjs/operators'; import {ScrollService} from 'src/app/_services/scroll.service'; @@ -81,6 +63,8 @@ const enum DEBUG_MODES { imports: [AsyncPipe, TranslocoDirective, InfiniteScrollModule, SafeStylePipe] }) export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit { + private readonly document = inject(DOCUMENT); + private readonly mangaReaderService = inject(MangaReaderService); private readonly readerService = inject(ReaderService); @@ -212,7 +196,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy, return this.webtoonImageWidth > (innerWidth || document.body.clientWidth); } - constructor(@Inject(DOCUMENT) private readonly document: Document) { + constructor() { + const document = this.document; + // This will always exist at this point in time since this is used within manga reader const reader = document.querySelector('.reading-area'); if (reader !== null) { diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html index fc59f587d..ecf89e486 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html @@ -49,18 +49,16 @@ } - +
    @if (readerMode !== ReaderMode.Webtoon) {
    - - + [showClickOverlay$]="showClickOverlay$" />
    @@ -94,32 +92,28 @@ [bookmark$]="showBookmarkEffect$" [pageNum$]="pageNum$" [showClickOverlay$]="showClickOverlay$" - [readingProfile]="readingProfile"> - + [readingProfile]="readingProfile" /> - + [getPage]="getPageFn" /> - + [getPage]="getPageFn" /> - + [getPage]="getPageFn" />
    } @else { @if (!isLoading && !inSetup) { @@ -135,8 +129,7 @@ [bookmarkPage]="showBookmarkEffectEvent" [fullscreenToggled]="fullscreenEvent" [readerSettings$]="readerSettings$" - [readingProfile]="readingProfile"> - + [readingProfile]="readingProfile" />
    } } @@ -152,11 +145,11 @@ @if (pageOptions.ceil > 0) {
    - +
    } @else {
    - +
    } diff --git a/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.ts index caa955298..be68aed44 100644 --- a/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.ts +++ b/UI/Web/src/app/manga-reader/_components/single-renderer/single-renderer.component.ts @@ -1,15 +1,5 @@ -import { DOCUMENT, NgIf, AsyncPipe } from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, computed, DestroyRef, effect, - EventEmitter, - inject, - Inject, Injector, - Input, - OnInit, - Output, signal, Signal, WritableSignal -} from '@angular/core'; +import { DOCUMENT, AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, DestroyRef, effect, EventEmitter, inject, Injector, Input, OnInit, Output, signal, Signal, WritableSignal } from '@angular/core'; import {combineLatest, combineLatestWith, filter, map, Observable, of, shareReplay, switchMap, tap} from 'rxjs'; import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; @@ -31,6 +21,10 @@ import {ReadingProfile} from "../../../_models/preferences/reading-profiles"; imports: [AsyncPipe, SafeStylePipe] }) export class SingleRendererComponent implements OnInit, ImageRenderer { + private readonly cdRef = inject(ChangeDetectorRef); + mangaReaderService = inject(MangaReaderService); + private document = inject(DOCUMENT); + private readonly utilityService = inject(UtilityService); private readonly injector = inject(Injector); @@ -64,9 +58,6 @@ export class SingleRendererComponent implements OnInit, ImageRenderer { get ReaderMode() {return ReaderMode;} get LayoutMode() {return LayoutMode;} - constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: MangaReaderService, - @Inject(DOCUMENT) private document: Document) {} - ngOnInit(): void { this.readerModeClass$ = this.readerSettings$.pipe( map(values => values.readerMode), diff --git a/UI/Web/src/app/manga-reader/_service/manga-reader.service.ts b/UI/Web/src/app/manga-reader/_service/manga-reader.service.ts index 084b1c0e7..43447dc3f 100644 --- a/UI/Web/src/app/manga-reader/_service/manga-reader.service.ts +++ b/UI/Web/src/app/manga-reader/_service/manga-reader.service.ts @@ -1,4 +1,4 @@ -import {ElementRef, Injectable, Renderer2, RendererFactory2} from '@angular/core'; +import { ElementRef, Injectable, Renderer2, RendererFactory2, inject } from '@angular/core'; import {PageSplitOption} from 'src/app/_models/preferences/page-split-option'; import {ScalingOption} from 'src/app/_models/preferences/scaling-option'; import {ReaderService} from 'src/app/_services/reader.service'; @@ -11,12 +11,16 @@ import {BookmarkInfo} from 'src/app/_models/manga-reader/bookmark-info'; providedIn: 'root' }) export class MangaReaderService { + private readerService = inject(ReaderService); + private pageDimensions: DimensionMap = {}; private pairs: {[key: number]: number} = {}; private renderer: Renderer2; - constructor(rendererFactory: RendererFactory2, private readerService: ReaderService) { + constructor() { + const rendererFactory = inject(RendererFactory2); + this.renderer = rendererFactory.createRenderer(null, null); } diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html index 2a13eb412..c688e6cdd 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html @@ -52,8 +52,7 @@ [hideSelectedItems]="true" [multiple]="isMultiSelectDropdownAllowed()" [infiniteScroll]="true" - [resettable]="true"> - + [resettable]="true" /> } } } @@ -71,7 +70,7 @@ }
    - +
    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 26dfd459a..cbfb4dad9 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -3,7 +3,7 @@ @if (utilityService.getActiveBreakpoint(); as activeBreakpoint) { @if (activeBreakpoint >= Breakpoint.Tablet) {
    - +
    } @else {
    @@ -13,7 +13,7 @@
    - +
    @@ -28,8 +28,7 @@ - + (update)="handleFilters($event)" />
    @@ -58,13 +57,13 @@ @if (utilityService.getActiveBreakpoint() > Breakpoint.Tablet) { - + }
    @if (utilityService.getActiveBreakpoint() <= Breakpoint.Tablet) {
    - +
    }
    diff --git a/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html b/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html index e3260c1d7..95c656d26 100644 --- a/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html +++ b/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html @@ -33,7 +33,7 @@ @for(option of groupedData.series; track option; let index = $index) {
  • - +
  • } @@ -46,7 +46,7 @@ @for(option of groupedData.collections; track option; let index = $index) {
  • - +
  • } @@ -59,7 +59,7 @@ @for(option of groupedData.readingLists; track option; let index = $index) {
  • - +
  • } @@ -71,7 +71,7 @@ @for(option of groupedData.bookmarks; track option; let index = $index) {
  • - +
  • } @@ -84,7 +84,7 @@ @for(option of groupedData.libraries; track option; let index = $index) {
  • - +
  • } @@ -96,7 +96,7 @@ @for(option of groupedData.genres; track option; let index = $index) {
  • - +
  • } @@ -108,7 +108,7 @@ @for(option of groupedData.tags; track option; let index = $index) {
  • - +
  • } @@ -120,7 +120,7 @@ @for(option of groupedData.persons; track option; let index = $index) {
  • - +
  • } @@ -132,7 +132,7 @@ @for(option of groupedData.chapters; track option; let index = $index) {
  • - +
  • } @@ -144,7 +144,7 @@ @for(option of groupedData.files; track option; let index = $index) {
  • - +
  • } @@ -156,7 +156,7 @@ @for(option of groupedData.annotations; track option; let index = $index) {
  • - +
  • } @@ -166,14 +166,14 @@
    • - +
    } @if (searchTerm.length > 0 && !isLoading) {
  • - +
    - +
    - + @let normalizedSearchTerm = searchTerm.toLowerCase().trim(); @@ -79,10 +79,10 @@
    - +
    - + @if (searchTerm.toLowerCase().trim(); as st) { @if (item.seriesName.toLowerCase().trim().indexOf(st) >= 0) { @@ -98,14 +98,14 @@
    - +
    {{item.title}} - +
    - +
    @@ -114,7 +114,7 @@
    {{item.title}} - +
    @@ -132,7 +132,7 @@
    + width="24px" [imageUrl]="imageService.getPersonImage(item.id)" [errorImage]="imageService.noPersonImage" />
    @@ -161,7 +161,7 @@
    @if (item.files.length > 0) { - + } {{item.titleName || item.range}} @@ -172,7 +172,7 @@
    - + {{item.filePath}}
    @@ -200,7 +200,7 @@ @if (accountService.currentUser$ | async; as user) { @if((breakpoint$ | async)! <= Breakpoint.Mobile) { diff --git a/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts b/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts index 41a8d164b..7c34f5a83 100644 --- a/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts +++ b/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts @@ -1,4 +1,4 @@ -import {Directive, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output} from '@angular/core'; +import { Directive, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, inject } from '@angular/core'; import { Subscription } from 'rxjs'; import {createSwipeSubscription, SwipeDirection, SwipeEvent, SwipeStartEvent} from './ag-swipe.core'; @@ -7,6 +7,9 @@ import {createSwipeSubscription, SwipeDirection, SwipeEvent, SwipeStartEvent} fr standalone: true }) export class SwipeDirective implements OnInit, OnDestroy { + private elementRef = inject(ElementRef); + private zone = inject(NgZone); + private swipeSubscription: Subscription | undefined; @Input() restrictSwipeToLeftSide: boolean = false; @@ -17,11 +20,6 @@ export class SwipeDirective implements OnInit, OnDestroy { @Output() swipeUp: EventEmitter = new EventEmitter(); @Output() swipeDown: EventEmitter = new EventEmitter(); - constructor( - private elementRef: ElementRef, - private zone: NgZone - ) {} - ngOnInit() { this.zone.runOutsideAngular(() => { this.swipeSubscription = createSwipeSubscription({ diff --git a/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.html b/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.html index 234349415..cc0518803 100644 --- a/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.html +++ b/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.html @@ -3,7 +3,7 @@
    @if (isLoading) { -
    +
    {{t('loading-message')}}
    @@ -14,10 +14,9 @@
    } - - - + /> @if (scrollMode === ScrollModeType.page && !isLoading) { @if (!isSearchOpen) { @@ -60,9 +57,9 @@
    - - - + + + @if (utilityService.getActiveBreakpoint() > Breakpoint.Mobile) {
    - +
    - - - + + + @@ -152,7 +149,7 @@
    - +
    diff --git a/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.scss b/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.scss index edf307f5b..e4edfff31 100644 --- a/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.scss +++ b/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.scss @@ -19,6 +19,10 @@ color: lightblue !important; } +.loading-message-container { + text-align: center; + min-width: 200px; +} .progress-container { width: 100%; diff --git a/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.ts b/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.ts index 38112e190..e09fadc1e 100644 --- a/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.ts +++ b/UI/Web/src/app/pdf-reader/_components/pdf-reader/pdf-reader.component.ts @@ -11,7 +11,7 @@ import { ViewChild } from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; -import {NgxExtendedPdfViewerModule, PageViewModeType, ProgressBarEvent, ScrollModeType} from 'ngx-extended-pdf-viewer'; +import {NgxExtendedPdfViewerModule, pdfDefaultOptions, PageViewModeType, ProgressBarEvent, ScrollModeType} from 'ngx-extended-pdf-viewer'; import {ToastrService} from 'ngx-toastr'; import {take} from 'rxjs'; import {BookService} from 'src/app/book-reader/_services/book.service'; @@ -126,6 +126,7 @@ export class PdfReaderComponent implements OnInit, OnDestroy { this.navService.hideNavBar(); this.themeService.clearThemes(); this.navService.hideSideNav(); + pdfDefaultOptions.disableAutoFetch = true; } @HostListener('window:keyup', ['$event']) diff --git a/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.html b/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.html index 4dcc3181a..a9e6ea53e 100644 --- a/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.html +++ b/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.html @@ -131,8 +131,7 @@ (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="person.coverImageLocked" - (resetClicked)="handleReset()"> - + (resetClicked)="handleReset()" />
  • diff --git a/UI/Web/src/app/person-detail/person-detail.component.html b/UI/Web/src/app/person-detail/person-detail.component.html index 28dc48579..3c3e73193 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.html +++ b/UI/Web/src/app/person-detail/person-detail.component.html @@ -5,13 +5,13 @@

    - + {{person.name}} @if (person.aniListId) { + [errorImage]="imageService.errorWebLinkImage" /> }

    @@ -24,13 +24,12 @@ @if (HasCoverImage) {
    - + [errorImage]="imageService.noPersonImage" />
    } @else { @@ -43,7 +42,7 @@
    - + @if (person.aliases.length > 0) { @@ -96,8 +95,7 @@ [imageUrl]="imageService.getSeriesCoverImage(item.id)" [title]="item.name" [suppressArchiveWarning]="true" - (clicked)="navigateToSeries(item)"> - + (clicked)="navigateToSeries(item)" />
    @@ -110,9 +108,7 @@
    - - - +
    diff --git a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html index f1ee4e02a..8a0070581 100644 --- a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html +++ b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html @@ -5,11 +5,11 @@ @for (item of scroll.viewPortItems; track trackByIdentity(i, item); let i = $index) {
    - - + + @if (showRemoveButton) { - + }
    @@ -24,11 +24,11 @@ [cdkDragData]="item" cdkDragBoundary=".example-list" [cdkDragDisabled]="accessibilityMode || disabled || bulkMode" cdkDragPreviewContainer="parent">
    - - + + @if (showRemoveButton) { - + }
    diff --git a/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html b/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html index 6c74b9f2e..bcc6c9165 100644 --- a/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html +++ b/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html @@ -1,7 +1,7 @@
    - +
    @@ -12,7 +12,7 @@

    - +
    } @@ -25,12 +25,12 @@
    - +
    @@ -49,12 +49,12 @@
    - +
    @@ -71,12 +71,12 @@
    - +
    diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html index 67683de34..7387cd9d7 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html @@ -5,13 +5,13 @@
    - +

    - + {{readingList.title}} @if(isLoading) {
    @@ -85,7 +85,7 @@
    - +
    @@ -100,7 +100,7 @@
    - +
    @@ -220,7 +220,7 @@ {{t('no-data')}}
    } @else if(isLoading) { - + } @if(formGroup.get('edit')?.value && (items.length > 100 || utilityService.getActiveBreakpoint() < Breakpoint.Tablet)) { diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts index 3056d7eb5..0e503aec2 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts @@ -1,14 +1,4 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - DestroyRef, - ElementRef, - Inject, - inject, - OnInit, - ViewChild -} from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, inject, OnInit, ViewChild } from '@angular/core'; import {ActivatedRoute, Router, RouterLink} from '@angular/router'; import {AsyncPipe, DatePipe, DecimalPipe, DOCUMENT, Location, NgClass, NgStyle} from '@angular/common'; import {ToastrService} from 'ngx-toastr'; @@ -80,6 +70,8 @@ enum TabID { RouterLink, VirtualScrollerModule, NgStyle, NgbNavOutlet, NgbNavItem, PromotedIconComponent, DefaultValuePipe, DetailsTabComponent] }) export class ReadingListDetailComponent implements OnInit { + private document = inject(DOCUMENT); + protected readonly MangaFormat = MangaFormat; protected readonly Breakpoint = Breakpoint; @@ -170,8 +162,6 @@ export class ReadingListDetailComponent implements OnInit { return 'calc(var(--vh)*100 - ' + totalHeight + 'px)'; } - constructor(@Inject(DOCUMENT) private document: Document) {} - ngOnInit(): void { const listId = this.route.snapshot.paramMap.get('id'); diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html index 988f08032..0937aebce 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html @@ -1,13 +1,13 @@
    - + @if (item.pagesRead === 0 && item.pagesTotal > 0) {
    } @if (item.pagesRead < item.pagesTotal && item.pagesTotal > 0 && item.pagesRead !== item.pagesTotal) {
    -

    +

    }
    @@ -38,11 +38,11 @@

    - + @if (item.releaseDate !== '0001-01-01T00:00:00') { diff --git a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html index 247cc38c0..9d8ec4a64 100644 --- a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html @@ -3,14 +3,14 @@

    {{t('title')}} - +

    @if (pagination) {
    {{t('item-count', {num: pagination.totalItems | number})}}
    }
    - + - + (selection)="bulkSelectionService.handleCardSelection('readingList', position, lists.length, $event)" /> diff --git a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts index 976bebbda..58c5a2c8f 100644 --- a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts @@ -35,6 +35,17 @@ import {User} from "../../../_models/user"; DecimalPipe, TranslocoDirective, BulkOperationsComponent] }) export class ReadingListsComponent implements OnInit { + private readingListService = inject(ReadingListService); + imageService = inject(ImageService); + private actionFactoryService = inject(ActionFactoryService); + private accountService = inject(AccountService); + private toastr = inject(ToastrService); + private router = inject(Router); + private jumpbarService = inject(JumpbarService); + private readonly cdRef = inject(ChangeDetectorRef); + private ngbModal = inject(NgbModal); + private titleService = inject(Title); + protected readonly WikiLink = WikiLink; protected readonly bulkSelectionService = inject(BulkSelectionService); @@ -51,11 +62,6 @@ export class ReadingListsComponent implements OnInit { globalActions: Array> = []; trackByIdentity = (index: number, item: ReadingList) => `${item.id}_${item.title}_${item.promoted}`; - - constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService, - private accountService: AccountService, private toastr: ToastrService, private router: Router, - private jumpbarService: JumpbarService, private readonly cdRef: ChangeDetectorRef, private ngbModal: NgbModal, private titleService: Title) { } - ngOnInit(): void { this.accountService.currentUser$.pipe(take(1)).subscribe(user => { if (user) { diff --git a/UI/Web/src/app/reading-list/_components/step-tracker/step-tracker.component.html b/UI/Web/src/app/reading-list/_components/step-tracker/step-tracker.component.html index 8f3f10754..c1ae3cac4 100644 --- a/UI/Web/src/app/reading-list/_components/step-tracker/step-tracker.component.html +++ b/UI/Web/src/app/reading-list/_components/step-tracker/step-tracker.component.html @@ -1,11 +1,11 @@ -
    -
      - -
    • -
      - {{step.title}} -
    • -
      -
    +
    +
      + @for (step of steps; track step) { +
    • +
      + {{step.title}} +
    • + } +
    \ No newline at end of file diff --git a/UI/Web/src/app/reading-list/_components/step-tracker/step-tracker.component.ts b/UI/Web/src/app/reading-list/_components/step-tracker/step-tracker.component.ts index 30f0fd25b..0f08299f6 100644 --- a/UI/Web/src/app/reading-list/_components/step-tracker/step-tracker.component.ts +++ b/UI/Web/src/app/reading-list/_components/step-tracker/step-tracker.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef, inject } from '@angular/core'; import {CommonModule} from "@angular/common"; @@ -18,10 +18,9 @@ export interface TimelineStep { changeDetection: ChangeDetectionStrategy.OnPush }) export class StepTrackerComponent { + private readonly cdRef = inject(ChangeDetectorRef); + @Input() steps: Array = []; @Input() currentStep: number = 0; - - constructor(private readonly cdRef: ChangeDetectorRef) {} - } diff --git a/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.html b/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.html index 0b7f61ca1..f9b7ef959 100644 --- a/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.html +++ b/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.html @@ -8,38 +8,48 @@
    -
    - - -
    + @if (isAdmin && !individualUserMode) { +
    + + +
    + }
    @@ -29,31 +35,29 @@
    - - - - + @if (data$ | async; as data) { + @if (data.length > 0) { + + } @else { + {{t('no-data')}} + } + }
    - - {{t('no-data')}} -
    diff --git a/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.ts b/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.ts index cbaa01675..7fcd4a014 100644 --- a/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.ts +++ b/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.ts @@ -8,7 +8,7 @@ import { PieDataItem } from '../../_models/pie-data-item'; import { TimePeriods } from '../top-readers/top-readers.component'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import { LineChartModule } from '@swimlane/ngx-charts'; -import { NgIf, NgFor, AsyncPipe } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import {TranslocoDirective} from "@jsverse/transloco"; const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" }; @@ -19,7 +19,7 @@ const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" }; templateUrl: './reading-activity.component.html', styleUrls: ['./reading-activity.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ReactiveFormsModule, NgIf, NgFor, LineChartModule, AsyncPipe, TranslocoDirective] + imports: [ReactiveFormsModule, LineChartModule, AsyncPipe, TranslocoDirective] }) export class ReadingActivityComponent implements OnInit { /** diff --git a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html index 6a19e0a8e..6f0dc647a 100644 --- a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html +++ b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html @@ -1,113 +1,107 @@ -
    - -
    - - {{t('series-count', {num: stats.seriesCount | number})}} - -
    -
    -
    - - -
    - - {{t('volume-count', {num: stats.volumeCount | number})}} - -
    -
    -
    - - -
    - - {{t('file-count', {num: stats.totalFiles | number})}} - -
    -
    -
    - - -
    - - {{stats.totalSize | bytes}} - -
    -
    -
    - - -
    - - {{t('genre-count', {num: stats.totalGenres | compactNumber})}} - -
    -
    -
    - - -
    - - {{t('tag-count', {num: stats.totalTags | compactNumber})}} - -
    -
    -
    - - -
    - - {{t('people-count', {num: stats.totalPeople | compactNumber})}} - -
    -
    -
    - - -
    - - {{stats.totalReadingTime | timeDuration}} - -
    -
    -
    + @if (stats$ | async; as stats) { +
    + +
    + + {{t('series-count', {num: stats.seriesCount | number})}} + +
    +
    +
    + +
    + + {{t('volume-count', {num: stats.volumeCount | number})}} + +
    +
    +
    + +
    + + {{t('file-count', {num: stats.totalFiles | number})}} + +
    +
    +
    + +
    + + {{stats.totalSize | bytes}} + +
    +
    +
    + +
    + + {{t('genre-count', {num: stats.totalGenres | compactNumber})}} + +
    +
    +
    + +
    + + {{t('tag-count', {num: stats.totalTags | compactNumber})}} + +
    +
    +
    + +
    + + {{t('people-count', {num: stats.totalPeople | compactNumber})}} + +
    +
    +
    + +
    + + {{stats.totalReadingTime | timeDuration}} + +
    +
    +
    + }
    - +
    - +
    - +
    - - +
    - +
    - +
    - +
    - +
    - +
    - +
    diff --git a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts index cb254f413..638ae0516 100644 --- a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts +++ b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts @@ -22,7 +22,7 @@ import {FileBreakdownStatsComponent} from '../file-breakdown-stats/file-breakdow import {TopReadersComponent} from '../top-readers/top-readers.component'; import {StatListComponent} from '../stat-list/stat-list.component'; import {IconAndTitleComponent} from '../../../shared/icon-and-title/icon-and-title.component'; -import {AsyncPipe, DecimalPipe, NgIf} from '@angular/common'; +import { AsyncPipe, DecimalPipe } from '@angular/common'; import {translate, TranslocoDirective} from "@jsverse/transloco"; import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; import {FilterField} from "../../../_models/metadata/v2/filter-field"; @@ -33,11 +33,17 @@ import {AccountService} from "../../../_services/account.service"; templateUrl: './server-stats.component.html', styleUrls: ['./server-stats.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, IconAndTitleComponent, StatListComponent, TopReadersComponent, FileBreakdownStatsComponent, - PublicationStatusStatsComponent, ReadingActivityComponent, DayBreakdownComponent, AsyncPipe, DecimalPipe, - CompactNumberPipe, TimeDurationPipe, BytesPipe, TranslocoDirective] + imports: [IconAndTitleComponent, StatListComponent, TopReadersComponent, FileBreakdownStatsComponent, PublicationStatusStatsComponent, ReadingActivityComponent, DayBreakdownComponent, AsyncPipe, DecimalPipe, CompactNumberPipe, TimeDurationPipe, BytesPipe, TranslocoDirective] }) export class ServerStatsComponent { + private statService = inject(StatisticsService); + private router = inject(Router); + private imageService = inject(ImageService); + private metadataService = inject(MetadataService); + private modalService = inject(NgbModal); + private utilityService = inject(UtilityService); + private filterUtilityService = inject(FilterUtilitiesService); + private readonly destroyRef = inject(DestroyRef); protected readonly accountService = inject(AccountService); @@ -67,9 +73,7 @@ export class ServerStatsComponent { get Breakpoint() { return Breakpoint; } - constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService, - private metadataService: MetadataService, private modalService: NgbModal, private utilityService: UtilityService, - private filterUtilityService: FilterUtilitiesService) { + constructor() { this.seriesImage = (data: PieDataItem) => { if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id); return ''; diff --git a/UI/Web/src/app/statistics/_components/stat-list/stat-list.component.html b/UI/Web/src/app/statistics/_components/stat-list/stat-list.component.html index 8d2d4f942..7c19ad80d 100644 --- a/UI/Web/src/app/statistics/_components/stat-list/stat-list.component.html +++ b/UI/Web/src/app/statistics/_components/stat-list/stat-list.component.html @@ -11,7 +11,7 @@
  • @if (image && image(item); as url) { @if (url && url.length > 0) { - + } } {{item.name}} diff --git a/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.ts b/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.ts index d9dd6300e..4f95003a2 100644 --- a/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.ts +++ b/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, inject } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { StatisticsService } from 'src/app/_services/statistics.service'; import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component'; @@ -18,6 +18,10 @@ import {translate, TranslocoDirective} from "@jsverse/transloco"; imports: [IconAndTitleComponent, DecimalPipe, CompactNumberPipe, TimeDurationPipe, TimeAgoPipe, TranslocoDirective] }) export class UserStatsInfoCardsComponent { + private statsService = inject(StatisticsService); + private modalService = inject(NgbModal); + private accountService = inject(AccountService); + @Input() totalPagesRead: number = 0; @Input() totalWordsRead: number = 0; @@ -26,8 +30,6 @@ export class UserStatsInfoCardsComponent { @Input() lastActive: string = ''; @Input() avgHoursPerWeekSpentReading: number = 0; - constructor(private statsService: StatisticsService, private modalService: NgbModal, private accountService: AccountService) { } - openPageByYearList() { const numberPipe = new CompactNumberPipe(); this.statsService.getPagesPerYear().subscribe(yearCounts => { diff --git a/UI/Web/src/app/statistics/_components/user-stats/user-stats.component.html b/UI/Web/src/app/statistics/_components/user-stats/user-stats.component.html index 3dc65aff7..2ed0656fb 100644 --- a/UI/Web/src/app/statistics/_components/user-stats/user-stats.component.html +++ b/UI/Web/src/app/statistics/_components/user-stats/user-stats.component.html @@ -1,25 +1,22 @@ -
    - -
    - - - + @if (userId) { +
    +
    + @if (userStats$ | async; as userStats) { + + } +
    +
    + +
    +
    + +
    +
    + +
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    + } diff --git a/UI/Web/src/app/statistics/_components/user-stats/user-stats.component.ts b/UI/Web/src/app/statistics/_components/user-stats/user-stats.component.ts index 2d788e735..4992b6be9 100644 --- a/UI/Web/src/app/statistics/_components/user-stats/user-stats.component.ts +++ b/UI/Web/src/app/statistics/_components/user-stats/user-stats.component.ts @@ -7,7 +7,7 @@ import {MemberService} from 'src/app/_services/member.service'; import {AccountService} from 'src/app/_services/account.service'; import {PieDataItem} from '../../_models/pie-data-item'; import {LibraryService} from 'src/app/_services/library.service'; -import {AsyncPipe, NgIf, PercentPipe} from '@angular/common'; +import { AsyncPipe, PercentPipe } from '@angular/common'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {StatListComponent} from '../stat-list/stat-list.component'; import {ReadingActivityComponent} from '../reading-activity/reading-activity.component'; @@ -21,14 +21,13 @@ import {DayBreakdownComponent} from "../day-breakdown/day-breakdown.component"; styleUrls: ['./user-stats.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - NgIf, - UserStatsInfoCardsComponent, - ReadingActivityComponent, - StatListComponent, - AsyncPipe, - TranslocoModule, - DayBreakdownComponent, - ] + UserStatsInfoCardsComponent, + ReadingActivityComponent, + StatListComponent, + AsyncPipe, + TranslocoModule, + DayBreakdownComponent +] }) export class UserStatsComponent implements OnInit { diff --git a/UI/Web/src/app/typeahead/_components/typeahead.component.html b/UI/Web/src/app/typeahead/_components/typeahead.component.html index b87b6cdd6..71b35e71f 100644 --- a/UI/Web/src/app/typeahead/_components/typeahead.component.html +++ b/UI/Web/src/app/typeahead/_components/typeahead.component.html @@ -12,7 +12,7 @@ @if (optionSelection) { @for (option of optionSelection.selected(); track option; let i = $index) { - + @if (!disabled) { } @@ -53,7 +53,7 @@
  • - +
  • } diff --git a/UI/Web/src/app/typeahead/_components/typeahead.component.ts b/UI/Web/src/app/typeahead/_components/typeahead.component.ts index 17dbc7b4c..9d6a26ae1 100644 --- a/UI/Web/src/app/typeahead/_components/typeahead.component.ts +++ b/UI/Web/src/app/typeahead/_components/typeahead.component.ts @@ -1,24 +1,6 @@ import {animate, state, style, transition, trigger} from '@angular/animations'; import {AsyncPipe, DOCUMENT, NgClass, NgTemplateOutlet} from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ContentChild, - DestroyRef, - ElementRef, - EventEmitter, - HostListener, - inject, - Inject, - Input, - OnInit, - Output, - Renderer2, - RendererStyleFlags2, - TemplateRef, - ViewChild -} from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, DestroyRef, ElementRef, EventEmitter, HostListener, inject, Input, OnInit, Output, Renderer2, RendererStyleFlags2, TemplateRef, ViewChild } from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {Observable, ReplaySubject} from 'rxjs'; import {auditTime, filter, map, shareReplay, switchMap, take, tap} from 'rxjs/operators'; @@ -52,6 +34,8 @@ const ANIMATION_SPEED = 200; ] }) export class TypeaheadComponent implements OnInit { + private document = inject(DOCUMENT); + /** * Settings for the typeahead */ @@ -102,8 +86,6 @@ export class TypeaheadComponent implements OnInit { private readonly renderer2 = inject(Renderer2); private readonly cdRef = inject(ChangeDetectorRef); - constructor(@Inject(DOCUMENT) private document: Document) { } - ngOnInit() { this.reset.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((resetToEmpty: boolean) => { this.clearSelections(resetToEmpty); diff --git a/UI/Web/src/app/user-settings/_modals/edit-device-modal/edit-device-modal.component.html b/UI/Web/src/app/user-settings/_modals/edit-device-modal/edit-device-modal.component.html index daf81cda6..b41d94a7a 100644 --- a/UI/Web/src/app/user-settings/_modals/edit-device-modal/edit-device-modal.component.html +++ b/UI/Web/src/app/user-settings/_modals/edit-device-modal/edit-device-modal.component.html @@ -24,7 +24,7 @@ {{t('email-tooltip')}} - + diff --git a/UI/Web/src/app/user-settings/change-age-restriction/change-age-restriction.component.html b/UI/Web/src/app/user-settings/change-age-restriction/change-age-restriction.component.html index 660cf3e89..a977ecd6b 100644 --- a/UI/Web/src/app/user-settings/change-age-restriction/change-age-restriction.component.html +++ b/UI/Web/src/app/user-settings/change-age-restriction/change-age-restriction.component.html @@ -9,7 +9,7 @@ - +
    diff --git a/UI/Web/src/app/user-settings/change-email/change-email.component.html b/UI/Web/src/app/user-settings/change-email/change-email.component.html index fd1d62bd7..63330eca7 100644 --- a/UI/Web/src/app/user-settings/change-email/change-email.component.html +++ b/UI/Web/src/app/user-settings/change-email/change-email.component.html @@ -2,7 +2,7 @@ - {{user?.email}} + {{user?.email | defaultValue}} @if(emailConfirmed) { {{t('email-confirmed')}} @@ -68,7 +68,7 @@

    {{t('email-updated-title')}}

    {{t('email-updated-description')}}

    - + }
    diff --git a/UI/Web/src/app/user-settings/change-email/change-email.component.ts b/UI/Web/src/app/user-settings/change-email/change-email.component.ts index cbd5a0b06..8189edbe5 100644 --- a/UI/Web/src/app/user-settings/change-email/change-email.component.ts +++ b/UI/Web/src/app/user-settings/change-email/change-email.component.ts @@ -9,13 +9,14 @@ import {ApiKeyComponent} from '../api-key/api-key.component'; import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; import {translate, TranslocoDirective} from "@jsverse/transloco"; import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; +import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; @Component({ selector: 'app-change-email', templateUrl: './change-email.component.html', styleUrls: ['./change-email.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgbTooltip, ReactiveFormsModule, ApiKeyComponent, TranslocoDirective, SettingItemComponent] + imports: [NgbTooltip, ReactiveFormsModule, ApiKeyComponent, TranslocoDirective, SettingItemComponent, DefaultValuePipe] }) export class ChangeEmailComponent implements OnInit { diff --git a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html index 8844da749..d02f1ef83 100644 --- a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html +++ b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html @@ -46,8 +46,7 @@ - - +
    @if (isEditMode) {
    - +
    } diff --git a/UI/Web/src/app/user-settings/theme-manager/theme-manager.component.html b/UI/Web/src/app/user-settings/theme-manager/theme-manager.component.html index 9e694327b..4e129ad73 100644 --- a/UI/Web/src/app/user-settings/theme-manager/theme-manager.component.html +++ b/UI/Web/src/app/user-settings/theme-manager/theme-manager.component.html @@ -21,7 +21,7 @@ @for (theme of downloadedThemes; track theme.name) { - + } @for (theme of downloadableThemes; track theme.name) { - + }
    @@ -55,7 +55,7 @@ @if (files && files.length > 0) { - + } @else if (hasAdmin$ | async) { @@ -104,7 +104,7 @@ - + } @else { @@ -112,7 +112,7 @@ - + } diff --git a/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html b/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html index 6bc5cd859..c6618710a 100644 --- a/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html +++ b/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html @@ -16,7 +16,7 @@ {{t('series-name-header')}} - + {{item.seriesName}} @@ -32,9 +32,7 @@ - - - + diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.html b/UI/Web/src/app/volume-detail/volume-detail.component.html index 0e9fb51da..fa82a1f92 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.html +++ b/UI/Web/src/app/volume-detail/volume-detail.component.html @@ -1,13 +1,13 @@ - +
    @if (volume && series && libraryType !== null) {
    - +
    @@ -17,7 +17,7 @@
    {{t('volume-num')}} - +
    @@ -26,8 +26,7 @@ [hasReadingProgress]="volume.pagesRead > 0" [readingTimeEntity]="volume" [libraryType]="libraryType" - [mangaFormat]="series.format"> - + [mangaFormat]="series.format" /> @if (libraryType !== null && series && volume.chapters.length === 1) {
    @@ -77,19 +76,19 @@
    - +
    - +
    - +
    @@ -172,7 +171,7 @@ [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, volume!.chapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true" - > + /> }
    @@ -185,7 +184,7 @@ {{t('related-tab')}} @defer (when activeTabId === TabID.Related; prefetch on idle) { - + } @@ -199,7 +198,7 @@ @defer (when activeTabId === TabID.Annotations; prefetch on idle) { - + } @@ -229,7 +228,7 @@ [tags]="tags" [readingTime]="volume" [language]="volume.chapters[0].language" - [format]="series.format"> + [format]="series.format" /> } @@ -242,7 +241,7 @@ - +
    diff --git a/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.html b/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.html index 291783375..86e18ad0c 100644 --- a/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.html +++ b/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.html @@ -12,7 +12,7 @@
    - + @if (filter) { + /> @if (!filterActive && series.length === 0) { diff --git a/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts b/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts index b16d1f9fc..52f54f2bb 100644 --- a/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts +++ b/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts @@ -1,17 +1,5 @@ import {DecimalPipe, DOCUMENT, NgStyle} from '@angular/common'; -import { - AfterContentChecked, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - DestroyRef, - ElementRef, - EventEmitter, - inject, - Inject, - OnInit, - ViewChild -} from '@angular/core'; +import { AfterContentChecked, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, EventEmitter, inject, OnInit, ViewChild } from '@angular/core'; import {Title} from '@angular/platform-browser'; import {ActivatedRoute, Router} from '@angular/router'; import {debounceTime, take} from 'rxjs'; @@ -54,6 +42,22 @@ import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; imports: [SideNavCompanionBarComponent, NgStyle, BulkOperationsComponent, CardDetailLayoutComponent, SeriesCardComponent, DecimalPipe, TranslocoDirective] }) export class WantToReadComponent implements OnInit, AfterContentChecked { + imageService = inject(ImageService); + private router = inject(Router); + private route = inject(ActivatedRoute); + private seriesService = inject(SeriesService); + private titleService = inject(Title); + bulkSelectionService = inject(BulkSelectionService); + private actionService = inject(ActionService); + private messageHub = inject(MessageHubService); + private filterUtilityService = inject(FilterUtilitiesService); + private utilityService = inject(UtilityService); + private document = inject(DOCUMENT); + private readonly cdRef = inject(ChangeDetectorRef); + private scrollService = inject(ScrollService); + private hubService = inject(MessageHubService); + private jumpbarService = inject(JumpbarService); + @ViewChild('scrollingBlock') scrollingBlock: ElementRef | undefined; @ViewChild('companionBar') companionBar: ElementRef | undefined; @@ -103,12 +107,7 @@ export class WantToReadComponent implements OnInit, AfterContentChecked { return 'calc(var(--vh)*100 - ' + totalHeight + 'px)'; } - constructor(public imageService: ImageService, private router: Router, private route: ActivatedRoute, - private seriesService: SeriesService, private titleService: Title, - public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService, - private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document, - private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService, private hubService: MessageHubService, - private jumpbarService: JumpbarService) { + constructor() { this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.titleService.setTitle('Kavita - ' + translate('want-to-read.title')); diff --git a/UI/Web/src/assets/langs/cs.json b/UI/Web/src/assets/langs/cs.json index 70f8b1a0e..027ebb3d8 100644 --- a/UI/Web/src/assets/langs/cs.json +++ b/UI/Web/src/assets/langs/cs.json @@ -2134,7 +2134,7 @@ }, "pdf-reader": { "incognito-mode": "Anonymní režim", - "loading-message": "Načítání......PDF může trvat déle, než se očekávalo", + "loading-message": "Načítání...", "light-theme-alt": "Světlý motiv", "dark-theme-alt": "Tmavý motiv", "close-reader-alt": "Zavřít čtečku", diff --git a/UI/Web/src/assets/langs/de.json b/UI/Web/src/assets/langs/de.json index cfe306061..8fd21f430 100644 --- a/UI/Web/src/assets/langs/de.json +++ b/UI/Web/src/assets/langs/de.json @@ -1584,7 +1584,7 @@ "help-label": "{{common.help}}" }, "pdf-reader": { - "loading-message": "Das Laden von……PDFs kann länger dauern als erwartet", + "loading-message": "Laden...", "incognito-mode": "Inkognito-Modus", "light-theme-alt": "Light Schema", "dark-theme-alt": "Dunkles Schema", diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index caf700976..d32bafd69 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -33,7 +33,7 @@ "client-id-label": "Client ID", "client-id-tooltip": "The ClientID set in your OIDC provider, can be anything", "secret-label": "Client secret", - "secret-tooltip": "The secret as generated by your OIDC provider", + "secret-tooltip": "The secret as generated by your OIDC provider, will not be shown after being set", "provision-accounts-label": "Provision accounts", "provision-accounts-tooltip": "Auto-create a new account when logging in via OIDC if it can't be matched to an existing one", "require-verified-email-label": "Require verified emails", @@ -205,6 +205,8 @@ "share-series-reviews-tooltip": "Allow your reviews to be visible to other users on this server", "highlight-bar-label": "Annotations highlight colors", "highlight-bar-tooltip": "These colors are shared between all books", + "colorscape-label": "Use ColorScape", + "colorscape-tooltip": "Global toggle to enable/disable the dynamic gradient feature. Will override theme settings", "kavitaplus-settings-title": "Kavita+", "anilist-scrobbling-label": "AniList Scrobbling", @@ -597,13 +599,22 @@ "library-type-pipe": { "book": "Book", - "comic": "Comic (Legacy)", + "comic": "Comic (Flexible)", "manga": "Manga", "comicVine": "Comic", "image": "Image", "lightNovel": "Light Novel" }, + "library-type-subtitle-pipe": { + "book": "General-purpose book library.", + "comic": "Comic library with support for volume grouping and trade paperbacks within a single series. Does not support multiple runs in same Series.", + "manga": "Japanese comics with volume grouping support. Also handles some loose image configurations.", + "comicVine": "Strict comic implementation based on Comic Vine metadata with integration for specialized tooling.", + "image": "Loose image files with limited support. Must largely follow wiki documentation.", + "lightNovel": "Book library with special handling optimized for light novels" + }, + "age-rating-pipe": { "unknown": "Unknown", "early-childhood": "Early Childhood", @@ -918,7 +929,12 @@ "create-title": "Create Annotation", "create": "Create", "close": "{{common.close}}", - "contains-spoilers-label": "{{annotation-card.contains-spoilers-label}}" + "contains-spoilers-label": "{{annotation-card.contains-spoilers-label}}", + "edit": "Edit" + }, + + "quill-wrapper": { + "clean-tooltip": "Remove all styles" }, "view-bookmark-drawer": { @@ -950,7 +966,9 @@ "highlight-bar": { "add-new-color-alt": "Add Custom Color", - "slot-label": "Highlight slot {{slot}}" + "slot-label": "Highlight slot {{slot}}", + "edit": "{{common.edit}}", + "close": "{{common.close}}" }, "book-reader": { @@ -981,12 +999,13 @@ "previous": "Previous", "go-to-page": "Go to page", - "go-to-page-prompt": "There are {{totalPages}} pages. Which page do you want to go to?", + "go-to-page-prompt": "There are {{totalPages}} pages. Which page do you want to go to? Page starts at 1.", "page-num-label": "Page {{page}}", "completion-label": "{{percent}} complete", - "force-selected-one-column": "Layout mode switched to One Column due to insufficient space to render Two Columns" + "force-selected-one-column": "Layout mode switched to One Column due to insufficient space to render Two Columns", + "forced-vertical-switch": "Layout mode switched as Two Column layout doesn't work with Vertical Writing Style" }, "personal-table-of-contents": { @@ -2150,7 +2169,7 @@ }, "pdf-reader": { - "loading-message": "Loading……PDFs may take longer than expected", + "loading-message": "Loading...", "incognito-mode": "Incognito Mode", "light-theme-alt": "Light Theme", "dark-theme-alt": "Dark Theme", diff --git a/UI/Web/src/assets/langs/es.json b/UI/Web/src/assets/langs/es.json index d9e4b5d5d..2e9a7eb5e 100644 --- a/UI/Web/src/assets/langs/es.json +++ b/UI/Web/src/assets/langs/es.json @@ -1516,7 +1516,7 @@ "help-label": "{{common.help}}" }, "pdf-reader": { - "loading-message": "Cargando……los PDF pueden ser más lentos de lo esperado", + "loading-message": "Cargando...", "incognito-mode": "Modo incógnito", "light-theme-alt": "Tema claro", "dark-theme-alt": "Tema oscuro", diff --git a/UI/Web/src/assets/langs/fr.json b/UI/Web/src/assets/langs/fr.json index dc8a31cc9..ec84a4d9c 100644 --- a/UI/Web/src/assets/langs/fr.json +++ b/UI/Web/src/assets/langs/fr.json @@ -1587,7 +1587,7 @@ "help-label": "{{common.help}}" }, "pdf-reader": { - "loading-message": "Chargement......de PDF peut prendre plus de temps que prévu", + "loading-message": "Chargement...", "incognito-mode": "Mode incognito", "light-theme-alt": "Thème clair", "dark-theme-alt": "Thème sombre", diff --git a/UI/Web/src/assets/langs/ga.json b/UI/Web/src/assets/langs/ga.json index 9d40bba37..a5913562d 100644 --- a/UI/Web/src/assets/langs/ga.json +++ b/UI/Web/src/assets/langs/ga.json @@ -1587,7 +1587,7 @@ "help-label": "{{common.help}}" }, "pdf-reader": { - "loading-message": "Á Luchtú...... D'fhéadfadh sé go dtógfadh PDFanna níos faide ná mar a bhíothas ag súil leis", + "loading-message": "Á Luchtú...", "incognito-mode": "Mód Incognito", "light-theme-alt": "Téama Solais", "dark-theme-alt": "Téama Dorcha", diff --git a/UI/Web/src/assets/langs/it.json b/UI/Web/src/assets/langs/it.json index a08fa68b4..3c92df4c6 100644 --- a/UI/Web/src/assets/langs/it.json +++ b/UI/Web/src/assets/langs/it.json @@ -1510,7 +1510,7 @@ "cbl-repo": "Puoi trovare molti elenchi di lettura nella community repo." }, "pdf-reader": { - "loading-message": "Caricamento... I PDF potrebbero richiedere più tempo del previsto", + "loading-message": "Caricamento...", "incognito-mode": "Modalità Incognito", "light-theme-alt": "Tema Chiaro", "dark-theme-alt": "Tema Scuro", diff --git a/UI/Web/src/assets/langs/ja.json b/UI/Web/src/assets/langs/ja.json index b8f942912..e6b921fe1 100644 --- a/UI/Web/src/assets/langs/ja.json +++ b/UI/Web/src/assets/langs/ja.json @@ -1388,7 +1388,7 @@ "comicvine-parsing-label": "Comic Vine シリーズのマッチングを使用" }, "pdf-reader": { - "loading-message": "ローディング...... PDF は期待以上の時間がかかる場合があります", + "loading-message": "ローディング...", "incognito-mode": "シークレットモード", "light-theme-alt": "ライトテーマ", "dark-theme-alt": "ダークテーマ", diff --git a/UI/Web/src/assets/langs/ko.json b/UI/Web/src/assets/langs/ko.json index 9b359acf3..105482ade 100644 --- a/UI/Web/src/assets/langs/ko.json +++ b/UI/Web/src/assets/langs/ko.json @@ -1587,7 +1587,7 @@ "help-label": "{{common.help}}" }, "pdf-reader": { - "loading-message": "로딩 중……PDF가 예상보다 오래 걸릴 수 있습니다", + "loading-message": "로딩 중...", "incognito-mode": "시크릿 모드", "light-theme-alt": "밝은 테마", "dark-theme-alt": "어두운 테마", diff --git a/UI/Web/src/assets/langs/nl.json b/UI/Web/src/assets/langs/nl.json index ba94d01c2..bac4be951 100644 --- a/UI/Web/src/assets/langs/nl.json +++ b/UI/Web/src/assets/langs/nl.json @@ -1328,7 +1328,7 @@ "final-import-step": "Laatste stap" }, "pdf-reader": { - "loading-message": "Het laden van……PDF's kunnen langer duren dan verwacht", + "loading-message": "Laden...", "incognito-mode": "Incognito modus", "light-theme-alt": "Licht thema", "dark-theme-alt": "Donker thema", diff --git a/UI/Web/src/assets/langs/pl.json b/UI/Web/src/assets/langs/pl.json index cfec5e635..267a92a34 100644 --- a/UI/Web/src/assets/langs/pl.json +++ b/UI/Web/src/assets/langs/pl.json @@ -1560,7 +1560,7 @@ "help-label": "{{common.help}}" }, "pdf-reader": { - "loading-message": "Ładowanie… Pliki PDF mogą ładować się dłużej niż oczekiwano", + "loading-message": "Ładowanie...", "incognito-mode": "Tryb incognito", "light-theme-alt": "Jasny motyw", "dark-theme-alt": "Ciemny motyw", diff --git a/UI/Web/src/assets/langs/pt.json b/UI/Web/src/assets/langs/pt.json index 14fd135ef..e263624b5 100644 --- a/UI/Web/src/assets/langs/pt.json +++ b/UI/Web/src/assets/langs/pt.json @@ -1571,7 +1571,7 @@ "help-label": "{{common.help}}" }, "pdf-reader": { - "loading-message": "A carregar....PDFs podem demorar mais que o esperado", + "loading-message": "A carregar...", "incognito-mode": "Modo Incógnito", "light-theme-alt": "Tema Claro", "dark-theme-alt": "Tema Escuro", diff --git a/UI/Web/src/assets/langs/pt_BR.json b/UI/Web/src/assets/langs/pt_BR.json index ade80ee8f..735af16d7 100644 --- a/UI/Web/src/assets/langs/pt_BR.json +++ b/UI/Web/src/assets/langs/pt_BR.json @@ -1587,7 +1587,7 @@ "help-label": "{{common.help}}" }, "pdf-reader": { - "loading-message": "Carregando... PDFs podem demorar mais do que o esperado", + "loading-message": "Carregando...", "incognito-mode": "Modo Incógnito", "light-theme-alt": "Tema Claro", "dark-theme-alt": "Tema Escuro", diff --git a/UI/Web/src/assets/langs/sk.json b/UI/Web/src/assets/langs/sk.json index 14e3c96ec..69c53cfb8 100644 --- a/UI/Web/src/assets/langs/sk.json +++ b/UI/Web/src/assets/langs/sk.json @@ -1540,7 +1540,7 @@ "comicvine-parsing-label": "Použite zhodu Comic Vine Serií" }, "pdf-reader": { - "loading-message": "Načítavanie……súborov PDF môže trvať dlhšie, ako sa očakávalo", + "loading-message": "Načítavanie...", "incognito-mode": "Mód Inkognito", "light-theme-alt": "Svetlá téma", "dark-theme-alt": "Tmavá téma", diff --git a/UI/Web/src/assets/langs/sv.json b/UI/Web/src/assets/langs/sv.json index 82fc1a823..ee6592573 100644 --- a/UI/Web/src/assets/langs/sv.json +++ b/UI/Web/src/assets/langs/sv.json @@ -2567,7 +2567,7 @@ "light-theme-alt": "Ljust Tema", "dark-theme-alt": "Mörkt Tema", "toggle-incognito": "Stäng av Inkognitoläge", - "loading-message": "Laddar……PDFs kan ta längre än väntat", + "loading-message": "Laddar...", "incognito-mode": "Inkognitoläge" }, "log-level-pipe": { diff --git a/UI/Web/src/assets/langs/ta.json b/UI/Web/src/assets/langs/ta.json index b081fba62..b98fd4116 100644 --- a/UI/Web/src/assets/langs/ta.json +++ b/UI/Web/src/assets/langs/ta.json @@ -2144,7 +2144,7 @@ "cbl-repo": "சமூகத்தில் பல வாசிப்பு பட்டியல்களை நீங்கள் காணலாம் களஞ்சி ." }, "pdf-reader": { - "loading-message": "ஏற்றுதல் …… PDF கள் எதிர்பார்த்ததை விட அதிக நேரம் ஆகலாம்", + "loading-message": "ஏற்றுதல்...", "incognito-mode": "மறைநிலை பயன்முறை", "light-theme-alt": "ஒளி கருப்பொருள்", "dark-theme-alt": "இருண்ட கருப்பொருள்", diff --git a/UI/Web/src/assets/langs/vi.json b/UI/Web/src/assets/langs/vi.json index f03fb8e2f..6eda3854e 100644 --- a/UI/Web/src/assets/langs/vi.json +++ b/UI/Web/src/assets/langs/vi.json @@ -1922,7 +1922,7 @@ "light-theme-alt": "Chủ Đề Sáng", "dark-theme-alt": "Chủ Đề Tối", "close-reader-alt": "Đóng Trình Đọc", - "loading-message": "Đang tải……PDF có thể mất nhiều thời gian hơn dự kiến", + "loading-message": "Đang tải...", "toggle-incognito": "Tắt chế độ Ẩn Danh" }, "pdf-layout-mode-pipe": { diff --git a/UI/Web/src/assets/langs/zh_Hans.json b/UI/Web/src/assets/langs/zh_Hans.json index 7ff99f81c..4fcb154d3 100644 --- a/UI/Web/src/assets/langs/zh_Hans.json +++ b/UI/Web/src/assets/langs/zh_Hans.json @@ -1587,7 +1587,7 @@ "help-label": "{{common.help}}" }, "pdf-reader": { - "loading-message": "加载中...PDF文件可能需要比预期更长的时间", + "loading-message": "加载中...", "incognito-mode": "隐身模式", "light-theme-alt": "明亮主题", "dark-theme-alt": "深色主题", diff --git a/UI/Web/src/assets/langs/zh_Hant.json b/UI/Web/src/assets/langs/zh_Hant.json index 4df117f25..5fd536df6 100644 --- a/UI/Web/src/assets/langs/zh_Hant.json +++ b/UI/Web/src/assets/langs/zh_Hant.json @@ -2348,7 +2348,7 @@ }, "pdf-reader": { "close-reader-alt": "關閉閱讀器", - "loading-message": "載入中…PDF 可能需要比預期更長的時間", + "loading-message": "載入中...", "incognito-mode": "無痕模式", "light-theme-alt": "淺色主題", "dark-theme-alt": "深色主題", diff --git a/UI/Web/src/environments/environment.proxy.ts b/UI/Web/src/environments/environment.proxy.ts new file mode 100644 index 000000000..0bb5c7ab1 --- /dev/null +++ b/UI/Web/src/environments/environment.proxy.ts @@ -0,0 +1,22 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +const IP = 'localhost'; + +export const environment = { + production: false, + apiUrl: 'http://' + IP + ':4200/api/', + hubUrl: 'http://'+ IP + ':4200/hubs/', + buyLink: 'https://buy.stripe.com/test_9AQ5mi058h1PcIo3cf?prefilled_promo_code=FREETRIAL', + manageLink: 'https://billing.stripe.com/p/login/test_14kfZocuh6Tz5ag7ss' +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/UI/Web/src/httpLoader.ts b/UI/Web/src/httpLoader.ts index a818bcdd8..c63d96fef 100644 --- a/UI/Web/src/httpLoader.ts +++ b/UI/Web/src/httpLoader.ts @@ -5,8 +5,9 @@ import cacheBusting from 'i18n-cache-busting.json'; // allowSyntheticDefaultImpo @Injectable({ providedIn: 'root' }) export class HttpLoader implements TranslocoLoader { + private http = inject(HttpClient); + private loadedVersions: { [key: string]: string } = {}; - constructor(private http: HttpClient) {} getTranslation(langPath: string) { const tokens = langPath.split('/'); diff --git a/UI/Web/src/styles.scss b/UI/Web/src/styles.scss index cac4fd9dd..bd2b91f28 100644 --- a/UI/Web/src/styles.scss +++ b/UI/Web/src/styles.scss @@ -57,6 +57,8 @@ @use './theme/utilities/global'; @use "./theme/utilities/spinners"; +@use './theme/directives/resize'; + // Global Styles @font-face { diff --git a/UI/Web/src/theme/components/_buttons.scss b/UI/Web/src/theme/components/_buttons.scss index 82603e761..387f39714 100644 --- a/UI/Web/src/theme/components/_buttons.scss +++ b/UI/Web/src/theme/components/_buttons.scss @@ -225,6 +225,20 @@ button i.fa { } +.btn-unstyled { + background: none; + border: none; + padding: 0; + margin: 0; + font: inherit; + cursor: pointer; + outline: inherit; +} + +.btn-unstyled:focus { + outline: none; +} + // //.btn-primary .btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show { // --bs-btn-active-bg: var(--primary-color-dark-shade); diff --git a/UI/Web/src/theme/directives/resize.scss b/UI/Web/src/theme/directives/resize.scss new file mode 100644 index 000000000..76cc06a48 --- /dev/null +++ b/UI/Web/src/theme/directives/resize.scss @@ -0,0 +1,71 @@ +.drag-handle-bottom, +.drag-handle-top { + position: sticky; + width: 100%; + height: 12px; + cursor: row-resize; + display: flex; + justify-content: center; + align-items: center; + z-index: 10; + background: transparent; + border: none; +} + +.drag-handle-bottom { + top: 0; + margin-top: 2px; +} + +.drag-handle-top { + bottom: 0; + margin-bottom: 2px; +} + +.drag-handle-start, +.drag-handle-end { + top: 50%; + transform: translateY(-50%); + width: 12px; + height: 60px; + cursor: col-resize; + display: flex; + justify-content: center; + align-items: center; + z-index: 10; + background: transparent; + border: none; +} + +.drag-handle-start { + position: absolute; + right: 0; +} + +.drag-handle-end { + position: absolute; + left: 0; +} + +.drag-indicator-horizontal { + width: 60px; + height: 8px; + background: var(--accent-bg-color); + border-radius: 4px; + transition: background-color 0.2s; +} + +.drag-indicator-vertical { + width: 8px; + height: 40px; + background: var(--accent-bg-color); + border-radius: 4px; + transition: background-color 0.2s; +} + +.drag-handle-bottom:hover .drag-indicator-horizontal, +.drag-handle-top:hover .drag-indicator-horizontal, +.drag-handle-start:hover .drag-indicator-vertical, +.drag-handle-end:hover .drag-indicator-vertical { + background: #6c757d; +}