More Metadata Stuff (#3537)
This commit is contained in:
parent
8d3dcc637e
commit
53b13da0c9
34 changed files with 4123 additions and 129 deletions
16
UI/Web/src/app/_pipes/library-name.pipe.ts
Normal file
16
UI/Web/src/app/_pipes/library-name.pipe.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import {inject, Pipe, PipeTransform} from '@angular/core';
|
||||
import {LibraryService} from "../_services/library.service";
|
||||
import {Observable} from "rxjs";
|
||||
|
||||
@Pipe({
|
||||
name: 'libraryName',
|
||||
standalone: true
|
||||
})
|
||||
export class LibraryNamePipe implements PipeTransform {
|
||||
private readonly libraryService = inject(LibraryService);
|
||||
|
||||
transform(libraryId: number): Observable<string> {
|
||||
return this.libraryService.getLibraryName(libraryId);
|
||||
}
|
||||
|
||||
}
|
||||
35
UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts
Normal file
35
UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {MetadataSettingField} from "../admin/_models/metadata-setting-field";
|
||||
import {translate} from "@jsverse/transloco";
|
||||
|
||||
@Pipe({
|
||||
name: 'metadataSettingFiled',
|
||||
standalone: true
|
||||
})
|
||||
export class MetadataSettingFiledPipe implements PipeTransform {
|
||||
|
||||
transform(value: MetadataSettingField): string {
|
||||
switch (value) {
|
||||
case MetadataSettingField.AgeRating:
|
||||
return translate('metadata-setting-field-pipe.age-rating');
|
||||
case MetadataSettingField.People:
|
||||
return translate('metadata-setting-field-pipe.people');
|
||||
case MetadataSettingField.Covers:
|
||||
return translate('metadata-setting-field-pipe.covers');
|
||||
case MetadataSettingField.Summary:
|
||||
return translate('metadata-setting-field-pipe.summary');
|
||||
case MetadataSettingField.PublicationStatus:
|
||||
return translate('metadata-setting-field-pipe.publication-status');
|
||||
case MetadataSettingField.StartDate:
|
||||
return translate('metadata-setting-field-pipe.start-date');
|
||||
case MetadataSettingField.Genres:
|
||||
return translate('metadata-setting-field-pipe.genres');
|
||||
case MetadataSettingField.Tags:
|
||||
return translate('metadata-setting-field-pipe.tags');
|
||||
case MetadataSettingField.LocalizedName:
|
||||
return translate('metadata-setting-field-pipe.localized-name');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
16
UI/Web/src/app/admin/_models/metadata-setting-field.ts
Normal file
16
UI/Web/src/app/admin/_models/metadata-setting-field.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export enum MetadataSettingField {
|
||||
Summary = 1,
|
||||
PublicationStatus = 2,
|
||||
StartDate = 3,
|
||||
Genres = 4,
|
||||
Tags = 5,
|
||||
LocalizedName = 6,
|
||||
Covers = 7,
|
||||
AgeRating = 8,
|
||||
People = 9
|
||||
}
|
||||
|
||||
export const allMetadataSettingField = Object.keys(MetadataSettingField)
|
||||
.filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0)
|
||||
.map(key => parseInt(key, 10)) as MetadataSettingField[];
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import {AgeRating} from "../../_models/metadata/age-rating";
|
||||
import {PersonRole} from "../../_models/metadata/person";
|
||||
import {MetadataSettingField} from "./metadata-setting-field";
|
||||
|
||||
export enum MetadataFieldType {
|
||||
Genre = 0,
|
||||
|
|
@ -22,6 +23,7 @@ export interface MetadataSettings {
|
|||
enableRelationships: boolean;
|
||||
enablePeople: boolean;
|
||||
enableStartDate: boolean;
|
||||
enableCoverImage: boolean;
|
||||
enableLocalizedName: boolean;
|
||||
enableGenres: boolean;
|
||||
enableTags: boolean;
|
||||
|
|
@ -31,4 +33,5 @@ export interface MetadataSettings {
|
|||
blacklist: Array<string>;
|
||||
whitelist: Array<string>;
|
||||
personRoles: Array<PersonRole>;
|
||||
overrides: Array<MetadataSettingField>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
[footerHeight]="50"
|
||||
>
|
||||
|
||||
<ngx-datatable-column name="lastModifiedUtc" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="3">
|
||||
<ngx-datatable-column name="series.name" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="3">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('series-name-header')}}
|
||||
</ng-template>
|
||||
|
|
@ -34,6 +34,15 @@
|
|||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
|
||||
<ngx-datatable-column name="series.libraryId" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="3">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('library-name-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" ngx-datatable-cell-template>
|
||||
{{item.series.libraryId | libraryName | async}}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
|
||||
|
||||
<ngx-datatable-column name="status" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="1">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
|
|
@ -70,9 +79,13 @@
|
|||
|
||||
<ngx-datatable-column name="" [width]="20" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="1">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('actions-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" let-idx="index" ngx-datatable-cell-template>
|
||||
<app-card-actionables [actions]="actions" (actionHandler)="performAction($event, item.series)"></app-card-actionables>
|
||||
<button class="btn btn-icon" (click)="fixMatch(item.series)">
|
||||
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('match-alt', {seriesName: item.series.name})}}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import {Router} from "@angular/router";
|
|||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
import {Series} from "../../_models/series";
|
||||
import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service";
|
||||
import {ActionService} from "../../_services/action.service";
|
||||
import {ManageService} from "../../_services/manage.service";
|
||||
import {ManageMatchSeries} from "../../_models/kavitaplus/manage-match-series";
|
||||
|
|
@ -19,46 +17,47 @@ import {MatchStateOptionPipe} from "../../_pipes/match-state.pipe";
|
|||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
import {debounceTime, distinctUntilChanged, switchMap, tap} from "rxjs";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapter";
|
||||
import {ScrobbleEventType} from "../../_models/scrobbling/scrobble-event";
|
||||
import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable";
|
||||
import {LibraryNamePipe} from "../../_pipes/library-name.pipe";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {EVENTS, MessageHubService} from "../../_services/message-hub.service";
|
||||
import {ScanSeriesEvent} from "../../_models/events/scan-series-event";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-matched-metadata',
|
||||
standalone: true,
|
||||
imports: [
|
||||
TranslocoDirective,
|
||||
ImageComponent,
|
||||
CardActionablesComponent,
|
||||
VirtualScrollerModule,
|
||||
ReactiveFormsModule,
|
||||
Select2Module,
|
||||
MatchStateOptionPipe,
|
||||
UtcToLocalTimePipe,
|
||||
DefaultValuePipe,
|
||||
NgxDatatableModule,
|
||||
],
|
||||
imports: [
|
||||
TranslocoDirective,
|
||||
ImageComponent,
|
||||
VirtualScrollerModule,
|
||||
ReactiveFormsModule,
|
||||
Select2Module,
|
||||
MatchStateOptionPipe,
|
||||
UtcToLocalTimePipe,
|
||||
DefaultValuePipe,
|
||||
NgxDatatableModule,
|
||||
LibraryNamePipe,
|
||||
AsyncPipe,
|
||||
],
|
||||
templateUrl: './manage-matched-metadata.component.html',
|
||||
styleUrl: './manage-matched-metadata.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ManageMatchedMetadataComponent implements OnInit {
|
||||
protected readonly MatchState = MatchStateOption;
|
||||
protected readonly ColumnMode = ColumnMode;
|
||||
protected readonly allMatchStates = allMatchStates.filter(m => m !== MatchStateOption.Matched); // Matched will have too many
|
||||
|
||||
private readonly licenseService = inject(LicenseService);
|
||||
private readonly actionFactory = inject(ActionFactoryService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly manageService = inject(ManageService);
|
||||
private readonly messageHub = inject(MessageHubService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
protected readonly imageService = inject(ImageService);
|
||||
|
||||
|
||||
isLoading: boolean = true;
|
||||
data: Array<ManageMatchSeries> = [];
|
||||
actions: Array<ActionItem<Series>> = this.actionFactory.getSeriesActions(this.fixMatch.bind(this))
|
||||
.filter(item => item.action === Action.Match);
|
||||
filterGroup = new FormGroup({
|
||||
'matchState': new FormControl(MatchStateOption.Error, []),
|
||||
});
|
||||
|
|
@ -71,6 +70,15 @@ export class ManageMatchedMetadataComponent implements OnInit {
|
|||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
this.filterGroup.valueChanges.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
|
|
@ -86,7 +94,6 @@ export class ManageMatchedMetadataComponent implements OnInit {
|
|||
).subscribe();
|
||||
|
||||
this.loadData().subscribe();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -108,21 +115,11 @@ export class ManageMatchedMetadataComponent implements OnInit {
|
|||
}));
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Series>, series: Series) {
|
||||
if (action.callback) {
|
||||
action.callback(action, series);
|
||||
}
|
||||
}
|
||||
|
||||
fixMatch(actionItem: ActionItem<Series>, series: Series) {
|
||||
fixMatch(series: Series) {
|
||||
this.actionService.matchSeries(series, result => {
|
||||
if (!result) return;
|
||||
this.loadData().subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly LooseLeafOrDefaultNumber = LooseLeafOrDefaultNumber;
|
||||
protected readonly ScrobbleEventType = ScrobbleEventType;
|
||||
protected readonly SpecialVolumeNumber = SpecialVolumeNumber;
|
||||
protected readonly ColumnMode = ColumnMode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,18 @@
|
|||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('enableLocalizedName'); as formControl) {
|
||||
<app-setting-switch [title]="t('localized-name-label')" [subtitle]="t('localized-name-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="localized-name" type="checkbox" class="form-check-input" formControlName="enableLocalizedName">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('enablePublicationStatus'); as formControl) {
|
||||
<app-setting-switch [title]="t('derive-publication-status-label')" [subtitle]="t('derive-publication-status-tooltip')">
|
||||
|
|
@ -65,6 +77,18 @@
|
|||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('enableCoverImage'); as formControl) {
|
||||
<app-setting-switch [title]="t('enable-cover-image-label')" [subtitle]="t('enable-cover-image-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="enable-cover-image" type="checkbox" class="form-check-input" formControlName="enableCoverImage">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if(settingsForm.get('enablePeople'); as formControl) {
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
|
|
@ -183,13 +207,13 @@
|
|||
<div formArrayName="ageRatingMappings">
|
||||
@for(mapping of ageRatingMappings.controls; track mapping; let i = $index) {
|
||||
<div [formGroupName]="i" class="row mb-2">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-4 d-flex align-items-center justify-content-center">
|
||||
<input type="text" class="form-control" formControlName="str" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-2 d-flex align-items-center justify-content-center">
|
||||
<i class="fa fa-arrow-right" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-4 d-flex align-items-center justify-content-center">
|
||||
<select class="form-select" formControlName="rating">
|
||||
@for (ageRating of ageRatings; track ageRating.value) {
|
||||
<option [value]="ageRating.value">
|
||||
|
|
@ -202,13 +226,21 @@
|
|||
<button class="btn btn-icon" (click)="removeAgeRatingMappingRow(i)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
@if($last) {
|
||||
<button class="btn btn-icon" (click)="addAgeRatingMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @empty {
|
||||
<button class="btn btn-secondary" (click)="addAgeRatingMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> {{t('add-age-rating-mapping-label')}}
|
||||
</button>
|
||||
}
|
||||
|
||||
<button class="btn btn-secondary" (click)="addAgeRatingMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> {{t('add-age-rating-mapping-label')}}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
|
@ -252,15 +284,39 @@
|
|||
<button class="btn btn-icon" (click)="removeFieldMappingRow(i)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
@if ($last) {
|
||||
<button class="btn btn-icon" (click)="addFieldMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @empty {
|
||||
<button class="btn btn-secondary" (click)="addFieldMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> {{t('add-field-mapping-label')}}
|
||||
</button>
|
||||
}
|
||||
|
||||
<button class="btn btn-secondary float-end" (click)="addFieldMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> {{t('add-field-mapping-label')}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
@if (settingsForm.get('overrides')) {
|
||||
<h5>{{t('overrides-label')}}</h5>
|
||||
<p>{{t('overrides-description')}}</p>
|
||||
<div class="row g-0 mt-4 mb-4" formArrayName="overrides">
|
||||
@for(field of allMetadataSettingFields; track field; let i = $index) {
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" [formControlName]="'override_' + i" [id]="'override-' + field">
|
||||
<label class="form-check-label" [for]="'override-' + field">{{ field | metadataSettingFiled }}</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</form>
|
||||
}
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import {MetadataFieldMapping, MetadataFieldType} from "../_models/metadata-setti
|
|||
import {PersonRole} from "../../_models/metadata/person";
|
||||
import {PersonRolePipe} from "../../_pipes/person-role.pipe";
|
||||
import {NgClass} from "@angular/common";
|
||||
import {allMetadataSettingField} from "../_models/metadata-setting-field";
|
||||
import {MetadataSettingFiledPipe} from "../../_pipes/metadata-setting-filed.pipe";
|
||||
|
||||
|
||||
@Component({
|
||||
|
|
@ -31,6 +33,7 @@ import {NgClass} from "@angular/common";
|
|||
TagBadgeComponent,
|
||||
AgeRatingPipe,
|
||||
PersonRolePipe,
|
||||
MetadataSettingFiledPipe,
|
||||
],
|
||||
templateUrl: './manage-metadata-settings.component.html',
|
||||
styleUrl: './manage-metadata-settings.component.scss',
|
||||
|
|
@ -52,6 +55,7 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||
fieldMappings = this.fb.array([]);
|
||||
personRoles: PersonRole[] = [PersonRole.Writer, PersonRole.CoverArtist, PersonRole.Character];
|
||||
isLoaded = false;
|
||||
allMetadataSettingFields = allMetadataSettingField;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.metadataService.getAllAgeRatings().subscribe(ratings => {
|
||||
|
|
@ -66,6 +70,7 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||
this.settingService.getMetadataSettings().subscribe(settings => {
|
||||
this.settingsForm.addControl('enabled', new FormControl(settings.enabled, []));
|
||||
this.settingsForm.addControl('enableSummary', new FormControl(settings.enableSummary, []));
|
||||
this.settingsForm.addControl('enableLocalizedName', new FormControl(settings.enableLocalizedName, []));
|
||||
this.settingsForm.addControl('enablePublicationStatus', new FormControl(settings.enablePublicationStatus, []));
|
||||
this.settingsForm.addControl('enableRelations', new FormControl(settings.enableRelationships, []));
|
||||
this.settingsForm.addControl('enableGenres', new FormControl(settings.enableGenres, []));
|
||||
|
|
@ -73,6 +78,7 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||
this.settingsForm.addControl('enableRelationships', new FormControl(settings.enableRelationships, []));
|
||||
this.settingsForm.addControl('enablePeople', new FormControl(settings.enablePeople, []));
|
||||
this.settingsForm.addControl('enableStartDate', new FormControl(settings.enableStartDate, []));
|
||||
this.settingsForm.addControl('enableCoverImage', new FormControl(settings.enableCoverImage, []));
|
||||
|
||||
this.settingsForm.addControl('blacklist', new FormControl((settings.blacklist || '').join(','), []));
|
||||
this.settingsForm.addControl('whitelist', new FormControl((settings.whitelist || '').join(','), []));
|
||||
|
|
@ -86,6 +92,15 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||
)
|
||||
));
|
||||
|
||||
this.settingsForm.addControl('overrides', this.fb.group(
|
||||
Object.fromEntries(
|
||||
this.allMetadataSettingFields.map((role, index) => [
|
||||
`override_${index}`,
|
||||
this.fb.control((settings.overrides || []).includes(role)),
|
||||
])
|
||||
)
|
||||
));
|
||||
|
||||
|
||||
if (settings.ageRatingMappings) {
|
||||
Object.entries(settings.ageRatingMappings).forEach(([str, rating]) => {
|
||||
|
|
@ -171,7 +186,10 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||
whitelist: (model.whitelist || '').split(',').map((item: string) => item.trim()),
|
||||
personRoles: Object.entries(this.settingsForm.get('personRoles')!.value)
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key, _]) => this.personRoles[parseInt(key.split('_')[1], 10)])
|
||||
.map(([key, _]) => this.personRoles[parseInt(key.split('_')[1], 10)]),
|
||||
overrides: Object.entries(this.settingsForm.get('overrides')!.value)
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key, _]) => this.allMetadataSettingFields[parseInt(key.split('_')[1], 10)])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -759,14 +759,17 @@
|
|||
"description": "All applicable Series that can be matched with External Metadata reside here. Kavita will prefetch or refresh series metadata, 50 series per 24 hours daily.",
|
||||
"status-header": "Status",
|
||||
"series-name-header": "Series",
|
||||
"library-name-header": "Library",
|
||||
"valid-until-header": "Next Refresh",
|
||||
"actions-header": "Actions",
|
||||
"matched-status-label": "Matched",
|
||||
"unmatched-status-label": "Not Matched",
|
||||
"blacklist-status-label": "Needs Manual Match",
|
||||
"dont-match-status-label": "{{dont-match-label}}",
|
||||
"all-status-label": "All",
|
||||
"dont-match-label": "Don't Match",
|
||||
"no-data": "{{common.no-data}}"
|
||||
"no-data": "{{common.no-data}}",
|
||||
"match-alt": "Match {{seriesName}}"
|
||||
},
|
||||
|
||||
"manage-user-tokens": {
|
||||
|
|
@ -785,12 +788,16 @@
|
|||
"enabled-tooltip": "Allow Kavita to download metadata and write to it's database.",
|
||||
"summary-label": "Summary",
|
||||
"summary-tooltip": "Allow Summary to be written when the field is unlocked.",
|
||||
"localized-name-label": "Localized Series Name",
|
||||
"localized-name-tooltip": "Allow Localized Name to be written when the field is unlocked. Kavita will attempt to make the best guess.",
|
||||
"derive-publication-status-label": "Publication Status",
|
||||
"derive-publication-status-tooltip": "Allow Publication Status to be derived from Total Chapter/Volume counts.",
|
||||
"enable-relations-label": "Relationships",
|
||||
"enable-relations-tooltip": "Allow Series Relationships to be <b>added</b>.",
|
||||
"enable-people-label": "People",
|
||||
"enable-people-tooltip": "Allow People (Characters, Writers, etc) to be <b>added</b>. All people include images.",
|
||||
"enable-cover-image-label": "Cover Image",
|
||||
"enable-cover-image-tooltip": "Allow Kavita to write the cover image for the Series",
|
||||
"enable-start-date-label": "Start Date",
|
||||
"enable-start-date-tooltip": "Allow Start Date of Series to be written to the Series",
|
||||
"enable-genres-label": "Genres",
|
||||
|
|
@ -812,7 +819,10 @@
|
|||
"field-mapping-description": "Setup rules for certain strings found in Genre/Tag field and map it to a new string in Genre/Tag and optionally remove it from the Source list. Only applicable when Genre/Tag are enabled to be written.",
|
||||
"first-last-name-label": "First Last Naming",
|
||||
"first-last-name-tooltip": "Ensure People's names are written First then Last",
|
||||
"person-roles-label": "Roles"
|
||||
"person-roles-label": "Roles",
|
||||
"overrides-label": "Overrides",
|
||||
"overrides-description": "Allow Kavita to write over locked fields"
|
||||
|
||||
},
|
||||
|
||||
"book-line-overlay": {
|
||||
|
|
@ -2613,6 +2623,19 @@
|
|||
"hours": "Hours"
|
||||
},
|
||||
|
||||
"metadata-setting-field-pipe": {
|
||||
"covers": "Covers",
|
||||
"age-rating": "{{metadata-fields.age-rating-title}}",
|
||||
"people": "{{tabs.people-tab}}",
|
||||
"summary": "{{filter-field-pipe.summary}}",
|
||||
"publication-status": "{{edit-series-modal.publication-status-title}}",
|
||||
"start-date": "{{manage-metadata-settings.enable-start-date-label}}",
|
||||
"genres": "{{metadata-fields.genres-title}}",
|
||||
"tags": "{{metadata-fields.tags-title}}",
|
||||
"localized-name": "{{edit-series-modal.localized-name-label}}"
|
||||
},
|
||||
|
||||
|
||||
"actionable": {
|
||||
"scan-library": "Scan Library",
|
||||
"scan-library-tooltip": "Scan library for changes. Use force scan to force checking every folder",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue