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:
HoneyandClaude Sonnet 4.6 committed 2026-04-26 13:01:34 +02:00
1 parent ce53c48835
commit 9aded56713
18 files changed
+3134 -2

No files matched your search

+1
View File
@@ -0,0 +1 @@
+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="90" fill="#07070f"/>
<rect width="512" height="512" rx="90" fill="url(#bg)"/>
<defs>
<radialGradient id="bg" cx="30%" cy="40%" r="80%">
<stop offset="0%" stop-color="#1a1a2e"/>
<stop offset="100%" stop-color="#07070f"/>
</radialGradient>
</defs>
<!-- Music note -->
<g fill="#e8a020">
<!-- Note head 1 -->
<ellipse cx="180" cy="340" rx="48" ry="36" transform="rotate(-15 180 340)"/>
<!-- Note head 2 -->
<ellipse cx="332" cy="300" rx="48" ry="36" transform="rotate(-15 332 300)"/>
<!-- Stem -->
<rect x="218" y="160" width="18" height="185" rx="9"/>
<rect x="370" y="120" width="18" height="185" rx="9"/>
<!-- Beam -->
<rect x="218" y="160" width="170" height="22" rx="11"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 855 B

+25
View File
@@ -0,0 +1,25 @@
{
"name": "NovaSonic",
"short_name": "NovaSonic",
"description": "Self-hosted music streaming PWA",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#07070f",
"theme_color": "#07070f",
"icons": [
{
"src": "icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "maskable"
}
]
}
+40
View File
@@ -0,0 +1,40 @@
const CACHE = 'ns-v38';
const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg'];
const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];
self.addEventListener('install', e => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(PRECACHE)));
self.skipWaiting();
});
self.addEventListener('activate', e => {
e.waitUntil(
caches.keys()
.then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
.then(() => self.clients.claim())
.then(() => self.clients.matchAll({ type: 'window', includeUncontrolled: true }))
.then(clients => clients.forEach(c => c.postMessage({ type: 'SW_UPDATED', version: CACHE })))
);
});
self.addEventListener('message', e => {
if (e.data?.type === 'GET_VERSION') {
e.source.postMessage({ type: 'VERSION', version: CACHE });
}
});
self.addEventListener('fetch', e => {
if (!e.request.url.startsWith(self.location.origin)) return;
if (SKIP_PATHS.some(p => e.request.url.includes(p))) return;
e.respondWith(
fetch(e.request)
.then(res => {
if (res.status === 200 && e.request.method === 'GET') {
const clone = res.clone();
caches.open(CACHE).then(c => c.put(e.request, clone));
}
return res;
})
.catch(() => caches.match(e.request).then(r => r || Response.error()))
);
});