From e61b7456df01dabbedc39a8ef3d93da67e172647 Mon Sep 17 00:00:00 2001 From: Logan Date: Thu, 13 Aug 2026 15:17:06 -0400 Subject: [PATCH] feat(explore): make the library badge request what it is on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 007 turned this badge from a ` + `; + } return html` - ${this.iconName() - ? html`` - : nothing} + ${icon} `; } diff --git a/frontend/src/components/top-results-row/top-results-row.ts b/frontend/src/components/top-results-row/top-results-row.ts index 3d97b10..7259d5c 100644 --- a/frontend/src/components/top-results-row/top-results-row.ts +++ b/frontend/src/components/top-results-row/top-results-row.ts @@ -341,6 +341,8 @@ export class TopResultsRow extends LitElement { status=${status} entity-type=${entityType} label=${r.name} + request-mbid=${r.mbid} + request-artist=${r.artistCredit ?? ''} size="22" >`} diff --git a/frontend/src/utils/library-status.ts b/frontend/src/utils/library-status.ts index 8c6326e..50be773 100644 --- a/frontend/src/utils/library-status.ts +++ b/frontend/src/utils/library-status.ts @@ -1,4 +1,6 @@ import { downloadStore } from '@store/download-store'; +import { libraryStore } from '@store/library-store'; +import type { download } from '@go/models'; import type { LibraryStatus } from '../components/library-status-indicator/library-status-indicator'; /** @@ -43,3 +45,62 @@ export function libraryStatusFor( return 'not-in-library'; } + +/** What a badge can ask for. Artists are deliberately absent: a + * discography subscription is `explore-artist-details`'s Follow + * button, which can say what it is committing to. */ +export type RequestableEntity = 'album' | 'track'; + +const ENTITY: Record = { + album: 'release-group', + track: 'recording', +}; + +/** + * Add or drop a request for one entity, and report which way it went. + * + * The counterpart to `libraryStatusFor`, here rather than in the badge + * because the badge is one of several things that can ask — + * `explore-album-details`'s "Want this" button is the other, and two + * implementations of "what does wanting something mean" is exactly what + * phase 1 was about. + * + * Returns `'wanted'` or `'cancelled'` so a caller can announce what + * happened; throws if the backend refused, because a badge that + * silently does nothing is what this whole plan is about. + */ +export async function toggleRequest(input: { + mbid: string; + entity: RequestableEntity; + title: string; + artist?: string; +}): Promise<'wanted' | 'cancelled'> { + const existing = downloadStore.requestFor(input.mbid); + + if (existing) { + await downloadStore.removeRequest(existing.id); + + return 'cancelled'; + } + + // A request belongs to a library because that is where its files + // will land. There is always at least one by the time anything is + // on screen — the first-run wizard blocks every pointer event until + // there is — but an explicit failure beats a request filed against + // library 0, which no import would ever match. + const libraryId = await libraryStore.getDefaultLibraryId(); + + if (!libraryId) throw new Error('no library to add this to'); + + await downloadStore.addRequest({ + mbid: input.mbid, + entity: ENTITY[input.entity], + libraryId, + artist: input.artist ?? '', + title: input.title, + scope: 'future', + secondary: false, + } as download.RequestInput); + + return 'wanted'; +} diff --git a/frontend/test/components/library-status.test.ts b/frontend/test/components/library-status.test.ts index c567b5a..af069b9 100644 --- a/frontend/test/components/library-status.test.ts +++ b/frontend/test/components/library-status.test.ts @@ -21,8 +21,16 @@ import '@components/explore-view/explore-view'; import type { Request } from '@store/download-store'; import { libraryStatusFor } from '@utils/library-status'; import { Events } from '../../src/events'; -import { emit, flush, stub } from '@test/support/harness'; -import { fixture, shadow, shadowAll } from '@test/support/render'; +import { notificationStore } from '@store/notification-store'; +import { + calls, + emit, + flush, + lastArgs, + stub, + stubFailure, +} from '@test/support/harness'; +import { fixture, shadow, shadowAll, update } from '@test/support/render'; const SEARCH = 'explore.Service.SearchLocal'; @@ -183,3 +191,171 @@ describe(' badges', () => { ); }); }); + +/** + * Plan 009 phase 3: the badge becomes a button where it can act. + * + * 007 made it `role="img"` because a control that cannot act is worse + * than none, and wrote down what would change the answer: a `