import { S } from './state.js';
import { esc } from './utils.js';
import { coverUrl } from './api.js';
// Circular imports — safe (only used inside function bodies)
import { playCurrent, _syncNpDirect, dlSongLocal, npToggleStar, _syncOverlayState, isPlaying } from './player.js';
import { navigate } from './views.js';
import { openPlModal, showSongInfo } from './ui.js';
// ─── NOW PLAYING OVERLAY ───
export function openNowPlaying() {
S.npOpen = true;
const overlay = document.getElementById('np-overlay');
overlay.classList.add('open');
if(S.npQueueMode) switchNpTab('player');
syncNowPlaying();
_syncOverlayState();
if(!overlay._swipeAttached) {
overlay._swipeAttached = true;
let startY=0, startT=0, dragging=false;
const handle = document.getElementById('np-drag-handle');
function onStart(clientY) { startY=clientY; startT=Date.now(); dragging=true; overlay.style.transition='none'; }
function onMove(clientY) { if(!dragging) return; const dy=Math.max(0,clientY-startY); overlay.style.transform=`translateY(${dy}px)`; }
function onEnd(clientY) {
if(!dragging) return; dragging=false; overlay.style.transition='';
const dy=clientY-startY, dt=Date.now()-startT;
if(dt<250&&dy>50 || dy>window.innerHeight*0.3) closeNowPlaying();
else overlay.style.transform='';
}
overlay.addEventListener('touchstart', e => {
const rect=overlay.getBoundingClientRect();
if(e.touches[0].clientY-rect.top < 100) onStart(e.touches[0].clientY);
}, {passive:true});
overlay.addEventListener('touchmove', e => onMove(e.touches[0].clientY), {passive:true});
overlay.addEventListener('touchend', e => onEnd(e.changedTouches[0].clientY), {passive:true});
handle.addEventListener('mousedown', e => { onStart(e.clientY); e.preventDefault(); });
document.addEventListener('mousemove', e => onMove(e.clientY));
document.addEventListener('mouseup', e => onEnd(e.clientY));
}
}
export function closeNowPlaying() {
S.npOpen = false;
const overlay = document.getElementById('np-overlay');
overlay.style.transform = '';
overlay.classList.remove('open');
_syncOverlayState();
}
export function syncNowPlaying() {
if(!S.npOpen) return;
const s = S.queue[S.qIdx];
if(!s) return;
_syncNpDirect(s);
const shuffleBtn = document.getElementById('np-shuffle-btn');
const repeatBtn = document.getElementById('np-repeat-btn');
const volSlider = document.getElementById('np-vol-slider');
if(shuffleBtn) shuffleBtn.classList.toggle('active', S.shuffle);
if(repeatBtn) repeatBtn.classList.toggle('active', S.repeat !== false);
if(volSlider) volSlider.value = Math.round(Math.sqrt(S.audio.volume) * 100);
const playing = isPlaying();
const npIcon = document.getElementById('np-play-icon');
if(npIcon) npIcon.innerHTML = playing
? ''
: '';
const cover = document.getElementById('np-cover');
if(cover) cover.classList.toggle('playing', playing);
}
export function switchNpTab(tab) {
S.npQueueMode = tab === 'queue';
S.npLyricsMode = tab === 'lyrics';
const overlay = document.getElementById('np-overlay');
overlay.classList.toggle('queue-mode', S.npQueueMode);
overlay.classList.toggle('lyrics-mode', S.npLyricsMode);
document.getElementById('np-tab-player').classList.toggle('active', tab === 'player');
document.getElementById('np-tab-lyrics').classList.toggle('active', S.npLyricsMode);
document.getElementById('np-tab-queue').classList.toggle('active', S.npQueueMode);
if(S.npQueueMode) renderNpQueue();
if(S.npLyricsMode) { const s = S.queue[S.qIdx]; if(s) _loadNpLyrics(s.id); }
}
// ─── LYRICS ───
let _lyricsLines = [];
let _lyricsSongId = null;
let _lyricsLoadToken = 0;
const LYRICS_LOOKAHEAD_MS = 200;
function _parseLrc(text) {
let offsetMs = 0;
const lines = [];
for(const raw of text.split('\n')) {
const offM = raw.match(/^\[offset:\s*([+-]?\d+)\s*\]/i);
if(offM) { offsetMs = parseInt(offM[1]); continue; }
const m = raw.match(/^\[(\d+):(\d+(?:\.\d+)?)\](.*)/);
if(m) lines.push({ start: Math.round((parseInt(m[1])*60 + parseFloat(m[2]))*1000), value: m[3].trim() });
}
if(offsetMs) lines.forEach(l => l.start = Math.max(0, l.start + offsetMs));
return lines.sort((a,b) => a.start - b.start).filter(l => l.value);
}
function _renderSyncedLines(content, lines) {
const top = document.createElement('div');
top.style.cssText = 'height:40vh;flex-shrink:0';
content.appendChild(top);
_lyricsLines = lines.map(line => {
const el = document.createElement('div');
el.className = 'lyrics-line';
el.textContent = line.value;
el.onclick = () => { S.audio.currentTime = line.start / 1000; };
content.appendChild(el);
return { start: line.start, el };
});
const bot = document.createElement('div');
bot.style.cssText = 'height:40vh;flex-shrink:0';
content.appendChild(bot);
updateLyricsHighlight(S.audio.currentTime * 1000);
}
async function _loadNpLyrics(songId) {
const content = document.getElementById('np-lyrics-content');
if(!content) return;
if(_lyricsSongId === songId && (_lyricsLines.length || content.querySelector('.lyrics-plain,.lyrics-none'))) {
if(_lyricsLines.length) updateLyricsHighlight(S.audio.currentTime * 1000);
return;
}
_lyricsSongId = songId;
_lyricsLines = [];
content.innerHTML = '
Lade Lyrics…
';
const token = ++_lyricsLoadToken;
const s = S.queue[S.qIdx];
const { getLyricsBySongId } = await import('./api.js');
if(token !== _lyricsLoadToken) return;
// 1. Try Navidrome structured lyrics
const lyrics = await getLyricsBySongId(songId);
if(token !== _lyricsLoadToken) return;
const track = lyrics[0];
if(track?.synced && track.line?.length) {
content.innerHTML = '';
_renderSyncedLines(content, track.line.map(l => ({ start: l.start, value: l.value || '' })).filter(l => l.value));
return;
}
// 2. Fallback: fetch lyrics from LRCLIB
// Use only first artist (multi-artist strings like "A / B" break lookup)
const firstArtist = (s?.artist||'').split(/[/,]/)[0].trim();
let lrcText = null;
let lrclibPlain = null;
try {
// Try with album first, then without (album mismatch causes 404)
const attempts = [
new URLSearchParams({ artist_name: firstArtist, track_name: s?.title||'', ...(s?.album ? { album_name: s.album } : {}) }),
...(s?.album ? [new URLSearchParams({ artist_name: firstArtist, track_name: s?.title||'' })] : []),
];
for(const p of attempts) {
if(token !== _lyricsLoadToken) return;
const r = await fetch(`https://lrclib.net/api/get?${p}`, { signal: AbortSignal.timeout(6000) });
if(r.ok) {
const d = await r.json();
lrcText = d.syncedLyrics || null;
lrclibPlain = d.plainLyrics || null;
break;
}
}
} catch {}
if(token !== _lyricsLoadToken) return;
content.innerHTML = '';
if(lrcText) {
const parsed = _parseLrc(lrcText);
if(parsed.length) { _renderSyncedLines(content, parsed); return; }
}
// 3. Plain text — prefer LRCLIB plain over Navidrome plain (usually better quality)
const plain = lrclibPlain
|| (track?.line?.length ? track.line.map(l => l.value).join('\n') : null);
if(plain) {
const el = document.createElement('div');
el.className = 'lyrics-plain';
el.textContent = plain;
content.appendChild(el);
} else {
const el = document.createElement('div');
el.className = 'lyrics-none';
el.style.cssText = 'text-align:center;padding:60px 0;color:rgba(255,255,255,.3);font-size:14px';
el.textContent = 'Keine Lyrics verfügbar';
content.appendChild(el);
}
}
export function updateLyricsHighlight(currentMs) {
if(!_lyricsLines.length || !S.npLyricsMode) return;
const adjusted = currentMs + LYRICS_LOOKAHEAD_MS;
let activeIdx = -1;
for(let i = 0; i < _lyricsLines.length; i++) {
if(_lyricsLines[i].start <= adjusted) activeIdx = i;
else break;
}
_lyricsLines.forEach((line, i) => {
line.el.classList.toggle('active', i === activeIdx);
line.el.classList.toggle('past', i < activeIdx);
});
if(activeIdx >= 0) {
const activeEl = _lyricsLines[activeIdx].el;
const container = document.getElementById('np-lyrics-view');
if(container) {
const target = activeEl.offsetTop - container.clientHeight / 2 + activeEl.clientHeight / 2;
container.scrollTo({ top: Math.max(0, target), behavior: 'smooth' });
}
}
}
export function onNpSongChange(songId) {
_lyricsSongId = null;
_lyricsLines = [];
const content = document.getElementById('np-lyrics-content');
if(content) content.innerHTML = '';
if(S.npLyricsMode) _loadNpLyrics(songId);
}
// ─── NP QUEUE ───
export function renderNpQueue() {
const el = document.getElementById('np-queue-list');
if(!el) return;
el.innerHTML = '';
if(!S.queue.length) {
el.innerHTML = 'Warteschlange leer
';
return;
}
S.queue.forEach((s, i) => {
const item = document.createElement('div');
item.className = 'np-queue-item' + (i===S.qIdx ? ' nq-active' : '');
const cv = coverUrl(s.coverArt, 128);
item.innerHTML = `${i+1}
${cv ? `

` : '♪'}
${esc(s.title)}
${esc(s.artist||'')}
`;
item.onclick = () => { S.qIdx=i; playCurrent(); switchNpTab('player'); };
el.appendChild(item);
});
el.querySelector('.nq-active')?.scrollIntoView({block:'nearest'});
}
// ─── NAVIGATION FROM NP ───
export function npGoArtist() {
const s = S.queue[S.qIdx]; if(!s) return;
closeNowPlaying();
if(s.artistId) navigate('artist-detail', {id:s.artistId, name:s.artist});
}
export function npGoAlbum() {
const s = S.queue[S.qIdx]; if(!s || !s.albumId) return;
closeNowPlaying();
navigate('album-detail', {id:s.albumId, name:s.album||'', artist:s.artist||'', artistId:s.artistId||'', coverArt:s.coverArt||''});
}
// ─── NP ACTION SHEET ───
export function openNpMenu() {
const s = S.queue[S.qIdx]; if(!s) return;
const cv = coverUrl(s.coverArt, 256);
document.getElementById('np-sheet-song').innerHTML = `
${cv ? `

` : '
🎵
'}
${esc(s.title)}
${esc(s.artist||'')}${s.album?' · '+esc(s.album):''}
`;
const items = [
{ icon: '', label: 'Song-Info anzeigen', fn: () => { closeNpMenu(); showSongInfo(s); } },
{ icon: '', label: 'Zu Playlist hinzufügen', fn: () => { closeNpMenu(); openPlModal([s]); } },
{ icon: '', label: 'Zum Künstler', fn: () => { closeNpMenu(); npGoArtist(); } },
{ icon: '', label: 'Zum Album', fn: () => { closeNpMenu(); closeNowPlaying(); if(s.albumId) navigate('album-detail',{id:s.albumId,name:s.album,artist:s.artist,artistId:s.artistId,coverArt:s.coverArt}); } },
{ icon: '', label: s._starred||s.starred ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen', fn: () => { closeNpMenu(); npToggleStar(); } },
{ icon: '', label: 'Herunterladen', fn: () => { closeNpMenu(); dlSongLocal(s); } },
];
document.getElementById('np-sheet-items').innerHTML = items.map((item,i) =>
`${item.icon}${item.label}
`
).join('');
document.querySelectorAll('.np-sheet-item').forEach((el,i) => el.addEventListener('click', () => items[i].fn()));
document.getElementById('np-sheet-overlay').classList.add('open');
document.getElementById('np-sheet').classList.add('open');
}
export function closeNpMenu() {
document.getElementById('np-sheet-overlay').classList.remove('open');
document.getElementById('np-sheet').classList.remove('open');
}