Files
NovaSonic/src/components.js
T
HoneyandClaude Sonnet 4.6 467ee8a0fe fix: consistent artist parsing across player, NP overlay, and song list
Extract parseArtists() helper — uses Navidrome's resolved artists array
when it has multiple entries, otherwise splits artist string on ';' only.
Removes old split on / that broke Au/Ra, HUNTR/X etc everywhere.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 13:08:33 +02:00

99 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { S } from './state.js';
import { esc, formatDur } from './utils.js';
import { coverUrl } from './api.js';
// Circular imports — safe (only used inside event handlers after module init)
import { playQueue, toggleStar, playPreview, parseArtists } from './player.js';
import { showCtxMenu, openDlModal } from './ui.js';
import { navigate, quickPlayAlbum, quickPlayArtist } from './views.js';
// ─── GENERIC CARD ───
export function makeCard(title, sub, imgUrl, fallback='💿') {
const c = document.createElement('div');
c.className = 'card';
c.innerHTML = `<div class="card-art">
${imgUrl ? `<img src="${imgUrl}" loading="lazy" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>${fallback}</div>'">` : `<div class="no-art">${fallback}</div>`}
<div class="play-overlay"><button class="play-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg></button></div>
</div>
<div class="card-info"><div class="card-title">${esc(title)}</div><div class="card-sub">${esc(sub||'')}</div></div>`;
return c;
}
export function makeAlbumCard(a) {
const cv = coverUrl(a.coverArt, 512);
const c = makeCard(a.name, a.artist||'', cv, '💿');
c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(a); };
c.querySelector('.card-info').onclick = () => navigate('album-detail', a);
return c;
}
// ─── SONG LIST ITEM (library) ───
export function makeSongItem(s, idx, list, { noArtistLink = false, noAlbumLink = false } = {}) {
const el = document.createElement('div');
if(s.starred && !s._starred) s._starred = true;
el.className = 'song-item' + (S.queue[S.qIdx]?.id === s.id ? ' playing' : '');
el.dataset.songId = s.id;
const cv = coverUrl(s.coverArt, 128);
const artistParts = parseArtists(s);
const artistHtml = artistParts.length
? artistParts.map(a => noArtistLink
? `<span style="color:var(--text2)">${esc(a.name)}</span>`
: `<span class="song-link" onclick="event.stopPropagation();s_goArtist(event,'${esc(a.name)}','${a.id||''}')">${esc(a.name)}</span>`
).join('<span style="color:var(--text3)"> • </span>')
: '';
const albumHtml = !noAlbumLink && (s.albumId||s.album)
? `<span style="color:var(--text3)"> · </span><span class="song-link" onclick="event.stopPropagation();s_goAlbum(event,'${s.albumId||''}','${esc(s.album||'')}','${esc(s.artist||'')}','${s.artistId||''}')">${esc(s.album||'')}</span>`
: '';
const playsHtml = S.cfg.showPlayCount
? `<span class="song-plays">${s.playCount ? s.playCount.toLocaleString() + '<span class="song-plays-x">×</span>' : '—'}</span>`
: '';
el.innerHTML = `<div class="song-num">${idx+1}</div>
<div class="song-thumb">${cv ? `<img src="${cv}" loading="lazy" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>♪</div>'">` : '<div class="no-art">♪</div>'}</div>
<div class="song-info">
<div class="song-title">${esc(s.title)}</div>
<div class="song-artist">${artistHtml}${albumHtml}</div>
</div>
<div class="song-meta">
<span class="song-dur">${formatDur(s.duration)}</span>
<div class="song-actions">
<button class="icon-btn${s._starred?' starred':''}" title="Favorit"><svg width="12" height="12" viewBox="0 0 24 24" fill="${s._starred?'currentColor':'none'}" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg></button>
<button class="icon-btn song-menu-btn" title="Mehr Optionen"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg></button>
</div>
${playsHtml}
</div>`;
const btns = el.querySelectorAll('.icon-btn');
btns[0].onclick = (e) => { e.stopPropagation(); toggleStar(s, btns[0]); };
btns[1].onclick = (e) => { e.stopPropagation(); showCtxMenu(e, s, list, idx); };
el.onclick = () => playQueue(list, idx);
el.addEventListener('contextmenu', e => { e.preventDefault(); showCtxMenu(e, s, list, idx); });
return el;
}
// ─── GLOBAL TRACK ITEM (iTunes / Spotify results) ───
export function makeGlobalTrackItem(t, idx, list) {
const el = document.createElement('div');
el.className = 'gtrack-item';
const art = t.artworkUrl60 || t.artworkUrl100 || '';
el.innerHTML = `
<div class="gtrack-art">${art ? `<img src="${art}" loading="lazy" onerror="this.parentNode.innerHTML='<div style=\\'width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:22px\\'>🎵</div>'">` : '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:22px">🎵</div>'}</div>
<div class="gtrack-info">
<div class="gtrack-title">${esc(t.trackName||t.collectionName||'—')}${t.trackExplicitness==='explicit'?'<span class="explicit-badge">E</span>':''}</div>
<div class="gtrack-sub">${esc(t.artistName||'—')} · ${esc(t.collectionName||'')} ${t.releaseDate?'· '+new Date(t.releaseDate).getFullYear():''}</div>
</div>
<div class="gtrack-actions">
${t.previewUrl ? `<button class="icon-btn blue" title="30s Preview" onclick="event.stopPropagation();playPreview(${JSON.stringify(t).replace(/"/g,'&quot;')})"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg></button>` : ''}
<button class="icon-btn green" title="Auf Server laden" onclick="event.stopPropagation();openDlModal(${JSON.stringify(t).replace(/"/g,'&quot;')})">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
</button>
</div>`;
el.onclick = () => { if(t.previewUrl) playPreview(t); };
return el;
}
// ─── SEARCH SECTION HEADER ───
export function mkSection(title) {
const d = document.createElement('div');
d.className = 'search-section';
d.innerHTML = `<div class="search-section-title">${title}</div>`;
return d;
}