AVIF Support & Much More! (#1992)
* Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures.
This commit is contained in:
parent
c1989e2819
commit
70690b747e
73 changed files with 778 additions and 566 deletions
|
|
@ -115,10 +115,10 @@ export class AccountService implements OnDestroy {
|
|||
this.currentUser = user;
|
||||
this.currentUserSource.next(user);
|
||||
|
||||
this.stopRefreshTokenTimer();
|
||||
|
||||
if (this.currentUser !== undefined) {
|
||||
this.startRefreshTokenTimer();
|
||||
} else {
|
||||
this.stopRefreshTokenTimer();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +264,6 @@ export class AccountService implements OnDestroy {
|
|||
|
||||
private refreshToken() {
|
||||
if (this.currentUser === null || this.currentUser === undefined) return of();
|
||||
|
||||
return this.httpClient.post<{token: string, refreshToken: string}>(this.baseUrl + 'account/refresh-token',
|
||||
{token: this.currentUser.token, refreshToken: this.currentUser.refreshToken}).pipe(map(user => {
|
||||
if (this.currentUser) {
|
||||
|
|
@ -277,23 +276,23 @@ export class AccountService implements OnDestroy {
|
|||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Every 10 mins refresh the token
|
||||
*/
|
||||
private startRefreshTokenTimer() {
|
||||
if (this.currentUser === null || this.currentUser === undefined) return;
|
||||
|
||||
if (this.refreshTokenTimeout !== undefined) {
|
||||
if (this.currentUser === null || this.currentUser === undefined) {
|
||||
this.stopRefreshTokenTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
const jwtToken = JSON.parse(atob(this.currentUser.token.split('.')[1]));
|
||||
// set a timeout to refresh the token 10 mins before it expires
|
||||
const expires = new Date(jwtToken.exp * 1000);
|
||||
const timeout = expires.getTime() - Date.now() - (60 * 10000);
|
||||
this.refreshTokenTimeout = setTimeout(() => this.refreshToken().subscribe(() => {}), timeout);
|
||||
this.stopRefreshTokenTimer();
|
||||
|
||||
this.refreshTokenTimeout = setInterval(() => this.refreshToken().subscribe(() => {}), (60 * 10_000));
|
||||
}
|
||||
|
||||
private stopRefreshTokenTimer() {
|
||||
if (this.refreshTokenTimeout !== undefined) {
|
||||
clearTimeout(this.refreshTokenTimeout);
|
||||
clearInterval(this.refreshTokenTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,12 +55,8 @@ export class ServerService {
|
|||
return this.httpClient.get<Job[]>(this.baseUrl + 'server/jobs');
|
||||
}
|
||||
|
||||
convertBookmarks() {
|
||||
return this.httpClient.post(this.baseUrl + 'server/convert-bookmarks', {});
|
||||
}
|
||||
|
||||
convertCovers() {
|
||||
return this.httpClient.post(this.baseUrl + 'server/convert-covers', {});
|
||||
convertMedia() {
|
||||
return this.httpClient.post(this.baseUrl + 'server/convert-media', {});
|
||||
}
|
||||
|
||||
getMediaErrors() {
|
||||
|
|
|
|||
7
UI/Web/src/app/admin/_models/encode-format.ts
Normal file
7
UI/Web/src/app/admin/_models/encode-format.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export enum EncodeFormat {
|
||||
PNG = 0,
|
||||
WebP = 1,
|
||||
AVIF = 2
|
||||
}
|
||||
|
||||
export const EncodeFormats = [{value: EncodeFormat.PNG, title: 'PNG'}, {value: EncodeFormat.WebP, title: 'WebP'}, {value: EncodeFormat.AVIF, title: 'AVIF'}];
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { EncodeFormat } from "./encode-format";
|
||||
|
||||
export interface ServerSettings {
|
||||
cacheDirectory: string;
|
||||
taskScan: string;
|
||||
|
|
@ -10,8 +12,7 @@ export interface ServerSettings {
|
|||
baseUrl: string;
|
||||
bookmarksDirectory: string;
|
||||
emailServiceUrl: string;
|
||||
convertBookmarkToWebP: boolean;
|
||||
convertCoverToWebP: boolean;
|
||||
encodeMediaAs: EncodeFormat;
|
||||
totalBackups: number;
|
||||
totalLogs: number;
|
||||
enableFolderWatching: boolean;
|
||||
|
|
|
|||
|
|
@ -2,28 +2,17 @@
|
|||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined" class="mb-2">
|
||||
|
||||
<div class="row g-0">
|
||||
<p>WebP can drastically reduce space requirements for files. WebP is not supported on all browsers or versions. To learn if these settings are appropriate for your setup, visit <a href="https://caniuse.com/?search=webp" target="_blank" rel="noopener noreferrer">Can I Use</a>.</p>
|
||||
<div *ngIf="settingsForm.dirty" class="alert alert-danger" role="alert">You must trigger the conversion to WebP task in Tasks Tab.</div>
|
||||
<p>WebP/AVIF can drastically reduce space requirements for files. WebP/AVIF is not supported on all browsers or versions. To learn if these settings are appropriate for your setup, visit <a href="https://caniuse.com/?search=webp" target="_blank" rel="noopener noreferrer">Can I Use WebP</a> or <a href="https://caniuse.com/?search=avif" target="_blank" rel="noopener noreferrer">Can I Use AVIF</a>.
|
||||
<b>You cannot convert back to PNG once you've gone to WebP/AVIF. You would need to refresh covers on your libraries to regenerate all covers. Bookmarks and favicons cannot be converted.</b></p>
|
||||
<div *ngIf="settingsForm.dirty" class="alert alert-danger" role="alert">You must trigger the media conversion task in Tasks Tab.</div>
|
||||
<div class="col-md-6 col-sm-12 mb-3">
|
||||
<label for="bookmark-webp" class="form-label me-1" aria-describedby="settings-convertBookmarkToWebP-help">Save Bookmarks as WebP</label>
|
||||
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="convertBookmarkToWebPTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #convertBookmarkToWebPTooltip>When saving bookmarks, convert them to WebP.</ng-template>
|
||||
<span class="visually-hidden" id="settings-convertBookmarkToWebP-help"><ng-container [ngTemplateOutlet]="convertBookmarkToWebPTooltip"></ng-container></span>
|
||||
<div class="form-check form-switch">
|
||||
<input id="bookmark-webp" type="checkbox" class="form-check-input" formControlName="convertBookmarkToWebP" role="switch">
|
||||
<label for="bookmark-webp" class="form-check-label" aria-describedby="settings-convertBookmarkToWebP-help">{{settingsForm.get('convertBookmarkToWebP')?.value ? 'WebP' : 'Original' }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-sm-12 mb-3">
|
||||
<label for="cover-webp" class="form-label me-1" aria-describedby="settings-convertCoverToWebP-help">Save Covers as WebP</label>
|
||||
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="convertCoverToWebPTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #convertCoverToWebPTooltip>When generating covers, convert them to WebP.</ng-template>
|
||||
<span class="visually-hidden" id="settings-convertCoverToWebP-help"><ng-container [ngTemplateOutlet]="convertBookmarkToWebPTooltip"></ng-container></span>
|
||||
<div class="form-check form-switch">
|
||||
<input id="cover-webp" type="checkbox" class="form-check-input" formControlName="convertCoverToWebP" role="switch">
|
||||
<label for="cover-webp" class="form-check-label" aria-describedby="settings-convertCoverToWebP-help">{{settingsForm.get('convertCoverToWebP')?.value ? 'WebP' : 'Original' }}</label>
|
||||
</div>
|
||||
<label for="settings-media-encodeMediaAs" class="form-label me-1">Save Media As</label>
|
||||
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="encodeMediaAsTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #encodeMediaAsTooltip>All media Kavita manages (covers, bookmarks, favicons) will be encoded as this type.</ng-template>
|
||||
<span class="visually-hidden" id="settings-media-encodeMediaAs-help"><ng-container [ngTemplateOutlet]="encodeMediaAsTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-media-encodeMediaAs-help" formControlName="encodeMediaAs" id="settings-media-encodeMediaAs">
|
||||
<option *ngFor="let format of EncodeFormats" [value]="format.value">{{format.title}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { SettingsService } from '../settings.service';
|
|||
import { ServerSettings } from '../_models/server-settings';
|
||||
import { DirectoryPickerComponent, DirectoryPickerResult } from '../_modals/directory-picker/directory-picker.component';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EncodeFormats } from '../_models/encode-format';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-media-settings',
|
||||
|
|
@ -16,29 +17,28 @@ export class ManageMediaSettingsComponent implements OnInit {
|
|||
|
||||
serverSettings!: ServerSettings;
|
||||
settingsForm: FormGroup = new FormGroup({});
|
||||
|
||||
get EncodeFormats() { return EncodeFormats; }
|
||||
|
||||
constructor(private settingsService: SettingsService, private toastr: ToastrService, private modalService: NgbModal, ) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.settingsForm.addControl('convertBookmarkToWebP', new FormControl(this.serverSettings.convertBookmarkToWebP, [Validators.required]));
|
||||
this.settingsForm.addControl('convertCoverToWebP', new FormControl(this.serverSettings.convertCoverToWebP, [Validators.required]));
|
||||
this.settingsForm.addControl('encodeMediaAs', new FormControl(this.serverSettings.encodeMediaAs, [Validators.required]));
|
||||
this.settingsForm.addControl('bookmarksDirectory', new FormControl(this.serverSettings.bookmarksDirectory, [Validators.required]));
|
||||
});
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP);
|
||||
this.settingsForm.get('convertCoverToWebP')?.setValue(this.serverSettings.convertCoverToWebP);
|
||||
this.settingsForm.get('encodeMediaAs')?.setValue(this.serverSettings.encodeMediaAs);
|
||||
this.settingsForm.get('bookmarksDirectory')?.setValue(this.serverSettings.bookmarksDirectory);
|
||||
this.settingsForm.markAsPristine();
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
const modelSettings = Object.assign({}, this.serverSettings);
|
||||
modelSettings.convertBookmarkToWebP = this.settingsForm.get('convertBookmarkToWebP')?.value;
|
||||
modelSettings.convertCoverToWebP = this.settingsForm.get('convertCoverToWebP')?.value;
|
||||
modelSettings.encodeMediaAs = parseInt(this.settingsForm.get('encodeMediaAs')?.value, 10);
|
||||
|
||||
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||
this.settingsForm.addControl('totalBackups', new FormControl(this.serverSettings.totalBackups, [Validators.required, Validators.min(1), Validators.max(30)]));
|
||||
this.settingsForm.addControl('totalLogs', new FormControl(this.serverSettings.totalLogs, [Validators.required, Validators.min(1), Validators.max(30)]));
|
||||
this.settingsForm.addControl('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required]));
|
||||
this.settingsForm.addControl('convertBookmarkToWebP', new FormControl(this.serverSettings.convertBookmarkToWebP, []));
|
||||
this.settingsForm.addControl('encodeMediaAs', new FormControl(this.serverSettings.encodeMediaAs, []));
|
||||
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)]));
|
||||
|
||||
this.serverService.getServerInfo().subscribe(info => {
|
||||
|
|
@ -76,7 +76,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||
this.settingsForm.get('totalBackups')?.setValue(this.serverSettings.totalBackups);
|
||||
this.settingsForm.get('totalLogs')?.setValue(this.serverSettings.totalLogs);
|
||||
this.settingsForm.get('enableFolderWatching')?.setValue(this.serverSettings.enableFolderWatching);
|
||||
this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP);
|
||||
this.settingsForm.get('encodeMediaAs')?.setValue(this.serverSettings.encodeMediaAs);
|
||||
this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName);
|
||||
this.settingsForm.markAsPristine();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,16 +34,10 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
recurringTasks$: Observable<Array<Job>> = of([]);
|
||||
adhocTasks: Array<AdhocTask> = [
|
||||
{
|
||||
name: 'Convert Bookmarks to WebP',
|
||||
description: 'Runs a long-running task which will convert all bookmarks to WebP. This is slow (especially on ARM devices).',
|
||||
api: this.serverService.convertBookmarks(),
|
||||
successMessage: 'Conversion of Bookmarks has been queued'
|
||||
},
|
||||
{
|
||||
name: 'Convert Covers to WebP',
|
||||
description: 'Runs a long-running task which will convert all existing covers to WebP. This is slow (especially on ARM devices).',
|
||||
api: this.serverService.convertCovers(),
|
||||
successMessage: 'Conversion of Covers has been queued'
|
||||
name: 'Convert Media to Target Encoding',
|
||||
description: 'Runs a long-running task which will convert all kavita-managed media to the target encoding. This is slow (especially on ARM devices).',
|
||||
api: this.serverService.convertMedia(),
|
||||
successMessage: 'Conversion of Media to Target Encoding has been queued'
|
||||
},
|
||||
{
|
||||
name: 'Clear Cache',
|
||||
|
|
@ -144,12 +138,6 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
runAdhocConvert() {
|
||||
this.serverService.convertBookmarks().subscribe(() => {
|
||||
this.toastr.success('Conversion of Bookmarks has been queued.');
|
||||
});
|
||||
}
|
||||
|
||||
runAdhoc(task: AdhocTask) {
|
||||
task.api.subscribe((data: any) => {
|
||||
if (task.successMessage.length > 0) {
|
||||
|
|
@ -159,6 +147,8 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
if (task.successFunction) {
|
||||
task.successFunction(data);
|
||||
}
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -357,10 +357,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<button class="btn btn-secondary" (click)="addWebLink()">
|
||||
<button class="btn btn-secondary me-1" (click)="addWebLink()">
|
||||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Add Link</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" (click)="removeWebLink(i)">
|
||||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Remove Link</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.editSeriesForm.get('releaseYear')?.patchValue(this.metadata.releaseYear);
|
||||
|
||||
this.WebLinks.forEach((link, index) => {
|
||||
this.editSeriesForm.addControl('link' + index, new FormControl(link, [Validators.required]));
|
||||
this.editSeriesForm.addControl('link' + index, new FormControl(link, []));
|
||||
});
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
|
|
@ -521,7 +521,16 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
|
||||
addWebLink() {
|
||||
this.metadata.webLinks += ',';
|
||||
this.editSeriesForm.addControl('link' + (this.WebLinks.length - 1), new FormControl('', [Validators.required]));
|
||||
this.editSeriesForm.addControl('link' + (this.WebLinks.length - 1), new FormControl('', []));
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
removeWebLink(index: number) {
|
||||
const tokens = this.metadata.webLinks.split(',');
|
||||
const tokenToRemove = tokens[index];
|
||||
|
||||
this.metadata.webLinks = tokens.filter(t => t != tokenToRemove).join(',');
|
||||
this.editSeriesForm.removeControl('link' + index, {emitEvent: true});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="col-auto ms-2">
|
||||
<ngb-rating class="rating-star" [(rate)]="series.userRating" (rateChange)="updateRating($event)" (click)="promptToReview()" [resettable]="false">
|
||||
<ngb-rating class="rating-star" [(rate)]="series.userRating" (rateChange)="updateRating($event)" [resettable]="false">
|
||||
<ng-template let-fill="fill" let-index="index">
|
||||
<span class="star" [class.filled]="(index < series.userRating) && series.userRating > 0">★</span>
|
||||
</ng-template>
|
||||
|
|
|
|||
|
|
@ -740,19 +740,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||
});
|
||||
}
|
||||
|
||||
async promptToReview() {
|
||||
// TODO: After a review has been set, we might just want to show an edit icon next to star rating which opens the review, instead of prompting each time.
|
||||
const shouldPrompt = this.isNullOrEmpty(this.series.userReview);
|
||||
const config = new ConfirmConfig();
|
||||
config.header = 'Confirm';
|
||||
config.content = 'Do you want to write a review?';
|
||||
config.buttons.push({text: 'No', type: 'secondary'});
|
||||
config.buttons.push({text: 'Yes', type: 'primary'});
|
||||
if (shouldPrompt && await this.confirmService.confirm('Do you want to write a review?', config)) {
|
||||
this.openReviewModal();
|
||||
}
|
||||
}
|
||||
|
||||
openReviewModal(force = false) {
|
||||
const modalRef = this.modalService.open(ReviewSeriesModalComponent, { scrollable: true, size: 'lg' });
|
||||
modalRef.componentInstance.series = this.series;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<meta name="theme-color" content="#000000">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="#000000">
|
||||
<!-- Don't allow indexing from Bots -->
|
||||
<meta name='robots' content='none' />
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue