Ability to turn off Metadata Parsing (#3872)
This commit is contained in:
parent
fa8d778c8d
commit
36aa5f5c85
63 changed files with 4257 additions and 186 deletions
|
|
@ -5,6 +5,11 @@
|
|||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
transition: transform 0.2s ease, background 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&.not-selectable:hover {
|
||||
cursor: not-allowed;
|
||||
background-color: var(--bs-card-color, #2c2c2c) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-card:hover {
|
||||
|
|
|
|||
120
UI/Web/src/app/_helpers/form-debug.ts
Normal file
120
UI/Web/src/app/_helpers/form-debug.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import {AbstractControl, FormArray, FormControl, FormGroup} from '@angular/forms';
|
||||
|
||||
interface ValidationIssue {
|
||||
path: string;
|
||||
controlType: string;
|
||||
value: any;
|
||||
errors: { [key: string]: any } | null;
|
||||
status: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export function analyzeFormGroupValidation(formGroup: FormGroup, basePath: string = ''): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
function analyzeControl(control: AbstractControl, path: string): void {
|
||||
// Determine control type for better debugging
|
||||
let controlType = 'AbstractControl';
|
||||
if (control instanceof FormGroup) {
|
||||
controlType = 'FormGroup';
|
||||
} else if (control instanceof FormArray) {
|
||||
controlType = 'FormArray';
|
||||
} else if (control instanceof FormControl) {
|
||||
controlType = 'FormControl';
|
||||
}
|
||||
|
||||
// Add issue if control has validation errors or is invalid
|
||||
if (control.invalid || control.errors || control.disabled) {
|
||||
issues.push({
|
||||
path: path || 'root',
|
||||
controlType,
|
||||
value: control.value,
|
||||
errors: control.errors,
|
||||
status: control.status,
|
||||
disabled: control.disabled
|
||||
});
|
||||
}
|
||||
|
||||
// Recursively check nested controls
|
||||
if (control instanceof FormGroup) {
|
||||
Object.keys(control.controls).forEach(key => {
|
||||
const childPath = path ? `${path}.${key}` : key;
|
||||
analyzeControl(control.controls[key], childPath);
|
||||
});
|
||||
} else if (control instanceof FormArray) {
|
||||
control.controls.forEach((childControl, index) => {
|
||||
const childPath = path ? `${path}[${index}]` : `[${index}]`;
|
||||
analyzeControl(childControl, childPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
analyzeControl(formGroup, basePath);
|
||||
return issues;
|
||||
}
|
||||
|
||||
export function printFormGroupValidation(formGroup: FormGroup, basePath: string = ''): void {
|
||||
const issues = analyzeFormGroupValidation(formGroup, basePath);
|
||||
|
||||
console.group(`🔍 FormGroup Validation Analysis (${basePath || 'root'})`);
|
||||
console.log(`Overall Status: ${formGroup.status}`);
|
||||
console.log(`Overall Valid: ${formGroup.valid}`);
|
||||
console.log(`Total Issues Found: ${issues.length}`);
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log('✅ No validation issues found!');
|
||||
} else {
|
||||
console.log('\n📋 Detailed Issues:');
|
||||
issues.forEach((issue, index) => {
|
||||
console.group(`${index + 1}. ${issue.path} (${issue.controlType})`);
|
||||
console.log(`Status: ${issue.status}`);
|
||||
console.log(`Value:`, issue.value);
|
||||
console.log(`Disabled: ${issue.disabled}`);
|
||||
|
||||
if (issue.errors) {
|
||||
console.log('Validation Errors:');
|
||||
Object.entries(issue.errors).forEach(([errorKey, errorValue]) => {
|
||||
console.log(` • ${errorKey}:`, errorValue);
|
||||
});
|
||||
} else {
|
||||
console.log('No specific validation errors (but control is invalid)');
|
||||
}
|
||||
console.groupEnd();
|
||||
});
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
// Alternative function that returns a formatted string instead of console logging
|
||||
export function getFormGroupValidationReport(formGroup: FormGroup, basePath: string = ''): string {
|
||||
const issues = analyzeFormGroupValidation(formGroup, basePath);
|
||||
|
||||
let report = `FormGroup Validation Report (${basePath || 'root'})\n`;
|
||||
report += `Overall Status: ${formGroup.status}\n`;
|
||||
report += `Overall Valid: ${formGroup.valid}\n`;
|
||||
report += `Total Issues Found: ${issues.length}\n\n`;
|
||||
|
||||
if (issues.length === 0) {
|
||||
report += '✅ No validation issues found!';
|
||||
} else {
|
||||
report += 'Detailed Issues:\n';
|
||||
issues.forEach((issue, index) => {
|
||||
report += `\n${index + 1}. ${issue.path} (${issue.controlType})\n`;
|
||||
report += ` Status: ${issue.status}\n`;
|
||||
report += ` Value: ${JSON.stringify(issue.value)}\n`;
|
||||
report += ` Disabled: ${issue.disabled}\n`;
|
||||
|
||||
if (issue.errors) {
|
||||
report += ' Validation Errors:\n';
|
||||
Object.entries(issue.errors).forEach(([errorKey, errorValue]) => {
|
||||
report += ` • ${errorKey}: ${JSON.stringify(errorValue)}\n`;
|
||||
});
|
||||
} else {
|
||||
report += ' No specific validation errors (but control is invalid)\n';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export interface ExternalMatchRateLimitErrorEvent {
|
||||
seriesId: number;
|
||||
seriesName: string;
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ export interface Library {
|
|||
manageReadingLists: boolean;
|
||||
allowScrobbling: boolean;
|
||||
allowMetadataMatching: boolean;
|
||||
enableMetadata: boolean;
|
||||
collapseSeriesRelationships: boolean;
|
||||
libraryFileTypes: Array<FileTypeGroup>;
|
||||
excludePatterns: Array<string>;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
||||
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { LibraryModifiedEvent } from '../_models/events/library-modified-event';
|
||||
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
||||
import { ThemeProgressEvent } from '../_models/events/theme-progress-event';
|
||||
import { UserUpdateEvent } from '../_models/events/user-update-event';
|
||||
import { User } from '../_models/user';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HubConnection, HubConnectionBuilder} from '@microsoft/signalr';
|
||||
import {BehaviorSubject, ReplaySubject} from 'rxjs';
|
||||
import {environment} from 'src/environments/environment';
|
||||
import {LibraryModifiedEvent} from '../_models/events/library-modified-event';
|
||||
import {NotificationProgressEvent} from '../_models/events/notification-progress-event';
|
||||
import {ThemeProgressEvent} from '../_models/events/theme-progress-event';
|
||||
import {UserUpdateEvent} from '../_models/events/user-update-event';
|
||||
import {User} from '../_models/user';
|
||||
import {DashboardUpdateEvent} from "../_models/events/dashboard-update-event";
|
||||
import {SideNavUpdateEvent} from "../_models/events/sidenav-update-event";
|
||||
import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event";
|
||||
import {ExternalMatchRateLimitErrorEvent} from "../_models/events/external-match-rate-limit-error-event";
|
||||
|
||||
export enum EVENTS {
|
||||
UpdateAvailable = 'UpdateAvailable',
|
||||
|
|
@ -114,6 +115,10 @@ export enum EVENTS {
|
|||
* A Person merged has been merged into another
|
||||
*/
|
||||
PersonMerged = 'PersonMerged',
|
||||
/**
|
||||
* A Rate limit error was hit when matching a series with Kavita+
|
||||
*/
|
||||
ExternalMatchRateLimitError = 'ExternalMatchRateLimitError'
|
||||
}
|
||||
|
||||
export interface Message<T> {
|
||||
|
|
@ -236,6 +241,13 @@ export class MessageHubService {
|
|||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.ExternalMatchRateLimitError, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.ExternalMatchRateLimitError,
|
||||
payload: resp.body as ExternalMatchRateLimitErrorEvent
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.NotificationProgress, (resp: NotificationProgressEvent) => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.NotificationProgress,
|
||||
|
|
|
|||
|
|
@ -266,13 +266,13 @@ export class ReaderService {
|
|||
|
||||
|
||||
getQueryParamsObject(incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) {
|
||||
let params: {[key: string]: any} = {};
|
||||
if (incognitoMode) {
|
||||
params['incognitoMode'] = true;
|
||||
}
|
||||
const params: {[key: string]: any} = {};
|
||||
params['incognitoMode'] = incognitoMode;
|
||||
|
||||
if (readingListMode) {
|
||||
params['readingListId'] = readingListId;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {LicenseService} from "../../_services/license.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {Series} from "../../_models/series";
|
||||
|
|
@ -23,6 +23,8 @@ import {EVENTS, MessageHubService} from "../../_services/message-hub.service";
|
|||
import {ScanSeriesEvent} from "../../_models/events/scan-series-event";
|
||||
import {LibraryTypePipe} from "../../_pipes/library-type.pipe";
|
||||
import {allKavitaPlusMetadataApplicableTypes} from "../../_models/library/library";
|
||||
import {ExternalMatchRateLimitErrorEvent} from "../../_models/events/external-match-rate-limit-error-event";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-matched-metadata',
|
||||
|
|
@ -55,6 +57,7 @@ export class ManageMatchedMetadataComponent implements OnInit {
|
|||
private readonly manageService = inject(ManageService);
|
||||
private readonly messageHub = inject(MessageHubService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
protected readonly imageService = inject(ImageService);
|
||||
|
||||
|
||||
|
|
@ -74,12 +77,19 @@ export class ManageMatchedMetadataComponent implements OnInit {
|
|||
}
|
||||
|
||||
this.messageHub.messages$.subscribe(message => {
|
||||
if (message.event !== EVENTS.ScanSeries) return;
|
||||
|
||||
const evt = message.payload as ScanSeriesEvent;
|
||||
if (this.data.filter(d => d.series.id === evt.seriesId).length > 0) {
|
||||
this.loadData();
|
||||
if (message.event == EVENTS.ScanSeries) {
|
||||
const evt = message.payload as ScanSeriesEvent;
|
||||
if (this.data.filter(d => d.series.id === evt.seriesId).length > 0) {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
if (message.event == EVENTS.ExternalMatchRateLimitError) {
|
||||
const evt = message.payload as ExternalMatchRateLimitErrorEvent;
|
||||
this.toastr.error(translate('toasts.external-match-rate-error', {seriesName: evt.seriesName}))
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
this.filterGroup.valueChanges.pipe(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
|
||||
<div class="tag-card" (click)="openFilter(FilterField.Genres, item.id)">
|
||||
<div class="tag-card" [ngClass]="{'not-selectable': item.seriesCount === 0}" (click)="openFilter(FilterField.Genres, item)">
|
||||
<div class="tag-name">{{ item.title }}</div>
|
||||
<div class="tag-meta">
|
||||
<span>{{t('series-count', {num: item.seriesCount | compactNumber})}}</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, inject, OnInit} from '@angular/core';
|
||||
import {CardDetailLayoutComponent} from "../../cards/card-detail-layout/card-detail-layout.component";
|
||||
import {DecimalPipe} from "@angular/common";
|
||||
import {DecimalPipe, NgClass} from "@angular/common";
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
} from "../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
|
||||
|
|
@ -24,7 +24,8 @@ import {Title} from "@angular/platform-browser";
|
|||
DecimalPipe,
|
||||
SideNavCompanionBarComponent,
|
||||
TranslocoDirective,
|
||||
CompactNumberPipe
|
||||
CompactNumberPipe,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './browse-genres.component.html',
|
||||
styleUrl: './browse-genres.component.scss',
|
||||
|
|
@ -62,7 +63,8 @@ export class BrowseGenresComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
openFilter(field: FilterField, value: string | number) {
|
||||
this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe();
|
||||
openFilter(field: FilterField, genre: BrowseGenre) {
|
||||
if (genre.seriesCount === 0) return; // We don't yet have an issue page
|
||||
this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${genre.id}`).subscribe();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
|
||||
<div class="tag-card" (click)="openFilter(FilterField.Tags, item.id)">
|
||||
<div class="tag-card" [ngClass]="{'not-selectable': item.seriesCount === 0}" (click)="openFilter(FilterField.Tags, item)">
|
||||
<div class="tag-name">{{ item.title }}</div>
|
||||
<div class="tag-meta">
|
||||
<span>{{t('series-count', {num: item.seriesCount | compactNumber})}}</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, inject, OnInit} from '@angular/core';
|
||||
import {CardDetailLayoutComponent} from "../../cards/card-detail-layout/card-detail-layout.component";
|
||||
import {DecimalPipe} from "@angular/common";
|
||||
import {DecimalPipe, NgClass} from "@angular/common";
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
} from "../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
|
||||
|
|
@ -25,7 +25,8 @@ import {Title} from "@angular/platform-browser";
|
|||
DecimalPipe,
|
||||
SideNavCompanionBarComponent,
|
||||
TranslocoDirective,
|
||||
CompactNumberPipe
|
||||
CompactNumberPipe,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './browse-tags.component.html',
|
||||
styleUrl: './browse-tags.component.scss',
|
||||
|
|
@ -61,7 +62,8 @@ export class BrowseTagsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
openFilter(field: FilterField, value: string | number) {
|
||||
this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe();
|
||||
openFilter(field: FilterField, tag: BrowseTag) {
|
||||
if (tag.seriesCount === 0) return; // We don't yet have an issue page
|
||||
this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${tag.id}`).subscribe();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,14 +229,17 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
|
||||
[disabled]="!(formGroup.get('edit')?.value || false)" [showRemoveButton]="formGroup.get('edit')?.value || false">
|
||||
<app-draggable-ordered-list [items]="items" [accessibilityMode]="accessibilityMode"
|
||||
[disabled]="!(formGroup.get('edit')?.value || false)"
|
||||
(orderUpdated)="orderUpdated($event)"
|
||||
(itemRemove)="removeItem($event)"
|
||||
[showRemoveButton]="formGroup.get('edit')?.value || false">
|
||||
|
||||
<ng-template #draggableItem let-item let-position="idx">
|
||||
<app-reading-list-item [ngClass]="{'content-container': items.length < 100, 'non-virtualized-container': items.length >= 100}" [item]="item"
|
||||
[position]="position" [libraryTypes]="libraryTypes"
|
||||
[promoted]="item.promoted" (read)="readChapter($event)"
|
||||
(remove)="itemRemoved($event, position)"
|
||||
(remove)="removeItem($event)"
|
||||
[showRemove]="false"/>
|
||||
</ng-template>
|
||||
</app-draggable-ordered-list>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ import {ImageService} from 'src/app/_services/image.service';
|
|||
import {ReadingListService} from 'src/app/_services/reading-list.service';
|
||||
import {
|
||||
DraggableOrderedListComponent,
|
||||
IndexUpdateEvent
|
||||
IndexUpdateEvent,
|
||||
ItemRemoveEvent
|
||||
} from '../draggable-ordered-list/draggable-ordered-list.component';
|
||||
import {forkJoin, startWith, tap} from 'rxjs';
|
||||
import {ReaderService} from 'src/app/_services/reader.service';
|
||||
|
|
@ -321,6 +322,7 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
}
|
||||
|
||||
editReadingList(readingList: ReadingList) {
|
||||
if (!readingList) return;
|
||||
this.actionService.editReadingList(readingList, (readingList: ReadingList) => {
|
||||
// Reload information around list
|
||||
this.readingListService.getReadingList(this.listId).subscribe(rl => {
|
||||
|
|
@ -347,10 +349,10 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
itemRemoved(item: ReadingListItem, position: number) {
|
||||
removeItem(removeEvent: ItemRemoveEvent) {
|
||||
if (!this.readingList) return;
|
||||
this.readingListService.deleteItem(this.readingList.id, item.id).subscribe(() => {
|
||||
this.items.splice(position, 1);
|
||||
this.readingListService.deleteItem(this.readingList.id, removeEvent.item.id).subscribe(() => {
|
||||
this.items.splice(removeEvent.position, 1);
|
||||
this.items = [...this.items];
|
||||
this.cdRef.markForCheck();
|
||||
this.toastr.success(translate('toasts.item-removed'));
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@
|
|||
{{item.title}}
|
||||
<div class="actions float-end">
|
||||
@if (showRemove) {
|
||||
<button class="btn btn-danger" (click)="remove.emit(item)">
|
||||
<span>
|
||||
<i class="fa fa-trash me-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<button class="btn btn-danger" (click)="removeItem(item)">
|
||||
<span>
|
||||
<i class="fa fa-trash me-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="d-none d-md-inline-block">{{t('remove')}}</span>
|
||||
</button>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {ImageComponent} from '../../../shared/image/image.component';
|
|||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component";
|
||||
import {ReadMoreComponent} from "../../../shared/read-more/read-more.component";
|
||||
import {ItemRemoveEvent} from "../draggable-ordered-list/draggable-ordered-list.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-reading-list-item',
|
||||
|
|
@ -33,9 +34,16 @@ export class ReadingListItemComponent {
|
|||
@Input() promoted: boolean = false;
|
||||
|
||||
@Output() read: EventEmitter<ReadingListItem> = new EventEmitter();
|
||||
@Output() remove: EventEmitter<ReadingListItem> = new EventEmitter();
|
||||
@Output() remove: EventEmitter<ItemRemoveEvent> = new EventEmitter();
|
||||
|
||||
readChapter(item: ReadingListItem) {
|
||||
this.read.emit(item);
|
||||
}
|
||||
|
||||
removeItem(item: ReadingListItem) {
|
||||
this.remove.emit({
|
||||
item: item,
|
||||
position: item.order
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ export class ExternalRatingComponent implements OnInit {
|
|||
ngOnInit() {
|
||||
this.reviewService.overallRating(this.seriesId, this.chapterId).subscribe(r => {
|
||||
this.overallRating = r.averageScore;
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
updateRating(rating: number) {
|
||||
|
|
@ -92,6 +93,4 @@ export class ExternalRatingComponent implements OnInit {
|
|||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected readonly RatingAuthority = RatingAuthority;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,16 @@
|
|||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('enable-metadata-label')" [subtitle]="t('enable-metadata-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input type="checkbox" id="enable-metadata" role="switch" formControlName="enableMetadata" class="form-check-input">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('manage-collection-label')" [subtitle]="t('manage-collection-tooltip')">
|
||||
<ng-template #switch>
|
||||
|
|
|
|||
|
|
@ -105,15 +105,16 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
libraryForm: FormGroup = new FormGroup({
|
||||
name: new FormControl<string>('', { nonNullable: true, validators: [Validators.required] }),
|
||||
type: new FormControl<LibraryType>(LibraryType.Manga, { nonNullable: true, validators: [Validators.required] }),
|
||||
folderWatching: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
|
||||
includeInDashboard: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
|
||||
includeInRecommended: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
|
||||
includeInSearch: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
|
||||
manageCollections: new FormControl<boolean>(false, { nonNullable: true, validators: [Validators.required] }),
|
||||
manageReadingLists: new FormControl<boolean>(false, { nonNullable: true, validators: [Validators.required] }),
|
||||
allowScrobbling: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
|
||||
allowMetadataMatching: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
|
||||
collapseSeriesRelationships: new FormControl<boolean>(false, { nonNullable: true, validators: [Validators.required] }),
|
||||
folderWatching: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
|
||||
includeInDashboard: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
|
||||
includeInRecommended: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
|
||||
includeInSearch: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
|
||||
manageCollections: new FormControl<boolean>(false, { nonNullable: true, validators: [] }),
|
||||
manageReadingLists: new FormControl<boolean>(false, { nonNullable: true, validators: [] }),
|
||||
allowScrobbling: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
|
||||
allowMetadataMatching: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
|
||||
collapseSeriesRelationships: new FormControl<boolean>(false, { nonNullable: true, validators: [] }),
|
||||
enableMetadata: new FormControl<boolean>(true, { nonNullable: true, validators: [] }), // required validator doesn't check value, just if true
|
||||
});
|
||||
|
||||
selectedFolders: string[] = [];
|
||||
|
|
@ -155,7 +156,7 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
this.libraryForm.get('allowScrobbling')?.disable();
|
||||
|
||||
if (this.IsMetadataDownloadEligible) {
|
||||
this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching);
|
||||
this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching ?? true);
|
||||
this.libraryForm.get('allowMetadataMatching')?.enable();
|
||||
} else {
|
||||
this.libraryForm.get('allowMetadataMatching')?.setValue(false);
|
||||
|
|
@ -184,6 +185,20 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
|
||||
this.setValues();
|
||||
|
||||
// Turn on/off manage collections/rl
|
||||
this.libraryForm.get('enableMetadata')?.valueChanges.pipe(
|
||||
tap(enabled => {
|
||||
const manageCollectionsFc = this.libraryForm.get('manageCollections');
|
||||
const manageReadingListsFc = this.libraryForm.get('manageReadingLists');
|
||||
|
||||
manageCollectionsFc?.setValue(enabled);
|
||||
manageReadingListsFc?.setValue(enabled);
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
|
||||
// This needs to only apply after first render
|
||||
this.libraryForm.get('type')?.valueChanges.pipe(
|
||||
tap((type: LibraryType) => {
|
||||
|
|
@ -257,6 +272,8 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
this.libraryForm.get('collapseSeriesRelationships')?.setValue(this.library.collapseSeriesRelationships);
|
||||
this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false);
|
||||
this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsMetadataDownloadEligible ? this.library.allowMetadataMatching : false);
|
||||
this.libraryForm.get('excludePatterns')?.setValue(this.excludePatterns ? this.library.excludePatterns : false);
|
||||
this.libraryForm.get('enableMetadata')?.setValue(this.library.enableMetadata, true);
|
||||
this.selectedFolders = this.library.folders;
|
||||
|
||||
this.madeChanges = false;
|
||||
|
|
|
|||
|
|
@ -1129,6 +1129,8 @@
|
|||
"include-in-dashboard-tooltip": "Should series from the library be included on the Dashboard. This affects all streams, like On Deck, Recently Updated, Recently Added, or any custom additions.",
|
||||
"include-in-search-label": "Include in Search",
|
||||
"include-in-search-tooltip": "Should series and any derived information (genres, people, files) from the library be included in search results.",
|
||||
"enable-metadata-label": "Enable Metadata (ComicInfo/Epub/PDF)",
|
||||
"enable-metadata-tooltip": "Allow Kavita to read metadata files which override filename parsing.",
|
||||
"force-scan": "Force Scan",
|
||||
"force-scan-tooltip": "This will force a scan on the library, treating like a fresh scan",
|
||||
"reset": "{{common.reset}}",
|
||||
|
|
@ -2743,7 +2745,8 @@
|
|||
"webtoon-override": "Switching to Webtoon mode due to images representing a webtoon.",
|
||||
"scrobble-gen-init": "Enqueued a job to generate scrobble events from past reading history and ratings, syncing them with connected services.",
|
||||
"series-bound-to-reading-profile": "Series bound to Reading Profile {{name}}",
|
||||
"library-bound-to-reading-profile": "Library bound to Reading Profile {{name}}"
|
||||
"library-bound-to-reading-profile": "Library bound to Reading Profile {{name}}",
|
||||
"external-match-rate-error": "Kavita ran out of rate looking up {{seriesName}}. Try again in 5 minutes."
|
||||
},
|
||||
|
||||
"read-time-pipe": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue