Customized Scheduler + Saved Kavita+ Details (#2644)
This commit is contained in:
parent
2092e120c3
commit
ad74871623
76 changed files with 6076 additions and 3370 deletions
|
|
@ -28,7 +28,8 @@ export enum FilterField
|
|||
Path = 24,
|
||||
FilePath = 25,
|
||||
WantToRead = 26,
|
||||
ReadingDate = 27
|
||||
ReadingDate = 27,
|
||||
AverageRating = 28
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -64,8 +64,10 @@ export class FilterFieldPipe implements PipeTransform {
|
|||
return translate('filter-field-pipe.file-path');
|
||||
case FilterField.WantToRead:
|
||||
return translate('filter-field-pipe.want-to-read');
|
||||
case FilterField.ReadingDate:
|
||||
case FilterField.ReadingDate:
|
||||
return translate('filter-field-pipe.read-date');
|
||||
case FilterField.AverageRating:
|
||||
return translate('filter-field-pipe.average-rating');
|
||||
default:
|
||||
throw new Error(`Invalid FilterField value: ${value}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {NgbActiveModal, NgbRating} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
|
|
@ -40,8 +48,8 @@ export class ReviewSeriesModalComponent implements OnInit {
|
|||
if (model.reviewBody.length < this.minLength) {
|
||||
return;
|
||||
}
|
||||
this.seriesService.updateReview(this.review.seriesId, model.reviewBody).subscribe(() => {
|
||||
this.modal.close({success: true});
|
||||
this.seriesService.updateReview(this.review.seriesId, model.reviewBody).subscribe(review => {
|
||||
this.modal.close({success: true, review: review});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface ServerSettings {
|
|||
cacheDirectory: string;
|
||||
taskScan: string;
|
||||
taskBackup: string;
|
||||
taskCleanup: string;
|
||||
loggingLevel: string;
|
||||
port: number;
|
||||
ipAddresses: string;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,9 @@
|
|||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="sort-name" class="form-label">{{t('sort-name-label')}}</label>
|
||||
<div class="input-group {{series.sortNameLocked ? 'lock-active' : ''}}"
|
||||
[class.is-invalid]="editSeriesForm.get('sortName')?.invalid && editSeriesForm.get('sortName')?.touched">
|
||||
<div class="input-group {{series.sortNameLocked ? 'lock-active' : ''}}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: series, field: 'sortNameLocked' }"></ng-container>
|
||||
<input id="sort-name" class="form-control" formControlName="sortName" type="text">
|
||||
<input id="sort-name" class="form-control" formControlName="sortName" type="text" [class.is-invalid]="editSeriesForm.get('sortName')?.invalid && editSeriesForm.get('sortName')?.touched">
|
||||
<ng-container *ngIf="editSeriesForm.get('sortName')?.errors as errors">
|
||||
<div class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('required-field')}}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from '@angular/core';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
||||
import {FilterStatement} from '../../../_models/metadata/v2/filter-statement';
|
||||
import {BehaviorSubject, distinctUntilChanged, filter, map, Observable, of, startWith, switchMap, tap} from 'rxjs';
|
||||
import {BehaviorSubject, distinctUntilChanged, filter, map, Observable, of, startWith, switchMap} from 'rxjs';
|
||||
import {MetadataService} from 'src/app/_services/metadata.service';
|
||||
import {mangaFormatFilters} from 'src/app/_models/metadata/series-filter';
|
||||
import {PersonRole} from 'src/app/_models/metadata/person';
|
||||
|
|
@ -53,11 +53,12 @@ class FilterRowUi {
|
|||
|
||||
const unitLabels: Map<FilterField, FilterRowUi> = new Map([
|
||||
[FilterField.ReadingDate, new FilterRowUi('unit-reading-date')],
|
||||
[FilterField.AverageRating, new FilterRowUi('unit-average-rating')],
|
||||
[FilterField.ReadProgress, new FilterRowUi('unit-reading-progress')],
|
||||
]);
|
||||
|
||||
const StringFields = [FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath];
|
||||
const NumberFields = [FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress, FilterField.UserRating];
|
||||
const NumberFields = [FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress, FilterField.UserRating, FilterField.AverageRating];
|
||||
const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, FilterField.AgeRating,
|
||||
FilterField.Translators, FilterField.Characters, FilterField.Publisher,
|
||||
FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer,
|
||||
|
|
@ -233,7 +234,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
} else if (BooleanFields.includes(this.preset.field)) {
|
||||
this.formGroup.get('filterValue')?.patchValue(val);
|
||||
} else if (DateFields.includes(this.preset.field)) {
|
||||
this.formGroup.get('filterValue')?.patchValue(this.dateParser.parse(val)); // TODO: Figure out how this works
|
||||
this.formGroup.get('filterValue')?.patchValue(this.dateParser.parse(val));
|
||||
}
|
||||
else if (DropdownFields.includes(this.preset.field)) {
|
||||
if (this.MultipleDropdownAllowed || val.includes(',')) {
|
||||
|
|
@ -332,7 +333,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
this.predicateType$.next(PredicateType.Number);
|
||||
if (this.loaded) {
|
||||
this.formGroup.get('filterValue')?.patchValue(0);
|
||||
this.formGroup.get('comparison')?.patchValue(NumberFields[0]);
|
||||
this.formGroup.get('comparison')?.patchValue(NumberComparisons[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -380,11 +381,9 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
|
||||
|
||||
onDateSelect(event: NgbDate) {
|
||||
console.log('date selected: ', event);
|
||||
this.propagateFilterUpdate();
|
||||
}
|
||||
updateIfDateFilled() {
|
||||
console.log('date inputted: ', this.formGroup.get('filterValue')?.value);
|
||||
this.propagateFilterUpdate();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,14 +13,12 @@ import {Rating} from "../../../_models/rating";
|
|||
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
||||
import {NgbPopover, NgbRating} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {LoadingComponent} from "../../../shared/loading/loading.component";
|
||||
import {AccountService} from "../../../_services/account.service";
|
||||
import {LibraryType} from "../../../_models/library/library";
|
||||
import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe";
|
||||
import {NgxStarsModule} from "ngx-stars";
|
||||
import {ThemeService} from "../../../_services/theme.service";
|
||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||
import {ImageComponent} from "../../../shared/image/image.component";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
|
||||
@Component({
|
||||
selector: 'app-external-rating',
|
||||
|
|
@ -35,7 +33,6 @@ export class ExternalRatingComponent implements OnInit {
|
|||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly themeService = inject(ThemeService);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
public readonly destroyRef = inject(DestroyRef);
|
||||
|
|
@ -45,30 +42,14 @@ export class ExternalRatingComponent implements OnInit {
|
|||
@Input({required: true}) userRating!: number;
|
||||
@Input({required: true}) hasUserRated!: boolean;
|
||||
@Input({required: true}) libraryType!: LibraryType;
|
||||
@Input({required: true}) ratings: Array<Rating> = [];
|
||||
|
||||
|
||||
ratings: Array<Rating> = [];
|
||||
isLoading: boolean = false;
|
||||
overallRating: number = -1;
|
||||
starColor = this.themeService.getCssVariable('--rating-star-color');
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
this.seriesService.getOverallRating(this.seriesId).subscribe(r => this.overallRating = r.averageScore);
|
||||
|
||||
this.accountService.hasValidLicense$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((res) => {
|
||||
if (!res) return;
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.seriesService.getRatings(this.seriesId).subscribe(res => {
|
||||
this.ratings = res;
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
}, () => {
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateRating(rating: number) {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@
|
|||
@if (seriesMetadata) {
|
||||
<div class="mt-2">
|
||||
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [readingLists]="readingLists" [series]="series"
|
||||
[libraryType]="libraryType"
|
||||
[libraryType]="libraryType" [ratings]="ratings"
|
||||
[hasReadingProgress]="hasReadingProgress"></app-series-metadata-detail>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import {
|
|||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {catchError, forkJoin, Observable, of} from 'rxjs';
|
||||
import {filter, map, take} from 'rxjs/operators';
|
||||
import {filter, map, take, tap} from 'rxjs/operators';
|
||||
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
|
||||
import {CardDetailDrawerComponent} from 'src/app/cards/card-detail-drawer/card-detail-drawer.component';
|
||||
import {EditSeriesModalComponent} from 'src/app/cards/_modals/edit-series-modal/edit-series-modal.component';
|
||||
|
|
@ -107,6 +107,7 @@ import {NextExpectedChapter} from "../../../_models/series-detail/next-expected-
|
|||
import {NextExpectedCardComponent} from "../../../cards/next-expected-card/next-expected-card.component";
|
||||
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
||||
import {MetadataService} from "../../../_services/metadata.service";
|
||||
import {Rating} from "../../../_models/rating";
|
||||
|
||||
interface RelatedSeriesPair {
|
||||
series: Series;
|
||||
|
|
@ -203,6 +204,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
activeTabId = TabID.Storyline;
|
||||
|
||||
reviews: Array<UserReview> = [];
|
||||
ratings: Array<Rating> = [];
|
||||
libraryType: LibraryType = LibraryType.Manga;
|
||||
seriesMetadata: SeriesMetadata | null = null;
|
||||
readingLists: Array<ReadingList> = [];
|
||||
|
|
@ -699,6 +701,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
|
||||
// Reviews
|
||||
this.reviews = [...data.reviews];
|
||||
this.ratings = [...data.ratings];
|
||||
|
||||
// Recommendations
|
||||
data.recommendations.ownedSeries.map(r => {
|
||||
|
|
@ -843,6 +846,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
const userReview = this.getUserReview();
|
||||
|
||||
const modalRef = this.modalService.open(ReviewSeriesModalComponent, { scrollable: true, size: 'lg' });
|
||||
modalRef.componentInstance.series = this.series;
|
||||
if (userReview.length > 0) {
|
||||
modalRef.componentInstance.review = userReview[0];
|
||||
} else {
|
||||
|
|
@ -852,12 +856,18 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
body: ''
|
||||
};
|
||||
}
|
||||
modalRef.componentInstance.series = this.series;
|
||||
modalRef.closed.subscribe((closeResult: {success: boolean}) => {
|
||||
if (closeResult.success) {
|
||||
this.loadReviews(); // TODO: Ensure reviews get updated here
|
||||
|
||||
modalRef.closed.subscribe((closeResult) => {
|
||||
// BUG: This never executes!
|
||||
console.log('Close Result: ')
|
||||
if (closeResult.success && closeResult.review !== null) {
|
||||
const index = this.reviews.findIndex(r => r.username === closeResult.review!.username);
|
||||
console.log('update index: ', index, ' with review ', closeResult.review);
|
||||
this.reviews[index] = closeResult.review;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" [heading]="t('rating-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-external-rating [seriesId]="series.id" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
|
||||
<app-external-rating [seriesId]="series.id" [ratings]="ratings" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {ReaderService} from 'src/app/_services/reader.service';
|
||||
import {TagBadgeComponent, TagBadgeCursor} from '../../../shared/tag-badge/tag-badge.component';
|
||||
import {FilterUtilitiesService} from '../../../shared/_services/filter-utilities.service';
|
||||
import {Breakpoint, UtilityService} from '../../../shared/_services/utility.service';
|
||||
|
|
@ -33,6 +32,7 @@ import {TranslocoDirective} from "@ngneat/transloco";
|
|||
import {FilterField} from "../../../_models/metadata/v2/filter-field";
|
||||
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
|
||||
import {ImageComponent} from "../../../shared/image/image.component";
|
||||
import {Rating} from "../../../_models/rating";
|
||||
|
||||
|
||||
@Component({
|
||||
|
|
@ -68,6 +68,7 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
|||
*/
|
||||
@Input() readingLists: Array<ReadingList> = [];
|
||||
@Input({required: true}) series!: Series;
|
||||
@Input({required: true}) ratings: Array<Rating> = [];
|
||||
|
||||
isCollapsed: boolean = true;
|
||||
hasExtendedProperties: boolean = false;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export class ConfirmService {
|
|||
config.content = content;
|
||||
}
|
||||
|
||||
const modalRef = this.modalService.open(ConfirmDialogComponent);
|
||||
const modalRef = this.modalService.open(ConfirmDialogComponent, {size: "lg", fullscreen: "md"});
|
||||
modalRef.componentInstance.config = config;
|
||||
modalRef.closed.pipe(take(1)).subscribe(result => {
|
||||
return resolve(result);
|
||||
|
|
|
|||
|
|
@ -1080,14 +1080,14 @@
|
|||
"host-name-validation": "Host name must start with http(s) and not end in /",
|
||||
|
||||
"sender-address-label": "Sender Address",
|
||||
"sender-address-tooltip": "",
|
||||
"sender-address-tooltip": "This is the email address from which the receiver will see when they receive the email. Typically the email address associated with the account.",
|
||||
"sender-displayname-label": "Display Name",
|
||||
"sender-displayname-tooltip": "",
|
||||
"sender-displayname-tooltip": "The name from which the receiver will see when they receive the email",
|
||||
"host-label": "Host",
|
||||
"host-tooltip": "",
|
||||
"host-tooltip": "Outgoing/SMTP address of your email server",
|
||||
"port-label": "Port",
|
||||
"port-tooltip": "",
|
||||
"username-label": "Username",
|
||||
"username-tooltip": "The username used to authenticate against the host",
|
||||
"password-label": "Password",
|
||||
"enable-ssl-label": "Use SSL on Email Server",
|
||||
"size-limit-label": "Size Limit",
|
||||
|
|
@ -1215,7 +1215,8 @@
|
|||
"discord-title": "Discord:",
|
||||
"donations-title": "Donations:",
|
||||
"source-title": "Source:",
|
||||
"feature-request-title": "Feature Requests"
|
||||
"feature-request-title": "Feature Requests:",
|
||||
"localization-title": "Localizations:"
|
||||
},
|
||||
|
||||
"manage-tasks-settings": {
|
||||
|
|
@ -1224,6 +1225,8 @@
|
|||
"library-scan-tooltip": "How often Kavita will scan and refresh metadata around library files.",
|
||||
"library-database-backup-label": "Library Database Backup",
|
||||
"library-database-backup-tooltip": "How often Kavita will backup the database.",
|
||||
"cleanup-label": "Cleanup",
|
||||
"cleanup-tooltip": "How often Kavita will run cleanup tasks. This can be heavy and should be performed at midnight in most cases",
|
||||
"adhoc-tasks-title": "Ad-hoc Tasks",
|
||||
"job-title-header": "Job Title",
|
||||
"description-header": "Description",
|
||||
|
|
@ -1231,11 +1234,20 @@
|
|||
"reset-to-default": "{{common.reset-to-default}}",
|
||||
"reset": "{{common.reset}}",
|
||||
"save": "{{common.save}}",
|
||||
"required": "{{validation.required-field}}",
|
||||
|
||||
"custom-label": "Custom Schedule (Cron Notation)",
|
||||
"cron-notation": "You must use cron notation for custom scheduling",
|
||||
|
||||
"recurring-tasks-title": "{{title}}",
|
||||
"last-executed-header": "Last Executed",
|
||||
"cron-header": "Cron",
|
||||
|
||||
"disabled": "Disabled",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"custom": "Custom",
|
||||
|
||||
|
||||
"convert-media-task": "Convert Media to Target Encoding",
|
||||
"convert-media-task-desc": "Runs a long-running task which will convert all kavita-managed media to the target encoding. This is slow (especially on ARM devices).",
|
||||
|
|
@ -1617,6 +1629,7 @@
|
|||
|
||||
"metadata-filter-row": {
|
||||
"unit-reading-date": "Date",
|
||||
"unit-average-rating": "Average Rating (Kavita+) - only for cached series",
|
||||
"unit-reading-progress": "Percent"
|
||||
},
|
||||
|
||||
|
|
@ -1917,7 +1930,8 @@
|
|||
"path": "Path",
|
||||
"file-path": "File Path",
|
||||
"want-to-read": "Want to Read",
|
||||
"read-date": "Reading Date"
|
||||
"read-date": "Reading Date",
|
||||
"average-rating": "Average Rating"
|
||||
},
|
||||
|
||||
"filter-comparison-pipe": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue