import { S } from './state.js'; import { toast } from './utils.js'; import { api } from './api.js'; import { injectPWA } from './pwa.js'; import { playQueue, playCurrent, togglePlay, prevSong, nextSong, onEnded, toggleShuffle, toggleRepeat, toggleQueue, setVolume, seekTo, initProgressScrub, updateProgress, updatePreviewProgress, stopPreview, toggleRadioMode, starCurrent, downloadCurrent, renderQueue, playPreview, toggleStar, dlSongLocal, _applyRadioBtn, _restoreLastPlayed, setPlayIcon, npToggleStar, updateMediaSession, } from './player.js'; import { navigate, _navGoBack, _navLoad, loadHomeData, onLocalSearch, onMobileSearch, onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum, searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio, loadRandom, loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll, } from './views.js'; import { openNowPlaying, closeNowPlaying, switchNpTab, npGoArtist, npGoAlbum, openNpMenu, closeNpMenu, openSettings, closeSettings, saveSettings, checkBackend, openSidebar, closeSidebar, switchNavMode, showCtxMenu, openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt, showSongInfo, closeInfoModal, openDlModal, closeDlModal, selectDlType, executeDownload, executeSpotifyDownload, openEditPlModal, closeEditPlModal, saveEditPlaylist, editPlUploadCover, editPlCoverSelected, editPlRemoveCover, toggleMobileSearch, closeMobileSearch, switchSetupTab, } from './ui.js'; // ─── CONNECT ─── async function doConnect() { const url = document.getElementById('srv-url').value.trim().replace(/\/$/,''); const user = document.getElementById('srv-user').value.trim(); const pass = document.getElementById('srv-pass').value; const lfKey = document.getElementById('lastfm-key').value.trim(); const backUrl = document.getElementById('backend-url').value.trim().replace(/\/$/,''); const spId = document.getElementById('spotify-client-id').value.trim(); const spSecret = document.getElementById('spotify-client-secret').value.trim(); if(!url || !user || !pass) { _showSetupError('Bitte Server-URL, Benutzernamen und Passwort ausfüllen.'); return; } const btn = document.getElementById('btn-connect'); btn.disabled=true; btn.textContent='Verbinde…'; document.getElementById('setup-error').style.display='none'; S.cfg = {url, user, pass, lastfmKey:lfKey, backendUrl:backUrl, spotifyClientId:spId, spotifyClientSecret:spSecret}; try { await api('ping'); localStorage.setItem('ns_cfg', JSON.stringify(S.cfg)); startApp(); } catch(e) { S.cfg = {url:'', user:'', pass:'', lastfmKey:'', backendUrl:''}; _showSetupError('Verbindung fehlgeschlagen: '+e.message); btn.disabled=false; btn.textContent='Verbinden'; } } function _showSetupError(m) { const el=document.getElementById('setup-error'); el.textContent=m; el.style.display='block'; } function disconnect() { localStorage.removeItem('ns_cfg'); localStorage.removeItem('ns_last_played'); sessionStorage.removeItem('ns_nav'); S.cfg = {url:'', user:'', pass:'', lastfmKey:'', backendUrl:'', spotifyClientId:'', spotifyClientSecret:''}; S.queue=[]; S.qIdx=-1; S._navStack=[]; S._navCursor=-1; S.audio.pause(); S.audio.src=''; document.getElementById('app').style.display='none'; document.getElementById('setup-screen').style.display='flex'; } async function startApp() { document.getElementById('setup-screen').style.display='none'; document.getElementById('app').style.display='flex'; if(_navLoad() && S._navCursor >= 0 && S._navStack[S._navCursor]) { const last = S._navStack[S._navCursor]; S._navJumping = true; navigate(last.page, last.data); S._navJumping = false; } else { navigate('home'); } loadHomeData(); _restoreLastPlayed(); if(localStorage.getItem('ns_radio')==='1') { S.radioMode=true; _applyRadioBtn(); } } // ─── INIT ─── window.addEventListener('load', () => { // Wire up audio elements from DOM S.audio = document.getElementById('audio-element'); S.previewAudio = document.getElementById('preview-audio'); const saved = localStorage.getItem('ns_cfg'); if(saved) { S.cfg = JSON.parse(saved); startApp(); } if(window.NS_SERVER_URL?.trim()) { const el=document.getElementById('srv-url'); if(el&&!el.value) el.value=window.NS_SERVER_URL.trim(); } if(window.NS_BACKEND_URL?.trim()) { const el=document.getElementById('backend-url'); if(el&&!el.value) el.value=window.NS_BACKEND_URL.trim(); } document.getElementById('srv-pass').addEventListener('keydown', e => { if(e.key==='Enter') doConnect(); }); S.audio.addEventListener('timeupdate', updateProgress); S.audio.addEventListener('ended', onEnded); S.audio.addEventListener('play', () => { setPlayIcon(true); document.getElementById('player-art').classList.add('spinning'); if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing'; }); S.audio.addEventListener('pause', () => { setPlayIcon(false); document.getElementById('player-art').classList.remove('spinning'); if(navigator.mediaSession) navigator.mediaSession.playbackState = 'paused'; }); S.previewAudio.addEventListener('timeupdate', updatePreviewProgress); S.previewAudio.addEventListener('ended', stopPreview); // Recovery: try to resume if audio stalls (network hiccup, background throttle) S.audio.addEventListener('stalled', () => { if(!S.audio.paused) S.audio.load(); }); S.audio.addEventListener('waiting', () => { /* let browser handle buffering */ }); S.audio.addEventListener('error', () => { if(!S.audio.paused && S.queue[S.qIdx]) { const t = setTimeout(() => { S.audio.load(); S.audio.play().catch(()=>{}); }, 2000); S.audio.addEventListener('play', () => clearTimeout(t), { once: true }); } }); S.audio.volume = 0.8; // Re-register MediaSession handlers when returning from background/screen-off/flip document.addEventListener('visibilitychange', () => { if(document.visibilityState !== 'visible') return; const s = S.queue[S.qIdx]; if(!s) return; updateMediaSession(s); if(navigator.mediaSession) navigator.mediaSession.playbackState = S.audio.paused ? 'paused' : 'playing'; }); // Watchdog: detect when audio silently stalled in background (ended event not fired, or frozen) let _wdLast = 0, _wdStalledAt = 0; setInterval(() => { if(!S.audio.src || S.audio.paused) { _wdStalledAt = 0; _wdLast = 0; return; } const dur = S.audio.duration; const cur = S.audio.currentTime; // ended but event didn't fire (background throttling) if(dur > 0 && cur >= dur - 0.5) { onEnded(); _wdStalledAt = 0; _wdLast = 0; return; } if(cur !== _wdLast) { _wdStalledAt = 0; _wdLast = cur; return; } if(!_wdStalledAt) { _wdStalledAt = Date.now(); return; } // frozen for >8s → try to resume if(Date.now() - _wdStalledAt > 8000) { _wdStalledAt = 0; S.audio.play().catch(() => {}); } }, 3000); injectPWA(); initProgressScrub(); // Click player-art to open Now Playing document.getElementById('player-art')?.addEventListener('click', openNowPlaying); // Swipe-up on player bar to open Now Playing (with drag visual feedback) const playerBar = document.querySelector('.player'); const npOverlay = document.getElementById('np-overlay'); if(playerBar) { let _sy=0, _dragging=false; function _resetDrag(open) { playerBar.classList.remove('dragging'); playerBar.style.transform = ''; if(npOverlay) { npOverlay.style.transition = ''; npOverlay.style.transform = ''; } if(open) openNowPlaying(); _sy=0; _dragging=false; } playerBar.addEventListener('touchstart', e => { if(e.target.closest('button') || e.target.closest('.progress-bar') || e.target.closest('.volume-wrap')) return; _sy=e.touches[0].clientY; _dragging=false; }, {passive:true}); playerBar.addEventListener('touchmove', e => { if(!_sy) return; const dy = _sy - e.touches[0].clientY; if(dy < 5) return; _dragging = true; playerBar.classList.add('dragging'); const lift = Math.min(Math.max(dy, 0), 160); playerBar.style.transform = `translateY(${-lift}px)`; if(npOverlay) { npOverlay.style.transition = 'none'; npOverlay.style.transform = `translateY(${Math.max(0, window.innerHeight - lift * 1.6)}px)`; } }, {passive:true}); playerBar.addEventListener('touchend', e => { if(!_sy) return; const dy = _sy - e.changedTouches[0].clientY; _resetDrag(_dragging ? dy > 60 : dy > 15); }, {passive:true}); playerBar.addEventListener('touchcancel', () => _resetDrag(false), {passive:true}); } }); window.addEventListener('popstate', () => _navGoBack()); document.addEventListener('keydown', e => { if(e.key !== 'Backspace') return; const t = document.activeElement?.tagName; if(t==='INPUT' || t==='TEXTAREA' || t==='SELECT') return; e.preventDefault(); _navGoBack(); }); try { history.replaceState({ nsIdx: 0 }, '', ''); } catch {} // ─── EXPOSE GLOBALS (for HTML onclick attributes) ─── Object.assign(window, { // Navigation navigate, onLocalSearch, onMobileSearch, onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum, searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio, // Player playQueue, togglePlay, prevSong, nextSong, toggleShuffle, toggleRepeat, toggleQueue, setVolume, seekTo, starCurrent, downloadCurrent, playPreview, toggleStar, dlSongLocal, toggleRadioMode, stopPreview, npToggleStar, // Now Playing openNowPlaying, closeNowPlaying, switchNpTab, npGoArtist, npGoAlbum, openNpMenu, closeNpMenu, npSeek: () => {}, // UI / Modals openSettings, closeSettings, saveSettings, checkBackend, openSidebar, closeSidebar, switchNavMode, showCtxMenu, // Views loadRandom, loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll, executeSpotifyDownload, openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt, showSongInfo, closeInfoModal, openDlModal, closeDlModal, selectDlType, executeDownload, openEditPlModal, closeEditPlModal, saveEditPlaylist, editPlUploadCover, editPlCoverSelected, editPlRemoveCover, toggleMobileSearch, closeMobileSearch, switchSetupTab, // App doConnect, disconnect, });