Customized Scheduler + Saved Kavita+ Details (#2644)

This commit is contained in:
Joe Milazzo 2024-01-22 12:10:57 -06:00 committed by GitHub
parent 2092e120c3
commit ad74871623
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 6076 additions and 3370 deletions

View file

@ -127,13 +127,6 @@ export class DirectoryPickerComponent implements OnInit {
});
}
shareFolder(fullPath: string, event: any) {
event.preventDefault();
event.stopPropagation();
this.modal.close({success: true, folderPath: fullPath});
}
share() {
this.modal.close({success: true, folderPath: this.path});
}
@ -142,20 +135,6 @@ export class DirectoryPickerComponent implements OnInit {
this.modal.close({success: false, folderPath: undefined});
}
getStem(path: string): string {
const lastPath = this.routeStack.peek();
if (lastPath && lastPath != path) {
let replaced = path.replace(lastPath, '');
if (replaced.startsWith('/') || replaced.startsWith('\\')) {
replaced = replaced.substring(1, replaced.length);
}
return replaced;
}
return path;
}
navigateTo(index: number) {
while(this.routeStack.items.length - 1 > index) {
this.routeStack.pop();

View file

@ -6,6 +6,7 @@ export interface ServerSettings {
cacheDirectory: string;
taskScan: string;
taskBackup: string;
taskCleanup: string;
loggingLevel: string;
port: number;
ipAddresses: string;

View file

@ -28,9 +28,11 @@
<div class="col-md-6 col-sm-12">
<div class="mb-3" style="width:100%">
<label for="email" class="form-label">{{t('email')}}</label>
<input class="form-control" inputmode="email" type="email" id="email" formControlName="email" aria-describedby="email-validations">
<input class="form-control" inputmode="email" type="email" id="email"
[class.is-invalid]="userForm.get('email')?.invalid && userForm.get('email')?.touched"
formControlName="email" aria-describedby="email-validations">
<div id="email-validations" class="invalid-feedback"
*ngIf="userForm.dirty || userForm.touched" [class.is-invalid]="userForm.get('email')?.invalid && userForm.get('email')?.touched">
*ngIf="userForm.dirty || userForm.touched">
<div *ngIf="userForm.get('email')?.errors?.required">
{{t('required')}}
</div>

View file

@ -73,9 +73,6 @@
<div class="col-md-6 col-sm-12 pe-2 ps-2 mb-2">
<label for="settings-password" class="form-label">{{t('password-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="passwordTooltip" role="button" tabindex="0"></i>
<ng-template #passwordTooltip>{{t('password-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-password-help"><ng-container [ngTemplateOutlet]="usernameTooltip"></ng-container></span>
<input type="password" class="form-control" aria-describedby="manga-header" formControlName="password" id="settings-password" />
</div>
</div>

View file

@ -99,6 +99,10 @@ export class ManageSettingsComponent implements OnInit {
async saveSettings() {
const modelSettings = this.settingsForm.value;
modelSettings.bookmarksDirectory = this.serverSettings.bookmarksDirectory;
modelSettings.smtpConfig = this.serverSettings.smtpConfig;
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings;
this.resetForm();

View file

@ -34,9 +34,13 @@
<div class="col-4">{{t('source-title')}}</div>
<div class="col"><a href="https://github.com/Kareadita/Kavita" target="_blank" rel="noopener noreferrer">github.com/Kareadita/Kavita</a></div>
</div>
<div class="row">
<div class="col-4">{{t('localization-title')}}</div>
<div class="col"><a href="https://hosted.weblate.org/engage/kavita/" target="_blank" rel="noopener noreferrer">Weblate</a><br/></div>
</div>
<div class="row">
<div class="col-4">{{t('feature-request-title')}}</div>
<div class="col"><a href="https://feats.kavitareader.com" target="_blank" rel="noopener noreferrer">https://feats.kavitareader.com</a><br/></div>
<div class="col"><a href="https://github.com/Kareadita/Kavita/discussions/2529" target="_blank" rel="noopener noreferrer">https://github.com/Kareadita/Kavita/discussions/</a><br/></div>
</div>
</div>

View file

@ -7,8 +7,30 @@
<ng-template #taskScanTooltip>{{t('library-scan-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-tasks-scan-help"><ng-container [ngTemplateOutlet]="taskScanTooltip"></ng-container></span>
<select class="form-select" aria-describedby="settings-tasks-scan-help" formControlName="taskScan" id="settings-tasks-scan">
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{t(freq)}}</option>
</select>
@if (settingsForm.get('taskScan')!.value === customOption) {
<div class="mt-3">
<label for="custom-task-scan" class="form-label">{{t('custom-label')}}</label>
<input class="form-control" type="text"
id="custom-task-scan" formControlName="taskScanCustom"
[class.is-invalid]="settingsForm.get('taskScanCustom')?.invalid && settingsForm.get('taskScanCustom')?.touched"
aria-describedby="task-scan-validations">
@if (settingsForm.dirty || settingsForm.touched) {
<div id="task-scan-validations" class="invalid-feedback">
<div *ngIf="settingsForm.get('taskScanCustom')?.errors?.required">
{{t('required')}}
</div>
<div *ngIf="settingsForm.get('taskScanCustom')?.errors?.invalidCron">
{{t('cron-notation')}}
</div>
</div>
}
</div>
}
</div>
<div class="mb-3">
@ -16,8 +38,65 @@
<ng-template #taskBackupTooltip>{{t('library-database-backup-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-tasks-backup-help"><ng-container [ngTemplateOutlet]="taskBackupTooltip"></ng-container></span>
<select class="form-select" aria-describedby="settings-tasks-backup-help" formControlName="taskBackup" id="settings-tasks-backup">
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{t(freq)}}</option>
</select>
@if (settingsForm.get('taskBackup')!.value === customOption) {
<div class="mt-3">
<label for="custom-task-backup" class="form-label">{{t('custom-label')}}</label>
<input class="form-control" type="text"
id="custom-task-backup" formControlName="taskBackupCustom"
[class.is-invalid]="settingsForm.get('taskBackupCustom')?.invalid && settingsForm.get('taskBackupCustom')?.touched"
aria-describedby="task-backup-validations">
@if (settingsForm.dirty || settingsForm.touched) {
<div id="task-backup-validations" class="invalid-feedback">
<div *ngIf="settingsForm.get('taskBackupCustom')?.errors?.required">
{{t('required')}}
</div>
<div *ngIf="settingsForm.get('taskBackupCustom')?.errors?.invalidCron">
{{t('cron-notation')}}
</div>
</div>
}
</div>
}
</div>
<div class="mb-3">
<label for="settings-tasks-cleanup" class="form-label">{{t('cleanup-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="taskCleanupTooltip" role="button" tabindex="0"></i>
<ng-template #taskCleanupTooltip>{{t('cleanup-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-tasks-cleanup-help"><ng-container [ngTemplateOutlet]="taskCleanupTooltip"></ng-container></span>
<select class="form-select" aria-describedby="settings-tasks-cleanup-help" formControlName="taskCleanup" id="settings-tasks-cleanup">
<option *ngFor="let freq of taskFrequenciesForCleanup" [value]="freq">{{t(freq)}}</option>
</select>
@if (settingsForm.get('taskCleanup')!.value === customOption) {
<div class="mt-3">
<label for="custom-task-cleanup" class="form-label">{{t('custom-label')}}</label>
<input class="form-control" type="text"
id="custom-task-cleanup" formControlName="taskCleanupCustom"
[class.is-invalid]="settingsForm.get('taskCleanupCustom')?.invalid && settingsForm.get('taskCleanupCustom')?.touched"
aria-describedby="task-cleanup-validations">
@if (settingsForm.dirty || settingsForm.touched) {
<div id="task-cleanup-validations" class="invalid-feedback">
<div *ngIf="settingsForm.get('taskCleanupCustom')?.errors?.required">
{{t('required')}}
</div>
<div *ngIf="settingsForm.get('taskCleanupCustom')?.errors?.invalidCron">
{{t('cron-notation')}}
</div>
</div>
}
</div>
}
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
<h4>{{t('adhoc-tasks-title')}}</h4>
@ -65,13 +144,6 @@
</tr>
</tbody>
</table>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</form>
</div>

View file

@ -1,10 +1,10 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {ToastrService} from 'ngx-toastr';
import {SettingsService} from '../settings.service';
import {ServerSettings} from '../_models/server-settings';
import {shareReplay, take} from 'rxjs/operators';
import {defer, forkJoin, Observable, of} from 'rxjs';
import {debounceTime, defer, distinctUntilChanged, forkJoin, Observable, of, switchMap, tap} from 'rxjs';
import {ServerService} from 'src/app/_services/server.service';
import {Job} from 'src/app/_models/job/job';
import {UpdateNotificationModalComponent} from 'src/app/shared/update-notification/update-notification-modal.component';
@ -12,10 +12,12 @@ import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {DownloadService} from 'src/app/shared/_services/download.service';
import {DefaultValuePipe} from '../../_pipes/default-value.pipe';
import {AsyncPipe, DatePipe, NgFor, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
import {translate, TranslocoModule} from "@ngneat/transloco";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
interface AdhocTask {
name: string;
description: string;
@ -30,15 +32,18 @@ interface AdhocTask {
styleUrls: ['./manage-tasks-settings.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, AsyncPipe, TitleCasePipe, DatePipe, DefaultValuePipe, TranslocoModule, NgTemplateOutlet, TranslocoLocaleModule, UtcToLocalTimePipe]
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, AsyncPipe, TitleCasePipe, DatePipe, DefaultValuePipe,
TranslocoModule, NgTemplateOutlet, TranslocoLocaleModule, UtcToLocalTimePipe]
})
export class ManageTasksSettingsComponent implements OnInit {
private readonly translocoService = inject(TranslocoService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef);
serverSettings!: ServerSettings;
settingsForm: FormGroup = new FormGroup({});
taskFrequencies: Array<string> = [];
taskFrequenciesForCleanup: Array<string> = [];
logLevels: Array<string> = [];
recurringTasks$: Observable<Array<Job>> = of([]);
@ -104,7 +109,7 @@ export class ManageTasksSettingsComponent implements OnInit {
successMessage: '',
successFunction: (update) => {
if (update === null) {
this.toastr.info(this.translocoService.translate('toasts.no-updates'));
this.toastr.info(translate('toasts.no-updates'));
return;
}
const modalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' });
@ -112,6 +117,7 @@ export class ManageTasksSettingsComponent implements OnInit {
}
},
];
customOption = 'custom';
constructor(private settingsService: SettingsService, private toastr: ToastrService,
private serverService: ServerService, private modalService: NgbModal,
@ -124,10 +130,79 @@ export class ManageTasksSettingsComponent implements OnInit {
settings: this.settingsService.getServerSettings()
}).subscribe(result => {
this.taskFrequencies = result.frequencies;
this.taskFrequencies.push(this.customOption);
this.taskFrequenciesForCleanup = this.taskFrequencies.filter(f => f !== 'disabled');
this.logLevels = result.levels;
this.serverSettings = result.settings;
this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required]));
this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required]));
this.settingsForm.addControl('taskCleanup', new FormControl(this.serverSettings.taskCleanup, [Validators.required]));
if (!this.taskFrequencies.includes(this.serverSettings.taskScan)) {
this.settingsForm.get('taskScan')?.setValue(this.customOption);
this.settingsForm.addControl('taskScanCustom', new FormControl(this.serverSettings.taskScan, [Validators.required]));
} else {
this.settingsForm.addControl('taskScanCustom', new FormControl('', [Validators.required]));
}
if (!this.taskFrequencies.includes(this.serverSettings.taskBackup)) {
this.settingsForm.get('taskBackup')?.setValue(this.customOption);
this.settingsForm.addControl('taskBackupCustom', new FormControl(this.serverSettings.taskBackup, [Validators.required]));
} else {
this.settingsForm.addControl('taskBackupCustom', new FormControl('', [Validators.required]));
}
if (!this.taskFrequenciesForCleanup.includes(this.serverSettings.taskCleanup)) {
this.settingsForm.get('taskCleanup')?.setValue(this.customOption);
this.settingsForm.addControl('taskCleanupCustom', new FormControl(this.serverSettings.taskCleanup, [Validators.required]));
} else {
this.settingsForm.addControl('taskCleanupCustom', new FormControl('', [Validators.required]));
}
this.settingsForm.get('taskScanCustom')?.valueChanges.pipe(
debounceTime(100),
switchMap(val => this.settingsService.isValidCronExpression(val)),
tap(isValid => {
if (isValid) {
this.settingsForm.get('taskScanCustom')?.setErrors(null);
} else {
this.settingsForm.get('taskScanCustom')?.setErrors({invalidCron: true})
}
this.cdRef.markForCheck();
}),
takeUntilDestroyed(this.destroyRef)
).subscribe();
this.settingsForm.get('taskBackupCustom')?.valueChanges.pipe(
debounceTime(100),
switchMap(val => this.settingsService.isValidCronExpression(val)),
tap(isValid => {
if (isValid) {
this.settingsForm.get('taskBackupCustom')?.setErrors(null);
} else {
this.settingsForm.get('taskBackupCustom')?.setErrors({invalidCron: true})
}
this.cdRef.markForCheck();
}),
takeUntilDestroyed(this.destroyRef)
).subscribe();
this.settingsForm.get('taskCleanupCustom')?.valueChanges.pipe(
debounceTime(100),
switchMap(val => this.settingsService.isValidCronExpression(val)),
tap(isValid => {
if (isValid) {
this.settingsForm.get('taskCleanupCustom')?.setErrors(null);
} else {
this.settingsForm.get('taskCleanupCustom')?.setErrors({invalidCron: true})
}
this.cdRef.markForCheck();
}),
takeUntilDestroyed(this.destroyRef)
).subscribe();
this.cdRef.markForCheck();
});
@ -135,9 +210,30 @@ export class ManageTasksSettingsComponent implements OnInit {
this.cdRef.markForCheck();
}
resetForm() {
this.settingsForm.get('taskScan')?.setValue(this.serverSettings.taskScan);
this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup);
this.settingsForm.get('taskCleanup')?.setValue(this.serverSettings.taskCleanup);
if (!this.taskFrequencies.includes(this.serverSettings.taskScan)) {
this.settingsForm.get('taskScanCustom')?.setValue(this.serverSettings.taskScan);
} else {
this.settingsForm.get('taskScanCustom')?.setValue('');
}
if (!this.taskFrequencies.includes(this.serverSettings.taskBackup)) {
this.settingsForm.get('taskBackupCustom')?.setValue(this.serverSettings.taskBackup);
} else {
this.settingsForm.get('taskBackupCustom')?.setValue('');
}
if (!this.taskFrequencies.includes(this.serverSettings.taskCleanup)) {
this.settingsForm.get('taskCleanupCustom')?.setValue(this.serverSettings.taskCleanup);
} else {
this.settingsForm.get('taskCleanupCustom')?.setValue('');
}
this.settingsForm.markAsPristine();
this.cdRef.markForCheck();
}
@ -146,12 +242,26 @@ export class ManageTasksSettingsComponent implements OnInit {
const modelSettings = Object.assign({}, this.serverSettings);
modelSettings.taskBackup = this.settingsForm.get('taskBackup')?.value;
modelSettings.taskScan = this.settingsForm.get('taskScan')?.value;
modelSettings.taskCleanup = this.settingsForm.get('taskCleanup')?.value;
if (this.serverSettings.taskBackup === this.customOption) {
modelSettings.taskBackup = this.settingsForm.get('taskBackupCustom')?.value;
}
if (this.serverSettings.taskScan === this.customOption) {
modelSettings.taskScan = this.settingsForm.get('taskScanCustom')?.value;
}
if (this.serverSettings.taskScan === this.customOption) {
modelSettings.taskCleanup = this.settingsForm.get('taskCleanupCustom')?.value;
}
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.serverSettings = settings;
this.resetForm();
this.recurringTasks$ = this.serverService.getRecurringJobs().pipe(shareReplay());
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated'));
this.toastr.success(translate('toasts.server-settings-updated'));
this.cdRef.markForCheck();
}, (err: any) => {
console.error('error: ', err);
@ -162,7 +272,7 @@ export class ManageTasksSettingsComponent implements OnInit {
this.settingsService.resetServerSettings().pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.serverSettings = settings;
this.resetForm();
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated'));
this.toastr.success(translate('toasts.server-settings-updated'));
}, (err: any) => {
console.error('error: ', err);
});
@ -171,7 +281,7 @@ export class ManageTasksSettingsComponent implements OnInit {
runAdhoc(task: AdhocTask) {
task.api.subscribe((data: any) => {
if (task.successMessage.length > 0) {
this.toastr.success(this.translocoService.translate('manage-tasks-settings.' + task.successMessage));
this.toastr.success(translate('manage-tasks-settings.' + task.successMessage));
}
if (task.successFunction) {

View file

@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map } from 'rxjs';
import {map, of} from 'rxjs';
import { environment } from 'src/environments/environment';
import { TextResonse } from '../_types/text-response';
import { ServerSettings } from './_models/server-settings';
@ -27,10 +27,6 @@ export class SettingsService {
return this.http.get<ServerSettings>(this.baseUrl + 'settings');
}
getBaseUrl() {
return this.http.get<string>(this.baseUrl + 'settings/base-url', TextResonse);
}
updateServerSettings(model: ServerSettings) {
return this.http.post<ServerSettings>(this.baseUrl + 'settings', model);
}
@ -70,4 +66,10 @@ export class SettingsService {
getOpdsEnabled() {
return this.http.get<string>(this.baseUrl + 'settings/opds-enabled', TextResonse).pipe(map(d => d === 'true'));
}
isValidCronExpression(val: string) {
if (val === '' || val === undefined || val === null) return of(false);
return this.http.get<string>(this.baseUrl + 'settings/is-valid-cron?cronExpression=' + val, TextResonse).pipe(map(d => d === 'true'));
}
}