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}.

This commit is contained in:
Joseph Milazzo 2024-07-13 11:52:23 -05:00
parent 9fae799c63
commit 58800c0b4e
88 changed files with 177 additions and 97 deletions

View file

@ -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")]
/// <summary>
/// List out the fonts
/// </summary>
/// <returns></returns>
[ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute)]
[HttpGet("all")]
public async Task<ActionResult<IEnumerable<EpubFontDto>>> GetFonts()
{
return Ok(await _unitOfWork.EpubFontRepository.GetFontDtosAsync());
}
/// <summary>
/// Returns a font
/// </summary>
/// <param name="fontId"></param>
/// <param name="apiKey"></param>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> 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);
}
/// <summary>
/// Removes a font from the system
/// </summary>
/// <param name="fontId"></param>
/// <param name="confirmed">If the font is in use by other users and an admin wants it deleted, they must confirm to force delete it</param>
/// <returns></returns>
[HttpDelete]
public async Task<IActionResult> DeleteFont(int fontId)
public async Task<IActionResult> 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();
}
/// <summary>
/// Manual upload
/// </summary>
/// <param name="formFile"></param>
/// <returns></returns>
[HttpPost("upload")]
public async Task<ActionResult<EpubFontDto>> 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<EpubFontDto>(font));
}
[HttpPost("upload-url")]
public async Task<ActionResult<EpubFontDto>> UploadFontByUrl(string url)
{
throw new NotImplementedException();
}
// [HttpPost("upload-url")]
// public async Task<ActionResult<EpubFontDto>> UploadFontByUrl(string url)
// {
// throw new NotImplementedException();
// }
private async Task<string> UploadToTemp(IFormFile file)
{

View file

@ -40,7 +40,7 @@ public class ThemeController : BaseApiController
_mapper = mapper;
}
[ResponseCache(CacheProfileName = "10Minute")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute)]
[AllowAnonymous]
[HttpGet]
public async Task<ActionResult<IEnumerable<SiteThemeDto>>> GetThemes()

View file

@ -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; }
}

View file

@ -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<IEnumerable<EpubFontDto>> GetFontDtosAsync()
{
return await _context.EpubFont
.OrderBy(s => s.Name == "Default" ? -1 : 0)
.ThenBy(s => s)
.ProjectTo<EpubFontDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
@ -67,7 +70,7 @@ public class EpubFontRepository: IEpubFontRepository
public async Task<EpubFontDto?> GetFontDtoByNameAsync(string name)
{
return await _context.EpubFont
.Where(f => f.Name.Equals(name))
.Where(f => f.NormalizedName.Equals(name.ToNormalized()))
.ProjectTo<EpubFontDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync();
}

View file

@ -32,13 +32,90 @@ public static class Seed
[
..new List<EpubFont>
{
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();

View file

@ -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)

View file

@ -13,6 +13,5 @@ export interface EpubFont {
id: number;
name: string;
provider: FontProvider;
created: Date;
lastModified: Date;
fileName: string;
}

View file

@ -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})`);
}

View file

@ -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);
});
})

View file

@ -16,7 +16,9 @@
<div class="mb-3">
<label for="library-type" class="form-label">{{t('font-family-label')}}</label>
<select class="form-select" id="library-type" formControlName="bookReaderFontFamily">
<option [value]="opt" *ngFor="let opt of fontOptions; let i = index">{{opt | titlecase}}</option>
@for(opt of fontFamilies; track opt) {
<option [value]="opt.name">{{opt.name | titlecase}}</option>
}
</select>
</div>
</div>

View file

@ -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<string> = [];
fontFamilies: Array<EpubFont> = [];
/**
* 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 + "'";

View file

@ -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, []));

View file

@ -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;
}

View file

@ -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