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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue