Collection Rework (#2830)
This commit is contained in:
parent
0dacc061f1
commit
deaaccb96a
93 changed files with 5413 additions and 1120 deletions
|
|
@ -3,6 +3,7 @@
|
|||
<h2 title>{{t('title')}}</h2>
|
||||
<h6 subtitle>{{t('item-count', {num: collections.length | number})}}</h6>
|
||||
</app-side-nav-companion-bar>
|
||||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<app-card-detail-layout
|
||||
[isLoading]="isLoading"
|
||||
[items]="collections"
|
||||
|
|
@ -13,12 +14,21 @@
|
|||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions"
|
||||
[imageUrl]="imageService.getCollectionCoverImage(item.id)"
|
||||
(clicked)="loadCollection(item)"></app-card-item>
|
||||
(clicked)="loadCollection(item)"
|
||||
(selection)="bulkSelectionService.handleCardSelection('collection', position, collections.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('collection', position)" [allowSelection]="true">
|
||||
|
||||
<ng-template #subtitle>
|
||||
<app-collection-owner [collection]="item"></app-collection-owner>
|
||||
</ng-template>
|
||||
</app-card-item>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #noData>
|
||||
{{t('no-data')}}
|
||||
<ng-container *ngIf="isAdmin$ | async"> {{t('create-one-part-1')}} <a href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/collections" rel="noopener noreferrer" target="_blank">{{t('create-one-part-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a></ng-container>
|
||||
@if(isAdmin$ | async) {
|
||||
{{t('create-one-part-1')}} <a href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/collections" rel="noopener noreferrer" target="_blank">{{t('create-one-part-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
|
||||
}
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
Component,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
|
|
@ -13,7 +14,7 @@ import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
|||
import {map, of} from 'rxjs';
|
||||
import {Observable} from 'rxjs/internal/Observable';
|
||||
import {EditCollectionTagsComponent} from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
||||
import {CollectionTag} from 'src/app/_models/collection-tag';
|
||||
import {UserCollection} from 'src/app/_models/collection-tag';
|
||||
import {JumpKey} from 'src/app/_models/jumpbar/jump-key';
|
||||
import {Tag} from 'src/app/_models/tag';
|
||||
import {AccountService} from 'src/app/_services/account.service';
|
||||
|
|
@ -22,14 +23,24 @@ import {CollectionTagService} from 'src/app/_services/collection-tag.service';
|
|||
import {ImageService} from 'src/app/_services/image.service';
|
||||
import {JumpbarService} from 'src/app/_services/jumpbar.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {AsyncPipe, DecimalPipe, NgIf} from '@angular/common';
|
||||
import {AsyncPipe, DecimalPipe} from '@angular/common';
|
||||
import {CardItemComponent} from '../../../cards/card-item/card-item.component';
|
||||
import {CardDetailLayoutComponent} from '../../../cards/card-detail-layout/card-detail-layout.component';
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
||||
import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
|
||||
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
||||
import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe";
|
||||
import {CollectionOwnerComponent} from "../collection-owner/collection-owner.component";
|
||||
import {User} from "../../../_models/user";
|
||||
import {BulkOperationsComponent} from "../../../cards/bulk-operations/bulk-operations.component";
|
||||
import {BulkSelectionService} from "../../../cards/bulk-selection.service";
|
||||
import {SeriesCardComponent} from "../../../cards/series-card/series-card.component";
|
||||
import {ActionService} from "../../../_services/action.service";
|
||||
import {KEY_CODES} from "../../../shared/_services/utility.service";
|
||||
|
||||
|
||||
@Component({
|
||||
|
|
@ -38,51 +49,87 @@ import {ToastrService} from "ngx-toastr";
|
|||
styleUrls: ['./all-collections.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [SideNavCompanionBarComponent, CardDetailLayoutComponent, CardItemComponent, NgIf, AsyncPipe, DecimalPipe, TranslocoDirective]
|
||||
imports: [SideNavCompanionBarComponent, CardDetailLayoutComponent, CardItemComponent, AsyncPipe, DecimalPipe, TranslocoDirective, ProviderImagePipe, ProviderNamePipe, CollectionOwnerComponent, BulkOperationsComponent, SeriesCardComponent]
|
||||
})
|
||||
export class AllCollectionsComponent implements OnInit {
|
||||
|
||||
isLoading: boolean = true;
|
||||
collections: CollectionTag[] = [];
|
||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||
jumpbarKeys: Array<JumpKey> = [];
|
||||
trackByIdentity = (index: number, item: CollectionTag) => `${item.id}_${item.title}`;
|
||||
isAdmin$: Observable<boolean> = of(false);
|
||||
|
||||
|
||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly collectionService = inject(CollectionTagService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
private readonly modalService = inject(NgbModal);
|
||||
private readonly titleService = inject(Title);
|
||||
private readonly jumpbarService = inject(JumpbarService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
public readonly imageService = inject(ImageService);
|
||||
public readonly accountService = inject(AccountService);
|
||||
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
public readonly actionService = inject(ActionService);
|
||||
|
||||
constructor(private collectionService: CollectionTagService, private router: Router,
|
||||
private actionFactoryService: ActionFactoryService, private modalService: NgbModal,
|
||||
private titleService: Title, private jumpbarService: JumpbarService,
|
||||
private readonly cdRef: ChangeDetectorRef, public imageService: ImageService,
|
||||
public accountService: AccountService) {
|
||||
protected readonly ScrobbleProvider = ScrobbleProvider;
|
||||
|
||||
isLoading: boolean = true;
|
||||
collections: UserCollection[] = [];
|
||||
collectionTagActions: ActionItem<UserCollection>[] = [];
|
||||
jumpbarKeys: Array<JumpKey> = [];
|
||||
isAdmin$: Observable<boolean> = of(false);
|
||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
trackByIdentity = (index: number, item: UserCollection) => `${item.id}_${item.title}`;
|
||||
user!: User;
|
||||
|
||||
@HostListener('document:keydown.shift', ['$event'])
|
||||
handleKeypress(event: KeyboardEvent) {
|
||||
if (event.key === KEY_CODES.SHIFT) {
|
||||
this.bulkSelectionService.isShiftDown = true;
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keyup.shift', ['$event'])
|
||||
handleKeyUp(event: KeyboardEvent) {
|
||||
if (event.key === KEY_CODES.SHIFT) {
|
||||
this.bulkSelectionService.isShiftDown = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
constructor() {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.titleService.setTitle('Kavita - ' + this.translocoService.translate('all-collections.title'));
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||
if (user) {
|
||||
this.user = user;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadPage();
|
||||
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this));
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||
if (!user) return;
|
||||
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this))
|
||||
.filter(action => this.collectionService.actionListFilter(action, user));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), map(user => {
|
||||
if (!user) return false;
|
||||
return this.accountService.hasAdminRole(user);
|
||||
}));
|
||||
}
|
||||
|
||||
loadCollection(item: CollectionTag) {
|
||||
loadCollection(item: UserCollection) {
|
||||
this.router.navigate(['collections', item.id]);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
this.collectionService.allCollections().subscribe(tags => {
|
||||
this.collections = [...tags];
|
||||
this.isLoading = false;
|
||||
this.jumpbarKeys = this.jumpbarService.getJumpKeys(tags, (t: Tag) => t.title);
|
||||
|
|
@ -90,8 +137,20 @@ export class AllCollectionsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
handleCollectionActionCallback(action: ActionItem<CollectionTag>, collectionTag: CollectionTag) {
|
||||
handleCollectionActionCallback(action: ActionItem<UserCollection>, collectionTag: UserCollection) {
|
||||
|
||||
if (collectionTag.owner != this.user.username) {
|
||||
this.toastr.error(translate('toasts.collection-not-owned'));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action.action) {
|
||||
case Action.Promote:
|
||||
this.collectionService.promoteMultipleCollections([collectionTag.id], true).subscribe();
|
||||
break;
|
||||
case Action.UnPromote:
|
||||
this.collectionService.promoteMultipleCollections([collectionTag.id], false).subscribe();
|
||||
break;
|
||||
case(Action.Delete):
|
||||
this.collectionService.deleteTag(collectionTag.id).subscribe(res => {
|
||||
this.toastr.success(res);
|
||||
|
|
@ -110,4 +169,33 @@ export class AllCollectionsComponent implements OnInit {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bulkActionCallback = (action: ActionItem<any>, data: any) => {
|
||||
const selectedCollectionIndexies = this.bulkSelectionService.getSelectedCardsForSource('collection');
|
||||
const selectedCollections = this.collections.filter((col, index: number) => selectedCollectionIndexies.includes(index + ''));
|
||||
|
||||
switch (action.action) {
|
||||
case Action.Promote:
|
||||
this.actionService.promoteMultipleCollections(selectedCollections, true, (success) => {
|
||||
if (!success) return;
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
});
|
||||
break;
|
||||
case Action.UnPromote:
|
||||
this.actionService.promoteMultipleCollections(selectedCollections, false, (success) => {
|
||||
if (!success) return;
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
});
|
||||
break;
|
||||
case Action.Delete:
|
||||
this.actionService.deleteMultipleCollections(selectedCollections, (successful) => {
|
||||
if (!successful) return;
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {EditCollectionTagsComponent} from 'src/app/cards/_modals/edit-collection
|
|||
import {FilterSettings} from 'src/app/metadata-filter/filter-settings';
|
||||
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
|
||||
import {KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {CollectionTag} from 'src/app/_models/collection-tag';
|
||||
import {UserCollection} from 'src/app/_models/collection-tag';
|
||||
import {SeriesAddedToCollectionEvent} from 'src/app/_models/events/series-added-to-collection-event';
|
||||
import {JumpKey} from 'src/app/_models/jumpbar/jump-key';
|
||||
import {Pagination} from 'src/app/_models/pagination';
|
||||
|
|
@ -52,6 +52,8 @@ import {CardActionablesComponent} from "../../../_single-module/card-actionables
|
|||
import {FilterField} from "../../../_models/metadata/v2/filter-field";
|
||||
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
|
||||
import {SeriesFilterV2} from "../../../_models/metadata/v2/series-filter-v2";
|
||||
import {AccountService} from "../../../_services/account.service";
|
||||
import {User} from "../../../_models/user";
|
||||
|
||||
@Component({
|
||||
selector: 'app-collection-detail',
|
||||
|
|
@ -63,20 +65,41 @@ import {SeriesFilterV2} from "../../../_models/metadata/v2/series-filter-v2";
|
|||
})
|
||||
export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
public readonly imageService = inject(ImageService);
|
||||
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
private readonly collectionService = inject(CollectionTagService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly modalService = inject(NgbModal);
|
||||
private readonly titleService = inject(Title);
|
||||
private readonly jumpbarService = inject(JumpbarService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly messageHub = inject(MessageHubService);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
private readonly utilityService = inject(UtilityService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly scrollService = inject(ScrollService);
|
||||
|
||||
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
|
||||
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
|
||||
|
||||
destroyRef = inject(DestroyRef);
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
collectionTag!: CollectionTag;
|
||||
|
||||
collectionTag!: UserCollection;
|
||||
isLoading: boolean = true;
|
||||
series: Array<Series> = [];
|
||||
pagination: Pagination = new Pagination();
|
||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||
collectionTagActions: ActionItem<UserCollection>[] = [];
|
||||
filter: SeriesFilterV2 | undefined = undefined;
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
summary: string = '';
|
||||
user!: User;
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
filterActiveCheck!: SeriesFilterV2;
|
||||
|
|
@ -153,12 +176,8 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
|||
return 'calc(var(--vh)*100 - ' + totalHeight + 'px)';
|
||||
}
|
||||
|
||||
constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute,
|
||||
private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService,
|
||||
private modalService: NgbModal, private titleService: Title, private jumpbarService: JumpbarService,
|
||||
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
|
||||
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
|
||||
private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService) {
|
||||
|
||||
constructor(@Inject(DOCUMENT) private document: Document) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
|
||||
const routeId = this.route.snapshot.paramMap.get('id');
|
||||
|
|
@ -184,7 +203,14 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this));
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||
if (!user) return;
|
||||
this.user = user;
|
||||
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this))
|
||||
.filter(action => this.collectionService.actionListFilter(action, user));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef), debounceTime(2000)).subscribe(event => {
|
||||
if (event.event == EVENTS.SeriesAddedToCollection) {
|
||||
|
|
@ -217,11 +243,10 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
|||
}
|
||||
|
||||
updateTag(tagId: number) {
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
this.collectionService.allCollections().subscribe(tags => {
|
||||
const matchingTags = tags.filter(t => t.id === tagId);
|
||||
if (matchingTags.length === 0) {
|
||||
this.toastr.error(this.translocoService.translate('errors.collection-invalid-access'));
|
||||
// TODO: Why would access need to be checked? Even if a id was guessed, the series wouldn't return
|
||||
this.router.navigateByUrl('/');
|
||||
return;
|
||||
}
|
||||
|
|
@ -261,8 +286,18 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
|||
});
|
||||
}
|
||||
|
||||
handleCollectionActionCallback(action: ActionItem<CollectionTag>, collectionTag: CollectionTag) {
|
||||
handleCollectionActionCallback(action: ActionItem<UserCollection>, collectionTag: UserCollection) {
|
||||
if (collectionTag.owner != this.user.username) {
|
||||
this.toastr.error(translate('toasts.collection-not-owned'));
|
||||
return;
|
||||
}
|
||||
switch (action.action) {
|
||||
case Action.Promote:
|
||||
this.collectionService.promoteMultipleCollections([this.collectionTag.id], true).subscribe();
|
||||
break;
|
||||
case Action.UnPromote:
|
||||
this.collectionService.promoteMultipleCollections([this.collectionTag.id], false).subscribe();
|
||||
break;
|
||||
case(Action.Edit):
|
||||
this.openEditCollectionTagModal(this.collectionTag);
|
||||
break;
|
||||
|
|
@ -283,7 +318,7 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
|||
}
|
||||
}
|
||||
|
||||
openEditCollectionTagModal(collectionTag: CollectionTag) {
|
||||
openEditCollectionTagModal(collectionTag: UserCollection) {
|
||||
const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true });
|
||||
modalRef.componentInstance.tag = this.collectionTag;
|
||||
modalRef.closed.subscribe((results: {success: boolean, coverImageUpdated: boolean}) => {
|
||||
|
|
@ -291,6 +326,4 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
|||
this.loadPage();
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly undefined = undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<ng-container *transloco="let t; read: 'collection-owner'">
|
||||
@if (accountService.currentUser$ | async; as user) {
|
||||
<div class="fw-light text-accent">
|
||||
{{t('collection-created-label', {owner: collection.owner})}}
|
||||
@if(collection.source !== ScrobbleProvider.Kavita) {
|
||||
{{t('collection-via-label')}}
|
||||
<app-image [imageUrl]="collection.source | providerImage"
|
||||
width="16px" height="16px"
|
||||
[ngbTooltip]="collection.source | providerName"
|
||||
[attr.aria-label]="collection.source | providerName"></app-image>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.text-accent {
|
||||
font-size: small;
|
||||
color: var(---accent-text-color);
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core';
|
||||
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
|
||||
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
||||
import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe";
|
||||
import {UserCollection} from "../../../_models/collection-tag";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {AsyncPipe, JsonPipe} from "@angular/common";
|
||||
import {AccountService} from "../../../_services/account.service";
|
||||
import {ImageComponent} from "../../../shared/image/image.component";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
|
||||
@Component({
|
||||
selector: 'app-collection-owner',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ProviderImagePipe,
|
||||
ProviderNamePipe,
|
||||
TranslocoDirective,
|
||||
AsyncPipe,
|
||||
JsonPipe,
|
||||
ImageComponent,
|
||||
NgbTooltip
|
||||
],
|
||||
templateUrl: './collection-owner.component.html',
|
||||
styleUrl: './collection-owner.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CollectionOwnerComponent {
|
||||
|
||||
protected readonly accountService = inject(AccountService);
|
||||
|
||||
protected readonly ScrobbleProvider = ScrobbleProvider;
|
||||
|
||||
@Input({required: true}) collection!: UserCollection;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue