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,63 +1,66 @@
|
|||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Choose a Directory</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ng-container *transloco="let t; read:'directory-picker'">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="typeahead-focus" class="form-label">Path</label>
|
||||
<div class="input-group">
|
||||
<input id="typeahead-focus" type="text" class="form-control" [(ngModel)]="path" [ngbTypeahead]="search"
|
||||
(focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)"
|
||||
(ngModelChange)="updateTable()" #instance="ngbTypeahead" placeholder="Start typing or select path"
|
||||
[resultTemplate]="rt" />
|
||||
</div>
|
||||
<ng-template #rt let-r="result" let-t="term">
|
||||
<ngb-highlight [result]="r" [term]="t"></ngb-highlight>
|
||||
</ng-template>
|
||||
<label for="typeahead-focus" class="form-label">{{t('path')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="typeahead-focus" type="text" class="form-control" [(ngModel)]="path" [ngbTypeahead]="search"
|
||||
(focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)"
|
||||
(ngModelChange)="updateTable()" #instance="ngbTypeahead" [placeholder]="t('path-placeholder')"
|
||||
[resultTemplate]="rt" />
|
||||
</div>
|
||||
<ng-template #rt let-r="result" let-t="term">
|
||||
<ngb-highlight [result]="r" [term]="t"></ngb-highlight>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<nav aria-label="directory breadcrumb">
|
||||
<ol class="breadcrumb" *ngIf="routeStack.peek() !== undefined; else noBreadcrumb">
|
||||
<li class="breadcrumb-item {{route === routeStack.peek() ? 'active' : ''}}"
|
||||
*ngFor="let route of routeStack.items; let index = index; let last = last;">
|
||||
<ng-container *ngIf="last; else nonActive">
|
||||
{{route}}
|
||||
</ng-container>
|
||||
<ng-template #nonActive>
|
||||
<a href="javascript:void(0);" (click)="navigateTo(index)">{{route}}</a>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ol>
|
||||
<ng-template #noBreadcrumb>
|
||||
<div class="breadcrumb">Select a folder to view breadcrumb. Don't see your directory? Try checking / first.
|
||||
</div>
|
||||
</ng-template>
|
||||
<ol class="breadcrumb" *ngIf="routeStack.peek() !== undefined; else noBreadcrumb">
|
||||
<li class="breadcrumb-item {{route === routeStack.peek() ? 'active' : ''}}"
|
||||
*ngFor="let route of routeStack.items; let index = index; let last = last;">
|
||||
<ng-container *ngIf="last; else nonActive">
|
||||
{{route}}
|
||||
</ng-container>
|
||||
<ng-template #nonActive>
|
||||
<a href="javascript:void(0);" (click)="navigateTo(index)">{{route}}</a>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ol>
|
||||
<ng-template #noBreadcrumb>
|
||||
<div class="breadcrumb">{{t('instructions')}}
|
||||
</div>
|
||||
</ng-template>
|
||||
</nav>
|
||||
|
||||
<table class="table table-striped scrollable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 40px;">Type</th>
|
||||
<th scope="col">Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr (click)="goBack()">
|
||||
<td><i class="fa-solid fa-arrow-turn-up" aria-hidden="true"></i></td>
|
||||
<td>...</td>
|
||||
</tr>
|
||||
<tr *ngFor="let folder of folders; let idx = index;" (click)="selectNode(folder)" style="cursor: pointer;" [ngClass]="{'disabled': folder.disabled}">
|
||||
<td><i class="fa-regular fa-folder" aria-hidden="true"></i></td>
|
||||
<td id="folder--{{idx}}">
|
||||
{{folder.name}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 40px;">{{t('type-header')}}</th>
|
||||
<th scope="col">{{t('name-header')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr (click)="goBack()">
|
||||
<td><i class="fa-solid fa-arrow-turn-up" aria-hidden="true"></i></td>
|
||||
<td>...</td>
|
||||
</tr>
|
||||
<tr *ngFor="let folder of folders; let idx = index;" (click)="selectNode(folder)" style="cursor: pointer;" [ngClass]="{'disabled': folder.disabled}">
|
||||
<td><i class="fa-regular fa-folder" aria-hidden="true"></i></td>
|
||||
<td id="folder--{{idx}}">
|
||||
{{folder.name}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-icon" *ngIf="helpUrl.length > 0" href="{{helpUrl}}" target="_blank" rel="noopener noreferrer">Help</a>
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="share()">Share</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-icon" *ngIf="helpUrl.length > 0" href="{{helpUrl}}" target="_blank" rel="noopener noreferrer">{{t('help')}}</a>
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">{{t('cancel')}}</button>
|
||||
<button type="button" class="btn btn-primary" (click)="share()">{{t('share')}}</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { DirectoryDto } from 'src/app/_models/system/directory-dto';
|
|||
import { LibraryService } from '../../../_services/library.service';
|
||||
import { NgIf, NgFor, NgClass } from '@angular/common';
|
||||
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
|
||||
export interface DirectoryPickerResult {
|
||||
|
|
@ -20,13 +21,13 @@ export interface DirectoryPickerResult {
|
|||
templateUrl: './directory-picker.component.html',
|
||||
styleUrls: ['./directory-picker.component.scss'],
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgbTypeahead, FormsModule, NgbHighlight, NgIf, NgFor, NgClass]
|
||||
imports: [ReactiveFormsModule, NgbTypeahead, FormsModule, NgbHighlight, NgIf, NgFor, NgClass, TranslocoModule]
|
||||
})
|
||||
export class DirectoryPickerComponent implements OnInit {
|
||||
|
||||
@Input() startingFolder: string = '';
|
||||
/**
|
||||
* Url to give more information about selecting directories. Passing nothing will suppress.
|
||||
* Url to give more information about selecting directories. Passing nothing will suppress.
|
||||
*/
|
||||
@Input() helpUrl: string = 'https://wiki.kavitareader.com/en/guides/first-time-setup#adding-a-library-to-kavita';
|
||||
|
||||
|
|
@ -161,7 +162,7 @@ export class DirectoryPickerComponent implements OnInit {
|
|||
while(this.routeStack.items.length - 1 > index) {
|
||||
this.routeStack.pop();
|
||||
}
|
||||
|
||||
|
||||
const fullPath = this.routeStack.items.join('/');
|
||||
this.path = fullPath;
|
||||
this.loadChildren(fullPath);
|
||||
|
|
|
|||
|
|
@ -1,33 +1,36 @@
|
|||
<ng-container *transloco="let t; read:'library-access-modal'">
|
||||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()">
|
||||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Library Access</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="list-group">
|
||||
<div class="form-check">
|
||||
<input id="selectall" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="selectall" class="form-check-label">{{selectAll ? 'Deselect' : 'Select'}} All</label>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="list-group-item" *ngFor="let library of allLibraries; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="library-{{i}}" type="checkbox" class="form-check-input" attr.aria-label="Library {{library.name}}"
|
||||
[ngModel]="selections.isSelected(library)" (change)="handleSelection(library)">
|
||||
<label attr.for="library-{{i}}" class="form-check-label">{{library.name}}</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="allLibraries.length === 0">
|
||||
There are no libraries setup yet.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="form-check">
|
||||
<input id="select-all" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="select-all" class="form-check-label">{{selectAll ? t('deselect-all') : t('select-all')}}</label>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="list-group-item" *ngFor="let library of allLibraries; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="library-{{i}}" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selections.isSelected(library)" (change)="handleSelection(library)">
|
||||
<label for="library-{{i}}" class="form-check-label">{{library.name}}</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="allLibraries.length === 0">
|
||||
{{t('no-data')}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" (click)="reset()">Reset</button>
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="save()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" (click)="reset()">{{t('reset')}}</button>
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">{{t('cancel')}}</button>
|
||||
<button type="button" class="btn btn-primary" (click)="save()">{{t('save')}}</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ import {LibraryService} from 'src/app/_services/library.service';
|
|||
import {SelectionModel} from 'src/app/typeahead/_components/typeahead.component';
|
||||
import {NgFor, NgIf} from '@angular/common';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-access-modal',
|
||||
templateUrl: './library-access-modal.component.html',
|
||||
styleUrls: ['./library-access-modal.component.scss'],
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, FormsModule, NgFor, NgIf],
|
||||
imports: [ReactiveFormsModule, FormsModule, NgFor, NgIf, TranslocoModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LibraryAccessModalComponent implements OnInit {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
<form [formGroup]="resetPasswordForm">
|
||||
<ng-container *transloco="let t; read:'reset-password-modal'">
|
||||
<form [formGroup]="resetPasswordForm">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Reset {{member.username | sentenceCase}}'s Password</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
|
||||
</button>
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('title', {username: member.username | sentenceCase})}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()">
|
||||
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info" *ngIf="errorMessage !== ''">
|
||||
<strong>Error: </strong> {{errorMessage}}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">New Password</label>
|
||||
<input id="password" class="form-control" minlength="4" formControlName="password" type="password">
|
||||
</div>
|
||||
<div class="alert alert-info" *ngIf="errorMessage !== ''">
|
||||
<strong>{{t('error-label')}}</strong> {{errorMessage}}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">{{t('new-password-label')}}</label>
|
||||
<input id="password" class="form-control" minlength="4" formControlName="password" type="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="resetPasswordForm.value.password.length === 0" (click)="save()">Save</button>
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">{{t('cancel')}}</button>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="resetPasswordForm.value.password.length === 0" (click)="save()">{{t('save')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { Member } from 'src/app/_models/auth/member';
|
|||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { SentenceCasePipe } from '../../../pipe/sentence-case.pipe';
|
||||
import { NgIf } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-reset-password-modal',
|
||||
templateUrl: './reset-password-modal.component.html',
|
||||
styleUrls: ['./reset-password-modal.component.scss'],
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgIf, SentenceCasePipe]
|
||||
imports: [ReactiveFormsModule, NgIf, SentenceCasePipe, TranslocoModule]
|
||||
})
|
||||
export class ResetPasswordModalComponent {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<app-side-nav-companion-bar>
|
||||
<h2 title>
|
||||
Admin Dashboard
|
||||
</h2>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid g-0">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
|
||||
<ng-container *transloco="let t; read: 'admin-dashboard'">
|
||||
<app-side-nav-companion-bar>
|
||||
<h2 title>
|
||||
{{t('title')}}
|
||||
</h2>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid g-0">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab" class=tab>
|
||||
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | sentenceCase }}</a>
|
||||
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ t(tab.title) }}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngIf="tab.fragment === TabID.General">
|
||||
<app-manage-settings></app-manage-settings>
|
||||
|
|
@ -18,10 +19,10 @@
|
|||
<app-manage-media-settings></app-manage-media-settings>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === TabID.Users">
|
||||
<app-manage-users></app-manage-users>
|
||||
<app-manage-users></app-manage-users>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === TabID.Libraries">
|
||||
<app-manage-library></app-manage-library>
|
||||
<app-manage-library></app-manage-library>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === TabID.Logs">
|
||||
<app-manage-logs></app-manage-logs>
|
||||
|
|
@ -36,12 +37,14 @@
|
|||
<app-manage-tasks-settings></app-manage-tasks-settings>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === TabID.KavitaPlus">
|
||||
<p>Kavita+ is a premium subscription service which unlocks features for all users on this Kavita instance. Buy a subscription to unlock <a href="https://wiki.kavitareader.com/en/kavita-plus" target="_blank" rel="noreferrer nofollow">premium benefits</a> today! <a href="https://wiki.kavitareader.com/en/kavita-plus#faq" target="_blank" rel="noreferrer nofollow">FAQ</a></p>
|
||||
<p>{{t('kavita+-desc-part-1')}} <a href="https://wiki.kavitareader.com/en/kavita-plus" target="_blank" rel="noreferrer nofollow">{{t('kavita+-desc-part-2')}}</a> {{t('kavita+-desc-part-3')}} <a href="https://wiki.kavitareader.com/en/kavita-plus#faq" target="_blank" rel="noreferrer nofollow">FAQ</a></p>
|
||||
<app-license></app-license>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="mt-3 mb-3"></div>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="mt-3 mb-3"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { NavService } from '../../_services/nav.service';
|
||||
import { SentenceCasePipe } from '../../pipe/sentence-case.pipe';
|
||||
import { LicenseComponent } from '../license/license.component';
|
||||
import { ManageTasksSettingsComponent } from '../manage-tasks-settings/manage-tasks-settings.component';
|
||||
import { ServerStatsComponent } from '../../statistics/_components/server-stats/server-stats.component';
|
||||
import { ManageSystemComponent } from '../manage-system/manage-system.component';
|
||||
import { ManageLogsComponent } from '../manage-logs/manage-logs.component';
|
||||
import { ManageLibraryComponent } from '../manage-library/manage-library.component';
|
||||
import { ManageUsersComponent } from '../manage-users/manage-users.component';
|
||||
import { ManageMediaSettingsComponent } from '../manage-media-settings/manage-media-settings.component';
|
||||
import { ManageEmailSettingsComponent } from '../manage-email-settings/manage-email-settings.component';
|
||||
import { ManageSettingsComponent } from '../manage-settings/manage-settings.component';
|
||||
import { NgFor, NgIf } from '@angular/common';
|
||||
import { NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgbNavOutlet } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {ActivatedRoute, RouterLink} from '@angular/router';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {ServerService} from 'src/app/_services/server.service';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {NavService} from '../../_services/nav.service';
|
||||
import {SentenceCasePipe} from '../../pipe/sentence-case.pipe';
|
||||
import {LicenseComponent} from '../license/license.component';
|
||||
import {ManageTasksSettingsComponent} from '../manage-tasks-settings/manage-tasks-settings.component';
|
||||
import {ServerStatsComponent} from '../../statistics/_components/server-stats/server-stats.component';
|
||||
import {ManageSystemComponent} from '../manage-system/manage-system.component';
|
||||
import {ManageLogsComponent} from '../manage-logs/manage-logs.component';
|
||||
import {ManageLibraryComponent} from '../manage-library/manage-library.component';
|
||||
import {ManageUsersComponent} from '../manage-users/manage-users.component';
|
||||
import {ManageMediaSettingsComponent} from '../manage-media-settings/manage-media-settings.component';
|
||||
import {ManageEmailSettingsComponent} from '../manage-email-settings/manage-email-settings.component';
|
||||
import {ManageSettingsComponent} from '../manage-settings/manage-settings.component';
|
||||
import {NgFor, NgIf} from '@angular/common';
|
||||
import {NgbNav, NgbNavContent, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavOutlet} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
} from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
enum TabID {
|
||||
General = '',
|
||||
|
|
@ -26,7 +29,6 @@ enum TabID {
|
|||
Users = 'users',
|
||||
Libraries = 'libraries',
|
||||
System = 'system',
|
||||
Plugins = 'plugins',
|
||||
Tasks = 'tasks',
|
||||
Logs = 'logs',
|
||||
Statistics = 'statistics',
|
||||
|
|
@ -38,24 +40,28 @@ enum TabID {
|
|||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.scss'],
|
||||
standalone: true,
|
||||
imports: [SideNavCompanionBarComponent, NgbNav, NgFor, NgbNavItem, NgbNavItemRole, NgbNavLink, RouterLink, NgbNavContent, NgIf, ManageSettingsComponent, ManageEmailSettingsComponent, ManageMediaSettingsComponent, ManageUsersComponent, ManageLibraryComponent, ManageLogsComponent, ManageSystemComponent, ServerStatsComponent, ManageTasksSettingsComponent, LicenseComponent, NgbNavOutlet, SentenceCasePipe]
|
||||
imports: [SideNavCompanionBarComponent, NgbNav, NgFor, NgbNavItem, NgbNavItemRole, NgbNavLink, RouterLink, NgbNavContent, NgIf, ManageSettingsComponent, ManageEmailSettingsComponent, ManageMediaSettingsComponent, ManageUsersComponent, ManageLibraryComponent, ManageLogsComponent, ManageSystemComponent, ServerStatsComponent, ManageTasksSettingsComponent, LicenseComponent, NgbNavOutlet, SentenceCasePipe, TranslocoModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
tabs: Array<{title: string, fragment: string}> = [
|
||||
{title: 'General', fragment: TabID.General},
|
||||
{title: 'Users', fragment: TabID.Users},
|
||||
{title: 'Libraries', fragment: TabID.Libraries},
|
||||
//{title: 'Logs', fragment: TabID.Logs},
|
||||
{title: 'Media', fragment: TabID.Media},
|
||||
{title: 'Email', fragment: TabID.Email},
|
||||
{title: 'Tasks', fragment: TabID.Tasks},
|
||||
{title: 'Statistics', fragment: TabID.Statistics},
|
||||
{title: 'System', fragment: TabID.System},
|
||||
{title: 'Kavita+', fragment: TabID.KavitaPlus},
|
||||
{title: 'general-tab', fragment: TabID.General},
|
||||
{title: 'users-tab', fragment: TabID.Users},
|
||||
{title: 'libraries-tab', fragment: TabID.Libraries},
|
||||
//{title: 'logs-tab', fragment: TabID.Logs},
|
||||
{title: 'media-tab', fragment: TabID.Media},
|
||||
{title: 'email-tab', fragment: TabID.Email},
|
||||
{title: 'tasks-tab', fragment: TabID.Tasks},
|
||||
{title: 'statistics-tab', fragment: TabID.Statistics},
|
||||
{title: 'system-tab', fragment: TabID.System},
|
||||
{title: 'kavita+-tab', fragment: TabID.KavitaPlus},
|
||||
];
|
||||
active = this.tabs[0];
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
|
||||
get TabID() {
|
||||
return TabID;
|
||||
}
|
||||
|
|
@ -69,11 +75,12 @@ export class DashboardComponent implements OnInit {
|
|||
} else {
|
||||
this.active = this.tabs[0]; // Default to first tab
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.titleService.setTitle('Kavita - Admin Dashboard');
|
||||
this.titleService.setTitle('Kavita - ' + this.translocoService.translate('admin-dashboard.title'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,68 +1,70 @@
|
|||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Edit {{member.username | sentenceCase}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
<ng-container *transloco="let t; read: 'edit-user'">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('edit')}} {{member.username | sentenceCase}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()">
|
||||
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal">
|
||||
|
||||
<form [formGroup]="userForm">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input id="username" class="form-control" formControlName="username" type="text"
|
||||
[class.is-invalid]="userForm.get('username')?.invalid && userForm.get('username')?.touched" aria-describedby="username-validations">
|
||||
<div id="username-validations" class="invalid-feedback" *ngIf="userForm.dirty || userForm.touched">
|
||||
<div *ngIf="userForm.get('username')?.errors?.required">
|
||||
This field is required
|
||||
<form [formGroup]="userForm">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">{{t('username')}}</label>
|
||||
<input id="username" class="form-control" formControlName="username" type="text"
|
||||
[class.is-invalid]="userForm.get('username')?.invalid && userForm.get('username')?.touched" aria-describedby="username-validations">
|
||||
<div id="username-validations" class="invalid-feedback" *ngIf="userForm.dirty || userForm.touched">
|
||||
<div *ngIf="userForm.get('username')?.errors?.required">
|
||||
{{t('required')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="mb-3" style="width:100%">
|
||||
<label for="email" class="form-label">{{t('email')}}</label>
|
||||
<input class="form-control" inputmode="email" type="email" id="email" formControlName="email" aria-describedby="email-validations">
|
||||
<div id="email-validations" class="invalid-feedback"
|
||||
*ngIf="userForm.dirty || userForm.touched" [class.is-invalid]="userForm.get('email')?.invalid && userForm.get('email')?.touched">
|
||||
<div *ngIf="userForm.get('email')?.errors?.required">
|
||||
{{t('required')}}
|
||||
</div>
|
||||
<div *ngIf="userForm.get('email')?.errors?.email">
|
||||
{{t('not-valid-email')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="mb-3" style="width:100%">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input class="form-control" inputmode="email" type="email" id="email" formControlName="email" aria-describedby="email-validations">
|
||||
<div id="email-validations" class="invalid-feedback"
|
||||
*ngIf="userForm.dirty || userForm.touched" [class.is-invalid]="userForm.get('email')?.invalid && userForm.get('email')?.touched">
|
||||
<div *ngIf="userForm.get('email')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
<div *ngIf="userForm.get('email')?.errors?.email">
|
||||
This must be a valid email address
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6">
|
||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true" [member]="member"></app-role-selector>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<app-library-selector (selected)="updateLibrarySelection($event)" [member]="member"></app-library-selector>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6">
|
||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true" [member]="member"></app-role-selector>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12">
|
||||
<app-restriction-selector (selected)="updateRestrictionSelection($event)" [isAdmin]="hasAdminRoleSelected" [member]="member"></app-restriction-selector>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="col-md-6">
|
||||
<app-library-selector (selected)="updateLibrarySelection($event)" [member]="member"></app-library-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12">
|
||||
<app-restriction-selector (selected)="updateRestrictionSelection($event)" [isAdmin]="hasAdminRoleSelected" [member]="member"></app-restriction-selector>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">
|
||||
{{t('cancel')}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="isSaving || !userForm.valid">
|
||||
<span *ngIf="isSaving" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span>{{isSaving ? t('saving') : t('update')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="isSaving || !userForm.valid">
|
||||
<span *ngIf="isSaving" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span>{{isSaving ? 'Saving...' : 'Update'}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ import { RestrictionSelectorComponent } from '../../user-settings/restriction-se
|
|||
import { LibrarySelectorComponent } from '../library-selector/library-selector.component';
|
||||
import { RoleSelectorComponent } from '../role-selector/role-selector.component';
|
||||
import { NgIf } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-user',
|
||||
templateUrl: './edit-user.component.html',
|
||||
styleUrls: ['./edit-user.component.scss'],
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgIf, RoleSelectorComponent, LibrarySelectorComponent, RestrictionSelectorComponent, SentenceCasePipe]
|
||||
imports: [ReactiveFormsModule, NgIf, RoleSelectorComponent, LibrarySelectorComponent, RestrictionSelectorComponent, SentenceCasePipe, TranslocoModule]
|
||||
})
|
||||
export class EditUserComponent implements OnInit {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,61 +1,61 @@
|
|||
<div class="modal-container">
|
||||
<ng-container *transloco="let t; read: 'invite-user'">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Invite User</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal">
|
||||
<p>
|
||||
Invite a user to your server. Enter their email in and we will send them an email to create an account. If you do not want to use our email service, you can <a href="https://wiki.kavitareader.com/en/guides/misc/email" rel="noopener noreferrer" target="_blank">host your own</a>
|
||||
email service or use a fake email (Forgot User will not work). A link will be presented regardless and can be used to setup the account manually.
|
||||
<p>
|
||||
{{t('description')}}
|
||||
</p>
|
||||
|
||||
<form [formGroup]="inviteForm" *ngIf="emailLink === ''">
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width:100%">
|
||||
<label for="email" class="form-label">{{t('email')}}</label>
|
||||
<input class="form-control" type="email" inputmode="email" id="email" formControlName="email" required [class.is-invalid]="inviteForm.get('email')?.invalid && inviteForm.get('email')?.touched">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="inviteForm.dirty || inviteForm.touched">
|
||||
<div *ngIf="email?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6">
|
||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true"></app-role-selector>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<app-library-selector (selected)="updateLibrarySelection($event)"></app-library-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12">
|
||||
<app-restriction-selector (selected)="updateRestrictionSelection($event)" [isAdmin]="hasAdminRoleSelected"></app-restriction-selector>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-container *ngIf="emailLink !== ''">
|
||||
<h4>{{t('setup-user-title')}}</h4>
|
||||
<p>{{t('setup-user-description')}}
|
||||
</p>
|
||||
|
||||
<form [formGroup]="inviteForm" *ngIf="emailLink === ''">
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width:100%">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input class="form-control" type="email" inputmode="email" id="email" formControlName="email" required [class.is-invalid]="inviteForm.get('email')?.invalid && inviteForm.get('email')?.touched">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="inviteForm.dirty || inviteForm.touched">
|
||||
<div *ngIf="email?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6">
|
||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true"></app-role-selector>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<app-library-selector (selected)="updateLibrarySelection($event)"></app-library-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12">
|
||||
<app-restriction-selector (selected)="updateRestrictionSelection($event)" [isAdmin]="hasAdminRoleSelected"></app-restriction-selector>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-container *ngIf="emailLink !== ''">
|
||||
<h4>User invited</h4>
|
||||
<p>You can use the following link below to setup the account for your user or use the copy button. You may need to log out before using the link to register a new user.
|
||||
If your server is externally accessible, an email will have been sent to the user and the links can be used by them to finish setting up their account.
|
||||
</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>
|
||||
|
||||
<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('setup-user-account-tooltip')" [showRefresh]="false" [transform]="makeLink"></app-api-key>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" (click)="invite()" [disabled]="isSending || !inviteForm.valid || emailLink !== ''">
|
||||
<span *ngIf="isSending" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span>{{isSending ? 'Inviting...' : 'Invite'}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">
|
||||
{{t('cancel')}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" (click)="invite()" [disabled]="isSending || !inviteForm.valid || emailLink !== ''">
|
||||
<span *ngIf="isSending" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span>{{isSending ? t('inviting') : t('invite')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@ import { RestrictionSelectorComponent } from '../../user-settings/restriction-se
|
|||
import { LibrarySelectorComponent } from '../library-selector/library-selector.component';
|
||||
import { RoleSelectorComponent } from '../role-selector/role-selector.component';
|
||||
import { NgIf } from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-invite-user',
|
||||
templateUrl: './invite-user.component.html',
|
||||
styleUrls: ['./invite-user.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgIf, ReactiveFormsModule, RoleSelectorComponent, LibrarySelectorComponent, RestrictionSelectorComponent, ApiKeyComponent]
|
||||
imports: [NgIf, ReactiveFormsModule, RoleSelectorComponent, LibrarySelectorComponent, RestrictionSelectorComponent, ApiKeyComponent, TranslocoModule]
|
||||
})
|
||||
export class InviteUserComponent implements OnInit {
|
||||
|
||||
|
|
@ -61,7 +62,7 @@ export class InviteUserComponent implements OnInit {
|
|||
this.emailLink = data.emailLink;
|
||||
this.isSending = false;
|
||||
if (data.emailSent) {
|
||||
this.toastr.info('Email sent to ' + email);
|
||||
this.toastr.info(translate('toasts.email-sent', {email: email}));
|
||||
this.modal.close(true);
|
||||
}
|
||||
}, err => {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
<h4>Libraries</h4>
|
||||
<div class="list-group" *ngIf="!isLoading">
|
||||
<ng-container *transloco="let t; read: 'library-selector'">
|
||||
<h4>{{t('title')}}</h4>
|
||||
<div class="list-group" *ngIf="!isLoading">
|
||||
<div class="form-check" *ngIf="allLibraries.length > 0">
|
||||
<input id="selectall" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="selectall" class="form-check-label">{{selectAll ? 'Deselect' : 'Select'}} All</label>
|
||||
<input id="select-all" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="select-all" class="form-check-label">{{selectAll ? t('deselect-all') : t('select-all')}} All</label>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="list-group-item" *ngFor="let library of allLibraries; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="library-{{i}}" type="checkbox" class="form-check-input" attr.aria-label="Library {{library.name}}"
|
||||
[ngModel]="selections.isSelected(library)" (change)="handleSelection(library)">
|
||||
<label attr.for="library-{{i}}" class="form-check-label">{{library.name}}</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="allLibraries.length === 0">
|
||||
There are no libraries setup yet.
|
||||
</li>
|
||||
<li class="list-group-item" *ngFor="let library of allLibraries; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="library-{{i}}" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selections.isSelected(library)" (change)="handleSelection(library)">
|
||||
<label for="library-{{i}}" class="form-check-label">{{library.name}}</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="allLibraries.length === 0">
|
||||
{{t('no-data')}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { Member } from 'src/app/_models/auth/member';
|
|||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { SelectionModel } from 'src/app/typeahead/_components/typeahead.component';
|
||||
import { NgIf, NgFor } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-selector',
|
||||
templateUrl: './library-selector.component.html',
|
||||
styleUrls: ['./library-selector.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgIf, ReactiveFormsModule, FormsModule, NgFor]
|
||||
imports: [NgIf, ReactiveFormsModule, FormsModule, NgFor, TranslocoModule]
|
||||
})
|
||||
export class LibrarySelectorComponent implements OnInit {
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ export class LibrarySelectorComponent implements OnInit {
|
|||
setupSelections() {
|
||||
this.selections = new SelectionModel<Library>(false, this.allLibraries);
|
||||
this.isLoading = false;
|
||||
|
||||
|
||||
// If a member is passed in, then auto-select their libraries
|
||||
if (this.member !== undefined) {
|
||||
this.member.libraries.forEach(lib => {
|
||||
|
|
|
|||
|
|
@ -1,85 +1,88 @@
|
|||
<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-10">
|
||||
<h4 id="license-key-header">Kavita+ License</h4>
|
||||
</div>
|
||||
<div class="col-2 text-end">
|
||||
<ng-container *ngIf="hasLicense; else noLicense">
|
||||
<ng-container *ngIf="hasValidLicense; else invalidLicenseBuy">
|
||||
<a class="btn btn-primary btn-sm me-1" [href]="manageLink" target="_blank" rel="noreferrer nofollow">Manage</a>
|
||||
<ng-container *transloco="let t; read: 'license'">
|
||||
<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-10">
|
||||
<h4 id="license-key-header">{{t('title')}}</h4>
|
||||
</div>
|
||||
<div class="col-2 text-end">
|
||||
<ng-container *ngIf="hasLicense; else noLicense">
|
||||
<ng-container *ngIf="hasValidLicense; else invalidLicenseBuy">
|
||||
<a class="btn btn-primary btn-sm me-1" [href]="manageLink" target="_blank" rel="noreferrer nofollow">{{t('manage')}}</a>
|
||||
</ng-container>
|
||||
<ng-template #invalidLicenseBuy>
|
||||
<a class="btn btn-primary btn-sm me-1"
|
||||
[ngbTooltip]="t('invalid-license-tooltip')"
|
||||
href="mailto:kavitareader@gmail.com?subject=Kavita+Subscription+Renewal&body=Description%3A%0D%0A%0D%0ALicense%20Key%3A%0D%0A%0D%0AYour%20Email%3A"
|
||||
>{{t('renew')}}</a>
|
||||
</ng-template>
|
||||
<button class="btn btn-secondary btn-sm me-1" style="width: 58px" (click)="validateLicense()">
|
||||
<span *ngIf="!isChecking">{{t('check')}}</span>
|
||||
<app-loading [loading]="isChecking" size="spinner-border-sm"></app-loading>
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" style="width: 62px" (click)="toggleViewMode()">
|
||||
<span *ngIf="!isViewMode">{{t('cancel')}}</span>
|
||||
<span *ngIf="isViewMode">{{t('edit')}}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-template #invalidLicenseBuy>
|
||||
<a class="btn btn-primary btn-sm me-1"
|
||||
ngbTooltip="If your subscription has ended, you must email support to get a new subscription created"
|
||||
href="mailto:kavitareader@gmail.com?subject=Kavita+Subscription+Renewal&body=Description%3A%0D%0A%0D%0ALicense%20Key%3A%0D%0A%0D%0AYour%20Email%3A"
|
||||
>Renew</a>
|
||||
<ng-template #noLicense>
|
||||
<a class="btn btn-secondary btn-sm me-1" [href]="buyLink" target="_blank" rel="noreferrer nofollow">{{t('buy')}}</a>
|
||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()">{{isViewMode ? t('activate') : t('cancel')}}</button>
|
||||
</ng-template>
|
||||
<button class="btn btn-secondary btn-sm me-1" style="width: 58px" (click)="validateLicense()">
|
||||
<span *ngIf="!isChecking">Check</span>
|
||||
<app-loading [loading]="isChecking" size="spinner-border-sm"></app-loading>
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" style="width: 62px" (click)="toggleViewMode()">
|
||||
<span *ngIf="!isViewMode">Cancel</span>
|
||||
<span *ngIf="isViewMode">Edit</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-template #noLicense>
|
||||
<a class="btn btn-secondary btn-sm me-1" [href]="buyLink" target="_blank" rel="noreferrer nofollow">Buy</a>
|
||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()">{{isViewMode ? 'Activate' : 'Cancel'}}</button>
|
||||
</ng-template>
|
||||
</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="hasLicense; else noToken">
|
||||
<span class="me-1">*********</span>
|
||||
<ng-container *ngIf="!isChecking; else checking">
|
||||
<i *ngIf="hasValidLicense" ngbTooltip="License is valid" class="fa-solid fa-check-circle successful-validation ms-1">
|
||||
<span class="visually-hidden">License is Valid</span>
|
||||
<i *ngIf="hasValidLicense" [ngbTooltip]="t('license-valid')" class="fa-solid fa-check-circle successful-validation ms-1">
|
||||
<span class="visually-hidden">{{t('license-valid')}}</span>
|
||||
</i>
|
||||
<i class="error fa-solid fa-exclamation-circle ms-1" ngbTooltip="License Invalid" *ngIf="!hasValidLicense">
|
||||
<span class="visually-hidden">License Not Valid</span>
|
||||
<i class="error fa-solid fa-exclamation-circle ms-1" [ngbTooltip]="t('license-not-valid')" *ngIf="!hasValidLicense">
|
||||
<span class="visually-hidden">{{t('license-not-valid')}}</span>
|
||||
</i>
|
||||
</ng-container>
|
||||
<ng-template #checking>
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
<span class="visually-hidden">{{t('loading')}}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</ng-container>
|
||||
<ng-template #noToken>No license key</ng-template>
|
||||
<ng-template #noToken>{{t('no-license-key')}}</ng-template>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<form [formGroup]="formGroup">
|
||||
<p>Enter the License Key and Email used to register with Stripe</p>
|
||||
<div class="form-group mb-3">
|
||||
<label for="license-key">License Key</label>
|
||||
<input id="license-key" type="text" class="form-control" formControlName="licenseKey" autocomplete="off"/>
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<form [formGroup]="formGroup">
|
||||
<p>{{t('activate-description')}}</p>
|
||||
<div class="form-group mb-3">
|
||||
<label for="license-key">{{t('activate-license-label')}}</label>
|
||||
<input id="license-key" type="text" class="form-control" formControlName="licenseKey" autocomplete="off"/>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="email">{{t('activate-email-label')}}</label>
|
||||
<input id="email" type="email" class="form-control" formControlName="email" autocomplete="off"/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<button type="button" class="flex-fill btn btn-danger me-1" aria-describedby="license-key-header" (click)="deleteLicense()">
|
||||
{{t('activate-delete')}}
|
||||
</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="license-key-header" [disabled]="!formGroup.get('email')?.value || !formGroup.get('licenseKey')?.value" (click)="saveForm()">
|
||||
<span *ngIf="!isSaving">{{t('activate-save')}}</span>
|
||||
<app-loading [loading]="isSaving" size="spinner-border-sm"></app-loading>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="email" class="form-control" formControlName="email" autocomplete="off"/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<button type="button" class="flex-fill btn btn-danger me-1" aria-describedby="license-key-header" (click)="deleteLicense()">
|
||||
Delete
|
||||
</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="license-key-header" [disabled]="!formGroup.get('email')?.value || !formGroup.get('licenseKey')?.value" (click)="saveForm()">
|
||||
<span *ngIf="!isSaving">Save</span>
|
||||
<app-loading [loading]="isSaving" size="spinner-border-sm"></app-loading>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { LoadingComponent } from '../../shared/loading/loading.component';
|
|||
import { NgbTooltip, NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf } from '@angular/common';
|
||||
import {environment} from "../../../environments/environment";
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-license',
|
||||
|
|
@ -22,7 +23,7 @@ import {environment} from "../../../environments/environment";
|
|||
styleUrls: ['./license.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgbTooltip, LoadingComponent, NgbCollapse, ReactiveFormsModule]
|
||||
imports: [NgIf, NgbTooltip, LoadingComponent, NgbCollapse, ReactiveFormsModule, TranslocoModule]
|
||||
})
|
||||
export class LicenseComponent implements OnInit {
|
||||
|
||||
|
|
@ -71,9 +72,9 @@ export class LicenseComponent implements OnInit {
|
|||
this.accountService.hasValidLicense(true).subscribe(isValid => {
|
||||
this.hasValidLicense = isValid;
|
||||
if (!this.hasValidLicense) {
|
||||
this.toastr.info("License Key saved, but it is not valid. Click check to revalidate the subscription. First time registration may take a min to propagate.");
|
||||
this.toastr.info(translate('toasts.k+-license-saved'));
|
||||
} else {
|
||||
this.toastr.success('Kavita+ unlocked!');
|
||||
this.toastr.success(translate('toasts.k+-unlocked'));
|
||||
}
|
||||
this.hasLicense = this.formGroup.get('licenseKey')!.value.length > 0;
|
||||
this.resetForm();
|
||||
|
|
@ -85,7 +86,7 @@ export class LicenseComponent implements OnInit {
|
|||
if (err.hasOwnProperty('error')) {
|
||||
this.toastr.error(JSON.parse(err['error'])['message']);
|
||||
} else {
|
||||
this.toastr.error("There was an error when activating your license. Please try again.");
|
||||
this.toastr.error(translate('toasts.k+-error'));
|
||||
}
|
||||
this.isSaving = false;
|
||||
this.cdRef.markForCheck();
|
||||
|
|
@ -93,7 +94,7 @@ export class LicenseComponent implements OnInit {
|
|||
}
|
||||
|
||||
async deleteLicense() {
|
||||
if (!await this.confirmService.confirm('This will only delete Kavita\'s license key and allow a buy link to show. This will not cancel your subscription! Use this only if directed by support!')) {
|
||||
if (!await this.confirmService.confirm(translate('k+-delete-key'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,53 +1,54 @@
|
|||
<p>This table contains issues found during scan or reading of your media. This list is non-managed.
|
||||
You can clear it at any time and use Library (Force) Scan to perform analysis. A list of some common errors and what
|
||||
they mean can be found on the <a rel="noopener noreferrer" target="_blank" href="https://wiki.kavitareader.com/en/guides/managing-your-files/scanner#media-errors">wiki</a>.</p>
|
||||
<ng-container *transloco="let t; read: 'manage-alerts'">
|
||||
<p>{{t('description-part-1')}} <a rel="noopener noreferrer" target="_blank" href="https://wiki.kavitareader.com/en/guides/managing-your-files/scanner#media-errors">{{t('description-part-2')}}</a></p>
|
||||
|
||||
<form [formGroup]="formGroup">
|
||||
<form [formGroup]="formGroup">
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-12">
|
||||
<label for="filter" class="visually-hidden">Filter</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" type="text" class="form-control" placeholder="Filter" formControlName="filter" />
|
||||
<button class="btn btn-primary" type="button" (click)="clear()">Clear Alerts</button>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label for="filter" class="visually-hidden">{{t('filter-label')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" type="text" class="form-control" [placeholder]="t('filter-label')" formControlName="filter" />
|
||||
<button class="btn btn-primary" type="button" (click)="clear()">{{t('clear-alerts')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="table table-striped table-hover table-sm table-hover">
|
||||
</form>
|
||||
<table class="table table-striped table-hover table-sm table-hover">
|
||||
<thead #header>
|
||||
<tr>
|
||||
<th scope="col"sortable="extension" (sort)="onSort($event)">
|
||||
Extension
|
||||
</th>
|
||||
<th scope="col" sortable="filePath" (sort)="onSort($event)">
|
||||
File
|
||||
</th>
|
||||
<th scope="col" sortable="comment" (sort)="onSort($event)">
|
||||
Comment
|
||||
</th>
|
||||
<th scope="col" sortable="details" (sort)="onSort($event)">
|
||||
Details
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col" sortable="extension" (sort)="onSort($event)">
|
||||
{{t('extension-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="filePath" (sort)="onSort($event)">
|
||||
{{t('file-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="comment" (sort)="onSort($event)">
|
||||
{{t('comment-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="details" (sort)="onSort($event)">
|
||||
{{t('details-header')}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody #container>
|
||||
<tr *ngIf="isLoading"><td colspan="4" style="text-align: center;"><app-loading [loading]="isLoading"></app-loading></td></tr>
|
||||
<ng-container *ngIf="data | filter: filterList as filteredData">
|
||||
<tr *ngIf="filteredData.length === 0 && !isLoading"><td colspan="4" style="text-align: center;">No issues</td></tr>
|
||||
<tr *ngFor="let item of filteredData; index as i">
|
||||
<td>
|
||||
{{item.extension}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.filePath}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.comment}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.details}}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<tr *ngIf="isLoading"><td colspan="4" style="text-align: center;"><app-loading [loading]="isLoading"></app-loading></td></tr>
|
||||
<ng-container *ngIf="data | filter: filterList as filteredData">
|
||||
<tr *ngIf="filteredData.length === 0 && !isLoading"><td colspan="4" style="text-align: center;">No issues</td></tr>
|
||||
<tr *ngFor="let item of filteredData; index as i">
|
||||
<td>
|
||||
{{item.extension}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.filePath}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.comment}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.details}}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||
import { FilterPipe } from '../../pipe/filter.pipe';
|
||||
import { LoadingComponent } from '../../shared/loading/loading.component';
|
||||
import { NgIf, NgFor } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-alerts',
|
||||
|
|
@ -27,7 +28,7 @@ import { NgIf, NgFor } from '@angular/common';
|
|||
styleUrls: ['./manage-alerts.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgIf, LoadingComponent, NgFor, FilterPipe, SortableHeader]
|
||||
imports: [ReactiveFormsModule, NgIf, LoadingComponent, NgFor, FilterPipe, SortableHeader, TranslocoModule]
|
||||
})
|
||||
export class ManageAlertsComponent implements OnInit {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,45 +1,46 @@
|
|||
<div class="container-fluid">
|
||||
<ng-container *transloco="let t; read: 'manage-email-settings'">
|
||||
<div class="container-fluid">
|
||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||
<h4>Email Services (SMTP)</h4>
|
||||
<p>Kavita comes out of the box with an email service to power tasks like inviting users, password reset requests, etc. Emails sent via our service are deleted immediately. You can use your own
|
||||
email service by setting up the <a href="https://github.com/Kareadita/KavitaEmail" target="_blank" rel="noopener noreferrer">Kavita Email</a> service. Set the URL of the email service and use the Test button to ensure it works.
|
||||
You can reset these settings to default at any time. There is no way to disable emails for authentication, although you are not required to use a
|
||||
valid email address for users. Confirmation links will always be saved to logs and presented in the UI.
|
||||
Registration/confirmation emails will not be sent if you are not accessing Kavita via a publicly reachable URL or unless the Host Name feature is configured.
|
||||
<span class="text-warning">If you want Send to Device to work you must host your own email service.</span>
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<label for="settings-emailservice" class="form-label">Email Service URL</label><i class="ms-1 fa fa-info-circle" placement="right" [ngbTooltip]="emailServiceTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #emailServiceTooltip>Use fully qualified URL of the email service. Do not include ending slash.</ng-template>
|
||||
<span class="visually-hidden" id="settings-emailservice-help"><ng-container [ngTemplateOutlet]="emailServiceTooltip"></ng-container></span>
|
||||
<div class="input-group">
|
||||
<input id="settings-emailservice" aria-describedby="settings-emailservice-help" class="form-control" formControlName="emailServiceUrl" type="url" autocapitalize="off" inputmode="url">
|
||||
<button class="btn btn-outline-secondary" (click)="resetEmailServiceUrl()">
|
||||
Reset
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" (click)="testEmailServiceUrl()">
|
||||
Test
|
||||
</button>
|
||||
</div>
|
||||
<h4>{{t('title')}}</h4>
|
||||
<p [innerHTML]="t('description', {link: link}) | safeHtml">
|
||||
<span class="text-warning">{{t('send-to-warning')}}</span>
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<label for="settings-emailservice" class="form-label">{{t('email-url-label')}}</label><i class="ms-1 fa fa-info-circle" placement="right" [ngbTooltip]="emailServiceTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #emailServiceTooltip>{{t('email-url-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-emailservice-help"><ng-container [ngTemplateOutlet]="emailServiceTooltip"></ng-container></span>
|
||||
<div class="input-group">
|
||||
<input id="settings-emailservice" aria-describedby="settings-emailservice-help" class="form-control" formControlName="emailServiceUrl" type="url" autocapitalize="off" inputmode="url">
|
||||
<button class="btn btn-outline-secondary" (click)="resetEmailServiceUrl()">
|
||||
{{t('reset')}}
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" (click)="testEmailServiceUrl()">
|
||||
{{t('test')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="settings-hostname" class="form-label">Host Name</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #hostNameTooltip>Domain Name (of Reverse Proxy). If set, email generation will always use this.</ng-template>
|
||||
<span class="visually-hidden" id="settings-hostname-help">Domain Name (of Reverse Proxy). If set, email generation will always use this.</span>
|
||||
<input id="settings-hostname" aria-describedby="settings-hostname-help" class="form-control" formControlName="hostName" type="text"
|
||||
[class.is-invalid]="settingsForm.get('hostName')?.invalid && settingsForm.get('hostName')?.touched">
|
||||
<div id="hostname-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
|
||||
<div *ngIf="settingsForm.get('hostName')?.errors?.pattern">
|
||||
Host name must start with http(s) and not end in /
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="settings-hostname" class="form-label">{{t('host-name-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #hostNameTooltip>{{t('host-name-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-hostname-help">
|
||||
<ng-container [ngTemplateOutlet]="hostNameTooltip"></ng-container>
|
||||
</span>
|
||||
<input id="settings-hostname" aria-describedby="settings-hostname-help" class="form-control" formControlName="hostName" type="text"
|
||||
[class.is-invalid]="settingsForm.get('hostName')?.invalid && settingsForm.get('hostName')?.touched">
|
||||
<div id="hostname-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
|
||||
<div *ngIf="settingsForm.get('hostName')?.errors?.pattern">
|
||||
{{t('host-name-validation')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">Reset to Default</button>
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">{{t('reset')}}</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,37 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs';
|
||||
import { SettingsService, EmailTestResult } from '../settings.service';
|
||||
import { ServerSettings } from '../_models/server-settings';
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {take} from 'rxjs';
|
||||
import {EmailTestResult, SettingsService} from '../settings.service';
|
||||
import {ServerSettings} from '../_models/server-settings';
|
||||
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {NgIf, NgTemplateOutlet} from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
import {SafeHtmlPipe} from "../../pipe/safe-html.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-email-settings',
|
||||
templateUrl: './manage-email-settings.component.html',
|
||||
styleUrls: ['./manage-email-settings.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet]
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe]
|
||||
})
|
||||
export class ManageEmailSettingsComponent implements OnInit {
|
||||
|
||||
serverSettings!: ServerSettings;
|
||||
settingsForm: FormGroup = new FormGroup({});
|
||||
|
||||
constructor(private settingsService: SettingsService, private toastr: ToastrService) { }
|
||||
link = '<a href="https://github.com/Kareadita/KavitaEmail" target="_blank" rel="noopener noreferrer">Kavita Email</a>';
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
constructor(private settingsService: SettingsService, private toastr: ToastrService, private translocoService: TranslocoService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
|
||||
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, []));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -33,18 +39,19 @@ export class ManageEmailSettingsComponent implements OnInit {
|
|||
this.settingsForm.get('emailServiceUrl')?.setValue(this.serverSettings.emailServiceUrl);
|
||||
this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName);
|
||||
this.settingsForm.markAsPristine();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
const modelSettings = Object.assign({}, this.serverSettings);
|
||||
modelSettings.emailServiceUrl = this.settingsForm.get('emailServiceUrl')?.value;
|
||||
modelSettings.hostName = this.settingsForm.get('hostName')?.value;
|
||||
|
||||
|
||||
|
||||
|
||||
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.toastr.success('Server settings updated');
|
||||
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated'));
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
@ -54,7 +61,7 @@ export class ManageEmailSettingsComponent implements OnInit {
|
|||
this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.toastr.success('Server settings updated');
|
||||
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated'));
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
@ -64,7 +71,7 @@ export class ManageEmailSettingsComponent implements OnInit {
|
|||
this.settingsService.resetEmailServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings.emailServiceUrl = settings.emailServiceUrl;
|
||||
this.resetForm();
|
||||
this.toastr.success('Email Service Reset');
|
||||
this.toastr.success(this.translocoService.translate('toasts.email-service-reset'));
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
@ -74,15 +81,15 @@ export class ManageEmailSettingsComponent implements OnInit {
|
|||
if (this.settingsForm.get('emailServiceUrl')?.value === '') return;
|
||||
this.settingsService.testEmailServerSettings(this.settingsForm.get('emailServiceUrl')?.value).pipe(take(1)).subscribe(async (result: EmailTestResult) => {
|
||||
if (result.successful) {
|
||||
this.toastr.success('Email Service was reachable');
|
||||
this.toastr.success(this.translocoService.translate('toasts.email-service-reachable'));
|
||||
} else {
|
||||
this.toastr.error('Email Service Url did not respond. ' + result.errorMessage);
|
||||
this.toastr.error(this.translocoService.translate('toasts.email-service-unresponsive') + result.errorMessage);
|
||||
}
|
||||
|
||||
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,41 @@
|
|||
<div class="container-fluid">
|
||||
<ng-container *transloco="let t; read: 'manage-library'">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>Libraries</h3></div>
|
||||
<div class="col-4"><button class="btn btn-primary float-end" (click)="addLibrary()" title="Add Library"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Add Library</span></button></div>
|
||||
<div class="col-8"><h3>{{t('title')}}</h3></div>
|
||||
<div class="col-4"><button class="btn btn-primary float-end" (click)="addLibrary()" [title]="t('add-library')">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden ms-1">{{t('add-library')}}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let library of libraries; let idx = index; trackBy: libraryTrackBy" class="list-group-item no-hover">
|
||||
<div>
|
||||
<h4>
|
||||
<span id="library-name--{{idx}}"><a [routerLink]="'/library/' + library.id">{{library.name}}</a></span>
|
||||
<div class="float-end">
|
||||
<button class="btn btn-secondary me-2 btn-sm" (click)="scanLibrary(library)" placement="top" ngbTooltip="Scan Library" aria-label="Scan Library"><i class="fa fa-sync-alt" title="Scan"></i></button>
|
||||
<button class="btn btn-danger me-2 btn-sm" [disabled]="deletionInProgress" (click)="deleteLibrary(library)"><i class="fa fa-trash" placement="top" ngbTooltip="Delete Library" attr.aria-label="Delete {{library.name | sentenceCase}}"></i></button>
|
||||
<button class="btn btn-primary btn-sm" (click)="editLibrary(library)"><i class="fa fa-pen" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{library.name | sentenceCase}}"></i></button>
|
||||
</div>
|
||||
</h4>
|
||||
<li *ngFor="let library of libraries; let idx = index; trackBy: libraryTrackBy" class="list-group-item no-hover">
|
||||
<div>
|
||||
<h4>
|
||||
<span id="library-name--{{idx}}"><a [routerLink]="'/library/' + library.id">{{library.name}}</a></span>
|
||||
<div class="float-end">
|
||||
<button class="btn btn-secondary me-2 btn-sm" (click)="scanLibrary(library)" placement="top" [ngbTooltip]="t('scan-library')" [attr.aria-label]="t('scan-library')"><i class="fa fa-sync-alt" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-danger me-2 btn-sm" [disabled]="deletionInProgress" (click)="deleteLibrary(library)"><i class="fa fa-trash" placement="top" [ngbTooltip]="t('delete-library')" [attr.aria-label]="t('delete-library-by-name', {name: library.name | sentenceCase})"></i></button>
|
||||
<button class="btn btn-primary btn-sm" (click)="editLibrary(library)"><i class="fa fa-pen" placement="top" [ngbTooltip]="t('edit-library')" [attr.aria-label]="t('edit-library-by-name', {name: library.name | sentenceCase})"></i></button>
|
||||
</div>
|
||||
<div>Type: {{library.type | libraryType}}</div>
|
||||
<div>Shared Folders: {{library.folders.length + ' folders'}}</div>
|
||||
<div>
|
||||
Last Scanned:
|
||||
<span *ngIf="library.lastScanned === '0001-01-01T00:00:00'; else activeDate">Never</span>
|
||||
<ng-template #activeDate>
|
||||
{{library.lastScanned | timeAgo}}
|
||||
</ng-template>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="loading" class="list-group-item">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="libraries.length === 0 && !loading">
|
||||
There are no libraries. Try creating one.
|
||||
</li>
|
||||
</h4>
|
||||
</div>
|
||||
<div>{{t('type-title')}} {{library.type | libraryType}}</div>
|
||||
<div>{{t('shared-folders-title')}} {{library.folders.length + ' folders'}}</div>
|
||||
<div>
|
||||
{{t('last-scanned-title')}}
|
||||
<span *ngIf="library.lastScanned === '0001-01-01T00:00:00'; else activeDate">Never</span>
|
||||
<ng-template #activeDate>
|
||||
{{library.lastScanned | timeAgo | defaultDate}}
|
||||
</ng-template>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="loading" class="list-group-item">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="invisible">{{t('loading')}}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="libraries.length === 0 && !loading">
|
||||
{{t('no-data')}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import { TimeAgoPipe } from '../../pipe/time-ago.pipe';
|
|||
import { LibraryTypePipe } from '../../pipe/library-type.pipe';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { NgFor, NgIf } from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
import {DefaultDatePipe} from "../../pipe/default-date.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-library',
|
||||
|
|
@ -29,7 +31,7 @@ import { NgFor, NgIf } from '@angular/common';
|
|||
styleUrls: ['./manage-library.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgFor, RouterLink, NgbTooltip, NgIf, LibraryTypePipe, TimeAgoPipe, SentenceCasePipe]
|
||||
imports: [NgFor, RouterLink, NgbTooltip, NgIf, LibraryTypePipe, TimeAgoPipe, SentenceCasePipe, TranslocoModule, DefaultDatePipe]
|
||||
})
|
||||
export class ManageLibraryComponent implements OnInit {
|
||||
|
||||
|
|
@ -116,20 +118,20 @@ export class ManageLibraryComponent implements OnInit {
|
|||
}
|
||||
|
||||
async deleteLibrary(library: Library) {
|
||||
if (await this.confirmService.confirm('Are you sure you want to delete the ' + library.name + ' library? You cannot undo this action.')) {
|
||||
if (await this.confirmService.confirm(translate('toast.confirm-library-delete', {name: library.name}))) {
|
||||
this.deletionInProgress = true;
|
||||
this.libraryService.delete(library.id).pipe(take(1)).subscribe(() => {
|
||||
this.deletionInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
this.getLibraries();
|
||||
this.toastr.success('Library ' + library.name + ' has been removed');
|
||||
this.toastr.success(translate('toasts.library-deleted', {name: library.name}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scanLibrary(library: Library) {
|
||||
this.libraryService.scan(library.id).pipe(take(1)).subscribe(() => {
|
||||
this.toastr.info('A scan has been queued for ' + library.name);
|
||||
this.toastr.info(translate('toasts.scan-queued', {name: library.name}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +1,79 @@
|
|||
<div class="container-fluid">
|
||||
<ng-container *transloco="let t; read: 'manage-media-settings'">
|
||||
<div class="container-fluid">
|
||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined" class="mb-2">
|
||||
|
||||
<div class="row g-0">
|
||||
<p>WebP/AVIF can drastically reduce space requirements for files. WebP/AVIF is not supported on all browsers or versions. To learn if these settings are appropriate for your setup, visit <a href="https://caniuse.com/?search=webp" target="_blank" rel="noopener noreferrer">Can I Use WebP</a> or <a href="https://caniuse.com/?search=avif" target="_blank" rel="noopener noreferrer">Can I Use AVIF</a>.
|
||||
<b>You cannot convert back to PNG once you've gone to WebP/AVIF. You would need to refresh covers on your libraries to regenerate all covers. Bookmarks and favicons cannot be converted.</b></p>
|
||||
<div *ngIf="settingsForm.get('encodeMediaAs')?.dirty" class="alert alert-danger" role="alert">You must trigger the media conversion task in Tasks Tab.</div>
|
||||
<div class="col-md-6 col-sm-12 mb-3">
|
||||
<label for="settings-media-encodeMediaAs" class="form-label me-1">Save Media As</label>
|
||||
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="encodeMediaAsTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #encodeMediaAsTooltip>All media Kavita manages (covers, bookmarks, favicons) will be encoded as this type.</ng-template>
|
||||
<span class="visually-hidden" id="settings-media-encodeMediaAs-help"><ng-container [ngTemplateOutlet]="encodeMediaAsTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-media-encodeMediaAs-help" formControlName="encodeMediaAs" id="settings-media-encodeMediaAs">
|
||||
<option *ngFor="let format of EncodeFormats" [value]="format.value">{{format.title}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<p>{{t('encode-as-description-part-1')}} <a href="https://caniuse.com/?search=webp" target="_blank" rel="noopener noreferrer">{{t('encode-as-description-part-2')}}</a>/<a href="https://caniuse.com/?search=avif" target="_blank" rel="noopener noreferrer">{{t('encode-as-description-part-3')}}</a>
|
||||
<br/><b>{{t('encode-as-warning')}}</b>
|
||||
</p>
|
||||
<div *ngIf="settingsForm.get('encodeMediaAs')?.dirty" class="alert alert-danger" role="alert">{{t('media-warning')}}</div>
|
||||
<div class="col-md-6 col-sm-12 mb-3">
|
||||
<label for="settings-media-encodeMediaAs" class="form-label me-1">{{t('encode-as-label')}}</label>
|
||||
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="encodeMediaAsTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #encodeMediaAsTooltip>{{t('encode-as-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-media-encodeMediaAs-help"><ng-container [ngTemplateOutlet]="encodeMediaAsTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-media-encodeMediaAs-help" formControlName="encodeMediaAs" id="settings-media-encodeMediaAs">
|
||||
<option *ngFor="let format of EncodeFormats" [value]="format.value">{{format.title}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="settings-bookmarksdir" class="form-label">Bookmarks Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="bookmarksDirectoryTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookmarksDirectoryTooltip>Location where bookmarks will be stored. Bookmarks are source files and can be large. Choose a location with adequate storage. Directory is managed; other files within directory will be deleted. If Docker, mount an additional volume and use that.</ng-template>
|
||||
<span class="visually-hidden" id="settings-bookmarksdir-help"><ng-container [ngTemplateOutlet]="bookmarksDirectoryTooltip"></ng-container></span>
|
||||
<div class="input-group">
|
||||
<input readonly id="settings-bookmarksdir" aria-describedby="settings-bookmarksdir-help" class="form-control" formControlName="bookmarksDirectory" type="text" aria-describedby="change-bookmarks-dir">
|
||||
<button id="change-bookmarks-dir" class="btn btn-primary" (click)="openDirectoryChooser(settingsForm.get('bookmarksDirectory')?.value, 'bookmarksDirectory')">
|
||||
Change
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="settings-bookmarksdir" class="form-label">{{t('bookmark-dir-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="bookmarksDirectoryTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookmarksDirectoryTooltip>{{t('bookmark-dir-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-bookmarksdir-help"><ng-container [ngTemplateOutlet]="bookmarksDirectoryTooltip"></ng-container></span>
|
||||
<div class="input-group">
|
||||
<input readonly id="settings-bookmarksdir" aria-describedby="settings-bookmarksdir-help" class="form-control" formControlName="bookmarksDirectory" type="text" aria-describedby="change-bookmarks-dir">
|
||||
<button id="change-bookmarks-dir" class="btn btn-primary" (click)="openDirectoryChooser(settingsForm.get('bookmarksDirectory')?.value, 'bookmarksDirectory')">
|
||||
{{t('change')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">Reset to Default</button>
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">{{t('reset')}}</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-3" ngbAccordion [destroyOnHide]="false">
|
||||
<div ngbAccordionItem>
|
||||
<h2 ngbAccordionHeader>
|
||||
<button ngbAccordionButton>
|
||||
Media Issues <span class="ms-1" *ngIf="alertCount > 0">({{alertCount}})</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<app-manage-alerts (alertCount)="alertCount = $event"></app-manage-alerts>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<div ngbAccordionItem>
|
||||
<h2 ngbAccordionHeader>
|
||||
<button ngbAccordionButton>
|
||||
{{t('media-issue-title')}} <span class="ms-1" *ngIf="alertCount > 0">({{alertCount}})</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<app-manage-alerts (alertCount)="alertCount = $event"></app-manage-alerts>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3" ngbAccordion [destroyOnHide]="false">
|
||||
<div ngbAccordionItem>
|
||||
<h2 ngbAccordionHeader>
|
||||
<button ngbAccordionButton>
|
||||
Scrobble Issues <span class="ms-1" *ngIf="scrobbleCount > 0">({{scrobbleCount}})</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<app-manage-scrobble-errors (scrobbleCount)="scrobbleCount = $event"></app-manage-scrobble-errors>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<div ngbAccordionItem>
|
||||
<h2 ngbAccordionHeader>
|
||||
<button ngbAccordionButton>
|
||||
{{t('scrobble-issue-title')}} <span class="ms-1" *ngIf="scrobbleCount > 0">({{scrobbleCount}})</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<app-manage-scrobble-errors (scrobbleCount)="scrobbleCount = $event"></app-manage-scrobble-errors>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,35 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { ServerSettings } from '../_models/server-settings';
|
||||
import { DirectoryPickerComponent, DirectoryPickerResult } from '../_modals/directory-picker/directory-picker.component';
|
||||
import { NgbModal, NgbTooltip, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EncodeFormats } from '../_models/encode-format';
|
||||
import { ManageScrobbleErrorsComponent } from '../manage-scrobble-errors/manage-scrobble-errors.component';
|
||||
import { ManageAlertsComponent } from '../manage-alerts/manage-alerts.component';
|
||||
import { NgIf, NgTemplateOutlet, NgFor } from '@angular/common';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {take} from 'rxjs';
|
||||
import {SettingsService} from '../settings.service';
|
||||
import {ServerSettings} from '../_models/server-settings';
|
||||
import {DirectoryPickerComponent, DirectoryPickerResult} from '../_modals/directory-picker/directory-picker.component';
|
||||
import {
|
||||
NgbAccordionBody,
|
||||
NgbAccordionButton,
|
||||
NgbAccordionCollapse,
|
||||
NgbAccordionDirective,
|
||||
NgbAccordionHeader,
|
||||
NgbAccordionItem,
|
||||
NgbAccordionToggle,
|
||||
NgbCollapse,
|
||||
NgbModal,
|
||||
NgbTooltip
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {EncodeFormats} from '../_models/encode-format';
|
||||
import {ManageScrobbleErrorsComponent} from '../manage-scrobble-errors/manage-scrobble-errors.component';
|
||||
import {ManageAlertsComponent} from '../manage-alerts/manage-alerts.component';
|
||||
import {NgFor, NgIf, NgTemplateOutlet} from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-media-settings',
|
||||
templateUrl: './manage-media-settings.component.html',
|
||||
styleUrls: ['./manage-media-settings.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, NgFor, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, ManageAlertsComponent, ManageScrobbleErrorsComponent]
|
||||
selector: 'app-manage-media-settings',
|
||||
templateUrl: './manage-media-settings.component.html',
|
||||
styleUrls: ['./manage-media-settings.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, NgFor, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, ManageAlertsComponent, ManageScrobbleErrorsComponent, TranslocoModule]
|
||||
})
|
||||
export class ManageMediaSettingsComponent implements OnInit {
|
||||
|
||||
|
|
@ -26,6 +39,9 @@ export class ManageMediaSettingsComponent implements OnInit {
|
|||
alertCount: number = 0;
|
||||
scrobbleCount: number = 0;
|
||||
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
get EncodeFormats() { return EncodeFormats; }
|
||||
|
||||
constructor(private settingsService: SettingsService, private toastr: ToastrService, private modalService: NgbModal, ) { }
|
||||
|
|
@ -35,6 +51,7 @@ export class ManageMediaSettingsComponent implements OnInit {
|
|||
this.serverSettings = settings;
|
||||
this.settingsForm.addControl('encodeMediaAs', new FormControl(this.serverSettings.encodeMediaAs, [Validators.required]));
|
||||
this.settingsForm.addControl('bookmarksDirectory', new FormControl(this.serverSettings.bookmarksDirectory, [Validators.required]));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +59,7 @@ export class ManageMediaSettingsComponent implements OnInit {
|
|||
this.settingsForm.get('encodeMediaAs')?.setValue(this.serverSettings.encodeMediaAs);
|
||||
this.settingsForm.get('bookmarksDirectory')?.setValue(this.serverSettings.bookmarksDirectory);
|
||||
this.settingsForm.markAsPristine();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
|
|
@ -52,7 +70,7 @@ export class ManageMediaSettingsComponent implements OnInit {
|
|||
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.toastr.success('Server settings updated');
|
||||
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated'));
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
@ -62,7 +80,7 @@ export class ManageMediaSettingsComponent implements OnInit {
|
|||
this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.toastr.success('Server settings updated');
|
||||
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated'));
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
@ -76,6 +94,7 @@ export class ManageMediaSettingsComponent implements OnInit {
|
|||
if (closeResult.success && closeResult.folderPath !== '') {
|
||||
this.settingsForm.get(formControl)?.setValue(closeResult.folderPath);
|
||||
this.settingsForm.markAsDirty();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +1,57 @@
|
|||
<p>This table contains issues found during scrobbling. This list is non-managed.
|
||||
You can clear it at any time and wait for the next scrobble upload to see. If there is an unknown series, you are best correcting the
|
||||
series name or localized series name or adding a weblink for the providers.</p>
|
||||
<ng-container *transloco="let t; read: 'manage-scrobble-errors'">
|
||||
<p>{{t('description')}}</p>
|
||||
|
||||
<form [formGroup]="formGroup">
|
||||
<form [formGroup]="formGroup">
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-12">
|
||||
<label for="filter" class="visually-hidden">Filter</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" type="text" class="form-control" placeholder="Filter" formControlName="filter" />
|
||||
<button class="btn btn-primary" type="button" (click)="clear()">Clear Errors</button>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label for="filter" class="visually-hidden">{{t('filter-label')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" type="text" class="form-control" [placeholder]="t('filter-label')" formControlName="filter" />
|
||||
<button class="btn btn-primary" type="button" (click)="clear()">{{t('clear-errors')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="table table-striped table-hover table-sm table-hover">
|
||||
</form>
|
||||
<table class="table table-striped table-hover table-sm table-hover">
|
||||
<thead #header>
|
||||
<tr>
|
||||
<th scope="col" sortable="seriesId" (sort)="onSort($event)">
|
||||
Series
|
||||
</th>
|
||||
<th scope="col" sortable="created" (sort)="onSort($event)">
|
||||
Created
|
||||
</th>
|
||||
<th scope="col" sortable="comment" (sort)="onSort($event)">
|
||||
Comment
|
||||
</th>
|
||||
<th scope="col" sortable="seriesId" (sort)="onSort($event)">
|
||||
{{t('series-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="created" (sort)="onSort($event)">
|
||||
{{t('created-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="comment" (sort)="onSort($event)">
|
||||
{{t('comment-header')}}
|
||||
</th>
|
||||
<th scope="col">
|
||||
Edit
|
||||
{{t('edit-header')}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody #container>
|
||||
<tr *ngIf="isLoading"><td colspan="4" style="text-align: center;"><app-loading [loading]="isLoading"></app-loading></td></tr>
|
||||
<ng-container *ngIf="data | filter: filterList as filteredData">
|
||||
<tr *ngIf="filteredData.length === 0 && !isLoading"><td colspan="4" style="text-align: center;">No issues</td></tr>
|
||||
<tr *ngFor="let item of filteredData; index as i">
|
||||
<td>
|
||||
<a href="library/{{item.libraryId}}/series/{{item.seriesId}}" target="_blank">{{item.details}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{item.created | date:'shortDate'}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.comment}}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-icon primary-icon" (click)="editSeries(item.seriesId)">
|
||||
<i class="fa fa-pen me-1" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Edit {{item.details}}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="filteredData.length === 0 && !isLoading"><td colspan="4" style="text-align: center;">No issues</td></tr>
|
||||
<tr *ngFor="let item of filteredData; index as i">
|
||||
<td>
|
||||
<a href="library/{{item.libraryId}}/series/{{item.seriesId}}" target="_blank">{{item.details}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{item.created | date:'shortDate'}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.comment}}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-icon primary-icon" (click)="editSeries(item.seriesId)">
|
||||
<i class="fa fa-pen me-1" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('edit-item-alt', {seriesName: item.details})}}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -25,11 +25,12 @@ import {EditSeriesModalComponent} from "../../cards/_modals/edit-series-modal/ed
|
|||
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {FilterPipe} from "../../pipe/filter.pipe";
|
||||
import {LoadingComponent} from "../../shared/loading/loading.component";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-scrobble-errors',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, FilterPipe, LoadingComponent, SortableHeader],
|
||||
imports: [CommonModule, ReactiveFormsModule, FilterPipe, LoadingComponent, SortableHeader, TranslocoModule],
|
||||
templateUrl: './manage-scrobble-errors.component.html',
|
||||
styleUrls: ['./manage-scrobble-errors.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -1,184 +1,205 @@
|
|||
<div class="container-fluid">
|
||||
<ng-container *transloco="let t; read: 'manage-settings'">
|
||||
<div class="container-fluid">
|
||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>Notice:</strong> Changing Port, Base Url, Cache Size or IPs requires a manual restart of Kavita to take effect.
|
||||
</div>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>{{t('notice')}}</strong> {{t('restart-required')}}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="settings-baseurl" class="form-label">Base Url</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #baseUrlTooltip>Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita. Not supported on Docker using non-root user.</ng-template>
|
||||
<span class="visually-hidden" id="settings-cachedir-help">Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita. Not supported on Docker using non-root user.</span>
|
||||
<div class="input-group">
|
||||
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text"
|
||||
[class.is-invalid]="settingsForm.get('baseUrl')?.invalid && settingsForm.get('baseUrl')?.touched">
|
||||
<button class="btn btn-outline-secondary" (click)="resetBaseUrl()">Reset</button>
|
||||
<div class="mb-3">
|
||||
<label for="settings-baseurl" class="form-label">{{t('base-url-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #baseUrlTooltip>{{t('base-url-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-cachedir-help">
|
||||
<ng-container [ngTemplateOutlet]="baseUrlTooltip"></ng-container>
|
||||
</span>
|
||||
<div class="input-group">
|
||||
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text"
|
||||
[class.is-invalid]="settingsForm.get('baseUrl')?.invalid && settingsForm.get('baseUrl')?.touched">
|
||||
<button class="btn btn-outline-secondary" (click)="resetBaseUrl()">{{t('reset')}}</button>
|
||||
</div>
|
||||
<div id="baseurl-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
|
||||
<div *ngIf="settingsForm.get('baseUrl')?.errors?.pattern">
|
||||
{{t('base-url-validation')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-8 col-sm-12 pe-2">
|
||||
<label for="settings-ipaddresses" class="form-label">{{t('ip-address-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="ipAddressesTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #ipAddressesTooltip>{{t('ip-address-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-ipaddresses-help">
|
||||
<ng-container [ngTemplateOutlet]="ipAddressesTooltip"></ng-container>
|
||||
</span>
|
||||
<div class="input-group">
|
||||
<input id="settings-ipaddresses" aria-describedby="settings-ipaddresses-help" class="form-control" formControlName="ipAddresses" type="text"
|
||||
[class.is-invalid]="settingsForm.get('ipAddresses')?.invalid && settingsForm.get('ipAddresses')?.touched">
|
||||
<button class="btn btn-outline-secondary" (click)="resetIPAddresses()">Reset</button>
|
||||
</div>
|
||||
<div id="ipaddresses-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
|
||||
<div *ngIf="settingsForm.get('ipAddresses')?.errors?.pattern">
|
||||
{{t('ip-address-validation')}}
|
||||
</div>
|
||||
<div id="baseurl-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
|
||||
<div *ngIf="settingsForm.get('baseUrl')?.errors?.pattern">
|
||||
Base URL must start and end with /
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-8 col-sm-12 pe-2">
|
||||
<label for="settings-ipaddresses" class="form-label">IP Addresses</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="ipAddressesTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #ipAddressesTooltip>This does not apply to Docker</ng-template>
|
||||
<span class="visually-hidden" id="settings-ipaddresses-help">Comma separated list of Ip addresses the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</span>
|
||||
<div class="input-group">
|
||||
<input id="settings-ipaddresses" aria-describedby="settings-ipaddresses-help" class="form-control" formControlName="ipAddresses" type="text"
|
||||
[class.is-invalid]="settingsForm.get('ipAddresses')?.invalid && settingsForm.get('ipAddresses')?.touched">
|
||||
<button class="btn btn-outline-secondary" (click)="resetIPAddresses()">Reset</button>
|
||||
</div>
|
||||
<div id="ipaddresses-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
|
||||
<div *ngIf="settingsForm.get('ipAddresses')?.errors?.pattern">
|
||||
IP addresses can only contain valid IPv4 or IPv6 addresses
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="settings-port" class="form-label">{{t('port-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #portTooltip>{{t('port-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-port-help">
|
||||
<ng-container [ngTemplateOutlet]="portTooltip"></ng-container>
|
||||
</span>
|
||||
<input id="settings-port" aria-describedby="settings-port-help" class="form-control" formControlName="port" type="number" step="1" min="1" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="settings-port" class="form-label">Port</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>
|
||||
<span class="visually-hidden" id="settings-port-help">Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</span>
|
||||
<input id="settings-port" aria-describedby="settings-port-help" class="form-control" formControlName="port" type="number" step="1" min="1" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
||||
</div>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="backup-tasks" class="form-label">{{t('backup-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="backupTasksTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #backupTasksTooltip>{{t('backup-tooltip')}}.</ng-template>
|
||||
<span class="visually-hidden" id="backup-tasks-help">
|
||||
<ng-container [ngTemplateOutlet]="backupTasksTooltip"></ng-container>
|
||||
</span>
|
||||
<input id="backup-tasks" aria-describedby="backup-tasks-help" class="form-control" formControlName="totalBackups"
|
||||
type="number" inputmode="numeric" step="1" min="1" max="30" onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||
[class.is-invalid]="settingsForm.get('totalBackups')?.invalid && settingsForm.get('totalBackups')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('totalBackups')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
{{t('min-backup-validation')}}
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.max">
|
||||
{{t('max-backup-validation', {num: errors.max.max})}}
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('field-required')}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="backup-tasks" class="form-label">Days of Backups</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="backupTasksTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #backupTasksTooltip>The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</ng-template>
|
||||
<span class="visually-hidden" id="backup-tasks-help">The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</span>
|
||||
<input id="backup-tasks" aria-describedby="backup-tasks-help" class="form-control" formControlName="totalBackups"
|
||||
type="number" inputmode="numeric" step="1" min="1" max="30" onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||
[class.is-invalid]="settingsForm.get('totalBackups')?.invalid && settingsForm.get('totalBackups')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('totalBackups')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
You must have at least 1 backup
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.max">
|
||||
You cannot have more than {{errors.max.max}} backups
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="log-tasks" class="form-label">Days of Logs</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="logTasksTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #logTasksTooltip>The number of logs to maintain. Default is 30, minumum is 1, maximum is 30.</ng-template>
|
||||
<span class="visually-hidden" id="log-tasks-help">The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</span>
|
||||
<input id="log-tasks" aria-describedby="log-tasks-help" class="form-control" formControlName="totalLogs"
|
||||
type="number" inputmode="numeric" step="1" min="1" max="30" onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||
[class.is-invalid]="settingsForm.get('totalLogs')?.invalid && settingsForm.get('totalLogs')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('totalLogs')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
You must have at least 1 log
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.max">
|
||||
You cannot have more than {{errors.max.max}} logs
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<label for="logging-level-port" class="form-label">Logging Level</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="loggingLevelTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #loggingLevelTooltip>Use debug to help identify issues. Debug can eat up a lot of disk space.</ng-template>
|
||||
<span class="visually-hidden" id="logging-level-port-help">Port the server listens on.</span>
|
||||
<select id="logging-level-port" aria-describedby="logging-level-port-help" class="form-select" formControlName="loggingLevel"
|
||||
[class.is-invalid]="settingsForm.get('loggingLevel')?.invalid && settingsForm.get('loggingLevel')?.touched">
|
||||
<option *ngFor="let level of logLevels" [value]="level">{{level | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="log-tasks" class="form-label">{{t('log-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="logTasksTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #logTasksTooltip>{{t('log-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="log-tasks-help">
|
||||
<ng-container [ngTemplateOutlet]="logTasksTooltip"></ng-container>
|
||||
</span>
|
||||
<input id="log-tasks" aria-describedby="log-tasks-help" class="form-control" formControlName="totalLogs"
|
||||
type="number" inputmode="numeric" step="1" min="1" max="30" onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||
[class.is-invalid]="settingsForm.get('totalLogs')?.invalid && settingsForm.get('totalLogs')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('totalLogs')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
{{t('min-log-validation')}}
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.max">
|
||||
{{t('max-logs-validation', {num: errors.max.max})}}
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('field-required')}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-2 mt-3">
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="cache-size" class="form-label">Cache Size</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheSizeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #cacheSizeTooltip>The amount of memory allowed for caching heavy APIs. Default is 75MB.</ng-template>
|
||||
<span class="visually-hidden" id="cache-size-help">The amount of memory allowed for caching heavy APIs. Default is 50MB.</span>
|
||||
<input id="cache-size" aria-describedby="cache-size-help" class="form-control" formControlName="cacheSize"
|
||||
type="number" inputmode="numeric" step="5" min="50" onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||
[class.is-invalid]="settingsForm.get('cacheSize')?.invalid && settingsForm.get('cacheSize')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('cacheSize')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
You must have at least 50 MB.
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="on-deck-progress-days" class="form-label">On Deck Last Progress (days)</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="onDeckProgressDaysTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #onDeckProgressDaysTooltip>The number of days since last progress before kicking something off On Deck.</ng-template>
|
||||
<span class="visually-hidden" id="on-deck-progress-days-help">The number of days since last progress before kicking something off On Deck.</span>
|
||||
<input id="on-deck-progress-days" aria-describedby="on-deck-progress-days-help" class="form-control" formControlName="onDeckProgressDays"
|
||||
type="number" inputmode="numeric" step="1" min="1"
|
||||
[class.is-invalid]="settingsForm.get('onDeckProgressDays')?.invalid && settingsForm.get('onDeckProgressDays')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('onDeckProgressDays')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
Must be at least 1 day
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="on-deck-update-days" class="form-label">On Deck Last Chapter Add (days)</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="onDeckUpdateDaysTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #onDeckUpdateDaysTooltip>The number of days since last chapter was added to include something On Deck.</ng-template>
|
||||
<span class="visually-hidden" id="on-deck-update-days-help">The number of days since last chapter was added to include something On Deck.</span>
|
||||
<input id="on-deck-update-days" aria-describedby="on-deck-update-days-help" class="form-control" formControlName="onDeckUpdateDays"
|
||||
type="number" inputmode="numeric" step="1" min="1"
|
||||
[class.is-invalid]="settingsForm.get('onDeckUpdateDays')?.invalid && settingsForm.get('onDeckUpdateDays')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('onDeckUpdateDays')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
Must be at least 1 day
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<label for="logging-level-port" class="form-label">{{t('logging-level-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="loggingLevelTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #loggingLevelTooltip>{{t('logging-level-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="logging-level-port-help">
|
||||
<ng-container [ngTemplateOutlet]="loggingLevelTooltip"></ng-container>
|
||||
</span>
|
||||
<select id="logging-level-port" aria-describedby="logging-level-port-help" class="form-select" formControlName="loggingLevel"
|
||||
[class.is-invalid]="settingsForm.get('loggingLevel')?.invalid && settingsForm.get('loggingLevel')?.touched">
|
||||
<option *ngFor="let level of logLevels" [value]="level">{{level | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 mt-3">
|
||||
<label for="stat-collection" class="form-label" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
|
||||
<p class="accent" id="collection-info">Send anonymous usage data to Kavita's servers. This includes information on certain features used, number of files, OS version, Kavita install version, CPU, and memory. We will use this information to prioritize features, bug fixes, and performance tuning. Requires restart to take effect. See the <a href="https://wiki.kavitareader.com/en/faq" rel="noopener noreferrer" target="_blank" referrerpolicy="no-refer">wiki</a> for what is collected.</p>
|
||||
<div class="form-check form-switch">
|
||||
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input" formControlName="allowStatCollection" role="switch">
|
||||
<label for="stat-collection" class="form-check-label">Send Data</label>
|
||||
</div>
|
||||
<div class="row g-0 mb-2 mt-3">
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="cache-size" class="form-label">{{t('cache-size-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="cacheSizeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #cacheSizeTooltip>{{t('cache-size-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="cache-size-help">
|
||||
<ng-container [ngTemplateOutlet]="cacheSizeTooltip"></ng-container>
|
||||
</span>
|
||||
<input id="cache-size" aria-describedby="cache-size-help" class="form-control" formControlName="cacheSize"
|
||||
type="number" inputmode="numeric" step="5" min="50" onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||
[class.is-invalid]="settingsForm.get('cacheSize')?.invalid && settingsForm.get('cacheSize')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('cacheSize')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
{{t('min-cache-validation')}}
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('field-required')}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="on-deck-progress-days" class="form-label">{{t('on-deck-last-progress-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="onDeckProgressDaysTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #onDeckProgressDaysTooltip>{{t('on-deck-last-progress-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="on-deck-progress-days-help">
|
||||
<ng-container [ngTemplateOutlet]="onDeckProgressDaysTooltip"></ng-container>
|
||||
</span>
|
||||
<input id="on-deck-progress-days" aria-describedby="on-deck-progress-days-help" class="form-control" formControlName="onDeckProgressDays"
|
||||
type="number" inputmode="numeric" step="1" min="1"
|
||||
[class.is-invalid]="settingsForm.get('onDeckProgressDays')?.invalid && settingsForm.get('onDeckProgressDays')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('onDeckProgressDays')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
{{t('min-days-validation')}}
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('field-required')}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<label for="on-deck-update-days" class="form-label">{{t('on-deck-last-chapter-add-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="onDeckUpdateDaysTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #onDeckUpdateDaysTooltip>{{t('on-deck-last-chapter-add-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="on-deck-update-days-help">
|
||||
<ng-container [ngTemplateOutlet]="onDeckUpdateDaysTooltip"></ng-container>
|
||||
</span>
|
||||
<input id="on-deck-update-days" aria-describedby="on-deck-update-days-help" class="form-control" formControlName="onDeckUpdateDays"
|
||||
type="number" inputmode="numeric" step="1" min="1"
|
||||
[class.is-invalid]="settingsForm.get('onDeckUpdateDays')?.invalid && settingsForm.get('onDeckUpdateDays')?.touched">
|
||||
<ng-container *ngIf="settingsForm.get('onDeckUpdateDays')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
{{t('min-days-validation')}}
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('field-required')}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="opds" aria-describedby="opds-info" class="form-label">OPDS</label>
|
||||
<p class="accent" id="opds-info">OPDS support will allow all users to use OPDS to read and download content from the server.</p>
|
||||
<div class="form-check form-switch">
|
||||
<input id="opds" type="checkbox" aria-label="OPDS Support" class="form-check-input" formControlName="enableOpds">
|
||||
<label for="opds" class="form-check-label">Enable OPDS</label>
|
||||
</div>
|
||||
<div class="mb-3 mt-3">
|
||||
<label for="stat-collection" class="form-label" aria-describedby="collection-info">{{t('allow-stats-label')}}</label>
|
||||
<p class="accent" id="collection-info">{{t('allow-stats-tooltip-part-1')}}<a href="https://wiki.kavitareader.com/en/faq" rel="noopener noreferrer" target="_blank" referrerpolicy="no-refer">wiki</a> {{t('allow-stats-tooltip-part-2')}}</p>
|
||||
<div class="form-check form-switch">
|
||||
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input" formControlName="allowStatCollection" role="switch">
|
||||
<label for="stat-collection" class="form-check-label">{{t('send-data')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="folder-watching" class="form-label" aria-describedby="folder-watching-info">Folder Watching</label>
|
||||
<p class="accent" id="folder-watching-info">Allows Kavita to monitor Library Folders to detect changes and invoke scanning on those changes. This allows content to be updated without manually invoking scans or waiting for nightly scans.</p>
|
||||
<div class="form-check form-switch">
|
||||
<input id="folder-watching" type="checkbox" class="form-check-input" formControlName="enableFolderWatching" role="switch">
|
||||
<label for="folder-watching" class="form-check-label">Enable Folder Watching</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="opds" aria-describedby="opds-info" class="form-label">{{t('opds-label')}}</label>
|
||||
<p class="accent" id="opds-info">{{t('opds-tooltip')}}</p>
|
||||
<div class="form-check form-switch">
|
||||
<input id="opds" type="checkbox" aria-label="OPDS Support" class="form-check-input" formControlName="enableOpds">
|
||||
<label for="opds" class="form-check-label">{{t('enable-opds')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">Reset to Default</button>
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">Save</button>
|
||||
<div class="mb-3">
|
||||
<label for="folder-watching" class="form-label" aria-describedby="folder-watching-info">{{t('folder-watching-label')}}</label>
|
||||
<p class="accent" id="folder-watching-info">{{t('folder-watching-tooltip')}}</p>
|
||||
<div class="form-check form-switch">
|
||||
<input id="folder-watching" type="checkbox" class="form-check-input" formControlName="enableFolderWatching" role="switch">
|
||||
<label for="folder-watching" class="form-check-label">{{t('enable-folder-watching')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">{{t('reset')}}</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, Validators, FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { TagBadgeCursor } from 'src/app/shared/tag-badge/tag-badge.component';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { ServerSettings } from '../_models/server-settings';
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, NgFor, TitleCasePipe } from '@angular/common';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {ServerService} from 'src/app/_services/server.service';
|
||||
import {SettingsService} from '../settings.service';
|
||||
import {ServerSettings} from '../_models/server-settings';
|
||||
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {NgFor, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
const ValidIpAddress = /^(\s*((([12]?\d{1,2}\.){3}[12]?\d{1,2})|(([\da-f]{0,4}\:){0,7}([\da-f]{0,4})))\s*\,)*\s*((([12]?\d{1,2}\.){3}[12]?\d{1,2})|(([\da-f]{0,4}\:){0,7}([\da-f]{0,4})))\s*$/i;
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-settings',
|
||||
templateUrl: './manage-settings.component.html',
|
||||
styleUrls: ['./manage-settings.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, TitleCasePipe]
|
||||
selector: 'app-manage-settings',
|
||||
templateUrl: './manage-settings.component.html',
|
||||
styleUrls: ['./manage-settings.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, TitleCasePipe, TranslocoModule, NgTemplateOutlet]
|
||||
})
|
||||
export class ManageSettingsComponent implements OnInit {
|
||||
|
||||
|
|
@ -24,6 +25,8 @@ export class ManageSettingsComponent implements OnInit {
|
|||
settingsForm: FormGroup = new FormGroup({});
|
||||
taskFrequencies: Array<string> = [];
|
||||
logLevels: Array<string> = [];
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
constructor(private settingsService: SettingsService, private toastr: ToastrService,
|
||||
private serverService: ServerService) { }
|
||||
|
|
@ -31,9 +34,11 @@ export class ManageSettingsComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
this.settingsService.getTaskFrequencies().pipe(take(1)).subscribe(frequencies => {
|
||||
this.taskFrequencies = frequencies;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.settingsService.getLoggingLevels().pipe(take(1)).subscribe(levels => {
|
||||
this.logLevels = levels;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
|
|
@ -60,9 +65,12 @@ export class ManageSettingsComponent implements OnInit {
|
|||
if (info.isDocker) {
|
||||
this.settingsForm.get('ipAddresses')?.disable();
|
||||
this.settingsForm.get('port')?.disable();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
})
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
|
|
@ -85,6 +93,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||
this.settingsForm.get('onDeckProgressDays')?.setValue(this.serverSettings.onDeckProgressDays);
|
||||
this.settingsForm.get('onDeckUpdateDays')?.setValue(this.serverSettings.onDeckUpdateDays);
|
||||
this.settingsForm.markAsPristine();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
|
@ -93,7 +102,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.toastr.success('Server settings updated');
|
||||
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated'));
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
@ -103,7 +112,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||
this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.toastr.success('Server settings updated');
|
||||
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated'));
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
@ -113,7 +122,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||
this.settingsService.resetIPAddressesSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings.ipAddresses = settings.ipAddresses;
|
||||
this.settingsForm.get('ipAddresses')?.setValue(this.serverSettings.ipAddresses);
|
||||
this.toastr.success('IP Addresses Reset');
|
||||
this.toastr.success(this.translocoService.translate('toasts.reset-ip-address'));
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
@ -123,11 +132,10 @@ export class ManageSettingsComponent implements OnInit {
|
|||
this.settingsService.resetBaseUrl().pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings.baseUrl = settings.baseUrl;
|
||||
this.settingsForm.get('baseUrl')?.setValue(this.serverSettings.baseUrl);
|
||||
this.toastr.success('Base Url Reset');
|
||||
this.toastr.success(this.translocoService.translate('toasts.reset-base-url'));
|
||||
this.cdRef.markForCheck();
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,43 @@
|
|||
<div class="container-fluid">
|
||||
<h3>About System</h3>
|
||||
<ng-container *transloco="let t; read: 'manage-system'">
|
||||
<div class="container-fluid">
|
||||
<h3>{{t('title')}}</h3>
|
||||
<hr/>
|
||||
<div class="mb-3" *ngIf="serverInfo">
|
||||
<dl>
|
||||
<dt>Version</dt>
|
||||
<dd>{{serverInfo.kavitaVersion}}</dd>
|
||||
<dl>
|
||||
<dt>{{t('version-title')}}</dt>
|
||||
<dd>{{serverInfo.kavitaVersion}}</dd>
|
||||
|
||||
<dt>Install ID</dt>
|
||||
<dd>{{serverInfo.installId}}</dd>
|
||||
</dl>
|
||||
<dt>{{t('installId-title')}}</dt>
|
||||
<dd>{{serverInfo.installId}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<h3>More Info</h3>
|
||||
<h3>{{t('more-info-title')}}</h3>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-4">Home page:</div>
|
||||
<div class="col"><a href="https://www.kavitareader.com" target="_blank" rel="noopener noreferrer">kavitareader.com</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Wiki:</div>
|
||||
<div class="col"><a href="https://wiki.kavitareader.com" target="_blank" rel="noopener noreferrer">wiki.kavitareader.com</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Discord:</div>
|
||||
<div class="col"><a href="https://discord.gg/b52wT37kt7" target="_blank" rel="noopener noreferrer">discord.gg/b52wT37kt7</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Donations:</div>
|
||||
<div class="col"><a href="https://opencollective.com/kavita" target="_blank" rel="noopener noreferrer">opencollective.com/kavita</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Source:</div>
|
||||
<div class="col"><a href="https://github.com/Kareadita/Kavita" target="_blank" rel="noopener noreferrer">github.com/Kareadita/Kavita</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Feature Requests:</div>
|
||||
<div class="col"><a href="https://feats.kavitareader.com" target="_blank" rel="noopener noreferrer">https://feats.kavitareader.com</a><br/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">{{t('home-page-title')}}</div>
|
||||
<div class="col"><a href="https://www.kavitareader.com" target="_blank" rel="noopener noreferrer">kavitareader.com</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">{{t('wiki-title')}}</div>
|
||||
<div class="col"><a href="https://wiki.kavitareader.com" target="_blank" rel="noopener noreferrer">wiki.kavitareader.com</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">{{t('discord-title')}}</div>
|
||||
<div class="col"><a href="https://discord.gg/b52wT37kt7" target="_blank" rel="noopener noreferrer">discord.gg/b52wT37kt7</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">{{t('donations-title')}}</div>
|
||||
<div class="col"><a href="https://opencollective.com/kavita" target="_blank" rel="noopener noreferrer">opencollective.com/kavita</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">{{t('source-title')}}</div>
|
||||
<div class="col"><a href="https://github.com/Kareadita/Kavita" target="_blank" rel="noopener noreferrer">github.com/Kareadita/Kavita</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">{{t('feature-request-title')}}</div>
|
||||
<div class="col"><a href="https://feats.kavitareader.com" target="_blank" rel="noopener noreferrer">https://feats.kavitareader.com</a><br/></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ import { SettingsService } from '../settings.service';
|
|||
import {ServerInfoSlim} from '../_models/server-info';
|
||||
import { ServerSettings } from '../_models/server-settings';
|
||||
import { NgIf } from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-system',
|
||||
templateUrl: './manage-system.component.html',
|
||||
styleUrls: ['./manage-system.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgIf]
|
||||
imports: [NgIf, TranslocoModule]
|
||||
})
|
||||
export class ManageSystemComponent implements OnInit {
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ export class ManageSystemComponent implements OnInit {
|
|||
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.toastr.success('Server settings updated');
|
||||
this.toastr.success(translate('toasts.server-settings-updated'));
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,73 +1,76 @@
|
|||
<div class="container-fluid">
|
||||
<ng-container *transloco="let t; read: 'manage-tasks-settings'">
|
||||
<div class="container-fluid">
|
||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||
<h4>Recurring Tasks</h4>
|
||||
<div class="mb-3">
|
||||
<label for="settings-tasks-scan" class="form-label">Library Scan</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskScanTooltip>How often Kavita will scan and refresh metadata around library files.</ng-template>
|
||||
<span class="visually-hidden" id="settings-tasks-scan-help">How often Kavita will scan and refresh metadata around library files.</span>
|
||||
<select class="form-select" aria-describedby="settings-tasks-scan-help" formControlName="taskScan" id="settings-tasks-scan">
|
||||
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<h4>{{t('title')}}</h4>
|
||||
<div class="mb-3">
|
||||
<label for="settings-tasks-scan" class="form-label">{{t('library-scan-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskScanTooltip>{{t('library-scan-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-tasks-scan-help"><ng-container [ngTemplateOutlet]="taskScanTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-tasks-scan-help" formControlName="taskScan" id="settings-tasks-scan">
|
||||
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="settings-tasks-backup" class="form-label">Library Database Backup</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskBackupTooltip>How often Kavita will backup the database.</ng-template>
|
||||
<span class="visually-hidden" id="settings-tasks-backup-help">How often Kavita will backup the database.</span>
|
||||
<select class="form-select" aria-describedby="settings-tasks-backup-help" formControlName="taskBackup" id="settings-tasks-backup">
|
||||
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="settings-tasks-backup" class="form-label">{{t('library-database-backup-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskBackupTooltip>{{t('library-database-backup-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="settings-tasks-backup-help"><ng-container [ngTemplateOutlet]="taskBackupTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-tasks-backup-help" formControlName="taskBackup" id="settings-tasks-backup">
|
||||
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h4>Ad-hoc Tasks</h4>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Job Title</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let task of adhocTasks; let idx = index;">
|
||||
<td id="adhoctask--{{idx}}">
|
||||
{{task.name}}
|
||||
</td>
|
||||
<td>
|
||||
{{task.description}}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-primary" (click)="runAdhoc(task)" attr.aria-labelledby="adhoctask--{{idx}}">Run</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>{{t('adhoc-tasks-title')}}</h4>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{t('job-title-header')}}</th>
|
||||
<th scope="col">{{t('description-header')}}</th>
|
||||
<th scope="col">{{t('action-header')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let task of adhocTasks; let idx = index;">
|
||||
<td id="adhoctask--{{idx}}">
|
||||
{{t(task.name)}}
|
||||
</td>
|
||||
<td>
|
||||
{{t(task.description)}}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-primary" (click)="runAdhoc(task)" attr.aria-labelledby="adhoctask--{{idx}}">Run</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Recurring Tasks</h4>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Job Title</th>
|
||||
<th scope="col">Last Executed</th>
|
||||
<th scope="col">Cron</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let task of recurringTasks$ | async; index as i">
|
||||
<td>
|
||||
{{task.title | titlecase}}
|
||||
</td>
|
||||
<td>{{task.lastExecution | date:'short' | defaultValue }}</td>
|
||||
<td>{{task.cron}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>{{t('recurring-tasks-title')}}</h4>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{t('job-title-header')}}</th>
|
||||
<th scope="col">{{t('last-executed-header')}}</th>
|
||||
<th scope="col">{{t('cron-header')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let task of recurringTasks$ | async; index as i">
|
||||
<td>
|
||||
{{task.title | titlecase}}
|
||||
</td>
|
||||
<td>{{task.lastExecution | date:'short' | defaultValue }}</td>
|
||||
<td>{{task.cron}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">Reset to Default</button>
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">{{t('reset')}}</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { ServerSettings } from '../_models/server-settings';
|
||||
import { shareReplay, take } from 'rxjs/operators';
|
||||
import { defer, forkJoin, Observable, of } from 'rxjs';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
import { Job } from 'src/app/_models/job/job';
|
||||
import { UpdateNotificationModalComponent } from 'src/app/shared/update-notification/update-notification-modal.component';
|
||||
import { NgbModal, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { DefaultValuePipe } from '../../pipe/default-value.pipe';
|
||||
import { NgIf, NgFor, AsyncPipe, TitleCasePipe, DatePipe } from '@angular/common';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {SettingsService} from '../settings.service';
|
||||
import {ServerSettings} from '../_models/server-settings';
|
||||
import {shareReplay, take} from 'rxjs/operators';
|
||||
import {defer, forkJoin, Observable, of} from 'rxjs';
|
||||
import {ServerService} from 'src/app/_services/server.service';
|
||||
import {Job} from 'src/app/_models/job/job';
|
||||
import {UpdateNotificationModalComponent} from 'src/app/shared/update-notification/update-notification-modal.component';
|
||||
import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {DownloadService} from 'src/app/shared/_services/download.service';
|
||||
import {DefaultValuePipe} from '../../pipe/default-value.pipe';
|
||||
import {AsyncPipe, DatePipe, NgFor, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
interface AdhocTask {
|
||||
name: string;
|
||||
|
|
@ -22,14 +23,17 @@ interface AdhocTask {
|
|||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-tasks-settings',
|
||||
templateUrl: './manage-tasks-settings.component.html',
|
||||
styleUrls: ['./manage-tasks-settings.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, AsyncPipe, TitleCasePipe, DatePipe, DefaultValuePipe]
|
||||
selector: 'app-manage-tasks-settings',
|
||||
templateUrl: './manage-tasks-settings.component.html',
|
||||
styleUrls: ['./manage-tasks-settings.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, AsyncPipe, TitleCasePipe, DatePipe, DefaultValuePipe, TranslocoModule, NgTemplateOutlet]
|
||||
})
|
||||
export class ManageTasksSettingsComponent implements OnInit {
|
||||
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
serverSettings!: ServerSettings;
|
||||
settingsForm: FormGroup = new FormGroup({});
|
||||
taskFrequencies: Array<string> = [];
|
||||
|
|
@ -39,55 +43,55 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
// noinspection JSVoidFunctionReturnValueUsed
|
||||
adhocTasks: Array<AdhocTask> = [
|
||||
{
|
||||
name: 'Convert Media to Target Encoding',
|
||||
description: 'Runs a long-running task which will convert all kavita-managed media to the target encoding. This is slow (especially on ARM devices).',
|
||||
name: 'convert-media-task',
|
||||
description: 'convert-media-task-desc',
|
||||
api: this.serverService.convertMedia(),
|
||||
successMessage: 'Conversion of Media to Target Encoding has been queued'
|
||||
successMessage: 'convert-media-task-success'
|
||||
},
|
||||
{
|
||||
name: 'Bust Cache',
|
||||
description: 'Busts the Kavita+ Cache - should only be used when debugging bad matches.',
|
||||
name: 'bust-cache-task',
|
||||
description: 'bust-cache-task-desc',
|
||||
api: this.serverService.bustCache(),
|
||||
successMessage: 'Kavita+ Cache busted'
|
||||
successMessage: 'bust-cache-task-success'
|
||||
},
|
||||
{
|
||||
name: 'Clear Reading Cache',
|
||||
description: 'Clears cached files for reading. Useful when you\'ve just updated a file that you were previously reading within the last 24 hours.',
|
||||
name: 'clear-reading-cache-task',
|
||||
description: 'clear-reading-cache-task-desc',
|
||||
api: this.serverService.clearCache(),
|
||||
successMessage: 'Cache has been cleared'
|
||||
successMessage: 'clear-reading-cache-task-success'
|
||||
},
|
||||
{
|
||||
name: 'Clean up Want to Read',
|
||||
description: 'Removes any series that users have fully read that are within Want to Read and have a publication status of Completed. Runs every 24 hours.',
|
||||
name: 'clean-up-want-to-read-task',
|
||||
description: 'clean-up-want-to-read-task-desc',
|
||||
api: this.serverService.cleanupWantToRead(),
|
||||
successMessage: 'Want to Read has been cleaned up'
|
||||
successMessage: 'clean-up-want-to-read-task-success'
|
||||
},
|
||||
{
|
||||
name: 'Backup Database',
|
||||
description: 'Takes a backup of the database, bookmarks, themes, manually uploaded covers, and config files.',
|
||||
name: 'backup-database-task',
|
||||
description: 'backup-database-task-desc',
|
||||
api: this.serverService.backupDatabase(),
|
||||
successMessage: 'A job to backup the database has been queued'
|
||||
successMessage: 'backup-database-task-success'
|
||||
},
|
||||
{
|
||||
name: 'Download Logs',
|
||||
description: 'Compiles all log files into a zip and downloads it.',
|
||||
name: 'download-logs-task',
|
||||
description: 'download-logs-task-desc',
|
||||
api: defer(() => of(this.downloadService.download('logs', undefined))),
|
||||
successMessage: ''
|
||||
},
|
||||
{
|
||||
name: 'Analyze Files',
|
||||
description: 'Runs a long-running task which will analyze files to generate extension and size. This should only be ran once for the v0.7 release. Not needed if you installed post v0.7.',
|
||||
name: 'analyze-files-task',
|
||||
description: 'analyze-files-task-desc',
|
||||
api: this.serverService.analyzeFiles(),
|
||||
successMessage: 'File analysis has been queued'
|
||||
successMessage: 'analyze-files-task-success'
|
||||
},
|
||||
{
|
||||
name: 'Check for Updates',
|
||||
description: 'See if there are any Stable releases ahead of your version.',
|
||||
name: 'check-for-updates-task',
|
||||
description: 'check-for-updates-task-desc',
|
||||
api: this.serverService.checkForUpdate(),
|
||||
successMessage: '',
|
||||
successFunction: (update) => {
|
||||
if (update === null) {
|
||||
this.toastr.info('No updates available');
|
||||
this.toastr.info(this.translocoService.translate('toasts.no-updates'));
|
||||
return;
|
||||
}
|
||||
const modalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' });
|
||||
|
|
@ -105,23 +109,24 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
frequencies: this.settingsService.getTaskFrequencies(),
|
||||
levels: this.settingsService.getLoggingLevels(),
|
||||
settings: this.settingsService.getServerSettings()
|
||||
}
|
||||
|
||||
).subscribe(result => {
|
||||
}).subscribe(result => {
|
||||
this.taskFrequencies = result.frequencies;
|
||||
this.logLevels = result.levels;
|
||||
this.serverSettings = result.settings;
|
||||
this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required]));
|
||||
this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required]));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.recurringTasks$ = this.serverService.getRecurringJobs().pipe(shareReplay());
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
this.settingsForm.get('taskScan')?.setValue(this.serverSettings.taskScan);
|
||||
this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup);
|
||||
this.settingsForm.markAsPristine();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
|
@ -133,7 +138,8 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.recurringTasks$ = this.serverService.getRecurringJobs().pipe(shareReplay());
|
||||
this.toastr.success('Server settings updated');
|
||||
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated'));
|
||||
this.cdRef.markForCheck();
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
@ -143,7 +149,7 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
this.settingsService.resetServerSettings().pipe(take(1)).subscribe(async (settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.toastr.success('Server settings updated');
|
||||
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated'));
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
|
@ -152,7 +158,7 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
runAdhoc(task: AdhocTask) {
|
||||
task.api.subscribe((data: any) => {
|
||||
if (task.successMessage.length > 0) {
|
||||
this.toastr.success(task.successMessage);
|
||||
this.toastr.success(this.translocoService.translate('manage-tasks-settings.' + task.successMessage));
|
||||
}
|
||||
|
||||
if (task.successFunction) {
|
||||
|
|
|
|||
|
|
@ -1,56 +1,64 @@
|
|||
<ng-container *transloco="let t; read: 'manage-users'">
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>Active Users</h3></div>
|
||||
<div class="col-4"><button class="btn btn-primary float-end" (click)="inviteUser()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Invite</span></button></div>
|
||||
<div class="col-8"><h3>{{t('title')}}</h3></div>
|
||||
<div class="col-4"><button class="btn btn-primary float-end" (click)="inviteUser()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> {{t('invite')}}</span></button></div>
|
||||
</div>
|
||||
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let member of members; let idx = index;" class="list-group-item no-hover">
|
||||
<div>
|
||||
<h4>
|
||||
<span id="member-name--{{idx}}">{{member.username | titlecase}} </span>
|
||||
<span *ngIf="member.username === loggedInUsername">
|
||||
<i class="fas fa-star" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">(You)</span>
|
||||
</span>
|
||||
<span class="badge bg-secondary text-dark" *ngIf="member.isPending">Pending</span>
|
||||
<div class="float-end" *ngIf="canEditMember(member)">
|
||||
<button class="btn btn-danger btn-sm me-2" (click)="deleteUser(member)" placement="top" ngbTooltip="Delete User" attr.aria-label="Delete User {{member.username | titlecase}}"><i class="fa fa-trash" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-primary btn-sm me-2" (click)="openEditUser(member)" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{member.username | titlecase}}"><i class="fa fa-pen" aria-hidden="true"></i></button>
|
||||
|
||||
<button *ngIf="member.isPending" class="btn btn-secondary btn-sm me-2" (click)="resendEmail(member)" placement="top" ngbTooltip="Resend Invite" attr.aria-label="Delete Invite {{member.username | titlecase}}">Resend</button>
|
||||
<button *ngIf="member.isPending" class="btn btn-secondary btn-sm me-2" (click)="setup(member)" placement="top" ngbTooltip="Setup User" attr.aria-label="Setup User {{member.username | titlecase}}">Setup</button>
|
||||
<button *ngIf="!member.isPending" class="btn btn-secondary btn-sm" (click)="updatePassword(member)" placement="top" ngbTooltip="Change Password" attr.aria-label="Change Password for {{member.username | titlecase}}"><i class="fa fa-key" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="user-info">
|
||||
<div>Last Active:
|
||||
<span *ngIf="member.lastActive === '0001-01-01T00:00:00'; else activeDate">Never</span>
|
||||
<ng-template #activeDate>
|
||||
{{member.lastActive | date: 'short'}} <i class="presence fa fa-circle ms-1" title="Online Now" aria-hidden="true" *ngIf="(messageHub.onlineUsers$ | async)?.includes(member.username)"></i>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div *ngIf="!hasAdminRole(member)">Sharing: {{formatLibraries(member)}}</div>
|
||||
<div class="row g-0">
|
||||
<div>
|
||||
Roles: <span *ngIf="getRoles(member).length === 0; else showRoles">None</span>
|
||||
<ng-template #showRoles>
|
||||
<app-tag-badge *ngFor="let role of getRoles(member)" class="col-auto">{{role}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let member of members; let idx = index;" class="list-group-item no-hover">
|
||||
<div>
|
||||
<h4>
|
||||
<span id="member-name--{{idx}}">{{member.username | titlecase}} </span>
|
||||
<span *ngIf="member.username === loggedInUsername">
|
||||
<i class="fas fa-star" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('you-alt')}}</span>
|
||||
</span>
|
||||
<span class="badge bg-secondary text-dark" *ngIf="member.isPending">{{t('pending-title')}}</span>
|
||||
<div class="float-end" *ngIf="canEditMember(member)">
|
||||
<button class="btn btn-danger btn-sm me-2" (click)="deleteUser(member)"
|
||||
placement="top" [ngbTooltip]="t('delete-user-tooltip')" [attr.aria-label]="t('delete-user-alt', {user: member.username | titlecase})">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm me-2" (click)="openEditUser(member)"
|
||||
placement="top" [ngbTooltip]="t('edit-user-tooltip')" [attr.aria-label]="t('edit-user-alt', {user: member.username | titlecase})">
|
||||
<i class="fa fa-pen" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<button *ngIf="member.isPending" class="btn btn-secondary btn-sm me-2" (click)="resendEmail(member)"
|
||||
placement="top" [ngbTooltip]="t('resend-invite-tooltip')" [attr.aria-label]="t('resend-invite-alt', {user: member.username | titlecase})">{{t('resend')}}}</button>
|
||||
<button *ngIf="member.isPending" class="btn btn-secondary btn-sm me-2" (click)="setup(member)"
|
||||
placement="top" [ngbTooltip]="t('setup-user-tooltip')" [attr.aria-label]="t('setup-user-alt', {user: member.username | titlecase})">Setup</button>
|
||||
<button *ngIf="!member.isPending" class="btn btn-secondary btn-sm" (click)="updatePassword(member)"
|
||||
placement="top" [ngbTooltip]="t('change-password-tooltip')" [attr.aria-label]="t('change-password-alt', {user: member.username | titlecase})"><i class="fa fa-key" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="loadingMembers" class="list-group-item">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</h4>
|
||||
<div class="user-info">
|
||||
<div>{{t('last-active-title')}}
|
||||
<span>{{member.lastActive | date: 'short' | defaultDate}} <i class="presence fa fa-circle ms-1" [title]="t('online-now-tooltip')" aria-hidden="true" *ngIf="(messageHub.onlineUsers$ | async)?.includes(member.username)"></i></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="members.length === 0 && !loadingMembers">
|
||||
There are no other users.
|
||||
</li>
|
||||
<div *ngIf="!hasAdminRole(member)">{{t('sharing-title')}} {{formatLibraries(member)}}</div>
|
||||
<div class="row g-0">
|
||||
<div>
|
||||
{{t('roles-title')}} <span *ngIf="getRoles(member).length === 0; else showRoles">{{t('none')}}</span>
|
||||
<ng-template #showRoles>
|
||||
<app-tag-badge *ngFor="let role of getRoles(member)" class="col-auto">{{role}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="loadingMembers" class="list-group-item">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="visually-hidden">{{t('loading')}}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="members.length === 0 && !loadingMembers">
|
||||
{{t('no-data')}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,35 +1,38 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { NgbModal, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
import { Member } from 'src/app/_models/auth/member';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ResetPasswordModalComponent } from '../_modals/reset-password-modal/reset-password-modal.component';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { Subject } from 'rxjs';
|
||||
import { MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
import { InviteUserComponent } from '../invite-user/invite-user.component';
|
||||
import { EditUserComponent } from '../edit-user/edit-user.component';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { TagBadgeComponent } from '../../shared/tag-badge/tag-badge.component';
|
||||
import { NgFor, NgIf, AsyncPipe, TitleCasePipe, DatePipe } from '@angular/common';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {MemberService} from 'src/app/_services/member.service';
|
||||
import {Member} from 'src/app/_models/auth/member';
|
||||
import {AccountService} from 'src/app/_services/account.service';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {ResetPasswordModalComponent} from '../_modals/reset-password-modal/reset-password-modal.component';
|
||||
import {ConfirmService} from 'src/app/shared/confirm.service';
|
||||
import {MessageHubService} from 'src/app/_services/message-hub.service';
|
||||
import {InviteUserComponent} from '../invite-user/invite-user.component';
|
||||
import {EditUserComponent} from '../edit-user/edit-user.component';
|
||||
import {ServerService} from 'src/app/_services/server.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {TagBadgeComponent} from '../../shared/tag-badge/tag-badge.component';
|
||||
import {AsyncPipe, DatePipe, NgFor, NgIf, TitleCasePipe} from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
import {DefaultDatePipe} from "../../pipe/default-date.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-users',
|
||||
templateUrl: './manage-users.component.html',
|
||||
styleUrls: ['./manage-users.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgFor, NgIf, NgbTooltip, TagBadgeComponent, AsyncPipe, TitleCasePipe, DatePipe]
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgFor, NgIf, NgbTooltip, TagBadgeComponent, AsyncPipe, TitleCasePipe, DatePipe, TranslocoModule, DefaultDatePipe]
|
||||
})
|
||||
export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
export class ManageUsersComponent implements OnInit {
|
||||
|
||||
members: Member[] = [];
|
||||
loggedInUsername = '';
|
||||
loadingMembers = false;
|
||||
|
||||
private onDestroy = new Subject<void>();
|
||||
translocoService = inject(TranslocoService);
|
||||
cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
constructor(private memberService: MemberService,
|
||||
private accountService: AccountService,
|
||||
|
|
@ -42,6 +45,7 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
this.accountService.currentUser$.pipe(take(1)).subscribe((user) => {
|
||||
if (user) {
|
||||
this.loggedInUsername = user.username;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -51,16 +55,13 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
this.loadMembers();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
loadMembers() {
|
||||
this.loadingMembers = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.memberService.getMembers(true).subscribe(members => {
|
||||
this.members = members;
|
||||
// Show logged in user at the top of the list
|
||||
// Show logged-in user at the top of the list
|
||||
this.members.sort((a: Member, b: Member) => {
|
||||
if (a.username === this.loggedInUsername) return 1;
|
||||
if (b.username === this.loggedInUsername) return 1;
|
||||
|
|
@ -72,6 +73,7 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
return 0;
|
||||
})
|
||||
this.loadingMembers = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -86,15 +88,15 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
this.loadMembers();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async deleteUser(member: Member) {
|
||||
if (await this.confirmService.confirm('Are you sure you want to delete this user?')) {
|
||||
if (await this.confirmService.confirm(this.translocoService.translate('toasts.confirm-delete-user'))) {
|
||||
this.memberService.deleteMember(member.username).subscribe(() => {
|
||||
setTimeout(() => {
|
||||
this.loadMembers();
|
||||
this.toastr.success(member.username + ' has been deleted.');
|
||||
}, 30); // SetTimeout because I've noticed this can run super fast and not give enough time for data to flush
|
||||
this.toastr.success(this.translocoService.translate('toasts.user-deleted', {user: member.username}));
|
||||
}, 30); // SetTimeout because I've noticed this can run superfast and not give enough time for data to flush
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -106,17 +108,15 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
log(o: any) {console.log(o)}
|
||||
|
||||
resendEmail(member: Member) {
|
||||
this.serverService.isServerAccessible().subscribe(canAccess => {
|
||||
this.accountService.resendConfirmationEmail(member.id).subscribe(async (email) => {
|
||||
if (canAccess) {
|
||||
this.toastr.info('Email sent to ' + member.username);
|
||||
this.toastr.info(this.translocoService.translate('toasts.email-sent-to-user', {user: member.username}));
|
||||
return;
|
||||
}
|
||||
await this.confirmService.alert(
|
||||
'Please click this link to confirm your email. You must confirm to be able to login. You may need to log out of the current account before clicking. <br/> <a href="' + email + '" target="_blank" rel="noopener noreferrer">' + email + '</a>');
|
||||
this.translocoService.translate('toasts.click-email-link') + '<br/> <a href="' + email + '" target="_blank" rel="noopener noreferrer">' + email + '</a>');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -136,7 +136,7 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
|
||||
formatLibraries(member: Member) {
|
||||
if (member.libraries.length === 0) {
|
||||
return 'None';
|
||||
return this.translocoService.translate('manage-users.none');
|
||||
}
|
||||
|
||||
return member.libraries.map(item => item.name).join(', ');
|
||||
|
|
@ -149,5 +149,5 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
getRoles(member: Member) {
|
||||
return member.roles.filter(item => item != 'Pleb');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
<h4>Roles</h4>
|
||||
<ul class="list-group">
|
||||
<ng-container *transloco="let t; read:'role-selector'">
|
||||
<h4>{{t('title')}}</h4>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" *ngFor="let role of selectedRoles; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="role-{{i}}" type="checkbox" attr.aria-label="Role {{role.data}}" class="form-check-input"
|
||||
[(ngModel)]="role.selected" [disabled]="role.disabled" name="role" (ngModelChange)="handleModelUpdate()">
|
||||
<label attr.for="role-{{i}}" class="form-check-label">{{role.data}}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="role-{{i}}" type="checkbox" class="form-check-input"
|
||||
[(ngModel)]="role.selected" [disabled]="role.disabled" name="role" (ngModelChange)="handleModelUpdate()">
|
||||
<label for="role-{{i}}" class="form-check-label">{{role.data}}</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { AccountService } from 'src/app/_services/account.service';
|
|||
import { MemberService } from 'src/app/_services/member.service';
|
||||
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
|
||||
import { NgFor } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-role-selector',
|
||||
|
|
@ -13,7 +14,7 @@ import { NgFor } from '@angular/common';
|
|||
styleUrls: ['./role-selector.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgFor, ReactiveFormsModule, FormsModule]
|
||||
imports: [NgFor, ReactiveFormsModule, FormsModule, TranslocoModule]
|
||||
})
|
||||
export class RoleSelectorComponent implements OnInit {
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue