Library Settings Modal + New Library Settings (#1660)

* Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web

Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fixed want to read button on series detail not performing the correct action

* Started the library settings. Added ability to update a cover image for a library.

Updated backup db to also copy reading list (and now library) cover images.

* Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library.

* Fixed a missing update event in backend when updating a library.

* Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid.

* Trim library names before you check anything

* General code cleanup

* Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries.

Refactored some code to streamline perf in some flows.

* Removed old components replaced with new modal

* Code smells

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Joe Milazzo 2022-11-18 09:38:32 -06:00 committed by GitHub
parent 48b15e564d
commit 73d77e6264
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2530 additions and 276 deletions

View file

@ -10,4 +10,9 @@ export interface Library {
lastScanned: string;
type: LibraryType;
folders: string[];
coverImage?: string;
folderWatching: boolean;
includeInDashboard: boolean;
includeInRecommended: boolean;
includeInSearch: boolean;
}

View file

@ -212,6 +212,13 @@ export class ActionFactoryService {
},
],
},
{
action: Action.Edit,
title: 'Settings',
callback: this.dummyCallback,
requiresAdmin: true,
children: [],
},
];
this.collectionTagActions = [

View file

@ -61,6 +61,10 @@ export class ImageService implements OnDestroy {
return part.substring(0, equalIndex).replace('Id', '');
}
getLibraryCoverImage(libraryId: number) {
return this.baseUrl + 'image/library-cover?libraryId=' + libraryId;
}
getVolumeCoverImage(volumeId: number) {
return this.baseUrl + 'image/volume-cover?volumeId=' + volumeId;
}

View file

@ -50,6 +50,10 @@ export class LibraryService {
}));
}
libraryNameExists(name: string) {
return this.httpClient.get<boolean>(this.baseUrl + 'library/name-exists?name=' + name);
}
listDirectories(rootPath: string) {
let query = '';
if (rootPath !== undefined && rootPath.length > 0) {

View file

@ -38,6 +38,10 @@ export class UploadService {
return this.httpClient.post<number>(this.baseUrl + 'upload/chapter', {id: chapterId, url: this._cleanBase64Url(url)});
}
updateLibraryCoverImage(libraryId: number, url: string) {
return this.httpClient.post<number>(this.baseUrl + 'upload/library', {id: libraryId, url: this._cleanBase64Url(url)});
}
resetChapterCoverLock(chapterId: number, ) {
return this.httpClient.post<number>(this.baseUrl + 'upload/reset-chapter-lock', {id: chapterId, url: ''});
}

View file

@ -1,42 +0,0 @@
<form [formGroup]="libraryForm">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{this.library !== undefined ? 'Edit' : 'New'}} Library</h4>
<button type="button" class="btn-close" aria-label="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="library-name" class="form-label">Name</label>
<input id="library-name" class="form-control" formControlName="name" type="text">
</div>
<div class="mb-3">
<label for="library-type" class="form-label">Type</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="typeTooltip" role="button" tabindex="0"></i>
<ng-template #typeTooltip>Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but fall back to embedded data.</ng-template>
<span class="visually-hidden" id="library-type-help">Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but fall back to embedded data.</span>
<select class="form-select" id="library-type" formControlName="type" aria-describedby="library-type-help"> <!-- [attr.disabled]="this.library" -->
<option [value]="i" *ngFor="let opt of libraryTypes; let i = index">{{opt}}</option>
</select>
</div>
<h4>Folders <button type="button" class="btn float-end btn-sm" (click)="openDirectoryPicker()"><i class="fa fa-plus" aria-hidden="true"></i></button></h4>
<ul class="list-group" style="width: 100%">
<li class="list-group-item" *ngFor="let folder of selectedFolders; let i = index">
{{folder}}
<button class="btn float-end btn-sm" (click)="removeFolder(folder)"><i class="fa fa-times-circle" aria-hidden="true"></i></button>
</li>
</ul>
</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="submit" class="btn btn-primary" (click)="submitLibrary()" [disabled]="!libraryForm.dirty && !madeChanges">Save</button>
</div>
</form>

View file

@ -1,115 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { ConfirmService } from 'src/app/shared/confirm.service';
import { Library } from 'src/app/_models/library';
import { LibraryService } from 'src/app/_services/library.service';
import { SettingsService } from '../../settings.service';
import { DirectoryPickerComponent, DirectoryPickerResult } from '../directory-picker/directory-picker.component';
@Component({
selector: 'app-library-editor-modal',
templateUrl: './library-editor-modal.component.html',
styleUrls: ['./library-editor-modal.component.scss']
})
export class LibraryEditorModalComponent implements OnInit {
@Input() library: Library | undefined = undefined;
libraryForm: FormGroup = new FormGroup({
name: new FormControl('', [Validators.required]),
type: new FormControl(0, [Validators.required])
});
selectedFolders: string[] = [];
errorMessage = '';
madeChanges = false;
libraryTypes: string[] = []
constructor(private modalService: NgbModal, private libraryService: LibraryService, public modal: NgbActiveModal, private settingService: SettingsService,
private toastr: ToastrService, private confirmService: ConfirmService) { }
ngOnInit(): void {
this.settingService.getLibraryTypes().subscribe((types) => {
this.libraryTypes = types;
});
this.setValues();
}
removeFolder(folder: string) {
this.selectedFolders = this.selectedFolders.filter(item => item !== folder);
this.madeChanges = true;
}
async submitLibrary() {
const model = this.libraryForm.value;
model.folders = this.selectedFolders;
if (this.libraryForm.errors) {
return;
}
if (this.library !== undefined) {
model.id = this.library.id;
model.folders = model.folders.map((item: string) => item.startsWith('\\') ? item.substr(1, item.length) : item);
model.type = parseInt(model.type, 10);
if (model.type !== this.library.type) {
if (!await this.confirmService.confirm(`Changing library type will trigger a new scan with different parsing rules and may lead to
series being re-created and hence you may loose progress and bookmarks. You should backup before you do this. Are you sure you want to continue?`)) return;
}
this.libraryService.update(model).subscribe(() => {
this.close(true);
}, err => {
this.errorMessage = err;
});
} else {
model.folders = model.folders.map((item: string) => item.startsWith('\\') ? item.substr(1, item.length) : item);
model.type = parseInt(model.type, 10);
this.libraryService.create(model).subscribe(() => {
this.toastr.success('Library created successfully.');
this.toastr.info('A scan has been started.');
this.close(true);
}, err => {
this.errorMessage = err;
});
}
}
close(returnVal= false) {
const model = this.libraryForm.value;
this.modal.close(returnVal);
}
reset() {
this.setValues();
}
setValues() {
if (this.library !== undefined) {
this.libraryForm.get('name')?.setValue(this.library.name);
this.libraryForm.get('type')?.setValue(this.library.type);
this.selectedFolders = this.library.folders;
this.madeChanges = false;
}
}
openDirectoryPicker() {
const modalRef = this.modalService.open(DirectoryPickerComponent, { scrollable: true, size: 'lg' });
modalRef.closed.subscribe((closeResult: DirectoryPickerResult) => {
if (closeResult.success) {
if (!this.selectedFolders.includes(closeResult.folderPath)) {
this.selectedFolders.push(closeResult.folderPath);
this.madeChanges = true;
}
}
});
}
}

View file

@ -5,7 +5,6 @@ import { DashboardComponent } from './dashboard/dashboard.component';
import { NgbDropdownModule, NgbNavModule, NgbTooltipModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { ManageLibraryComponent } from './manage-library/manage-library.component';
import { ManageUsersComponent } from './manage-users/manage-users.component';
import { LibraryEditorModalComponent } from './_modals/library-editor-modal/library-editor-modal.component';
import { SharedModule } from '../shared/shared.module';
import { LibraryAccessModalComponent } from './_modals/library-access-modal/library-access-modal.component';
import { DirectoryPickerComponent } from './_modals/directory-picker/directory-picker.component';
@ -34,7 +33,6 @@ import { VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
ManageUsersComponent,
DashboardComponent,
ManageLibraryComponent,
LibraryEditorModalComponent,
LibraryAccessModalComponent,
DirectoryPickerComponent,
ResetPasswordModalComponent,

View file

@ -3,7 +3,7 @@
<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">&nbsp;Add Library</span></button></div>
</div>
<ul class="list-group" *ngIf="!createLibraryToggle; else createLibrary">
<ul class="list-group">
<li *ngFor="let library of libraries; let idx = index; trackBy: libraryTrackBy" class="list-group-item no-hover">
<div>
<h4>
@ -34,7 +34,4 @@
There are no libraries. Try creating one.
</li>
</ul>
<ng-template #createLibrary>
<app-library-editor-modal></app-library-editor-modal>
</ng-template>
</div>

View file

@ -4,12 +4,12 @@ import { ToastrService } from 'ngx-toastr';
import { Subject } from 'rxjs';
import { distinctUntilChanged, filter, take, takeUntil } from 'rxjs/operators';
import { ConfirmService } from 'src/app/shared/confirm.service';
import { LibrarySettingsModalComponent } from 'src/app/sidenav/_components/library-settings-modal/library-settings-modal.component';
import { NotificationProgressEvent } from 'src/app/_models/events/notification-progress-event';
import { ScanSeriesEvent } from 'src/app/_models/events/scan-series-event';
import { Library, LibraryType } from 'src/app/_models/library';
import { Library } from 'src/app/_models/library';
import { LibraryService } from 'src/app/_services/library.service';
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
import { LibraryEditorModalComponent } from '../_modals/library-editor-modal/library-editor-modal.component';
@Component({
selector: 'app-manage-library',
@ -20,7 +20,6 @@ import { LibraryEditorModalComponent } from '../_modals/library-editor-modal/lib
export class ManageLibraryComponent implements OnInit, OnDestroy {
libraries: Library[] = [];
createLibraryToggle = false;
loading = false;
/**
* If a deletion is in progress for a library
@ -90,7 +89,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
}
editLibrary(library: Library) {
const modalRef = this.modalService.open(LibraryEditorModalComponent);
const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' });
modalRef.componentInstance.library = library;
modalRef.closed.pipe(takeUntil(this.onDestroy)).subscribe(refresh => {
if (refresh) {
@ -100,7 +99,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
}
addLibrary() {
const modalRef = this.modalService.open(LibraryEditorModalComponent);
const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' });
modalRef.closed.pipe(takeUntil(this.onDestroy)).subscribe(refresh => {
if (refresh) {
this.getLibraries();

View file

@ -1,12 +1,3 @@
.accent {
font-style: italic;
font-size: 0.7rem;
background-color: lightgray;
padding: 10px;
color: black;
border-radius: 6px;
}
.invalid-feedback {
display: inherit;
}

View file

@ -1,7 +1,9 @@
<ng-container *ngIf="all$ | async as all">
<p *ngIf="all.length === 0">Nothing to show here. Add some metadata to your library, read something or rate something.</p>
<p *ngIf="all.length === 0">
Nothing to show here. Add some metadata to your library, read something or rate something. This library may also have recommendations turned off.
</p>
</ng-container>
<ng-container *ngIf="onDeck$ | async as onDeck">

View file

@ -796,9 +796,9 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
toggleWantToRead() {
if (this.isWantToRead) {
this.actionService.addMultipleSeriesToWantToReadList([this.series.id]);
} else {
this.actionService.removeMultipleSeriesFromWantToReadList([this.series.id]);
} else {
this.actionService.addMultipleSeriesToWantToReadList([this.series.id]);
}
this.isWantToRead = !this.isWantToRead;

View file

@ -0,0 +1,162 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">
<ng-container *ngIf="!isAddLibrary; else addLibraryTitle">
Edit {{library.name | sentenceCase}}
</ng-container>
<ng-template #addLibraryTitle>
Add Library
</ng-template>
</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
</div>
<form [formGroup]="libraryForm">
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills"
orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
<li [ngbNavItem]="TabID.General">
<a ngbNavLink>{{TabID.General}}</a>
<ng-template ngbNavContent>
<div class="mb-3">
<label for="library-name" class="form-label">Name</label>
<input id="library-name" class="form-control" formControlName="name" type="text" [class.is-invalid]="libraryForm.get('name')?.invalid && libraryForm.get('name')?.touched">
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="libraryForm.dirty || libraryForm.touched">
<div *ngIf="libraryForm.get('name')?.errors?.required">
This field is required
</div>
<div *ngIf="libraryForm.get('name')?.errors?.duplicateName">
Library name must be unique
</div>
</div>
</div>
<div class="mb-3">
<label for="library-type" class="form-label">Type</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="typeTooltip" role="button" tabindex="0"></i>
<ng-template #typeTooltip>Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but have different naming in the UI.</ng-template>
<span class="visually-hidden" id="library-type-help">Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but have different naming in the UI.</span>
<select class="form-select" id="library-type" formControlName="type" aria-describedby="library-type-help">
<option [value]="i" *ngFor="let opt of libraryTypes; let i = index">{{opt}}</option>
</select>
</div>
<div *ngIf="!isAddLibrary">
Last Scanned:
<span *ngIf="library.lastScanned == '0001-01-01T00:00:00'; else activeDate">Never</span>
<ng-template #activeDate>
{{library.lastScanned | date: 'short'}}
</ng-template>
</div>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Folder" [disabled]="isAddLibrary && setupStep < 1">
<a ngbNavLink>{{TabID.Folder}}</a>
<ng-template ngbNavContent>
<p>Add folders to your library</p>
<ul class="list-group" style="width: 100%">
<li class="list-group-item" *ngFor="let folder of selectedFolders; let i = index">
{{folder}}
<button class="btn float-end btn-sm" (click)="removeFolder(folder)"><i class="fa fa-times-circle" aria-hidden="true"></i></button>
</li>
</ul>
<div class="row mt-2">
<button class="btn btn-secondary float-end btn-sm" (click)="openDirectoryPicker()">
<i class="fa fa-plus" aria-hidden="true"></i>
Browse for Media Folders
</button>
</div>
<div class="row mt-2">
<p>Help us out by following <a href="https://wiki.kavitareader.com/en/guides/managing-your-files" rel="noopener noreferrer" target="_blank" referrerpolicy="no-refer">our guide</a> to naming and organizing your media.</p>
</div>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Cover" [disabled]="isAddLibrary && setupStep < 2">
<a ngbNavLink>{{TabID.Cover}}</a>
<ng-template ngbNavContent>
<p *ngIf="isAddLibrary" class="alert alert-secondary" role="alert">Custom library image icons are optional</p>
<p>Library image should not be large. Aim for a small file, 32x32 pixels in size. Kavita does not perform validation on size.</p>
<app-cover-image-chooser [(imageUrls)]="imageUrls"
[showReset]="false"
[showApplyButton]="true"
(applyCover)="applyCoverImage($event)"
(resetCover)="resetCoverImage()"
>
</app-cover-image-chooser>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Advanced" [disabled]="isAddLibrary && setupStep < 3">
<a ngbNavLink>{{TabID.Advanced}}</a>
<ng-template ngbNavContent>
<div class="row">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="lib-folder-watching" role="switch" formControlName="folderWatching" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="lib-folder-watching">Folder Watching</label>
</div>
</div>
<p class="accent">
Override Server folder watching for this library. If off, folder watching won't run on the folders this library contains. If libraries share folders, then folders may still be ran against.
</p>
</div>
</div>
<div class="row">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="include-dashboard" role="switch" formControlName="includeInDashboard" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="include-dashboard">Include in Dashboard</label>
</div>
</div>
<p class="accent">
Should series from the library be included on the Dashboard. This affects all streams, like On Deck, Recently Updated, Recently Added, or any custom additions.
</p>
</div>
</div>
<div class="row">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="include-recommended" role="switch" formControlName="includeInRecommended" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="include-recommended">Include in Recommended</label>
</div>
</div>
<p class="accent">
Should series from the library be included on the Recommended page.
</p>
</div>
</div>
<div class="row">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="include-search" role="switch" formControlName="includeInSearch" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="include-search">Include in Search</label>
</div>
</div>
<p class="accent">
Should series and any derived information (genres, people, files) from the library be included in search results.
</p>
</div>
</div>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
</div>
</form>
<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>
<ng-container *ngIf="isAddLibrary && setupStep != 3; else editLibraryButton">
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="isNextDisabled() || libraryForm.invalid">Next</button>
</ng-container>
<ng-template #editLibraryButton>
<button type="button" class="btn btn-primary" [disabled]="isDisabled()" (click)="save()">Save</button>
</ng-template>
</div>

View file

@ -0,0 +1,216 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { debounceTime, distinctUntilChanged, filter, switchMap, tap } from 'rxjs';
import { SettingsService } from 'src/app/admin/settings.service';
import { DirectoryPickerComponent, DirectoryPickerResult } 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';
import { LibraryService } from 'src/app/_services/library.service';
import { UploadService } from 'src/app/_services/upload.service';
enum TabID {
General = 'General',
Folder = 'Folder',
Cover = 'Cover',
Advanced = 'Advanced'
}
enum StepID {
General = 0,
Folder = 1,
Cover = 2,
Advanced = 3
}
@Component({
selector: 'app-library-settings-modal',
templateUrl: './library-settings-modal.component.html',
styleUrls: ['./library-settings-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LibrarySettingsModalComponent implements OnInit {
@Input() library!: Library;
active = TabID.General;
imageUrls: Array<string> = [];
libraryForm: FormGroup = new FormGroup({
name: new FormControl<string>('', { nonNullable: true, validators: [Validators.required] }),
type: new FormControl<LibraryType>(LibraryType.Manga, { nonNullable: true, validators: [Validators.required] }),
folderWatching: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
includeInDashboard: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
includeInRecommended: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
includeInSearch: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }),
});
selectedFolders: string[] = [];
madeChanges = false;
libraryTypes: string[] = []
isAddLibrary = false;
setupStep = StepID.General;
get Breakpoint() { return Breakpoint; }
get TabID() { return TabID; }
get StepID() { return StepID; }
constructor(public utilityService: UtilityService, private uploadService: UploadService, private modalService: NgbModal,
private settingService: SettingsService, public modal: NgbActiveModal, private confirmService: ConfirmService,
private libraryService: LibraryService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void {
this.settingService.getLibraryTypes().subscribe((types) => {
this.libraryTypes = types;
this.cdRef.markForCheck();
});
if (this.library === undefined) {
this.isAddLibrary = true;
this.cdRef.markForCheck();
}
if (this.library?.coverImage != null && this.library?.coverImage !== '') {
this.imageUrls.push(this.library.coverImage);
this.cdRef.markForCheck();
}
this.libraryForm.get('name')?.valueChanges.pipe(
debounceTime(100),
distinctUntilChanged(),
switchMap(name => this.libraryService.libraryNameExists(name)),
tap(exists => {
const isExistingName = this.libraryForm.get('name')?.value === this.library?.name;
console.log('isExistingName', isExistingName)
if (!exists || isExistingName) {
this.libraryForm.get('name')?.setErrors(null);
} else {
this.libraryForm.get('name')?.setErrors({duplicateName: true})
}
this.cdRef.markForCheck();
})
).subscribe();
this.setValues();
}
setValues() {
if (this.library !== undefined) {
this.libraryForm.get('name')?.setValue(this.library.name);
this.libraryForm.get('type')?.setValue(this.library.type);
this.libraryForm.get('folderWatching')?.setValue(this.library.folderWatching);
this.libraryForm.get('includeInDashboard')?.setValue(this.library.includeInDashboard);
this.libraryForm.get('includeInRecommended')?.setValue(this.library.includeInRecommended);
this.libraryForm.get('includeInSearch')?.setValue(this.library.includeInSearch);
this.selectedFolders = this.library.folders;
this.madeChanges = false;
this.cdRef.markForCheck();
}
}
isDisabled() {
return !(this.libraryForm.valid && this.selectedFolders.length > 0);
}
reset() {
this.setValues();
}
close(returnVal= false) {
this.modal.close(returnVal);
}
async save() {
const model = this.libraryForm.value;
model.folders = this.selectedFolders;
if (this.libraryForm.errors) {
return;
}
if (this.library !== undefined) {
model.id = this.library.id;
model.folders = model.folders.map((item: string) => item.startsWith('\\') ? item.substr(1, item.length) : item);
model.type = parseInt(model.type, 10);
if (model.type !== this.library.type) {
if (!await this.confirmService.confirm(`Changing library type will trigger a new scan with different parsing rules and may lead to
series being re-created and hence you may loose progress and bookmarks. You should backup before you do this. Are you sure you want to continue?`)) return;
}
this.libraryService.update(model).subscribe(() => {
this.close(true);
});
} else {
model.folders = model.folders.map((item: string) => item.startsWith('\\') ? item.substr(1, item.length) : item);
model.type = parseInt(model.type, 10);
this.libraryService.create(model).subscribe(() => {
this.toastr.success('Library created successfully. A scan has been started.');
this.close(true);
});
}
}
nextStep() {
this.setupStep++;
switch(this.setupStep) {
case StepID.Folder:
this.active = TabID.Folder;
break;
case StepID.Cover:
this.active = TabID.Cover;
break;
case StepID.Advanced:
this.active = TabID.Advanced;
break;
}
this.cdRef.markForCheck();
}
applyCoverImage(coverUrl: string) {
this.uploadService.updateLibraryCoverImage(this.library.id, coverUrl).subscribe(() => {});
}
resetCoverImage() {
this.uploadService.updateLibraryCoverImage(this.library.id, '').subscribe(() => {});
}
openDirectoryPicker() {
const modalRef = this.modalService.open(DirectoryPickerComponent, { scrollable: true, size: 'lg' });
modalRef.closed.subscribe((closeResult: DirectoryPickerResult) => {
if (closeResult.success) {
if (!this.selectedFolders.includes(closeResult.folderPath)) {
this.selectedFolders.push(closeResult.folderPath);
this.madeChanges = true;
this.cdRef.markForCheck();
}
}
});
}
removeFolder(folder: string) {
this.selectedFolders = this.selectedFolders.filter(item => item !== folder);
this.madeChanges = true;
this.cdRef.markForCheck();
}
isNextDisabled() {
switch (this.setupStep) {
case StepID.General:
return this.libraryForm.get('name')?.invalid || this.libraryForm.get('type')?.invalid;
case StepID.Folder:
return this.selectedFolders.length === 0;
case StepID.Cover:
return false; // Covers are optional
case StepID.Advanced:
return false; // Advanced are optional
}
}
}

View file

@ -15,7 +15,10 @@
<div class="active-highlight"></div>
<span class="phone-hidden" title="{{title}}">
<div>
<i class="fa {{icon}}" aria-hidden="true"></i>
<ng-container *ngIf="imageUrl != null && imageUrl != ''; else iconImg">
<img [src]="imageUrl" alt="icon" class="side-nav-img">
</ng-container>
<ng-template #iconImg><i class="fa {{icon}}" aria-hidden="true"></i></ng-template>
</div>
</span>
<span class="side-nav-text">

View file

@ -21,6 +21,11 @@
}
}
.side-nav-img {
width: 20px;
height: 18px;
}
span {
&:last-child {
@ -81,7 +86,6 @@
}
.side-nav-text, i {
color: var(--side-nav-item-active-text-color) !important;
}
@ -107,46 +111,19 @@ a {
@media (max-width: 576px) {
.side-nav-item {
align-items: center;
//display: flex;
//justify-content: space-between;
padding: 15px 10px;
//width: 100%;
height: 70px;
//min-height: 40px;
// overflow: hidden;
font-size: 1rem;
//cursor: pointer;
.side-nav-text {
// padding-left: 10px;
// opacity: 1;
// min-width: 100px;
width: 100%;
// div {
// min-width: 102px;
// width: 100%
// }
}
&.closed {
// .side-nav-text {
// opacity: 0;
// }
.card-actions {
//opacity: 0;
font-size: inherit;
}
}
// span {
// &:last-child {
// flex-grow: 1;
// justify-content: end;
// }
// }
}
}

View file

@ -14,6 +14,7 @@ export class SideNavItemComponent implements OnInit, OnDestroy {
* Icon to display next to item. ie) 'fa-home'
*/
@Input() icon: string = '';
@Input() imageUrl: string | null = '';
/**
* Text for the item
*/

View file

@ -21,7 +21,7 @@
</div>
</div>
<app-side-nav-item *ngFor="let library of libraries | filter: filterLibrary" [link]="'/library/' + library.id + '/'"
[icon]="getLibraryTypeIcon(library.type)" [title]="library.name" [comparisonMethod]="'startsWith'">
[icon]="getLibraryTypeIcon(library.type)" [imageUrl]="getLibraryImage(library)" [title]="library.name" [comparisonMethod]="'startsWith'">
<ng-container actions>
<app-card-actionables [actions]="actions" [labelBy]="library.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event, library)"></app-card-actionables>
</ng-container>

View file

@ -1,7 +1,9 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Subject } from 'rxjs';
import { filter, map, shareReplay, take, takeUntil } from 'rxjs/operators';
import { ImageService } from 'src/app/_services/image.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { Breakpoint, UtilityService } from '../../shared/_services/utility.service';
import { Library, LibraryType } from '../../_models/library';
@ -10,6 +12,7 @@ import { Action, ActionFactoryService, ActionItem } from '../../_services/action
import { ActionService } from '../../_services/action.service';
import { LibraryService } from '../../_services/library.service';
import { NavService } from '../../_services/nav.service';
import { LibrarySettingsModalComponent } from '../_components/library-settings-modal/library-settings-modal.component';
@Component({
selector: 'app-side-nav',
@ -33,7 +36,8 @@ export class SideNavComponent implements OnInit, OnDestroy {
constructor(public accountService: AccountService, private libraryService: LibraryService,
public utilityService: UtilityService, private messageHub: MessageHubService,
private actionFactoryService: ActionFactoryService, private actionService: ActionService,
public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef) {
public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef,
private modalService: NgbModal, private imageService: ImageService) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
@ -64,7 +68,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
this.messageHub.messages$.pipe(takeUntil(this.onDestroy), filter(event => event.event === EVENTS.LibraryModified)).subscribe(event => {
this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => {
this.libraries = libraries;
this.libraries = [...libraries];
this.cdRef.markForCheck();
});
});
@ -86,6 +90,20 @@ export class SideNavComponent implements OnInit, OnDestroy {
case (Action.AnalyzeFiles):
this.actionService.analyzeFiles(library);
break;
case (Action.Edit):
const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' });
modalRef.componentInstance.library = library;
modalRef.closed.subscribe((closeResult: {success: boolean, library: Library, coverImageUpdate: boolean}) => {
window.scrollTo(0, 0);
if (closeResult.success) {
}
if (closeResult.coverImageUpdate) {
}
});
break;
default:
break;
}
@ -108,6 +126,11 @@ export class SideNavComponent implements OnInit, OnDestroy {
}
}
getLibraryImage(library: Library) {
if (library.coverImage) return this.imageService.getLibraryCoverImage(library.id);
return null;
}
toggleNavBar() {
this.navService.toggleSideNav();
}

View file

@ -5,9 +5,10 @@ import { SideNavItemComponent } from './side-nav-item/side-nav-item.component';
import { SideNavComponent } from './side-nav/side-nav.component';
import { PipeModule } from '../pipe/pipe.module';
import { CardsModule } from '../cards/cards.module';
import { FormsModule } from '@angular/forms';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { RouterModule } from '@angular/router';
import { LibrarySettingsModalComponent } from './_components/library-settings-modal/library-settings-modal.component';
@ -15,7 +16,8 @@ import { RouterModule } from '@angular/router';
declarations: [
SideNavCompanionBarComponent,
SideNavItemComponent,
SideNavComponent
SideNavComponent,
LibrarySettingsModalComponent
],
imports: [
CommonModule,
@ -24,6 +26,8 @@ import { RouterModule } from '@angular/router';
CardsModule,
FormsModule,
NgbTooltipModule,
NgbNavModule,
ReactiveFormsModule
],
exports: [
SideNavCompanionBarComponent,

View file

@ -25,6 +25,9 @@ hr {
color: var(--accent-text-color) !important;
box-shadow: inset 0px 0px 8px 1px var(--accent-bg-color) !important;
font-size: var(--accent-text-size) !important;
font-style: italic;
padding: 10px;
border-radius: 6px;
}
.text-muted {
@ -37,4 +40,5 @@ hr {
.form-switch .form-check-input:checked {
background-color: var(--primary-color);
}
}