Collection Rework (#2830)
This commit is contained in:
parent
0dacc061f1
commit
deaaccb96a
93 changed files with 5413 additions and 1120 deletions
|
|
@ -6,23 +6,37 @@
|
|||
</div>
|
||||
<form style="width: 100%" [formGroup]="listForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3" *ngIf="lists.length >= 5">
|
||||
<label for="filter" class="form-label">{{t('filter-label')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item clickable" tabindex="0" role="option" *ngFor="let collectionTag of lists | filter: filterList; let i = index; trackBy: collectionTitleTrackby" (click)="addToCollection(collectionTag)">
|
||||
{{collectionTag.title}} <i class="fa fa-angle-double-up" *ngIf="collectionTag.promoted" [title]="t('promoted')"></i>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">{{t('no-data')}}</li>
|
||||
<li class="list-group-item" *ngIf="loading">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">{{t('loading')}}</span>
|
||||
@if (lists.length >= 5) {
|
||||
<div class="mb-3">
|
||||
<label for="filter" class="form-label">{{t('filter-label')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ul class="list-group">
|
||||
@for(collectionTag of lists | filter: filterList; let i = $index; track collectionTag.title) {
|
||||
<li class="list-group-item clickable" tabindex="0" role="option" (click)="addToCollection(collectionTag)">
|
||||
{{collectionTag.title}}
|
||||
@if (collectionTag.promoted) {
|
||||
<i class="fa fa-angle-double-up" [title]="t('promoted')"></i>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (lists.length === 0 && !loading) {
|
||||
<li class="list-group-item">{{t('no-data')}}</li>
|
||||
}
|
||||
|
||||
@if (loading) {
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">{{t('loading')}}</span>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer" style="justify-content: normal">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
import {FormGroup, FormControl, ReactiveFormsModule} from '@angular/forms';
|
||||
import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import {UserCollection} from 'src/app/_models/collection-tag';
|
||||
import { ReadingList } from 'src/app/_models/reading-list';
|
||||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||
import {CommonModule} from "@angular/common";
|
||||
|
|
@ -31,28 +31,25 @@ import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco
|
|||
})
|
||||
export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
|
||||
|
||||
private readonly modal = inject(NgbActiveModal);
|
||||
private readonly collectionService = inject(CollectionTagService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
@Input({required: true}) title!: string;
|
||||
/**
|
||||
* Series Ids to add to Collection Tag
|
||||
*/
|
||||
@Input() seriesIds: Array<number> = [];
|
||||
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
|
||||
|
||||
/**
|
||||
* All existing collections sorted by recent use date
|
||||
*/
|
||||
lists: Array<CollectionTag> = [];
|
||||
lists: Array<UserCollection> = [];
|
||||
loading: boolean = false;
|
||||
listForm: FormGroup = new FormGroup({});
|
||||
|
||||
collectionTitleTrackby = (index: number, item: CollectionTag) => `${item.title}`;
|
||||
|
||||
|
||||
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
|
||||
|
||||
|
||||
constructor(private modal: NgbActiveModal, private collectionService: CollectionTagService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.listForm.addControl('title', new FormControl(this.title, []));
|
||||
|
|
@ -60,7 +57,7 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
|
|||
|
||||
this.loading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
this.collectionService.allCollections(true).subscribe(tags => {
|
||||
this.lists = tags;
|
||||
this.loading = false;
|
||||
this.cdRef.markForCheck();
|
||||
|
|
@ -87,7 +84,7 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
|
|||
});
|
||||
}
|
||||
|
||||
addToCollection(tag: CollectionTag) {
|
||||
addToCollection(tag: UserCollection) {
|
||||
if (this.seriesIds.length === 0) return;
|
||||
|
||||
this.collectionService.addByMultiple(tag.id, this.seriesIds, '').subscribe(() => {
|
||||
|
|
|
|||
|
|
@ -16,14 +16,16 @@
|
|||
<label for="library-name" class="form-label">{{t('name-label')}}</label>
|
||||
<input id="library-name" class="form-control" formControlName="title" type="text"
|
||||
[class.is-invalid]="collectionTagForm.get('title')?.invalid && collectionTagForm.get('title')?.touched">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="collectionTagForm.dirty || collectionTagForm.touched">
|
||||
<div *ngIf="collectionTagForm.get('title')?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
@if (collectionTagForm.dirty || collectionTagForm.touched) {
|
||||
<div id="inviteForm-validations" class="invalid-feedback">
|
||||
@if (collectionTagForm.get('title')?.errors?.required) {
|
||||
<div>{{t('required-field')}}</div>
|
||||
}
|
||||
@if (collectionTagForm.get('title')?.errors?.duplicateName) {
|
||||
<div>{{t('name-validation')}}</div>
|
||||
}
|
||||
</div>
|
||||
<div *ngIf="collectionTagForm.get('title')?.errors?.duplicateName">
|
||||
{{t('name-validation')}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12 ms-2">
|
||||
<div class="form-check form-switch">
|
||||
|
|
@ -49,32 +51,46 @@
|
|||
<li [ngbNavItem]="TabID.Series">
|
||||
<a ngbNavLink>{{t(TabID.Series)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="list-group" *ngIf="!isLoading">
|
||||
<h6>{{t('series-title')}}</h6>
|
||||
<div class="form-check">
|
||||
<input id="selectall" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="selectall" class="form-check-label">{{selectAll ? t('deselect-all') : t('select-all')}}</label>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="list-group-item" *ngFor="let item of series; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="series-{{i}}" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selections.isSelected(item)" (change)="handleSelection(item)">
|
||||
<label attr.for="series-{{i}}" class="form-check-label">{{item.name}} ({{libraryName(item.libraryId)}})</label>
|
||||
@if (!isLoading) {
|
||||
<div class="list-group">
|
||||
<form [formGroup]="formGroup">
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-12">
|
||||
<label for="filter" class="visually-hidden">{{t('filter-label')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" type="text" class="form-control" [placeholder]="t('filter-label')" formControlName="filter" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex justify-content-center" *ngIf="pagination && series.length !== 0">
|
||||
<ngb-pagination
|
||||
*ngIf="pagination.totalPages > 1"
|
||||
[(page)]="pagination.currentPage"
|
||||
[pageSize]="pagination.itemsPerPage"
|
||||
(pageChange)="onPageChange($event)"
|
||||
[rotate]="false" [ellipses]="false" [boundaryLinks]="true"
|
||||
[collectionSize]="pagination.totalItems"></ngb-pagination>
|
||||
</form>
|
||||
<div class="form-check">
|
||||
<input id="select-all" type="checkbox" class="form-check-input" [disabled]="tag.source !== ScrobbleProvider.Kavita"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="select-all" class="form-check-label">{{selectAll ? t('deselect-all') : t('select-all')}}</label>
|
||||
</div>
|
||||
<ul>
|
||||
@for (item of series | filter: filterList; let i = $index; track item.id) {
|
||||
<li class="list-group-item">
|
||||
<div class="form-check">
|
||||
<input id="series-{{i}}" type="checkbox" class="form-check-input" [disabled]="tag.source !== ScrobbleProvider.Kavita"
|
||||
[ngModel]="selections.isSelected(item)" (change)="handleSelection(item)">
|
||||
<label for="series-{{i}}" class="form-check-label">{{item.name}} ({{libraryName(item.libraryId)}})</label>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@if (pagination && series.length !== 0 && pagination.totalPages > 1) {
|
||||
<div class="d-flex justify-content-center">
|
||||
<ngb-pagination
|
||||
[(page)]="pagination.currentPage"
|
||||
[pageSize]="pagination.itemsPerPage"
|
||||
(pageChange)="onPageChange($event)"
|
||||
[rotate]="false" [ellipses]="false" [boundaryLinks]="true"
|
||||
[collectionSize]="pagination.totalItems"></ngb-pagination>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
|
||||
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {
|
||||
NgbActiveModal,
|
||||
|
|
@ -15,25 +7,30 @@ import {
|
|||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavOutlet,
|
||||
NgbPagination, NgbTooltip
|
||||
NgbPagination,
|
||||
NgbTooltip
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { debounceTime, distinctUntilChanged, forkJoin, switchMap, tap } from 'rxjs';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { SelectionModel } from 'src/app/typeahead/_components/typeahead.component';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { Pagination } from 'src/app/_models/pagination';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {debounceTime, distinctUntilChanged, forkJoin, switchMap, tap} from 'rxjs';
|
||||
import {ConfirmService} from 'src/app/shared/confirm.service';
|
||||
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {SelectionModel} from 'src/app/typeahead/_components/typeahead.component';
|
||||
import {UserCollection} from 'src/app/_models/collection-tag';
|
||||
import {Pagination} from 'src/app/_models/pagination';
|
||||
import {Series} from 'src/app/_models/series';
|
||||
import {CollectionTagService} from 'src/app/_services/collection-tag.service';
|
||||
import {ImageService} from 'src/app/_services/image.service';
|
||||
import {LibraryService} from 'src/app/_services/library.service';
|
||||
import {SeriesService} from 'src/app/_services/series.service';
|
||||
import {UploadService} from 'src/app/_services/upload.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {CommonModule, NgTemplateOutlet} from "@angular/common";
|
||||
import {CoverImageChooserComponent} from "../../cover-image-chooser/cover-image-chooser.component";
|
||||
import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
|
||||
import {FilterPipe} from "../../../_pipes/filter.pipe";
|
||||
import {ScrobbleError} from "../../../_models/scrobbling/scrobble-error";
|
||||
import {AccountService} from "../../../_services/account.service";
|
||||
|
||||
|
||||
enum TabID {
|
||||
|
|
@ -45,14 +42,33 @@ enum TabID {
|
|||
@Component({
|
||||
selector: 'app-edit-collection-tags',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, ReactiveFormsModule, FormsModule, NgbPagination, CoverImageChooserComponent, NgbNavOutlet, NgbTooltip, TranslocoDirective],
|
||||
imports: [NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, ReactiveFormsModule, FormsModule, NgbPagination,
|
||||
CoverImageChooserComponent, NgbNavOutlet, NgbTooltip, TranslocoDirective, NgTemplateOutlet, FilterPipe],
|
||||
templateUrl: './edit-collection-tags.component.html',
|
||||
styleUrls: ['./edit-collection-tags.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditCollectionTagsComponent implements OnInit {
|
||||
|
||||
@Input({required: true}) tag!: CollectionTag;
|
||||
public readonly modal = inject(NgbActiveModal);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly collectionService = inject(CollectionTagService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
private readonly libraryService = inject(LibraryService);
|
||||
private readonly imageService = inject(ImageService);
|
||||
private readonly uploadService = inject(UploadService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly accountService = inject(AccountService);
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly ScrobbleProvider = ScrobbleProvider;
|
||||
|
||||
@Input({required: true}) tag!: UserCollection;
|
||||
|
||||
series: Array<Series> = [];
|
||||
selections!: SelectionModel<Series>;
|
||||
isLoading: boolean = true;
|
||||
|
|
@ -64,25 +80,18 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
active = TabID.General;
|
||||
imageUrls: Array<string> = [];
|
||||
selectedCover: string = '';
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
formGroup = new FormGroup({'filter': new FormControl('', [])});
|
||||
|
||||
|
||||
get hasSomeSelected() {
|
||||
return this.selections != null && this.selections.hasSomeSelected();
|
||||
}
|
||||
|
||||
get Breakpoint() {
|
||||
return Breakpoint;
|
||||
filterList = (listItem: Series) => {
|
||||
const query = (this.formGroup.get('filter')?.value || '').toLowerCase();
|
||||
return listItem.name.toLowerCase().indexOf(query) >= 0 || listItem.localizedName.toLowerCase().indexOf(query) >= 0;
|
||||
}
|
||||
|
||||
get TabID() {
|
||||
return TabID;
|
||||
}
|
||||
|
||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
|
||||
private collectionService: CollectionTagService, private toastr: ToastrService,
|
||||
private confirmService: ConfirmService, private libraryService: LibraryService,
|
||||
private imageService: ImageService, private uploadService: UploadService,
|
||||
public utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.pagination == undefined) {
|
||||
|
|
@ -96,6 +105,20 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
promoted: new FormControl(this.tag.promoted, { nonNullable: true, validators: [] }),
|
||||
});
|
||||
|
||||
if (this.tag.source !== ScrobbleProvider.Kavita) {
|
||||
this.collectionTagForm.get('title')?.disable();
|
||||
this.collectionTagForm.get('summary')?.disable();
|
||||
}
|
||||
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||
if (!user) return;
|
||||
if (!this.accountService.hasPromoteRole(user)) {
|
||||
this.collectionTagForm.get('promoted')?.disable();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.collectionTagForm.get('title')?.valueChanges.pipe(
|
||||
debounceTime(100),
|
||||
distinctUntilChanged(),
|
||||
|
|
@ -169,6 +192,9 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
const unselectedIds = this.selections.unselected().map(s => s.id);
|
||||
const tag = this.collectionTagForm.value;
|
||||
tag.id = this.tag.id;
|
||||
tag.title = this.collectionTagForm.get('title')!.value;
|
||||
tag.summary = this.collectionTagForm.get('summary')!.value;
|
||||
|
||||
|
||||
if (unselectedIds.length == this.series.length &&
|
||||
!await this.confirmService.confirm(translate('toasts.no-series-collection-warning'))) {
|
||||
|
|
@ -177,9 +203,13 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
|
||||
const apis = [
|
||||
this.collectionService.updateTag(tag),
|
||||
this.collectionService.updateSeriesForTag(tag, this.selections.unselected().map(s => s.id))
|
||||
];
|
||||
|
||||
const unselectedSeries = this.selections.unselected().map(s => s.id);
|
||||
if (unselectedSeries.length > 0) {
|
||||
apis.push(this.collectionService.updateSeriesForTag(tag, unselectedSeries));
|
||||
}
|
||||
|
||||
if (selectedIndex > 0) {
|
||||
apis.push(this.uploadService.updateCollectionCoverImage(this.tag.id, this.selectedCover));
|
||||
}
|
||||
|
|
@ -207,5 +237,4 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,13 +72,15 @@
|
|||
<div class="row g-0">
|
||||
<div class="col-lg-8 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="collections" class="form-label">{{t('collections-label')}}</label>
|
||||
<app-typeahead (selectedData)="updateCollections($event)" [settings]="collectionTagSettings" [locked]="true">
|
||||
<label for="language" class="form-label">{{t('language-label')}}</label>
|
||||
<app-typeahead (selectedData)="updateLanguage($event);metadata.languageLocked = true;" [settings]="languageSettings"
|
||||
[(locked)]="metadata.languageLocked" (onUnlock)="metadata.languageLocked = false"
|
||||
(newItemAdded)="metadata.languageLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
{{item.title}} ({{item.isoCode}})
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
|
|
@ -138,22 +140,10 @@
|
|||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-4 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="language" class="form-label">{{t('language-label')}}</label>
|
||||
<app-typeahead (selectedData)="updateLanguage($event);metadata.languageLocked = true;" [settings]="languageSettings"
|
||||
[(locked)]="metadata.languageLocked" (onUnlock)="metadata.languageLocked = false"
|
||||
(newItemAdded)="metadata.languageLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}} ({{item.isoCode}})
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-12 pe-2">
|
||||
<!-- <div class="col-lg-4 col-md-12 pe-2">-->
|
||||
<!-- -->
|
||||
<!-- </div>-->
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="age-rating" class="form-label">{{t('age-rating-label')}}</label>
|
||||
<div class="input-group {{metadata.ageRatingLocked ? 'lock-active' : ''}}">
|
||||
|
|
@ -164,7 +154,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="col-lg-6 col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="publication-status" class="form-label">{{t('publication-status-label')}}</label>
|
||||
<div class="input-group {{metadata.publicationStatusLocked ? 'lock-active' : ''}}">
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import { map } 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';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { Genre } from 'src/app/_models/metadata/genre';
|
||||
import { AgeRatingDto } from 'src/app/_models/metadata/age-rating-dto';
|
||||
import { Language } from 'src/app/_models/metadata/language';
|
||||
|
|
@ -31,7 +30,6 @@ import { Person, PersonRole } from 'src/app/_models/metadata/person';
|
|||
import { Series } from 'src/app/_models/series';
|
||||
import { SeriesMetadata } from 'src/app/_models/metadata/series-metadata';
|
||||
import { Tag } from 'src/app/_models/tag';
|
||||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||
|
|
@ -119,7 +117,6 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
private readonly fb = inject(FormBuilder);
|
||||
public readonly imageService = inject(ImageService);
|
||||
private readonly libraryService = inject(LibraryService);
|
||||
private readonly collectionService = inject(CollectionTagService);
|
||||
private readonly uploadService = inject(UploadService);
|
||||
private readonly metadataService = inject(MetadataService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
|
@ -155,10 +152,8 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
tagsSettings: TypeaheadSettings<Tag> = new TypeaheadSettings();
|
||||
languageSettings: TypeaheadSettings<Language> = new TypeaheadSettings();
|
||||
peopleSettings: {[PersonRole: string]: TypeaheadSettings<Person>} = {};
|
||||
collectionTagSettings: TypeaheadSettings<CollectionTag> = new TypeaheadSettings();
|
||||
genreSettings: TypeaheadSettings<Genre> = new TypeaheadSettings();
|
||||
|
||||
collectionTags: CollectionTag[] = [];
|
||||
tags: Tag[] = [];
|
||||
genres: Genre[] = [];
|
||||
ageRatings: Array<AgeRatingDto> = [];
|
||||
|
|
@ -330,44 +325,15 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
|
||||
setupTypeaheads() {
|
||||
forkJoin([
|
||||
this.setupCollectionTagsSettings(),
|
||||
this.setupTagSettings(),
|
||||
this.setupGenreTypeahead(),
|
||||
this.setupPersonTypeahead(),
|
||||
this.setupLanguageTypeahead()
|
||||
]).subscribe(results => {
|
||||
this.collectionTags = this.metadata.collectionTags;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
setupCollectionTagsSettings() {
|
||||
this.collectionTagSettings.minCharacters = 0;
|
||||
this.collectionTagSettings.multiple = true;
|
||||
this.collectionTagSettings.id = 'collections';
|
||||
this.collectionTagSettings.unique = true;
|
||||
this.collectionTagSettings.addIfNonExisting = true;
|
||||
this.collectionTagSettings.fetchFn = (filter: string) => this.fetchCollectionTags(filter).pipe(map(items => this.collectionTagSettings.compareFn(items, filter)));
|
||||
this.collectionTagSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, title: title, promoted: false, coverImage: '', summary: '', coverImageLocked: false };
|
||||
});
|
||||
this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.collectionTagSettings.compareFnForAdd = (options: CollectionTag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.collectionTagSettings.selectionCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||
return a.title === b.title;
|
||||
}
|
||||
|
||||
if (this.metadata.collectionTags) {
|
||||
this.collectionTagSettings.savedData = this.metadata.collectionTags;
|
||||
}
|
||||
|
||||
return of(true);
|
||||
}
|
||||
|
||||
setupTagSettings() {
|
||||
this.tagsSettings.minCharacters = 0;
|
||||
this.tagsSettings.multiple = true;
|
||||
|
|
@ -545,10 +511,6 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
fetchCollectionTags(filter: string = '') {
|
||||
return this.collectionService.search(filter);
|
||||
}
|
||||
|
||||
updateWeblinks(items: Array<string>) {
|
||||
this.metadata.webLinks = items.map(s => s.replaceAll(',', '%2C')).join(',');
|
||||
}
|
||||
|
|
@ -559,7 +521,7 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
const selectedIndex = this.editSeriesForm.get('coverImageIndex')?.value || 0;
|
||||
|
||||
const apis = [
|
||||
this.seriesService.updateMetadata(this.metadata, this.collectionTags)
|
||||
this.seriesService.updateMetadata(this.metadata)
|
||||
];
|
||||
|
||||
// We only need to call updateSeries if we changed name, sort name, or localized name or reset a cover image
|
||||
|
|
@ -585,10 +547,6 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
updateCollections(tags: CollectionTag[]) {
|
||||
this.collectionTags = tags;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateTags(tags: Tag[]) {
|
||||
this.tags = tags;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {ReplaySubject} from 'rxjs';
|
|||
import {filter} from 'rxjs/operators';
|
||||
import {Action, ActionFactoryService, ActionItem} from '../_services/action-factory.service';
|
||||
|
||||
type DataSource = 'volume' | 'chapter' | 'special' | 'series' | 'bookmark' | 'sideNavStream';
|
||||
type DataSource = 'volume' | 'chapter' | 'special' | 'series' | 'bookmark' | 'sideNavStream' | 'collection';
|
||||
|
||||
/**
|
||||
* Responsible for handling selections on cards. Can handle multiple card sources next to each other in different loops.
|
||||
|
|
@ -155,6 +155,10 @@ export class BulkSelectionService {
|
|||
return this.applyFilterToList(this.actionFactory.getSideNavStreamActions(callback), [Action.MarkAsInvisible, Action.MarkAsVisible]);
|
||||
}
|
||||
|
||||
if (Object.keys(this.selectedCards).filter(item => item === 'collection').length > 0) {
|
||||
return this.applyFilterToList(this.actionFactory.getCollectionTagActions(callback), [Action.Promote, Action.UnPromote, Action.Delete]);
|
||||
}
|
||||
|
||||
return this.applyFilterToList(this.actionFactory.getVolumeActions(callback), allowedActions);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,45 +31,53 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
<div class="bulk-mode {{bulkSelectionService.hasSelections() ? 'always-show' : ''}}" (click)="handleSelection($event)" *ngIf="allowSelection">
|
||||
<input type="checkbox" class="form-check-input" attr.aria-labelledby="{{title}}_{{entity.id}}" [ngModel]="selected" [ngModelOptions]="{standalone: true}">
|
||||
</div>
|
||||
|
||||
<div class="count" *ngIf="count > 1">
|
||||
<span class="badge bg-primary">{{count}}</span>
|
||||
</div>
|
||||
<div class="card-overlay"></div>
|
||||
@if (overlayInformation | safeHtml; as info) {
|
||||
<div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}" *ngIf="info !== '' || info !== undefined">
|
||||
<div class="position-relative">
|
||||
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="info" placement="top" [innerHTML]="info"></span>
|
||||
</div>
|
||||
@if (allowSelection) {
|
||||
<div class="bulk-mode {{bulkSelectionService.hasSelections() ? 'always-show' : ''}}" (click)="handleSelection($event)">
|
||||
<input type="checkbox" class="form-check-input" attr.aria-labelledby="{{title}}_{{entity.id}}" [ngModel]="selected" [ngModelOptions]="{standalone: true}">
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
|
||||
<div>
|
||||
@if (count > 1) {
|
||||
<div class="count">
|
||||
<span class="badge bg-primary">{{count}}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-overlay"></div>
|
||||
@if (overlayInformation | safeHtml; as info) {
|
||||
@if (info !== '' || info !== null) {
|
||||
<div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}">
|
||||
<div class="position-relative">
|
||||
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="info" placement="top" [innerHTML]="info"></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if (title.length > 0 || actions.length > 0) {
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<span class="card-title" placement="top" id="{{title}}_{{entity.id}}" [ngbTooltip]="tooltipTitle" (click)="handleClick($event)" tabindex="0">
|
||||
<span *ngIf="isPromoted()">
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">(promoted)</span>
|
||||
</span>
|
||||
<ng-container *ngIf="format | mangaFormat as formatString">
|
||||
<i class="fa {{format | mangaFormatIcon}} me-1" aria-hidden="true" *ngIf="format !== MangaFormat.UNKNOWN" title="{{formatString}}"></i>
|
||||
<span class="visually-hidden">{{formatString}}</span>
|
||||
</ng-container>
|
||||
<app-promoted-icon [promoted]="isPromoted()"></app-promoted-icon>
|
||||
<app-series-format [format]="format"></app-series-format>
|
||||
{{title}}
|
||||
</span>
|
||||
<span class="card-actions float-end" *ngIf="actions && actions.length > 0">
|
||||
<span class="card-actions float-end" *ngIf="actions && actions.length > 0">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
||||
</span>
|
||||
</div>
|
||||
@if (subtitleTemplate) {
|
||||
<div style="text-align: center">
|
||||
<ng-container [ngTemplateOutlet]="subtitleTemplate" [ngTemplateOutletContext]="{ $implicit: entity }"></ng-container>
|
||||
</div>
|
||||
}
|
||||
@if (!suppressLibraryLink && libraryName) {
|
||||
<a class="card-title library" [routerLink]="['/library', libraryId]" routerLinkActive="router-link-active">
|
||||
{{libraryName | sentenceCase}}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<span class="card-title library" [ngbTooltip]="subtitle" placement="top" *ngIf="subtitle.length > 0">{{subtitle}}</span>
|
||||
<a class="card-title library" [routerLink]="['/library', libraryId]" routerLinkActive="router-link-active" *ngIf="!suppressLibraryLink && libraryName">
|
||||
{{libraryName | sentenceCase}}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, DestroyRef,
|
||||
Component, ContentChild, DestroyRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output
|
||||
Output, TemplateRef
|
||||
} from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { DownloadEvent, DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { UserCollection } from 'src/app/_models/collection-tag';
|
||||
import { UserProgressUpdateEvent } from 'src/app/_models/events/user-progress-update-event';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { PageBookmark } from 'src/app/_models/readers/page-bookmark';
|
||||
|
|
@ -44,6 +44,8 @@ import {CardActionablesComponent} from "../../_single-module/card-actionables/ca
|
|||
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
|
||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
import {PromotedIconComponent} from "../../shared/_components/promoted-icon/promoted-icon.component";
|
||||
import {SeriesFormatComponent} from "../../shared/series-format/series-format.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-item',
|
||||
|
|
@ -62,7 +64,9 @@ import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
|||
RouterLink,
|
||||
TranslocoModule,
|
||||
SafeHtmlPipe,
|
||||
RouterLinkActive
|
||||
RouterLinkActive,
|
||||
PromotedIconComponent,
|
||||
SeriesFormatComponent
|
||||
],
|
||||
templateUrl: './card-item.component.html',
|
||||
styleUrls: ['./card-item.component.scss'],
|
||||
|
|
@ -81,6 +85,7 @@ export class CardItemComponent implements OnInit {
|
|||
private readonly scrollService = inject(ScrollService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
|
||||
/**
|
||||
|
|
@ -91,10 +96,6 @@ export class CardItemComponent implements OnInit {
|
|||
* Name of the card
|
||||
*/
|
||||
@Input() title = '';
|
||||
/**
|
||||
* Shows below the title. Defaults to not visible
|
||||
*/
|
||||
@Input() subtitle = '';
|
||||
/**
|
||||
* Any actions to perform on the card
|
||||
*/
|
||||
|
|
@ -114,7 +115,7 @@ export class CardItemComponent implements OnInit {
|
|||
/**
|
||||
* This is the entity we are representing. It will be returned if an action is executed.
|
||||
*/
|
||||
@Input({required: true}) entity!: Series | Volume | Chapter | CollectionTag | PageBookmark | RecentlyAddedItem | NextExpectedChapter;
|
||||
@Input({required: true}) entity!: Series | Volume | Chapter | UserCollection | PageBookmark | RecentlyAddedItem | NextExpectedChapter;
|
||||
/**
|
||||
* If the entity is selected or not.
|
||||
*/
|
||||
|
|
@ -147,6 +148,7 @@ export class CardItemComponent implements OnInit {
|
|||
* When the card is selected.
|
||||
*/
|
||||
@Output() selection = new EventEmitter<boolean>();
|
||||
@ContentChild('subtitle') subtitleTemplate!: TemplateRef<any>;
|
||||
/**
|
||||
* Library name item belongs to
|
||||
*/
|
||||
|
|
@ -351,7 +353,7 @@ export class CardItemComponent implements OnInit {
|
|||
|
||||
|
||||
isPromoted() {
|
||||
const tag = this.entity as CollectionTag;
|
||||
const tag = this.entity as UserCollection;
|
||||
return tag.hasOwnProperty('promoted') && tag.promoted;
|
||||
}
|
||||
|
||||
|
|
@ -378,5 +380,10 @@ export class CardItemComponent implements OnInit {
|
|||
// this.actions = this.actions.filter(a => a.title !== 'Send To');
|
||||
// }
|
||||
}
|
||||
|
||||
// this.actions = this.actions.filter(a => {
|
||||
// if (!a.isAllowed) return true;
|
||||
// return a.isAllowed(a, this.entity);
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@
|
|||
<ng-container>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('format-title')" [clickable]="true"
|
||||
[fontClasses]="'fa ' + (series.format | mangaFormatIcon)"
|
||||
[fontClasses]="series.format | mangaFormatIcon"
|
||||
(click)="handleGoTo(FilterField.Formats, series.format)" [title]="t('format-title')">
|
||||
{{series.format | mangaFormat}}
|
||||
</app-icon-and-title>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue