@@ -3283,6 +3296,15 @@
Popular Tracks
+
+
+ In Your Library
+
+
+
+
Albums
diff --git a/js/db.js b/js/db.js
index 76477aa..122155b 100644
--- a/js/db.js
+++ b/js/db.js
@@ -172,11 +172,13 @@ export class MusicDatabase {
if (exists) {
await this.performTransaction(storeName, 'readwrite', (store) => store.delete(key));
+ window.dispatchEvent(new CustomEvent('favorites-changed'));
return false; // Removed
} else {
const minified = this._minifyItem(type, item);
const entry = { ...minified, addedAt: Date.now() };
await this.performTransaction(storeName, 'readwrite', (store) => store.put(entry));
+ window.dispatchEvent(new CustomEvent('favorites-changed'));
return true; // Added
}
}
@@ -600,6 +602,7 @@ export class MusicDatabase {
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
this._dispatchPlaylistSync('update', playlist);
+ window.dispatchEvent(new CustomEvent('playlist-tracks-changed'));
return playlist;
}
@@ -623,6 +626,7 @@ export class MusicDatabase {
this._updatePlaylistMetadata(playlist);
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
this._dispatchPlaylistSync('update', playlist);
+ window.dispatchEvent(new CustomEvent('playlist-tracks-changed'));
}
return playlist;
@@ -643,6 +647,7 @@ export class MusicDatabase {
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
this._dispatchPlaylistSync('update', playlist);
+ window.dispatchEvent(new CustomEvent('playlist-tracks-changed'));
return playlist;
}
diff --git a/js/ui.js b/js/ui.js
index 1998434..e363cc8 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -3072,10 +3072,10 @@ export class UIRenderer {
dateDisplay =
window.innerWidth > 768
? releaseDate.toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- })
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })
: year;
}
}
@@ -3927,6 +3927,8 @@ export class UIRenderer {
const epsSection = document.getElementById('artist-section-eps');
const similarContainer = document.getElementById('artist-detail-similar');
const similarSection = document.getElementById('artist-section-similar');
+ const inLibraryContainer = document.getElementById('artist-detail-in-library');
+ const inLibrarySection = document.getElementById('artist-section-in-library');
const dlBtn = document.getElementById('download-discography-btn');
if (dlBtn) dlBtn.innerHTML = `${SVG_DOWNLOAD}Download Discography`;
@@ -3948,6 +3950,14 @@ export class UIRenderer {
if (loadUnreleasedSection) loadUnreleasedSection.style.display = 'none';
if (similarContainer) similarContainer.innerHTML = this.createSkeletonCards(6, true);
if (similarSection) similarSection.style.display = 'block';
+ if (inLibrarySection) inLibrarySection.style.display = 'none';
+ if (inLibraryContainer) {
+ inLibraryContainer.innerHTML = '';
+ inLibraryContainer.style.display = 'none';
+ }
+ // Reset chevron state
+ const chevronEl = document.getElementById('in-library-chevron');
+ if (chevronEl) chevronEl.style.transform = 'rotate(0deg)';
try {
const artist = await this.api.getArtist(artistId, provider);
@@ -4168,9 +4178,9 @@ export class UIRenderer {
${artist.popularity}% popularity
${(artist.artistRoles || [])
- .filter((role) => role.category)
- .map((role) => `${role.category}`)
- .join('')}
+ .filter((role) => role.category)
+ .map((role) => `${role.category}`)
+ .join('')}
`;
@@ -4182,6 +4192,205 @@ export class UIRenderer {
this.renderListWithTracks(tracksContainer, artist.tracks, true);
+ // "In your library" section: find liked tracks and playlist tracks for this artist
+ if (inLibraryContainer && inLibrarySection) {
+ const artistNameLower = artist.name.toLowerCase();
+
+ const isTrackByArtist = (track) => {
+ if (track.artists && Array.isArray(track.artists)) {
+ return track.artists.some(
+ (a) => a && a.name && a.name.toLowerCase() === artistNameLower
+ );
+ }
+ if (track.artist) {
+ const artistStr = typeof track.artist === 'string' ? track.artist : track.artist.name;
+ if (artistStr && artistStr.toLowerCase() === artistNameLower) return true;
+ }
+ return false;
+ };
+
+ const refreshInLibrary = async () => {
+ try {
+ const seenIds = new Set();
+ const libraryTracks = [];
+ const trackSourceMap = new Map(); // trackId -> Array<{ label, href }>
+
+ const addSource = (trackId, source) => {
+ if (!trackSourceMap.has(trackId)) {
+ trackSourceMap.set(trackId, []);
+ }
+ trackSourceMap.get(trackId).push(source);
+ };
+
+ // Get liked tracks
+ const likedTracks = await db.getFavorites('track');
+ for (const track of likedTracks) {
+ if (isTrackByArtist(track)) {
+ if (!seenIds.has(track.id)) {
+ seenIds.add(track.id);
+ libraryTracks.push(track);
+ }
+ addSource(track.id, { label: 'Liked Tracks', href: '/library' });
+ }
+ }
+
+ // Get tracks from user playlists
+ const userPlaylists = await db.getPlaylists(true);
+ for (const playlist of userPlaylists) {
+ if (playlist.tracks && Array.isArray(playlist.tracks)) {
+ for (const track of playlist.tracks) {
+ if (isTrackByArtist(track)) {
+ if (!seenIds.has(track.id)) {
+ seenIds.add(track.id);
+ libraryTracks.push(track);
+ }
+ const label = playlist.name || playlist.title || 'Playlist';
+ addSource(track.id, {
+ label,
+ href: `/userplaylist/${playlist.id}`
+ });
+ }
+ }
+ }
+ }
+
+ // Sort alphabetically by title
+ libraryTracks.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
+
+ if (libraryTracks.length > 0) {
+ inLibrarySection.style.display = 'block';
+ this.renderListWithTracks(inLibraryContainer, libraryTracks, true);
+
+ // Inject source labels into each track's .artist div
+ const trackElements = inLibraryContainer.querySelectorAll('.track-item');
+ trackElements.forEach((el, idx) => {
+ const track = libraryTracks[idx];
+ if (!track) return;
+ const sources = trackSourceMap.get(track.id);
+ if (!sources || sources.length === 0) return;
+ const artistDiv = el.querySelector('.track-item-details .artist');
+ if (!artistDiv) return;
+
+ // Extract artist name and year from existing content
+ const artistLinks = artistDiv.querySelectorAll('.artist-link');
+ const artistNames = Array.from(artistLinks).map(a => a.textContent).join(', ');
+ const truncatedArtist = artistNames.length > 15 ? artistNames.slice(0, 20) + '…' : artistNames;
+
+ // Extract year from text content (pattern: " • 2024")
+ const fullText = artistDiv.textContent;
+ const yearMatch = fullText.match(/\s•\s(\d{4})/);
+ const yearText = yearMatch ? ` • ${yearMatch[1]}` : '';
+
+ // Build source content
+ const sourceSpan = document.createElement('span');
+ sourceSpan.className = 'library-source';
+
+ if (sources.length === 1) {
+ const srcLabel = sources[0].label.length > 15 ? sources[0].label.slice(0, 15) + '…' : sources[0].label;
+ sourceSpan.innerHTML = `· Source: ${srcLabel}`;
+ sourceSpan.style.cursor = 'pointer';
+ sourceSpan.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ navigate(sources[0].href);
+ });
+ } else {
+ sourceSpan.innerHTML = `· Source: Multiple Playlists`;
+ sourceSpan.style.cursor = 'pointer';
+ sourceSpan.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const modal = document.getElementById('goto-playlist-modal');
+ const list = document.getElementById('goto-playlist-list');
+ const cancelBtn = document.getElementById('goto-playlist-cancel');
+ const overlay = modal.querySelector('.modal-overlay');
+
+ list.innerHTML = sources.map((s) =>
+ `
+ ${s.label}
+
`
+ ).join('');
+
+ const closeModal = () => {
+ modal.classList.remove('active');
+ };
+
+ list.onclick = (ev) => {
+ const option = ev.target.closest('.modal-option');
+ if (!option) return;
+ const href = option.dataset.href;
+ closeModal();
+ if (href) navigate(href);
+ };
+
+ cancelBtn.onclick = closeModal;
+ overlay.onclick = closeModal;
+ modal.classList.add('active');
+ });
+ }
+
+ // Rebuild artist div with structured layout
+ artistDiv.innerHTML = '';
+ artistDiv.classList.add('library-artist-flex');
+
+ const artistNameSpan = document.createElement('span');
+ artistNameSpan.className = 'library-artist-name';
+ artistNameSpan.textContent = truncatedArtist;
+
+ const yearSpan = document.createElement('span');
+ yearSpan.className = 'library-year';
+ yearSpan.textContent = yearText;
+
+ artistDiv.appendChild(artistNameSpan);
+ artistDiv.appendChild(yearSpan);
+ artistDiv.appendChild(sourceSpan);
+ });
+ } else {
+ inLibrarySection.style.display = 'none';
+ }
+ } catch (err) {
+ console.warn('Failed to load library tracks for artist:', err);
+ inLibrarySection.style.display = 'none';
+ }
+ };
+
+ // Initial load
+ refreshInLibrary().then(() => {
+ inLibraryContainer.style.display = 'none';
+ });
+
+ // Setup chevron toggle (once)
+ const toggle = document.getElementById('in-library-toggle');
+ const chevron = document.getElementById('in-library-chevron');
+ if (toggle) {
+ toggle.onclick = () => {
+ const isOpen = inLibraryContainer.style.display !== 'none';
+ inLibraryContainer.style.display = isOpen ? 'none' : '';
+ if (chevron) {
+ chevron.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(90deg)';
+ }
+ };
+ }
+
+ // Real-time updates: refresh when favorites or playlists change
+ let refreshTimeout;
+ const debouncedRefresh = () => {
+ clearTimeout(refreshTimeout);
+ refreshTimeout = setTimeout(() => refreshInLibrary(), 300);
+ };
+ window.addEventListener('favorites-changed', debouncedRefresh);
+ window.addEventListener('playlist-tracks-changed', debouncedRefresh);
+
+ // Cleanup listeners when navigating away
+ const cleanupOnNav = () => {
+ window.removeEventListener('favorites-changed', debouncedRefresh);
+ window.removeEventListener('playlist-tracks-changed', debouncedRefresh);
+ window.removeEventListener('popstate', cleanupOnNav);
+ };
+ window.addEventListener('popstate', cleanupOnNav, { once: true });
+ }
+
// Update header like button
const artistLikeBtn = document.getElementById('like-artist-btn');
if (artistLikeBtn) {
@@ -4812,13 +5021,13 @@ export class UIRenderer {
${
isUser
- ? `
+ ? `
`
- : ''
+ : ''
}