import { S } from '../state.js';
import { esc, formatDur } from '../utils.js';
// Exposed on window so inline onerror= handlers can call it.
// Retries the same src up to 3 times with increasing delays, then shows fallback.
window._imgRetry = function(img, fallback) {
const n = (img._r = (img._r || 0) + 1);
if (n > 3 || !img.src) { img.parentNode.innerHTML = `
${fallback}
`; return; }
setTimeout(() => { const s = img.src; img.src = ''; img.src = s; }, 1500 * n);
};
import { coverUrl, dispTitle, dispArtistParts, dispArtistById, dispAlbumName } from '../api.js';
// Circular imports — safe (only used inside event handlers after module init)
import { playQueue, toggleStar, playPreview, parseArtists } from '../player/player.js';
import { showCtxMenu, openDlModal } from './ui.js';
import { navigate, quickPlayAlbum, quickPlayArtist } from '../views/views.js';
// ─── GENERIC CARD ───
export function makeCard(title, sub, imgUrl, fallback='💿') {
const c = document.createElement('div');
c.className = 'card';
c.innerHTML = `
${imgUrl ? `
` : `
${fallback}
`}
${esc(title)}
${esc(sub||'')}
`;
return c;
}
export function makeAlbumCard(a) {
const cv = coverUrl(a.coverArt, 512);
const c = makeCard(dispAlbumName(a), dispArtistById(a.artistId, 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 } = {}) {
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 = dispArtistParts(s, parseArtists);
const artistHtml = artistParts.length
? artistParts.map(a => noArtistLink
? `${esc(a.name)} `
: `${esc(a.name)} `
).join(' • ')
: '';
const playsHtml = S.cfg.showPlayCount
? `${s.playCount ? s.playCount.toLocaleString() + '× ' : '—'} `
: '';
el.innerHTML = `${idx+1}
${cv ? `
` : '
♪
'}
${esc(dispTitle(s))}
${artistHtml}
`;
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, { noShuffle: true });
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 = `
${art ? `
` : '
🎵
'}
${t.trackNumber || ''}
${esc(t.trackName||t.collectionName||'—')}${t.trackExplicitness==='explicit'?'E ':''}
${esc(t.artistName||'—')} · ${esc(t.collectionName||'')} ${t.releaseDate?'· '+new Date(t.releaseDate).getFullYear():''}
${t.previewUrl ? `
` : ''}
`;
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 = `${title}
`;
return d;
}
// ─── HORIZONTAL SCROLL-ROW ARROWS (desktop) ───
const _CHEV_L = ' ';
const _CHEV_R = ' ';
/** Wrap a `.scroll-row` with left/right arrows that appear only for the scrollable direction. */
export function enhanceScrollRow(row) {
if (!row || row.dataset.saEnhanced) return;
row.dataset.saEnhanced = '1';
const wrap = document.createElement('div');
wrap.className = 'scroll-row-wrap';
row.parentNode.insertBefore(wrap, row);
wrap.appendChild(row);
const mkBtn = (cls, html, label) => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'scroll-arrow ' + cls;
b.setAttribute('aria-label', label);
b.innerHTML = html;
wrap.appendChild(b);
return b;
};
const left = mkBtn('sa-left', _CHEV_L, 'Zurück');
const right = mkBtn('sa-right', _CHEV_R, 'Weiter');
const EPS = 4;
// Cheap per-scroll work — just toggle which arrow is available; rAF-throttled to once per frame.
let _rafPending = false;
const updateArrows = () => {
_rafPending = false;
left.classList.toggle('can-scroll', row.scrollLeft > EPS);
right.classList.toggle('can-scroll', row.scrollLeft < row.scrollWidth - row.clientWidth - EPS);
};
const onScroll = () => { if (!_rafPending) { _rafPending = true; requestAnimationFrame(updateArrows); } };
// Expensive (getBoundingClientRect forces layout) — only on size/content change, NOT every scroll.
const positionArrows = () => {
const art = row.querySelector('.card-art') || row.querySelector('.card');
if (art) {
const top = art.getBoundingClientRect().top - wrap.getBoundingClientRect().top + art.offsetHeight / 2;
if (top > 0) { left.style.top = right.style.top = top + 'px'; }
}
updateArrows();
};
const step = () => Math.max(160, row.clientWidth * 0.8);
left.addEventListener('click', () => row.scrollBy({ left: -step(), behavior: 'smooth' }));
right.addEventListener('click', () => row.scrollBy({ left: step(), behavior: 'smooth' }));
row.addEventListener('scroll', onScroll, { passive: true });
new ResizeObserver(positionArrows).observe(row);
new MutationObserver(positionArrows).observe(row, { childList: true });
positionArrows();
}
/** Enhance every `.scroll-row` currently in the DOM (the static Home rows). */
export function initScrollRows() {
document.querySelectorAll('.scroll-row').forEach(enhanceScrollRow);
}