UI centered font management
This commit is contained in:
parent
1f2ea8f59d
commit
a956bb18ec
18 changed files with 774 additions and 146 deletions
|
@ -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<ActionResult<IEnumerable<EpubFontDto>>> GetFonts()
|
||||
{
|
||||
return Ok(await _unitOfWork.EpubFontRepository.GetFontDtos());
|
||||
return Ok(await _unitOfWork.EpubFontRepository.GetFontDtosAsync());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
[HttpGet("download-font")]
|
||||
public async Task<IActionResult> GetFont(int fontId)
|
||||
public async Task<IActionResult> GetFont(int fontId, string apiKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var font = await _unitOfWork.EpubFontRepository.GetFont(fontId);
|
||||
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 = GetContentType(font.FileName);
|
||||
return File(await _fontService.GetContent(fontId), contentType, font.FileName);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
|
||||
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<IActionResult> DeleteFont(int fontId)
|
||||
{
|
||||
_taskScheduler.ScanEpubFonts();
|
||||
await _fontService.Delete(fontId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private string GetContentType(string fileName)
|
||||
[HttpPost("upload")]
|
||||
public async Task<ActionResult<EpubFontDto>> UploadFont(IFormFile formFile)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
return extension switch
|
||||
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<EpubFontDto>(font));
|
||||
}
|
||||
|
||||
[HttpPost("upload-url")]
|
||||
public async Task<ActionResult<EpubFontDto>> UploadFontByUrl(string url)
|
||||
{
|
||||
".ttf" => "application/font-tff",
|
||||
".otf" => "application/font-otf",
|
||||
".woff" => "application/font-woff",
|
||||
".woff2" => "application/font-woff2",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private async Task<string> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,12 @@ public interface IEpubFontRepository
|
|||
void Add(EpubFont font);
|
||||
void Remove(EpubFont font);
|
||||
void Update(EpubFont font);
|
||||
Task<IEnumerable<EpubFontDto>> GetFontDtos();
|
||||
Task<EpubFontDto?> GetFontDto(int fontId);
|
||||
Task<IEnumerable<EpubFont>> GetFonts();
|
||||
Task<EpubFont?> GetFont(int fontId);
|
||||
Task<IEnumerable<EpubFontDto>> GetFontDtosAsync();
|
||||
Task<EpubFontDto?> GetFontDtoAsync(int fontId);
|
||||
Task<EpubFontDto?> GetFontDtoByNameAsync(string name);
|
||||
Task<IEnumerable<EpubFont>> GetFontsAsync();
|
||||
Task<EpubFont?> GetFontAsync(int fontId);
|
||||
Task<bool> IsFontInUseAsync(int fontId);
|
||||
}
|
||||
|
||||
public class EpubFontRepository: IEpubFontRepository
|
||||
|
@ -47,14 +49,14 @@ public class EpubFontRepository: IEpubFontRepository
|
|||
_context.Entry(font).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EpubFontDto>> GetFontDtos()
|
||||
public async Task<IEnumerable<EpubFontDto>> GetFontDtosAsync()
|
||||
{
|
||||
return await _context.EpubFont
|
||||
.ProjectTo<EpubFontDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<EpubFontDto?> GetFontDto(int fontId)
|
||||
public async Task<EpubFontDto?> GetFontDtoAsync(int fontId)
|
||||
{
|
||||
return await _context.EpubFont
|
||||
.Where(f => f.Id == fontId)
|
||||
|
@ -62,16 +64,35 @@ public class EpubFontRepository: IEpubFontRepository
|
|||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EpubFont>> GetFonts()
|
||||
public async Task<EpubFontDto?> GetFontDtoByNameAsync(string name)
|
||||
{
|
||||
return await _context.EpubFont
|
||||
.Where(f => f.Name.Equals(name))
|
||||
.ProjectTo<EpubFontDto>(_mapper.ConfigurationProvider)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EpubFont>> GetFontsAsync()
|
||||
{
|
||||
return await _context.EpubFont
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<EpubFont?> GetFont(int fontId)
|
||||
public async Task<EpubFont?> GetFontAsync(int fontId)
|
||||
{
|
||||
return await _context.EpubFont
|
||||
.Where(f => f.Id == fontId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ public interface IUserRepository
|
|||
Task<IList<AppUserBookmark>> GetAllBookmarksByIds(IList<int> bookmarkIds);
|
||||
Task<AppUser?> GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None);
|
||||
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
|
||||
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByFontAsync(string fontName);
|
||||
Task<bool> HasAccessToLibrary(int libraryId, int userId);
|
||||
Task<bool> HasAccessToSeries(int userId, int seriesId);
|
||||
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None);
|
||||
|
@ -260,6 +261,14 @@ public class UserRepository : IUserRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByFontAsync(string fontName)
|
||||
{
|
||||
return await _context.AppUserPreferences
|
||||
.Where(p => p.BookReaderFontFamily == fontName)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> HasAccessToLibrary(int libraryId, int userId)
|
||||
{
|
||||
return await _context.Library
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If there is an enqueued or scheduled task for <see cref="ScannerService.ScanLibrary"/> method
|
||||
/// </summary>
|
||||
|
|
|
@ -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<byte[]> GetContent(int fontId);
|
||||
Task Scan();
|
||||
Task<EpubFont> 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> _messageHub;
|
||||
private readonly ILogger<FontService> _logger;
|
||||
|
||||
public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, IHubContext<MessageHub> messageHub,
|
||||
ILogger<FontService> logger)
|
||||
public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger<FontService> logger)
|
||||
{
|
||||
_directoryService = directoryService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_messageHub = messageHub;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<byte[]> GetContent(int fontId)
|
||||
public async Task<EpubFont> 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);
|
||||
if (!_directoryService.FileSystem.File.Exists(path))
|
||||
{
|
||||
_logger.LogInformation("Unable to create font from manual upload as font not in temp");
|
||||
throw new KavitaException("errors.font-manual-upload");
|
||||
}
|
||||
|
||||
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 fileName = _directoryService.FileSystem.FileInfo.New(path).Name;
|
||||
var nakedFileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(fileName);
|
||||
var fontName = Parser.PrettifyFileName(nakedFileName);
|
||||
var normalizedName = Parser.Normalize(nakedFileName);
|
||||
if (allFontNames.Contains(normalizedName)) continue;
|
||||
|
||||
_unitOfWork.EpubFontRepository.Add(new EpubFont()
|
||||
if (await _unitOfWork.EpubFontRepository.GetFontDtoByNameAsync(fontName) != null)
|
||||
{
|
||||
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 = _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())
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -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]", " ");
|
||||
}
|
||||
}
|
||||
|
|
18
UI/Web/src/app/_models/preferences/epub-font.ts
Normal file
18
UI/Web/src/app/_models/preferences/epub-font.ts
Normal file
|
@ -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;
|
||||
}
|
61
UI/Web/src/app/_services/font.service.ts
Normal file
61
UI/Web/src/app/_services/font.service.ts
Normal file
|
@ -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<EpubFont[]>(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<Array<EpubFont>>(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<EpubFont>(this.baseUrl + "font/upload", formData);
|
||||
}
|
||||
|
||||
uploadFromUrl(url: string) {
|
||||
|
||||
}
|
||||
|
||||
deleteFont(id: number) {
|
||||
return this.httpClient.delete(this.baseUrl + `font?fontId=${id}`);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<EpubFont[]> {
|
||||
return this.http.get<Array<EpubFont>>(this.baseUrl + 'Font/GetFonts')
|
||||
}
|
||||
|
||||
getBookChapters(chapterId: number) {
|
||||
return this.http.get<Array<BookChapterItem>>(this.baseUrl + 'book/' + chapterId + '/chapters');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
<ng-container *transloco="let t; read:'font-manager'">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<h3>{{t('title')}}</h3>
|
||||
</div>
|
||||
|
||||
<p>{{t('description')}}</p>
|
||||
|
||||
<div class="d-flex justify-content-center flex-grow-1">
|
||||
<div class="card d-flex col-lg-9 col-md-7 col-sm-4 col-xs-4 p-3">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="mx-auto">
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-evenly">
|
||||
{{t('preview-default')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isUploadingFont) {
|
||||
<app-loading [loading]="isUploadingFont"></app-loading>
|
||||
} @else {
|
||||
<form [formGroup]="form">
|
||||
<ngx-file-drop (onFileDrop)="dropped($event)" [accept]="acceptableExtensions" [directory]="false"
|
||||
dropZoneClassName="file-upload" contentClassName="file-upload-zone">
|
||||
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
||||
<div class="row g-0 p-3" *ngIf="mode === 'all'">
|
||||
<div class="mx-auto">
|
||||
<div class="row g-0 mb-3">
|
||||
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-evenly">
|
||||
<a class="pe-0" href="javascript:void(0)" (click)="changeMode('url')">
|
||||
<span class="phone-hidden">{{t('enter-an-url-pre-title', {url: ''})}}</span>{{t('url')}}
|
||||
</a>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-container *ngIf="mode === 'url'">
|
||||
<div class="row g-0 mt-3 pb-3 ms-md-2 me-md-2">
|
||||
<div class="input-group col-auto me-md-2" style="width: 83%">
|
||||
<label class="input-group-text" for="load-font">{{t('url-label')}}</label>
|
||||
<input type="text" autofocus autocomplete="off" class="form-control" formControlName="fontUrl" placeholder="https://" id="load-font">
|
||||
<button class="btn btn-outline-secondary" type="button" id="load-font-addon" (click)="uploadFromUrl(); mode='all';" [disabled]="(form.get('fontUrl')?.value).length === 0">
|
||||
{{t('load')}}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary col-auto" href="javascript:void(0)" (click)="mode = 'all'">
|
||||
<i class="fas fa-share" aria-hidden="true" style="transform: rotateY(180deg)"></i>
|
||||
<span class="phone-hidden">{{t('back')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-template>
|
||||
</ngx-file-drop>
|
||||
</form>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3 mb-3 g-0">
|
||||
<div class="scroller">
|
||||
<div class="pe-2">
|
||||
<ul style="height: 100%" class="list-group list-group-flush">
|
||||
|
||||
@for (font of fontService.fonts$ | async; track font.name) {
|
||||
<ng-container [ngTemplateOutlet]="availableFont" [ngTemplateOutletContext]="{ $implicit: font}"></ng-container>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #availableFont let-item>
|
||||
<li class="list-group-item d-flex flex-column align-items-start border-bottom border-info">
|
||||
<div class="d-flex justify-content-between w-100">
|
||||
<div class="ms-2 me-auto fs-5">
|
||||
<div class="fw-bold">{{item.name | sentenceCase}}</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
@if (item.hasOwnProperty('provider') && item.provider === FontProvider.User && item.hasOwnProperty('id')) {
|
||||
<button class="btn btn-danger me-1" (click)="deleteFont(item.id)">{{t('delete')}}</button>
|
||||
}
|
||||
|
||||
<div *ngIf="item.hasOwnProperty('provider')" class="align-self-center">
|
||||
<span class="pill p-1 mx-1 provider">{{item.provider | siteThemeProvider}}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<span class="p-1 me-1 preview mt-2 flex-grow-1 text-center w-100 fs-4 fs-lg-3" [ngStyle]="{'font-family': item.name, 'word-break': 'keep-all'}">
|
||||
The quick brown fox jumps over the lazy dog
|
||||
</span>
|
||||
</li>
|
||||
</ng-template>
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
|
@ -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<EpubFont> = [];
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -619,6 +619,10 @@
|
|||
<app-theme-manager></app-theme-manager>
|
||||
}
|
||||
|
||||
@defer (when tab.fragment === FragmentID.Font; prefetch on idle) {
|
||||
<app-font-manager></app-font-manager>
|
||||
}
|
||||
|
||||
@defer (when tab.fragment === FragmentID.Devices; prefetch on idle) {
|
||||
<app-manage-devices></app-manage-devices>
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
})
|
||||
|
|
|
@ -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": {
|
||||
|
|
208
openapi.json
208
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": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue