Files
NovaSonic/src/main.js
T
HoneyandClaude Sonnet 4.6 e31ddec843 fix(mobile): restore playback state + MediaSession stability
- _restoreLastPlayed: set audio.src + call updateMediaSession so play
  button works immediately and notification shows artwork on app open
- visibilitychange: re-register all MediaSession handlers after screen
  unlock, fold/flip, or background return (fixes vanishing notification)
- Stall watchdog (3s interval): detects frozen audio in background and
  calls play() to recover; also catches missed 'ended' events so
  auto-advance works reliably when screen is off
- stalled/error listeners: attempt audio reload on network hiccup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:53:25 +02:00

237 lines
10 KiB
JavaScript

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,
});