282 lines
12 KiB
JavaScript
282 lines
12 KiB
JavaScript
import { S } from './state.js';
|
|
import { toast } from './utils.js';
|
|
import { api } from './api.js';
|
|
import { injectPWA } from './pwa.js';
|
|
import { loadSavedTheme } from './themes.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, _applyShuffleRepeat, _restoreLastPlayed, setPlayIcon,
|
|
npToggleStar, updateMediaSession,
|
|
} from './player.js';
|
|
import {
|
|
navigate, _navGoBack, _navLoad, onLocalSearch, onMobileSearch,
|
|
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
|
|
searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio,
|
|
loadRandom,
|
|
} from './views.js';
|
|
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll } from './spotify.js';
|
|
import {
|
|
absInitFromStorage, absOpenSettings, absConnectSettings, absSetLibrary, absDisconnect, absVerifyApiKey,
|
|
absPlayItem, absTogglePlay, absSeek, absSkipSecs, absNextChapter, absPrevChapter,
|
|
absSetSpeed, absSleepIn, absOpenPlayer, absClosePlayer, absToggleChapters,
|
|
absToggleSpeedPicker, absToggleSleepPicker, absSeekToChapter, absProgDown, absProgMove,
|
|
absProgUp, loadAbsBooks, absOpenBook, loadAbsBookDetail, absPlayChapter,
|
|
absLoadSettingsLibraries, absToggleChapterTrack, absInterrupt,
|
|
} from './abs.js';
|
|
import {
|
|
openNowPlaying, closeNowPlaying, switchNpTab, npGoArtist, npGoAlbum,
|
|
openNpMenu, closeNpMenu,
|
|
} from './nowplaying.js';
|
|
import {
|
|
openSettings, closeSettings, saveSettings, confirmDisconnect,
|
|
checkBackend, openSidebar, closeSidebar, switchNavMode, showCtxMenu,
|
|
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist,
|
|
createPlaylistPrompt, showSongInfo, closeInfoModal, openDlModal, closeDlModal,
|
|
selectDlType, executeDownload, executeSpotifyDownload, openEditPlModal, closeEditPlModal,
|
|
saveEditPlaylist, editPlUploadCover, editPlCoverSelected, editPlRemoveCover,
|
|
deletePlaylist, _confirmOk, _confirmCancel,
|
|
toggleMobileSearch, closeMobileSearch, switchSetupTab,
|
|
toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, _notifNavigate,
|
|
_pickTheme,
|
|
} 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 _prev = (() => { try { return JSON.parse(localStorage.getItem('ns_cfg') || '{}'); } catch { return {}; } })();
|
|
const lfKey = document.getElementById('lastfm-key')?.value.trim() || _prev.lastfmKey || '';
|
|
const backUrl = (document.getElementById('backend-url')?.value.trim() || _prev.backendUrl || window.NS_BACKEND_URL?.trim() || '').replace(/\/$/,'');
|
|
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};
|
|
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:''};
|
|
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';
|
|
absInitFromStorage();
|
|
if (S.abs.token) {
|
|
document.getElementById('nav-abs-section').style.display = '';
|
|
const bnav = document.getElementById('bnav-audiobooks');
|
|
if (bnav) bnav.style.display = '';
|
|
absLoadSettingsLibraries();
|
|
}
|
|
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');
|
|
}
|
|
initNotifBell();
|
|
_restoreLastPlayed();
|
|
if(localStorage.getItem('ns_radio')==='1') { S.radioMode=true; _applyRadioBtn(); }
|
|
const sr = localStorage.getItem('ns_repeat');
|
|
if(sr === 'all' || sr === 'one') S.repeat = sr;
|
|
if(localStorage.getItem('ns_shuffle') === '1') S.shuffle = true;
|
|
_applyShuffleRepeat();
|
|
}
|
|
|
|
// ─── INIT ───
|
|
window.addEventListener('load', () => {
|
|
loadSavedTheme();
|
|
|
|
// 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);
|
|
if(!S.cfg.backendUrl && window.NS_BACKEND_URL?.trim()) S.cfg.backendUrl = window.NS_BACKEND_URL.trim().replace(/\/$/,'');
|
|
startApp();
|
|
}
|
|
if(window.NS_SERVER_URL?.trim()) { const el=document.getElementById('srv-url'); if(el&&!el.value) el.value=window.NS_SERVER_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', () => {
|
|
absInterrupt();
|
|
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.65;
|
|
|
|
// 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, confirmDisconnect, 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, deletePlaylist, _confirmOk, _confirmCancel,
|
|
toggleMobileSearch, closeMobileSearch, switchSetupTab,
|
|
toggleNotifPanel, deleteNotif, clearNotifs, _notifNavigate, _pickTheme,
|
|
// App
|
|
doConnect, disconnect,
|
|
// Audiobookshelf
|
|
absPlayItem, absTogglePlay, absSeek, absSkipSecs, absNextChapter, absPrevChapter,
|
|
absSetSpeed, absSleepIn, absOpenPlayer, absClosePlayer, absToggleChapters,
|
|
absToggleSpeedPicker, absToggleSleepPicker, absSeekToChapter,
|
|
absProgDown, absProgMove, absProgUp,
|
|
absOpenBook, absPlayChapter, absConnectSettings, absSetLibrary, absDisconnect, absToggleChapterTrack,
|
|
absOnSearch: (val) => {
|
|
clearTimeout(window._absSearchTimer);
|
|
window._absSearchTimer = setTimeout(() => {
|
|
if (S.abs.currentLibraryId) loadAbsBooks(S.abs.currentLibraryId, val.trim());
|
|
}, 350);
|
|
},
|
|
});
|