Migrate to Vite + ES module architecture
Split monolithic app.js (~1850 lines) into maintainable ES modules: - src/state.js — shared mutable state (S object) - src/utils.js — md5, esc, formatDur, toast - src/api.js — Subsonic API, Navidrome REST, iTunes API - src/player.js — playback, transport, queue, media session, preview - src/views.js — navigation, page loaders, card makers, search - src/ui.js — all modals, overlays, sidebar, settings - src/pwa.js — service worker registration + update banner - src/main.js — app init, connect, window globals Updated Dockerfile to multi-stage (Node build → nginx serve). Static assets (sw.js, manifest.json, icon.svg) moved to public/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ce53c48835
commit
9aded56713
18 files changed
+3134
-2
No files matched your search
+164
@@ -0,0 +1,164 @@
|
||||
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,
|
||||
} from './player.js';
|
||||
import {
|
||||
navigate, _navGoBack, _navLoad, loadHomeData, onLocalSearch, onMobileSearch,
|
||||
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
|
||||
searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio,
|
||||
} 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, 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(/\/$/,'');
|
||||
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';
|
||||
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);
|
||||
S.audio.volume = 0.8;
|
||||
|
||||
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
|
||||
const playerBar = document.querySelector('.player');
|
||||
if(playerBar) {
|
||||
let _sy=0, _st=0;
|
||||
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; _st=Date.now();
|
||||
}, {passive:true});
|
||||
playerBar.addEventListener('touchend', e => {
|
||||
if(!_sy) return;
|
||||
const dy=_sy-e.changedTouches[0].clientY, dt=Date.now()-_st;
|
||||
if(dy>40 || (dt<300&&dy>15)) openNowPlaying();
|
||||
_sy=0;
|
||||
}, {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,
|
||||
// Now Playing
|
||||
openNowPlaying, closeNowPlaying, switchNpTab, npGoArtist, npGoAlbum,
|
||||
openNpMenu, closeNpMenu,
|
||||
// UI / Modals
|
||||
openSettings, closeSettings, saveSettings, checkBackend,
|
||||
openSidebar, closeSidebar, showCtxMenu,
|
||||
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt,
|
||||
showSongInfo, closeInfoModal,
|
||||
openDlModal, closeDlModal, selectDlType, executeDownload,
|
||||
openEditPlModal, closeEditPlModal, saveEditPlaylist,
|
||||
editPlUploadCover, editPlCoverSelected, editPlRemoveCover,
|
||||
toggleMobileSearch, closeMobileSearch, switchSetupTab,
|
||||
// App
|
||||
doConnect, disconnect,
|
||||
});
|
||||
Reference in new issue
Block a user