import { LitElement, html, css, nothing } from 'lit'; import { customElement, property, state, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { designTokens } from '../../styles/tokens.css'; import { srOnly } from '../../styles/sr-only.css'; import { unownedLabel, unownedStyles } from '@utils/ownership'; import { LookupReleaseGroup, BrowseReleases, GetThumbnail, } from '@go/explore/service.js'; import { GetAlbumTracks, GetAlbumCompleteness, GetFilePathsByRecordingMBIDs, } from '@go/library/library.js'; import * as library from '@go/library/models.js'; import type * as download from '@go/download/models.js'; import type * as explore from '@go/explore/models.js'; type MBReleaseGroup = explore.MBReleaseGroup; type MBRelease = explore.MBRelease; type MBTrack = explore.MBTrack; import { exploreCache } from '../../store/explore-cache'; import { libraryStore } from '../../store/library-store'; import { creditLink, exploreLinkStyles } from '../../utils/explore-link'; import { creditStore } from '@store/credit-store'; import { describeError } from '../../utils/describe-error'; import { EventsOn } from '@runtime/runtime'; import { Events } from '../../events'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '../library-status-indicator/library-status-indicator.js'; import { libraryStatusFor } from '@utils/library-status'; import { ICON_AUTOTAG } from '@utils/icon-language'; import type { LibraryStatus } from '../library-status-indicator/library-status-indicator.js'; import '../catalog-scope-notice/catalog-scope-notice.js'; import type { CatalogScope } from '../catalog-scope-notice/catalog-scope-notice.js'; import '@awesome.me/webawesome/dist/components/button/button.js'; import '../download-picker/download-picker'; import { downloadStore } from '../../store/download-store'; import { MatchForAlbum, ApplyAsync } from '@go/autotagservice/service.js'; import type * as autotagservice from '@go/autotagservice/models.js'; import { confirmAction } from '../confirm-dialog/confirm-dialog'; import { queueStore } from '../../store/queue-store'; import type { QueueSource } from '../../store/queue-store'; import { notificationStore } from '../../store/notification-store'; import '../notifications/inline-notice'; import { ContextMenuController, contextMenuStyles, isContextMenuKey, } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js'; import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type { MenuSurface } from '../menu-surface/menu-surface'; import '../menu-surface/menu-surface'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import { dictByName } from '@utils/binding'; import type { TrackDetails } from '@components/track-details/track-details.js'; import { showTrackDetailsForPath } from '@utils/track-details-opener.js'; import '@components/playlist-picker/playlist-picker.js'; import { ICON_CAN_REQUEST, ICON_PLAYLIST, ICON_QUEUE, ICON_REQUESTED, } from '@utils/icon-language'; /** * The region the album header's own failures are rendered in. * * "Inline" says *not global*, not *where* — so the region is named * once, here, rather than spelled at each call site. */ export const ExploreAlbumRegion = 'explore-album'; /** * How long to wait on a background release browse that has reported * neither success nor failure before treating it as gone. See * `armReleasesFallback` for why this is generous. */ const RELEASES_FALLBACK_MS = 60000; /* ── Utility functions (duplicated per Knowledge Pattern #9 — no cross-component imports) ── */ /** The earliest release date in a group, for tie-breaking. */ function earliestDate(releases: MBRelease[]): string { let earliest = '\uffff'; for (const r of releases) { const d = r.date || '\uffff'; if (d < earliest) earliest = d; } return earliest; } function extractYear(dateStr: string): string { if (!dateStr) return ''; return dateStr.substring(0, 4); } /** * Convert a duration in milliseconds to a human-readable "m:ss" string. * Returns "0:00" for zero/negative/NaN values. */ function formatDuration(ms: number): string { if (!ms || ms <= 0) return '0:00'; const totalSeconds = Math.round(ms / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes}:${String(seconds).padStart(2, '0')}`; } /* ── Types ── */ interface ReleaseCluster { representative: MBRelease; allReleases: MBRelease[]; fingerprint: string; /** Standard-version score, computed by buildClusters. */ score: number; } /** Synthetic kinds — virtual entries pinned at the top of the dropdown. */ type SyntheticKind = 'standard' | 'library'; /** Unified version-entry shape covering both synthetic and real clusters. */ interface VersionEntry { /** Stable identifier for the dropdown