First pass at cleaning up the UI flow to mimic closer to the Theme manager.
This commit is contained in:
parent
58800c0b4e
commit
19be1f2bbb
7 changed files with 183 additions and 105 deletions
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}')`);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -71,8 +71,7 @@
|
|||
</ngx-file-drop>
|
||||
}
|
||||
|
||||
}
|
||||
@else {
|
||||
} @else {
|
||||
<h4>
|
||||
{{selectedTheme.name | sentenceCase}}
|
||||
<div class="float-end">
|
||||
|
|
21
openapi.json
21
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": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue