diff --git a/UI/Web/src/app/_models/actionables/action.ts b/UI/Web/src/app/_models/actionables/action.ts index abc86d4a6..702e8fc30 100644 --- a/UI/Web/src/app/_models/actionables/action.ts +++ b/UI/Web/src/app/_models/actionables/action.ts @@ -124,4 +124,6 @@ export enum Action { * A special action to just navigate somewhere */ Navigate = 38, + AddToDashboard = 39, + AddToSideNav = 40, } diff --git a/UI/Web/src/app/_models/wiki.ts b/UI/Web/src/app/_models/wiki.ts index a7ac3e0b5..912b3b4ca 100644 --- a/UI/Web/src/app/_models/wiki.ts +++ b/UI/Web/src/app/_models/wiki.ts @@ -17,8 +17,8 @@ export enum WikiLink { Scanner = 'https://wiki.kavitareader.com/guides/scanner', ScannerExclude = 'https://wiki.kavitareader.com/guides/admin-settings/libraries#exclude-patterns', Library = 'https://wiki.kavitareader.com/guides/admin-settings/libraries', - UpdateNative = 'https://wiki.kavitareader.com/guides/updating/updating-native', - UpdateDocker = 'https://wiki.kavitareader.com/guides/updating/updating-docker', + UpdateNative = 'https://wiki.kavitareader.com/guides/installation/native/', + UpdateDocker = 'https://wiki.kavitareader.com/guides/installation/docker/', OpdsClients = 'https://wiki.kavitareader.com/guides/features/opds/#opds-capable-clients', Guides = 'https://wiki.kavitareader.com/guides', ReadingProfiles = "https://wiki.kavitareader.com/guides/user-settings/reading-profiles/", diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index f43bc1c49..f7d190b63 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -1211,6 +1211,40 @@ export class ActionFactoryService { ]; this.smartFilterActions = [ + { + action: Action.Submenu, + title: 'add-to', + description: '', + + callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, + + requiredRoles: [], + children: [ + { + action: Action.AddToDashboard, + title: 'add-to-dashboard', + description: 'add-to-dashboard-tooltip', + + callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, + + requiredRoles: [], + children: [], + }, + { + action: Action.AddToSideNav, + title: 'add-to-side-nav', + description: 'add-to-side-nav-tooltip', + + callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, + + requiredRoles: [], + children: [], + }, + ], + }, { action: Action.Edit, title: 'rename', diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index e7101310c..516feabe8 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -63,6 +63,7 @@ import {ModalResult} from "../_models/modal/modal-result"; import {addToModal, editModal} from "../_models/modal/modal-options"; import {ModalService, TypedModalRef} from "./modal.service"; import {FilterService} from "src/app/_services/filter.service"; +import {DashboardService} from "./dashboard.service"; export type LibraryActionCallback = (library: Partial) => void; @@ -101,6 +102,7 @@ export class ActionService { private readonly annotationsService = inject(AnnotationService); private readonly sideNavService = inject(NavService); private readonly filterService = inject(FilterService); + private readonly dashboardService = inject(DashboardService); private readingListModalRef: TypedModalRef | TypedModalRef> | null = null; private collectionModalRef: TypedModalRef> | null = null; @@ -821,6 +823,14 @@ export class ActionService { */ handleSmartFilterAction(action: ActionItem, smartFilter: SmartFilter, allFilters: SmartFilter[]) { switch (action.action) { + case Action.AddToDashboard: + return this.dashboardService.createDashboardStream(smartFilter.id).pipe( + map(() => this.fromAction(action, smartFilter, 'none')) + ); + case Action.AddToSideNav: + return this.sideNavService.createSideNavStream(smartFilter.id).pipe( + map(() => this.fromAction(action, smartFilter, 'none')) + ); case Action.Edit: const ref = this.modalService.open(EditSmartFilterModalComponent, editModal()); ref.componentInstance.smartFilter = smartFilter; diff --git a/UI/Web/src/app/dashboard/_components/dashboard.component.ts b/UI/Web/src/app/dashboard/_components/dashboard.component.ts index 722201acb..0d22e02d9 100644 --- a/UI/Web/src/app/dashboard/_components/dashboard.component.ts +++ b/UI/Web/src/app/dashboard/_components/dashboard.component.ts @@ -302,7 +302,20 @@ export class DashboardComponent { } async handleFilterSectionClick(stream: DashboardStream) { - await this.router.navigateByUrl('all-series?' + stream.smartFilterEncoded); + switch (stream.entityType) { + case FilterEntityType.Series: + await this.router.navigateByUrl('all-series?' + stream.smartFilterEncoded); + break; + case FilterEntityType.ReadingList: + await this.router.navigateByUrl('lists?' + stream.smartFilterEncoded); + break; + case FilterEntityType.Annotation: + await this.router.navigateByUrl('browse/annotations?' + stream.smartFilterEncoded); + break; + case FilterEntityType.Person: + await this.router.navigateByUrl('browse/people?' + stream.smartFilterEncoded); + break; + } } // TODO: See if we can put this into the carousel and have a custom tokens (not in the original list) to forward to a callback handler diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html index 0068c495c..4645df45a 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html @@ -51,10 +51,10 @@ @let normalizedSearchTerm = searchTerm.toLowerCase().trim(); - @if (item.name.toLowerCase().trim().indexOf(normalizedSearchTerm) >= 0) { - {{item.name}} + @if (item.localizedName.toLowerCase().trim().indexOf(normalizedSearchTerm) >= 0) { + {{item.localizedName}} } @else { - + {{item.name}} }
in {{item.libraryName}}
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 c97b99993..f869e4ff8 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 @@ -4,7 +4,7 @@ import { computed, DestroyRef, inject, - input, + input, OnInit, signal, TemplateRef, viewChild @@ -17,7 +17,7 @@ import {APP_BASE_HREF, AsyncPipe} from "@angular/common"; 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, tap} from "rxjs"; +import {filter, Observable, switchMap, tap} from "rxjs"; import {SeriesService} from "../../../_services/series.service"; import {QueryContext} from "../../../_models/metadata/v2/query-context"; import {map, shareReplay} from "rxjs/operators"; @@ -34,6 +34,12 @@ import {CardConfigFactory} from "../../../_services/card-config-factory.service" import {EntityCardComponent} from "../../../cards/entity-card/entity-card.component"; import {PromotedIconComponent} from "../../../shared/_components/promoted-icon/promoted-icon.component"; import {CardEntity, CardEntityFactory} from "../../../_models/card/card-entity"; +import {Action} from "../../../_models/actionables/action"; +import {User} from "../../../_models/user/user"; +import {ActionItem} from "../../../_models/actionables/action-item"; +import {EVENTS, MessageHubService} from "../../../_services/message-hub.service"; +import {DashboardService} from "../../../_services/dashboard.service"; +import {NavService} from "../../../_services/nav.service"; @Component({ selector: 'app-manage-smart-filters', @@ -42,7 +48,7 @@ import {CardEntity, CardEntityFactory} from "../../../_models/card/card-entity"; styleUrls: ['./manage-smart-filters.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ManageSmartFiltersComponent { +export class ManageSmartFiltersComponent implements OnInit { private readonly filterService = inject(FilterService); private readonly filterUtilityService = inject(FilterUtilitiesService); @@ -53,6 +59,9 @@ export class ManageSmartFiltersComponent { private readonly actionFactoryService = inject(ActionFactoryService); private readonly destroyRef = inject(DestroyRef); private readonly cardConfigFactory = inject(CardConfigFactory); + private readonly messageHub = inject(MessageHubService); + private readonly dashboardService = inject(DashboardService); + private readonly sideNavService = inject(NavService); protected readonly baseUrl = inject(APP_BASE_HREF); target = input<'_self' | '_blank'>('_blank'); @@ -77,9 +86,11 @@ export class ManageSmartFiltersComponent { 'entityType': new FormControl(FilterEntityType.Series, []), }); protected readonly filterApiMap = signal<{ [key: number]: Observable }>({}); - protected readonly actions = computed(() => this.actionFactoryService.getSmartFilterActions(this.filters())); + protected readonly actions = computed(() => this.actionFactoryService.getSmartFilterActions(this.filters(), this.shouldRenderFunc.bind(this))); protected readonly filterQuery = signal(''); protected readonly filterEntityType = signal(FilterEntityType.Series); + private readonly dashboardFilters = signal>(new Set()); + private readonly sideNavFilters = signal>(new Set()); protected titleTemplateRef = viewChild>('title'); protected readonly readingListConfig = computed(() => this.cardConfigFactory.forReadingList({titleRef: this.titleTemplateRef(), overrides: {allowSelection: false, actionableFunc: () => []}})); @@ -96,12 +107,44 @@ export class ManageSmartFiltersComponent { tap(val => this.filterEntityType.set(parseInt(val + '', 10))) ).subscribe(); + this.messageHub.messages$.pipe( + tap(msg => { + if (msg.event === EVENTS.SideNavUpdate) { + this.sideNavService.getSideNavStreams(true).subscribe(streams => { + this.sideNavFilters.set(new Set(streams.map(stream => stream.id))); + }); + } else if (msg.event === EVENTS.DashboardUpdate) { + this.dashboardService.getDashboardStreams(true).subscribe(streams => { + this.dashboardFilters.set(new Set(streams.map(stream => stream.id))); + }); + } + }), + ).subscribe(); + } + + ngOnInit() { + this.sideNavService.getSideNavStreams(true).subscribe(streams => { + this.sideNavFilters.set(new Set(streams.map(stream => stream.smartFilterId))); + }); + this.dashboardService.getDashboardStreams(true).subscribe(streams => { + this.dashboardFilters.set(new Set(streams.map(stream => stream.smartFilterId))); + }); } getFilterLink(filter: SmartFilter) { return this.baseUrl + FilterUtilitiesService.getFilterLink(filter.entityType, filter.filter); } + shouldRenderFunc(action: ActionItem, item: SmartFilter, user: User) { + switch (action.action) { + case Action.AddToDashboard: + return !this.dashboardFilters().has(item.id); + case Action.AddToSideNav: + return !this.sideNavFilters().has(item.id); + } + + return true; + } loadData() { this.filterService.getAllFilters().subscribe(filters => { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 1c8b1f4bf..ca86fafbe 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -3826,7 +3826,11 @@ "export-v2": "CBL v2", "export-v2-tooltip": "Use CBL v2 (JSON)", "cbl-manager": "CBL Manager", - "cbl-manager-tooltip": "Manage/Sync CBLs" + "cbl-manager-tooltip": "Manage/Sync CBLs", + "add-to-dashboard": "Add to Dashboard", + "add-to-dashboard-tooltip": "Adds the smart filter to the bottom of your Dashboard", + "add-to-side-nav": "Add to Side Nav", + "add-to-side-nav-tooltip": "Adds the smart filter to the bottom of your Side Nav" }, "preferences": {