diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 131e20cac..6d2f7053e 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -870,6 +870,14 @@ export class ActionFactoryService { ]; this.smartFilterActions = [ + { + action: Action.Edit, + title: 'rename', + description: 'rename-tooltip', + callback: this.dummyCallback, + requiresAdmin: false, + children: [], + }, { action: Action.Delete, title: 'delete', diff --git a/UI/Web/src/app/all-filters/all-filters.component.html b/UI/Web/src/app/all-filters/all-filters.component.html index 2f487574a..7b502232b 100644 --- a/UI/Web/src/app/all-filters/all-filters.component.html +++ b/UI/Web/src/app/all-filters/all-filters.component.html @@ -9,9 +9,7 @@ {{t('count', {count: filters.length | number})}} {{t('create')}} - - diff --git a/UI/Web/src/app/all-filters/all-filters.component.ts b/UI/Web/src/app/all-filters/all-filters.component.ts index e41b2a83e..40da8e10b 100644 --- a/UI/Web/src/app/all-filters/all-filters.component.ts +++ b/UI/Web/src/app/all-filters/all-filters.component.ts @@ -9,7 +9,7 @@ import {FilterService} from "../_services/filter.service"; import {Router} from "@angular/router"; import {Series} from "../_models/series"; import {JumpbarService} from "../_services/jumpbar.service"; -import {Action, ActionFactoryService, ActionItem} from "../_services/action-factory.service"; +import {ActionFactoryService} from "../_services/action-factory.service"; import {ActionService} from "../_services/action.service"; import {ManageSmartFiltersComponent} from "../sidenav/_components/manage-smart-filters/manage-smart-filters.component"; import {DecimalPipe} from "@angular/common"; @@ -29,7 +29,7 @@ export class AllFiltersComponent implements OnInit { private readonly actionFactory = inject(ActionFactoryService); private readonly actionService = inject(ActionService); - filterActions = this.actionFactory.getSmartFilterActions(this.handleAction.bind(this)); + jumpbarKeys: Array = []; filters: SmartFilter[] = []; isLoading = true; @@ -46,22 +46,4 @@ export class AllFiltersComponent implements OnInit { this.cdRef.markForCheck(); }); } - - async deleteFilter(filter: SmartFilter) { - await this.actionService.deleteFilter(filter.id, success => { - this.filters = this.filters.filter(f => f.id != filter.id); - this.jumpbarKeys = this.jumpbarService.getJumpKeys(this.filters, (s: Series) => s.name); - this.cdRef.markForCheck(); - }); - } - - async handleAction(action: ActionItem, filter: SmartFilter) { - switch (action.action) { - case(Action.Delete): - await this.deleteFilter(filter); - break; - default: - break; - } - } } diff --git a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.html b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.html index 157825782..85cd9bc8f 100644 --- a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.html +++ b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.html @@ -3,8 +3,11 @@ @if (alwaysShow || items && items.length > 0) { + @if (actionables.length > 0) { + + } - {{title}} + {{title}} @if (iconClasses !== '') { } diff --git a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts index c28e3ee92..043b87482 100644 --- a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts +++ b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts @@ -9,17 +9,19 @@ import { Output, TemplateRef } from '@angular/core'; -import { Swiper, SwiperEvents } from 'swiper/types'; -import { SwiperModule } from 'swiper/angular'; -import { NgClass, NgTemplateOutlet } from '@angular/common'; +import {Swiper, SwiperEvents} from 'swiper/types'; +import {SwiperModule} from 'swiper/angular'; +import {NgClass, NgTemplateOutlet} from '@angular/common'; import {TranslocoDirective} from "@jsverse/transloco"; +import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component"; +import {ActionItem} from "../../../_services/action-factory.service"; @Component({ selector: 'app-carousel-reel', templateUrl: './carousel-reel.component.html', styleUrls: ['./carousel-reel.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgClass, SwiperModule, NgTemplateOutlet, TranslocoDirective] + imports: [NgClass, SwiperModule, NgTemplateOutlet, TranslocoDirective, CardActionablesComponent] }) export class CarouselReelComponent { @@ -29,6 +31,10 @@ export class CarouselReelComponent { @ContentChild('promptToAdd') promptToAddTemplate!: TemplateRef; @Input() items: any[] = []; @Input() title = ''; + /** + * If provided, will render the title as an anchor + */ + @Input() titleLink = ''; @Input() clickableTitle: boolean = true; @Input() iconClasses = ''; /** @@ -39,7 +45,12 @@ export class CarouselReelComponent { * Track by identity. By default, this has an implementation based on title, item's name, pagesRead, and index */ @Input() trackByIdentity: (index: number, item: any) => string = (index: number, item: any) => `${this.title}_${item.id}_${item?.name}_${item?.pagesRead}_${index}`; + /** + * Actionables to render to the left of the title + */ + @Input() actionables: Array> = []; @Output() sectionClick = new EventEmitter(); + @Output() handleAction = new EventEmitter>(); swiper: Swiper | undefined; @@ -67,4 +78,8 @@ export class CarouselReelComponent { [this.swiper] = eventParams; this.cdRef.detectChanges(); } + + performAction(action: ActionItem) { + this.handleAction.emit(action); + } } diff --git a/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.html b/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.html index c78737bcb..890161f41 100644 --- a/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.html +++ b/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.html @@ -1,6 +1,6 @@ - @if (filters.length >= 3) { + @if (filters.length >= 5) { {{t('filter')}} @@ -12,27 +12,24 @@ - @for(f of filters | filter: filterList; track f.name) { - - - @if (isErrored(f)) { - - {{t('errored')}} - } - {{f.name}} - - - - - {{t('edit')}} - + @for(stream of filters | filter: filterList; track stream.name) { - - - {{t('delete')}} - - - + @if (isErrored(stream)) { + + + {{t('errored')}} + + {{stream.name}} - {{t('errored')}} + + } @else { + @if(filterApiMap[stream.name] | async; as data) { + + + + + + } + } } @empty { {{t('no-data')}} diff --git a/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.ts b/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.ts index e7f44df8a..f202f01eb 100644 --- a/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.ts +++ b/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.ts @@ -5,24 +5,34 @@ import {TranslocoDirective} from "@jsverse/transloco"; import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; import {FilterPipe} from "../../../_pipes/filter.pipe"; import {ActionService} from "../../../_services/action.service"; -import {NgbModal, NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; -import {RouterLink} from "@angular/router"; -import {APP_BASE_HREF} from "@angular/common"; +import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; +import {APP_BASE_HREF, AsyncPipe} from "@angular/common"; import {EditSmartFilterModalComponent} from "../edit-smart-filter-modal/edit-smart-filter-modal.component"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {CarouselReelComponent} from "../../../carousel/_components/carousel-reel/carousel-reel.component"; +import {SeriesCardComponent} from "../../../cards/series-card/series-card.component"; +import {Observable, switchMap} from "rxjs"; +import {SeriesService} from "../../../_services/series.service"; +import {QueryContext} from "../../../_models/metadata/v2/query-context"; +import {map, shareReplay} from "rxjs/operators"; +import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service"; +import {Action, ActionFactoryService, ActionItem} from "../../../_services/action-factory.service"; @Component({ - selector: 'app-manage-smart-filters', - imports: [ReactiveFormsModule, TranslocoDirective, FilterPipe, NgbTooltip], - templateUrl: './manage-smart-filters.component.html', - styleUrls: ['./manage-smart-filters.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'app-manage-smart-filters', + imports: [ReactiveFormsModule, TranslocoDirective, FilterPipe, CarouselReelComponent, SeriesCardComponent, AsyncPipe], + templateUrl: './manage-smart-filters.component.html', + styleUrls: ['./manage-smart-filters.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ManageSmartFiltersComponent { private readonly filterService = inject(FilterService); + private readonly filterUtilityService = inject(FilterUtilitiesService); + private readonly seriesService = inject(SeriesService); private readonly cdRef = inject(ChangeDetectorRef); private readonly actionService = inject(ActionService); + private readonly actionFactoryService = inject(ActionFactoryService); private readonly destroyRef = inject(DestroyRef); private readonly modelService = inject(NgbModal); protected readonly baseUrl = inject(APP_BASE_HREF); @@ -33,6 +43,8 @@ export class ManageSmartFiltersComponent { listForm: FormGroup = new FormGroup({ 'filterQuery': new FormControl('', []) }); + filterApiMap: { [key: string]: Observable } = {}; + actions: Array> = this.actionFactoryService.getSmartFilterActions(this.handleAction.bind(this)); filterList = (listItem: SmartFilter) => { const filterVal = (this.listForm.value.filterQuery || '').toLowerCase(); @@ -46,6 +58,16 @@ export class ManageSmartFiltersComponent { loadData() { this.filterService.getAllFilters().subscribe(filters => { this.filters = filters; + + this.filterApiMap = {}; + for(let filter of filters) { + this.filterApiMap[filter.name] = this.filterUtilityService.decodeFilter(filter.filter).pipe( + switchMap(filter => { + return this.seriesService.getAllSeriesV2(0, 20, filter, QueryContext.Dashboard); + })) + .pipe(map(d => d.result), takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true})); + } + this.cdRef.markForCheck(); }); } @@ -59,6 +81,17 @@ export class ManageSmartFiltersComponent { return !decodeURIComponent(filter.filter).includes('¦'); } + handleAction(action: ActionItem, smartFilter: SmartFilter) { + switch (action.action) { + case Action.Edit: + this.editFilter(smartFilter); + break; + case Action.Delete: + this.deleteFilter(smartFilter); + break; + } + } + async deleteFilter(f: SmartFilter) { await this.actionService.deleteFilter(f.id, success => { if (!success) return; diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index dc409848f..f9221bea9 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -2757,7 +2757,9 @@ "copy-settings": "Copy Settings From", "match": "Match", "match-tooltip": "Match Series with Kavita+ manually", - "reorder": "Reorder" + "reorder": "Reorder", + "rename": "Rename", + "rename-tooltip": "Rename the Smart Filter" }, "preferences": {