From 59298b5b178e2e1172e8820a266c2ceba9d7f52b Mon Sep 17 00:00:00 2001
From: Broque Thomas <26755000+Nezreka@users.noreply.github.com>
Date: Wed, 22 Apr 2026 19:53:56 -0700
Subject: [PATCH] Restore big-photo album cards on artist-detail page
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The standalone /artist-detail page rendered releases via createReleaseCard
in a stacked layout: square image on top, then title, then year, then a
completion bar — all inside a 300px-tall card with internal padding. The
inline Artists page (now retired) used a richer treatment: full-bleed
artwork with a dark gradient overlay and the title + year pinned at the
bottom. This commit brings that look to the standalone page.
Card markup (still .release-card so all the existing JS filter +
state hooks work, plus .album-card for the visual):
...
[optional .mb-card-icon]
Image loads lazily via the existing observeLazyBackgrounds /
data-bg-src plumbing in core.js — call moved into populateRelease-
Section so each batch of new cards gets observed.
Completion overlay (top-right floating badge):
- Library artists: 'Checking…' / '✓ Owned' / 'N/M' / 'X%' / 'Missing'
based on release.owned + track_completion shape (existing logic
preserved, just rendered as a badge instead of a bar).
- Source artists (no library data): omitted entirely. The card just
shows artwork + title + year, which is what the user asked for.
CSS: scoped overrides under #artist-detail-page .release-card.album-card
neutralize the old release-card background gradient, internal padding,
fixed 300px height, and flex column layout. Cards become aspect-ratio:1
square with overflow:hidden so the image fills and the gradient + text
sit on top.
Filter state (data-is-live / data-is-compilation / data-is-featured)
still tagged on each card so the Include filter group keeps working.
Smoke: library Kendrick Lamar should now look like the inline Artists
page used to — square cards, big artwork, name + year on the bottom.
Source-clicked artist (Schoolboy Q from Deezer) shows the same
visual without the completion overlay.
---
webui/static/library.js | 249 ++++++++++++++++------------------------
webui/static/style.css | 64 +++++++++++
2 files changed, 163 insertions(+), 150 deletions(-)
diff --git a/webui/static/library.js b/webui/static/library.js
index f70c1bac..589b82bf 100644
--- a/webui/static/library.js
+++ b/webui/static/library.js
@@ -1391,22 +1391,34 @@ function populateReleaseSection(sectionType, releases) {
grid.appendChild(card);
});
+ // Trigger lazy background-image loading on the new cards
+ if (typeof observeLazyBackgrounds === 'function') {
+ observeLazyBackgrounds(grid);
+ }
+
console.log(`📀 Populated ${sectionType} section: ${ownedCount} owned, ${missingCount} missing`);
- console.log(`📀 Grid element:`, grid);
- console.log(`📀 Grid children count:`, grid.children.length);
}
function createReleaseCard(release) {
const card = document.createElement("div");
const isChecking = release.owned === null;
- card.className = `release-card${isChecking ? " checking" : (release.owned ? "" : " missing")}`;
+ // .release-card keeps existing filter/state CSS + JS queries working;
+ // .album-card adopts the big-photo visual treatment from the retired
+ // inline Artists page (full-bleed image, gradient overlay, info pinned).
+ let stateCls = '';
+ if (isChecking) stateCls = ' checking';
+ else if (release.owned === false) stateCls = ' missing';
+ card.className = `release-card album-card${stateCls}`;
+
const releaseId = release.id || "";
card.setAttribute("data-release-id", releaseId);
+ card.setAttribute("data-album-id", releaseId);
+ card.setAttribute("data-album-name", release.title || "");
+ card.setAttribute("data-album-type", release.album_type || "album");
// Store mutable reference so stream updates propagate to click handler
card._releaseData = release;
// Tag card for content-type filtering
- const titleLower = (release.title || '').toLowerCase();
const livePattern = /\b(live)\b|\(live[^)]*\)|\[live[^]]*\]/i;
const compilationPattern = /\b(greatest hits|best of|collection|anthology|essential)\b/i;
const featuredPattern = /\(?\bfeat\.?\s|\bft\.?\s|\bfeaturing\b/i;
@@ -1417,10 +1429,90 @@ function createReleaseCard(release) {
card.setAttribute("data-is-compilation", isCompilation ? "true" : "false");
card.setAttribute("data-is-featured", isFeatured ? "true" : "false");
- // Add MusicBrainz icon if available
- let mbIcon = null;
+ // Background image — use data-bg-src for IntersectionObserver lazy loading
+ // (observeLazyBackgrounds is called by the caller after appending the grid).
+ const imageDiv = document.createElement("div");
+ imageDiv.className = "album-card-image";
+ if (release.image_url && release.image_url.trim() !== "") {
+ imageDiv.dataset.bgSrc = release.image_url;
+ }
+ card.appendChild(imageDiv);
+
+ // Completion overlay — top-right floating badge. For library artists this
+ // shows the ownership state; for source artists (no library data) the
+ // overlay is omitted entirely so the card just shows the artwork + title.
+ const isSourceContext = (document.body.dataset.artistSource === 'source');
+ if (!isSourceContext) {
+ const overlay = document.createElement("div");
+ let overlayCls = '';
+ let overlayLabel = '';
+
+ if (isChecking || release.track_completion === 'checking') {
+ overlayCls = 'checking';
+ overlayLabel = 'Checking...';
+ } else if (release.owned) {
+ const tc = release.track_completion;
+ if (tc && typeof tc === 'object') {
+ const ownedTracks = tc.owned_tracks || 0;
+ const totalTracks = tc.total_tracks || 0;
+ const missingTracks = tc.missing_tracks || 0;
+ if (missingTracks === 0) {
+ overlayCls = 'completed';
+ overlayLabel = '✓ Owned';
+ } else {
+ const pct = totalTracks > 0 ? Math.round((ownedTracks / totalTracks) * 100) : 0;
+ overlayCls = pct >= 75 ? 'nearly_complete' : 'partial';
+ overlayLabel = `${ownedTracks}/${totalTracks}`;
+ }
+ } else {
+ const pct = release.track_completion || 100;
+ if (pct === 100) {
+ overlayCls = 'completed';
+ overlayLabel = '✓ Owned';
+ } else {
+ overlayCls = pct >= 75 ? 'nearly_complete' : 'partial';
+ overlayLabel = `${pct}%`;
+ }
+ }
+ } else {
+ overlayCls = 'missing';
+ overlayLabel = 'Missing';
+ }
+
+ overlay.className = `completion-overlay ${overlayCls}`;
+ overlay.innerHTML = `${overlayLabel}`;
+ card.appendChild(overlay);
+ }
+
+ // Year — extract from release_date or fall back to year field
+ let yearText = "";
+ if (release.release_date) {
+ try {
+ const yearMatch = release.release_date.match(/^(\d{4})/);
+ if (yearMatch) {
+ const ry = parseInt(yearMatch[1]);
+ if (ry && ry > 1900 && ry <= new Date().getFullYear() + 1) yearText = ry.toString();
+ } else {
+ const ry = new Date(release.release_date).getFullYear();
+ if (ry && !isNaN(ry) && ry > 1900 && ry <= new Date().getFullYear() + 1) yearText = ry.toString();
+ }
+ } catch (e) { /* fall through */ }
+ }
+ if (!yearText && release.year) yearText = release.year.toString();
+
+ // Content (bottom-pinned over gradient)
+ const content = document.createElement("div");
+ content.className = "album-card-content";
+ const _esc = (s) => String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>');
+ content.innerHTML = `
+ ${_esc(release.title)}
+ ${yearText ? `${_esc(yearText)}
` : ''}
+ `;
+ card.appendChild(content);
+
+ // Add MusicBrainz icon LAST so it sits above the gradient overlay
if (release.musicbrainz_release_id) {
- mbIcon = document.createElement("div");
+ const mbIcon = document.createElement("div");
mbIcon.className = "mb-card-icon";
mbIcon.title = "View on MusicBrainz";
mbIcon.innerHTML = `
`;
@@ -1428,149 +1520,6 @@ function createReleaseCard(release) {
e.stopPropagation();
window.open(`https://musicbrainz.org/release/${release.musicbrainz_release_id}`, '_blank');
};
- }
-
- // Create image
- const imageContainer = document.createElement("div");
- if (release.image_url && release.image_url.trim() !== "") {
- const img = document.createElement("img");
- img.src = release.image_url;
- img.alt = release.title;
- img.className = "release-image";
- img.loading = 'lazy';
- img.onerror = () => {
- imageContainer.innerHTML = `💿
`;
- };
- imageContainer.appendChild(img);
- } else {
- imageContainer.innerHTML = `💿
`;
- }
-
- // Create title
- const title = document.createElement("h4");
- title.className = "release-title";
- title.textContent = release.title;
- title.title = release.title;
-
- // Create year - extract from release_date (Spotify format) or fall back to year field
- const year = document.createElement("div");
- year.className = "release-year";
-
- let yearText = "Unknown Year";
-
- // DEBUG: Log the release data to see what we're working with (remove this after testing)
- // console.log(`🔍 DEBUG: Release "${release.title}" data:`, {
- // title: release.title,
- // owned: release.owned,
- // year: release.year,
- // release_date: release.release_date,
- // track_completion: release.track_completion
- // });
-
- // First try to extract year from release_date (Spotify format: "YYYY-MM-DD")
- if (release.release_date) {
- try {
- // Extract year directly from string to avoid timezone issues
- const yearMatch = release.release_date.match(/^(\d{4})/);
- if (yearMatch) {
- const releaseYear = parseInt(yearMatch[1]);
- if (releaseYear && !isNaN(releaseYear) && releaseYear > 1900 && releaseYear <= new Date().getFullYear() + 1) {
- yearText = releaseYear.toString();
- }
- } else {
- // Fallback to Date parsing if format is different
- const releaseYear = new Date(release.release_date).getFullYear();
- if (releaseYear && !isNaN(releaseYear) && releaseYear > 1900 && releaseYear <= new Date().getFullYear() + 1) {
- yearText = releaseYear.toString();
- }
- }
- } catch (e) {
- console.warn('Error parsing release_date:', release.release_date, e);
- }
- }
-
- // Fallback to direct year field if release_date parsing failed
- if (yearText === "Unknown Year" && release.year) {
- yearText = release.year.toString();
- }
-
- year.textContent = yearText;
-
- // Create completion info
- const completion = document.createElement("div");
- completion.className = "release-completion";
-
- const completionText = document.createElement("span");
- const completionBar = document.createElement("div");
- completionBar.className = "completion-bar";
-
- const completionFill = document.createElement("div");
- completionFill.className = "completion-fill";
-
- if (release.owned === null || release.track_completion === 'checking') {
- // Checking state - ownership not yet resolved
- completionText.textContent = "Checking...";
- completionText.className = "completion-text checking";
- completionFill.className += " checking";
- completionFill.style.width = "100%";
- } else if (release.owned) {
- // Handle new detailed track completion object
- if (release.track_completion && typeof release.track_completion === 'object') {
- const completion = release.track_completion;
- const percentage = completion.percentage || 100;
- const ownedTracks = completion.owned_tracks || 0;
- const totalTracks = completion.total_tracks || 0;
- const missingTracks = completion.missing_tracks || 0;
-
- completionFill.style.width = `${percentage}%`;
-
- if (missingTracks === 0) {
- completionText.textContent = `Complete (${ownedTracks})`;
- completionText.className = "completion-text complete";
- completionFill.className += " complete";
- } else {
- completionText.textContent = `${ownedTracks}/${totalTracks} tracks`;
- completionText.className = "completion-text partial";
- completionFill.className += " partial";
-
- // Add missing tracks indicator
- completionText.title = `Missing ${missingTracks} track${missingTracks !== 1 ? 's' : ''}`;
- }
- } else {
- // Fallback for legacy simple percentage
- const percentage = release.track_completion || 100;
- completionFill.style.width = `${percentage}%`;
-
- if (percentage === 100) {
- completionText.textContent = "Complete";
- completionText.className = "completion-text complete";
- completionFill.className += " complete";
- } else {
- completionText.textContent = `${percentage}%`;
- completionText.className = "completion-text partial";
- completionFill.className += " partial";
- }
- }
- } else {
- const totalTr = release.total_tracks || release.track_completion?.total_tracks || 0;
- completionText.textContent = totalTr > 0 ? `Missing (${totalTr} tracks)` : "Not in library";
- completionText.className = "completion-text missing";
- completionFill.className += " missing";
- completionFill.style.width = "0%";
- }
-
- completionBar.appendChild(completionFill);
- completion.appendChild(completionText);
- completion.appendChild(completionBar);
-
- // Assemble card
- card.appendChild(imageContainer);
- card.appendChild(title);
- card.appendChild(year);
- card.appendChild(completion);
-
- // Add MusicBrainz icon LAST to ensure it's on top
- if (release.musicbrainz_release_id && mbIcon) { // Check if mbIcon was created
card.appendChild(mbIcon);
}
diff --git a/webui/static/style.css b/webui/static/style.css
index 99f28c7d..ee13cc8e 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -59449,3 +59449,67 @@ body[data-artist-source="source"] #artist-detail-page #library-artist-enhance-bt
position: relative;
z-index: 2;
}
+
+
+/* =========================================================================
+ Album cards on /artist-detail — big-photo treatment
+ =========================================================================
+ .release-card keeps the existing JS filter/state hooks; .album-card
+ layers on the visual treatment from the retired inline Artists page
+ (full-bleed image, gradient overlay, info pinned at bottom).
+
+ The base .release-card rule still applies (background gradient, padding,
+ fixed 300px height, flex column). These overrides switch off the parts
+ that fight with the .album-card layout. */
+
+#artist-detail-page .release-card.album-card {
+ background: rgba(18, 18, 18, 1);
+ backdrop-filter: none;
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 14px;
+ padding: 0;
+ display: block;
+ height: auto;
+ aspect-ratio: 1;
+ overflow: hidden;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
+ box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
+ border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+#artist-detail-page .release-card.album-card:hover {
+ background: rgba(18, 18, 18, 1);
+ transform: translateY(-5px) scale(1.02);
+ border-color: rgba(var(--accent-rgb), 0.25);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5),
+ 0 0 24px rgba(var(--accent-rgb), 0.12);
+}
+
+#artist-detail-page .release-card.album-card.missing {
+ opacity: 1; /* let the completion-overlay convey state, not the whole card */
+}
+
+/* The fit-content override on the discography page also conflicts with
+ aspect-ratio:1 — clear it for album-card variants. */
+#artist-detail-page .discography-sections .release-card.album-card {
+ height: auto;
+}
+
+/* MusicBrainz icon position (was tied to the old card's padding) */
+#artist-detail-page .release-card.album-card .mb-card-icon {
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ z-index: 3;
+ background: rgba(0, 0, 0, 0.65);
+ border-radius: 6px;
+ padding: 4px 6px;
+ cursor: pointer;
+ opacity: 0.85;
+ transition: opacity 0.2s ease;
+}
+
+#artist-detail-page .release-card.album-card .mb-card-icon:hover {
+ opacity: 1;
+}