First pass at cleaning up the UI flow to mimic closer to the Theme manager.

This commit is contained in:
Joseph Milazzo 2024-07-13 12:32:51 -05:00
parent 58800c0b4e
commit 19be1f2bbb
7 changed files with 183 additions and 105 deletions

View file

@ -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;
}
/// <summary>
@ -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);
}
/// <summary>
@ -109,11 +111,21 @@ public class FontController : BaseApiController
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> 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<string> UploadToTemp(IFormFile file)
{

View file

@ -18,6 +18,7 @@ public interface IFontService
{
Task<EpubFont> 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<FontService> _logger;
private readonly IEventHub _eventHub;
public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger<FontService> logger)
private const string SupportedFontUrlPrefix = "https://fonts.google.com/specimen/";
public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger<FontService> logger, IEventHub eventHub)
{
_directoryService = directoryService;
_unitOfWork = unitOfWork;
_logger = logger;
_eventHub = eventHub;
}
public async Task<EpubFont> 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;

View file

@ -34,12 +34,14 @@ export class FontService {
getFonts() {
return this.httpClient.get<Array<EpubFont>>(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}')`);
}

View file

@ -6,10 +6,25 @@
<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="row g-0 theme-container">
<div class="col-lg-3 col-md-5 col-sm-7 col-xs-7 scroller">
<div class="pe-2">
<ul style="height: 100%" class="list-group list-group-flush">
@for (font of fonts; track font.name) {
<ng-container [ngTemplateOutlet]="fontOption" [ngTemplateOutletContext]="{ $implicit: font}"></ng-container>
}
</ul>
</div>
</div>
<div class="col-lg-9 col-md-7 col-sm-4 col-xs-4 ps-3">
<div class="card p-3">
@if (selectedFont === undefined) {
<div class="row pb-4">
<div class="mx-auto">
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly">
@ -19,14 +34,15 @@
</div>
</div>
@if (isUploadingFont) {
@if (files && files.length > 0) {
<app-loading [loading]="isUploadingFont"></app-loading>
} @else {
<form [formGroup]="form">
} @else if (hasAdmin$ | async) {
<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="row g-0 mt-3 pb-3">
<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>
@ -34,10 +50,6 @@
<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>
@ -46,53 +58,49 @@
</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>&nbsp;
<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>
} @else if (selectedFont) {
<h4>
{{selectedFont.name | sentenceCase}}
<div class="float-end">
@if (selectedFont.provider !== FontProvider.System && selectedFont.name !== 'Default') {
<button class="btn btn-danger me-1" (click)="deleteFont(selectedFont.id)">{{t('delete')}}</button>
}
</ul>
</div>
</h4>
<div>
<ng-container [ngTemplateOutlet]="availableFont" [ngTemplateOutletContext]="{ $implicit: selectedFont}"></ng-container>
</div>
}
</div>
</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">
<ng-template #fontOption let-item>
@if (item !== undefined) {
<li class="list-group-item d-flex justify-content-between align-items-start {{selectedFont && selectedFont.name === item.name ? 'active' : ''}}" (click)="selectFont(item)">
<div class="ms-2 me-auto">
<div class="fw-bold">{{item.name | sentenceCase}}</div>
</div>
<div><span class="pill p-1 mx-1 provider">{{item.provider | siteThemeProvider}}</span></div>
</li>
}
</ng-template>
<ng-template #availableFont let-item>
<div class="d-flex justify-content-between w-100">
@if (item.name === 'Default') {
<div class="ms-2 me-auto fs-6">
This font cannot be previewed. This will take the default style from the book.
</div>
}
<div class="d-flex justify-content-end">
@if (item.hasOwnProperty('provider') && item.provider === FontProvider.User && item.hasOwnProperty('id')) {
@ -106,13 +114,8 @@
</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'}">
<div 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>
</div>
</ng-template>
</ng-container>

View file

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

View file

@ -71,8 +71,7 @@
</ngx-file-drop>
}
}
@else {
} @else {
<h4>
{{selectedTheme.name | sentenceCase}}
<div class="float-end">

View file

@ -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": [