Localization - First Pass (#2174)
* Started designing the backend localization service * Worked in Transloco for initial PoC * Worked in Transloco for initial PoC * Translated the login screen * translated dashboard screen * Started work on the backend * Fixed a logic bug * translated edit-user screen * Hooked up the backend for having a locale property. * Hooked up the ability to view the available locales and switch to them. * Made the localization service languages be derived from what's in langs/ directory. * Fixed up localization switching * Switched when we check for a license on UI bootstrap * Tweaked some code * Fixed the bug where dashboard wasn't loading and made it so language switching is working. * Fixed a bug on dashboard with languagePath * Converted user-scrobble-history.component.html * Converted spoiler.component.html * Converted review-series-modal.component.html * Converted review-card-modal.component.html * Updated the readme * Translated using Weblate (English) Currently translated at 100.0% (54 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/en/ * Converted review-card.component.html * Deleted dead component * Converted want-to-read.component.html * Added translation using Weblate (Korean) * Translated using Weblate (Spanish) Currently translated at 40.7% (22 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/ * Translated using Weblate (Korean) Currently translated at 62.9% (34 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-preferences.component.html * Translated using Weblate (Korean) Currently translated at 92.5% (50 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-holds.component.html * Converted theme-manager.component.html * Converted restriction-selector.component.html * Converted manage-devices.component.html * Converted edit-device.component.html * Converted change-password.component.html * Converted change-email.component.html * Converted change-age-restriction.component.html * Converted api-key.component.html * Converted anilist-key.component.html * Converted typeahead.component.html * Converted user-stats-info-cards.component.html * Converted user-stats.component.html * Converted top-readers.component.html * Converted some pipes and ensure translation is loaded before the app. * Finished all but one pipe for localization * Converted directory-picker.component.html * Converted library-access-modal.component.html * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Merged weblate in * ... -> … update * Updated the readme * Updateded all fonts to be woff2 * Cleaned up some strings to increase re-use * Removed an old flow (that doesn't exist in backend any longer) from when we introduced emails on Kavita. * Converted Series detail * Lots more converted * Lots more converted & hooked up the ability to flatten during prod build the language files. * Lots more converted * Lots more converted & fixed a bunch of broken pipes due to inject() * Lots more converted * Lots more converted * Lots more converted & fixed some bad keys * Lots more converted * Fixed some bugs with admin dasbhoard nested tabs not rendering on first load due to not using onpush change detection * Fixed up some localization errors and fixed forgot password error when the user doesn't have change password permission * Fixed a stupid build issue again * Started adding errors for interceptor and backend. * Finished off manga-reader * More translations * Few fixes * Fixed a bug where character tag badges weren't showing the name on chapter info * All components are translated * All toasts are translated * All confirm/alerts are translated * Trying something new for the backend * Migrated the localization strings for the backend into a new file. * Updated the localization service to be able to do backend localization with fallback to english. * Cleaned up some external reviews code to reduce looping * Localized AccountController.cs * 60% done with controllers * All controllers are done * All KavitaExceptions are covered * Some shakeout fixes * Prep for initial merge * Everything is done except options and basic shakeout proves response times are good. Unit tests are broken. * Fixed up the unit tests * All unit tests are now working * Removed some quantifier * I'm not sure I can support localization for some Volume/Chapter/Book strings within the codebase. --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: majora2007 <kavitareader@gmail.com> Co-authored-by: expertjun <jtrobin@naver.com> Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
This commit is contained in:
parent
670bf82c38
commit
3b23d63234
389 changed files with 13652 additions and 7925 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {inject, Pipe, PipeTransform} from '@angular/core';
|
||||
import { DevicePlatform } from 'src/app/_models/device/device-platform';
|
||||
import {TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Pipe({
|
||||
name: 'devicePlatform',
|
||||
|
|
@ -7,12 +8,14 @@ import { DevicePlatform } from 'src/app/_models/device/device-platform';
|
|||
})
|
||||
export class DevicePlatformPipe implements PipeTransform {
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
transform(value: DevicePlatform): string {
|
||||
switch(value) {
|
||||
case DevicePlatform.Kindle: return 'Kindle';
|
||||
case DevicePlatform.Kobo: return 'Kobo';
|
||||
case DevicePlatform.PocketBook: return 'PocketBook';
|
||||
case DevicePlatform.Custom: return 'Custom';
|
||||
case DevicePlatform.Custom: return this.translocoService.translate('device.platform-pipe.custom');
|
||||
default: return value + '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {inject, Pipe, PipeTransform} from '@angular/core';
|
||||
import { ThemeProvider } from 'src/app/_models/preferences/site-theme';
|
||||
import {TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
|
||||
@Pipe({
|
||||
|
|
@ -8,13 +9,15 @@ import { ThemeProvider } from 'src/app/_models/preferences/site-theme';
|
|||
})
|
||||
export class SiteThemeProviderPipe implements PipeTransform {
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
transform(provider: ThemeProvider | undefined | null): string {
|
||||
if (provider === null || provider === undefined) return '';
|
||||
switch(provider) {
|
||||
case ThemeProvider.System:
|
||||
return 'System';
|
||||
return this.translocoService.translate('site-theme-provider-pipe.system');
|
||||
case ThemeProvider.User:
|
||||
return 'User';
|
||||
return this.translocoService.translate('site-theme-provider-pipe.user');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,53 @@
|
|||
<div class="card mt-2">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<div class="container-fluid row mb-2">
|
||||
<div class="col-10 col-sm-11"><h4 id="anilist-token-header">Scrobbling Providers</h4></div>
|
||||
<div class="col-1 text-end">
|
||||
<button class="btn btn-primary btn-sm" [disabled]="!hasValidLicense" (click)="toggleViewMode()">{{isViewMode ? 'Edit' : 'Cancel'}}</button>
|
||||
<ng-container *transloco="let t; read:'scrobbling-providers'">
|
||||
<div class="card mt-2">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<div class="container-fluid row mb-2">
|
||||
<div class="col-10 col-sm-11"><h4 id="anilist-token-header">{{t('title')}}</h4></div>
|
||||
<div class="col-1 text-end">
|
||||
<button class="btn btn-primary btn-sm" [disabled]="!hasValidLicense" (click)="toggleViewMode()">{{isViewMode ? t('edit') : t('cancel')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="isViewMode">
|
||||
<div class="container-fluid row">
|
||||
<ng-container *ngIf="isViewMode">
|
||||
<div class="container-fluid row">
|
||||
<span class="col-12">
|
||||
<ng-container *ngIf="!hasValidLicense; else showToken">
|
||||
This feature requires an active <a href="" target="_blank" rel="noreferrer nofollow">Kavita+</a> license
|
||||
{{t('requires', {product: 'Kavita+'})}}
|
||||
</ng-container>
|
||||
<ng-template #showToken>
|
||||
<ng-container *ngIf="token && token.length > 0; else noToken">
|
||||
<img class="me-2" width="32" height="32" ngSrc="assets/images/ExternalServices/AniList.png" alt="AniList" ngbTooltip="AniList"> Token Set
|
||||
<i class="error fa-solid fa-exclamation-circle" ngbTooltip="Token Expired" *ngIf="tokenExpired">
|
||||
<span class="visually-hidden">Token Expired</span>
|
||||
<img class="me-2" width="32" height="32" ngSrc="assets/images/ExternalServices/AniList.png" alt="AniList" ngbTooltip="AniList"> {{t('token-set')}}
|
||||
<i class="error fa-solid fa-exclamation-circle" [ngbTooltip]="t('token-expired')" *ngIf="tokenExpired">
|
||||
<span class="visually-hidden">{{t('token-expired')}}</span>
|
||||
</i>
|
||||
</ng-container>
|
||||
<ng-template #noToken>No Token Set</ng-template>
|
||||
<ng-template #noToken>{{t('no-token-set')}}</ng-template>
|
||||
</ng-template>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<p>First time users should click on "Generate" below to allow KavitaPlus to talk with Anilist.
|
||||
Once you authorize the program, copy and paste the token in the input below. You can regenerate your token at any time.</p>
|
||||
<form [formGroup]="formGroup">
|
||||
<div class="form-group mb-3">
|
||||
<label for="anilist-token">AniList Token Goes Here</label>
|
||||
<textarea id="anilist-token" rows="2" cols="3" class="form-control" formControlName="aniListToken"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<a class="flex-fill btn btn-secondary me-2"
|
||||
href="https://anilist.co/api/v2/oauth/authorize?client_id=12809&redirect_url=https://anilist.co/api/v2/oauth/pin&response_type=token"
|
||||
target="_blank" rel="noopener noreferrer">Generate</a>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="anilist-token-header" (click)="saveForm()">Save</button>
|
||||
</ng-container>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<p>{{t('instructions', {service: 'AniList'})}}</p>
|
||||
<form [formGroup]="formGroup">
|
||||
<div class="form-group mb-3">
|
||||
<label for="anilist-token">{{t('token-input-label', {service: 'AniList'})}}</label>
|
||||
<textarea id="anilist-token" rows="2" cols="3" class="form-control" formControlName="aniListToken"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<a class="flex-fill btn btn-secondary me-2"
|
||||
href="https://anilist.co/api/v2/oauth/authorize?client_id=12809&redirect_url=https://anilist.co/api/v2/oauth/pin&response_type=token"
|
||||
target="_blank" rel="noopener noreferrer">{{t('generate')}}</a>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="anilist-token-header" (click)="saveForm()">{{t('save')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.se
|
|||
import {AccountService} from "../../_services/account.service";
|
||||
import { NgbTooltip, NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, NgOptimizedImage } from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-anilist-key',
|
||||
|
|
@ -21,7 +22,7 @@ import { NgIf, NgOptimizedImage } from '@angular/common';
|
|||
styleUrls: ['./anilist-key.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgOptimizedImage, NgbTooltip, NgbCollapse, ReactiveFormsModule]
|
||||
imports: [NgIf, NgOptimizedImage, NgbTooltip, NgbCollapse, ReactiveFormsModule, TranslocoModule]
|
||||
})
|
||||
export class AnilistKeyComponent implements OnInit {
|
||||
|
||||
|
|
@ -65,7 +66,7 @@ export class AnilistKeyComponent implements OnInit {
|
|||
|
||||
saveForm() {
|
||||
this.scrobblingService.updateAniListToken(this.formGroup.get('aniListToken')!.value).subscribe(() => {
|
||||
this.toastr.success('AniList Token has been updated');
|
||||
this.toastr.success(translate('toasts.anilist-token-updated'));
|
||||
this.token = this.formGroup.get('aniListToken')!.value;
|
||||
this.resetForm();
|
||||
this.isViewMode = true;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
<div class="mb-3">
|
||||
<label for="api-key--{{title}}" class="form-label">{{title}}</label><span *ngIf="tooltipText.length > 0"> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0"></i></span>
|
||||
<ng-template #tooltip>{{tooltipText}}</ng-template>
|
||||
<div class="input-group">
|
||||
<input #apiKey type="text" readonly class="form-control" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||
<div id="button-addon4">
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="copy()" title="Copy"><span class="visually-hidden">Copy</span><i class="fa fa-copy" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-danger" type="button" [ngbTooltip]="tipContent" (click)="refresh()" *ngIf="showRefresh"><span class="visually-hidden">Regenerate</span><i class="fa fa-sync-alt" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<ng-template #tipContent>
|
||||
Regenerating your API key will invalidate any existing clients.
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *transloco="let t; read:'api-key'">
|
||||
<div class="mb-3">
|
||||
<label for="api-key--{{title}}" class="form-label">{{title}}</label><span *ngIf="tooltipText.length > 0"> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0"></i></span>
|
||||
<ng-template #tooltip>{{tooltipText}}</ng-template>
|
||||
<div class="input-group">
|
||||
<input #apiKey type="text" readonly class="form-control" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||
<div id="button-addon4">
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="copy()" [title]="t('copy')"><span class="visually-hidden">Copy</span><i class="fa fa-copy" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-danger" type="button" [ngbTooltip]="tipContent" (click)="refresh()" *ngIf="showRefresh"><span class="visually-hidden">Regenerate</span><i class="fa fa-sync-alt" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<ng-template #tipContent>
|
||||
{{t('regen-warning')}}
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {Clipboard} from '@angular/cdk/clipboard';
|
|||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf } from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-api-key',
|
||||
|
|
@ -21,7 +22,7 @@ import { NgIf } from '@angular/common';
|
|||
styleUrls: ['./api-key.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgbTooltip]
|
||||
imports: [NgIf, NgbTooltip, TranslocoModule]
|
||||
})
|
||||
export class ApiKeyComponent implements OnInit {
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ export class ApiKeyComponent implements OnInit {
|
|||
@ViewChild('apiKey') inputElem!: ElementRef;
|
||||
key: string = '';
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
|
||||
|
||||
constructor(private confirmService: ConfirmService, private accountService: AccountService, private toastr: ToastrService, private clipboard: Clipboard,
|
||||
|
|
@ -43,7 +45,7 @@ export class ApiKeyComponent implements OnInit {
|
|||
if (user) {
|
||||
key = user.apiKey;
|
||||
} else {
|
||||
key = 'ERROR - KEY NOT SET';
|
||||
key = this.translocoService.translate('api-key.no-key');
|
||||
}
|
||||
|
||||
if (this.transform != undefined) {
|
||||
|
|
@ -61,13 +63,13 @@ export class ApiKeyComponent implements OnInit {
|
|||
}
|
||||
|
||||
async refresh() {
|
||||
if (!await this.confirmService.confirm('This will invalidate any OPDS configurations you have setup. Are you sure you want to continue?')) {
|
||||
if (!await this.confirmService.confirm(this.translocoService.translate('api-key.confirm-reset'))) {
|
||||
return;
|
||||
}
|
||||
this.accountService.resetApiKey().subscribe(newKey => {
|
||||
this.key = newKey;
|
||||
this.cdRef.markForCheck();
|
||||
this.toastr.success('API Key reset');
|
||||
this.toastr.success(this.translocoService.translate('api-key.key-reset'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,38 +1,41 @@
|
|||
<div class="card mt-2">
|
||||
<ng-container *transloco="let t; read:'change-age-restriction'">
|
||||
<div class="card mt-2">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<div class="container-fluid row mb-2">
|
||||
<div class="col-10 col-sm-11"><h4 id="age-restriction">Age Restriction</h4></div>
|
||||
<div class="col-1 text-end">
|
||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()" *ngIf="(hasChangeAgeRestrictionAbility | async)">{{isViewMode ? 'Edit' : 'Cancel'}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-title">
|
||||
<div class="container-fluid row mb-2">
|
||||
<div class="col-10 col-sm-11"><h4 id="age-restriction">{{t('age-restriction-label')}}</h4></div>
|
||||
<div class="col-1 text-end">
|
||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()" *ngIf="(hasChangeAgeRestrictionAbility | async)">{{isViewMode ? t('edit') : t('cancel')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="isViewMode">
|
||||
<div class="container-fluid row">
|
||||
<span class="col-12">{{user?.ageRestriction?.ageRating| ageRating | async}}
|
||||
<ng-container *ngIf="user?.ageRestriction?.ageRating !== AgeRating.NotApplicable && user?.ageRestriction?.includeUnknowns">
|
||||
<span class="ms-1 me-1">+</span> Unknowns
|
||||
<ng-container *ngIf="isViewMode">
|
||||
<div class="container-fluid row">
|
||||
<span class="col-12">{{user?.ageRestriction?.ageRating | ageRating }}
|
||||
<ng-container *ngIf="user?.ageRestriction?.ageRating !== AgeRating.NotApplicable && user?.ageRestriction?.includeUnknowns">
|
||||
<span class="ms-1 me-1">+</span> {{t('unknowns')}}
|
||||
</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<ng-container *ngIf="user">
|
||||
<app-restriction-selector (selected)="updateRestrictionSelection($event)" [showContext]="false" [member]="user" [reset]="reset"></app-restriction-selector>
|
||||
|
||||
<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-secondary me-2" aria-describedby="age-restriction" (click)="resetForm()">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="age-restriction" (click)="saveForm()">Save</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<ng-container *ngIf="user">
|
||||
<app-restriction-selector (selected)="updateRestrictionSelection($event)" [showContext]="false" [member]="user" [reset]="reset"></app-restriction-selector>
|
||||
|
||||
<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-secondary me-2" aria-describedby="age-restriction" (click)="resetForm()">{{t('reset')}}</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="age-restriction" (click)="saveForm()">{{t('save')}}</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ import {
|
|||
Component, DestroyRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Observable, of, Subject, takeUntil, shareReplay, map, take } from 'rxjs';
|
||||
import { Observable, of, shareReplay, map, take } from 'rxjs';
|
||||
import { AgeRestriction } from 'src/app/_models/metadata/age-restriction';
|
||||
import { AgeRating } from 'src/app/_models/metadata/age-rating';
|
||||
import { User } from 'src/app/_models/user';
|
||||
|
|
@ -18,6 +17,7 @@ import { AgeRatingPipe } from '../../pipe/age-rating.pipe';
|
|||
import { RestrictionSelectorComponent } from '../restriction-selector/restriction-selector.component';
|
||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, AsyncPipe } from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-age-restriction',
|
||||
|
|
@ -25,7 +25,7 @@ import { NgIf, AsyncPipe } from '@angular/common';
|
|||
styleUrls: ['./change-age-restriction.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgbCollapse, RestrictionSelectorComponent, AsyncPipe, AgeRatingPipe]
|
||||
imports: [NgIf, NgbCollapse, RestrictionSelectorComponent, AsyncPipe, AgeRatingPipe, TranslocoModule]
|
||||
})
|
||||
export class ChangeAgeRestrictionComponent implements OnInit {
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ export class ChangeAgeRestrictionComponent implements OnInit {
|
|||
if (this.user === undefined) { return; }
|
||||
|
||||
this.accountService.updateAgeRestriction(this.selectedRestriction.ageRating, this.selectedRestriction.includeUnknowns).subscribe(() => {
|
||||
this.toastr.success('Age Restriction has been updated');
|
||||
this.toastr.success(translate('toasts.age-restriction-updated'));
|
||||
this.originalRestriction = this.selectedRestriction;
|
||||
if (this.user) {
|
||||
this.user.ageRestriction.ageRating = this.selectedRestriction.ageRating;
|
||||
|
|
|
|||
|
|
@ -1,79 +1,80 @@
|
|||
<div class="card mt-2">
|
||||
<ng-container *transloco="let t; read:'change-email'">
|
||||
<div class="card mt-2">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<div class="container-fluid row mb-2">
|
||||
<div class="col-10 col-sm-11">
|
||||
<h4 id="email-card">Email
|
||||
<ng-container *ngIf="!emailConfirmed">
|
||||
<i class="fa-solid fa-circle ms-1 confirm-icon" aria-hidden="true" ngbTooltip="This email is not confirmed"></i>
|
||||
<span class="visually-hidden">This email is not confirmed</span>
|
||||
</ng-container>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-1 text-end">
|
||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()">{{isViewMode ? 'Edit' : 'Cancel'}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-title">
|
||||
<div class="container-fluid row mb-2">
|
||||
<div class="col-10 col-sm-11">
|
||||
<h4 id="email-card">{{t('email-label')}}
|
||||
<ng-container *ngIf="!emailConfirmed">
|
||||
<i class="fa-solid fa-circle ms-1 confirm-icon" aria-hidden="true" [ngbTooltip]="t('email-not-confirmed')"></i>
|
||||
<span class="visually-hidden">{{t('email-not-confirmed')}}</span>
|
||||
</ng-container>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-1 text-end">
|
||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()">{{isViewMode ? 'Edit' : 'Cancel'}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="isViewMode">
|
||||
<div class="container-fluid row">
|
||||
<span class="col-12">{{user?.email}}</span>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="isViewMode">
|
||||
<div class="container-fluid row">
|
||||
<span class="col-12">{{user?.email}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<ng-container>
|
||||
<div class="alert alert-danger" role="alert" *ngIf="errors.length > 0">
|
||||
<div *ngFor="let error of errors">{{error}}</div>
|
||||
</div>
|
||||
<form [formGroup]="form">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label visually-hidden">{{t('email-label')}}</label>
|
||||
<input class="form-control custom-input" type="email" id="email" formControlName="email"
|
||||
[class.is-invalid]="form.get('email')?.invalid && form.get('email')?.touched">
|
||||
<div id="email-validations" class="invalid-feedback" *ngIf="form.dirty || form.touched">
|
||||
<div *ngIf="form.get('email')?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">{{t('current-password-label')}}</label>
|
||||
<input class="form-control custom-input" type="password" id="password" formControlName="password"
|
||||
[class.is-invalid]="form.get('password')?.invalid && form.get('password')?.touched">
|
||||
<div id="password-validations" class="invalid-feedback" *ngIf="form.dirty || form.touched">
|
||||
<div *ngIf="form.get('password')?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
</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-secondary me-2" aria-describedby="email-card" (click)="resetForm()">{{t('reset')}}</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="email-card" (click)="saveForm()" [disabled]="!form.valid || !(form.dirty || form.touched)">{{t('save')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<ng-container>
|
||||
<div class="alert alert-danger" role="alert" *ngIf="errors.length > 0">
|
||||
<div *ngFor="let error of errors">{{error}}</div>
|
||||
</div>
|
||||
<form [formGroup]="form">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label visually-hidden">Email</label>
|
||||
<input class="form-control custom-input" type="email" id="email" formControlName="email"
|
||||
[class.is-invalid]="form.get('email')?.invalid && form.get('email')?.touched">
|
||||
<div id="email-validations" class="invalid-feedback" *ngIf="form.dirty || form.touched">
|
||||
<div *ngIf="form.get('email')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="emailLink !== ''">
|
||||
<h4>{{t('email-updated-title')}}</h4>
|
||||
<p>{{t('email-updated-description')}}</p>
|
||||
<a class="email-link" href="{{emailLink}}" target="_blank" rel="noopener noreferrer">{{t('setup-user-account')}}</a>
|
||||
<app-api-key [title]="t('invite-url-label')" [tooltipText]="t('invite-url-tooltip')" [showRefresh]="false" [transform]="makeLink"></app-api-key>
|
||||
</ng-container>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Current Password</label>
|
||||
<input class="form-control custom-input" type="password" id="password" formControlName="password"
|
||||
[class.is-invalid]="form.get('password')?.invalid && form.get('password')?.touched">
|
||||
<div id="password-validations" class="invalid-feedback" *ngIf="form.dirty || form.touched">
|
||||
<div *ngIf="form.get('password')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #noPermission>
|
||||
<p>{{t('permission-error')}}</p>
|
||||
</ng-template>
|
||||
</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-secondary me-2" aria-describedby="email-card" (click)="resetForm()">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="email-card" (click)="saveForm()" [disabled]="!form.valid || !(form.dirty || form.touched)">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="emailLink !== ''">
|
||||
<h4>Email Updated</h4>
|
||||
<p>You can use the following link below to confirm the email for your account.
|
||||
If your server is externally accessible, an email will have been sent to the email and the link can be used to confirm the email.
|
||||
</p>
|
||||
<a class="email-link" href="{{emailLink}}" target="_blank" rel="noopener noreferrer">Setup user's account</a>
|
||||
<app-api-key title="Invite Url" tooltipText="Copy this and paste in a new tab. You may need to log out." [showRefresh]="false" [transform]="makeLink"></app-api-key>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #noPermission>
|
||||
<p>You do not have permission to change your password. Reach out to the admin of the server.</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||
import { ApiKeyComponent } from '../api-key/api-key.component';
|
||||
import { NgbTooltip, NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, NgFor } from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-email',
|
||||
|
|
@ -16,7 +17,7 @@ import { NgIf, NgFor } from '@angular/common';
|
|||
styleUrls: ['./change-email.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgbTooltip, NgbCollapse, NgFor, ReactiveFormsModule, ApiKeyComponent]
|
||||
imports: [NgIf, NgbTooltip, NgbCollapse, NgFor, ReactiveFormsModule, ApiKeyComponent, TranslocoModule]
|
||||
})
|
||||
export class ChangeEmailComponent implements OnInit {
|
||||
|
||||
|
|
@ -61,12 +62,12 @@ export class ChangeEmailComponent implements OnInit {
|
|||
this.accountService.updateEmail(model.email, model.password).subscribe((updateEmailResponse: UpdateEmailResponse) => {
|
||||
if (updateEmailResponse.emailSent) {
|
||||
if (updateEmailResponse.hadNoExistingEmail) {
|
||||
this.toastr.success('An email has been sent to ' + model.email + ' for confirmation.');
|
||||
this.toastr.success(translate('toasts.email-sent-to-no-existing', {email: model.email}));
|
||||
} else {
|
||||
this.toastr.success('An email has been sent to your old email address for confirmation');
|
||||
this.toastr.success(translate('toasts.email-send-to'));
|
||||
}
|
||||
} else {
|
||||
this.toastr.success('The server is not publicly accessible. Ask the admin to fetch your confirmation link from the logs');
|
||||
this.toastr.success(translate('toasts.change-email-private'));
|
||||
}
|
||||
|
||||
this.isViewMode = true;
|
||||
|
|
|
|||
|
|
@ -1,75 +1,73 @@
|
|||
<div class="card mt-2">
|
||||
<ng-container *transloco="let t; read:'change-password'">
|
||||
<div class="card mt-2">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<div class="container-fluid row mb-2">
|
||||
<div class="col-10 col-sm-11"><h4>Password</h4></div>
|
||||
<div class="col-1 text-end">
|
||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()" *ngIf="(hasChangePasswordAbility | async)">{{isViewMode ? 'Edit' : 'Cancel'}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-title">
|
||||
<div class="container-fluid row mb-2">
|
||||
<div class="col-10 col-sm-11"><h4>{{t('password-label')}}</h4></div>
|
||||
<div class="col-1 text-end">
|
||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()" *ngIf="(hasChangePasswordAbility | async)">{{isViewMode ? t('edit') : t('cancel')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="isViewMode">
|
||||
<div class="container-fluid row">
|
||||
<span class="col-12">***************</span>
|
||||
<ng-container *ngIf="isViewMode">
|
||||
<div class="container-fluid row">
|
||||
<span class="col-12">***************</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<ng-container>
|
||||
<div class="alert alert-danger" role="alert" *ngIf="resetPasswordErrors.length > 0">
|
||||
<div *ngFor="let error of resetPasswordErrors">{{error}}</div>
|
||||
</div>
|
||||
<form [formGroup]="passwordChangeForm">
|
||||
<div class="mb-3">
|
||||
<label for="oldpass" class="form-label">{{t('current-password-label')}}</label>
|
||||
<input class="form-control custom-input" type="password" id="oldpass" formControlName="oldPassword"
|
||||
[class.is-invalid]="passwordChangeForm.get('oldPassword')?.invalid && passwordChangeForm.get('oldPassword')?.touched">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
||||
<div *ngIf="passwordChangeForm.get('oldPassword')?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new-password">{{t('new-password-label')}}</label>
|
||||
<input class="form-control" type="password" id="new-password" formControlName="password" aria-describedby="new-password-validations"
|
||||
[class.is-invalid]="passwordChangeForm.get('password')?.invalid && passwordChangeForm.get('password')?.touched">
|
||||
<div id="new-password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
||||
<div *ngIf="password?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm-password">{{t('confirm-password-label')}}</label>
|
||||
<input class="form-control" type="password" id="confirm-password" formControlName="confirmPassword" aria-describedby="confirm-password-validations"
|
||||
[class.is-invalid]="passwordChangeForm.get('confirmPassword')?.invalid && passwordChangeForm.get('confirmPassword')?.touched">
|
||||
<div id="confirm-password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
||||
<div *ngIf="!passwordsMatch">
|
||||
{{t('passwords-must-match')}}
|
||||
</div>
|
||||
<div *ngIf="confirmPassword?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
</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-secondary me-2" aria-describedby="password-panel" (click)="resetPasswordForm()">{{t('reset')}}</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="password-panel" (click)="savePasswordForm()" [disabled]="!passwordChangeForm.valid || !(passwordChangeForm.dirty || passwordChangeForm.touched)">{{t('save')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<ng-container>
|
||||
<div class="alert alert-danger" role="alert" *ngIf="resetPasswordErrors.length > 0">
|
||||
<div *ngFor="let error of resetPasswordErrors">{{error}}</div>
|
||||
</div>
|
||||
<form [formGroup]="passwordChangeForm">
|
||||
<div class="mb-3">
|
||||
<label for="oldpass" class="form-label">Current Password</label>
|
||||
<input class="form-control custom-input" type="password" id="oldpass" formControlName="oldPassword"
|
||||
[class.is-invalid]="passwordChangeForm.get('oldPassword')?.invalid && passwordChangeForm.get('oldPassword')?.touched">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
||||
<div *ngIf="passwordChangeForm.get('oldPassword')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new-password">New Password</label>
|
||||
<input class="form-control" type="password" id="new-password" formControlName="password" aria-describedby="new-password-validations"
|
||||
[class.is-invalid]="passwordChangeForm.get('password')?.invalid && passwordChangeForm.get('password')?.touched">
|
||||
<div id="new-password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
||||
<div *ngIf="password?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm-password">Confirm Password</label>
|
||||
<input class="form-control" type="password" id="confirm-password" formControlName="confirmPassword" aria-describedby="confirm-password-validations"
|
||||
[class.is-invalid]="passwordChangeForm.get('confirmPassword')?.invalid && passwordChangeForm.get('confirmPassword')?.touched">
|
||||
<div id="confirm-password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
||||
<div *ngIf="!passwordsMatch">
|
||||
Passwords must match
|
||||
</div>
|
||||
<div *ngIf="confirmPassword?.errors?.required">
|
||||
This field is required
|
||||
</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-secondary me-2" aria-describedby="password-panel" (click)="resetPasswordForm()">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="password-panel" (click)="savePasswordForm()" [disabled]="!passwordChangeForm.valid || !(passwordChangeForm.dirty || passwordChangeForm.touched)">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #noPermission>
|
||||
<p>You do not have permission to change your password. Reach out to the admin of the server.</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<ng-template #noPermission>
|
||||
<p>{{t('permission-error')}}</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { AccountService } from 'src/app/_services/account.service';
|
|||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, NgFor, AsyncPipe } from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-password',
|
||||
|
|
@ -22,7 +23,7 @@ import { NgIf, NgFor, AsyncPipe } from '@angular/common';
|
|||
styleUrls: ['./change-password.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgbCollapse, NgFor, ReactiveFormsModule, AsyncPipe]
|
||||
imports: [NgIf, NgbCollapse, NgFor, ReactiveFormsModule, AsyncPipe, TranslocoModule]
|
||||
})
|
||||
export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
|
@ -82,7 +83,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
|||
const model = this.passwordChangeForm.value;
|
||||
this.resetPasswordErrors = [];
|
||||
this.observableHandles.push(this.accountService.resetPassword(this.user?.username, model.confirmPassword, model.oldPassword).subscribe(() => {
|
||||
this.toastr.success('Password has been updated');
|
||||
this.toastr.success(translate('toasts.password-updated'));
|
||||
this.resetPasswordForm();
|
||||
this.isViewMode = true;
|
||||
}, err => {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,53 @@
|
|||
<div class="card">
|
||||
<ng-container *transloco="let t; read:'edit-device'">
|
||||
<div class="card">
|
||||
<form [formGroup]="settingsForm" class="card-body">
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="settings-name" class="form-label">Device Name</label>
|
||||
<input id="settings-name" class="form-control" formControlName="name" type="text" [class.is-invalid]="settingsForm.get('name')?.invalid && settingsForm.get('name')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('name')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="email" class="form-label">Email</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="emailTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #emailTooltip>This email will be used to accept the file via Send To</ng-template>
|
||||
<span class="visually-hidden" id="email-help">The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</span>
|
||||
|
||||
<input id="email" aria-describedby="email-help"
|
||||
class="form-control" formControlName="email" type="email" inputmode="email"
|
||||
placeholder="id@kindle.com" [class.is-invalid]="settingsForm.get('email')?.invalid && settingsForm.get('email')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('email')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.email">
|
||||
This must be a valid email
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="device-platform" class="form-label">Device Platform</label>
|
||||
<select id="device-platform" aria-describedby="device-platform-help" class="form-select" formControlName="platform"
|
||||
[class.is-invalid]="settingsForm.get('platform')?.invalid && settingsForm.get('platform')?.touched">
|
||||
<option *ngFor="let patform of devicePlatforms" [value]="patform">{{patform | devicePlatform}}</option>
|
||||
</select>
|
||||
<ng-container *ngIf="settingsForm.get('platform')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="settings-name" class="form-label">{{t('device-name-label')}}</label>
|
||||
<input id="settings-name" class="form-control" formControlName="name" type="text" [class.is-invalid]="settingsForm.get('name')?.invalid && settingsForm.get('name')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('name')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('required-field')}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="addDevice()" [disabled]="!settingsForm.dirty">Save</button>
|
||||
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="email" class="form-label">{{t('email-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="emailTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #emailTooltip>{{t('email-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="email-help">
|
||||
<ng-container [ngTemplateOutlet]="emailTooltip"></ng-container>
|
||||
</span>
|
||||
|
||||
<input id="email" aria-describedby="email-help"
|
||||
class="form-control" formControlName="email" type="email" inputmode="email"
|
||||
placeholder="id@kindle.com" [class.is-invalid]="settingsForm.get('email')?.invalid && settingsForm.get('email')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('email')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.email">
|
||||
{{t('valid-email')}}
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('required-field')}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="device-platform" class="form-label">{{t('device-platform-label')}}</label>
|
||||
<select id="device-platform" aria-describedby="device-platform-help" class="form-select" formControlName="platform"
|
||||
[class.is-invalid]="settingsForm.get('platform')?.invalid && settingsForm.get('platform')?.touched">
|
||||
<option *ngFor="let platform of devicePlatforms" [value]="platform">{{platform | devicePlatform}}</option>
|
||||
</select>
|
||||
<ng-container *ngIf="settingsForm.get('platform')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('required-field')}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="addDevice()" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -6,21 +6,20 @@ import {
|
|||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges
|
||||
} from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Device } from 'src/app/_models/device/device';
|
||||
import { DevicePlatform, devicePlatforms } from 'src/app/_models/device/device-platform';
|
||||
import { DeviceService } from 'src/app/_services/device.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { DevicePlatformPipe } from '../_pipes/device-platform.pipe';
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, NgFor } from '@angular/common';
|
||||
import {NgIf, NgFor, NgTemplateOutlet} from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-device',
|
||||
|
|
@ -28,7 +27,7 @@ import { NgIf, NgFor } from '@angular/common';
|
|||
styleUrls: ['./edit-device.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgIf, NgbTooltip, NgFor, DevicePlatformPipe]
|
||||
imports: [ReactiveFormsModule, NgIf, NgbTooltip, NgFor, DevicePlatformPipe, TranslocoModule, NgTemplateOutlet]
|
||||
})
|
||||
export class EditDeviceComponent implements OnInit, OnChanges {
|
||||
|
||||
|
|
@ -76,7 +75,7 @@ export class EditDeviceComponent implements OnInit, OnChanges {
|
|||
if (this.device !== undefined) {
|
||||
this.deviceService.updateDevice(this.device.id, this.settingsForm.value.name, parseInt(this.settingsForm.value.platform, 10), this.settingsForm.value.email).subscribe(() => {
|
||||
this.settingsForm.reset();
|
||||
this.toastr.success('Device updated');
|
||||
this.toastr.success(translate('toasts.device-updated'));
|
||||
this.cdRef.markForCheck();
|
||||
this.deviceUpdated.emit();
|
||||
})
|
||||
|
|
@ -85,7 +84,7 @@ export class EditDeviceComponent implements OnInit, OnChanges {
|
|||
|
||||
this.deviceService.createDevice(this.settingsForm.value.name, parseInt(this.settingsForm.value.platform, 10), this.settingsForm.value.email).subscribe(() => {
|
||||
this.settingsForm.reset();
|
||||
this.toastr.success('Device created');
|
||||
this.toastr.success(translate('toasts.device-created'));
|
||||
this.cdRef.markForCheck();
|
||||
this.deviceAdded.emit();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,39 +1,42 @@
|
|||
<div class="container-fluid">
|
||||
<ng-container *transloco="let t; read:'manage-devices'">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>Device Manager</h3></div>
|
||||
<div class="col-4">
|
||||
<button class="btn btn-primary float-end" (click)="collapse.toggle()" [attr.aria-expanded]="!addDeviceIsCollapsed"
|
||||
aria-controls="collapseExample">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-8"><h3>{{t('title')}}</h3></div>
|
||||
<div class="col-4">
|
||||
<button class="btn btn-primary float-end" (click)="collapse.toggle()" [attr.aria-expanded]="!addDeviceIsCollapsed"
|
||||
aria-controls="collapseExample">
|
||||
<i class="fa fa-plus me-1" aria-hidden="true"></i>{{t('add')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
This section is for you to setup devices that cannot connect to Kavita via a web browser and instead have an email address that accepts files.
|
||||
<p>
|
||||
{{t('description')}}
|
||||
</p>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="addDeviceIsCollapsed">
|
||||
<app-edit-device [device]="device" (deviceAdded)="loadDevices()" (deviceUpdated)="loadDevices()"></app-edit-device>
|
||||
<app-edit-device [device]="device" (deviceAdded)="loadDevices()" (deviceUpdated)="loadDevices()"></app-edit-device>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-2">
|
||||
<h4>Devices</h4>
|
||||
<p *ngIf="devices.length === 0">
|
||||
There are no devices setup yet
|
||||
</p>
|
||||
<ng-container *ngFor="let device of devices">
|
||||
<div class="card col-auto me-3 mb-3" style="width: 18rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{device.name | sentenceCase}}</h5>
|
||||
Platform: <h6 class="card-subtitle mb-2 text-muted">{{device.platform | devicePlatform}}</h6>
|
||||
Email: <h6 class="card-subtitle mb-2 text-muted">{{device.emailAddress}}</h6>
|
||||
<h4>{{t('devices-title')}}</h4>
|
||||
<p *ngIf="devices.length === 0">
|
||||
{{t('no-devices')}}
|
||||
</p>
|
||||
<ng-container *ngFor="let device of devices">
|
||||
<div class="card col-auto me-3 mb-3" style="width: 18rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{device.name | sentenceCase}}</h5>
|
||||
{{t('platform-label')}}<h6 class="card-subtitle mb-2 text-muted">{{device.platform | devicePlatform}}</h6>
|
||||
{{t('email-label')}}<h6 class="card-subtitle mb-2 text-muted">{{device.emailAddress}}</h6>
|
||||
|
||||
<button class="btn btn-danger me-2" (click)="deleteDevice(device)">Delete</button>
|
||||
<button class="btn btn-primary" (click)="editDevice(device)">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<button class="btn btn-danger me-2" (click)="deleteDevice(device)">{{t('delete')}}</button>
|
||||
<button class="btn btn-primary" (click)="editDevice(device)">{{t('edit')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Device } from 'src/app/_models/device/device';
|
||||
import { DevicePlatform, devicePlatforms } from 'src/app/_models/device/device-platform';
|
||||
import { DeviceService } from 'src/app/_services/device.service';
|
||||
import { DevicePlatformPipe } from '../_pipes/device-platform.pipe';
|
||||
import { SentenceCasePipe } from '../../pipe/sentence-case.pipe';
|
||||
import { NgIf, NgFor } from '@angular/common';
|
||||
import { EditDeviceComponent } from '../edit-device/edit-device.component';
|
||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-devices',
|
||||
|
|
@ -17,7 +16,7 @@ import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
|||
styleUrls: ['./manage-devices.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgbCollapse, EditDeviceComponent, NgIf, NgFor, SentenceCasePipe, DevicePlatformPipe]
|
||||
imports: [NgbCollapse, EditDeviceComponent, NgIf, NgFor, SentenceCasePipe, DevicePlatformPipe, TranslocoModule]
|
||||
})
|
||||
export class ManageDevicesComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
|
@ -28,7 +27,7 @@ export class ManageDevicesComponent implements OnInit, OnDestroy {
|
|||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
constructor(public deviceService: DeviceService, private toastr: ToastrService,
|
||||
constructor(public deviceService: DeviceService, private toastr: ToastrService,
|
||||
private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -49,7 +48,7 @@ export class ManageDevicesComponent implements OnInit, OnDestroy {
|
|||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
deleteDevice(device: Device) {
|
||||
this.deviceService.deleteDevice(device.id).subscribe(() => {
|
||||
const index = this.devices.indexOf(device);
|
||||
|
|
|
|||
|
|
@ -1,31 +1,35 @@
|
|||
<ng-container *ngIf="restrictionForm">
|
||||
<ng-container *transloco="let t; read:'restriction-selector'">
|
||||
<ng-container *ngIf="restrictionForm">
|
||||
<ng-container *ngIf="showContext">
|
||||
<h4>Age Rating Restriction</h4>
|
||||
<p>When selected, all series and reading lists that have at least one item that is greater than the selected restriction will be pruned from results.
|
||||
<ng-container *ngIf="isAdmin">This is not applicable for admins.</ng-container>
|
||||
</p>
|
||||
<h4>{{t('title')}}</h4>
|
||||
<p>{{t('description')}}
|
||||
<ng-container *ngIf="isAdmin">{{t('not-applicable-for-admins')}}</ng-container>
|
||||
</p>
|
||||
</ng-container>
|
||||
<form [formGroup]="restrictionForm">
|
||||
<div class="mb-3">
|
||||
<label for="age-rating" class="form-label visually-hidden">Age Rating</label>
|
||||
<div class="input-group">
|
||||
<select class="form-select"id="age-rating" formControlName="ageRating">
|
||||
<option value="-1">No Restriction</option>
|
||||
<option *ngFor="let opt of ageRatings" [value]="opt.value">{{opt.title | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="age-rating" class="form-label visually-hidden">{{t('age-rating-label')}}</label>
|
||||
<div class="input-group">
|
||||
<select class="form-select" id="age-rating" formControlName="ageRating">
|
||||
<option value="-1">{{t('no-restriction')}}</option>
|
||||
<option *ngFor="let opt of ageRatings" [value]="opt.value">{{opt.title | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="auto-close" role="switch" formControlName="ageRestrictionIncludeUnknowns" class="form-check-input" aria-describedby="include-unknowns-help" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="auto-close">{{t('include-unknowns-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="includeUnknownsTooltip" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="auto-close" role="switch" formControlName="ageRestrictionIncludeUnknowns" class="form-check-input" aria-describedby="include-unknowns-help" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="auto-close">Include Unknowns</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="includeUnknownsTooltip" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
|
||||
<ng-template #includeUnknownsTooltip>If true, Unknowns will be allowed with Age Restrcition. This could lead to untagged media leaking to users with Age restrictions.</ng-template>
|
||||
<span class="visually-hidden" id="include-unknowns-help">If true, Unknowns will be allowed with Age Restrcition. This could lead to untagged media leaking to users with Age restrictions.</span>
|
||||
</div>
|
||||
<ng-template #includeUnknownsTooltip>{{t('include-unknowns-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="include-unknowns-help">
|
||||
<ng-container [ngTemplateOutlet]="includeUnknownsTooltip"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import { AgeRatingDto } from 'src/app/_models/metadata/age-rating-dto';
|
|||
import { User } from 'src/app/_models/user';
|
||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, NgFor, TitleCasePipe } from '@angular/common';
|
||||
import {NgIf, NgFor, TitleCasePipe, NgTemplateOutlet} from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-restriction-selector',
|
||||
|
|
@ -15,7 +16,7 @@ import { NgIf, NgFor, TitleCasePipe } from '@angular/common';
|
|||
styleUrls: ['./restriction-selector.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, ReactiveFormsModule, NgFor, NgbTooltip, TitleCasePipe]
|
||||
imports: [NgIf, ReactiveFormsModule, NgFor, NgbTooltip, TitleCasePipe, TranslocoModule, NgTemplateOutlet]
|
||||
})
|
||||
export class RestrictionSelectorComponent implements OnInit, OnChanges {
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ export class RestrictionSelectorComponent implements OnInit, OnChanges {
|
|||
@Input() showContext: boolean = true;
|
||||
@Input() reset: EventEmitter<AgeRestriction> | undefined;
|
||||
@Output() selected: EventEmitter<AgeRestriction> = new EventEmitter<AgeRestriction>();
|
||||
|
||||
|
||||
|
||||
ageRatings: Array<AgeRatingDto> = [];
|
||||
restrictionForm: FormGroup | undefined;
|
||||
|
|
@ -39,7 +40,7 @@ export class RestrictionSelectorComponent implements OnInit, OnChanges {
|
|||
this.restrictionForm = new FormGroup({
|
||||
'ageRating': new FormControl(this.member?.ageRestriction.ageRating || AgeRating.NotApplicable || AgeRating.NotApplicable, []),
|
||||
'ageRestrictionIncludeUnknowns': new FormControl(this.member?.ageRestriction.includeUnknowns || false, []),
|
||||
|
||||
|
||||
});
|
||||
|
||||
if (this.isAdmin) {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
<div class="container-fluid">
|
||||
<ng-container *transloco="let t; read:'theme-manager'">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>Theme Manager</h3></div>
|
||||
<div class="col-4" *ngIf="isAdmin">
|
||||
<button class="btn btn-primary float-end" (click)="scan()">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i> Scan
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-8"><h3>{{t('title')}}</h3></div>
|
||||
<div class="col-4" *ngIf="isAdmin">
|
||||
<button class="btn btn-primary float-end" (click)="scan()">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i> {{t('scan')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p *ngIf="isAdmin">
|
||||
Looking for a light or e-ink theme? We have some custom themes you can use on our <a href="https://github.com/Kareadita/Themes" target="_blank" rel="noopener noreferrer">theme github</a>.
|
||||
<p *ngIf="isAdmin">
|
||||
{{t('looking-for-theme')}}<a href="https://github.com/Kareadita/Themes" target="_blank" rel="noopener noreferrer">{{t('looking-for-theme-continued')}}</a>
|
||||
</p>
|
||||
|
||||
<div class="row g-0">
|
||||
<h4>Site Themes</h4>
|
||||
<ng-container *ngFor="let theme of (themeService.themes$ | async)">
|
||||
<div class="card col-auto me-3 mb-3" style="width: 18rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{theme.name | sentenceCase}}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">{{theme.provider | siteThemeProvider}}</h6>
|
||||
<button class="btn btn-secondary me-2" [disabled]="theme.isDefault" *ngIf="isAdmin" (click)="updateDefault(theme)">Set Default</button>
|
||||
<button class="btn btn-primary" (click)="applyTheme(theme)" [disabled]="currentTheme?.id === theme.id">{{currentTheme?.id === theme.id ? 'Applied' : 'Apply'}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h4>{{t('site-themes')}}</h4>
|
||||
<ng-container *ngFor="let theme of (themeService.themes$ | async)">
|
||||
<div class="card col-auto me-3 mb-3" style="width: 18rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{theme.name | sentenceCase}}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">{{theme.provider | siteThemeProvider}}</h6>
|
||||
<button class="btn btn-secondary me-2" [disabled]="theme.isDefault" *ngIf="isAdmin" (click)="updateDefault(theme)">{{t('set-default')}}</button>
|
||||
<button class="btn btn-primary" (click)="applyTheme(theme)" [disabled]="currentTheme?.id === theme.id">{{currentTheme?.id === theme.id ? t('applied') : t('apply')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@ import {
|
|||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { distinctUntilChanged, Subject, take, takeUntil } from 'rxjs';
|
||||
import { distinctUntilChanged, take } from 'rxjs';
|
||||
import { ThemeService } from 'src/app/_services/theme.service';
|
||||
import { SiteTheme, ThemeProvider } from 'src/app/_models/preferences/site-theme';
|
||||
import { User } from 'src/app/_models/user';
|
||||
|
|
@ -17,6 +15,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||
import { SiteThemeProviderPipe } from '../_pipes/site-theme-provider.pipe';
|
||||
import { SentenceCasePipe } from '../../pipe/sentence-case.pipe';
|
||||
import { NgIf, NgFor, AsyncPipe } from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-theme-manager',
|
||||
|
|
@ -24,7 +23,7 @@ import { NgIf, NgFor, AsyncPipe } from '@angular/common';
|
|||
styleUrls: ['./theme-manager.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgFor, AsyncPipe, SentenceCasePipe, SiteThemeProviderPipe]
|
||||
imports: [NgIf, NgFor, AsyncPipe, SentenceCasePipe, SiteThemeProviderPipe, TranslocoModule]
|
||||
})
|
||||
export class ThemeManagerComponent {
|
||||
|
||||
|
|
@ -32,10 +31,8 @@ export class ThemeManagerComponent {
|
|||
isAdmin: boolean = false;
|
||||
user: User | undefined;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly translocService = inject(TranslocoService);
|
||||
|
||||
get ThemeProvider() {
|
||||
return ThemeProvider;
|
||||
}
|
||||
|
||||
constructor(public themeService: ThemeService, private accountService: AccountService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) {
|
||||
|
|
@ -72,13 +69,13 @@ export class ThemeManagerComponent {
|
|||
|
||||
updateDefault(theme: SiteTheme) {
|
||||
this.themeService.setDefault(theme.id).subscribe(() => {
|
||||
this.toastr.success('Site default has been updated to ' + theme.name);
|
||||
this.toastr.success(this.translocService.translate('theme-manager.updated-toastr', {name: theme.name}));
|
||||
});
|
||||
}
|
||||
|
||||
scan() {
|
||||
this.themeService.scan().subscribe(() => {
|
||||
this.toastr.info('A site theme scan has been queued');
|
||||
this.toastr.info(this.translocService.translate('theme-manager.scan-queued'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<h5>Scrobble Holds</h5>
|
||||
<p>This is a user-managed list of Series that will not be scrobbled to upstream providers. You can remove a series at
|
||||
any time and the next Scrobble-able event (reading progress, rating, want to read status) will trigger events.</p>
|
||||
<ng-container *transloco="let t; read:'user-holds'">
|
||||
<h5>{{t('title')}}</h5>
|
||||
<p>{{t('description')}}</p>
|
||||
|
||||
<div class="row g-0">
|
||||
<ul class="list-group mb-2">
|
||||
<li class="list-group-item list-group-item-light" *ngFor="let hold of holds$ | async">
|
||||
<a class="btn-link" href="/library/{{hold.libraryId}}/series/{{hold.seriesId}}" target="_blank">{{hold.seriesName}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<ul class="list-group mb-2">
|
||||
<li class="list-group-item list-group-item-light" *ngFor="let hold of holds$ | async">
|
||||
<a class="btn-link" href="/library/{{hold.libraryId}}/series/{{hold.seriesId}}" target="_blank">{{hold.seriesName}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@ import {
|
|||
NgbAccordionDirective, NgbAccordionHeader,
|
||||
NgbAccordionItem
|
||||
} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-holds',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ScrobbleEventTypePipe, NgbAccordionDirective, NgbAccordionCollapse, NgbAccordionBody, NgbAccordionItem, NgbAccordionHeader],
|
||||
imports: [CommonModule, ScrobbleEventTypePipe, NgbAccordionDirective, NgbAccordionCollapse, NgbAccordionBody, NgbAccordionItem, NgbAccordionHeader, TranslocoModule],
|
||||
templateUrl: './user-holds.component.html',
|
||||
styleUrls: ['./user-holds.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -1,385 +1,440 @@
|
|||
<app-side-nav-companion-bar>
|
||||
<h2 title>
|
||||
User Dashboard
|
||||
</h2>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | sentenceCase }}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Account">
|
||||
<app-change-email></app-change-email>
|
||||
<app-change-password></app-change-password>
|
||||
<app-change-age-restriction></app-change-age-restriction>
|
||||
<app-anilist-key></app-anilist-key>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Preferences">
|
||||
<p>
|
||||
These are global settings that are bound to your account.
|
||||
</p>
|
||||
<ng-container *transloco="let t; read:'user-preferences'">
|
||||
<app-side-nav-companion-bar>
|
||||
<h2 title>
|
||||
{{t('title')}}
|
||||
</h2>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ t(tab.title) }}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Account">
|
||||
<app-change-email></app-change-email>
|
||||
<app-change-password></app-change-password>
|
||||
<app-change-age-restriction></app-change-age-restriction>
|
||||
<app-anilist-key></app-anilist-key>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Preferences">
|
||||
<p>
|
||||
{{t('pref-description')}}
|
||||
</p>
|
||||
|
||||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||
<div ngbAccordion [closeOthers]="true" #acc="ngbAccordion">
|
||||
<div ngbAccordionItem [id]="AccordionPanelID.GlobalSettings" title="Global Settings" [collapsed]="false">
|
||||
<h2 class="accordion-header" ngbAccordionHeader>
|
||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)" aria-controls="collapseOne">
|
||||
Global Settings
|
||||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||
<div ngbAccordion [closeOthers]="true" #acc="ngbAccordion">
|
||||
<div ngbAccordionItem [id]="AccordionPanelID.GlobalSettings" [collapsed]="false">
|
||||
<h2 class="accordion-header" ngbAccordionHeader>
|
||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)" aria-controls="collapseOne">
|
||||
{{t('global-settings-title')}}
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-global-layoutmode" class="form-label">{{t('page-layout-mode-label')}}</label>
|
||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #layoutModeTooltip>{{t('page-layout-mode-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-global-layoutmode-help">
|
||||
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
|
||||
</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="globalPageLayoutMode" id="settings-global-layoutmode">
|
||||
<option *ngFor="let opt of pageLayoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label>
|
||||
<i class="fa fa-info-circle ms-1"
|
||||
aria-hidden="true" placement="right" [ngbTooltip]="localeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-global-locale-help">
|
||||
<ng-container [ngTemplateOutlet]="localeTooltip"></ng-container>
|
||||
</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="locale" id="settings-global-locale">
|
||||
<option *ngFor="let opt of locales" [value]="opt.isoCode">{{opt.title | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="blur-unread-summaries" role="switch" formControlName="blurUnreadSummaries" class="form-check-input" aria-describedby="settings-global-blurUnreadSummaries-help" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="blur-unread-summaries">{{t('blur-unread-summaries-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="blurUnreadSummariesTooltip" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
|
||||
<ng-template #blurUnreadSummariesTooltip>{{t('blur-unread-summaries-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-global-blurUnreadSummaries-help">
|
||||
<ng-container [ngTemplateOutlet]="blurUnreadSummariesTooltip"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="prompt-download" role="switch" formControlName="promptForDownloadSize" class="form-check-input" aria-describedby="settings-global-promptForDownloadSize-help" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="prompt-download">{{t('prompt-on-download-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="promptForDownloadSizeTooltip" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
|
||||
<ng-template #promptForDownloadSizeTooltip>{{t('prompt-on-download-tooltip', {size: '100'})}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-global-promptForDownloadSize-help">
|
||||
<ng-container [ngTemplateOutlet]="promptForDownloadSizeTooltip"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="no-transitions" role="switch" formControlName="noTransitions" class="form-check-input"
|
||||
aria-describedby="settings-global-noTransitions-help" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="no-transitions">{{t('disable-animations-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="noTransitionsTooltip" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
|
||||
<ng-template #noTransitionsTooltip>{{t('disable-animations-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-global-noTransitions-help">
|
||||
<ng-container [ngTemplateOutlet]="noTransitionsTooltip"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="collapse-relationships" role="switch" formControlName="collapseSeriesRelationships"
|
||||
aria-describedby="settings-collapse-relationships-help" class="form-check-input" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="collapse-relationships">{{t('collapse-series-relationships-label')}}</label>
|
||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="collapseSeriesRelationshipsTooltip" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
<ng-template #collapseSeriesRelationshipsTooltip>{{t('collapse-series-relationships-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-collapse-relationships-help">
|
||||
<ng-container [ngTemplateOutlet]="collapseSeriesRelationshipsTooltip"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="share-reviews" role="switch" formControlName="shareReviews"
|
||||
aria-describedby="settings-share-reviews-help" class="form-check-input" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="share-reviews">{{t('share-series-reviews-label')}}</label>
|
||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="shareReviewsTooltip" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
<ng-template #shareReviewsTooltip>{{t('share-series-reviews-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-share-reviews-help">
|
||||
<ng-container [ngTemplateOutlet]="shareReviewsTooltip"></ng-container>
|
||||
</span>
|
||||
</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-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ngbAccordionItem [id]="AccordionPanelID.ImageReader">
|
||||
<h2 class="accordion-header" ngbAccordionHeader>
|
||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
|
||||
{{t('image-reader-settings-title')}}
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-global-layoutmode" class="form-label">Page Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #layoutModeTooltip>Show items as cards or list view on Series Detail page</ng-template>
|
||||
<span class="visually-hidden" id="settings-global-layoutmode-help">Show items as cards or list view on Series Detail page</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="globalPageLayoutMode" id="settings-global-layoutmode">
|
||||
<option *ngFor="let opt of pageLayoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="blur-unread-summaries" role="switch" formControlName="blurUnreadSummaries" class="form-check-input" aria-describedby="settings-global-blurUnreadSummaries-help" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="blur-unread-summaries">Blur Unread Summaries</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="blurUnreadSummariesTooltip" role="button" tabindex="0"></i>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #readingDirectionTooltip>{{t('reading-direction-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-reading-direction-help">
|
||||
<ng-container [ngTemplateOutlet]="readingDirectionTooltip"></ng-container>
|
||||
</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ng-template #blurUnreadSummariesTooltip>Blurs summary text on volumes or chapters that have no read progress (to avoid spoilers)</ng-template>
|
||||
<span class="visually-hidden" id="settings-global-blurUnreadSummaries-help">Blurs summary text on volumes or chapters that have no read progress (to avoid spoilers)</span>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-scaling-option" class="form-label">{{t('scaling-option-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="scalingOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #scalingOptionTooltip>{{t('scaling-option-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-scaling-option-help">
|
||||
<ng-container [ngTemplateOutlet]="scalingOptionTooltip"></ng-container>
|
||||
</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
|
||||
<option *ngFor="let opt of scalingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="prompt-download" role="switch" formControlName="promptForDownloadSize" class="form-check-input" aria-describedby="settings-global-promptForDownloadSize-help" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="prompt-download">Prompt on Download</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="promptForDownloadSizeTooltip" role="button" tabindex="0"></i>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-pagesplit-option" class="form-label">{{t('page-splitting-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #pageSplitOptionTooltip>{{t('page-splitting-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-pagesplit-option-help">
|
||||
<ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container>
|
||||
</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
|
||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-readingmode-option" class="form-label">{{t('reading-mode-label')}}</label>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
|
||||
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="true">
|
||||
<label for="settings-layoutmode-option" class="form-label">{{t('layout-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #layoutModeTooltip>{{t('layout-mode-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-layoutmode-option-help">
|
||||
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
|
||||
</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
|
||||
<option *ngFor="let opt of layoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-background-color-option" class="form-label">{{t('background-color-label')}}</label>
|
||||
<input [value]="user!.preferences!.backgroundColor"
|
||||
class="form-control"
|
||||
id="settings-background-color-option"
|
||||
(colorPickerChange)="handleBackgroundColorChange()"
|
||||
[style.background]="user!.preferences!.backgroundColor"
|
||||
[cpAlphaChannel]="'disabled'"
|
||||
[(colorPicker)]="user!.preferences!.backgroundColor"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="auto-close">{{t('auto-close-menu-label')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="show-screen-hints">{{t('show-screen-hints-label')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook" class="form-check-input" [value]="true">
|
||||
<label class="form-check-label me-1" for="emulate-book">{{t('emulate-comic-book-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="swipe-to-paginate" role="switch" formControlName="swipeToPaginate" class="form-check-input" [value]="true">
|
||||
<label class="form-check-label me-1" for="swipe-to-paginate">{{t('swipe-to-paginate-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
</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-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ngbAccordionItem [id]="AccordionPanelID.BookReader">
|
||||
<h2 class="accordion-header" ngbAccordionHeader>
|
||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
|
||||
{{t('book-reader-settings-title')}}
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label id="taptopaginate-label" class="form-label"></label>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
|
||||
<label for="taptopaginate" class="form-check-label">{{t('tap-to-paginate-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #tapToPaginateOptionTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-taptopaginate-option-help">
|
||||
<ng-container [ngTemplateOutlet]="tapToPaginateOptionTooltip"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label id="immersivemode-label" class="form-label"></label>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label">
|
||||
<label for="immersivemode" class="form-check-label">{{t('immersive-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #immersivemodeOptionTooltip>{{t('immersive-mode-label')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-immersivemode-option-help">
|
||||
<ng-container [ngTemplateOutlet]="immersivemodeOptionTooltip"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-book-reading-direction-book-help">
|
||||
<ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container>
|
||||
</span>
|
||||
<select id="settings-book-reading-direction" class="form-select" aria-describedby="settings-book-reading-direction-help" formControlName="bookReaderReadingDirection">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ng-template #promptForDownloadSizeTooltip>Prompt when a download exceeds 100MB in size</ng-template>
|
||||
<span class="visually-hidden" id="settings-global-promptForDownloadSize-help">Prompt when a download exceeds 100MB in size</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="no-transitions" role="switch" formControlName="noTransitions" class="form-check-input"
|
||||
aria-describedby="settings-global-noTransitions-help" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="no-transitions">Disable Animations</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="noTransitionsTooltip" role="button" tabindex="0"></i>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-fontfamily-option" class="form-label">{{t('font-family-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #fontFamilyOptionTooltip>{{t('font-family-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-fontfamily-option-help">
|
||||
<ng-container [ngTemplateOutlet]="fontFamilyOptionTooltip"></ng-container>
|
||||
</span>
|
||||
<select id="settings-fontfamily-option" class="form-select" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
||||
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-writing-style" class="form-label me-1">{{t('writing-style-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" aria-describedby="settings-book-writing-style-help" placement="right" [ngbTooltip]="bookWritingStyleToolTip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookWritingStyleToolTip>{{t('writing-style-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-book-writing-style-help">
|
||||
<ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container>
|
||||
</span>
|
||||
<select class="form-select" aria-describedby="settings-book-writing-style-help" formControlName="bookReaderWritingStyle" id="settings-book-writing-style" >
|
||||
<option *ngFor="let opt of bookWritingStyles" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ng-template #noTransitionsTooltip>Turns off animations in the site. Useful for e-ink readers</ng-template>
|
||||
<span class="visually-hidden" id="settings-global-noTransitions-help">Turns off animations in the site. Useful for e-ink readers</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="collapse-relationships" role="switch" formControlName="collapseSeriesRelationships"
|
||||
aria-describedby="settings-collapse-relationships-help" class="form-check-input" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="collapse-relationships">Collapse Series Relationships</label>
|
||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="collapseSeriesRelationshipsTooltip" role="button" tabindex="0"></i>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-layout-mode" class="form-label">{{t('layout-mode-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLayoutModeTooltip>{{t('layout-mode-book-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-book-layout-mode-help">
|
||||
<ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container>
|
||||
</span>
|
||||
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
|
||||
<option *ngFor="let opt of bookLayoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<ng-template #collapseSeriesRelationshipsTooltip>Experimental: Should Kavita show Series that have no relationships or is the parent/prequel</ng-template>
|
||||
<span class="visually-hidden" id="settings-collapse-relationships-help">Experimental: Should Kavita show Series that have no relationships or is the parent/prequel</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="share-reviews" role="switch" formControlName="shareReviews"
|
||||
aria-describedby="settings-share-reviews-help" class="form-check-input" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="share-reviews">Share Series Reviews</label>
|
||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="shareReviewsTooltip" role="button" tabindex="0"></i>
|
||||
<div class="row g-0">
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-color-theme-option" class="form-label">{{t('color-theme-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookColorThemeTooltip>{{t('color-theme-book-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-color-theme-option-help">
|
||||
<ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container>
|
||||
</span>
|
||||
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
|
||||
<option *ngFor="let opt of bookColorThemes" [value]="opt.name">{{opt.name | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<ng-template #shareReviewsTooltip>Should Kavita include your reviews of Series for other users</ng-template>
|
||||
<span class="visually-hidden" id="settings-share-reviews-help">Should Kavita include your reviews of Series for other users</span>
|
||||
</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-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label for="fontsize" class="form-label range-label">{{t('font-size-book-label')}}</label>
|
||||
<input type="range" class="form-range" id="fontsize"
|
||||
min="50" max="300" step="10" formControlName="bookReaderFontSize">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<div class="range-label">
|
||||
<label class="form-label" for="linespacing">{{t('line-height-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLineHeightOptionTooltip>{{t('line-height-book-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-booklineheight-option-help">
|
||||
<ng-container [ngTemplateOutlet]="bookLineHeightOptionTooltip"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
|
||||
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<div class="range-label">
|
||||
<label class="form-label">{{t('margin-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReaderMarginOptionTooltip>{{t('margin-book-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-bookmargin-option-help">
|
||||
<ng-container [ngTemplateOutlet]="bookReaderMarginOptionTooltip"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
|
||||
</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-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ngbAccordionItem [id]="AccordionPanelID.ImageReader" title="Image Reader">
|
||||
<h2 class="accordion-header" ngbAccordionHeader>
|
||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
|
||||
Image Reader
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-reading-direction" class="form-label">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #readingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="visually-hidden" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-scaling-option" class="form-label">Scaling Options</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskBackupTooltip>How to scale the image to your screen.</ng-template>
|
||||
<span class="visually-hidden" id="settings-scaling-option-help">How to scale the image to your screen.</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
|
||||
<option *ngFor="let opt of scalingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-pagesplit-option" class="form-label">Page Splitting</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #pageSplitOptionTooltip>How to split a full width image (ie both left and right images are combined)</ng-template>
|
||||
<span class="visually-hidden" id="settings-pagesplit-option-help">How to split a full width image (ie both left and right images are combined)</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
|
||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-readingmode-option" class="form-label">Reading Mode</label>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
|
||||
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="true">
|
||||
<label for="settings-layoutmode-option" class="form-label">Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #layoutModeTooltip>Render a single image to the screen to two side-by-side images</ng-template>
|
||||
<span class="visually-hidden" id="settings-layoutmode-option-help"><ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
|
||||
<option *ngFor="let opt of layoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-background-color-option" class="form-label">Background Color</label>
|
||||
<input [value]="user.preferences.backgroundColor"
|
||||
class="form-control"
|
||||
id="settings-background-color-option"
|
||||
(colorPickerChange)="handleBackgroundColorChange()"
|
||||
[style.background]="user.preferences.backgroundColor"
|
||||
[cpAlphaChannel]="'disabled'"
|
||||
[(colorPicker)]="user.preferences.backgroundColor"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="show-screen-hints">Show Screen Hints</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook" class="form-check-input" [value]="true">
|
||||
<label class="form-check-label me-1" for="emulate-book">Emulate comic book</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="swipe-to-paginate" role="switch" formControlName="swipeToPaginate" class="form-check-input" [value]="true">
|
||||
<label class="form-check-label me-1" for="swipe-to-paginate">Swipe to Paginate</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
</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-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ngbAccordionItem [id]="AccordionPanelID.BookReader" title="Book Reader">
|
||||
<h2 class="accordion-header" ngbAccordionHeader>
|
||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
|
||||
Book Reader
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label id="taptopaginate-label" class="form-label"></label>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
|
||||
<label for="taptopaginate" class="form-check-label">Tap to Paginate</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #tapToPaginateOptionTooltip>Should the sides of the book reader screen allow tapping on it to move to prev/next page</ng-template>
|
||||
<span class="visually-hidden" id="settings-taptopaginate-option-help">Should the sides of the book reader screen allow tapping on it to move to prev/next page</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label id="immersivemode-label" class="form-label"></label>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label">
|
||||
<label for="immersivemode" class="form-check-label">Immersive Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #immersivemodeOptionTooltip>This will hide the menu behind a click on the reader document and turn tap to paginate on</ng-template>
|
||||
<span class="visually-hidden" id="settings-immersivemode-option-help">This will hide the menu behind a click on the reader document and turn tap to paginate on</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-reading-direction" class="form-label">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReadingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="visually-hidden" id="settings-book-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select id="settings-book-reading-direction" class="form-select" aria-describedby="settings-book-reading-direction-help" formControlName="bookReaderReadingDirection">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-fontfamily-option" class="form-label">Font Family</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #fontFamilyOptionTooltip>Font family to load up. Default will load the book's default font</ng-template>
|
||||
<span class="visually-hidden" id="settings-fontfamily-option-help">Font family to load up. Default will load the book's default font</span>
|
||||
<select id="settings-fontfamily-option" class="form-select" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
||||
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-writing-style" class="form-label me-1">Writing Style</label><i class="fa fa-info-circle" aria-hidden="true" aria-describedby="settings-book-writing-style-help" placement="right" [ngbTooltip]="bookWritingStyleToolTip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookWritingStyleToolTip>Changes the direction of the text. Horizontal is left to right, vertical is top to bottom.</ng-template>
|
||||
<span class="visually-hidden" id="settings-book-writing-style-help"><ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-book-writing-style-help" formControlName="bookReaderWritingStyle" id="settings-book-writing-style" >
|
||||
<option *ngFor="let opt of bookWritingStyles" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-layout-mode" class="form-label">Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLayoutModeTooltip>How content should be laid out. Scroll is as the book packs it. 1 or 2 Column fits to the height of the device and fits 1 or 2 columns of text per page</ng-template>
|
||||
<span class="visually-hidden" id="settings-book-layout-mode-help"><ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
|
||||
<option *ngFor="let opt of bookLayoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-color-theme-option" class="form-label">Color Theme</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookColorThemeTooltip>What color theme to apply to the book reader content and menuing</ng-template>
|
||||
<span class="visually-hidden" id="settings-color-theme-option-help"><ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
|
||||
<option *ngFor="let opt of bookColorThemes" [value]="opt.name">{{opt.name | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label for="fontsize" class="form-label range-label">Font Size</label>
|
||||
<input type="range" class="form-range" id="fontsize"
|
||||
min="50" max="300" step="10" formControlName="bookReaderFontSize">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<div class="range-label">
|
||||
<label class="form-label" for="linespacing">Line Height</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLineHeightOptionTooltip>How much spacing between the lines of the book</ng-template>
|
||||
<span class="visually-hidden" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
|
||||
</div>
|
||||
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
|
||||
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<div class="range-label">
|
||||
<label class="form-label">Margin</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReaderMarginOptionTooltip>How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</ng-template>
|
||||
<span class="visually-hidden" id="settings-bookmargin-option-help">How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</span>
|
||||
</div>
|
||||
|
||||
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
|
||||
</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-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Clients">
|
||||
<div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">OPDS is not enabled on this server. This will not affect Tachiyomi users.</div>
|
||||
<p>All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.</p>
|
||||
<app-api-key tooltipText="The API key is like a password. Keep it secret, Keep it safe."></app-api-key>
|
||||
<app-api-key title="OPDS URL" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Theme">
|
||||
<app-theme-manager></app-theme-manager>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Devices">
|
||||
<app-manage-devices></app-manage-devices>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Stats">
|
||||
<app-user-stats></app-user-stats>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Scrobbling">
|
||||
<app-user-scrobble-history></app-user-scrobble-history>
|
||||
<app-user-holds></app-user-holds>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
</div>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Clients">
|
||||
<div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">{{t('clients-opds-alert')}}</div>
|
||||
<p>{{t('clients-opds-description')}}</p>
|
||||
<app-api-key [tooltipText]="t('clients-api-key-tooltip')"></app-api-key>
|
||||
<app-api-key [title]="t('clients-opds-url-tooltip')" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Theme">
|
||||
<app-theme-manager></app-theme-manager>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Devices">
|
||||
<app-manage-devices></app-manage-devices>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Stats">
|
||||
<app-user-stats></app-user-stats>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Scrobbling">
|
||||
<app-user-scrobble-history></app-user-scrobble-history>
|
||||
<app-user-holds></app-user-holds>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { AccountService } from 'src/app/_services/account.service';
|
|||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { SettingsService } from 'src/app/admin/settings.service';
|
||||
import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import {forkJoin} from 'rxjs';
|
||||
import { bookColorThemes } from 'src/app/book-reader/_components/reader-settings/reader-settings.component';
|
||||
import { BookService } from 'src/app/book-reader/_services/book.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
|
|
@ -46,6 +46,9 @@ import { ChangeEmailComponent } from '../change-email/change-email.component';
|
|||
import { NgFor, NgIf, NgTemplateOutlet, TitleCasePipe } from '@angular/common';
|
||||
import { NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgbNavOutlet } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {LocalizationService} from "../../_services/localization.service";
|
||||
import {Language} from "../../_models/metadata/language";
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
enum AccordionPanelID {
|
||||
ImageReader = 'image-reader',
|
||||
|
|
@ -70,7 +73,10 @@ enum FragmentID {
|
|||
styleUrls: ['./user-preferences.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [SideNavCompanionBarComponent, NgbNav, NgFor, NgbNavItem, NgbNavItemRole, NgbNavLink, RouterLink, NgbNavContent, NgIf, ChangeEmailComponent, ChangePasswordComponent, ChangeAgeRestrictionComponent, AnilistKeyComponent, ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, ColorPickerModule, ApiKeyComponent, ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe]
|
||||
imports: [SideNavCompanionBarComponent, NgbNav, NgFor, NgbNavItem, NgbNavItemRole, NgbNavLink, RouterLink, NgbNavContent, NgIf, ChangeEmailComponent,
|
||||
ChangePasswordComponent, ChangeAgeRestrictionComponent, AnilistKeyComponent, ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader,
|
||||
NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, ColorPickerModule, ApiKeyComponent,
|
||||
ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe, TranslocoModule]
|
||||
})
|
||||
export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
|
@ -91,18 +97,20 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
fontFamilies: Array<string> = [];
|
||||
|
||||
tabs: Array<{title: string, fragment: string}> = [
|
||||
{title: 'Account', fragment: FragmentID.Account},
|
||||
{title: 'Preferences', fragment: FragmentID.Preferences},
|
||||
{title: '3rd Party Clients', fragment: FragmentID.Clients},
|
||||
{title: 'Theme', fragment: FragmentID.Theme},
|
||||
{title: 'Devices', fragment: FragmentID.Devices},
|
||||
{title: 'Stats', fragment: FragmentID.Stats},
|
||||
{title: 'account-tab', fragment: FragmentID.Account},
|
||||
{title: 'preferences-tab', fragment: FragmentID.Preferences},
|
||||
{title: '3rd-party-clients-tab', fragment: FragmentID.Clients},
|
||||
{title: 'theme-tab', fragment: FragmentID.Theme},
|
||||
{title: 'devices-tab', fragment: FragmentID.Devices},
|
||||
{title: 'stats-tab', fragment: FragmentID.Stats},
|
||||
];
|
||||
locales: Array<Language> = [{title: 'English', isoCode: 'en'}];
|
||||
active = this.tabs[1];
|
||||
opdsEnabled: boolean = false;
|
||||
opdsUrl: string = '';
|
||||
makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; };
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly trasnlocoService = inject(TranslocoService);
|
||||
|
||||
get AccordionPanelID() {
|
||||
return AccordionPanelID;
|
||||
|
|
@ -115,7 +123,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
|
||||
constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService,
|
||||
private titleService: Title, private route: ActivatedRoute, private settingsService: SettingsService,
|
||||
private router: Router, private readonly cdRef: ChangeDetectorRef) {
|
||||
private router: Router, private readonly cdRef: ChangeDetectorRef, public localizationService: LocalizationService) {
|
||||
this.fontFamilies = this.bookService.getFontFamilies().map(f => f.title);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
|
@ -124,9 +132,14 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.localizationService.getLocales().subscribe(res => {
|
||||
this.locales = res;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.accountService.hasValidLicense().subscribe(res => {
|
||||
if (res) {
|
||||
this.tabs.push({title: 'Scrobbling', fragment: FragmentID.Scrobbling});
|
||||
this.tabs.push({title: 'scrobbling-tab', fragment: FragmentID.Scrobbling});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
|
@ -195,6 +208,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
this.settingsForm.addControl('noTransitions', new FormControl(this.user.preferences.noTransitions, []));
|
||||
this.settingsForm.addControl('collapseSeriesRelationships', new FormControl(this.user.preferences.collapseSeriesRelationships, []));
|
||||
this.settingsForm.addControl('shareReviews', new FormControl(this.user.preferences.shareReviews, []));
|
||||
this.settingsForm.addControl('locale', new FormControl(this.user.preferences.locale, []));
|
||||
|
||||
if (this.locales.length === 1) {
|
||||
this.settingsForm.get('locale')?.disable();
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
|
@ -241,6 +259,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
this.settingsForm.get('swipeToPaginate')?.setValue(this.user.preferences.swipeToPaginate);
|
||||
this.settingsForm.get('collapseSeriesRelationships')?.setValue(this.user.preferences.collapseSeriesRelationships);
|
||||
this.settingsForm.get('shareReviews')?.setValue(this.user.preferences.shareReviews);
|
||||
this.settingsForm.get('locale')?.setValue(this.user.preferences.locale);
|
||||
this.cdRef.markForCheck();
|
||||
this.settingsForm.markAsPristine();
|
||||
}
|
||||
|
|
@ -275,11 +294,12 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
emulateBook: modelSettings.emulateBook,
|
||||
swipeToPaginate: modelSettings.swipeToPaginate,
|
||||
collapseSeriesRelationships: modelSettings.collapseSeriesRelationships,
|
||||
shareReviews: modelSettings.shareReviews
|
||||
shareReviews: modelSettings.shareReviews,
|
||||
locale: modelSettings.locale
|
||||
};
|
||||
|
||||
this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||
this.toastr.success('User preferences updated');
|
||||
this.toastr.success(this.trasnlocoService.translate('user-preferences.success-toast'));
|
||||
if (this.user) {
|
||||
this.user.preferences = updatedPrefs;
|
||||
this.cdRef.markForCheck();
|
||||
|
|
@ -294,6 +314,4 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
this.settingsForm.markAsTouched();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
protected readonly undefined = undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
NgbAccordionBody,
|
||||
NgbAccordionButton, NgbAccordionCollapse,
|
||||
NgbAccordionDirective, NgbAccordionHeader, NgbAccordionItem,
|
||||
NgbAccordionModule, NgbAccordionToggle,
|
||||
NgbCollapseModule,
|
||||
NgbNavModule,
|
||||
NgbTooltipModule
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue