A ton of random bugs and polish (#3668)
This commit is contained in:
parent
b45d92ea5c
commit
de651215f5
144 changed files with 852 additions and 848 deletions
62
UI/Web/src/app/_helpers/browser.ts
Normal file
62
UI/Web/src/app/_helpers/browser.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
export const isSafari = [
|
||||
'iPad Simulator',
|
||||
'iPhone Simulator',
|
||||
'iPod Simulator',
|
||||
'iPad',
|
||||
'iPhone',
|
||||
'iPod'
|
||||
].includes(navigator.platform)
|
||||
// iPad on iOS 13 detection
|
||||
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document);
|
||||
|
||||
/**
|
||||
* Represents a Version for a browser
|
||||
*/
|
||||
export class Version {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
|
||||
constructor(major: number, minor: number, patch: number) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.patch = patch;
|
||||
}
|
||||
|
||||
isLessThan(other: Version): boolean {
|
||||
if (this.major < other.major) return true;
|
||||
if (this.major > other.major) return false;
|
||||
if (this.minor < other.minor) return true;
|
||||
if (this.minor > other.minor) return false;
|
||||
return this.patch < other.patch;
|
||||
}
|
||||
|
||||
isGreaterThan(other: Version): boolean {
|
||||
if (this.major > other.major) return true;
|
||||
if (this.major < other.major) return false;
|
||||
if (this.minor > other.minor) return true;
|
||||
if (this.minor < other.minor) return false;
|
||||
return this.patch > other.patch;
|
||||
}
|
||||
|
||||
isEqualTo(other: Version): boolean {
|
||||
return (
|
||||
this.major === other.major &&
|
||||
this.minor === other.minor &&
|
||||
this.patch === other.patch
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const getIosVersion = () => {
|
||||
const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/);
|
||||
if (match) {
|
||||
const major = parseInt(match[1], 10);
|
||||
const minor = parseInt(match[2], 10);
|
||||
const patch = parseInt(match[3] || '0', 10);
|
||||
|
||||
return new Version(major, minor, patch);
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import {FileTypeGroup} from "./file-type-group.enum";
|
||||
import {IHasCover} from "../common/i-has-cover";
|
||||
|
||||
export enum LibraryType {
|
||||
Manga = 0,
|
||||
|
@ -10,6 +9,8 @@ export enum LibraryType {
|
|||
ComicVine = 5
|
||||
}
|
||||
|
||||
export const allLibraryTypes = [LibraryType.Manga, LibraryType.ComicVine, LibraryType.Comic, LibraryType.Book, LibraryType.LightNovel, LibraryType.Images];
|
||||
|
||||
export interface Library {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
|
@ -11,7 +11,7 @@ import {TranslocoService} from "@jsverse/transloco";
|
|||
})
|
||||
export class LibraryTypePipe implements PipeTransform {
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
transform(libraryType: LibraryType): string {
|
||||
switch (libraryType) {
|
||||
case LibraryType.Book:
|
||||
|
|
|
@ -23,6 +23,7 @@ import {Volume} from "../_models/volume";
|
|||
import {UtilityService} from "../shared/_services/utility.service";
|
||||
import {translate} from "@jsverse/transloco";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {getIosVersion, isSafari, Version} from "../_helpers/browser";
|
||||
|
||||
|
||||
export const CHAPTER_ID_DOESNT_EXIST = -1;
|
||||
|
@ -46,7 +47,8 @@ export class ReaderService {
|
|||
// Override background color for reader and restore it onDestroy
|
||||
private originalBodyColor!: string;
|
||||
|
||||
private noSleep = new NoSleep();
|
||||
|
||||
private noSleep: NoSleep = new NoSleep();
|
||||
|
||||
constructor(private httpClient: HttpClient, @Inject(DOCUMENT) private document: Document) {
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||
|
@ -56,17 +58,18 @@ export class ReaderService {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
enableWakeLock(element?: Element | Document) {
|
||||
// Enable wake lock.
|
||||
// (must be wrapped in a user input event handler e.g. a mouse or touch handler)
|
||||
|
||||
if (!element) element = this.document;
|
||||
|
||||
const enableNoSleepHandler = () => {
|
||||
const enableNoSleepHandler = async () => {
|
||||
element!.removeEventListener('click', enableNoSleepHandler, false);
|
||||
element!.removeEventListener('touchmove', enableNoSleepHandler, false);
|
||||
element!.removeEventListener('mousemove', enableNoSleepHandler, false);
|
||||
this.noSleep!.enable();
|
||||
await this.noSleep.enable();
|
||||
};
|
||||
|
||||
// Enable wake lock.
|
||||
|
|
|
@ -41,10 +41,6 @@ export class ServerService {
|
|||
return this.http.post(this.baseUrl + 'server/backup-db', {});
|
||||
}
|
||||
|
||||
analyzeFiles() {
|
||||
return this.http.post(this.baseUrl + 'server/analyze-files', {});
|
||||
}
|
||||
|
||||
syncThemes() {
|
||||
return this.http.post(this.baseUrl + 'server/sync-themes', {});
|
||||
}
|
||||
|
@ -58,10 +54,6 @@ export class ServerService {
|
|||
.pipe(map(r => parseInt(r, 10)));
|
||||
}
|
||||
|
||||
checkForUpdates() {
|
||||
return this.http.get<UpdateVersionEvent>(this.baseUrl + 'server/check-for-updates', {});
|
||||
}
|
||||
|
||||
getChangelog(count: number = 0) {
|
||||
return this.http.get<UpdateVersionEvent[]>(this.baseUrl + 'server/changelog?count=' + count, {});
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<div class="mb-3 ms-1">
|
||||
<h4 class="header">{{t('genres-title')}}</h4>
|
||||
<div class="ms-3">
|
||||
<app-badge-expander [includeComma]="true" [items]="genres" [itemsTillExpander]="3">
|
||||
<app-badge-expander [includeComma]="true" [items]="genres" [itemsTillExpander]="3" [defaultExpanded]="true">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Genres, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
|
@ -52,7 +52,7 @@
|
|||
<div class="mb-3 ms-1">
|
||||
<h4 class="header">{{t('tags-title')}}</h4>
|
||||
<div class="ms-3">
|
||||
<app-badge-expander [includeComma]="true" [items]="tags" [itemsTillExpander]="3">
|
||||
<app-badge-expander [includeComma]="true" [items]="tags" [itemsTillExpander]="3" [defaultExpanded]="true">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Tags, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
|
|
|
@ -37,7 +37,7 @@ import {DownloadService} from "../../shared/_services/download.service";
|
|||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||
import {TypeaheadComponent} from "../../typeahead/_components/typeahead.component";
|
||||
import {forkJoin, Observable, of, tap} from "rxjs";
|
||||
import {map} from "rxjs/operators";
|
||||
import {map, switchMap} from "rxjs/operators";
|
||||
import {EntityTitleComponent} from "../../cards/entity-title/entity-title.component";
|
||||
import {SettingButtonComponent} from "../../settings/_components/setting-button/setting-button.component";
|
||||
import {CoverImageChooserComponent} from "../../cards/cover-image-chooser/cover-image-chooser.component";
|
||||
|
@ -212,11 +212,13 @@ export class EditChapterModalComponent implements OnInit {
|
|||
this.editForm.addControl('coverImageIndex', new FormControl(0, []));
|
||||
this.editForm.addControl('coverImageLocked', new FormControl(this.chapter.coverImageLocked, []));
|
||||
|
||||
this.metadataService.getAllValidLanguages().subscribe(validLanguages => {
|
||||
this.validLanguages = validLanguages;
|
||||
this.setupLanguageTypeahead();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.metadataService.getAllValidLanguages().pipe(
|
||||
tap(validLanguages => {
|
||||
this.validLanguages = validLanguages;
|
||||
this.cdRef.markForCheck();
|
||||
}),
|
||||
switchMap(_ => this.setupLanguageTypeahead())
|
||||
).subscribe();
|
||||
|
||||
this.metadataService.getAllAgeRatings().subscribe(ratings => {
|
||||
this.ageRatings = ratings;
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
|
||||
<div class="row g-0 mt-2">
|
||||
@if(settingsForm.get('enableSsl'); as formControl) {
|
||||
<app-setting-switch [title]="t('enable-ssl-label')">
|
||||
<app-setting-switch labelId="setting-enable-ssl" [title]="t('enable-ssl-label')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="setting-enable-ssl" type="checkbox" class="form-check-input" formControlName="enableSsl">
|
||||
|
|
|
@ -1,77 +1,72 @@
|
|||
<ng-container *transloco="let t; read: 'manage-system'">
|
||||
<div class="container-fluid">
|
||||
|
||||
@if (serverInfo) {
|
||||
<div class="mb-3">
|
||||
<h3>{{t('title')}}</h3>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div>{{t('version-title')}}</div>
|
||||
<div>{{serverInfo.kavitaVersion}}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div>{{t('installId-title')}}</div>
|
||||
<div>{{serverInfo.installId}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div>{{t('first-install-version-title')}}</div>
|
||||
<div>{{serverInfo.firstInstallVersion}}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div>{{t('first-install-date-title')}}</div>
|
||||
<div>{{serverInfo.firstInstallDate | date:'shortDate'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
}
|
||||
|
||||
@if (serverInfo) {
|
||||
<div class="mb-3">
|
||||
<h3>{{t('more-info-title')}}</h3>
|
||||
<h3>{{t('title')}}</h3>
|
||||
|
||||
<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 class="col-4">
|
||||
<div>{{t('version-title')}}</div>
|
||||
<div>{{serverInfo.kavitaVersion}}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div>{{t('installId-title')}}</div>
|
||||
<div>{{serverInfo.installId}}</div>
|
||||
</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('localization-title')}}</div>
|
||||
<div class="col"><a href="https://hosted.weblate.org/engage/kavita/" target="_blank" rel="noopener noreferrer">Weblate</a><br/></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">{{t('feature-request-title')}}</div>
|
||||
<div class="col"><a href="https://github.com/Kareadita/Kavita/discussions/2529" target="_blank" rel="noopener noreferrer">https://github.com/Kareadita/Kavita/discussions/</a><br/></div>
|
||||
<div class="col-4">
|
||||
<div>{{t('first-install-version-title')}}</div>
|
||||
<div>{{serverInfo.firstInstallVersion}}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div>{{t('first-install-date-title')}}</div>
|
||||
<div>{{serverInfo.firstInstallDate | date:'shortDate'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<h3>{{t('updates-title')}}</h3>
|
||||
<app-changelog></app-changelog>
|
||||
<div class="mb-3">
|
||||
<h3>{{t('more-info-title')}}</h3>
|
||||
<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('localization-title')}}</div>
|
||||
<div class="col"><a href="https://hosted.weblate.org/engage/kavita/" target="_blank" rel="noopener noreferrer">Weblate</a><br/></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">{{t('feature-request-title')}}</div>
|
||||
<div class="col"><a href="https://github.com/Kareadita/Kavita/discussions/2529" target="_blank" rel="noopener noreferrer">https://github.com/Kareadita/Kavita/discussions/</a><br/></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h3>{{t('updates-title')}}</h3>
|
||||
<app-changelog></app-changelog>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {ServerService} from 'src/app/_services/server.service';
|
||||
import {ServerInfoSlim} from '../_models/server-info';
|
||||
import {DatePipe, NgIf} from '@angular/common';
|
||||
import {DatePipe} from '@angular/common';
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ChangelogComponent} from "../../announcements/_components/changelog/changelog.component";
|
||||
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-system',
|
||||
|
@ -13,7 +11,7 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
|||
styleUrls: ['./manage-system.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf, TranslocoDirective, ChangelogComponent, DefaultDatePipe, DefaultValuePipe, DatePipe]
|
||||
imports: [TranslocoDirective, ChangelogComponent, DatePipe]
|
||||
})
|
||||
export class ManageSystemComponent implements OnInit {
|
||||
|
||||
|
|
|
@ -1,194 +1,192 @@
|
|||
<ng-container *transloco="let t; read: 'manage-tasks-settings'">
|
||||
<div class="container-fluid">
|
||||
@if (serverSettings) {
|
||||
<form [formGroup]="settingsForm">
|
||||
@if (serverSettings) {
|
||||
<form [formGroup]="settingsForm">
|
||||
|
||||
<h4>{{t('title')}}</h4>
|
||||
<ng-container>
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if (settingsForm.get('taskScan'); as formControl) {
|
||||
<app-setting-item [title]="t('library-scan-label')" [subtitle]="t('library-scan-tooltip')">
|
||||
<ng-template #view>
|
||||
@if (formControl.value === customOption) {
|
||||
{{t(formControl.value)}} ({{settingsForm.get('taskScanCustom')?.value}})
|
||||
} @else {
|
||||
{{t(formControl.value)}}
|
||||
<h4>{{t('title')}}</h4>
|
||||
<ng-container>
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if (settingsForm.get('taskScan'); as formControl) {
|
||||
<app-setting-item [title]="t('library-scan-label')" [subtitle]="t('library-scan-tooltip')">
|
||||
<ng-template #view>
|
||||
@if (formControl.value === customOption) {
|
||||
{{t(formControl.value)}} ({{settingsForm.get('taskScanCustom')?.value}})
|
||||
} @else {
|
||||
{{t(formControl.value)}}
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
|
||||
<select class="form-select" aria-describedby="settings-tasks-scan-help" formControlName="taskScan" id="settings-tasks-scan">
|
||||
@for(freq of taskFrequencies; track freq) {
|
||||
<option [value]="freq">{{t(freq)}}</option>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
</select>
|
||||
|
||||
<select class="form-select" aria-describedby="settings-tasks-scan-help" formControlName="taskScan" id="settings-tasks-scan">
|
||||
@for(freq of taskFrequencies; track freq) {
|
||||
<option [value]="freq">{{t(freq)}}</option>
|
||||
@if (formControl.value === customOption) {
|
||||
<div class="mt-3">
|
||||
<label for="custom-task-scan" class="form-label">{{t('custom-label')}}</label>
|
||||
<input class="form-control" type="text"
|
||||
id="custom-task-scan" formControlName="taskScanCustom"
|
||||
[class.is-invalid]="settingsForm.get('taskScanCustom')?.invalid && settingsForm.get('taskScanCustom')?.touched"
|
||||
aria-describedby="task-scan-validations">
|
||||
|
||||
@if (settingsForm.dirty || !settingsForm.untouched) {
|
||||
<div id="task-scan-validations" class="invalid-feedback" style="display: inline-block">
|
||||
@if(settingsForm.get('taskScanCustom')?.errors?.required) {
|
||||
<div>{{t('required')}}</div>
|
||||
}
|
||||
@if(settingsForm.get('taskScanCustom')?.errors?.invalidCron) {
|
||||
<div>{{t('cron-notation')}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (formControl.value === customOption) {
|
||||
<div class="mt-3">
|
||||
<label for="custom-task-scan" class="form-label">{{t('custom-label')}}</label>
|
||||
<input class="form-control" type="text"
|
||||
id="custom-task-scan" formControlName="taskScanCustom"
|
||||
[class.is-invalid]="settingsForm.get('taskScanCustom')?.invalid && settingsForm.get('taskScanCustom')?.touched"
|
||||
aria-describedby="task-scan-validations">
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if (settingsForm.get('taskBackup'); as formControl) {
|
||||
<app-setting-item [title]="t('library-database-backup-label')" [subtitle]="t('library-database-backup-tooltip')">
|
||||
<ng-template #view>
|
||||
@if (formControl.value === customOption) {
|
||||
{{t(formControl.value)}} ({{settingsForm.get('taskBackupCustom')?.value}})
|
||||
} @else {
|
||||
{{t(formControl.value)}}
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
|
||||
@if (settingsForm.dirty || !settingsForm.untouched) {
|
||||
<div id="task-scan-validations" class="invalid-feedback" style="display: inline-block">
|
||||
@if(settingsForm.get('taskScanCustom')?.errors?.required) {
|
||||
<div>{{t('required')}}</div>
|
||||
}
|
||||
@if(settingsForm.get('taskScanCustom')?.errors?.invalidCron) {
|
||||
<div>{{t('cron-notation')}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<select class="form-select" aria-describedby="settings-tasks-backup-help" formControlName="taskBackup" id="settings-tasks-backup">
|
||||
@for(freq of taskFrequencies; track freq) {
|
||||
<option [value]="freq">{{t(freq)}}</option>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
</select>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if (settingsForm.get('taskBackup'); as formControl) {
|
||||
<app-setting-item [title]="t('library-database-backup-label')" [subtitle]="t('library-database-backup-tooltip')">
|
||||
<ng-template #view>
|
||||
@if (formControl.value === customOption) {
|
||||
{{t(formControl.value)}} ({{settingsForm.get('taskBackupCustom')?.value}})
|
||||
} @else {
|
||||
{{t(formControl.value)}}
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
@if (formControl.value === customOption) {
|
||||
<div class="mt-3">
|
||||
<label for="custom-task-scan" class="form-label">{{t('custom-label')}}</label>
|
||||
<input class="form-control" type="text"
|
||||
id="custom-task-backup" formControlName="taskBackupCustom"
|
||||
[class.is-invalid]="settingsForm.get('taskBackupCustom')?.invalid && settingsForm.get('taskBackupCustom')?.touched"
|
||||
aria-describedby="task-scan-validations">
|
||||
|
||||
<select class="form-select" aria-describedby="settings-tasks-backup-help" formControlName="taskBackup" id="settings-tasks-backup">
|
||||
@for(freq of taskFrequencies; track freq) {
|
||||
<option [value]="freq">{{t(freq)}}</option>
|
||||
@if (settingsForm.dirty || !settingsForm.untouched) {
|
||||
<div id="task-backup-validations" class="invalid-feedback" style="display: inline-block">
|
||||
@if(settingsForm.get('taskBackupCustom')?.errors?.required) {
|
||||
<div>{{t('required')}}</div>
|
||||
}
|
||||
@if(settingsForm.get('taskBackupCustom')?.errors?.invalidCron) {
|
||||
<div>{{t('cron-notation')}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (formControl.value === customOption) {
|
||||
<div class="mt-3">
|
||||
<label for="custom-task-scan" class="form-label">{{t('custom-label')}}</label>
|
||||
<input class="form-control" type="text"
|
||||
id="custom-task-backup" formControlName="taskBackupCustom"
|
||||
[class.is-invalid]="settingsForm.get('taskBackupCustom')?.invalid && settingsForm.get('taskBackupCustom')?.touched"
|
||||
aria-describedby="task-scan-validations">
|
||||
|
||||
@if (settingsForm.dirty || !settingsForm.untouched) {
|
||||
<div id="task-backup-validations" class="invalid-feedback" style="display: inline-block">
|
||||
@if(settingsForm.get('taskBackupCustom')?.errors?.required) {
|
||||
<div>{{t('required')}}</div>
|
||||
}
|
||||
@if(settingsForm.get('taskBackupCustom')?.errors?.invalidCron) {
|
||||
<div>{{t('cron-notation')}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if (settingsForm.get('taskCleanup'); as formControl) {
|
||||
<app-setting-item [title]="t('cleanup-label')" [subtitle]="t('cleanup-tooltip')">
|
||||
<ng-template #view>
|
||||
@if (formControl.value === customOption) {
|
||||
{{t(formControl.value)}} ({{settingsForm.get('taskCleanupCustom')?.value}})
|
||||
} @else {
|
||||
{{t(formControl.value)}}
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
|
||||
<select class="form-select" aria-describedby="settings-tasks-cleanup-help" formControlName="taskCleanup" id="settings-tasks-cleanup">
|
||||
@for(freq of taskFrequenciesForCleanup; track freq) {
|
||||
<option [value]="freq">{{t(freq)}}</option>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
</select>
|
||||
|
||||
@if (formControl.value === customOption) {
|
||||
<div class="mt-3">
|
||||
<label for="custom-task-scan" class="form-label">{{t('custom-label')}}</label>
|
||||
<input class="form-control" type="text"
|
||||
id="custom-task-cleanup" formControlName="taskCleanupCustom"
|
||||
[class.is-invalid]="settingsForm.get('taskCleanupCustom')?.invalid && settingsForm.get('taskCleanupCustom')?.touched"
|
||||
aria-describedby="task-scan-validations">
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if (settingsForm.get('taskCleanup'); as formControl) {
|
||||
<app-setting-item [title]="t('cleanup-label')" [subtitle]="t('cleanup-tooltip')">
|
||||
<ng-template #view>
|
||||
@if (formControl.value === customOption) {
|
||||
{{t(formControl.value)}} ({{settingsForm.get('taskCleanupCustom')?.value}})
|
||||
} @else {
|
||||
{{t(formControl.value)}}
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
|
||||
<select class="form-select" aria-describedby="settings-tasks-cleanup-help" formControlName="taskCleanup" id="settings-tasks-cleanup">
|
||||
@for(freq of taskFrequenciesForCleanup; track freq) {
|
||||
<option [value]="freq">{{t(freq)}}</option>
|
||||
@if (settingsForm.get('taskCleanupCustom')?.invalid) {
|
||||
<div id="task-cleanup-validations" class="invalid-feedback" style="display: inline-block">
|
||||
@if(settingsForm.get('taskCleanupCustom')?.errors?.required) {
|
||||
<div>{{t('required')}}</div>
|
||||
}
|
||||
@if(settingsForm.get('taskCleanupCustom')?.errors?.invalidCron) {
|
||||
<div>{{t('cron-notation')}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@if (formControl.value === customOption) {
|
||||
<div class="mt-3">
|
||||
<label for="custom-task-scan" class="form-label">{{t('custom-label')}}</label>
|
||||
<input class="form-control" type="text"
|
||||
id="custom-task-cleanup" formControlName="taskCleanupCustom"
|
||||
[class.is-invalid]="settingsForm.get('taskCleanupCustom')?.invalid && settingsForm.get('taskCleanupCustom')?.touched"
|
||||
aria-describedby="task-scan-validations">
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
@if (settingsForm.get('taskCleanupCustom')?.invalid) {
|
||||
<div id="task-cleanup-validations" class="invalid-feedback" style="display: inline-block">
|
||||
@if(settingsForm.get('taskCleanupCustom')?.errors?.required) {
|
||||
<div>{{t('required')}}</div>
|
||||
}
|
||||
@if(settingsForm.get('taskCleanupCustom')?.errors?.invalidCron) {
|
||||
<div>{{t('cron-notation')}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
<h4>{{t('adhoc-tasks-title')}}</h4>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
@for(task of adhocTasks; track task.name; let idx = $index) {
|
||||
<div class="mt-4 mb-4">
|
||||
<app-setting-button [subtitle]="t(task.description)">
|
||||
<button class="btn btn-secondary btn-sm mb-2" (click)="runAdhoc(task)">{{t(task.name)}}</button>
|
||||
</app-setting-button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h4>{{t('adhoc-tasks-title')}}</h4>
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
@for(task of adhocTasks; track task.name; let idx = $index) {
|
||||
<div class="mt-4 mb-4">
|
||||
<app-setting-button [subtitle]="t(task.description)">
|
||||
<button class="btn btn-secondary btn-sm mb-2" (click)="runAdhoc(task)">{{t(task.name)}}</button>
|
||||
</app-setting-button>
|
||||
</div>
|
||||
}
|
||||
<h4>{{t('recurring-tasks-title')}}</h4>
|
||||
<ngx-datatable
|
||||
class="bootstrap"
|
||||
[rows]="recurringTasks$ | async"
|
||||
[columnMode]="ColumnMode.flex"
|
||||
rowHeight="auto"
|
||||
[footerHeight]="50"
|
||||
[limit]="15"
|
||||
>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
<h4>{{t('recurring-tasks-title')}}</h4>
|
||||
<ngx-datatable
|
||||
class="bootstrap"
|
||||
[rows]="recurringTasks$ | async"
|
||||
[columnMode]="ColumnMode.flex"
|
||||
rowHeight="auto"
|
||||
[footerHeight]="50"
|
||||
[limit]="15"
|
||||
>
|
||||
|
||||
<ngx-datatable-column prop="title" [sortable]="true" [draggable]="false" [resizeable]="false" [flexGrow]="3">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('job-title-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" ngx-datatable-cell-template>
|
||||
{{item.title | titlecase}}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
<ngx-datatable-column prop="title" [sortable]="true" [draggable]="false" [resizeable]="false" [flexGrow]="3">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('job-title-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" ngx-datatable-cell-template>
|
||||
{{item.title | titlecase}}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
|
||||
|
||||
<ngx-datatable-column prop="lastExecutionUtc" [sortable]="true" [draggable]="false" [resizeable]="false" [flexGrow]="1">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('last-executed-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" let-idx="index" ngx-datatable-cell-template>
|
||||
{{item.lastExecutionUtc | utcToLocalTime | defaultValue }}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
<ngx-datatable-column prop="lastExecutionUtc" [sortable]="true" [draggable]="false" [resizeable]="false" [flexGrow]="1">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('last-executed-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" let-idx="index" ngx-datatable-cell-template>
|
||||
{{item.lastExecutionUtc | utcToLocalTime | defaultValue }}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
|
||||
<ngx-datatable-column prop="cron" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="1">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('cron-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" ngx-datatable-cell-template>
|
||||
{{item.cron}}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
</ngx-datatable>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
<ngx-datatable-column prop="cron" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="1">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('cron-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" ngx-datatable-cell-template>
|
||||
{{item.cron}}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
</ngx-datatable>
|
||||
</form>
|
||||
}
|
||||
</ng-container>
|
||||
|
|
|
@ -106,13 +106,6 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
api: defer(() => of(this.downloadService.download('logs', undefined))),
|
||||
successMessage: ''
|
||||
},
|
||||
// TODO: Remove this in v0.9. Users should have all updated by then
|
||||
{
|
||||
name: 'analyze-files-task',
|
||||
description: 'analyze-files-task-desc',
|
||||
api: this.serverService.analyzeFiles(),
|
||||
successMessage: 'analyze-files-task-success'
|
||||
},
|
||||
{
|
||||
name: 'sync-themes-task',
|
||||
description: 'sync-themes-task-desc',
|
||||
|
|
|
@ -1,52 +1,49 @@
|
|||
<ng-container *transloco="let t; read:'manage-user-tokens'">
|
||||
<div class="container-fluid">
|
||||
<p>{{t('description')}}</p>
|
||||
<p>{{t('description')}}</p>
|
||||
|
||||
|
||||
<ngx-datatable
|
||||
class="bootstrap"
|
||||
[rows]="users"
|
||||
[columnMode]="ColumnMode.force"
|
||||
rowHeight="auto"
|
||||
[footerHeight]="50"
|
||||
>
|
||||
<ngx-datatable
|
||||
class="bootstrap"
|
||||
[rows]="users"
|
||||
[columnMode]="ColumnMode.force"
|
||||
rowHeight="auto"
|
||||
[footerHeight]="50"
|
||||
>
|
||||
|
||||
<ngx-datatable-column prop="username" [sortable]="true" [draggable]="false" [resizeable]="false">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('username-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" ngx-datatable-cell-template>
|
||||
{{item.username}}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
<ngx-datatable-column prop="username" [sortable]="true" [draggable]="false" [resizeable]="false">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('username-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" ngx-datatable-cell-template>
|
||||
{{item.username}}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
|
||||
|
||||
<ngx-datatable-column prop="aniListValidUntilUtc" [sortable]="false" [draggable]="false" [resizeable]="false">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('anilist-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" let-idx="index" ngx-datatable-cell-template>
|
||||
@if (item.isAniListTokenSet) {
|
||||
{{t('token-set-label')}} <span class="text-muted ms-1">{{t('expires-label', {date: item.aniListValidUntilUtc | utcToLocalTime})}}</span>
|
||||
} @else {
|
||||
{{null | defaultValue}}
|
||||
}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
<ngx-datatable-column prop="aniListValidUntilUtc" [sortable]="false" [draggable]="false" [resizeable]="false">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('anilist-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" let-idx="index" ngx-datatable-cell-template>
|
||||
@if (item.isAniListTokenSet) {
|
||||
{{t('token-set-label')}} <span class="text-muted ms-1">{{t('expires-label', {date: item.aniListValidUntilUtc | utcToLocalTime})}}</span>
|
||||
} @else {
|
||||
{{null | defaultValue}}
|
||||
}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
|
||||
<ngx-datatable-column prop="validUntilUtc" [sortable]="false" [draggable]="false" [resizeable]="false">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('mal-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" ngx-datatable-cell-template>
|
||||
@if (item.isMalTokenSet) {
|
||||
{{t('token-set-label')}}
|
||||
} @else {
|
||||
{{null | defaultValue}}
|
||||
}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
</ngx-datatable>
|
||||
|
||||
</div>
|
||||
<ngx-datatable-column prop="validUntilUtc" [sortable]="false" [draggable]="false" [resizeable]="false">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('mal-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" ngx-datatable-cell-template>
|
||||
@if (item.isMalTokenSet) {
|
||||
{{t('token-set-label')}}
|
||||
} @else {
|
||||
{{null | defaultValue}}
|
||||
}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
</ngx-datatable>
|
||||
</ng-container>
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
NgbTooltip
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {forkJoin, Observable, of, tap} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from 'src/app/typeahead/_models/typeahead-settings';
|
||||
import {Chapter, LooseLeafOrDefaultNumber, SpecialVolumeNumber} from 'src/app/_models/chapter';
|
||||
|
@ -238,10 +238,7 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.metadataService.getAllValidLanguages().subscribe(validLanguages => {
|
||||
this.validLanguages = validLanguages;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
this.seriesService.getMetadata(this.series.id).subscribe(metadata => {
|
||||
if (metadata) {
|
||||
|
@ -437,30 +434,41 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
}
|
||||
|
||||
setupLanguageTypeahead() {
|
||||
this.languageSettings.minCharacters = 0;
|
||||
this.languageSettings.multiple = false;
|
||||
this.languageSettings.id = 'language';
|
||||
this.languageSettings.unique = true;
|
||||
this.languageSettings.showLocked = true;
|
||||
this.languageSettings.addIfNonExisting = false;
|
||||
this.languageSettings.compareFn = (options: Language[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.languageSettings.compareFnForAdd = (options: Language[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages)
|
||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||
|
||||
this.languageSettings.selectionCompareFn = (a: Language, b: Language) => {
|
||||
return a.isoCode == b.isoCode;
|
||||
}
|
||||
|
||||
const l = this.validLanguages.find(l => l.isoCode === this.metadata.language);
|
||||
if (l !== undefined) {
|
||||
this.languageSettings.savedData = l;
|
||||
}
|
||||
return of(true);
|
||||
return this.metadataService.getAllValidLanguages()
|
||||
.pipe(
|
||||
tap(validLanguages => {
|
||||
this.validLanguages = validLanguages;
|
||||
|
||||
this.languageSettings.minCharacters = 0;
|
||||
this.languageSettings.multiple = false;
|
||||
this.languageSettings.id = 'language';
|
||||
this.languageSettings.unique = true;
|
||||
this.languageSettings.showLocked = true;
|
||||
this.languageSettings.addIfNonExisting = false;
|
||||
this.languageSettings.compareFn = (options: Language[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.languageSettings.compareFnForAdd = (options: Language[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages)
|
||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||
|
||||
this.languageSettings.selectionCompareFn = (a: Language, b: Language) => {
|
||||
return a.isoCode == b.isoCode;
|
||||
}
|
||||
|
||||
const l = this.validLanguages.find(l => l.isoCode === this.metadata.language);
|
||||
if (l !== undefined) {
|
||||
this.languageSettings.savedData = l;
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}),
|
||||
switchMap(_ => of(true))
|
||||
);
|
||||
}
|
||||
|
||||
setupPersonTypeahead() {
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
<ng-container *transloco="let t; read: 'import-mal-collection-modal'">
|
||||
<p>{{t('description')}}</p>
|
||||
|
||||
@if (stacks.length === 0) {
|
||||
<p>{{t('nothing-found')}}</p>
|
||||
}
|
||||
|
||||
<ul>
|
||||
@for(stack of stacks; track stack.url) {
|
||||
<li class="mb-2">
|
||||
|
@ -21,6 +17,8 @@
|
|||
} @empty {
|
||||
@if (isLoading) {
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
} @else {
|
||||
<p>{{t('nothing-found')}}</p>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
|
|
|
@ -22,6 +22,7 @@ import { MangaReaderService } from '../../_service/manga-reader.service';
|
|||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { SafeStylePipe } from '../../../_pipes/safe-style.pipe';
|
||||
import { NgClass, AsyncPipe } from '@angular/common';
|
||||
import {isSafari} from "../../../_helpers/browser";
|
||||
|
||||
const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRightToLeft];
|
||||
|
||||
|
@ -35,13 +36,21 @@ const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRigh
|
|||
})
|
||||
export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRenderer {
|
||||
|
||||
protected readonly isSafari = isSafari;
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly mangaReaderService = inject(MangaReaderService);
|
||||
private readonly readerService = inject(ReaderService);
|
||||
|
||||
|
||||
@Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
|
||||
@Input({required: true}) image$!: Observable<HTMLImageElement | null>;
|
||||
@Input({required: true}) bookmark$!: Observable<number>;
|
||||
@Input({required: true}) showClickOverlay$!: Observable<boolean>;
|
||||
@Input() imageFit$!: Observable<FITTING_OPTION>;
|
||||
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
|
||||
@ViewChild('content') canvas: ElementRef | undefined;
|
||||
private ctx!: CanvasRenderingContext2D;
|
||||
|
@ -67,7 +76,6 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRend
|
|||
|
||||
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, private mangaReaderService: MangaReaderService, private readerService: ReaderService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.readerSettings$.pipe(takeUntilDestroyed(this.destroyRef), tap((value: ReaderSetting) => {
|
||||
|
@ -250,21 +258,11 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRend
|
|||
setCanvasSize() {
|
||||
if (this.canvasImage == null) return;
|
||||
if (!this.ctx || !this.canvas) { return; }
|
||||
const isSafari = [
|
||||
'iPad Simulator',
|
||||
'iPhone Simulator',
|
||||
'iPod Simulator',
|
||||
'iPad',
|
||||
'iPhone',
|
||||
'iPod'
|
||||
].includes(navigator.platform)
|
||||
// iPad on iOS 13 detection
|
||||
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document);
|
||||
const canvasLimit = isSafari ? 16_777_216 : 124_992_400;
|
||||
const canvasLimit = this.isSafari ? 16_777_216 : 124_992_400;
|
||||
const needsScaling = this.canvasImage.width * this.canvasImage.height > canvasLimit;
|
||||
if (needsScaling) {
|
||||
this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384;
|
||||
this.canvas.nativeElement.height = isSafari ? 4_096 : 16_384;
|
||||
this.canvas.nativeElement.width = this.isSafari ? 4_096 : 16_384;
|
||||
this.canvas.nativeElement.height = this.isSafari ? 4_096 : 16_384;
|
||||
} else {
|
||||
this.canvas.nativeElement.width = this.canvasImage.width;
|
||||
this.canvas.nativeElement.height = this.canvasImage.height;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<ng-container *transloco="let t; read: 'metadata-filter'">
|
||||
<ng-container *ngIf="toggleService.toggleState$ | async as isOpen">
|
||||
<ng-container *ngIf="utilityService.getActiveBreakpoint() as activeBreakpoint">
|
||||
<div *ngIf="activeBreakpoint >= Breakpoint.Tablet; else mobileView" [@inOutAnimation]>
|
||||
<ng-container [ngTemplateOutlet]="filterSection" [ngTemplateOutletContext]="{isOpen: isOpen}"></ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #mobileView>
|
||||
@if (toggleService.toggleState$ | async; as isOpen) {
|
||||
@if (utilityService.getActiveBreakpoint(); as activeBreakpoint) {
|
||||
@if (activeBreakpoint >= Breakpoint.Tablet) {
|
||||
<div [@inOutAnimation]>
|
||||
<ng-container [ngTemplateOutlet]="filterSection" [ngTemplateOutletContext]="{isOpen: isOpen}"></ng-container>
|
||||
</div>
|
||||
} @else {
|
||||
<div>
|
||||
<app-drawer #commentDrawer="drawer" [isOpen]="isOpen" [options]="{topOffset: 75}" (drawerClosed)="toggleService.set(false)" [width]="600">
|
||||
<h5 header>
|
||||
|
@ -17,60 +17,62 @@
|
|||
</div>
|
||||
</app-drawer>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<ng-template #filterSection let-isOpen="isOpen">
|
||||
<div class="filter-section mx-auto pb-3" *ngIf="fullyLoaded && filterV2">
|
||||
<div class="row justify-content-center g-0">
|
||||
<app-metadata-builder [filter]="filterV2"
|
||||
[availableFilterFields]="allFilterFields"
|
||||
[statementLimit]="filterSettings.statementLimit"
|
||||
(update)="handleFilters($event)">
|
||||
</app-metadata-builder>
|
||||
</div>
|
||||
<form [formGroup]="sortGroup" class="container-fluid">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-sm-3">
|
||||
<div class="form-group pe-1">
|
||||
<label for="limit-to" class="form-label">{{t('limit-label')}}</label>
|
||||
<input id="limit-to" type="number" inputmode="numeric" class="form-control" formControlName="limitTo">
|
||||
@if (fullyLoaded && filterV2) {
|
||||
<div class="filter-section mx-auto pb-3">
|
||||
<div class="row justify-content-center g-0">
|
||||
<app-metadata-builder [filter]="filterV2"
|
||||
[availableFilterFields]="allFilterFields"
|
||||
[statementLimit]="filterSettings.statementLimit"
|
||||
(update)="handleFilters($event)">
|
||||
</app-metadata-builder>
|
||||
</div>
|
||||
<form [formGroup]="sortGroup" class="container-fluid">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-sm-3">
|
||||
<div class="form-group pe-1">
|
||||
<label for="limit-to" class="form-label">{{t('limit-label')}}</label>
|
||||
<input id="limit-to" type="number" inputmode="numeric" class="form-control" formControlName="limitTo">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-9">
|
||||
<div class="col-md-3 col-sm-9">
|
||||
<label for="sort-options" class="form-label">{{t('sort-by-label')}}</label>
|
||||
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0;" [disabled]="filterSettings.sortDisabled">
|
||||
<i class="fa fa-arrow-up" [title]="t('ascending-alt')" *ngIf="isAscendingSort; else descSort"></i>
|
||||
<ng-template #descSort>
|
||||
@if (isAscendingSort) {
|
||||
<i class="fa fa-arrow-up" [title]="t('ascending-alt')"></i>
|
||||
} @else {
|
||||
<i class="fa fa-arrow-down" [title]="t('descending-alt')"></i>
|
||||
</ng-template>
|
||||
}
|
||||
</button>
|
||||
<select id="sort-options" class="form-select" formControlName="sortField" style="height: 38px;">
|
||||
<option *ngFor="let field of allSortFields" [value]="field">{{field | sortField}}</option>
|
||||
@for(field of allSortFields; track field.value) {
|
||||
<option [value]="field.value">{{field.title}}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12" [ngClass]="{'mt-3': utilityService.getActiveBreakpoint() <= Breakpoint.Mobile}">
|
||||
<label for="filter-name" class="form-label">{{t('filter-name-label')}}</label>
|
||||
<input id="filter-name" type="text" class="form-control" formControlName="name">
|
||||
<!-- <select2 [data]="smartFilters"-->
|
||||
<!-- id="filter-name"-->
|
||||
<!-- formControlName="name"-->
|
||||
<!-- (update)="loadSavedFilter($event)"-->
|
||||
<!-- (autoCreateItem)="createFilterValue($event)"-->
|
||||
<!-- [autoCreate]="true"-->
|
||||
<!-- displaySearchStatus="always"-->
|
||||
<!-- [resettable]="true">-->
|
||||
<!-- </select2>-->
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12" [ngClass]="{'mt-3': utilityService.getActiveBreakpoint() <= Breakpoint.Mobile}">
|
||||
<label for="filter-name" class="form-label">{{t('filter-name-label')}}</label>
|
||||
<input id="filter-name" type="text" class="form-control" formControlName="name">
|
||||
</div>
|
||||
|
||||
@if (utilityService.getActiveBreakpoint() > Breakpoint.Tablet) {
|
||||
<ng-container [ngTemplateOutlet]="buttons"></ng-container>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="utilityService.getActiveBreakpoint() > Breakpoint.Tablet" [ngTemplateOutlet]="buttons"></ng-container>
|
||||
</div>
|
||||
<div class="row mb-3" *ngIf="utilityService.getActiveBreakpoint() <= Breakpoint.Tablet">
|
||||
<ng-container [ngTemplateOutlet]="buttons"></ng-container>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@if (utilityService.getActiveBreakpoint() <= Breakpoint.Tablet) {
|
||||
<div class="row mb-3">
|
||||
<ng-container [ngTemplateOutlet]="buttons"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #buttons>
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
Output
|
||||
} from '@angular/core';
|
||||
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {NgbCollapse, NgbModal, NgbRating, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {NgbCollapse} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {Breakpoint, UtilityService} from '../shared/_services/utility.service';
|
||||
import {Library} from '../_models/library/library';
|
||||
import {allSortFields, FilterEvent, FilterItem, SortField} from '../_models/metadata/series-filter';
|
||||
|
@ -19,28 +19,15 @@ import {ToggleService} from '../_services/toggle.service';
|
|||
import {FilterSettings} from './filter-settings';
|
||||
import {SeriesFilterV2} from '../_models/metadata/v2/series-filter-v2';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {TypeaheadComponent} from '../typeahead/_components/typeahead.component';
|
||||
import {DrawerComponent} from '../shared/drawer/drawer.component';
|
||||
import {AsyncPipe, NgClass, NgForOf, NgIf, NgTemplateOutlet} from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@jsverse/transloco";
|
||||
import {AsyncPipe, NgClass, NgTemplateOutlet} from '@angular/common';
|
||||
import {translate, TranslocoModule, TranslocoService} from "@jsverse/transloco";
|
||||
import {SortFieldPipe} from "../_pipes/sort-field.pipe";
|
||||
import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component";
|
||||
import {allFields} from "../_models/metadata/v2/filter-field";
|
||||
import {MetadataService} from "../_services/metadata.service";
|
||||
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
|
||||
import {FilterService} from "../_services/filter.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {
|
||||
Select2AutoCreateEvent,
|
||||
Select2Module,
|
||||
Select2Option,
|
||||
Select2UpdateEvent,
|
||||
Select2UpdateValue
|
||||
} from "ng-select2-component";
|
||||
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
||||
import {animate, state, style, transition, trigger} from "@angular/animations";
|
||||
|
||||
const ANIMATION_SPEED = 750;
|
||||
import {animate, style, transition, trigger} from "@angular/animations";
|
||||
|
||||
@Component({
|
||||
selector: 'app-metadata-filter',
|
||||
|
@ -71,65 +58,53 @@ const ANIMATION_SPEED = 750;
|
|||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgbCollapse, NgTemplateOutlet, DrawerComponent, NgbTooltip, TypeaheadComponent,
|
||||
ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe,
|
||||
MetadataBuilderComponent, NgForOf, Select2Module, NgClass]
|
||||
imports: [NgTemplateOutlet, DrawerComponent,
|
||||
ReactiveFormsModule, FormsModule, AsyncPipe, TranslocoModule,
|
||||
MetadataBuilderComponent, NgClass]
|
||||
})
|
||||
export class MetadataFilterComponent implements OnInit {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly filterService = inject(FilterService);
|
||||
protected readonly toggleService = inject(ToggleService);
|
||||
protected readonly translocoService = inject(TranslocoService);
|
||||
private readonly sortFieldPipe = new SortFieldPipe(this.translocoService);
|
||||
|
||||
/**
|
||||
* This toggles the opening/collapsing of the metadata filter code
|
||||
*/
|
||||
@Input() filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Should filtering be shown on the page
|
||||
*/
|
||||
@Input() filteringDisabled: boolean = false;
|
||||
|
||||
@Input({required: true}) filterSettings!: FilterSettings;
|
||||
|
||||
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
|
||||
|
||||
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
public readonly filterUtilitiesService = inject(FilterUtilitiesService);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Controls the visibility of extended controls that sit below the main header.
|
||||
*/
|
||||
filteringCollapsed: boolean = true;
|
||||
|
||||
libraries: Array<FilterItem<Library>> = [];
|
||||
|
||||
sortGroup!: FormGroup;
|
||||
isAscendingSort: boolean = true;
|
||||
|
||||
updateApplied: number = 0;
|
||||
|
||||
fullyLoaded: boolean = false;
|
||||
filterV2: SeriesFilterV2 | undefined;
|
||||
allSortFields = allSortFields;
|
||||
allFilterFields = allFields;
|
||||
|
||||
smartFilters!: Array<Select2Option>;
|
||||
protected readonly allSortFields = allSortFields.map(f => {
|
||||
return {title: this.sortFieldPipe.transform(f), value: f};
|
||||
}).sort((a, b) => a.title.localeCompare(b.title));
|
||||
protected readonly allFilterFields = allFields;
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
|
||||
|
||||
constructor(public toggleService: ToggleService, private filterService: FilterService) {
|
||||
this.filterService.getAllFilters().subscribe(res => {
|
||||
this.smartFilters = res.map(r => {
|
||||
return {
|
||||
value: r,
|
||||
label: r.name,
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.filterSettings === undefined) {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<app-cover-image [entity]="series" [coverImage]="imageService.getSeriesCoverImage(series.id)" [continueTitle]="ContinuePointTitle" (read)="read()"></app-cover-image>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12">
|
||||
<div class="col-xl-10 col-lg-7 col-md-12 col-sm-12 col-xs-12">
|
||||
<h4 class="title mb-2">
|
||||
<span>{{series.name}}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
<div class="col-auto text-end align-self-end justify-content-end edit-btn">
|
||||
@if (showEdit) {
|
||||
<button type="button" class="btn btn-text btn-sm" (click)="toggleEditMode()" [disabled]="!canEdit">
|
||||
<button type="button" class="btn btn-text btn-sm btn-alignment" (click)="toggleEditMode()" [disabled]="!canEdit">
|
||||
{{isEditMode ? t('common.close') : (editLabel || t('common.edit'))}}
|
||||
</button>
|
||||
}
|
||||
|
@ -28,7 +28,7 @@
|
|||
@if (isEditMode) {
|
||||
<ng-container [ngTemplateOutlet]="valueEditRef"></ng-container>
|
||||
} @else {
|
||||
<span class="view-value" (click)="toggleEditMode()">
|
||||
<span class="view-value" [ngClass]="{'non-selectable': (!canEdit || !showEdit)}" (click)="toggleEditMode()">
|
||||
<ng-container [ngTemplateOutlet]="valueViewRef"></ng-container>
|
||||
</span>
|
||||
}
|
||||
|
|
|
@ -12,23 +12,11 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
//.setting-title.no-anim.edit:hover ~ .edit-btn {
|
||||
// opacity: 1;
|
||||
// transition: none;
|
||||
//}
|
||||
//
|
||||
//.edit-btn {
|
||||
// opacity: 0;
|
||||
// transition: opacity 0.5s ease-out;
|
||||
// transition-delay: 0.5s;
|
||||
//
|
||||
// &:hover {
|
||||
// opacity: 1;
|
||||
// transition: opacity 0.3s ease-out;
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//.setting-title.no-anim + .edit-btn,
|
||||
//.setting-title.no-anim.edit:hover ~ .edit-btn {
|
||||
// transition: none !important;
|
||||
//}
|
||||
.non-selectable {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn-alignment {
|
||||
padding-bottom: 0.5rem; // Align with h6
|
||||
padding-top: 0;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {NgTemplateOutlet} from "@angular/common";
|
||||
import {NgClass, NgTemplateOutlet} from "@angular/common";
|
||||
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
|
||||
import {filter, fromEvent, tap} from "rxjs";
|
||||
import {AbstractControl} from "@angular/forms";
|
||||
|
@ -19,7 +19,8 @@ import {AbstractControl} from "@angular/forms";
|
|||
imports: [
|
||||
TranslocoDirective,
|
||||
NgTemplateOutlet,
|
||||
SafeHtmlPipe
|
||||
SafeHtmlPipe,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './setting-item.component.html',
|
||||
styleUrl: './setting-item.component.scss',
|
||||
|
|
|
@ -7,7 +7,13 @@
|
|||
}
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<h6 class="section-title" [id]="id || title">{{title}}</h6>
|
||||
<h6 class="section-title" [id]="id || title">
|
||||
@if (labelId) {
|
||||
<label class="reset-label" [for]="labelId">{{title}}</label>
|
||||
} @else {
|
||||
{{title}}
|
||||
}
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, ContentChild,
|
||||
Component, ContentChild, ElementRef,
|
||||
inject,
|
||||
Input,
|
||||
TemplateRef
|
||||
|
@ -22,12 +23,39 @@ import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
|
|||
styleUrl: './setting-switch.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SettingSwitchComponent {
|
||||
export class SettingSwitchComponent implements AfterContentInit {
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly elementRef = inject(ElementRef);
|
||||
|
||||
@Input({required:true}) title: string = '';
|
||||
@Input() subtitle: string | undefined = undefined;
|
||||
@Input() id: string | undefined = undefined;
|
||||
@ContentChild('switch') switchRef!: TemplateRef<any>;
|
||||
|
||||
/**
|
||||
* For wiring up with a real label
|
||||
*/
|
||||
labelId: string = '';
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
setTimeout(() => {
|
||||
if (this.id) {
|
||||
this.labelId = this.id;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
const element = this.elementRef.nativeElement;
|
||||
const inputElement = element.querySelector('input');
|
||||
|
||||
if (inputElement && inputElement.id) {
|
||||
this.labelId = inputElement.id;
|
||||
this.cdRef.markForCheck();
|
||||
} else {
|
||||
console.warn('No input with ID found in app-setting-switch. For accessibility, please ensure the input has an ID.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
<ng-container *transloco="let t;">
|
||||
<div class="container-fluid">
|
||||
<div class="settings-row g-0 row">
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-10">
|
||||
<h6 class="section-title" [id]="id || title">{{title}}
|
||||
<div class="col-auto">
|
||||
<h6 class="section-title" [id]="id || title">
|
||||
@if (labelId) {
|
||||
<label class="reset-label" [for]="labelId">{{title}}</label>
|
||||
} @else {
|
||||
{{title}}
|
||||
}
|
||||
@if (titleExtraRef) {
|
||||
<ng-container [ngTemplateOutlet]="titleExtraRef"></ng-container>
|
||||
}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-2 text-end align-self-end justify-content-end">
|
||||
<button type="button" class="btn btn-text btn-sm" (click)="toggleViewMode()" [disabled]="!canEdit">{{isEditMode ? t('common.close') : t('common.edit')}}</button>
|
||||
|
||||
<div class="col-auto text-end align-self-end justify-content-end edit-btn">
|
||||
<button type="button" class="btn btn-text btn-sm btn-alignment" (click)="toggleViewMode()" [disabled]="!canEdit">{{isEditMode ? t('common.close') : t('common.edit')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.btn-alignment {
|
||||
padding-bottom: 0.5rem; // Align with h6
|
||||
padding-top: 0;
|
||||
}
|
|
@ -26,6 +26,10 @@ export class SettingTitleComponent {
|
|||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
@Input({required:true}) title: string = '';
|
||||
/**
|
||||
* If passed, will generate a proper label element. Requires `id` to be passed as well
|
||||
*/
|
||||
@Input() labelId: string | undefined = undefined;
|
||||
@Input() id: string | undefined = undefined;
|
||||
@Input() canEdit: boolean = true;
|
||||
@Input() isEditMode: boolean = false;
|
||||
|
|
|
@ -28,6 +28,10 @@ export class BadgeExpanderComponent implements OnInit, OnChanges {
|
|||
@Input() itemsTillExpander: number = 4;
|
||||
@Input() allowToggle: boolean = true;
|
||||
@Input() includeComma: boolean = true;
|
||||
/**
|
||||
* If should be expanded by default. Defaults to false.
|
||||
*/
|
||||
@Input() defaultExpanded: boolean = false;
|
||||
/**
|
||||
* Invoked when the "and more" is clicked
|
||||
*/
|
||||
|
@ -39,10 +43,20 @@ export class BadgeExpanderComponent implements OnInit, OnChanges {
|
|||
isCollapsed: boolean = false;
|
||||
|
||||
get itemsLeft() {
|
||||
if (this.defaultExpanded) return 0;
|
||||
|
||||
return Math.max(this.items.length - this.itemsTillExpander, 0);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
if (this.defaultExpanded) {
|
||||
this.isCollapsed = false;
|
||||
this.visibleItems = this.items;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
this.visibleItems = this.items.slice(0, this.itemsTillExpander);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
<div class="mb-3">
|
||||
<label for="library-name" class="form-label">{{t('name-label')}}</label>
|
||||
@if (libraryForm.get('name'); as formControl) {
|
||||
<input id="library-name" class="form-control" formControlName="name" type="text" [class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
@if (libraryForm.dirty || libraryForm.touched) {
|
||||
<input id="library-name" class="form-control" formControlName="name" type="text" [class.is-invalid]="formControl.invalid && !formControl.untouched">
|
||||
@if (libraryForm.dirty || !libraryForm.untouched) {
|
||||
<div id="inviteForm-validations" class="invalid-feedback">
|
||||
@if (formControl.errors?.required) {
|
||||
<div>{{t('required-field')}}</div>
|
||||
|
@ -52,8 +52,8 @@
|
|||
<ng-container [ngTemplateOutlet]="typeTooltip"></ng-container>
|
||||
</span>
|
||||
<select class="form-select" id="library-type" formControlName="type" aria-describedby="library-type-help">
|
||||
@for (opt of libraryTypes; track opt; let i = $index) {
|
||||
<option [value]="i">{{opt}}</option>
|
||||
@for (opt of libraryTypes; track opt.value) {
|
||||
<option [value]="opt.value">{{opt.title}}</option>
|
||||
}
|
||||
|
||||
</select>
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
} from 'src/app/admin/_modals/directory-picker/directory-picker.component';
|
||||
import {ConfirmService} from 'src/app/shared/confirm.service';
|
||||
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {Library, LibraryType} from 'src/app/_models/library/library';
|
||||
import {allLibraryTypes, Library, LibraryType} from 'src/app/_models/library/library';
|
||||
import {ImageService} from 'src/app/_services/image.service';
|
||||
import {LibraryService} from 'src/app/_services/library.service';
|
||||
import {UploadService} from 'src/app/_services/upload.service';
|
||||
|
@ -47,6 +47,7 @@ import {SettingSwitchComponent} from "../../../settings/_components/setting-swit
|
|||
import {SettingButtonComponent} from "../../../settings/_components/setting-button/setting-button.component";
|
||||
import {Action, ActionFactoryService, ActionItem} from "../../../_services/action-factory.service";
|
||||
import {ActionService} from "../../../_services/action.service";
|
||||
import {LibraryTypePipe} from "../../../_pipes/library-type.pipe";
|
||||
|
||||
enum TabID {
|
||||
General = 'general-tab',
|
||||
|
@ -68,7 +69,7 @@ enum StepID {
|
|||
standalone: true,
|
||||
imports: [CommonModule, NgbModalModule, NgbNavLink, NgbNavItem, NgbNavContent, ReactiveFormsModule, NgbTooltip,
|
||||
SentenceCasePipe, NgbNav, NgbNavOutlet, CoverImageChooserComponent, TranslocoModule, DefaultDatePipe,
|
||||
FileTypeGroupPipe, EditListComponent, SettingItemComponent, SettingSwitchComponent, SettingButtonComponent],
|
||||
FileTypeGroupPipe, EditListComponent, SettingItemComponent, SettingSwitchComponent, SettingButtonComponent, LibraryTypePipe],
|
||||
templateUrl: './library-settings-modal.component.html',
|
||||
styleUrls: ['./library-settings-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
@ -94,6 +95,7 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
protected readonly TabID = TabID;
|
||||
protected readonly WikiLink = WikiLink;
|
||||
protected readonly Action = Action;
|
||||
protected readonly libraryTypePipe = new LibraryTypePipe();
|
||||
|
||||
@Input({required: true}) library!: Library | undefined;
|
||||
|
||||
|
@ -119,7 +121,9 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
|
||||
selectedFolders: string[] = [];
|
||||
madeChanges = false;
|
||||
libraryTypes: string[] = []
|
||||
libraryTypes = allLibraryTypes.map(f => {
|
||||
return {title: this.libraryTypePipe.transform(f), value: f};
|
||||
}).sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
isAddLibrary = false;
|
||||
setupStep = StepID.General;
|
||||
|
@ -134,11 +138,6 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingService.getLibraryTypes().subscribe((types) => {
|
||||
this.libraryTypes = types;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
if (this.library === undefined) {
|
||||
this.isAddLibrary = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
@ -1,117 +1,113 @@
|
|||
<ng-container *transloco="let t; read:'server-stats'">
|
||||
<div class="container-fluid">
|
||||
<div class="row g-0 mt-4 mb-3 d-flex justify-content-around" *ngIf="stats$ | async as stats">
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-series-label')" [clickable]="false" fontClasses="fa-solid fa-book-open" [title]="t('total-series-tooltip', {count: stats.seriesCount | number})">
|
||||
{{t('series-count', {num: stats.seriesCount | number})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container >
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-volumes-label')" [clickable]="false" fontClasses="fas fa-book" [title]="t('total-volumes-tooltip', {count: stats.volumeCount | number})">
|
||||
{{t('volume-count', {num: stats.volumeCount | number})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-files-label')" [clickable]="false" fontClasses="fa-regular fa-file" [title]="t('total-files-tooltip', {count: stats.totalFiles | number})">
|
||||
{{t('file-count', {num: stats.totalFiles | number})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-size-label')" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" [title]="t('total-size-label')">
|
||||
{{stats.totalSize | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-genres-label')" [clickable]="true" fontClasses="fa-solid fa-tags" [title]="t('total-genres-tooltip', {count: stats.totalGenres | number})" (click)="openGenreList();$event.stopPropagation();">
|
||||
{{t('genre-count', {num: stats.totalGenres | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-tags-label')" [clickable]="true" fontClasses="fa-solid fa-tags" [title]="t('total-tags-tooltip', {count: stats.totalTags | number})" (click)="openTagList();$event.stopPropagation();">
|
||||
{{t('tag-count', {num: stats.totalTags | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-people-label')" [clickable]="true" fontClasses="fa-solid fa-user-tag" [title]="t('total-people-tooltip', {count: stats.totalPeople | number})" (click)="openPeopleList();$event.stopPropagation();">
|
||||
{{t('people-count', {num: stats.totalPeople | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-read-time-label')" [clickable]="false" fontClasses="fas fa-eye" [title]="t('total-read-time-tooltip', {count: stats.totalReadingTime | number})">
|
||||
{{stats.totalReadingTime | timeDuration}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="grid row g-0 pt-2 pb-2 d-flex justify-content-around">
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="releaseYears$" [title]="t('release-years-title')" [label]="t('series')"></app-stat-list>
|
||||
<div class="row g-0 mt-4 mb-3 d-flex justify-content-around" *ngIf="stats$ | async as stats">
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-series-label')" [clickable]="false" fontClasses="fa-solid fa-book-open" [title]="t('total-series-tooltip', {count: stats.seriesCount | number})">
|
||||
{{t('series-count', {num: stats.seriesCount | number})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveUsers$" [title]="t('most-active-users-title')" [label]="t('reads')"></app-stat-list>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container >
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-volumes-label')" [clickable]="false" fontClasses="fas fa-book" [title]="t('total-volumes-tooltip', {count: stats.volumeCount | number})">
|
||||
{{t('volume-count', {num: stats.volumeCount | number})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveLibrary$" [title]="t('popular-libraries-title')" [label]="t('reads')"></app-stat-list>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-files-label')" [clickable]="false" fontClasses="fa-regular fa-file" [title]="t('total-files-tooltip', {count: stats.totalFiles | number})">
|
||||
{{t('file-count', {num: stats.totalFiles | number})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveSeries$" [title]="t('popular-series-title')" [image]="seriesImage" [handleClick]="openSeries">
|
||||
</app-stat-list>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-size-label')" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" [title]="t('total-size-label')">
|
||||
{{stats.totalSize | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="recentlyRead$" [title]="t('recently-read-title')" [image]="seriesImage" [handleClick]="openSeries"></app-stat-list>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-genres-label')" [clickable]="true" fontClasses="fa-solid fa-tags" [title]="t('total-genres-tooltip', {count: stats.totalGenres | number})" (click)="openGenreList();$event.stopPropagation();">
|
||||
{{t('genre-count', {num: stats.totalGenres | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<div class="row g-0 pt-2 pb-2">
|
||||
<app-top-readers></app-top-readers>
|
||||
</div>
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-tags-label')" [clickable]="true" fontClasses="fa-solid fa-tags" [title]="t('total-tags-tooltip', {count: stats.totalTags | number})" (click)="openTagList();$event.stopPropagation();">
|
||||
{{t('tag-count', {num: stats.totalTags | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<app-file-breakdown-stats></app-file-breakdown-stats>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<app-publication-status-stats></app-publication-status-stats>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<app-reading-activity [isAdmin]="true"></app-reading-activity>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<app-day-breakdown></app-day-breakdown>
|
||||
</div>
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-people-label')" [clickable]="true" fontClasses="fa-solid fa-user-tag" [title]="t('total-people-tooltip', {count: stats.totalPeople | number})" (click)="openPeopleList();$event.stopPropagation();">
|
||||
{{t('people-count', {num: stats.totalPeople | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-read-time-label')" [clickable]="false" fontClasses="fas fa-eye" [title]="t('total-read-time-tooltip', {count: stats.totalReadingTime | number})">
|
||||
{{stats.totalReadingTime | timeDuration}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="grid row g-0 pt-2 pb-2 d-flex justify-content-around">
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="releaseYears$" [title]="t('release-years-title')" [label]="t('series')"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveUsers$" [title]="t('most-active-users-title')" [label]="t('reads')"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveLibrary$" [title]="t('popular-libraries-title')" [label]="t('reads')"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveSeries$" [title]="t('popular-series-title')" [image]="seriesImage" [handleClick]="openSeries">
|
||||
</app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="recentlyRead$" [title]="t('recently-read-title')" [image]="seriesImage" [handleClick]="openSeries"></app-stat-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-2 pb-2">
|
||||
<app-top-readers></app-top-readers>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<app-file-breakdown-stats></app-file-breakdown-stats>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<app-publication-status-stats></app-publication-status-stats>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<app-reading-activity [isAdmin]="true"></app-reading-activity>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<app-day-breakdown></app-day-breakdown>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
<app-setting-item [title]="title" [showEdit]="false" [canEdit]="false" [subtitle]="tooltipText" [toggleOnViewClick]="false">
|
||||
<ng-template #view>
|
||||
<input #apiKey [type]="InputType" readonly class="d-inline-flex form-control" style="width: 80%" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||
<input #apiKey [type]="InputType" readonly class="d-inline-flex form-control" style="width: 80%" id="api-key--{{title}}"
|
||||
aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||
|
||||
<div class="d-inline-flex">
|
||||
@if (hideData) {
|
||||
|
@ -14,17 +15,10 @@
|
|||
<button class="btn btn-primary-text" type="button" (click)="copy()" [title]="t('copy')">
|
||||
{{t('copy')}}
|
||||
</button>
|
||||
@if (showRefresh) {
|
||||
<button class="btn btn-danger-text" [ngbTooltip]="t('regen-warning')" (click)="refresh()">{{t('reset')}}</button>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
@if (showRefresh) {
|
||||
<ng-template #titleActions>
|
||||
<button class="btn btn-danger-outline" [ngbTooltip]="tipContent" (click)="refresh()">Reset</button>
|
||||
</ng-template>
|
||||
}
|
||||
</app-setting-item>
|
||||
|
||||
<ng-template #tipContent>
|
||||
{{t('regen-warning')}}
|
||||
</ng-template>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
@ -337,7 +337,8 @@
|
|||
"regen-warning": "Regenerating your API key will invalidate any existing clients.",
|
||||
"no-key": "ERROR - KEY NOT SET",
|
||||
"confirm-reset": "This will invalidate any OPDS configurations you have setup. Are you sure you want to continue?",
|
||||
"key-reset": "API Key reset"
|
||||
"key-reset": "API Key reset",
|
||||
"reset": "Reset"
|
||||
},
|
||||
|
||||
"scrobbling-providers": {
|
||||
|
@ -571,9 +572,9 @@
|
|||
|
||||
"library-type-pipe": {
|
||||
"book": "Book",
|
||||
"comic": "Comic",
|
||||
"comic": "Comic (Legacy)",
|
||||
"manga": "Manga",
|
||||
"comicVine": "Comic Vine",
|
||||
"comicVine": "Comic",
|
||||
"image": "Image",
|
||||
"lightNovel": "Light Novel"
|
||||
},
|
||||
|
@ -1576,10 +1577,6 @@
|
|||
"download-logs-task": "Download Logs",
|
||||
"download-logs-task-desc": "Compiles all log files into a zip and downloads it.",
|
||||
|
||||
"analyze-files-task": "Analyze Files",
|
||||
"analyze-files-task-desc": "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.",
|
||||
"analyze-files-task-success": "File analysis has been queued",
|
||||
|
||||
"sync-themes-task": "Sync Themes",
|
||||
"sync-themes-task-desc": "Synchronize downloaded themes with upstream changes if version matches.",
|
||||
"sync-themes-success": "Synchronization of themes has been queued",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue