From d77090beff2126a07b0b3fdc66a3fd2a04f43192 Mon Sep 17 00:00:00 2001 From: Amelia <77553571+Fesaa@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:32:01 +0200 Subject: [PATCH 1/9] First go will add comments in draft pull request --- API/API.csproj | 1 + API/Controllers/FontController.cs | 72 + API/DTOs/Font/EpubFontDto.cs | 13 + API/Data/DataContext.cs | 1 + ...20240621211843_EpubFontInitial.Designer.cs | 3078 +++++++++++++++++ .../20240621211843_EpubFontInitial.cs | 42 + .../Migrations/DataContextModelSnapshot.cs | 37 +- API/Data/Repositories/EpubFontRepository.cs | 77 + API/Data/Seed.cs | 31 + API/Data/UnitOfWork.cs | 2 + API/Entities/Enums/Font/FontProvider.cs | 13 + API/Entities/EpubFont.cs | 37 + .../ApplicationServiceExtensions.cs | 1 + API/Helpers/AutoMapperProfiles.cs | 3 + API/Program.cs | 1 + API/Services/DirectoryService.cs | 5 + API/Services/TaskScheduler.cs | 13 +- API/Services/Tasks/FontService.cs | 108 + .../book-reader/book-reader.component.scss | 42 - .../book-reader/book-reader.component.ts | 9 + .../reader-settings.component.ts | 18 +- .../app/book-reader/_services/book.service.ts | 27 +- .../user-preferences.component.ts | 6 +- 23 files changed, 3570 insertions(+), 67 deletions(-) create mode 100644 API/Controllers/FontController.cs create mode 100644 API/DTOs/Font/EpubFontDto.cs create mode 100644 API/Data/Migrations/20240621211843_EpubFontInitial.Designer.cs create mode 100644 API/Data/Migrations/20240621211843_EpubFontInitial.cs create mode 100644 API/Data/Repositories/EpubFontRepository.cs create mode 100644 API/Entities/Enums/Font/FontProvider.cs create mode 100644 API/Entities/EpubFont.cs create mode 100644 API/Services/Tasks/FontService.cs diff --git a/API/API.csproj b/API/API.csproj index 2f97240df..1375d7b79 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -190,6 +190,7 @@ + Always diff --git a/API/Controllers/FontController.cs b/API/Controllers/FontController.cs new file mode 100644 index 000000000..601f65d0e --- /dev/null +++ b/API/Controllers/FontController.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using API.Data; +using API.DTOs.Font; +using API.Services; +using API.Services.Tasks; +using Kavita.Common; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers; + +public class FontController : BaseApiController +{ + private readonly IUnitOfWork _unitOfWork; + private readonly IFontService _fontService; + private readonly ITaskScheduler _taskScheduler; + + public FontController(IUnitOfWork unitOfWork, IFontService fontService, ITaskScheduler taskScheduler) + { + _unitOfWork = unitOfWork; + _fontService = fontService; + _taskScheduler = taskScheduler; + } + + [ResponseCache(CacheProfileName = "10Minute")] + [AllowAnonymous] + [HttpGet("GetFonts")] + public async Task>> GetFonts() + { + return Ok(await _unitOfWork.EpubFontRepository.GetFontDtos()); + } + + [AllowAnonymous] + [HttpGet("download-font")] + public async Task GetFont(int fontId) + { + try + { + var font = await _unitOfWork.EpubFontRepository.GetFont(fontId); + if (font == null) return NotFound(); + var contentType = GetContentType(font.FileName); + return File(await _fontService.GetContent(fontId), contentType, font.FileName); + } + catch (KavitaException ex) + { + return BadRequest(ex.Message); + } + } + + [AllowAnonymous] + [HttpPost("scan")] + public IActionResult Scan() + { + _taskScheduler.ScanEpubFonts(); + return Ok(); + } + + private string GetContentType(string fileName) + { + var extension = Path.GetExtension(fileName).ToLowerInvariant(); + return extension switch + { + ".ttf" => "application/font-tff", + ".otf" => "application/font-otf", + ".woff" => "application/font-woff", + ".woff2" => "application/font-woff2", + _ => "application/octet-stream", + }; + } +} diff --git a/API/DTOs/Font/EpubFontDto.cs b/API/DTOs/Font/EpubFontDto.cs new file mode 100644 index 000000000..9397e8357 --- /dev/null +++ b/API/DTOs/Font/EpubFontDto.cs @@ -0,0 +1,13 @@ +using System; +using API.Entities.Enums.Font; + +namespace API.DTOs.Font; + +public class EpubFontDto +{ + public int Id { get; set; } + public string Name { get; set; } + public FontProvider Provider { get; set; } + public DateTime Created { get; set; } + public DateTime LastModified { get; set; } +} diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 4af165249..96252d0bd 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -66,6 +66,7 @@ public sealed class DataContext : IdentityDbContext ManualMigrationHistory { get; set; } = null!; public DbSet SeriesBlacklist { get; set; } = null!; public DbSet AppUserCollection { get; set; } = null!; + public DbSet EpubFont { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) diff --git a/API/Data/Migrations/20240621211843_EpubFontInitial.Designer.cs b/API/Data/Migrations/20240621211843_EpubFontInitial.Designer.cs new file mode 100644 index 000000000..986801433 --- /dev/null +++ b/API/Data/Migrations/20240621211843_EpubFontInitial.Designer.cs @@ -0,0 +1,3078 @@ +// +using System; +using API.Data; +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("20240621211843_EpubFontInitial")] + partial class EpubFontInitial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); + + 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("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("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + 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.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + 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.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + 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("Promoted") + .HasColumnType("INTEGER"); + + 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("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("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("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.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.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("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("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("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + 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("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .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("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .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.EpubFont", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("EpubFont"); + }); + + 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.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + 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("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("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("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.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.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("AverageScore") + .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.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("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + 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.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("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("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.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + 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("Promoted") + .HasColumnType("INTEGER"); + + 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("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .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("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("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + 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("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("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + 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("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + 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.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + 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.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.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.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.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("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .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("Bookmarks"); + + b.Navigation("Collections"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + 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.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/20240621211843_EpubFontInitial.cs b/API/Data/Migrations/20240621211843_EpubFontInitial.cs new file mode 100644 index 000000000..cd073fd35 --- /dev/null +++ b/API/Data/Migrations/20240621211843_EpubFontInitial.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class EpubFontInitial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EpubFont", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: true), + NormalizedName = table.Column(type: "TEXT", nullable: true), + FileName = table.Column(type: "TEXT", nullable: true), + Provider = table.Column(type: "INTEGER", nullable: false), + Created = table.Column(type: "TEXT", nullable: false), + CreatedUtc = table.Column(type: "TEXT", nullable: false), + LastModified = table.Column(type: "TEXT", nullable: false), + LastModifiedUtc = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EpubFont", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EpubFont"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index c59b774d6..ba7e61ab5 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -907,6 +907,41 @@ namespace API.Data.Migrations b.ToTable("Device"); }); + modelBuilder.Entity("API.Entities.EpubFont", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("EpubFont"); + }); + modelBuilder.Entity("API.Entities.FolderPath", b => { b.Property("Id") diff --git a/API/Data/Repositories/EpubFontRepository.cs b/API/Data/Repositories/EpubFontRepository.cs new file mode 100644 index 000000000..c29938be5 --- /dev/null +++ b/API/Data/Repositories/EpubFontRepository.cs @@ -0,0 +1,77 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.DTOs.Font; +using API.Entities; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Microsoft.EntityFrameworkCore; + +namespace API.Data.Repositories; + +public interface IEpubFontRepository +{ + void Add(EpubFont font); + void Remove(EpubFont font); + void Update(EpubFont font); + Task> GetFontDtos(); + Task GetFontDto(int fontId); + Task> GetFonts(); + Task GetFont(int fontId); +} + +public class EpubFontRepository: IEpubFontRepository +{ + private readonly DataContext _context; + private readonly IMapper _mapper; + + public EpubFontRepository(DataContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public void Add(EpubFont font) + { + _context.Add(font); + } + + public void Remove(EpubFont font) + { + _context.Remove(font); + } + + public void Update(EpubFont font) + { + _context.Entry(font).State = EntityState.Modified; + } + + public async Task> GetFontDtos() + { + return await _context.EpubFont + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + } + + public async Task GetFontDto(int fontId) + { + return await _context.EpubFont + .Where(f => f.Id == fontId) + .ProjectTo(_mapper.ConfigurationProvider) + .FirstOrDefaultAsync(); + } + + public async Task> GetFonts() + { + return await _context.EpubFont + .ToListAsync(); + } + + public async Task GetFont(int fontId) + { + return await _context.EpubFont + .Where(f => f.Id == fontId) + .FirstOrDefaultAsync(); + } +} diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index ddc682c32..ea96ea37a 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -9,9 +9,11 @@ using API.Constants; using API.Data.Repositories; using API.Entities; using API.Entities.Enums; +using API.Entities.Enums.Font; using API.Entities.Enums.Theme; using API.Extensions; using API.Services; +using API.Services.Tasks.Scanner.Parser; using Kavita.Common; using Kavita.Common.EnvironmentInfo; using Microsoft.AspNetCore.Identity; @@ -26,6 +28,20 @@ public static class Seed /// public static ImmutableArray DefaultSettings; + public static readonly ImmutableArray DefaultFonts = + [ + ..new List + { + new () + { + Name = "Merriweather", + NormalizedName = Parser.Normalize("Merriweather"), + Provider = FontProvider.System, + FileName = "Merriweather-Regular.woff2", + } + } + ]; + public static readonly ImmutableArray DefaultThemes = [ ..new List { @@ -153,6 +169,21 @@ public static class Seed await context.SaveChangesAsync(); } + public static async Task SeedFonts(DataContext context) + { + await context.Database.EnsureCreatedAsync(); + foreach (var font in DefaultFonts) + { + var existing = context.SiteTheme.FirstOrDefaultAsync(f => f.Name.Equals(font.Name)); + if (existing == null) + { + await context.EpubFont.AddAsync(font); + } + } + + await context.SaveChangesAsync(); + } + public static async Task SeedDefaultStreams(IUnitOfWork unitOfWork) { var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.DashboardStreams); diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index 97ef3e07b..645d258f0 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -31,6 +31,7 @@ public interface IUnitOfWork IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; } IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } + IEpubFontRepository EpubFontRepository { get; } bool Commit(); Task CommitAsync(); bool HasChanges(); @@ -74,6 +75,7 @@ public class UnitOfWork : IUnitOfWork public IAppUserSmartFilterRepository AppUserSmartFilterRepository => new AppUserSmartFilterRepository(_context, _mapper); public IAppUserExternalSourceRepository AppUserExternalSourceRepository => new AppUserExternalSourceRepository(_context, _mapper); public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository => new ExternalSeriesMetadataRepository(_context, _mapper); + public IEpubFontRepository EpubFontRepository => new EpubFontRepository(_context, _mapper); /// /// Commits changes to the DB. Completes the open transaction. diff --git a/API/Entities/Enums/Font/FontProvider.cs b/API/Entities/Enums/Font/FontProvider.cs new file mode 100644 index 000000000..ee944844a --- /dev/null +++ b/API/Entities/Enums/Font/FontProvider.cs @@ -0,0 +1,13 @@ +namespace API.Entities.Enums.Font; + +public enum FontProvider +{ + /// + /// Font is provider by System, always avaible + /// + System = 1, + /// + /// Font provider by the User + /// + User = 2, +} diff --git a/API/Entities/EpubFont.cs b/API/Entities/EpubFont.cs new file mode 100644 index 000000000..0cf745db6 --- /dev/null +++ b/API/Entities/EpubFont.cs @@ -0,0 +1,37 @@ +using System; +using API.Entities.Enums.Font; +using API.Entities.Interfaces; +using API.Services; + +namespace API.Entities; + +/// +/// Represents a user provider font to be used in the epub reader +/// +public class EpubFont: IEntityDate +{ + public int Id { get; set; } + + /// + /// Name of the font + /// + public required string Name { get; set; } + /// + /// Normalized name for lookups + /// + public required string NormalizedName { get; set; } + /// + /// Filename of the font, stored under + /// + /// System provided fonts use an alternative location as they are packaged with the app + public required string FileName { get; set; } + /// + /// Where the font came from + /// + public FontProvider Provider { get; set; } + + public DateTime Created { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModified { get; set; } + public DateTime LastModifiedUtc { get; set; } +} diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index f68b4461d..28f9cbbad 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -53,6 +53,7 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 874fabd9a..5648e5b90 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -10,6 +10,7 @@ using API.DTOs.Dashboard; using API.DTOs.Device; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; +using API.DTOs.Font; using API.DTOs.MediaErrors; using API.DTOs.Metadata; using API.DTOs.Progress; @@ -257,6 +258,8 @@ public class AutoMapperProfiles : Profile opt => opt.MapFrom(src => src.BookReaderLayoutMode)); + CreateMap(); + CreateMap(); diff --git a/API/Program.cs b/API/Program.cs index 9668a06da..6e4341ee6 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -126,6 +126,7 @@ public class Program await Seed.SeedRoles(services.GetRequiredService>()); await Seed.SeedSettings(context, directoryService); await Seed.SeedThemes(context); + await Seed.SeedFonts(context); await Seed.SeedDefaultStreams(unitOfWork); await Seed.SeedDefaultSideNavStreams(unitOfWork); await Seed.SeedUserApiKeys(context); diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 8c6c796c9..184f9d3bb 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -33,6 +33,7 @@ public interface IDirectoryService /// Original BookmarkDirectory. Only used for resetting directory. Use for actual path. /// string BookmarkDirectory { get; } + string EpubFontDirectory { get; } /// /// Lists out top-level folders for a given directory. Filters out System and Hidden folders. /// @@ -88,6 +89,8 @@ public class DirectoryService : IDirectoryService public string LocalizationDirectory { get; } public string CustomizedTemplateDirectory { get; } public string TemplateDirectory { get; } + public string EpubFontDirectory { get; } + private readonly ILogger _logger; private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase; @@ -125,6 +128,8 @@ public class DirectoryService : IDirectoryService ExistOrCreate(CustomizedTemplateDirectory); TemplateDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "EmailTemplates"); ExistOrCreate(TemplateDirectory); + EpubFontDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "fonts"); + ExistOrCreate(EpubFontDirectory); } /// diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 9704259c4..43b3ff5a7 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -35,6 +35,7 @@ public interface ITaskScheduler void CovertAllCoversToEncoding(); Task CleanupDbEntries(); Task CheckForUpdate(); + void ScanEpubFonts(); } public class TaskScheduler : ITaskScheduler @@ -57,6 +58,7 @@ public class TaskScheduler : ITaskScheduler private readonly ILicenseService _licenseService; private readonly IExternalMetadataService _externalMetadataService; private readonly ISmartCollectionSyncService _smartCollectionSyncService; + private readonly IFontService _fontService; public static BackgroundJobServer Client => new (); public const string ScanQueue = "scan"; @@ -93,7 +95,8 @@ public class TaskScheduler : ITaskScheduler ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService, IThemeService themeService, IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService, IMediaConversionService mediaConversionService, IScrobblingService scrobblingService, ILicenseService licenseService, - IExternalMetadataService externalMetadataService, ISmartCollectionSyncService smartCollectionSyncService) + IExternalMetadataService externalMetadataService, ISmartCollectionSyncService smartCollectionSyncService, + IFontService fontService) { _cacheService = cacheService; _logger = logger; @@ -112,6 +115,7 @@ public class TaskScheduler : ITaskScheduler _licenseService = licenseService; _externalMetadataService = externalMetadataService; _smartCollectionSyncService = smartCollectionSyncService; + _fontService = fontService; } public async Task ScheduleTasks() @@ -431,6 +435,13 @@ public class TaskScheduler : ITaskScheduler await _versionUpdaterService.PushUpdate(update); } + // TODO: Make this auto scan from time to time? + public void ScanEpubFonts() + { + _logger.LogInformation("Starting Epub Font scam"); + BackgroundJob.Enqueue(() => _fontService.Scan()); + } + /// /// If there is an enqueued or scheduled task for method /// diff --git a/API/Services/Tasks/FontService.cs b/API/Services/Tasks/FontService.cs new file mode 100644 index 000000000..cfa47f67f --- /dev/null +++ b/API/Services/Tasks/FontService.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using API.Data; +using API.Entities; +using API.Entities.Enums.Font; +using API.Services.Tasks.Scanner.Parser; +using API.SignalR; +using Kavita.Common; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; + +namespace API.Services.Tasks; + +public interface IFontService +{ + Task GetContent(int fontId); + Task Scan(); +} + +public class FontService: IFontService +{ + + private readonly IDirectoryService _directoryService; + private readonly IUnitOfWork _unitOfWork; + private readonly IHubContext _messageHub; + private readonly ILogger _logger; + + public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, IHubContext messageHub, + ILogger logger) + { + _directoryService = directoryService; + _unitOfWork = unitOfWork; + _messageHub = messageHub; + _logger = logger; + } + + public async Task GetContent(int fontId) + { + // TODO: Differentiate between Provider.User & Provider.System + var font = await _unitOfWork.EpubFontRepository.GetFont(fontId); + if (font == null) throw new KavitaException("Font file missing or invalid"); + var fontFile = _directoryService.FileSystem.Path.Join(_directoryService.EpubFontDirectory, font.FileName); + if (string.IsNullOrEmpty(fontFile) || !_directoryService.FileSystem.File.Exists(fontFile)) + throw new KavitaException("Font file missing or invalid"); + return await _directoryService.FileSystem.File.ReadAllBytesAsync(fontFile); + } + + public async Task Scan() + { + _directoryService.Exists(_directoryService.EpubFontDirectory); + var reservedNames = Seed.DefaultFonts.Select(f => f.NormalizedName).ToList(); + var fontFiles = + _directoryService.GetFilesWithExtension(Parser.NormalizePath(_directoryService.EpubFontDirectory), @"\.[woff2|tff|otf|woff]") + .Where(name => !reservedNames.Contains(Parser.Normalize(name))).ToList(); + + var allFonts = (await _unitOfWork.EpubFontRepository.GetFonts()).ToList(); + var userFonts = allFonts.Where(f => f.Provider == FontProvider.User).ToList(); + + foreach (var userFont in userFonts) + { + var filePath = Parser.NormalizePath( + _directoryService.FileSystem.Path.Join(_directoryService.EpubFontDirectory, userFont.FileName)); + if (_directoryService.FileSystem.File.Exists(filePath)) continue; + allFonts.Remove(userFont); + await RemoveFont(userFont); + + // TODO: Send update to UI + _logger.LogInformation("Removed a font because it didn't exist on disk {FilePath}", filePath); + } + + var allFontNames = allFonts.Select(f => f.NormalizedName).ToList(); + foreach (var fontFile in fontFiles) + { + var nakedFileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(fontFile); + // TODO: discuss this, using this to "prettyfy" the file name, to display in the UI + var fontName = Regex.Replace(nakedFileName, "[^a-zA-Z0-9]", " "); + var normalizedName = Parser.Normalize(nakedFileName); + if (allFontNames.Contains(normalizedName)) continue; + + _unitOfWork.EpubFontRepository.Add(new EpubFont() + { + Name = fontName, + NormalizedName = normalizedName, + FileName = _directoryService.FileSystem.Path.GetFileName(fontFile), + Provider = FontProvider.User, + }); + + // TODO: Send update to UI + _logger.LogInformation("Added a new font from disk {FontFile}", fontFile); + } + if (_unitOfWork.HasChanges()) + { + await _unitOfWork.CommitAsync(); + } + + // TODO: Send update to UI + _logger.LogInformation("Finished FontService#Scan"); + } + + public async Task RemoveFont(EpubFont font) + { + // TODO: Default font? Ask in kavita discord if needed, as we can always fallback to the browsers default font. + _unitOfWork.EpubFontRepository.Remove(font); + await _unitOfWork.CommitAsync(); + } +} 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 8d9a36f78..77cd193bb 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 @@ -1,45 +1,3 @@ -@font-face { - font-family: "Fira_Sans"; - src: url(../../../../assets/fonts/Fira_Sans/FiraSans-Regular.woff2) format("woff2"); - font-display: swap; -} - -@font-face { - font-family: "Lato"; - src: url(../../../../assets/fonts/Lato/Lato-Regular.woff2) format("woff2"); - font-display: swap; -} - -@font-face { - font-family: "Libre_Baskerville"; - src: url(../../../../assets/fonts/Libre_Baskerville/LibreBaskerville-Regular.woff2) format("woff2"); - font-display: swap; -} - -@font-face { - font-family: "Merriweather"; - src: url(../../../../assets/fonts/Merriweather/Merriweather-Regular.woff2) format("woff2"); - font-display: swap; -} - -@font-face { - font-family: "Nanum_Gothic"; - src: url(../../../../assets/fonts/Nanum_Gothic/NanumGothic-Regular.woff2) format("woff2"); - font-display: swap; -} - -@font-face { - font-family: "RocknRoll_One"; - src: url(../../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.woff2) format("woff2"); - font-display: swap; -} - -@font-face { - font-family: "OpenDyslexic2"; - src: url(../../../../assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.woff2) format("woff2"); - font-display: swap; -} - :root { --br-actionbar-button-text-color: #6c757d; --accordion-body-bg-color: black; 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 7a672693b..5e76f8007 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 @@ -583,6 +583,15 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } ngOnInit(): void { + this.bookService.getEpubFonts().subscribe(fonts => { + fonts.forEach(font => { + const fontFace = new FontFace(font.name, `url(${this.bookService.baseUrl}Font/download-font?fontId=${font.id})`); + fontFace.load().then(loadedFace => { + (document as any).fonts.add(loadedFace); + }); + }) + }) + const libraryId = this.route.snapshot.paramMap.get('libraryId'); const seriesId = this.route.snapshot.paramMap.get('seriesId'); const chapterId = this.route.snapshot.paramMap.get('chapterId'); 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 b4ea74806..0d19684c3 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 @@ -19,7 +19,7 @@ import { ThemeProvider } from 'src/app/_models/preferences/site-theme'; import { User } from 'src/app/_models/user'; import { AccountService } from 'src/app/_services/account.service'; import { ThemeService } from 'src/app/_services/theme.service'; -import { FontFamily, BookService } from '../../_services/book.service'; +import {BookService, EpubFont} from '../../_services/book.service'; import { BookBlackTheme } from '../../_models/book-black-theme'; import { BookDarkTheme } from '../../_models/book-dark-theme'; import { BookWhiteTheme } from '../../_models/book-white-theme'; @@ -131,7 +131,7 @@ export class ReaderSettingsComponent implements OnInit { * List of all font families user can select from */ fontOptions: Array = []; - fontFamilies: Array = []; + fontFamilies: Array = []; /** * Internal property used to capture all the different css properties to render on all elements */ @@ -174,10 +174,11 @@ export class ReaderSettingsComponent implements OnInit { private readonly cdRef: ChangeDetectorRef) {} ngOnInit(): void { - - this.fontFamilies = this.bookService.getFontFamilies(); - this.fontOptions = this.fontFamilies.map(f => f.title); - this.cdRef.markForCheck(); + this.bookService.getEpubFonts().subscribe(fonts => { + this.fontFamilies = fonts; + this.fontOptions = fonts.map(f => f.name); + this.cdRef.markForCheck(); + }) this.accountService.currentUser$.pipe(take(1)).subscribe(user => { if (user) { @@ -208,11 +209,10 @@ export class ReaderSettingsComponent implements OnInit { this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, [])); this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(fontName => { - const familyName = this.fontFamilies.filter(f => f.title === fontName)[0].family; - if (familyName === 'default') { + if (fontName === 'default') { this.pageStyles['font-family'] = 'inherit'; } else { - this.pageStyles['font-family'] = "'" + familyName + "'"; + this.pageStyles['font-family'] = "'" + fontName + "'"; } this.styleUpdate.emit(this.pageStyles); diff --git a/UI/Web/src/app/book-reader/_services/book.service.ts b/UI/Web/src/app/book-reader/_services/book.service.ts index 65549ab48..208ccd1d4 100644 --- a/UI/Web/src/app/book-reader/_services/book.service.ts +++ b/UI/Web/src/app/book-reader/_services/book.service.ts @@ -4,16 +4,19 @@ import { TextResonse } from 'src/app/_types/text-response'; import { environment } from 'src/environments/environment'; import { BookChapterItem } from '../_models/book-chapter-item'; import { BookInfo } from '../_models/book-info'; +import {Observable} from "rxjs"; -export interface FontFamily { - /** - * What the user should see - */ - title: string; - /** - * The actual font face - */ - family: string; +export enum FontProvider { + System = 1, + User = 2, +} + +export interface EpubFont { + id: number; + name: string; + provider: FontProvider; + created: Date; + lastModified: Date; } @Injectable({ @@ -25,10 +28,8 @@ export class BookService { constructor(private http: HttpClient) { } - getFontFamilies(): Array { - return [{title: 'default', family: 'default'}, {title: 'EBGaramond', family: 'EBGaramond'}, {title: 'Fira Sans', family: 'Fira_Sans'}, - {title: 'Lato', family: 'Lato'}, {title: 'Libre Baskerville', family: 'Libre_Baskerville'}, {title: 'Merriweather', family: 'Merriweather'}, - {title: 'Nanum Gothic', family: 'Nanum_Gothic'}, {title: 'RocknRoll One', family: 'RocknRoll_One'}, {title: 'Open Dyslexic', family: 'OpenDyslexic2'}]; + getEpubFonts(): Observable { + return this.http.get>(this.baseUrl + 'Font/GetFonts') } getBookChapters(chapterId: number) { diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index c50814872..582d2bfff 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -166,7 +166,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { constructor() { - this.fontFamilies = this.bookService.getFontFamilies().map(f => f.title); this.cdRef.markForCheck(); this.accountService.getOpdsUrl().subscribe(res => { @@ -174,6 +173,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.cdRef.markForCheck(); }); + this.bookService.getEpubFonts().subscribe(res => { + this.fontFamilies = res.map(f => f.name); + this.cdRef.markForCheck(); + }) + this.settingsService.getOpdsEnabled().subscribe(res => { this.opdsEnabled = res; this.cdRef.markForCheck(); From a956bb18ec37ad9c6e2aa80400236bd2e0ba1a12 Mon Sep 17 00:00:00 2001 From: Fesaa <77553571+Fesaa@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:57:47 +0200 Subject: [PATCH 2/9] UI centered font management --- API/Controllers/FontController.cs | 97 +++++--- API/Data/Repositories/EpubFontRepository.cs | 37 ++- API/Data/Repositories/UserRepository.cs | 9 + API/Services/TaskScheduler.cs | 13 +- API/Services/Tasks/FontService.cs | 125 ++++++----- API/Services/Tasks/Scanner/Parser/Parser.cs | 9 + .../src/app/_models/preferences/epub-font.ts | 18 ++ UI/Web/src/app/_services/font.service.ts | 61 +++++ .../book-reader/book-reader.component.ts | 9 +- .../reader-settings.component.ts | 8 +- .../app/book-reader/_services/book.service.ts | 18 -- .../font-manager/font-manager.component.html | 118 ++++++++++ .../font-manager/font-manager.component.scss | 26 +++ .../font-manager/font-manager.component.ts | 123 ++++++++++ .../user-preferences.component.html | 4 + .../user-preferences.component.ts | 11 +- UI/Web/src/assets/langs/en.json | 24 +- openapi.json | 210 +++++++++++++++++- 18 files changed, 774 insertions(+), 146 deletions(-) create mode 100644 UI/Web/src/app/_models/preferences/epub-font.ts create mode 100644 UI/Web/src/app/_services/font.service.ts create mode 100644 UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html create mode 100644 UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.scss create mode 100644 UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts diff --git a/API/Controllers/FontController.cs b/API/Controllers/FontController.cs index 601f65d0e..7f36bf521 100644 --- a/API/Controllers/FontController.cs +++ b/API/Controllers/FontController.cs @@ -1,72 +1,103 @@ +using System; using System.Collections.Generic; using System.IO; +using System.Text.RegularExpressions; using System.Threading.Tasks; +using API.Constants; using API.Data; using API.DTOs.Font; +using API.Extensions; using API.Services; using API.Services.Tasks; +using API.Services.Tasks.Scanner.Parser; +using AutoMapper; using Kavita.Common; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using MimeTypes; +using Serilog; namespace API.Controllers; public class FontController : BaseApiController { private readonly IUnitOfWork _unitOfWork; - private readonly IFontService _fontService; + private readonly IDirectoryService _directoryService; private readonly ITaskScheduler _taskScheduler; + private readonly IFontService _fontService; + private readonly IMapper _mapper; - public FontController(IUnitOfWork unitOfWork, IFontService fontService, ITaskScheduler taskScheduler) + private readonly Regex _fontFileExtensionRegex = new(Parser.FontFileExtensions, RegexOptions.IgnoreCase, Parser.RegexTimeout); + + public FontController(IUnitOfWork unitOfWork, ITaskScheduler taskScheduler, IDirectoryService directoryService, + IFontService fontService, IMapper mapper) { _unitOfWork = unitOfWork; - _fontService = fontService; + _directoryService = directoryService; _taskScheduler = taskScheduler; + _fontService = fontService; + _mapper = mapper; } [ResponseCache(CacheProfileName = "10Minute")] - [AllowAnonymous] - [HttpGet("GetFonts")] + [HttpGet("all")] public async Task>> GetFonts() { - return Ok(await _unitOfWork.EpubFontRepository.GetFontDtos()); + return Ok(await _unitOfWork.EpubFontRepository.GetFontDtosAsync()); } + [HttpGet] [AllowAnonymous] - [HttpGet("download-font")] - public async Task GetFont(int fontId) + public async Task GetFont(int fontId, string apiKey) { - try - { - var font = await _unitOfWork.EpubFontRepository.GetFont(fontId); - if (font == null) return NotFound(); - var contentType = GetContentType(font.FileName); - return File(await _fontService.GetContent(fontId), contentType, font.FileName); - } - catch (KavitaException ex) - { - return BadRequest(ex.Message); - } + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + + if (userId == 0) return BadRequest(); + + var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); + + if (font == null) return NotFound(); + + var contentType = MimeTypeMap.GetMimeType(Path.GetExtension(font.FileName)); + var path = Path.Join(_directoryService.EpubFontDirectory, font.FileName); + return PhysicalFile(path, contentType); } - [AllowAnonymous] - [HttpPost("scan")] - public IActionResult Scan() + [HttpDelete] + public async Task DeleteFont(int fontId) { - _taskScheduler.ScanEpubFonts(); + await _fontService.Delete(fontId); return Ok(); } - private string GetContentType(string fileName) + [HttpPost("upload")] + public async Task> UploadFont(IFormFile formFile) { - var extension = Path.GetExtension(fileName).ToLowerInvariant(); - return extension switch - { - ".ttf" => "application/font-tff", - ".otf" => "application/font-otf", - ".woff" => "application/font-woff", - ".woff2" => "application/font-woff2", - _ => "application/octet-stream", - }; + if (!_fontFileExtensionRegex.IsMatch(Path.GetExtension(formFile.FileName))) + return BadRequest("Invalid file"); + + if (formFile.FileName.Contains("..")) + return BadRequest("Invalid file"); + + var tempFile = await UploadToTemp(formFile); + var font = await _fontService.CreateFontFromFileAsync(tempFile); + return Ok(_mapper.Map(font)); + } + + [HttpPost("upload-url")] + public async Task> UploadFontByUrl(string url) + { + throw new NotImplementedException(); + } + + private async Task UploadToTemp(IFormFile file) + { + var outputFile = Path.Join(_directoryService.TempDirectory, file.FileName); + await using var stream = System.IO.File.Create(outputFile); + await file.CopyToAsync(stream); + stream.Close(); + return outputFile; } } diff --git a/API/Data/Repositories/EpubFontRepository.cs b/API/Data/Repositories/EpubFontRepository.cs index c29938be5..7be343b55 100644 --- a/API/Data/Repositories/EpubFontRepository.cs +++ b/API/Data/Repositories/EpubFontRepository.cs @@ -15,10 +15,12 @@ public interface IEpubFontRepository void Add(EpubFont font); void Remove(EpubFont font); void Update(EpubFont font); - Task> GetFontDtos(); - Task GetFontDto(int fontId); - Task> GetFonts(); - Task GetFont(int fontId); + Task> GetFontDtosAsync(); + Task GetFontDtoAsync(int fontId); + Task GetFontDtoByNameAsync(string name); + Task> GetFontsAsync(); + Task GetFontAsync(int fontId); + Task IsFontInUseAsync(int fontId); } public class EpubFontRepository: IEpubFontRepository @@ -47,14 +49,14 @@ public class EpubFontRepository: IEpubFontRepository _context.Entry(font).State = EntityState.Modified; } - public async Task> GetFontDtos() + public async Task> GetFontDtosAsync() { return await _context.EpubFont .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } - public async Task GetFontDto(int fontId) + public async Task GetFontDtoAsync(int fontId) { return await _context.EpubFont .Where(f => f.Id == fontId) @@ -62,16 +64,35 @@ public class EpubFontRepository: IEpubFontRepository .FirstOrDefaultAsync(); } - public async Task> GetFonts() + public async Task GetFontDtoByNameAsync(string name) + { + return await _context.EpubFont + .Where(f => f.Name.Equals(name)) + .ProjectTo(_mapper.ConfigurationProvider) + .FirstOrDefaultAsync(); + } + + public async Task> GetFontsAsync() { return await _context.EpubFont .ToListAsync(); } - public async Task GetFont(int fontId) + public async Task GetFontAsync(int fontId) { return await _context.EpubFont .Where(f => f.Id == fontId) .FirstOrDefaultAsync(); } + + public async Task IsFontInUseAsync(int fontId) + { + return await _context.AppUserPreferences + .Join(_context.EpubFont, + preference => preference.BookReaderFontFamily, + font => font.Name, + (preference, font) => new { preference, font }) + .AnyAsync(joined => joined.font.Id == fontId); + } + } diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 07723bf1b..70b4ef8f4 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -76,6 +76,7 @@ public interface IUserRepository Task> GetAllBookmarksByIds(IList bookmarkIds); Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None); Task> GetAllPreferencesByThemeAsync(int themeId); + Task> GetAllPreferencesByFontAsync(string fontName); Task HasAccessToLibrary(int libraryId, int userId); Task HasAccessToSeries(int userId, int seriesId); Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None); @@ -260,6 +261,14 @@ public class UserRepository : IUserRepository .ToListAsync(); } + public async Task> GetAllPreferencesByFontAsync(string fontName) + { + return await _context.AppUserPreferences + .Where(p => p.BookReaderFontFamily == fontName) + .AsSplitQuery() + .ToListAsync(); + } + public async Task HasAccessToLibrary(int libraryId, int userId) { return await _context.Library diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 1c5c803e3..ebfc2f145 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -37,7 +37,6 @@ public interface ITaskScheduler void CovertAllCoversToEncoding(); Task CleanupDbEntries(); Task CheckForUpdate(); - void ScanEpubFonts(); } public class TaskScheduler : ITaskScheduler @@ -60,7 +59,6 @@ public class TaskScheduler : ITaskScheduler private readonly ILicenseService _licenseService; private readonly IExternalMetadataService _externalMetadataService; private readonly ISmartCollectionSyncService _smartCollectionSyncService; - private readonly IFontService _fontService; private readonly IEventHub _eventHub; public static BackgroundJobServer Client => new (); @@ -98,8 +96,7 @@ public class TaskScheduler : ITaskScheduler ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService, IThemeService themeService, IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService, IMediaConversionService mediaConversionService, IScrobblingService scrobblingService, ILicenseService licenseService, - IExternalMetadataService externalMetadataService, ISmartCollectionSyncService smartCollectionSyncService, IEventHub eventHub, - IFontService fontService) + IExternalMetadataService externalMetadataService, ISmartCollectionSyncService smartCollectionSyncService, IEventHub eventHub) { _cacheService = cacheService; _logger = logger; @@ -118,7 +115,6 @@ public class TaskScheduler : ITaskScheduler _licenseService = licenseService; _externalMetadataService = externalMetadataService; _smartCollectionSyncService = smartCollectionSyncService; - _fontService = fontService; _eventHub = eventHub; } @@ -448,13 +444,6 @@ public class TaskScheduler : ITaskScheduler await _versionUpdaterService.PushUpdate(update); } - // TODO: Make this auto scan from time to time? - public void ScanEpubFonts() - { - _logger.LogInformation("Starting Epub Font scam"); - BackgroundJob.Enqueue(() => _fontService.Scan()); - } - /// /// If there is an enqueued or scheduled task for method /// diff --git a/API/Services/Tasks/FontService.cs b/API/Services/Tasks/FontService.cs index cfa47f67f..a85148857 100644 --- a/API/Services/Tasks/FontService.cs +++ b/API/Services/Tasks/FontService.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -15,93 +16,99 @@ namespace API.Services.Tasks; public interface IFontService { - Task GetContent(int fontId); - Task Scan(); + Task CreateFontFromFileAsync(string path); + Task Delete(int fontId); } public class FontService: IFontService { + public static readonly string DefaultFont = "default"; + private readonly IDirectoryService _directoryService; private readonly IUnitOfWork _unitOfWork; - private readonly IHubContext _messageHub; private readonly ILogger _logger; - public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, IHubContext messageHub, - ILogger logger) + public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger logger) { _directoryService = directoryService; _unitOfWork = unitOfWork; - _messageHub = messageHub; _logger = logger; } - public async Task GetContent(int fontId) + public async Task CreateFontFromFileAsync(string path) { - // TODO: Differentiate between Provider.User & Provider.System - var font = await _unitOfWork.EpubFontRepository.GetFont(fontId); - if (font == null) throw new KavitaException("Font file missing or invalid"); - var fontFile = _directoryService.FileSystem.Path.Join(_directoryService.EpubFontDirectory, font.FileName); - if (string.IsNullOrEmpty(fontFile) || !_directoryService.FileSystem.File.Exists(fontFile)) - throw new KavitaException("Font file missing or invalid"); - return await _directoryService.FileSystem.File.ReadAllBytesAsync(fontFile); - } - - public async Task Scan() - { - _directoryService.Exists(_directoryService.EpubFontDirectory); - var reservedNames = Seed.DefaultFonts.Select(f => f.NormalizedName).ToList(); - var fontFiles = - _directoryService.GetFilesWithExtension(Parser.NormalizePath(_directoryService.EpubFontDirectory), @"\.[woff2|tff|otf|woff]") - .Where(name => !reservedNames.Contains(Parser.Normalize(name))).ToList(); - - var allFonts = (await _unitOfWork.EpubFontRepository.GetFonts()).ToList(); - var userFonts = allFonts.Where(f => f.Provider == FontProvider.User).ToList(); - - foreach (var userFont in userFonts) + if (!_directoryService.FileSystem.File.Exists(path)) { - var filePath = Parser.NormalizePath( - _directoryService.FileSystem.Path.Join(_directoryService.EpubFontDirectory, userFont.FileName)); - if (_directoryService.FileSystem.File.Exists(filePath)) continue; - allFonts.Remove(userFont); - await RemoveFont(userFont); - - // TODO: Send update to UI - _logger.LogInformation("Removed a font because it didn't exist on disk {FilePath}", filePath); + _logger.LogInformation("Unable to create font from manual upload as font not in temp"); + throw new KavitaException("errors.font-manual-upload"); } - var allFontNames = allFonts.Select(f => f.NormalizedName).ToList(); - foreach (var fontFile in fontFiles) - { - var nakedFileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(fontFile); - // TODO: discuss this, using this to "prettyfy" the file name, to display in the UI - var fontName = Regex.Replace(nakedFileName, "[^a-zA-Z0-9]", " "); - var normalizedName = Parser.Normalize(nakedFileName); - if (allFontNames.Contains(normalizedName)) continue; + var fileName = _directoryService.FileSystem.FileInfo.New(path).Name; + var nakedFileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(fileName); + var fontName = Parser.PrettifyFileName(nakedFileName); + var normalizedName = Parser.Normalize(nakedFileName); - _unitOfWork.EpubFontRepository.Add(new EpubFont() - { - Name = fontName, - NormalizedName = normalizedName, - FileName = _directoryService.FileSystem.Path.GetFileName(fontFile), - Provider = FontProvider.User, - }); - - // TODO: Send update to UI - _logger.LogInformation("Added a new font from disk {FontFile}", fontFile); - } - if (_unitOfWork.HasChanges()) + if (await _unitOfWork.EpubFontRepository.GetFontDtoByNameAsync(fontName) != null) { - await _unitOfWork.CommitAsync(); + throw new KavitaException("errors.font-already-in-use"); } + _directoryService.CopyFileToDirectory(path, _directoryService.EpubFontDirectory); + var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.EpubFontDirectory, fileName); + + var font = new EpubFont() + { + Name = fontName, + NormalizedName = normalizedName, + FileName = Path.GetFileName(finalLocation), + Provider = FontProvider.User + }; + _unitOfWork.EpubFontRepository.Add(font); + await _unitOfWork.CommitAsync(); + // TODO: Send update to UI - _logger.LogInformation("Finished FontService#Scan"); + return font; + } + + public async Task Delete(int fontId) + { + if (await _unitOfWork.EpubFontRepository.IsFontInUseAsync(fontId)) + { + throw new KavitaException("errors.delete-font-in-use"); + } + + var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); + if (font == null) + return; + + await RemoveFont(font); } public async Task RemoveFont(EpubFont font) { - // TODO: Default font? Ask in kavita discord if needed, as we can always fallback to the browsers default font. + if (font.Provider == FontProvider.System) + return; + + var prefs = await _unitOfWork.UserRepository.GetAllPreferencesByFontAsync(font.Name); + foreach (var pref in prefs) + { + pref.BookReaderFontFamily = DefaultFont; + _unitOfWork.UserRepository.Update(pref); + } + + try + { + // Copy the theme file to temp for nightly removal (to give user time to reclaim if made a mistake) + var existingLocation = + _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, font.FileName); + var newLocation = + _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, font.FileName); + _directoryService.CopyFileToDirectory(existingLocation, newLocation); + _directoryService.DeleteFiles([existingLocation]); + } + catch (Exception) { /* Swallow */ } + _unitOfWork.EpubFontRepository.Remove(font); await _unitOfWork.CommitAsync(); } diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 840e7a6d8..f581c8de5 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -32,6 +32,7 @@ public static class Parser private const string BookFileExtensions = EpubFileExtension + "|" + PdfFileExtension; private const string XmlRegexExtensions = @"\.xml"; public const string MacOsMetadataFileStartsWith = @"._"; + public const string FontFileExtensions = @"\.[woff2|tff|otf|woff]"; public const string SupportedExtensions = ArchiveFileExtensions + "|" + ImageFileExtensions + "|" + BookFileExtensions; @@ -1259,4 +1260,12 @@ public static class Parser } return filename; } + + /** + * Replaced non-alphanumerical chars with a space + */ + public static string PrettifyFileName(string name) + { + return Regex.Replace(name, "[^a-zA-Z0-9]", " "); + } } diff --git a/UI/Web/src/app/_models/preferences/epub-font.ts b/UI/Web/src/app/_models/preferences/epub-font.ts new file mode 100644 index 000000000..29ffa2cb6 --- /dev/null +++ b/UI/Web/src/app/_models/preferences/epub-font.ts @@ -0,0 +1,18 @@ +/** + * Where does the font come from + */ +export enum FontProvider { + System = 1, + User = 2, +} + +/** + * Font used in the book reader + */ +export interface EpubFont { + id: number; + name: string; + provider: FontProvider; + created: Date; + lastModified: Date; +} diff --git a/UI/Web/src/app/_services/font.service.ts b/UI/Web/src/app/_services/font.service.ts new file mode 100644 index 000000000..4ff4e5f1c --- /dev/null +++ b/UI/Web/src/app/_services/font.service.ts @@ -0,0 +1,61 @@ +import {DestroyRef, inject, Injectable} from "@angular/core"; +import {map, ReplaySubject} from "rxjs"; +import {EpubFont} from "../_models/preferences/epub-font"; +import {environment} from 'src/environments/environment'; +import {HttpClient} from "@angular/common/http"; +import {EVENTS, MessageHubService} from "./message-hub.service"; +import {NgxFileDropEntry} from "ngx-file-drop"; +import {AccountService} from "./account.service"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {NotificationProgressEvent} from "../_models/events/notification-progress-event"; + +@Injectable({ + providedIn: 'root' +}) +export class FontService { + private readonly destroyRef = inject(DestroyRef); + public defaultEpubFont: string = 'default'; + + private fontsSource = new ReplaySubject(1); + public fonts$ = this.fontsSource.asObservable(); + + baseUrl: string = environment.apiUrl; + apiKey: string = ''; + encodedKey: string = ''; + + constructor(private httpClient: HttpClient, messageHub: MessageHubService, private accountService: AccountService) { + this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => { + if (user) { + this.apiKey = user.apiKey; + this.encodedKey = encodeURIComponent(this.apiKey); + } + }); + } + + getFonts() { + return this.httpClient.get>(this.baseUrl + 'font/all').pipe(map(fonts => { + this.fontsSource.next(fonts); + + return fonts; + })); + } + + getFontFace(font: EpubFont): FontFace { + return new FontFace(font.name, `url(${this.baseUrl}font?fontId=${font.id}&apiKey=${this.encodedKey})`); + } + + uploadFont(fontFile: File, fileEntry: NgxFileDropEntry) { + const formData = new FormData(); + formData.append('formFile', fontFile, fileEntry.relativePath); + return this.httpClient.post(this.baseUrl + "font/upload", formData); + } + + uploadFromUrl(url: string) { + + } + + deleteFont(id: number) { + return this.httpClient.delete(this.baseUrl + `font?fontId=${id}`); + } + +} 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 5e76f8007..08cecc80b 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 @@ -52,6 +52,7 @@ import { PersonalToCEvent } from "../personal-table-of-contents/personal-table-of-contents.component"; import {translate, TranslocoDirective} from "@ngneat/transloco"; +import {FontService} from "../../../_services/font.service"; enum TabID { @@ -124,6 +125,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { private readonly libraryService = inject(LibraryService); private readonly themeService = inject(ThemeService); private readonly cdRef = inject(ChangeDetectorRef); + private readonly fontService = inject(FontService); protected readonly BookPageLayoutMode = BookPageLayoutMode; protected readonly WritingStyle = WritingStyle; @@ -583,11 +585,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } ngOnInit(): void { - this.bookService.getEpubFonts().subscribe(fonts => { + this.fontService.getFonts().subscribe(fonts => { fonts.forEach(font => { - const fontFace = new FontFace(font.name, `url(${this.bookService.baseUrl}Font/download-font?fontId=${font.id})`); - fontFace.load().then(loadedFace => { - (document as any).fonts.add(loadedFace); + this.fontService.getFontFace(font).load().then(loadedFace => { + (this.document as any).fonts.add(loadedFace); }); }) }) 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 0d19684c3..20b1bb99e 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 @@ -19,7 +19,7 @@ import { ThemeProvider } from 'src/app/_models/preferences/site-theme'; import { User } from 'src/app/_models/user'; import { AccountService } from 'src/app/_services/account.service'; import { ThemeService } from 'src/app/_services/theme.service'; -import {BookService, EpubFont} from '../../_services/book.service'; +import {BookService} from '../../_services/book.service'; import { BookBlackTheme } from '../../_models/book-black-theme'; import { BookDarkTheme } from '../../_models/book-dark-theme'; import { BookWhiteTheme } from '../../_models/book-white-theme'; @@ -27,6 +27,8 @@ import { BookPaperTheme } from '../../_models/book-paper-theme'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import { NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import {TranslocoDirective} from "@ngneat/transloco"; +import {FontService} from "../../../_services/font.service"; +import {EpubFont} from "../../../_models/preferences/epub-font"; /** * Used for book reader. Do not use for other components @@ -171,10 +173,10 @@ export class ReaderSettingsComponent implements OnInit { constructor(private bookService: BookService, private accountService: AccountService, @Inject(DOCUMENT) private document: Document, private themeService: ThemeService, - private readonly cdRef: ChangeDetectorRef) {} + private readonly cdRef: ChangeDetectorRef, private fontService: FontService) {} ngOnInit(): void { - this.bookService.getEpubFonts().subscribe(fonts => { + this.fontService.getFonts().subscribe(fonts => { this.fontFamilies = fonts; this.fontOptions = fonts.map(f => f.name); this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/book-reader/_services/book.service.ts b/UI/Web/src/app/book-reader/_services/book.service.ts index 208ccd1d4..18e96048d 100644 --- a/UI/Web/src/app/book-reader/_services/book.service.ts +++ b/UI/Web/src/app/book-reader/_services/book.service.ts @@ -4,20 +4,6 @@ import { TextResonse } from 'src/app/_types/text-response'; import { environment } from 'src/environments/environment'; import { BookChapterItem } from '../_models/book-chapter-item'; import { BookInfo } from '../_models/book-info'; -import {Observable} from "rxjs"; - -export enum FontProvider { - System = 1, - User = 2, -} - -export interface EpubFont { - id: number; - name: string; - provider: FontProvider; - created: Date; - lastModified: Date; -} @Injectable({ providedIn: 'root' @@ -28,10 +14,6 @@ export class BookService { constructor(private http: HttpClient) { } - getEpubFonts(): Observable { - return this.http.get>(this.baseUrl + 'Font/GetFonts') - } - getBookChapters(chapterId: number) { return this.http.get>(this.baseUrl + 'book/' + chapterId + '/chapters'); } diff --git a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html new file mode 100644 index 000000000..fc615be0c --- /dev/null +++ b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html @@ -0,0 +1,118 @@ + +
+
+

{{t('title')}}

+
+ +

{{t('description')}}

+ +
+
+ +
+
+
+
+ {{t('preview-default')}} +
+
+
+
+ + @if (isUploadingFont) { + + } @else { +
+ + + + + + +
+
+ + + +
+ +
+ +
+ +
+
+
+ } + +
+
+ +
+
+
+
    + + @for (font of fontService.fonts$ | async; track font.name) { + + } +
+
+
+
+ +
+ + +
  • +
    +
    +
    {{item.name | sentenceCase}}
    +
    + +
    + @if (item.hasOwnProperty('provider') && item.provider === FontProvider.User && item.hasOwnProperty('id')) { + + } + +
    + {{item.provider | siteThemeProvider}} +
    + +
    +
    + + + + The quick brown fox jumps over the lazy dog + +
  • +
    + + + +
    diff --git a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.scss b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.scss new file mode 100644 index 000000000..10cde3389 --- /dev/null +++ b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.scss @@ -0,0 +1,26 @@ +.pill { + font-size: .8rem; + background-color: var(--card-bg-color); + border-radius: 0.375rem; +} + +.list-group-item, .list-group-item.active { + border-top-width: 0; + border-bottom-width: 0; +} + +ngx-file-drop ::ng-deep > div { + // styling for the outer drop box + width: 100%; + border: 2px solid var(--primary-color); + border-radius: 5px; + height: 100px; + margin: auto; + + > div { + // styling for the inner box (template) + width: 100%; + display: inline-block; + + } +} diff --git a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts new file mode 100644 index 000000000..c6275388b --- /dev/null +++ b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts @@ -0,0 +1,123 @@ +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Inject, inject, OnInit} from '@angular/core'; +import {translate, TranslocoDirective} from "@ngneat/transloco"; +import {FontService} from "src/app/_services/font.service"; +import {AccountService} from "../../../_services/account.service"; +import {ToastrService} from "ngx-toastr"; +import {ConfirmService} from "../../../shared/confirm.service"; +import {EpubFont, FontProvider} from 'src/app/_models/preferences/epub-font'; +import {User} from "../../../_models/user"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {shareReplay} from "rxjs/operators"; +import {map} from "rxjs"; +import {NgxFileDropEntry, NgxFileDropModule} from "ngx-file-drop"; +import {AsyncPipe, DOCUMENT, NgIf, NgStyle, NgTemplateOutlet} from "@angular/common"; +import {LoadingComponent} from "../../../shared/loading/loading.component"; +import {FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms"; +import {SentenceCasePipe} from "../../../_pipes/sentence-case.pipe"; +import {SiteThemeProviderPipe} from "../../../_pipes/site-theme-provider.pipe"; + +@Component({ + selector: 'app-font-manager', + imports: [ + TranslocoDirective, + AsyncPipe, + LoadingComponent, + NgxFileDropModule, + FormsModule, + NgIf, + ReactiveFormsModule, + SentenceCasePipe, + SiteThemeProviderPipe, + NgTemplateOutlet, + NgStyle + ], + templateUrl: './font-manager.component.html', + styleUrl: './font-manager.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class FontManagerComponent implements OnInit { + private readonly destroyRef = inject(DestroyRef); + protected readonly fontService = inject(FontService); + private readonly accountService = inject(AccountService); + public readonly fb = inject(FormBuilder); + private readonly toastr = inject(ToastrService); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly confirmService = inject(ConfirmService); + + protected readonly FontProvider = FontProvider; + + user: User | undefined; + fonts: Array = []; + hasAdmin$ = this.accountService.currentUser$.pipe( + takeUntilDestroyed(this.destroyRef), shareReplay({refCount: true, bufferSize: 1}), + map(c => c && this.accountService.hasAdminRole(c)) + ); + + form!: FormGroup; + acceptableExtensions = ['.woff2', 'woff', 'tff', 'otf'].join(','); + mode: 'file' | 'url' | 'all' = 'all'; + isUploadingFont: boolean = false; + + constructor(@Inject(DOCUMENT) private document: Document) { + } + + ngOnInit() { + this.form = this.fb.group({ + coverImageUrl: new FormControl('', []) + }); + this.cdRef.markForCheck(); + + this.fontService.getFonts().subscribe(fonts => { + this.fonts = fonts; + this.fonts.forEach(font => { + this.fontService.getFontFace(font).load().then(loadedFace => { + (this.document as any).fonts.add(loadedFace); + }); + }) + + this.cdRef.markForCheck(); + }); + } + + dropped(files: NgxFileDropEntry[]) { + for (const droppedFile of files) { + if (!droppedFile.fileEntry.isFile) { + continue; + } + + const fileEntry = droppedFile.fileEntry as FileSystemFileEntry; + fileEntry.file((file: File) => { + this.fontService.uploadFont(file, droppedFile).subscribe(f => { + this.isUploadingFont = false; + this.fonts = [...this.fonts, f]; + this.cdRef.markForCheck(); + }); + }); + } + this.isUploadingFont = true; + this.cdRef.markForCheck(); + } + + uploadFromUrl(url?: string) { + url = url || this.form.get('coverImageUrl')?.value.trim(); + if (!url || url === '') return; + } + + async deleteFont(id: number) { + if (!await this.confirmService.confirm(translate('toasts.confirm-delete-font'))) { + return; + } + + this.fontService.deleteFont(id).subscribe(() => { + this.fonts = this.fonts.filter(f => f.id !== id); + this.cdRef.markForCheck(); + }); + } + + changeMode(mode: 'file' | 'url' | 'all') { + this.mode = mode; + } + + +} diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html index 4ac8b10b2..6da2b5b9c 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html @@ -619,6 +619,10 @@ } + @defer (when tab.fragment === FragmentID.Font; prefetch on idle) { + + } + @defer (when tab.fragment === FragmentID.Devices; prefetch on idle) { } diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index 582d2bfff..9da748e86 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -33,7 +33,6 @@ import {SettingsService} from 'src/app/admin/settings.service'; import {BookPageLayoutMode} from 'src/app/_models/readers/book-page-layout-mode'; import {forkJoin} from 'rxjs'; import {bookColorThemes} from 'src/app/book-reader/_components/reader-settings/reader-settings.component'; -import {BookService} from 'src/app/book-reader/_services/book.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {SentenceCasePipe} from '../../_pipes/sentence-case.pipe'; import {UserHoldsComponent} from '../user-holds/user-holds.component'; @@ -77,6 +76,8 @@ import {PdfTheme} from "../../_models/preferences/pdf-theme"; import {PdfScrollMode} from "../../_models/preferences/pdf-scroll-mode"; import {PdfLayoutMode} from "../../_models/preferences/pdf-layout-mode"; import {PdfSpreadMode} from "../../_models/preferences/pdf-spread-mode"; +import {FontManagerComponent} from "../font-manager/font-manager/font-manager.component"; +import {FontService} from "../../_services/font.service"; enum AccordionPanelID { ImageReader = 'image-reader', @@ -90,6 +91,7 @@ enum FragmentID { Preferences = '', Clients = 'clients', Theme = 'theme', + Font = 'font', Devices = 'devices', Stats = 'stats', Scrobbling = 'scrobbling' @@ -105,14 +107,14 @@ enum FragmentID { ChangePasswordComponent, ChangeAgeRestrictionComponent, ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, ColorPickerModule, ApiKeyComponent, ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe, - TranslocoDirective, LoadingComponent, ManageScrobblingProvidersComponent, PdfLayoutModePipe], + TranslocoDirective, LoadingComponent, ManageScrobblingProvidersComponent, PdfLayoutModePipe, FontManagerComponent], }) export class UserPreferencesComponent implements OnInit, OnDestroy { private readonly destroyRef = inject(DestroyRef); private readonly accountService = inject(AccountService); private readonly toastr = inject(ToastrService); - private readonly bookService = inject(BookService); + private readonly fontService = inject(FontService); private readonly titleService = inject(Title); private readonly route = inject(ActivatedRoute); private readonly settingsService = inject(SettingsService); @@ -153,6 +155,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { {title: 'preferences-tab', fragment: FragmentID.Preferences}, {title: '3rd-party-clients-tab', fragment: FragmentID.Clients}, {title: 'theme-tab', fragment: FragmentID.Theme}, + {title: 'font-tab', fragment: FragmentID.Font}, {title: 'devices-tab', fragment: FragmentID.Devices}, {title: 'stats-tab', fragment: FragmentID.Stats}, ]; @@ -173,7 +176,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.cdRef.markForCheck(); }); - this.bookService.getEpubFonts().subscribe(res => { + this.fontService.getFonts().subscribe(res => { this.fontFamilies = res.map(f => f.name); this.cdRef.markForCheck(); }) diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index da092a9b0..61da27916 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -102,6 +102,7 @@ "preferences-tab": "Preferences", "3rd-party-clients-tab": "3rd Party Clients", "theme-tab": "Theme", + "font-tab": "Font", "devices-tab": "Devices", "stats-tab": "Stats", "scrobbling-tab": "Scrobbling", @@ -203,6 +204,21 @@ "preview-title": "Preview" }, + "font-manager": { + "title": "Font Manager", + "description": "Kavita comes with a few fonts, but you can add your own. This page is for adding and removing fonts, use the book reading settings to choose the font you want to use.", + "enter-an-url-pre-title": "Enter an {{url}}", + "url": "url", + "drag-n-drop": "{{cover-image-chooser.drag-n-drop}}", + "upload": "{{cover-image-chooser.upload}}", + "upload-continued": "a font file", + "preview-default": "Upload your own font via file or url", + "delete": "{{common.delete}}", + "url-label": "Url", + "back": "Back", + "load": "Load" + }, + "theme": { "theme-dark": "Dark", "theme-black": "Black", @@ -1972,7 +1988,10 @@ "invalid-password-reset-url": "Invalid reset password url", "delete-theme-in-use": "Theme is currently in use by at least one user, cannot delete", "theme-manual-upload": "There was an issue creating Theme from manual upload", - "theme-already-in-use": "Theme already exists by that name" + "theme-already-in-use": "Theme already exists by that name", + "delete-font-in-use": "Font is currently in use by at least one user, cannot delete", + "font-manual-upload": "There was an issue creating Font from manual upload", + "font-already-in-use": "Font already exists by that name" }, "metadata-builder": { @@ -2218,7 +2237,8 @@ "pdf-book-mode-screen-size": "Screen too small for Book mode", "stack-imported": "Stack Imported", "confirm-delete-theme": "Removing this theme will delete it from the disk. You can grab it from temp directory before removal", - "mal-token-required": "MAL Token is required, set in User Settings" + "mal-token-required": "MAL Token is required, set in User Settings", + "confirm-delete-font": "Removing this font will delete it from the disk. You can grab it from temp directory before removal" }, "actionable": { diff --git a/openapi.json b/openapi.json index 1cdd53a63..29f60d182 100644 --- a/openapi.json +++ b/openapi.json @@ -2314,6 +2314,180 @@ } } }, + "/api/Font/all": { + "get": { + "tags": [ + "Font" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EpubFontDto" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EpubFontDto" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EpubFontDto" + } + } + } + } + } + } + } + }, + "/api/Font": { + "get": { + "tags": [ + "Font" + ], + "parameters": [ + { + "name": "fontId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "apiKey", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Font" + ], + "parameters": [ + { + "name": "fontId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/Font/upload": { + "post": { + "tags": [ + "Font" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "formFile": { + "type": "string", + "format": "binary" + } + } + }, + "encoding": { + "formFile": { + "style": "form" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/EpubFontDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/EpubFontDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/EpubFontDto" + } + } + } + } + } + } + }, + "/api/Font/upload-url": { + "post": { + "tags": [ + "Font" + ], + "parameters": [ + { + "name": "url", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/EpubFontDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/EpubFontDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/EpubFontDto" + } + } + } + } + } + } + }, "/api/Health": { "get": { "tags": [ @@ -13610,7 +13784,7 @@ }, "missingSeriesFromSource": { "type": "string", - "description": "A \r\n separated string of all missing series", + "description": "A \n separated string of all missing series", "nullable": true }, "appUser": { @@ -13705,7 +13879,7 @@ }, "missingSeriesFromSource": { "type": "string", - "description": "A \r\n separated string of all missing series", + "description": "A \n separated string of all missing series", "nullable": true } }, @@ -16146,6 +16320,36 @@ "additionalProperties": false, "description": "Represents if Test Email Service URL was successful or not and if any error occured" }, + "EpubFontDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "provider": { + "enum": [ + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "created": { + "type": "string", + "format": "date-time" + }, + "lastModified": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, "ExternalRating": { "type": "object", "properties": { @@ -22350,4 +22554,4 @@ "description": "Responsible for all things Want To Read" } ] -} +} \ No newline at end of file From 9fae799c63e548e30471548f337a2b077ee33372 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 13 Jul 2024 08:24:35 -0500 Subject: [PATCH 3/9] Setup gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 612917a47..dbe37af31 100644 --- a/.gitignore +++ b/.gitignore @@ -508,6 +508,7 @@ UI/Web/dist/ /API/config/logs/ /API/config/backups/ /API/config/cache/ +/API/config/fonts/ /API/config/temp/ /API/config/themes/ /API/config/stats/ From 58800c0b4efbc4e8021f4efdc6e092c199222f71 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 13 Jul 2024 11:52:23 -0500 Subject: [PATCH 4/9] Refactored how system fonts were loaded (at least covered for local development) so that instead of going through the api, they are instead resolved by using assets/fonts/{fontName}/{fontFile}. --- API/Controllers/FontController.cs | 63 ++++++++---- API/Controllers/ThemeController.cs | 2 +- API/DTOs/Font/EpubFontDto.cs | 4 +- API/Data/Repositories/EpubFontRepository.cs | 5 +- API/Data/Seed.cs | 96 ++++++++++++++++-- API/Services/Tasks/FontService.cs | 6 +- .../src/app/_models/preferences/epub-font.ts | 3 +- UI/Web/src/app/_services/font.service.ts | 9 +- .../book-reader/book-reader.component.ts | 1 + .../reader-settings.component.html | 4 +- .../reader-settings.component.ts | 8 +- .../user-preferences.component.ts | 3 +- .../EBGaramond-Italic-VariableFont_wght.ttf | Bin .../EBGaramond-Italic-VariableFont_wght.woff2 | Bin .../EBGaramond-VariableFont_wght.ttf | Bin .../EBGaramond-VariableFont_wght.woff2 | Bin .../fonts/{EBGarmond => EB Garmond}/OFL.txt | 0 .../FiraSans-Black.ttf | Bin .../FiraSans-Black.woff2 | Bin .../FiraSans-BlackItalic.ttf | Bin .../FiraSans-BlackItalic.woff2 | Bin .../FiraSans-Bold.ttf | Bin .../FiraSans-Bold.woff2 | Bin .../FiraSans-BoldItalic.ttf | Bin .../FiraSans-BoldItalic.woff2 | Bin .../FiraSans-ExtraBold.ttf | Bin .../FiraSans-ExtraBold.woff2 | Bin .../FiraSans-ExtraBoldItalic.ttf | Bin .../FiraSans-ExtraBoldItalic.woff2 | Bin .../FiraSans-ExtraLight.ttf | Bin .../FiraSans-ExtraLight.woff2 | Bin .../FiraSans-ExtraLightItalic.ttf | Bin .../FiraSans-ExtraLightItalic.woff2 | Bin .../FiraSans-Italic.ttf | Bin .../FiraSans-Italic.woff2 | Bin .../FiraSans-Light.ttf | Bin .../FiraSans-Light.woff2 | Bin .../FiraSans-LightItalic.ttf | Bin .../FiraSans-LightItalic.woff2 | Bin .../FiraSans-Medium.ttf | Bin .../FiraSans-Medium.woff2 | Bin .../FiraSans-MediumItalic.ttf | Bin .../FiraSans-MediumItalic.woff2 | Bin .../FiraSans-Regular.ttf | Bin .../FiraSans-Regular.woff2 | Bin .../FiraSans-SemiBold.ttf | Bin .../FiraSans-SemiBold.woff2 | Bin .../FiraSans-SemiBoldItalic.ttf | Bin .../FiraSans-SemiBoldItalic.woff2 | Bin .../FiraSans-Thin.ttf | Bin .../FiraSans-Thin.woff2 | Bin .../FiraSans-ThinItalic.ttf | Bin .../FiraSans-ThinItalic.woff2 | Bin .../fonts/{Fira_Sans => Fira Sans}/OFL.txt | 0 .../LibreBaskerville-Bold.ttf | Bin .../LibreBaskerville-Bold.woff2 | Bin .../LibreBaskerville-Italic.ttf | Bin .../LibreBaskerville-Italic.woff2 | Bin .../LibreBaskerville-Regular.ttf | Bin .../LibreBaskerville-Regular.woff2 | Bin .../OFL.txt | 0 .../LibreCaslonText-Bold.ttf | Bin .../LibreCaslonText-Bold.woff2 | Bin .../LibreCaslonText-Italic.ttf | Bin .../LibreCaslonText-Italic.woff2 | Bin .../LibreCaslonText-Regular.ttf | Bin .../LibreCaslonText-Regular.woff2 | Bin .../{Libre_Caslon => Libre Caslon}/OFL.txt | 0 .../NanumGothic-Bold.ttf | Bin .../NanumGothic-Bold.woff2 | Bin .../NanumGothic-ExtraBold.ttf | Bin .../NanumGothic-ExtraBold.woff2 | Bin .../NanumGothic-Regular.ttf | Bin .../NanumGothic-Regular.woff2 | Bin .../{Nanum_Gothic => Nanum Gothic}/OFL.txt | 0 .../OpenDyslexic-Bold.otf | Bin .../OpenDyslexic-Bold.woff2 | Bin .../OpenDyslexic-BoldItalic.otf | Bin .../OpenDyslexic-BoldItalic.woff2 | Bin .../OpenDyslexic-Italic.otf | Bin .../OpenDyslexic-Italic.woff2 | Bin .../OpenDyslexic-Regular.otf | Bin .../OpenDyslexic-Regular.woff2 | Bin .../{RocknRoll_One => RocknRoll One}/OFL.txt | 0 .../RocknRollOne-Regular.ttf | Bin .../RocknRollOne-Regular.woff2 | Bin UI/Web/src/styles.scss | 2 +- openapi.json | 68 ++++--------- 88 files changed, 177 insertions(+), 97 deletions(-) rename UI/Web/src/assets/fonts/{EBGarmond => EB Garmond}/EBGaramond-Italic-VariableFont_wght.ttf (100%) rename UI/Web/src/assets/fonts/{EBGarmond => EB Garmond}/EBGaramond-Italic-VariableFont_wght.woff2 (100%) rename UI/Web/src/assets/fonts/{EBGarmond => EB Garmond}/EBGaramond-VariableFont_wght.ttf (100%) rename UI/Web/src/assets/fonts/{EBGarmond => EB Garmond}/EBGaramond-VariableFont_wght.woff2 (100%) rename UI/Web/src/assets/fonts/{EBGarmond => EB Garmond}/OFL.txt (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Black.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Black.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-BlackItalic.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-BlackItalic.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Bold.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Bold.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-BoldItalic.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-BoldItalic.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-ExtraBold.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-ExtraBold.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-ExtraBoldItalic.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-ExtraBoldItalic.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-ExtraLight.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-ExtraLight.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-ExtraLightItalic.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-ExtraLightItalic.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Italic.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Italic.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Light.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Light.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-LightItalic.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-LightItalic.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Medium.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Medium.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-MediumItalic.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-MediumItalic.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Regular.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Regular.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-SemiBold.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-SemiBold.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-SemiBoldItalic.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-SemiBoldItalic.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Thin.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-Thin.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-ThinItalic.ttf (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/FiraSans-ThinItalic.woff2 (100%) rename UI/Web/src/assets/fonts/{Fira_Sans => Fira Sans}/OFL.txt (100%) rename UI/Web/src/assets/fonts/{Libre_Baskerville => Libre Baskerville}/LibreBaskerville-Bold.ttf (100%) rename UI/Web/src/assets/fonts/{Libre_Baskerville => Libre Baskerville}/LibreBaskerville-Bold.woff2 (100%) rename UI/Web/src/assets/fonts/{Libre_Baskerville => Libre Baskerville}/LibreBaskerville-Italic.ttf (100%) rename UI/Web/src/assets/fonts/{Libre_Baskerville => Libre Baskerville}/LibreBaskerville-Italic.woff2 (100%) rename UI/Web/src/assets/fonts/{Libre_Baskerville => Libre Baskerville}/LibreBaskerville-Regular.ttf (100%) rename UI/Web/src/assets/fonts/{Libre_Baskerville => Libre Baskerville}/LibreBaskerville-Regular.woff2 (100%) rename UI/Web/src/assets/fonts/{Libre_Baskerville => Libre Baskerville}/OFL.txt (100%) rename UI/Web/src/assets/fonts/{Libre_Caslon => Libre Caslon}/LibreCaslonText-Bold.ttf (100%) rename UI/Web/src/assets/fonts/{Libre_Caslon => Libre Caslon}/LibreCaslonText-Bold.woff2 (100%) rename UI/Web/src/assets/fonts/{Libre_Caslon => Libre Caslon}/LibreCaslonText-Italic.ttf (100%) rename UI/Web/src/assets/fonts/{Libre_Caslon => Libre Caslon}/LibreCaslonText-Italic.woff2 (100%) rename UI/Web/src/assets/fonts/{Libre_Caslon => Libre Caslon}/LibreCaslonText-Regular.ttf (100%) rename UI/Web/src/assets/fonts/{Libre_Caslon => Libre Caslon}/LibreCaslonText-Regular.woff2 (100%) rename UI/Web/src/assets/fonts/{Libre_Caslon => Libre Caslon}/OFL.txt (100%) rename UI/Web/src/assets/fonts/{Nanum_Gothic => Nanum Gothic}/NanumGothic-Bold.ttf (100%) rename UI/Web/src/assets/fonts/{Nanum_Gothic => Nanum Gothic}/NanumGothic-Bold.woff2 (100%) rename UI/Web/src/assets/fonts/{Nanum_Gothic => Nanum Gothic}/NanumGothic-ExtraBold.ttf (100%) rename UI/Web/src/assets/fonts/{Nanum_Gothic => Nanum Gothic}/NanumGothic-ExtraBold.woff2 (100%) rename UI/Web/src/assets/fonts/{Nanum_Gothic => Nanum Gothic}/NanumGothic-Regular.ttf (100%) rename UI/Web/src/assets/fonts/{Nanum_Gothic => Nanum Gothic}/NanumGothic-Regular.woff2 (100%) rename UI/Web/src/assets/fonts/{Nanum_Gothic => Nanum Gothic}/OFL.txt (100%) rename UI/Web/src/assets/fonts/{OpenDyslexic2 => Open Dyslexic 2}/OpenDyslexic-Bold.otf (100%) rename UI/Web/src/assets/fonts/{OpenDyslexic2 => Open Dyslexic 2}/OpenDyslexic-Bold.woff2 (100%) rename UI/Web/src/assets/fonts/{OpenDyslexic2 => Open Dyslexic 2}/OpenDyslexic-BoldItalic.otf (100%) rename UI/Web/src/assets/fonts/{OpenDyslexic2 => Open Dyslexic 2}/OpenDyslexic-BoldItalic.woff2 (100%) rename UI/Web/src/assets/fonts/{OpenDyslexic2 => Open Dyslexic 2}/OpenDyslexic-Italic.otf (100%) rename UI/Web/src/assets/fonts/{OpenDyslexic2 => Open Dyslexic 2}/OpenDyslexic-Italic.woff2 (100%) rename UI/Web/src/assets/fonts/{OpenDyslexic2 => Open Dyslexic 2}/OpenDyslexic-Regular.otf (100%) rename UI/Web/src/assets/fonts/{OpenDyslexic2 => Open Dyslexic 2}/OpenDyslexic-Regular.woff2 (100%) rename UI/Web/src/assets/fonts/{RocknRoll_One => RocknRoll One}/OFL.txt (100%) rename UI/Web/src/assets/fonts/{RocknRoll_One => RocknRoll One}/RocknRollOne-Regular.ttf (100%) rename UI/Web/src/assets/fonts/{RocknRoll_One => RocknRoll One}/RocknRollOne-Regular.woff2 (100%) diff --git a/API/Controllers/FontController.cs b/API/Controllers/FontController.cs index 7f36bf521..6db01aebe 100644 --- a/API/Controllers/FontController.cs +++ b/API/Controllers/FontController.cs @@ -6,18 +6,15 @@ using System.Threading.Tasks; using API.Constants; using API.Data; using API.DTOs.Font; -using API.Extensions; +using API.Entities.Enums.Font; using API.Services; using API.Services.Tasks; using API.Services.Tasks.Scanner.Parser; using AutoMapper; -using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using MimeTypes; -using Serilog; namespace API.Controllers; @@ -25,72 +22,98 @@ public class FontController : BaseApiController { private readonly IUnitOfWork _unitOfWork; private readonly IDirectoryService _directoryService; - private readonly ITaskScheduler _taskScheduler; private readonly IFontService _fontService; private readonly IMapper _mapper; private readonly Regex _fontFileExtensionRegex = new(Parser.FontFileExtensions, RegexOptions.IgnoreCase, Parser.RegexTimeout); - public FontController(IUnitOfWork unitOfWork, ITaskScheduler taskScheduler, IDirectoryService directoryService, + public FontController(IUnitOfWork unitOfWork, IDirectoryService directoryService, IFontService fontService, IMapper mapper) { _unitOfWork = unitOfWork; _directoryService = directoryService; - _taskScheduler = taskScheduler; _fontService = fontService; _mapper = mapper; } - [ResponseCache(CacheProfileName = "10Minute")] + /// + /// List out the fonts + /// + /// + [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute)] [HttpGet("all")] public async Task>> GetFonts() { return Ok(await _unitOfWork.EpubFontRepository.GetFontDtosAsync()); } + /// + /// Returns a font + /// + /// + /// + /// [HttpGet] [AllowAnonymous] public async Task GetFont(int fontId, string apiKey) { var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); - if (userId == 0) return BadRequest(); var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); - if (font == null) return NotFound(); + // var fontDirectory = _directoryService.EpubFontDirectory; + // if (font.Provider == FontProvider.System) + // { + // fontDirectory = _directoryService. + // } + var contentType = MimeTypeMap.GetMimeType(Path.GetExtension(font.FileName)); var path = Path.Join(_directoryService.EpubFontDirectory, font.FileName); + return PhysicalFile(path, contentType); } + /// + /// Removes a font from the system + /// + /// + /// If the font is in use by other users and an admin wants it deleted, they must confirm to force delete it + /// [HttpDelete] - public async Task DeleteFont(int fontId) + public async Task DeleteFont(int fontId, bool confirmed = false) { + // TODO: We need to check if this font is used by anyone else and if so, need to inform the user + // Need to check if this is a system font as well + var forceDelete = User.IsInRole(PolicyConstants.AdminRole) && confirmed; await _fontService.Delete(fontId); return Ok(); } + /// + /// Manual upload + /// + /// + /// [HttpPost("upload")] public async Task> UploadFont(IFormFile formFile) { - if (!_fontFileExtensionRegex.IsMatch(Path.GetExtension(formFile.FileName))) - return BadRequest("Invalid file"); + if (!_fontFileExtensionRegex.IsMatch(Path.GetExtension(formFile.FileName))) return BadRequest("Invalid file"); + + if (formFile.FileName.Contains("..")) return BadRequest("Invalid file"); - if (formFile.FileName.Contains("..")) - return BadRequest("Invalid file"); var tempFile = await UploadToTemp(formFile); var font = await _fontService.CreateFontFromFileAsync(tempFile); return Ok(_mapper.Map(font)); } - [HttpPost("upload-url")] - public async Task> UploadFontByUrl(string url) - { - throw new NotImplementedException(); - } + // [HttpPost("upload-url")] + // public async Task> UploadFontByUrl(string url) + // { + // throw new NotImplementedException(); + // } private async Task UploadToTemp(IFormFile file) { diff --git a/API/Controllers/ThemeController.cs b/API/Controllers/ThemeController.cs index fb9371919..cdffa43ca 100644 --- a/API/Controllers/ThemeController.cs +++ b/API/Controllers/ThemeController.cs @@ -40,7 +40,7 @@ public class ThemeController : BaseApiController _mapper = mapper; } - [ResponseCache(CacheProfileName = "10Minute")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute)] [AllowAnonymous] [HttpGet] public async Task>> GetThemes() diff --git a/API/DTOs/Font/EpubFontDto.cs b/API/DTOs/Font/EpubFontDto.cs index 9397e8357..8eb2712a4 100644 --- a/API/DTOs/Font/EpubFontDto.cs +++ b/API/DTOs/Font/EpubFontDto.cs @@ -8,6 +8,6 @@ public class EpubFontDto public int Id { get; set; } public string Name { get; set; } public FontProvider Provider { get; set; } - public DateTime Created { get; set; } - public DateTime LastModified { get; set; } + public string FileName { get; set; } + } diff --git a/API/Data/Repositories/EpubFontRepository.cs b/API/Data/Repositories/EpubFontRepository.cs index 7be343b55..8a396d6dd 100644 --- a/API/Data/Repositories/EpubFontRepository.cs +++ b/API/Data/Repositories/EpubFontRepository.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using API.DTOs.Font; using API.Entities; +using API.Extensions; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; @@ -52,6 +53,8 @@ public class EpubFontRepository: IEpubFontRepository public async Task> GetFontDtosAsync() { return await _context.EpubFont + .OrderBy(s => s.Name == "Default" ? -1 : 0) + .ThenBy(s => s) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } @@ -67,7 +70,7 @@ public class EpubFontRepository: IEpubFontRepository public async Task GetFontDtoByNameAsync(string name) { return await _context.EpubFont - .Where(f => f.Name.Equals(name)) + .Where(f => f.NormalizedName.Equals(name.ToNormalized())) .ProjectTo(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(); } diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index ea96ea37a..8ddd960e6 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -32,13 +32,90 @@ public static class Seed [ ..new List { + new () + { + Name = "Default", + NormalizedName = Parser.Normalize("Default"), + Provider = FontProvider.System, + FileName = string.Empty, + }, new () { Name = "Merriweather", NormalizedName = Parser.Normalize("Merriweather"), Provider = FontProvider.System, FileName = "Merriweather-Regular.woff2", - } + }, + new () + { + Name = "EB Garamond", + NormalizedName = Parser.Normalize("EB Garamond"), + Provider = FontProvider.System, + FileName = "EBGaramond-VariableFont_wght.woff2", + }, + new () + { + Name = "Fira Sans", + NormalizedName = Parser.Normalize("Fira Sans"), + Provider = FontProvider.System, + FileName = "FiraSans-Regular.woff2", + }, + new () + { + Name = "Lato", + NormalizedName = Parser.Normalize("Lato"), + Provider = FontProvider.System, + FileName = "Lato-Regular.woff2", + }, + new () + { + Name = "Libre Baskerville", + NormalizedName = Parser.Normalize("Libre Baskerville"), + Provider = FontProvider.System, + FileName = "LibreBaskerville-Regular.woff2", + }, + new () + { + Name = "Libre Caslon", + NormalizedName = Parser.Normalize("Libre Caslon"), + Provider = FontProvider.System, + FileName = "LibreCaslonText-Regular.woff2", + }, + new () + { + Name = "Nanum Gothic", + NormalizedName = Parser.Normalize("Nanum Gothic"), + Provider = FontProvider.System, + FileName = "NanumGothic-Regular.woff2", + }, + new () + { + Name = "Open Dyslexic 2", + NormalizedName = Parser.Normalize("Open Dyslexic 2"), + Provider = FontProvider.System, + FileName = "OpenDyslexic-Regular.woff2", + }, + new () + { + Name = "Oswald", + NormalizedName = Parser.Normalize("Oswald"), + Provider = FontProvider.System, + FileName = "Oswald-VariableFont_wght.woff2", + }, + new () + { + Name = "RocknRoll One", + NormalizedName = Parser.Normalize("RocknRoll One"), + Provider = FontProvider.System, + FileName = "RocknRollOne-Regular.woff2", + }, + new () + { + Name = "Spartan", + NormalizedName = Parser.Normalize("Spartan"), + Provider = FontProvider.System, + FileName = "Spartan-VariableFont_wght.woff2", + }, } ]; @@ -159,7 +236,7 @@ public static class Seed foreach (var theme in DefaultThemes) { - var existing = context.SiteTheme.FirstOrDefault(s => s.Name.Equals(theme.Name)); + var existing = await context.SiteTheme.FirstOrDefaultAsync(s => s.Name.Equals(theme.Name)); if (existing == null) { await context.SiteTheme.AddAsync(theme); @@ -172,9 +249,10 @@ public static class Seed public static async Task SeedFonts(DataContext context) { await context.Database.EnsureCreatedAsync(); + foreach (var font in DefaultFonts) { - var existing = context.SiteTheme.FirstOrDefaultAsync(f => f.Name.Equals(font.Name)); + var existing = await context.EpubFont.FirstOrDefaultAsync(f => f.Name.Equals(font.Name)); if (existing == null) { await context.EpubFont.AddAsync(font); @@ -290,7 +368,7 @@ public static class Seed foreach (var defaultSetting in DefaultSettings) { - var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key); + var existing = await context.ServerSetting.FirstOrDefaultAsync(s => s.Key == defaultSetting.Key); if (existing == null) { await context.ServerSetting.AddAsync(defaultSetting); @@ -300,15 +378,15 @@ public static class Seed await context.SaveChangesAsync(); // Port, IpAddresses and LoggingLevel are managed in appSettings.json. Update the DB values to match - context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value = + (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.Port)).Value = Configuration.Port + string.Empty; - context.ServerSetting.First(s => s.Key == ServerSettingKey.IpAddresses).Value = + (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.IpAddresses)).Value = Configuration.IpAddresses; - context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheDirectory).Value = + (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CacheDirectory)).Value = directoryService.CacheDirectory + string.Empty; - context.ServerSetting.First(s => s.Key == ServerSettingKey.BackupDirectory).Value = + (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.BackupDirectory)).Value = DirectoryService.BackupDirectory + string.Empty; - context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheSize).Value = + (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CacheSize)).Value = Configuration.CacheSize + string.Empty; await context.SaveChangesAsync(); diff --git a/API/Services/Tasks/FontService.cs b/API/Services/Tasks/FontService.cs index a85148857..7869088c8 100644 --- a/API/Services/Tasks/FontService.cs +++ b/API/Services/Tasks/FontService.cs @@ -79,16 +79,14 @@ public class FontService: IFontService } var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); - if (font == null) - return; + if (font == null) return; await RemoveFont(font); } public async Task RemoveFont(EpubFont font) { - if (font.Provider == FontProvider.System) - return; + if (font.Provider == FontProvider.System) return; var prefs = await _unitOfWork.UserRepository.GetAllPreferencesByFontAsync(font.Name); foreach (var pref in prefs) diff --git a/UI/Web/src/app/_models/preferences/epub-font.ts b/UI/Web/src/app/_models/preferences/epub-font.ts index 29ffa2cb6..8ac1747f7 100644 --- a/UI/Web/src/app/_models/preferences/epub-font.ts +++ b/UI/Web/src/app/_models/preferences/epub-font.ts @@ -13,6 +13,5 @@ export interface EpubFont { id: number; name: string; provider: FontProvider; - created: Date; - lastModified: Date; + fileName: string; } diff --git a/UI/Web/src/app/_services/font.service.ts b/UI/Web/src/app/_services/font.service.ts index 4ff4e5f1c..ecb9b95a9 100644 --- a/UI/Web/src/app/_services/font.service.ts +++ b/UI/Web/src/app/_services/font.service.ts @@ -1,13 +1,12 @@ import {DestroyRef, inject, Injectable} from "@angular/core"; import {map, ReplaySubject} from "rxjs"; -import {EpubFont} from "../_models/preferences/epub-font"; +import {EpubFont, FontProvider} from "../_models/preferences/epub-font"; import {environment} from 'src/environments/environment'; import {HttpClient} from "@angular/common/http"; -import {EVENTS, MessageHubService} from "./message-hub.service"; +import {MessageHubService} from "./message-hub.service"; import {NgxFileDropEntry} from "ngx-file-drop"; import {AccountService} from "./account.service"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {NotificationProgressEvent} from "../_models/events/notification-progress-event"; @Injectable({ providedIn: 'root' @@ -41,6 +40,10 @@ export class FontService { } getFontFace(font: EpubFont): FontFace { + if (font.provider === FontProvider.System) { + return new FontFace(font.name, `url('/assets/fonts/${font.name}/${font.fileName}')`); + } + return new FontFace(font.name, `url(${this.baseUrl}font?fontId=${font.id}&apiKey=${this.encodedKey})`); } 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 08cecc80b..a6680a1db 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 @@ -588,6 +588,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.fontService.getFonts().subscribe(fonts => { fonts.forEach(font => { this.fontService.getFontFace(font).load().then(loadedFace => { + console.log('loaded font: ', loadedFace); (this.document as any).fonts.add(loadedFace); }); }) 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 5e2602807..8040678c3 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 @@ -16,7 +16,9 @@
    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 20b1bb99e..e9fb03a49 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 @@ -85,6 +85,7 @@ export const bookColorThemes = [ ]; const mobileBreakpointMarginOverride = 700; +const defaultFontFamily = 'Default'; @Component({ selector: 'app-reader-settings', @@ -132,7 +133,6 @@ export class ReaderSettingsComponent implements OnInit { /** * List of all font families user can select from */ - fontOptions: Array = []; fontFamilies: Array = []; /** * Internal property used to capture all the different css properties to render on all elements @@ -178,7 +178,6 @@ export class ReaderSettingsComponent implements OnInit { ngOnInit(): void { this.fontService.getFonts().subscribe(fonts => { this.fontFamilies = fonts; - this.fontOptions = fonts.map(f => f.name); this.cdRef.markForCheck(); }) @@ -187,7 +186,7 @@ export class ReaderSettingsComponent implements OnInit { this.user = user; if (this.user.preferences.bookReaderFontFamily === undefined) { - this.user.preferences.bookReaderFontFamily = 'default'; + this.user.preferences.bookReaderFontFamily = defaultFontFamily; } if (this.user.preferences.bookReaderFontSize === undefined || this.user.preferences.bookReaderFontSize < 50) { this.user.preferences.bookReaderFontSize = 100; @@ -211,7 +210,8 @@ export class ReaderSettingsComponent implements OnInit { this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, [])); this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(fontName => { - if (fontName === 'default') { + console.log('updating font-family to ', fontName); + if (fontName === defaultFontFamily) { this.pageStyles['font-family'] = 'inherit'; } else { this.pageStyles['font-family'] = "'" + fontName + "'"; diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index 9da748e86..90c4bb4f8 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -74,7 +74,6 @@ import {ManageScrobblingProvidersComponent} from "../manage-scrobbling-providers import {PdfLayoutModePipe} from "../../pdf-reader/_pipe/pdf-layout-mode.pipe"; import {PdfTheme} from "../../_models/preferences/pdf-theme"; import {PdfScrollMode} from "../../_models/preferences/pdf-scroll-mode"; -import {PdfLayoutMode} from "../../_models/preferences/pdf-layout-mode"; import {PdfSpreadMode} from "../../_models/preferences/pdf-spread-mode"; import {FontManagerComponent} from "../font-manager/font-manager/font-manager.component"; import {FontService} from "../../_services/font.service"; @@ -234,7 +233,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.user.preferences = results.pref; if (this.fontFamilies.indexOf(this.user.preferences.bookReaderFontFamily) < 0) { - this.user.preferences.bookReaderFontFamily = 'default'; + this.user.preferences.bookReaderFontFamily = 'Default'; } this.settingsForm.addControl('readingDirection', new FormControl(this.user.preferences.readingDirection, [])); diff --git a/UI/Web/src/assets/fonts/EBGarmond/EBGaramond-Italic-VariableFont_wght.ttf b/UI/Web/src/assets/fonts/EB Garmond/EBGaramond-Italic-VariableFont_wght.ttf similarity index 100% rename from UI/Web/src/assets/fonts/EBGarmond/EBGaramond-Italic-VariableFont_wght.ttf rename to UI/Web/src/assets/fonts/EB Garmond/EBGaramond-Italic-VariableFont_wght.ttf diff --git a/UI/Web/src/assets/fonts/EBGarmond/EBGaramond-Italic-VariableFont_wght.woff2 b/UI/Web/src/assets/fonts/EB Garmond/EBGaramond-Italic-VariableFont_wght.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/EBGarmond/EBGaramond-Italic-VariableFont_wght.woff2 rename to UI/Web/src/assets/fonts/EB Garmond/EBGaramond-Italic-VariableFont_wght.woff2 diff --git a/UI/Web/src/assets/fonts/EBGarmond/EBGaramond-VariableFont_wght.ttf b/UI/Web/src/assets/fonts/EB Garmond/EBGaramond-VariableFont_wght.ttf similarity index 100% rename from UI/Web/src/assets/fonts/EBGarmond/EBGaramond-VariableFont_wght.ttf rename to UI/Web/src/assets/fonts/EB Garmond/EBGaramond-VariableFont_wght.ttf diff --git a/UI/Web/src/assets/fonts/EBGarmond/EBGaramond-VariableFont_wght.woff2 b/UI/Web/src/assets/fonts/EB Garmond/EBGaramond-VariableFont_wght.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/EBGarmond/EBGaramond-VariableFont_wght.woff2 rename to UI/Web/src/assets/fonts/EB Garmond/EBGaramond-VariableFont_wght.woff2 diff --git a/UI/Web/src/assets/fonts/EBGarmond/OFL.txt b/UI/Web/src/assets/fonts/EB Garmond/OFL.txt similarity index 100% rename from UI/Web/src/assets/fonts/EBGarmond/OFL.txt rename to UI/Web/src/assets/fonts/EB Garmond/OFL.txt diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Black.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Black.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Black.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Black.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Black.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Black.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Black.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Black.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-BlackItalic.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-BlackItalic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-BlackItalic.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-BlackItalic.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-BlackItalic.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-BlackItalic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-BlackItalic.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-BlackItalic.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Bold.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Bold.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Bold.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Bold.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Bold.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Bold.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Bold.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Bold.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-BoldItalic.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-BoldItalic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-BoldItalic.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-BoldItalic.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-BoldItalic.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-BoldItalic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-BoldItalic.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-BoldItalic.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraBold.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraBold.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraBold.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraBold.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraBold.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraBold.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraBold.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraBold.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraBoldItalic.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraBoldItalic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraBoldItalic.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraBoldItalic.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraBoldItalic.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraBoldItalic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraBoldItalic.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraBoldItalic.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraLight.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraLight.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraLight.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraLight.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraLight.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraLight.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraLight.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraLight.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraLightItalic.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraLightItalic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraLightItalic.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraLightItalic.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraLightItalic.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraLightItalic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ExtraLightItalic.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-ExtraLightItalic.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Italic.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Italic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Italic.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Italic.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Italic.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Italic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Italic.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Italic.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Light.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Light.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Light.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Light.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Light.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Light.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Light.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Light.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-LightItalic.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-LightItalic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-LightItalic.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-LightItalic.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-LightItalic.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-LightItalic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-LightItalic.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-LightItalic.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Medium.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Medium.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Medium.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Medium.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Medium.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Medium.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Medium.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Medium.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-MediumItalic.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-MediumItalic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-MediumItalic.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-MediumItalic.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-MediumItalic.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-MediumItalic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-MediumItalic.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-MediumItalic.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Regular.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Regular.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Regular.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Regular.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Regular.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Regular.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Regular.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Regular.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-SemiBold.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-SemiBold.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-SemiBold.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-SemiBold.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-SemiBold.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-SemiBold.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-SemiBold.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-SemiBold.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-SemiBoldItalic.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-SemiBoldItalic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-SemiBoldItalic.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-SemiBoldItalic.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-SemiBoldItalic.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-SemiBoldItalic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-SemiBoldItalic.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-SemiBoldItalic.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Thin.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Thin.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Thin.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Thin.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Thin.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-Thin.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-Thin.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-Thin.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ThinItalic.ttf b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-ThinItalic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ThinItalic.ttf rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-ThinItalic.ttf diff --git a/UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ThinItalic.woff2 b/UI/Web/src/assets/fonts/Fira Sans/FiraSans-ThinItalic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/FiraSans-ThinItalic.woff2 rename to UI/Web/src/assets/fonts/Fira Sans/FiraSans-ThinItalic.woff2 diff --git a/UI/Web/src/assets/fonts/Fira_Sans/OFL.txt b/UI/Web/src/assets/fonts/Fira Sans/OFL.txt similarity index 100% rename from UI/Web/src/assets/fonts/Fira_Sans/OFL.txt rename to UI/Web/src/assets/fonts/Fira Sans/OFL.txt diff --git a/UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Bold.ttf b/UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Bold.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Bold.ttf rename to UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Bold.ttf diff --git a/UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Bold.woff2 b/UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Bold.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Bold.woff2 rename to UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Bold.woff2 diff --git a/UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Italic.ttf b/UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Italic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Italic.ttf rename to UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Italic.ttf diff --git a/UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Italic.woff2 b/UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Italic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Italic.woff2 rename to UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Italic.woff2 diff --git a/UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Regular.ttf b/UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Regular.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Regular.ttf rename to UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Regular.ttf diff --git a/UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Regular.woff2 b/UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Regular.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Baskerville/LibreBaskerville-Regular.woff2 rename to UI/Web/src/assets/fonts/Libre Baskerville/LibreBaskerville-Regular.woff2 diff --git a/UI/Web/src/assets/fonts/Libre_Baskerville/OFL.txt b/UI/Web/src/assets/fonts/Libre Baskerville/OFL.txt similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Baskerville/OFL.txt rename to UI/Web/src/assets/fonts/Libre Baskerville/OFL.txt diff --git a/UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Bold.ttf b/UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Bold.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Bold.ttf rename to UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Bold.ttf diff --git a/UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Bold.woff2 b/UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Bold.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Bold.woff2 rename to UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Bold.woff2 diff --git a/UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Italic.ttf b/UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Italic.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Italic.ttf rename to UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Italic.ttf diff --git a/UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Italic.woff2 b/UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Italic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Italic.woff2 rename to UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Italic.woff2 diff --git a/UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Regular.ttf b/UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Regular.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Regular.ttf rename to UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Regular.ttf diff --git a/UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Regular.woff2 b/UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Regular.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Caslon/LibreCaslonText-Regular.woff2 rename to UI/Web/src/assets/fonts/Libre Caslon/LibreCaslonText-Regular.woff2 diff --git a/UI/Web/src/assets/fonts/Libre_Caslon/OFL.txt b/UI/Web/src/assets/fonts/Libre Caslon/OFL.txt similarity index 100% rename from UI/Web/src/assets/fonts/Libre_Caslon/OFL.txt rename to UI/Web/src/assets/fonts/Libre Caslon/OFL.txt diff --git a/UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-Bold.ttf b/UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-Bold.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-Bold.ttf rename to UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-Bold.ttf diff --git a/UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-Bold.woff2 b/UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-Bold.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-Bold.woff2 rename to UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-Bold.woff2 diff --git a/UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-ExtraBold.ttf b/UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-ExtraBold.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-ExtraBold.ttf rename to UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-ExtraBold.ttf diff --git a/UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-ExtraBold.woff2 b/UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-ExtraBold.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-ExtraBold.woff2 rename to UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-ExtraBold.woff2 diff --git a/UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-Regular.ttf b/UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-Regular.ttf similarity index 100% rename from UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-Regular.ttf rename to UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-Regular.ttf diff --git a/UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-Regular.woff2 b/UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-Regular.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/Nanum_Gothic/NanumGothic-Regular.woff2 rename to UI/Web/src/assets/fonts/Nanum Gothic/NanumGothic-Regular.woff2 diff --git a/UI/Web/src/assets/fonts/Nanum_Gothic/OFL.txt b/UI/Web/src/assets/fonts/Nanum Gothic/OFL.txt similarity index 100% rename from UI/Web/src/assets/fonts/Nanum_Gothic/OFL.txt rename to UI/Web/src/assets/fonts/Nanum Gothic/OFL.txt diff --git a/UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Bold.otf b/UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Bold.otf similarity index 100% rename from UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Bold.otf rename to UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Bold.otf diff --git a/UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Bold.woff2 b/UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Bold.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Bold.woff2 rename to UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Bold.woff2 diff --git a/UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-BoldItalic.otf b/UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-BoldItalic.otf similarity index 100% rename from UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-BoldItalic.otf rename to UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-BoldItalic.otf diff --git a/UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-BoldItalic.woff2 b/UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-BoldItalic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-BoldItalic.woff2 rename to UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-BoldItalic.woff2 diff --git a/UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Italic.otf b/UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Italic.otf similarity index 100% rename from UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Italic.otf rename to UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Italic.otf diff --git a/UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Italic.woff2 b/UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Italic.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Italic.woff2 rename to UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Italic.woff2 diff --git a/UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.otf b/UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Regular.otf similarity index 100% rename from UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.otf rename to UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Regular.otf diff --git a/UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.woff2 b/UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Regular.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.woff2 rename to UI/Web/src/assets/fonts/Open Dyslexic 2/OpenDyslexic-Regular.woff2 diff --git a/UI/Web/src/assets/fonts/RocknRoll_One/OFL.txt b/UI/Web/src/assets/fonts/RocknRoll One/OFL.txt similarity index 100% rename from UI/Web/src/assets/fonts/RocknRoll_One/OFL.txt rename to UI/Web/src/assets/fonts/RocknRoll One/OFL.txt diff --git a/UI/Web/src/assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf b/UI/Web/src/assets/fonts/RocknRoll One/RocknRollOne-Regular.ttf similarity index 100% rename from UI/Web/src/assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf rename to UI/Web/src/assets/fonts/RocknRoll One/RocknRollOne-Regular.ttf diff --git a/UI/Web/src/assets/fonts/RocknRoll_One/RocknRollOne-Regular.woff2 b/UI/Web/src/assets/fonts/RocknRoll One/RocknRollOne-Regular.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/RocknRoll_One/RocknRollOne-Regular.woff2 rename to UI/Web/src/assets/fonts/RocknRoll One/RocknRollOne-Regular.woff2 diff --git a/UI/Web/src/styles.scss b/UI/Web/src/styles.scss index 5e7de8cab..296298e85 100644 --- a/UI/Web/src/styles.scss +++ b/UI/Web/src/styles.scss @@ -53,7 +53,7 @@ // Global Styles @font-face { font-family: "EBGarmond"; - src: url("assets/fonts/EBGarmond/EBGaramond-VariableFont_wght.woff2") format("woff2"); + src: url("assets/fonts/EB Garmond/EBGaramond-VariableFont_wght.woff2") format("woff2"); font-display: swap; } diff --git a/openapi.json b/openapi.json index fcf5fbd5b..e72afcbc2 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.1", "info": { "title": "Kavita", - "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.2.0", + "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.2.1", "license": { "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" @@ -2319,6 +2319,7 @@ "tags": [ "Font" ], + "summary": "List out the fonts", "responses": { "200": { "description": "OK", @@ -2357,10 +2358,12 @@ "tags": [ "Font" ], + "summary": "Returns a font", "parameters": [ { "name": "fontId", "in": "query", + "description": "", "schema": { "type": "integer", "format": "int32" @@ -2369,6 +2372,7 @@ { "name": "apiKey", "in": "query", + "description": "", "schema": { "type": "string" } @@ -2384,14 +2388,25 @@ "tags": [ "Font" ], + "summary": "Removes a font from the system", "parameters": [ { "name": "fontId", "in": "query", + "description": "", "schema": { "type": "integer", "format": "int32" } + }, + { + "name": "confirmed", + "in": "query", + "description": "If the font is in use by other users and an admin wants it deleted, they must confirm to force delete it", + "schema": { + "type": "boolean", + "default": false + } } ], "responses": { @@ -2406,6 +2421,7 @@ "tags": [ "Font" ], + "summary": "Manual upload", "requestBody": { "content": { "multipart/form-data": { @@ -2450,44 +2466,6 @@ } } }, - "/api/Font/upload-url": { - "post": { - "tags": [ - "Font" - ], - "parameters": [ - { - "name": "url", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/EpubFontDto" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/EpubFontDto" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/EpubFontDto" - } - } - } - } - } - } - }, "/api/Health": { "get": { "tags": [ @@ -13818,7 +13796,7 @@ }, "missingSeriesFromSource": { "type": "string", - "description": "A \n separated string of all missing series", + "description": "A \r\n separated string of all missing series", "nullable": true }, "appUser": { @@ -13913,7 +13891,7 @@ }, "missingSeriesFromSource": { "type": "string", - "description": "A \n separated string of all missing series", + "description": "A \r\n separated string of all missing series", "nullable": true } }, @@ -16373,13 +16351,9 @@ "type": "integer", "format": "int32" }, - "created": { + "fileName": { "type": "string", - "format": "date-time" - }, - "lastModified": { - "type": "string", - "format": "date-time" + "nullable": true } }, "additionalProperties": false From 19be1f2bbb32caf549b8439abf260c15013c2d6a Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 13 Jul 2024 12:32:51 -0500 Subject: [PATCH 5/9] First pass at cleaning up the UI flow to mimic closer to the Theme manager. --- API/Controllers/FontController.cs | 36 ++-- API/Services/Tasks/FontService.cs | 22 ++- UI/Web/src/app/_services/font.service.ts | 4 +- .../font-manager/font-manager.component.html | 165 +++++++++--------- .../font-manager/font-manager.component.ts | 37 +++- .../theme-manager.component.html | 3 +- openapi.json | 21 +++ 7 files changed, 183 insertions(+), 105 deletions(-) diff --git a/API/Controllers/FontController.cs b/API/Controllers/FontController.cs index 6db01aebe..dbd100c52 100644 --- a/API/Controllers/FontController.cs +++ b/API/Controllers/FontController.cs @@ -7,10 +7,12 @@ using API.Constants; using API.Data; using API.DTOs.Font; using API.Entities.Enums.Font; +using API.Extensions; using API.Services; using API.Services.Tasks; using API.Services.Tasks.Scanner.Parser; using AutoMapper; +using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -18,22 +20,25 @@ using MimeTypes; namespace API.Controllers; +[Authorize] public class FontController : BaseApiController { private readonly IUnitOfWork _unitOfWork; private readonly IDirectoryService _directoryService; private readonly IFontService _fontService; private readonly IMapper _mapper; + private readonly ILocalizationService _localizationService; private readonly Regex _fontFileExtensionRegex = new(Parser.FontFileExtensions, RegexOptions.IgnoreCase, Parser.RegexTimeout); public FontController(IUnitOfWork unitOfWork, IDirectoryService directoryService, - IFontService fontService, IMapper mapper) + IFontService fontService, IMapper mapper, ILocalizationService localizationService) { _unitOfWork = unitOfWork; _directoryService = directoryService; _fontService = fontService; _mapper = mapper; + _localizationService = localizationService; } /// @@ -63,16 +68,13 @@ public class FontController : BaseApiController var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); if (font == null) return NotFound(); - // var fontDirectory = _directoryService.EpubFontDirectory; - // if (font.Provider == FontProvider.System) - // { - // fontDirectory = _directoryService. - // } + if (font.Provider == FontProvider.System) return BadRequest("System provided fonts are not loaded by API"); + var contentType = MimeTypeMap.GetMimeType(Path.GetExtension(font.FileName)); var path = Path.Join(_directoryService.EpubFontDirectory, font.FileName); - return PhysicalFile(path, contentType); + return PhysicalFile(path, contentType, true); } /// @@ -109,11 +111,21 @@ public class FontController : BaseApiController return Ok(_mapper.Map(font)); } - // [HttpPost("upload-url")] - // public async Task> UploadFontByUrl(string url) - // { - // throw new NotImplementedException(); - // } + [HttpPost("upload-url")] + public async Task UploadFontByUrl(string url) + { + // Validate url + try + { + await _fontService.CreateFontFromUrl(url); + } + catch (KavitaException ex) + { + return BadRequest(_localizationService.Translate(User.GetUserId(), ex.Message)); + } + + return Ok(); + } private async Task UploadToTemp(IFormFile file) { diff --git a/API/Services/Tasks/FontService.cs b/API/Services/Tasks/FontService.cs index 7869088c8..37e6d3130 100644 --- a/API/Services/Tasks/FontService.cs +++ b/API/Services/Tasks/FontService.cs @@ -18,6 +18,7 @@ public interface IFontService { Task CreateFontFromFileAsync(string path); Task Delete(int fontId); + Task CreateFontFromUrl(string url); } public class FontService: IFontService @@ -28,12 +29,16 @@ public class FontService: IFontService private readonly IDirectoryService _directoryService; private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; + private readonly IEventHub _eventHub; - public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger logger) + private const string SupportedFontUrlPrefix = "https://fonts.google.com/specimen/"; + + public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger logger, IEventHub eventHub) { _directoryService = directoryService; _unitOfWork = unitOfWork; _logger = logger; + _eventHub = eventHub; } public async Task CreateFontFromFileAsync(string path) @@ -84,6 +89,21 @@ public class FontService: IFontService await RemoveFont(font); } + public Task CreateFontFromUrl(string url) + { + if (!url.StartsWith(SupportedFontUrlPrefix)) + { + throw new KavitaException("font-url-not-allowed"); + } + + // Extract Font name from url + var fontFamily = url.Split(SupportedFontUrlPrefix)[1].Split("?")[0]; + _logger.LogInformation("Preparing to download {FontName} font", fontFamily); + + // TODO: Send a font update event + return Task.CompletedTask; + } + public async Task RemoveFont(EpubFont font) { if (font.Provider == FontProvider.System) return; diff --git a/UI/Web/src/app/_services/font.service.ts b/UI/Web/src/app/_services/font.service.ts index ecb9b95a9..ee6f4952b 100644 --- a/UI/Web/src/app/_services/font.service.ts +++ b/UI/Web/src/app/_services/font.service.ts @@ -34,12 +34,14 @@ export class FontService { getFonts() { return this.httpClient.get>(this.baseUrl + 'font/all').pipe(map(fonts => { this.fontsSource.next(fonts); - return fonts; })); } + + getFontFace(font: EpubFont): FontFace { + // TODO: We need to refactor this so that we loadFonts with an array, fonts have an id to remove them, and we don't keep populating the document if (font.provider === FontProvider.System) { return new FontFace(font.name, `url('/assets/fonts/${font.name}/${font.fileName}')`); } diff --git a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html index fc615be0c..a0b0c6405 100644 --- a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html +++ b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html @@ -6,93 +6,101 @@

    {{t('description')}}

    -
    -
    -
    -
    -
    -
    - {{t('preview-default')}} -
    -
    -
    -
    - - @if (isUploadingFont) { - - } @else { -
    - - - - - - -
    -
    - - - -
    - -
    - -
    - -
    -
    -
    - } - -
    -
    - -
    -
    +
    +
      - @for (font of fontService.fonts$ | async; track font.name) { - + @for (font of fonts; track font.name) { + }
    -
    +
    +
    + + @if (selectedFont === undefined) { + +
    +
    +
    +
    + {{t('preview-default')}} +
    +
    +
    +
    + + + @if (files && files.length > 0) { + + } @else if (hasAdmin$ | async) { + + + +
    +
    +
    + +
    + +
    +
    + {{t('drag-n-drop')}} + + {{t('upload')}} {{t('upload-continued')}} +
    +
    +
    +
    + +
    + +
    + } + + } @else if (selectedFont) { +

    + {{selectedFont.name | sentenceCase}} +
    + @if (selectedFont.provider !== FontProvider.System && selectedFont.name !== 'Default') { + + } +
    +

    + +
    + +
    + } +
    +
    +
    - -
  • -
    -
    + + @if (item !== undefined) { +
  • +
    {{item.name | sentenceCase}}
    +
    {{item.provider | siteThemeProvider}}
    +
  • + } +
    + + + +
    + @if (item.name === 'Default') { +
    + This font cannot be previewed. This will take the default style from the book. +
    + }
    @if (item.hasOwnProperty('provider') && item.provider === FontProvider.User && item.hasOwnProperty('id')) { @@ -106,13 +114,8 @@
    - - +
    The quick brown fox jumps over the lazy dog - - +
    - - - diff --git a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts index c6275388b..6686b966e 100644 --- a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts +++ b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts @@ -15,6 +15,11 @@ import {LoadingComponent} from "../../../shared/loading/loading.component"; import {FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms"; import {SentenceCasePipe} from "../../../_pipes/sentence-case.pipe"; import {SiteThemeProviderPipe} from "../../../_pipes/site-theme-provider.pipe"; +import {ThemeProvider} from "../../../_models/preferences/site-theme"; +import {CarouselReelComponent} from "../../../carousel/_components/carousel-reel/carousel-reel.component"; +import {DefaultValuePipe} from "../../../_pipes/default-value.pipe"; +import {ImageComponent} from "../../../shared/image/image.component"; +import {SafeUrlPipe} from "../../../_pipes/safe-url.pipe"; @Component({ selector: 'app-font-manager', @@ -29,7 +34,11 @@ import {SiteThemeProviderPipe} from "../../../_pipes/site-theme-provider.pipe"; SentenceCasePipe, SiteThemeProviderPipe, NgTemplateOutlet, - NgStyle + NgStyle, + CarouselReelComponent, + DefaultValuePipe, + ImageComponent, + SafeUrlPipe ], templateUrl: './font-manager.component.html', styleUrl: './font-manager.component.scss', @@ -55,12 +64,15 @@ export class FontManagerComponent implements OnInit { ); form!: FormGroup; + selectedFont: EpubFont | undefined = undefined; + + files: NgxFileDropEntry[] = []; acceptableExtensions = ['.woff2', 'woff', 'tff', 'otf'].join(','); mode: 'file' | 'url' | 'all' = 'all'; isUploadingFont: boolean = false; - constructor(@Inject(DOCUMENT) private document: Document) { - } + + constructor(@Inject(DOCUMENT) private document: Document) {} ngOnInit() { this.form = this.fb.group({ @@ -70,16 +82,24 @@ export class FontManagerComponent implements OnInit { this.fontService.getFonts().subscribe(fonts => { this.fonts = fonts; - this.fonts.forEach(font => { - this.fontService.getFontFace(font).load().then(loadedFace => { - (this.document as any).fonts.add(loadedFace); - }); - }) + // this.fonts.forEach(font => { + // this.fontService.getFontFace(font).load().then(loadedFace => { + // (this.document as any).fonts.add(loadedFace); + // }); + // }) this.cdRef.markForCheck(); }); } + selectFont(font: EpubFont) { + this.fontService.getFontFace(font).load().then(loadedFace => { + (this.document as any).fonts.add(loadedFace); + }); + this.selectedFont = font; + this.cdRef.markForCheck(); + } + dropped(files: NgxFileDropEntry[]) { for (const droppedFile of files) { if (!droppedFile.fileEntry.isFile) { @@ -120,4 +140,5 @@ export class FontManagerComponent implements OnInit { } + protected readonly ThemeProvider = ThemeProvider; } 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 7563ee311..398311ab6 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 @@ -71,8 +71,7 @@ } - } - @else { + } @else {

    {{selectedTheme.name | sentenceCase}}
    diff --git a/openapi.json b/openapi.json index e72afcbc2..629c6cc3f 100644 --- a/openapi.json +++ b/openapi.json @@ -2466,6 +2466,27 @@ } } }, + "/api/Font/upload-url": { + "post": { + "tags": [ + "Font" + ], + "parameters": [ + { + "name": "url", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/Health": { "get": { "tags": [ From 5f1ea3c306af56fa87c4aa04c34a9d52817720cb Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 13 Jul 2024 12:35:53 -0500 Subject: [PATCH 6/9] Fixed a bad rename for EB Garamond font family. --- .../EBGaramond-Italic-VariableFont_wght.ttf | Bin .../EBGaramond-Italic-VariableFont_wght.woff2 | Bin .../EBGaramond-VariableFont_wght.ttf | Bin .../EBGaramond-VariableFont_wght.woff2 | Bin .../fonts/{EB Garmond => EB Garamond}/OFL.txt | 0 UI/Web/src/styles.scss | 2 +- 6 files changed, 1 insertion(+), 1 deletion(-) rename UI/Web/src/assets/fonts/{EB Garmond => EB Garamond}/EBGaramond-Italic-VariableFont_wght.ttf (100%) rename UI/Web/src/assets/fonts/{EB Garmond => EB Garamond}/EBGaramond-Italic-VariableFont_wght.woff2 (100%) rename UI/Web/src/assets/fonts/{EB Garmond => EB Garamond}/EBGaramond-VariableFont_wght.ttf (100%) rename UI/Web/src/assets/fonts/{EB Garmond => EB Garamond}/EBGaramond-VariableFont_wght.woff2 (100%) rename UI/Web/src/assets/fonts/{EB Garmond => EB Garamond}/OFL.txt (100%) diff --git a/UI/Web/src/assets/fonts/EB Garmond/EBGaramond-Italic-VariableFont_wght.ttf b/UI/Web/src/assets/fonts/EB Garamond/EBGaramond-Italic-VariableFont_wght.ttf similarity index 100% rename from UI/Web/src/assets/fonts/EB Garmond/EBGaramond-Italic-VariableFont_wght.ttf rename to UI/Web/src/assets/fonts/EB Garamond/EBGaramond-Italic-VariableFont_wght.ttf diff --git a/UI/Web/src/assets/fonts/EB Garmond/EBGaramond-Italic-VariableFont_wght.woff2 b/UI/Web/src/assets/fonts/EB Garamond/EBGaramond-Italic-VariableFont_wght.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/EB Garmond/EBGaramond-Italic-VariableFont_wght.woff2 rename to UI/Web/src/assets/fonts/EB Garamond/EBGaramond-Italic-VariableFont_wght.woff2 diff --git a/UI/Web/src/assets/fonts/EB Garmond/EBGaramond-VariableFont_wght.ttf b/UI/Web/src/assets/fonts/EB Garamond/EBGaramond-VariableFont_wght.ttf similarity index 100% rename from UI/Web/src/assets/fonts/EB Garmond/EBGaramond-VariableFont_wght.ttf rename to UI/Web/src/assets/fonts/EB Garamond/EBGaramond-VariableFont_wght.ttf diff --git a/UI/Web/src/assets/fonts/EB Garmond/EBGaramond-VariableFont_wght.woff2 b/UI/Web/src/assets/fonts/EB Garamond/EBGaramond-VariableFont_wght.woff2 similarity index 100% rename from UI/Web/src/assets/fonts/EB Garmond/EBGaramond-VariableFont_wght.woff2 rename to UI/Web/src/assets/fonts/EB Garamond/EBGaramond-VariableFont_wght.woff2 diff --git a/UI/Web/src/assets/fonts/EB Garmond/OFL.txt b/UI/Web/src/assets/fonts/EB Garamond/OFL.txt similarity index 100% rename from UI/Web/src/assets/fonts/EB Garmond/OFL.txt rename to UI/Web/src/assets/fonts/EB Garamond/OFL.txt diff --git a/UI/Web/src/styles.scss b/UI/Web/src/styles.scss index 296298e85..65e28f152 100644 --- a/UI/Web/src/styles.scss +++ b/UI/Web/src/styles.scss @@ -53,7 +53,7 @@ // Global Styles @font-face { font-family: "EBGarmond"; - src: url("assets/fonts/EB Garmond/EBGaramond-VariableFont_wght.woff2") format("woff2"); + src: url("assets/fonts/EB Garamond/EBGaramond-VariableFont_wght.woff2") format("woff2"); font-display: swap; } From 01185bdb0a3be5b53a99ae3bf0f1ef731490fb33 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 13 Jul 2024 12:54:02 -0500 Subject: [PATCH 7/9] Fixed up the wiring for uploading by url. Method needs to be implemented. --- API/Controllers/FontController.cs | 4 +- UI/Web/src/app/_services/font.service.ts | 2 +- .../font-manager/font-manager.component.html | 55 ++++++++++++++----- .../font-manager/font-manager.component.ts | 30 +++++----- openapi.json | 2 +- 5 files changed, 59 insertions(+), 34 deletions(-) diff --git a/API/Controllers/FontController.cs b/API/Controllers/FontController.cs index dbd100c52..364122ac6 100644 --- a/API/Controllers/FontController.cs +++ b/API/Controllers/FontController.cs @@ -111,8 +111,8 @@ public class FontController : BaseApiController return Ok(_mapper.Map(font)); } - [HttpPost("upload-url")] - public async Task UploadFontByUrl(string url) + [HttpPost("upload-by-url")] + public async Task UploadFontByUrl([FromQuery]string url) { // Validate url try diff --git a/UI/Web/src/app/_services/font.service.ts b/UI/Web/src/app/_services/font.service.ts index ee6f4952b..96e54bf78 100644 --- a/UI/Web/src/app/_services/font.service.ts +++ b/UI/Web/src/app/_services/font.service.ts @@ -56,7 +56,7 @@ export class FontService { } uploadFromUrl(url: string) { - + return this.httpClient.post(this.baseUrl + "font/upload-by-url?url=" + encodeURIComponent(url), {}); } deleteFont(id: number) { diff --git a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html index a0b0c6405..459d3a5f4 100644 --- a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html +++ b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html @@ -37,27 +37,54 @@ @if (files && files.length > 0) { - } @else if (hasAdmin$ | async) { + } @else { -
    -
    -
    - -
    -
    -
    - {{t('drag-n-drop')}} - - {{t('upload')}} {{t('upload-continued')}} + @switch (mode) { + @case ('all') { + -
    -
    - + } + @case ('url') { +
    +
    +
    + + + +
    + +
    +
    + } + } diff --git a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts index 6686b966e..b5ee62fe6 100644 --- a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts +++ b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.ts @@ -15,7 +15,6 @@ import {LoadingComponent} from "../../../shared/loading/loading.component"; import {FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms"; import {SentenceCasePipe} from "../../../_pipes/sentence-case.pipe"; import {SiteThemeProviderPipe} from "../../../_pipes/site-theme-provider.pipe"; -import {ThemeProvider} from "../../../_models/preferences/site-theme"; import {CarouselReelComponent} from "../../../carousel/_components/carousel-reel/carousel-reel.component"; import {DefaultValuePipe} from "../../../_pipes/default-value.pipe"; import {ImageComponent} from "../../../shared/image/image.component"; @@ -63,7 +62,10 @@ export class FontManagerComponent implements OnInit { map(c => c && this.accountService.hasAdminRole(c)) ); - form!: FormGroup; + form: FormGroup = new FormGroup({ + fontUrl: new FormControl('', []) + }); + selectedFont: EpubFont | undefined = undefined; files: NgxFileDropEntry[] = []; @@ -75,19 +77,12 @@ export class FontManagerComponent implements OnInit { constructor(@Inject(DOCUMENT) private document: Document) {} ngOnInit() { - this.form = this.fb.group({ - coverImageUrl: new FormControl('', []) - }); - this.cdRef.markForCheck(); + this.loadFonts(); + } + loadFonts() { this.fontService.getFonts().subscribe(fonts => { this.fonts = fonts; - // this.fonts.forEach(font => { - // this.fontService.getFontFace(font).load().then(loadedFace => { - // (this.document as any).fonts.add(loadedFace); - // }); - // }) - this.cdRef.markForCheck(); }); } @@ -119,9 +114,13 @@ export class FontManagerComponent implements OnInit { this.cdRef.markForCheck(); } - uploadFromUrl(url?: string) { - url = url || this.form.get('coverImageUrl')?.value.trim(); + uploadFromUrl() { + const url = this.form.get('fontUrl')?.value.trim(); if (!url || url === '') return; + + this.fontService.uploadFromUrl(url).subscribe(() => { + this.loadFonts(); + }); } async deleteFont(id: number) { @@ -137,8 +136,7 @@ export class FontManagerComponent implements OnInit { changeMode(mode: 'file' | 'url' | 'all') { this.mode = mode; + this.cdRef.markForCheck(); } - - protected readonly ThemeProvider = ThemeProvider; } diff --git a/openapi.json b/openapi.json index 629c6cc3f..f8fdd9fc9 100644 --- a/openapi.json +++ b/openapi.json @@ -2466,7 +2466,7 @@ } } }, - "/api/Font/upload-url": { + "/api/Font/upload-by-url": { "post": { "tags": [ "Font" From 2bf9adc7af8cb902ec73579b215dc808c391728a Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 13 Jul 2024 12:59:58 -0500 Subject: [PATCH 8/9] Small change --- .../font-manager/font-manager.component.html | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html index 459d3a5f4..b3b3df3cc 100644 --- a/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html +++ b/UI/Web/src/app/user-settings/font-manager/font-manager/font-manager.component.html @@ -121,7 +121,7 @@ - + @if (item) {
    @if (item.name === 'Default') {
    @@ -133,16 +133,12 @@ @if (item.hasOwnProperty('provider') && item.provider === FontProvider.User && item.hasOwnProperty('id')) { } - -
    - {{item.provider | siteThemeProvider}} -
    -
    -
    - The quick brown fox jumps over the lazy dog +
    + The quick brown fox jumps over the lazy dog
    + } From e63c9427591d18ab3023422cf410689a151bd5ba Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 13 Jul 2024 13:36:02 -0500 Subject: [PATCH 9/9] Added a locale string --- API/I18N/en.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/API/I18N/en.json b/API/I18N/en.json index e038a9ea8..c7f1e8627 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -218,6 +218,9 @@ "scan-libraries": "Scan Libraries", "kavita+-data-refresh": "Kavita+ Data Refresh", "backup": "Backup", - "update-yearly-stats": "Update Yearly Stats" + "update-yearly-stats": "Update Yearly Stats", + + + "font-url-not-allowed": "Uploading a Font by url is only allowed from Google Fonts" }