diff --git a/CHANGELOG.md b/CHANGELOG.md index 709948c..b23b217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,13 @@ like Android), concert events become **reliable across web/Android**, several An **YouTube downloads are recovered** after they broke on two independent fronts. ### Added +- **Android APK download in Settings (Web)** β€” a "πŸ€– Android App" section in Settings offers the + latest APK so co-users self-serve instead of the maintainer hand-delivering builds. The backend + serves the staged APK (`GET /api/apk`, metadata at `/api/apk/info`); the button appears only when an + APK is present and shows version/size/date. **Maintainer workflow:** drop the release build into the + backend's data volume β€” `docker cp app-release.apk novasonic-backend:/data/novasonic.apk` (optional + `docker cp version.txt novasonic-backend:/data/novasonic.version.txt`). The Android *source* is + already in the repo; only build artifacts stay out of git. - **Unified Global Search field (Web)** β€” the separate Song-Importer page/sidebar entry is gone. The one search field now auto-detects: a **link** (Spotify/SoundCloud/YouTube) opens the importer preview, plain **text** runs a Deezer search. Cleaner left nav, same behaviour as Android. diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 5e3f32f..a26e2ef 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -466,6 +466,33 @@ app.get('/api/events-debug/:artist', (req, res) => { }).on('error', e => res.json({ error: e.message })); }); +// ── NovaSonic Android APK distribution ────────────────────────────────────── +// The web app offers a "download the Android app" button that points here, so co-users always get +// the latest APK without the maintainer hand-delivering it. Drop the latest release build into the +// writable data volume (APK_PATH, default /data/novasonic.apk): +// docker cp app-release.apk novasonic-backend:/data/novasonic.apk +// Optional: a sidecar /data/novasonic.version.txt with a version string (e.g. "2.2") shown in the UI. +const APK_PATH = process.env.APK_PATH || path.join(CONFIG.dataPath, 'novasonic.apk'); +const APK_VER_PATH = path.join(CONFIG.dataPath, 'novasonic.version.txt'); + +app.get('/api/apk/info', (req, res) => { + fs.stat(APK_PATH, (err, st) => { + if (err) return res.json({ available: false }); + let version = null; + try { version = fs.readFileSync(APK_VER_PATH, 'utf8').trim() || null; } catch {} + res.json({ available: true, size: st.size, updatedAt: st.mtimeMs, version }); + }); +}); + +app.get('/api/apk', (req, res) => { + fs.stat(APK_PATH, (err) => { + if (err) return res.status(404).json({ error: 'Keine APK hinterlegt' }); + res.setHeader('Content-Type', 'application/vnd.android.package-archive'); + res.setHeader('Content-Disposition', 'attachment; filename="NovaSonic.apk"'); + res.sendFile(APK_PATH); + }); +}); + app.get('/api/health', (req, res) => { res.json({ status: 'ok', ytdlpVersion, diff --git a/index.html b/index.html index c9d9325..f0686c7 100644 --- a/index.html +++ b/index.html @@ -605,6 +605,18 @@
+

πŸ“Š diff --git a/src/ui/settings.js b/src/ui/settings.js index 23aa9e8..c0f2c9a 100644 --- a/src/ui/settings.js +++ b/src/ui/settings.js @@ -54,6 +54,27 @@ export function openSettings() { _renderThemePicker(); document.getElementById('settings-modal').classList.add('open'); checkBackend(); + _loadApkInfo(); +} + +// Show the "download the Android APK" button only if the backend actually has an APK staged. +async function _loadApkInfo() { + const section = document.getElementById('s-apk-section'); + const btn = document.getElementById('s-apk-btn'); + const meta = document.getElementById('s-apk-meta'); + if (!section || !btn) return; + section.style.display = 'none'; + const base = S.cfg.backendUrl; + if (!base) return; + try { + const info = await fetch(`${base}/api/apk/info`).then(r => r.json()); + if (!info?.available) return; + btn.href = `${base}/api/apk`; + const mb = info.size ? (info.size / 1048576).toFixed(1) + ' MB' : ''; + const date = info.updatedAt ? new Date(info.updatedAt).toLocaleDateString('de-DE') : ''; + if (meta) meta.textContent = [info.version && `v${info.version}`, mb, date && `Stand ${date}`].filter(Boolean).join(' Β· '); + section.style.display = ''; + } catch {} } function _renderThemePicker() {