Metadata Downloading (#3525)
This commit is contained in:
parent
eb66763078
commit
f4fd7230ea
108 changed files with 6296 additions and 484 deletions
|
|
@ -24,6 +24,7 @@ export interface Library {
|
|||
manageCollections: boolean;
|
||||
manageReadingLists: boolean;
|
||||
allowScrobbling: boolean;
|
||||
allowMetadataMatching: boolean;
|
||||
collapseSeriesRelationships: boolean;
|
||||
libraryFileTypes: Array<FileTypeGroup>;
|
||||
excludePatterns: Array<string>;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ export interface Preferences {
|
|||
collapseSeriesRelationships: boolean;
|
||||
shareReviews: boolean;
|
||||
locale: string;
|
||||
|
||||
// Kavita+
|
||||
aniListScrobblingEnabled: boolean;
|
||||
wantToReadSync: boolean;
|
||||
}
|
||||
|
||||
export const readingDirections = [{text: 'left-to-right', value: ReadingDirection.LeftToRight}, {text: 'right-to-left', value: ReadingDirection.RightToLeft}];
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ export class SeriesService {
|
|||
}
|
||||
|
||||
updateMatch(seriesId: number, series: ExternalSeriesDetail) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'series/update-match?seriesId=' + seriesId, series, TextResonse);
|
||||
return this.httpClient.post<string>(this.baseUrl + 'series/update-match?seriesId=' + seriesId + '&aniListId=' + series.aniListId, {}, TextResonse);
|
||||
}
|
||||
|
||||
updateDontMatch(seriesId: number, dontMatch: boolean) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<ng-container *transloco="let t; read:'match-series-result-item'">
|
||||
|
||||
<div class="d-flex p-1 clickable" (click)="selectItem()">
|
||||
<div style="width: 32px" class="me-1">
|
||||
@if (item.series.coverUrl) {
|
||||
|
|
@ -11,7 +12,7 @@
|
|||
@for(synm of item.series.synonyms; track synm; let last = $last) {
|
||||
{{synm}}
|
||||
@if (!last) {
|
||||
<span>, </span>
|
||||
<span>, </span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
|
@ -23,16 +24,27 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex p-1 justify-content-between">
|
||||
<span class="me-1"><a (click)="$event.stopPropagation()" [href]="item.series.siteUrl" rel="noreferrer noopener" target="_blank">{{t('details')}}</a></span>
|
||||
@if ((item.series.volumeCount || 0) > 0 || (item.series.chapterCount || 0) > 0) {
|
||||
<span class="me-1">{{t('volume-count', {num: item.series.volumeCount})}}</span>
|
||||
<span class="me-1">{{t('chapter-count', {num: item.series.chapterCount})}}</span>
|
||||
} @else {
|
||||
<span class="me-1">{{t('releasing')}}</span>
|
||||
}
|
||||
@if (isSelected) {
|
||||
<div class="d-flex p-1 justify-content-center">
|
||||
<app-loading [absolute]="false" [loading]="true"></app-loading>
|
||||
<span class="ms-2">{{t('updating-metadata-status')}}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="d-flex p-1 justify-content-between">
|
||||
<span class="me-1"><a (click)="$event.stopPropagation()" [href]="item.series.siteUrl" rel="noreferrer noopener" target="_blank">{{t('details')}}</a></span>
|
||||
@if ((item.series.volumeCount || 0) > 0 || (item.series.chapterCount || 0) > 0) {
|
||||
<span class="me-1">{{t('volume-count', {num: item.series.volumeCount})}}</span>
|
||||
<span class="me-1">{{t('chapter-count', {num: item.series.chapterCount})}}</span>
|
||||
} @else {
|
||||
<span class="me-1">{{t('releasing')}}</span>
|
||||
}
|
||||
|
||||
<span class="me-1">{{item.series.plusMediaFormat | plusMediaFormat}}</span>
|
||||
<span class="me-1">({{item.matchRating | translocoPercent}})</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="me-1">{{item.series.plusMediaFormat | plusMediaFormat}}</span>
|
||||
<span class="me-1">({{item.matchRating | translocoPercent}})</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {TranslocoPercentPipe} from "@jsverse/transloco-locale";
|
|||
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {PlusMediaFormatPipe} from "../../_pipes/plus-media-format.pipe";
|
||||
import {LoadingComponent} from "../../shared/loading/loading.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-match-series-result-item',
|
||||
|
|
@ -24,7 +25,8 @@ import {PlusMediaFormatPipe} from "../../_pipes/plus-media-format.pipe";
|
|||
TranslocoPercentPipe,
|
||||
ReadMoreComponent,
|
||||
TranslocoDirective,
|
||||
PlusMediaFormatPipe
|
||||
PlusMediaFormatPipe,
|
||||
LoadingComponent
|
||||
],
|
||||
templateUrl: './match-series-result-item.component.html',
|
||||
styleUrl: './match-series-result-item.component.scss',
|
||||
|
|
@ -37,7 +39,13 @@ export class MatchSeriesResultItemComponent {
|
|||
@Input({required: true}) item!: ExternalSeriesMatch;
|
||||
@Output() selected: EventEmitter<ExternalSeriesMatch> = new EventEmitter();
|
||||
|
||||
isSelected = false;
|
||||
|
||||
selectItem() {
|
||||
if (this.isSelected) return;
|
||||
|
||||
this.isSelected = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.selected.emit(this.item);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
@if (tokenExpired) {
|
||||
<p class="alert alert-warning">{{t('token-expired')}}</p>
|
||||
} @else if (!(accountService.currentUser$ | async)!.preferences.aniListScrobblingEnabled) {
|
||||
<p class="alert alert-warning">{{t('scrobbling-disabled')}}</p>
|
||||
}
|
||||
|
||||
<p>{{t('description')}}</p>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import {ToastrService} from "ngx-toastr";
|
|||
import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapter";
|
||||
import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable";
|
||||
import {CardActionablesComponent} from "../card-actionables/card-actionables.component";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
|
||||
export interface DataTablePage {
|
||||
pageNumber: number,
|
||||
|
|
@ -29,8 +31,8 @@ export interface DataTablePage {
|
|||
@Component({
|
||||
selector: 'app-user-scrobble-history',
|
||||
standalone: true,
|
||||
imports: [ScrobbleEventTypePipe, ReactiveFormsModule, TranslocoModule,
|
||||
DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, CardActionablesComponent],
|
||||
imports: [ScrobbleEventTypePipe, ReactiveFormsModule, TranslocoModule,
|
||||
DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, CardActionablesComponent, AsyncPipe],
|
||||
templateUrl: './user-scrobble-history.component.html',
|
||||
styleUrls: ['./user-scrobble-history.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -40,12 +42,14 @@ export class UserScrobbleHistoryComponent implements OnInit {
|
|||
protected readonly SpecialVolumeNumber = SpecialVolumeNumber;
|
||||
protected readonly LooseLeafOrDefaultNumber = LooseLeafOrDefaultNumber;
|
||||
protected readonly ColumnMode = ColumnMode;
|
||||
protected readonly ScrobbleEventType = ScrobbleEventType;
|
||||
|
||||
private readonly scrobblingService = inject(ScrobblingService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
protected readonly ScrobbleEventType = ScrobbleEventType;
|
||||
protected readonly accountService = inject(AccountService);
|
||||
|
||||
|
||||
|
||||
tokenExpired = false;
|
||||
|
|
|
|||
34
UI/Web/src/app/admin/_models/metadata-settings.ts
Normal file
34
UI/Web/src/app/admin/_models/metadata-settings.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import {AgeRating} from "../../_models/metadata/age-rating";
|
||||
import {PersonRole} from "../../_models/metadata/person";
|
||||
|
||||
export enum MetadataFieldType {
|
||||
Genre = 0,
|
||||
Tag = 1
|
||||
}
|
||||
|
||||
export interface MetadataFieldMapping {
|
||||
id: number;
|
||||
sourceType: MetadataFieldType;
|
||||
destinationType: MetadataFieldType;
|
||||
sourceValue: string;
|
||||
destinationValue: string;
|
||||
excludeFromSource: boolean;
|
||||
}
|
||||
|
||||
export interface MetadataSettings {
|
||||
enabled: boolean;
|
||||
enableSummary: boolean;
|
||||
enablePublicationStatus: boolean;
|
||||
enableRelationships: boolean;
|
||||
enablePeople: boolean;
|
||||
enableStartDate: boolean;
|
||||
enableLocalizedName: boolean;
|
||||
enableGenres: boolean;
|
||||
enableTags: boolean;
|
||||
firstLastPeopleNaming: boolean;
|
||||
ageRatingMappings: Map<string, AgeRating>;
|
||||
fieldMappings: Array<MetadataFieldMapping>;
|
||||
blacklist: Array<string>;
|
||||
whitelist: Array<string>;
|
||||
personRoles: Array<PersonRole>;
|
||||
}
|
||||
|
|
@ -6,166 +6,165 @@
|
|||
|
||||
<div class="container-fluid">
|
||||
<p>{{t('kavita+-desc-part-1')}} <a [href]="WikiLink.KavitaPlus" target="_blank" rel="noreferrer nofollow">{{t('kavita+-desc-part-2')}}</a> {{t('kavita+-desc-part-3')}}</p>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="formGroup">
|
||||
<div class="mt-2">
|
||||
<app-setting-item [title]="t('title')" (editMode)="updateEditMode($event)" [isEditMode]="!isViewMode" [showEdit]="hasLicense">
|
||||
<ng-template #titleExtra>
|
||||
<button class="btn btn-icon btn-sm" (click)="loadLicenseInfo(true)">
|
||||
@if (isChecking) {
|
||||
<app-loading [loading]="isChecking" size="spinner-border-sm"></app-loading>
|
||||
} @else if (hasLicense) {
|
||||
<span>
|
||||
<form [formGroup]="formGroup">
|
||||
<div class="mt-2">
|
||||
<app-setting-item [title]="t('title')" (editMode)="updateEditMode($event)" [isEditMode]="!isViewMode" [showEdit]="hasLicense">
|
||||
<ng-template #titleExtra>
|
||||
<button class="btn btn-icon btn-sm" (click)="loadLicenseInfo(true)">
|
||||
@if (isChecking) {
|
||||
<app-loading [loading]="isChecking" size="spinner-border-sm"></app-loading>
|
||||
} @else if (hasLicense) {
|
||||
<span>
|
||||
<i class="fa-solid fa-refresh" tabindex="0" [ngbTooltip]="t('check')"></i>
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #view>
|
||||
@if (hasLicense) {
|
||||
<span class="me-1">*********</span>
|
||||
}
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #view>
|
||||
@if (hasLicense) {
|
||||
<span class="me-1">*********</span>
|
||||
|
||||
@if (isChecking) {
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">{{t('loading')}}</span>
|
||||
</div>
|
||||
} @else {
|
||||
@if (licenseInfo?.isActive) {
|
||||
<i [ngbTooltip]="t('license-valid')" class="fa-solid fa-check-circle successful-validation ms-1">
|
||||
<span class="visually-hidden">{{t('license-valid')}}</span>
|
||||
</i>
|
||||
@if (isChecking) {
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">{{t('loading')}}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<i class="error fa-solid fa-exclamation-circle ms-1" [ngbTooltip]="t('license-not-valid')">
|
||||
<span class="visually-hidden">{{t('license-not-valid')}}</span>
|
||||
@if (licenseInfo?.isActive) {
|
||||
<i [ngbTooltip]="t('license-valid')" class="fa-solid fa-check-circle successful-validation ms-1">
|
||||
<span class="visually-hidden">{{t('license-valid')}}</span>
|
||||
</i>
|
||||
} @else {
|
||||
<i class="error fa-solid fa-exclamation-circle ms-1" [ngbTooltip]="t('license-not-valid')">
|
||||
<span class="visually-hidden">{{t('license-not-valid')}}</span>
|
||||
</i>
|
||||
}
|
||||
}
|
||||
@if (!isChecking && hasLicense && !licenseInfo) {
|
||||
<div><span class="error">{{t('license-mismatch')}}</span></div>
|
||||
}
|
||||
|
||||
} @else {
|
||||
{{t('no-license-key')}}
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #edit>
|
||||
<div class="form-group mb-3">
|
||||
<label for="license-key">{{t('activate-license-label')}}</label>
|
||||
<input id="license-key" type="text" class="form-control" formControlName="licenseKey" autocomplete="off"/>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="email">{{t('activate-email-label')}}</label>
|
||||
<input id="email" type="email" class="form-control" formControlName="email" autocomplete="off"/>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="discordId">{{t('activate-discordId-label')}}</label>
|
||||
<i class="fa fa-circle-info ms-1" aria-hidden="true" [ngbTooltip]="t('activate-discordId-tooltip')"></i>
|
||||
<a class="ms-1" [href]="WikiLink.KavitaPlusDiscordId" target="_blank" rel="noopener noreferrer">{{t('help-label')}}</a>
|
||||
<input id="discordId" type="text" class="form-control" formControlName="discordId" autocomplete="off" [class.is-invalid]="formGroup.get('discordId')?.invalid && formGroup.get('discordId')?.touched"/>
|
||||
@if (formGroup.dirty || !formGroup.untouched) {
|
||||
<div id="inviteForm-validations" class="invalid-feedback">
|
||||
@if (formGroup.get('discordId')?.errors?.pattern) {
|
||||
<div>
|
||||
{{t('discord-validation')}}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<!-- <button type="button" class="flex-fill btn btn-danger me-1" aria-describedby="license-key-header"-->
|
||||
<!-- (click)="deleteLicense()">-->
|
||||
<!-- {{t('activate-delete')}}-->
|
||||
<!-- </button>-->
|
||||
<button type="button" class="flex-fill btn btn-danger me-1" aria-describedby="license-key-header"
|
||||
[ngbTooltip]="t('activate-reset-tooltip')"
|
||||
[disabled]="!formGroup.get('email')?.value || !formGroup.get('licenseKey')?.value" (click)="resetLicense()">
|
||||
{{t('activate-reset')}}
|
||||
</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="license-key-header"
|
||||
[disabled]="!formGroup.get('email')?.value || !formGroup.get('licenseKey')?.value" (click)="saveForm()">
|
||||
@if (!isSaving) {
|
||||
<span>{{t('activate-save')}}</span>
|
||||
}
|
||||
|
||||
<app-loading [loading]="isSaving" size="spinner-border-sm"></app-loading>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #titleActions>
|
||||
@if (hasLicense) {
|
||||
@if (licenseInfo?.isActive) {
|
||||
<a class="btn btn-primary-outline btn-sm me-1" [href]="manageLink" target="_blank" rel="noreferrer nofollow">{{t('manage')}}</a>
|
||||
} @else {
|
||||
<a class="btn btn-primary-outline btn-sm me-1"
|
||||
[ngbTooltip]="t('invalid-license-tooltip')"
|
||||
href="mailto:kavitareader@gmail.com?subject=Kavita+Subscription+Renewal&body=Description%3A%0D%0A%0D%0ALicense%20Key%3A%0D%0A%0D%0AYour%20Email%3A"
|
||||
>{{t('renew')}}</a>
|
||||
}
|
||||
} @else {
|
||||
<a class="btn btn-secondary btn-sm me-1" [href]="buyLink" target="_blank" rel="noreferrer nofollow">{{t('buy')}}</a>
|
||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()">{{isViewMode ? t('activate') : t('cancel')}}</button>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
@if (hasLicense && licenseInfo) {
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
<div class="row g-0 mt-3">
|
||||
<h3>{{t('info-title')}}</h3>
|
||||
<div class="mb-2 col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('license-active-label')">
|
||||
<ng-template #view>
|
||||
@if (isChecking) {
|
||||
{{null | defaultValue}}
|
||||
} @else {
|
||||
<i class="fas {{licenseInfo.isActive ? 'fa-check-circle' : 'fa-circle-xmark error'}}">
|
||||
<span class="visually-hidden">{{licenseInfo.isActive ? t('valid') : t('invalid')}]</span>
|
||||
</i>
|
||||
}
|
||||
}
|
||||
@if (!isChecking && hasLicense && !licenseInfo) {
|
||||
<div><span class="error">{{t('license-mismatch')}}</span></div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
} @else {
|
||||
{{t('no-license-key')}}
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #edit>
|
||||
<div class="form-group mb-3">
|
||||
<label for="license-key">{{t('activate-license-label')}}</label>
|
||||
<input id="license-key" type="text" class="form-control" formControlName="licenseKey" autocomplete="off"/>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="email">{{t('activate-email-label')}}</label>
|
||||
<input id="email" type="email" class="form-control" formControlName="email" autocomplete="off"/>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="discordId">{{t('activate-discordId-label')}}</label>
|
||||
<i class="fa fa-circle-info ms-1" aria-hidden="true" [ngbTooltip]="t('activate-discordId-tooltip')"></i>
|
||||
<a class="ms-1" [href]="WikiLink.KavitaPlusDiscordId" target="_blank" rel="noopener noreferrer">{{t('help-label')}}</a>
|
||||
<input id="discordId" type="text" class="form-control" formControlName="discordId" autocomplete="off" [class.is-invalid]="formGroup.get('discordId')?.invalid && formGroup.get('discordId')?.touched"/>
|
||||
@if (formGroup.dirty || !formGroup.untouched) {
|
||||
<div id="inviteForm-validations" class="invalid-feedback">
|
||||
@if (formGroup.get('discordId')?.errors?.pattern) {
|
||||
<div>
|
||||
{{t('discord-validation')}}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<!-- <button type="button" class="flex-fill btn btn-danger me-1" aria-describedby="license-key-header"-->
|
||||
<!-- (click)="deleteLicense()">-->
|
||||
<!-- {{t('activate-delete')}}-->
|
||||
<!-- </button>-->
|
||||
<button type="button" class="flex-fill btn btn-danger me-1" aria-describedby="license-key-header"
|
||||
[ngbTooltip]="t('activate-reset-tooltip')"
|
||||
[disabled]="!formGroup.get('email')?.value || !formGroup.get('licenseKey')?.value" (click)="resetLicense()">
|
||||
{{t('activate-reset')}}
|
||||
</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="license-key-header"
|
||||
[disabled]="!formGroup.get('email')?.value || !formGroup.get('licenseKey')?.value" (click)="saveForm()">
|
||||
@if (!isSaving) {
|
||||
<span>{{t('activate-save')}}</span>
|
||||
}
|
||||
|
||||
<app-loading [loading]="isSaving" size="spinner-border-sm"></app-loading>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #titleActions>
|
||||
@if (hasLicense) {
|
||||
@if (licenseInfo?.isActive) {
|
||||
<a class="btn btn-primary-outline btn-sm me-1" [href]="manageLink" target="_blank" rel="noreferrer nofollow">{{t('manage')}}</a>
|
||||
} @else {
|
||||
<a class="btn btn-primary-outline btn-sm me-1"
|
||||
[ngbTooltip]="t('invalid-license-tooltip')"
|
||||
href="mailto:kavitareader@gmail.com?subject=Kavita+Subscription+Renewal&body=Description%3A%0D%0A%0D%0ALicense%20Key%3A%0D%0A%0D%0AYour%20Email%3A"
|
||||
>{{t('renew')}}</a>
|
||||
}
|
||||
} @else {
|
||||
<a class="btn btn-secondary btn-sm me-1" [href]="buyLink" target="_blank" rel="noreferrer nofollow">{{t('buy')}}</a>
|
||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()">{{isViewMode ? t('activate') : t('cancel')}}</button>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
@if (hasLicense && licenseInfo) {
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
<div class="row g-0 mt-3">
|
||||
<h3 class="container-fluid">{{t('info-title')}}</h3>
|
||||
<div class="mb-2 col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('license-active-label')">
|
||||
<ng-template #view>
|
||||
@if (isChecking) {
|
||||
{{null | defaultValue}}
|
||||
} @else {
|
||||
<i class="fas {{licenseInfo.isActive ? 'fa-check-circle' : 'fa-circle-xmark error'}}">
|
||||
<span class="visually-hidden">{{licenseInfo.isActive ? t('valid') : t('invalid')}]</span>
|
||||
<div class="mb-2 col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('supported-version-label')">
|
||||
<ng-template #view>
|
||||
<i class="fas {{licenseInfo.isValidVersion ? 'fa-check-circle' : 'fa-circle-xmark error'}}">
|
||||
<span class="visually-hidden">{{isValidVersion ? t('valid') : t('invalid')}]</span>
|
||||
</i>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('supported-version-label')">
|
||||
<ng-template #view>
|
||||
<i class="fas {{licenseInfo.isValidVersion ? 'fa-check-circle' : 'fa-circle-xmark error'}}">
|
||||
<span class="visually-hidden">{{isValidVersion ? t('valid') : t('invalid')}]</span>
|
||||
</i>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
<div class="mb-2 col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('expiration-label')">
|
||||
<ng-template #view>
|
||||
{{licenseInfo.expirationDate | utcToLocalTime | defaultValue}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('expiration-label')">
|
||||
<ng-template #view>
|
||||
{{licenseInfo.expirationDate | utcToLocalTime | defaultValue}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
<div class="mb-2 col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('total-subbed-months-label')">
|
||||
<ng-template #view>
|
||||
{{licenseInfo.totalMonthsSubbed | number}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('total-subbed-months-label')">
|
||||
<ng-template #view>
|
||||
{{licenseInfo.totalMonthsSubbed | number}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('email-label')">
|
||||
<ng-template #view>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('email-label')">
|
||||
<ng-template #view>
|
||||
<span (click)="toggleEmailShow()" class="col-12 clickable">
|
||||
@if (showEmail) {
|
||||
{{licenseInfo.registeredEmail}}
|
||||
|
|
@ -173,28 +172,31 @@
|
|||
***************
|
||||
}
|
||||
</span>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
<!-- Actions around license -->
|
||||
<h3>{{t('actions-title')}}</h3>
|
||||
|
||||
<div class="mt-2 mb-2">
|
||||
<app-setting-button [subtitle]="t('delete-tooltip')">
|
||||
<button type="button" class="flex-fill btn btn-danger mt-1" aria-describedby="license-key-header" (click)="deleteLicense()">
|
||||
{{t('activate-delete')}}
|
||||
</button>
|
||||
</app-setting-button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-2">
|
||||
<app-setting-button [subtitle]="t('manage-tooltip')">
|
||||
<a class="btn btn-primary btn-sm mt-1" [href]="manageLink" target="_blank" rel="noreferrer nofollow">{{t('manage')}}</a>
|
||||
</app-setting-button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
<!-- Actions around license -->
|
||||
<h3 class="container-fluid">{{t('actions-title')}}</h3>
|
||||
|
||||
<div class="mt-2 mb-2">
|
||||
<app-setting-button [subtitle]="t('delete-tooltip')">
|
||||
<button type="button" class="flex-fill btn btn-danger mt-1" aria-describedby="license-key-header" (click)="deleteLicense()">
|
||||
{{t('activate-delete')}}
|
||||
</button>
|
||||
</app-setting-button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-2">
|
||||
<app-setting-button [subtitle]="t('manage-tooltip')">
|
||||
<a class="btn btn-primary btn-sm mt-1" [href]="manageLink" target="_blank" rel="noreferrer nofollow">{{t('manage')}}</a>
|
||||
</app-setting-button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,266 @@
|
|||
<ng-container *transloco="let t; read:'manage-metadata-settings'">
|
||||
|
||||
|
||||
<p>{{t('description')}}</p>
|
||||
@if (isLoaded) {
|
||||
<form [formGroup]="settingsForm">
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('enabled'); as formControl) {
|
||||
<app-setting-switch [title]="t('enabled-label')" [subtitle]="t('enabled-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="enabled" type="checkbox" class="form-check-input" formControlName="enabled">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('enableSummary'); as formControl) {
|
||||
<app-setting-switch [title]="t('summary-label')" [subtitle]="t('summary-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="summary" type="checkbox" class="form-check-input" formControlName="enableSummary">
|
||||
</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')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="derive-publication-status" type="checkbox" class="form-check-input" formControlName="enablePublicationStatus">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('enableRelationships'); as formControl) {
|
||||
<app-setting-switch [title]="t('enable-relations-label')" [subtitle]="t('enable-relations-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="enable-relations-status" type="checkbox" class="form-check-input" formControlName="enableRelationships">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('enableStartDate'); as formControl) {
|
||||
<app-setting-switch [title]="t('enable-start-date-label')" [subtitle]="t('enable-start-date-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="enable-start-date-status" type="checkbox" class="form-check-input" formControlName="enableStartDate">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if(settingsForm.get('enablePeople'); as formControl) {
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
|
||||
<div class="col-6">
|
||||
<app-setting-switch [title]="t('enable-people-label')" [subtitle]="t('enable-people-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="enable-people-status" type="checkbox" class="form-check-input" formControlName="enablePeople">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<app-setting-switch [title]="t('first-last-name-label')" [subtitle]="t('first-last-name-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="enable-first-last-name" type="checkbox" class="form-check-input" formControlName="firstLastPeopleNaming">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@if (settingsForm.get('personRoles')) {
|
||||
<h5>{{t('person-roles-label')}}</h5>
|
||||
<div class="row g-0 mt-4 mb-4" formArrayName="personRoles">
|
||||
@for(role of personRoles; track role; let i = $index) {
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" [formControlName]="'personRole_' + i" [id]="'role-' + role">
|
||||
<label class="form-check-label" [for]="'role-' + role">{{ role | personRole }}</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<div class="col-md-6">
|
||||
@if(settingsForm.get('enableGenres'); as formControl) {
|
||||
<app-setting-switch [title]="t('enable-genres-label')" [subtitle]="t('enable-genres-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="enable-genres-status" type="checkbox" class="form-check-input" formControlName="enableGenres">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@if(settingsForm.get('enableTags'); as formControl) {
|
||||
<app-setting-switch [title]="t('enable-tags-label')" [subtitle]="t('enable-tags-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="enable-tags-status" type="checkbox" class="form-check-input" formControlName="enableTags">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('blacklist'); as formControl) {
|
||||
<app-setting-item [title]="t('blacklist-label')" [subtitle]="t('blacklist-tooltip')">
|
||||
<ng-template #view>
|
||||
@let val = (formControl.value || '').split(',');
|
||||
|
||||
@for(opt of val; track opt) {
|
||||
<app-tag-badge>{{opt.trim()}}</app-tag-badge>
|
||||
} @empty {
|
||||
{{null | defaultValue}}
|
||||
}
|
||||
</ng-template>s
|
||||
<ng-template #edit>
|
||||
<textarea rows="3" id="blacklist" class="form-control" formControlName="blacklist"></textarea>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('whitelist'); as formControl) {
|
||||
<app-setting-item [title]="t('whitelist-label')" [subtitle]="t('whitelist-tooltip')">
|
||||
<ng-template #view>
|
||||
@let val = (formControl.value || '').split(',');
|
||||
|
||||
@for(opt of val; track opt) {
|
||||
<app-tag-badge>{{opt.trim()}}</app-tag-badge>
|
||||
} @empty {
|
||||
{{null | defaultValue}}
|
||||
}
|
||||
</ng-template>s
|
||||
<ng-template #edit>
|
||||
<textarea rows="3" id="whitelist" class="form-control" formControlName="whitelist"></textarea>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
<h4>{{t('age-rating-mapping-title')}}</h4>
|
||||
<p>{{t('age-rating-mapping-description')}}</p>
|
||||
|
||||
<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">
|
||||
<input type="text" class="form-control" formControlName="str" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<i class="fa fa-arrow-right" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select class="form-select" formControlName="rating">
|
||||
@for (ageRating of ageRatings; track ageRating.value) {
|
||||
<option [value]="ageRating.value">
|
||||
{{ageRating.value | ageRating}}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-icon" (click)="removeAgeRatingMappingRow(i)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Field Mapping Table -->
|
||||
<h4>{{t('field-mapping-title')}}</h4>
|
||||
<p>{{t('field-mapping-description')}}</p>
|
||||
<div formArrayName="fieldMappings">
|
||||
@for (mapping of fieldMappings.controls; track mapping; let i = $index) {
|
||||
<div [formGroupName]="i" class="row mb-2">
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" formControlName="sourceType">
|
||||
<option [value]="MetadataFieldType.Genre">{{t('genre')}}</option>
|
||||
<option [value]="MetadataFieldType.Tag">{{t('tag')}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" formControlName="sourceValue"
|
||||
placeholder="Source genre/tag" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" formControlName="destinationType">
|
||||
<option [value]="MetadataFieldType.Genre">{{t('genre')}}</option>
|
||||
<option [value]="MetadataFieldType.Tag">{{t('tag')}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" formControlName="destinationValue"
|
||||
placeholder="Destination genre/tag" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-check">
|
||||
<input id="remove-source-tag-{{i}}" type="checkbox" class="form-check-input"
|
||||
formControlName="excludeFromSource">
|
||||
<label [for]="'remove-source-tag-' + i" class="form-check-label">
|
||||
{{t('remove-source-tag-label')}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-icon" (click)="removeFieldMappingRow(i)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<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>
|
||||
|
||||
</form>
|
||||
}
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component";
|
||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component";
|
||||
import {SettingsService} from "../settings.service";
|
||||
import {debounceTime, switchMap} from "rxjs";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {filter, map} from "rxjs/operators";
|
||||
import {AgeRatingPipe} from "../../_pipes/age-rating.pipe";
|
||||
import {AgeRating} from "../../_models/metadata/age-rating";
|
||||
import {MetadataService} from "../../_services/metadata.service";
|
||||
import {AgeRatingDto} from "../../_models/metadata/age-rating-dto";
|
||||
import {MetadataFieldMapping, MetadataFieldType} from "../_models/metadata-settings";
|
||||
import {PersonRole} from "../../_models/metadata/person";
|
||||
import {PersonRolePipe} from "../../_pipes/person-role.pipe";
|
||||
import {NgClass} from "@angular/common";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-metadata-settings',
|
||||
standalone: true,
|
||||
imports: [
|
||||
TranslocoDirective,
|
||||
ReactiveFormsModule,
|
||||
SettingSwitchComponent,
|
||||
SettingItemComponent,
|
||||
DefaultValuePipe,
|
||||
TagBadgeComponent,
|
||||
AgeRatingPipe,
|
||||
PersonRolePipe,
|
||||
],
|
||||
templateUrl: './manage-metadata-settings.component.html',
|
||||
styleUrl: './manage-metadata-settings.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ManageMetadataSettingsComponent implements OnInit {
|
||||
|
||||
protected readonly MetadataFieldType = MetadataFieldType;
|
||||
|
||||
private readonly settingService = inject(SettingsService);
|
||||
private readonly metadataService = inject(MetadataService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly fb = inject(FormBuilder);
|
||||
|
||||
settingsForm: FormGroup = new FormGroup({});
|
||||
ageRatings: Array<AgeRatingDto> = [];
|
||||
ageRatingMappings = this.fb.array([]);
|
||||
fieldMappings = this.fb.array([]);
|
||||
personRoles: PersonRole[] = [PersonRole.Writer, PersonRole.CoverArtist, PersonRole.Character];
|
||||
isLoaded = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.metadataService.getAllAgeRatings().subscribe(ratings => {
|
||||
this.ageRatings = ratings;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
this.settingsForm.addControl('ageRatingMappings', this.ageRatingMappings);
|
||||
this.settingsForm.addControl('fieldMappings', this.fieldMappings);
|
||||
|
||||
this.settingService.getMetadataSettings().subscribe(settings => {
|
||||
this.settingsForm.addControl('enabled', new FormControl(settings.enabled, []));
|
||||
this.settingsForm.addControl('enableSummary', new FormControl(settings.enableSummary, []));
|
||||
this.settingsForm.addControl('enablePublicationStatus', new FormControl(settings.enablePublicationStatus, []));
|
||||
this.settingsForm.addControl('enableRelations', new FormControl(settings.enableRelationships, []));
|
||||
this.settingsForm.addControl('enableGenres', new FormControl(settings.enableGenres, []));
|
||||
this.settingsForm.addControl('enableTags', new FormControl(settings.enableTags, []));
|
||||
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('blacklist', new FormControl((settings.blacklist || '').join(','), []));
|
||||
this.settingsForm.addControl('whitelist', new FormControl((settings.whitelist || '').join(','), []));
|
||||
this.settingsForm.addControl('firstLastPeopleNaming', new FormControl((settings.firstLastPeopleNaming), []));
|
||||
this.settingsForm.addControl('personRoles', this.fb.group(
|
||||
Object.fromEntries(
|
||||
this.personRoles.map((role, index) => [
|
||||
`personRole_${index}`,
|
||||
this.fb.control((settings.personRoles || this.personRoles).includes(role)),
|
||||
])
|
||||
)
|
||||
));
|
||||
|
||||
|
||||
if (settings.ageRatingMappings) {
|
||||
Object.entries(settings.ageRatingMappings).forEach(([str, rating]) => {
|
||||
this.addAgeRatingMapping(str, rating);
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.fieldMappings) {
|
||||
settings.fieldMappings.forEach(mapping => {
|
||||
this.addFieldMapping(mapping);
|
||||
});
|
||||
}
|
||||
|
||||
this.settingsForm.get('enablePeople')?.valueChanges.subscribe(enabled => {
|
||||
const firstLastControl = this.settingsForm.get('firstLastPeopleNaming');
|
||||
if (enabled) {
|
||||
firstLastControl?.enable();
|
||||
} else {
|
||||
firstLastControl?.disable();
|
||||
}
|
||||
});
|
||||
|
||||
this.settingsForm.get('enablePeople')?.updateValueAndValidity();
|
||||
|
||||
// Disable personRoles checkboxes based on enablePeople state
|
||||
this.settingsForm.get('enablePeople')?.valueChanges.subscribe(enabled => {
|
||||
const personRolesArray = this.settingsForm.get('personRoles') as FormArray;
|
||||
if (enabled) {
|
||||
personRolesArray.enable();
|
||||
} else {
|
||||
personRolesArray.disable();
|
||||
}
|
||||
});
|
||||
|
||||
this.isLoaded = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.settingsForm.valueChanges.pipe(
|
||||
debounceTime(300),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(_ => this.packData()),
|
||||
switchMap((data) => this.settingService.updateMetadataSettings(data)),
|
||||
).subscribe();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
packData(withFieldMappings: boolean = true) {
|
||||
const model = this.settingsForm.value;
|
||||
|
||||
// Convert FormArray to dictionary
|
||||
const ageRatingMappings = this.ageRatingMappings.controls.reduce((acc, control) => {
|
||||
// @ts-ignore
|
||||
const { str, rating } = control.value;
|
||||
if (str && rating) {
|
||||
// @ts-ignore
|
||||
acc[str] = parseInt(rating + '', 10) as AgeRating;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const fieldMappings = this.fieldMappings.controls.map((control) => {
|
||||
const value = control.value as MetadataFieldMapping;
|
||||
|
||||
return {
|
||||
id: value.id,
|
||||
sourceType: parseInt(value.sourceType + '', 10),
|
||||
destinationType: parseInt(value.destinationType + '', 10),
|
||||
sourceValue: value.sourceValue,
|
||||
destinationValue: value.destinationValue,
|
||||
excludeFromSource: value.excludeFromSource
|
||||
}
|
||||
}).filter(m => m.sourceValue.length > 0);
|
||||
|
||||
// Translate blacklist string -> Array<string>
|
||||
return {
|
||||
...model,
|
||||
ageRatingMappings,
|
||||
fieldMappings: withFieldMappings ? fieldMappings : [],
|
||||
blacklist: (model.blacklist || '').split(',').map((item: string) => item.trim()),
|
||||
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)])
|
||||
}
|
||||
}
|
||||
|
||||
addAgeRatingMapping(str: string = '', rating: AgeRating = AgeRating.Unknown) {
|
||||
const mappingGroup = this.fb.group({
|
||||
str: [str, Validators.required],
|
||||
rating: [rating, Validators.required]
|
||||
});
|
||||
// @ts-ignore
|
||||
this.ageRatingMappings.push(mappingGroup);
|
||||
}
|
||||
|
||||
removeAgeRatingMappingRow(index: number) {
|
||||
this.ageRatingMappings.removeAt(index);
|
||||
}
|
||||
|
||||
addFieldMapping(mapping: MetadataFieldMapping | null = null) {
|
||||
const mappingGroup = this.fb.group({
|
||||
id: [mapping?.id || 0],
|
||||
sourceType: [mapping?.sourceType || MetadataFieldType.Genre, Validators.required],
|
||||
destinationType: [mapping?.destinationType || MetadataFieldType.Genre, Validators.required],
|
||||
sourceValue: [mapping?.sourceValue || '', Validators.required],
|
||||
destinationValue: [mapping?.destinationValue || ''],
|
||||
excludeFromSource: [mapping?.excludeFromSource || false]
|
||||
});
|
||||
|
||||
// Autofill destination value if empty when source value loses focus
|
||||
mappingGroup.get('sourceValue')?.valueChanges
|
||||
.pipe(
|
||||
filter(() => !mappingGroup.get('destinationValue')?.value)
|
||||
)
|
||||
.subscribe(sourceValue => {
|
||||
mappingGroup.get('destinationValue')?.setValue(sourceValue);
|
||||
});
|
||||
|
||||
//@ts-ignore
|
||||
this.fieldMappings.push(mappingGroup);
|
||||
}
|
||||
|
||||
removeFieldMappingRow(index: number) {
|
||||
this.fieldMappings.removeAt(index);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import {map, of} from 'rxjs';
|
|||
import { environment } from 'src/environments/environment';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import { ServerSettings } from './_models/server-settings';
|
||||
import {MetadataSettings} from "./_models/metadata-settings";
|
||||
|
||||
/**
|
||||
* Used only for the Test Email Service call
|
||||
|
|
@ -27,6 +28,13 @@ export class SettingsService {
|
|||
return this.http.get<ServerSettings>(this.baseUrl + 'settings');
|
||||
}
|
||||
|
||||
getMetadataSettings() {
|
||||
return this.http.get<MetadataSettings>(this.baseUrl + 'settings/metadata-settings');
|
||||
}
|
||||
updateMetadataSettings(model: MetadataSettings) {
|
||||
return this.http.post<MetadataSettings>(this.baseUrl + 'settings/metadata-settings', model);
|
||||
}
|
||||
|
||||
updateServerSettings(model: ServerSettings) {
|
||||
return this.http.post<ServerSettings>(this.baseUrl + 'settings', model);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ function deepClone(obj: any): any {
|
|||
@Component({
|
||||
selector: 'app-series-card',
|
||||
standalone: true,
|
||||
imports: [CardItemComponent, RelationshipPipe, CardActionablesComponent, DefaultValuePipe, DownloadIndicatorComponent,
|
||||
EntityTitleComponent, FormsModule, ImageComponent, NgbProgressbar, NgbTooltip, RouterLink, TranslocoDirective,
|
||||
imports: [RelationshipPipe, CardActionablesComponent, DefaultValuePipe, DownloadIndicatorComponent,
|
||||
FormsModule, ImageComponent, NgbProgressbar, NgbTooltip, RouterLink, TranslocoDirective,
|
||||
SeriesFormatComponent, DecimalPipe],
|
||||
templateUrl: './series-card.component.html',
|
||||
styleUrls: ['./series-card.component.scss'],
|
||||
|
|
@ -245,6 +245,13 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||
case(Action.Edit):
|
||||
this.openEditModal(series);
|
||||
break;
|
||||
case Action.Match:
|
||||
this.actionService.matchSeries(this.series, (refreshNeeded) => {
|
||||
if (refreshNeeded) {
|
||||
this.reload.emit(series.id);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case(Action.AddToReadingList):
|
||||
this.actionService.addSeriesToReadingList(series);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -480,7 +480,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
} else if (event.event === EVENTS.ScanSeries) {
|
||||
const seriesScanEvent = event.payload as ScanSeriesEvent;
|
||||
if (seriesScanEvent.seriesId === this.seriesId) {
|
||||
//this.loadSeries(this.seriesId);
|
||||
this.loadPageSource.next(false);
|
||||
}
|
||||
} else if (event.event === EVENTS.CoverUpdate) {
|
||||
|
|
@ -491,7 +490,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
} else if (event.event === EVENTS.ChapterRemoved) {
|
||||
const removedEvent = event.payload as ChapterRemovedEvent;
|
||||
if (removedEvent.seriesId !== this.seriesId) return;
|
||||
//this.loadSeries(this.seriesId, false);
|
||||
this.loadPageSource.next(false);
|
||||
}
|
||||
});
|
||||
|
|
@ -751,6 +749,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))
|
||||
.filter(action => action.action !== Action.Edit);
|
||||
|
||||
this.licenseService.hasValidLicense$.subscribe(hasLic => {
|
||||
if (!hasLic) {
|
||||
this.seriesActions = this.seriesActions.filter(action => action.action !== Action.Match);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.seriesService.getRelatedForSeries(this.seriesId).subscribe((relations: RelatedSeries) => {
|
||||
this.relationShips = relations;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<ng-container *transloco="let t;">
|
||||
<div class="container-fluid">
|
||||
<div>
|
||||
<ng-content></ng-content>
|
||||
|
||||
@if (subtitle) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<ng-container *transloco="let t;">
|
||||
<div class="container-fluid">
|
||||
<div>
|
||||
<div class="settings-row g-0 row">
|
||||
<div class="col-10 setting-title">
|
||||
<h6 class="section-title">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<ng-container *transloco="let t;">
|
||||
<div class="container-fluid">
|
||||
<div>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-11">
|
||||
<h6 class="section-title" [id]="id || title">{{title}}</h6>
|
||||
|
|
|
|||
|
|
@ -122,6 +122,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
@defer (when fragment === SettingsTabId.Metadata; prefetch on idle) {
|
||||
@if(hasActiveLicense && fragment === SettingsTabId.Metadata) {
|
||||
<div class="scale col-md-12">
|
||||
<app-manage-metadata-settings></app-manage-metadata-settings>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@defer (when fragment === SettingsTabId.ScrobblingHolds; prefetch on idle) {
|
||||
@if(hasActiveLicense && fragment === SettingsTabId.ScrobblingHolds) {
|
||||
<div class="scale col-md-12">
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ import {ManageMatchedMetadataComponent} from "../../../admin/manage-matched-meta
|
|||
import {ManageUserTokensComponent} from "../../../admin/manage-user-tokens/manage-user-tokens.component";
|
||||
import {EmailHistoryComponent} from "../../../admin/email-history/email-history.component";
|
||||
import {ScrobblingHoldsComponent} from "../../../user-settings/user-holds/scrobbling-holds.component";
|
||||
import {
|
||||
ManageMetadataSettingsComponent
|
||||
} from "../../../admin/manage-metadata-settings/manage-metadata-settings.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
|
|
@ -84,7 +87,8 @@ import {ScrobblingHoldsComponent} from "../../../user-settings/user-holds/scrobb
|
|||
ManageMatchedMetadataComponent,
|
||||
ManageUserTokensComponent,
|
||||
EmailHistoryComponent,
|
||||
ScrobblingHoldsComponent
|
||||
ScrobblingHoldsComponent,
|
||||
ManageMetadataSettingsComponent
|
||||
],
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrl: './settings.component.scss',
|
||||
|
|
|
|||
|
|
@ -157,6 +157,16 @@
|
|||
</app-setting-switch>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('allow-metadata-matching-label')" [subtitle]="t('allow-metadata-matching-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input type="checkbox" id="metadata-matching" role="switch" formControlName="allowMetadataMatching" 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('folder-watching-label')" [subtitle]="t('folder-watching-tooltip')">
|
||||
<ng-template #switch>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ import {
|
|||
} from '@angular/core';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {
|
||||
NgbAccordionBody,
|
||||
NgbAccordionButton, NgbAccordionCollapse,
|
||||
NgbAccordionDirective, NgbAccordionHeader, NgbAccordionItem,
|
||||
NgbActiveModal,
|
||||
NgbModal,
|
||||
NgbModalModule,
|
||||
|
|
@ -71,7 +68,7 @@ enum StepID {
|
|||
standalone: true,
|
||||
imports: [CommonModule, NgbModalModule, NgbNavLink, NgbNavItem, NgbNavContent, ReactiveFormsModule, NgbTooltip,
|
||||
SentenceCasePipe, NgbNav, NgbNavOutlet, CoverImageChooserComponent, TranslocoModule, DefaultDatePipe,
|
||||
FileTypeGroupPipe, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionButton, NgbAccordionCollapse, NgbAccordionBody, EditListComponent, SettingItemComponent, SettingSwitchComponent, SettingButtonComponent],
|
||||
FileTypeGroupPipe, EditListComponent, SettingItemComponent, SettingSwitchComponent, SettingButtonComponent],
|
||||
templateUrl: './library-settings-modal.component.html',
|
||||
styleUrls: ['./library-settings-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -116,6 +113,7 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
manageCollections: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
|
||||
manageReadingLists: new FormControl<boolean>(true, { 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] }),
|
||||
});
|
||||
|
||||
|
|
@ -153,7 +151,9 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
|
||||
if (this.library && !(this.library.type === LibraryType.Manga || this.library.type === LibraryType.LightNovel) ) {
|
||||
this.libraryForm.get('allowScrobbling')?.setValue(false);
|
||||
this.libraryForm.get('allowMetadataMatching')?.setValue(false);
|
||||
this.libraryForm.get('allowScrobbling')?.disable();
|
||||
this.libraryForm.get('allowMetadataMatching')?.disable();
|
||||
}
|
||||
|
||||
this.libraryForm.get('name')?.valueChanges.pipe(
|
||||
|
|
@ -216,8 +216,10 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible);
|
||||
if (!this.IsKavitaPlusEligible) {
|
||||
this.libraryForm.get('allowScrobbling')?.disable();
|
||||
this.libraryForm.get('allowMetadataMatching')?.disable();
|
||||
} else {
|
||||
this.libraryForm.get('allowScrobbling')?.enable();
|
||||
this.libraryForm.get('allowMetadataMatching')?.enable();
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}),
|
||||
|
|
@ -237,6 +239,7 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
this.libraryForm.get('manageReadingLists')?.setValue(this.library.manageReadingLists);
|
||||
this.libraryForm.get('collapseSeriesRelationships')?.setValue(this.library.collapseSeriesRelationships);
|
||||
this.libraryForm.get('allowScrobbling')?.setValue(this.library.allowScrobbling);
|
||||
this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching);
|
||||
this.selectedFolders = this.library.folders;
|
||||
this.madeChanges = false;
|
||||
for(let fileTypeGroup of allFileTypeGroup) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export enum SettingsTabId {
|
|||
MALStackImport = 'mal-stack-import',
|
||||
MatchedMetadata = 'admin-matched-metadata',
|
||||
ManageUserTokens = 'admin-manage-tokens',
|
||||
Metadata = 'admin-metadata',
|
||||
|
||||
// Non-Admin
|
||||
Account = 'account',
|
||||
|
|
@ -233,6 +234,7 @@ export class PreferenceNavComponent implements AfterViewInit {
|
|||
this.matchedMetadataBadgeCount$
|
||||
));
|
||||
kavitaPlusSection.children.push(new SideNavItem(SettingsTabId.ManageUserTokens, [Role.Admin]));
|
||||
kavitaPlusSection.children.push(new SideNavItem(SettingsTabId.Metadata, [Role.Admin]));
|
||||
|
||||
// Scrobbling History needs to be per-user and allow admin to view all
|
||||
kavitaPlusSection.children.push(new SideNavItem(SettingsTabId.ScrobblingHolds, []));
|
||||
|
|
|
|||
|
|
@ -87,6 +87,37 @@
|
|||
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
|
||||
@if (licenseService.hasValidLicense$ | async) {
|
||||
<h4 id="kavitaplus-heading" class="mt-3">{{t('kavitaplus-settings-title')}}</h4>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('aniListScrobblingEnabled'); as formControl) {
|
||||
<app-setting-switch [title]="t('anilist-scrobbling-label')" [subtitle]="t('anilist-scrobbling-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="setting-anilist-scrobbling" type="checkbox" class="form-check-input" formControlName="aniListScrobblingEnabled">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('wantToReadSync'); as formControl) {
|
||||
<app-setting-switch [title]="t('want-to-read-sync-label')" [subtitle]="t('want-to-read-sync-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="setting-want-to-read-sync" type="checkbox" class="form-check-input" formControlName="wantToReadSync">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
<div class="setting-section-break"></div>
|
||||
}
|
||||
|
||||
|
||||
<h4 id="image-reader-heading" class="mt-3">{{t('image-reader-settings-title')}}</h4>
|
||||
<ng-container>
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import {
|
|||
NgbAccordionDirective, NgbAccordionHeader,
|
||||
NgbAccordionItem, NgbTooltip
|
||||
} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
|
||||
import {AsyncPipe, NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
|
||||
import {ColorPickerModule} from "ngx-color-picker";
|
||||
import {SettingTitleComponent} from "../../settings/_components/setting-title/setting-title.component";
|
||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||
|
|
@ -53,26 +53,17 @@ import {PdfSpreadModePipe} from "../../_pipes/pdf-spread-mode.pipe";
|
|||
import {PdfThemePipe} from "../../_pipes/pdf-theme.pipe";
|
||||
import {PdfScrollModeTypePipe} from "../../pdf-reader/_pipe/pdf-scroll-mode.pipe";
|
||||
import {PdfScrollModePipe} from "../../_pipes/pdf-scroll-mode.pipe";
|
||||
import {LicenseService} from "../../_services/license.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manga-user-preferences',
|
||||
standalone: true,
|
||||
imports: [
|
||||
TranslocoDirective,
|
||||
NgbAccordionDirective,
|
||||
ReactiveFormsModule,
|
||||
NgbAccordionItem,
|
||||
NgbAccordionCollapse,
|
||||
NgbAccordionBody,
|
||||
NgbAccordionHeader,
|
||||
NgbAccordionButton,
|
||||
NgbTooltip,
|
||||
NgTemplateOutlet,
|
||||
TitleCasePipe,
|
||||
ColorPickerModule,
|
||||
SettingTitleComponent,
|
||||
SettingItemComponent,
|
||||
PageLayoutModePipe,
|
||||
SettingSwitchComponent,
|
||||
ReadingDirectionPipe,
|
||||
ScalingOptionPipe,
|
||||
|
|
@ -82,12 +73,10 @@ import {PdfScrollModePipe} from "../../_pipes/pdf-scroll-mode.pipe";
|
|||
NgStyle,
|
||||
WritingStylePipe,
|
||||
BookPageLayoutModePipe,
|
||||
PdfSpreadTypePipe,
|
||||
PdfSpreadTypePipe,
|
||||
PdfSpreadModePipe,
|
||||
PdfThemePipe,
|
||||
PdfScrollModeTypePipe,
|
||||
PdfScrollModePipe
|
||||
PdfScrollModePipe,
|
||||
AsyncPipe
|
||||
],
|
||||
templateUrl: './manage-user-preferences.component.html',
|
||||
styleUrl: './manage-user-preferences.component.scss',
|
||||
|
|
@ -102,6 +91,7 @@ export class ManageUserPreferencesComponent implements OnInit {
|
|||
private readonly router = inject(Router);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly localizationService = inject(LocalizationService);
|
||||
protected readonly licenseService = inject(LicenseService);
|
||||
|
||||
protected readonly readingDirections = readingDirections;
|
||||
protected readonly scalingOptions = scalingOptions;
|
||||
|
|
@ -199,6 +189,9 @@ export class ManageUserPreferencesComponent implements OnInit {
|
|||
this.settingsForm.addControl('shareReviews', new FormControl(this.user.preferences.shareReviews, []));
|
||||
this.settingsForm.addControl('locale', new FormControl(this.user.preferences.locale || 'en', []));
|
||||
|
||||
this.settingsForm.addControl('aniListScrobblingEnabled', new FormControl(this.user.preferences.aniListScrobblingEnabled || false, []));
|
||||
this.settingsForm.addControl('wantToReadSync', new FormControl(this.user.preferences.wantToReadSync || false, []));
|
||||
|
||||
|
||||
// Automatically save settings as we edit them
|
||||
this.settingsForm.valueChanges.pipe(
|
||||
|
|
@ -267,6 +260,9 @@ export class ManageUserPreferencesComponent implements OnInit {
|
|||
this.settingsForm.get('collapseSeriesRelationships')?.setValue(this.user.preferences.collapseSeriesRelationships, {onlySelf: true, emitEvent: false});
|
||||
this.settingsForm.get('shareReviews')?.setValue(this.user.preferences.shareReviews, {onlySelf: true, emitEvent: false});
|
||||
this.settingsForm.get('locale')?.setValue(this.user.preferences.locale || 'en', {onlySelf: true, emitEvent: false});
|
||||
|
||||
this.settingsForm.get('aniListScrobblingEnabled')?.setValue(this.user.preferences.aniListScrobblingEnabled || false, {onlySelf: true, emitEvent: false});
|
||||
this.settingsForm.get('wantToReadSync')?.setValue(this.user.preferences.wantToReadSync || false, {onlySelf: true, emitEvent: false});
|
||||
}
|
||||
|
||||
packSettings(): Preferences {
|
||||
|
|
@ -303,6 +299,8 @@ export class ManageUserPreferencesComponent implements OnInit {
|
|||
pdfTheme: parseInt(modelSettings.pdfTheme, 10),
|
||||
pdfScrollMode: parseInt(modelSettings.pdfScrollMode, 10),
|
||||
pdfSpreadMode: parseInt(modelSettings.pdfSpreadMode, 10),
|
||||
aniListScrobblingEnabled: modelSettings.aniListScrobblingEnabled,
|
||||
wantToReadSync: modelSettings.wantToReadSync
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@
|
|||
"not-processed": "Not Processed",
|
||||
"special": "{{entity-title.special}}",
|
||||
"generate-scrobble-events": "Backfill Events",
|
||||
"token-expired": "Your AniList token is Expired! Scrobbling events will not process until you renew on Accounts page."
|
||||
"token-expired": "Your AniList token is Expired! Scrobbling events will not process until you renew on Accounts page.",
|
||||
"scrobbling-disabled": "Scrobbling is disabled on your Account Settings."
|
||||
},
|
||||
|
||||
"scrobble-event-type-pipe": {
|
||||
|
|
@ -128,6 +129,12 @@
|
|||
"share-series-reviews-label": "Share Series Reviews",
|
||||
"share-series-reviews-tooltip": "Should Kavita include your reviews of Series for other users",
|
||||
|
||||
"kavitaplus-settings-title": "Kavita+",
|
||||
"anilist-scrobbling-label": "AniList Scrobbling",
|
||||
"anilist-scrobbling-tooltip": "Allow Kavita to Scrobble (one-way sync) reading progress and ratings to AniList",
|
||||
"want-to-read-sync-label": "Want To Read Sync",
|
||||
"want-to-read-sync-tooltip": "Allow Kavita to add items to your Want to Read list based on AniList and MAL series in Pending readlist",
|
||||
|
||||
"image-reader-settings-title": "Image Reader",
|
||||
"reading-direction-label": "Reading Direction",
|
||||
"reading-direction-tooltip": "Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.",
|
||||
|
|
@ -771,6 +778,42 @@
|
|||
"expires-label": "Expires: {{date}}"
|
||||
},
|
||||
|
||||
"manage-metadata-settings": {
|
||||
"description": "Kavita+ has the ability to download and write some limited metadata to the Database. This page allows for you to toggle what is in scope.",
|
||||
"enabled-label": "Enable Metadata Download",
|
||||
"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.",
|
||||
"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-start-date-label": "Start Date",
|
||||
"enable-start-date-tooltip": "Allow Start Date of Series to be written to the Series",
|
||||
"enable-genres-label": "Genres",
|
||||
"enable-genres-tooltip": "Allow Series Genres to be written.",
|
||||
"enable-tags-label": "Tags",
|
||||
"enable-tags-tooltip": "Allow Series Tags to be written.",
|
||||
"blacklist-label": "Blacklist Genres/Tags",
|
||||
"blacklist-tooltip": "Anything in this list will be removed from both Genre and Tag processing. This is a place to add genres/tags you <b>do not</b> want written. Ensure they are comma-separated.",
|
||||
"whitelist-label": "Whitelist Tags",
|
||||
"whitelist-tooltip": "Only allow a string in this list from being written for <b>Tags</b>. Ensure they are comma-separated.",
|
||||
"age-rating-mapping-title": "Age Rating Mapping",
|
||||
"age-rating-mapping-description": "Any strings on the left if found in either Genre or Tags will set the Age Rating on the Series.",
|
||||
"genre": "Genre",
|
||||
"tag": "Tag",
|
||||
"remove-source-tag-label": "Remove Source Tag",
|
||||
"add-field-mapping-label": "Add Field Mapping",
|
||||
"add-age-rating-mapping-label": "Add Age Rating Mapping",
|
||||
"field-mapping-title": "Field Mapping",
|
||||
"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"
|
||||
},
|
||||
|
||||
"book-line-overlay": {
|
||||
"copy": "Copy",
|
||||
"bookmark": "Bookmark",
|
||||
|
|
@ -938,7 +981,8 @@
|
|||
"volume-count": "{{server-stats.volume-count}}",
|
||||
"chapter-count": "{{common.chapter-count}}",
|
||||
"releasing": "Releasing",
|
||||
"details": "View page"
|
||||
"details": "View page",
|
||||
"updating-metadata-status": "Updating Metadata"
|
||||
},
|
||||
|
||||
"metadata-fields": {
|
||||
|
|
@ -1062,6 +1106,8 @@
|
|||
"manage-reading-list-tooltip": "Should Kavita create Reading Lists from StoryArc/StoryArcNumber and AlternativeSeries/AlternativeCount tags found within ComicInfo.xml/opf files",
|
||||
"allow-scrobbling-label": "Allow Scrobbling",
|
||||
"allow-scrobbling-tooltip": "Should Kavita scrobble reading events, want to read status, ratings, and reviews to configured providers. This will only occur if the server has an active Kavita+ Subscription.",
|
||||
"allow-metadata-matching-label": "Allow Metadata Matching",
|
||||
"allow-metadata-matching-tooltip": "Should Kavita download metadata for Series within this Library. This will only occur if the server has an active Kavita+ Subscription.",
|
||||
"folder-watching-label": "Folder Watching",
|
||||
"folder-watching-tooltip": "Override Server folder watching for this library. If off, folder watching won't run on the folders this library contains. If libraries share folders, then folders may still be ran against. Will always wait 10 minutes before triggering scan.",
|
||||
"include-in-dashboard-label": "Include in Dashboard",
|
||||
|
|
@ -1616,6 +1662,7 @@
|
|||
"admin-kavitaplus": "License",
|
||||
"admin-matched-metadata": "Matched Metadata",
|
||||
"admin-manage-tokens": "Manage User Tokens",
|
||||
"admin-metadata": "Manage Metadata",
|
||||
"scrobble-holds": "Scrobble Holds",
|
||||
"account": "Account",
|
||||
"preferences": "Preferences",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue