diff --git a/src/components/InfoHeader.vue b/src/components/InfoHeader.vue index e78a204c9..7fd6c9c6a 100644 --- a/src/components/InfoHeader.vue +++ b/src/components/InfoHeader.vue @@ -377,6 +377,9 @@ outlined class="cursor-pointer" @click="handleMediaItemClick(genre, 0, 0)" + @contextmenu.prevent=" + (e: MouseEvent) => showGenreChipContextMenu(e, genre) + " > {{ getGenreDisplayName(genre.name, genre.translation_key, t, te) @@ -506,6 +509,38 @@ watch( { immediate: true }, ); +const showGenreChipContextMenu = (evt: MouseEvent, genre: Genre) => { + if ( + !compProps.item || + !isAdmin.value || + compProps.item.provider !== "library" + ) + return; + const mediaItem = compProps.item; + const menuItems: ContextMenuItem[] = [ + { + label: "exclude_genre", + icon: "mdi-cancel", + action: async () => { + await api.excludeGenreFromItem( + genre.item_id, + mediaItem.media_type, + mediaItem.item_id, + ); + mappedGenres.value = mappedGenres.value.filter( + (g) => g.item_id !== genre.item_id, + ); + eventbus.emit("genreExcluded"); + }, + }, + ]; + eventbus.emit("contextmenu", { + items: menuItems, + posX: evt.clientX, + posY: evt.clientY, + }); +}; + const albumClick = function (item: Album | ItemMapping) { // album entry clicked router.push({ diff --git a/src/components/ProviderDetails.vue b/src/components/ProviderDetails.vue index e6a8b5eff..9f119d7d4 100644 --- a/src/components/ProviderDetails.vue +++ b/src/components/ProviderDetails.vue @@ -125,10 +125,19 @@ + diff --git a/src/plugins/api/index.ts b/src/plugins/api/index.ts index ee45791bb..206c92858 100644 --- a/src/plugins/api/index.ts +++ b/src/plugins/api/index.ts @@ -1033,6 +1033,40 @@ export class MusicAssistantApi { }); } + public excludeGenreFromItem( + genre_id: string, + media_type: string, + media_id: string, + ): Promise { + return this.sendCommand("music/genres/exclude_genre_from_media_item", { + genre_id, + media_type, + media_id, + }); + } + + public removeGenreExclusion( + genre_id: string, + media_type: string, + media_id: string, + ): Promise { + return this.sendCommand("music/genres/remove_genre_exclusion", { + genre_id, + media_type, + media_id, + }); + } + + public getGenreExclusionsForItem( + media_type: string, + media_id: string, + ): Promise { + return this.sendCommand("music/genres/genre_exclusions_for_media_item", { + media_type, + media_id, + }); + } + public async getGenreOverviewRows( item_id: string, provider_instance_id_or_domain: string, diff --git a/src/plugins/eventbus.ts b/src/plugins/eventbus.ts index baaaa8f80..92b5440f7 100644 --- a/src/plugins/eventbus.ts +++ b/src/plugins/eventbus.ts @@ -43,6 +43,7 @@ export type Events = { deleteGenreDialog: DeleteGenreDialogEvent; linkGenreDialog: LinkGenreDialogEvent; clearSelection: void; + genreExcluded: void; "homescreen-edit-toggle": void; "mobile-sidebar-open": void; }; diff --git a/src/translations/en.json b/src/translations/en.json index 353763dbe..e002e1cd7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -317,6 +317,9 @@ "remove": "Remove", "remove_alias": "Remove alias", "remove_alias_failed": "Failed to remove alias", + "exclude_genre": "Exclude genre", + "genre_exclusions": "Genre Exclusions", + "remove_genre_exclusion": "Remove exclusion", "remove_library": "Remove from library", "remove_playlist": "Remove from playlist", "search": "Search",