diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c86367..de91adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,16 +72,15 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an ## [2.4] — 2026-09-06 ### Added -- **Wiedergabe fortsetzen, wo du aufgehört hast (Web + Android)** — bisher überlebte im Web nur der - zuletzt gespielte Titel und die Warteschlange, aber **nicht die Position im Titel**; Android - sicherte allein den letzten Song, ohne Warteschlange. Jetzt merken sich beide Warteschlange, - Listenposition und Sekunde im Titel. Web schreibt gedrosselt beim Abspielen sowie sicher beim - Pausieren, Wegwischen und Wechseln des Tabs (`pagehide`/`visibilitychange` statt `beforeunload`, - das am Handy unzuverlässig ist). Android sichert bei jedem Titelwechsel und beim Pausieren und - stellt die Warteschlange nach dem Verbinden wieder her — **vorbereitet, aber nicht abgespielt**: - ein Druck auf Play macht weiter. Lag die Position im letzten Rest des Titels, beginnt er von vorn, - statt zwei Sekunden Abspann zu spielen. Gespeichert werden nur Song-Ids; verschwundene Titel - werden beim Wiederherstellen übersprungen. +- **Zuletzt Gehörtes überlebt den Neustart (Web + Android)** — Android sicherte bisher nur den + letzten Song, ohne Warteschlange: die Now-Playing-Leiste blieb nach dem Öffnen **leer, bis man + einmal auf Play drückte**. Jetzt merken sich beide Plattformen Warteschlange und aktuellen Titel + und stellen sie beim Start wieder her — Android **vorbereitet, aber nicht abgespielt**, damit + einen die App beim Öffnen nicht überfällt; läuft schon etwas, passiert nichts. Der Titel beginnt + von vorn: die Sekunde im Titel wird bewusst **nicht** gespeichert, das spart dauerndes Schreiben + während der Wiedergabe. Gesichert werden nur Song-Ids, weil Titel und Cover sich durch + Metadaten-Bearbeitung ändern können; verschwundene Titel werden übersprungen. + - **Streitpunkte im Musik-Duell (Web + Android)** — gemeinsame Künstler, sortiert nach dem größten **Ungleichgewicht** statt nach der Summe: wo einer 120 Plays hat und der andere 3. Ein Balken zeigt das Kräfteverhältnis. Mindestens vier gemeinsame Plays und 50 % Unwucht, damit ein einzelner diff --git a/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt b/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt index 57ea095..060de14 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt @@ -169,8 +169,7 @@ class MusicRepository(private val context: Context) { suspend fun saveTheme(themeId: String) = prefs.saveTheme(themeId) - suspend fun saveQueueState(ids: List, index: Int, positionMs: Long) = - prefs.saveQueueState(ids, index, positionMs) + suspend fun saveQueueState(ids: List, index: Int) = prefs.saveQueueState(ids, index) suspend fun readQueueState() = prefs.readQueueState() suspend fun saveLastSong(song: com.novasonic.app.api.model.Song) = prefs.saveLastSong(song.id, song.title, song.artist ?: "", song.coverArt) diff --git a/android/app/src/main/kotlin/com/novasonic/app/data/PrefsStore.kt b/android/app/src/main/kotlin/com/novasonic/app/data/PrefsStore.kt index 26ca013..89fd19d 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/data/PrefsStore.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/data/PrefsStore.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow @@ -32,10 +31,11 @@ class PrefsStore(private val context: Context) { val KEY_LAST_SONG_TITLE = stringPreferencesKey("last_song_title") val KEY_LAST_SONG_ARTIST = stringPreferencesKey("last_song_artist") val KEY_LAST_SONG_COVER = stringPreferencesKey("last_song_cover") - // Warteschlange fortsetzen: Song-Ids, Position in der Liste und Position im Titel. + // Warteschlange fortsetzen: Song-Ids und Position in der Liste. Bewusst OHNE Sekunde im + // Titel — der zuletzt gehoerte Titel beginnt von vorn, das genuegt und spart staendiges + // Schreiben waehrend der Wiedergabe. val KEY_QUEUE_IDS = stringPreferencesKey("queue_ids") val KEY_QUEUE_INDEX = intPreferencesKey("queue_index") - val KEY_QUEUE_POSITION = longPreferencesKey("queue_position_ms") } data class Credentials(val serverUrl: String, val username: String, val password: String) { @@ -148,15 +148,14 @@ class PrefsStore(private val context: Context) { * Sichert die Warteschlange als Id-Liste samt Position. Nur Ids, keine vollstaendigen Songs: * Titel und Cover koennen sich aendern (Metadaten-Bearbeitung), die Id ist der stabile Bezug. */ - suspend fun saveQueueState(ids: List, index: Int, positionMs: Long) { + suspend fun saveQueueState(ids: List, index: Int) { context.dataStore.edit { prefs -> - prefs[KEY_QUEUE_IDS] = ids.joinToString(",") - prefs[KEY_QUEUE_INDEX] = index.coerceAtLeast(0) - prefs[KEY_QUEUE_POSITION] = positionMs.coerceAtLeast(0) + prefs[KEY_QUEUE_IDS] = ids.joinToString(",") + prefs[KEY_QUEUE_INDEX] = index.coerceAtLeast(0) } } - data class QueueState(val ids: List, val index: Int, val positionMs: Long) + data class QueueState(val ids: List, val index: Int) suspend fun readQueueState(): QueueState? { val prefs = context.dataStore.data.first() @@ -164,8 +163,7 @@ class PrefsStore(private val context: Context) { if (ids.isEmpty()) return null return QueueState( ids = ids, - index = (prefs[KEY_QUEUE_INDEX] ?: 0).coerceIn(0, ids.lastIndex), - positionMs = prefs[KEY_QUEUE_POSITION] ?: 0L, + index = (prefs[KEY_QUEUE_INDEX] ?: 0).coerceIn(0, ids.lastIndex), ) } diff --git a/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt b/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt index f433b26..7d64f4b 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt @@ -62,16 +62,16 @@ class PlayerController(private val context: Context, private val repo: MusicRepo } /** - * Haelt Warteschlange, Listenposition und Position im Titel fest. ABS-Hoerbuecher bleiben aussen - * vor — die haben ihren eigenen Fortschritt auf dem Server. + * Haelt Warteschlange und Listenposition fest — ohne die Sekunde im Titel: der zuletzt gehoerte + * Titel beginnt von vorn. ABS-Hoerbuecher bleiben aussen vor, die haben ihren eigenen + * Fortschritt auf dem Server. */ private fun persistQueueState() { val ctrl = controller ?: return val ids = queue.map { it.id } if (ids.isEmpty() || ids.any { it.startsWith("abs:") }) return val index = ctrl.currentMediaItemIndex - val pos = ctrl.currentPosition - scope.launch { runCatching { repo.saveQueueState(ids, index, pos) } } + scope.launch { runCatching { repo.saveQueueState(ids, index) } } } /** @@ -89,7 +89,7 @@ class PlayerController(private val context: Context, private val repo: MusicRepo if (songs.isEmpty()) return@launch val index = zustand.index.coerceIn(0, songs.lastIndex) queue = songs - ctrl.setMediaItems(songs.map { it.toMediaItem(repo) }, index, zustand.positionMs) + ctrl.setMediaItems(songs.map { it.toMediaItem(repo) }, index, 0L) ctrl.prepare() syncState() } diff --git a/src/main.js b/src/main.js index 7b9cd4f..300a78f 100644 --- a/src/main.js +++ b/src/main.js @@ -13,7 +13,7 @@ import { toggleShuffle, toggleRepeat, toggleQueue, setVolume, seekTo, initProgressScrub, updateProgress, updatePreviewProgress, stopPreview, toggleRadioMode, starCurrent, downloadCurrent, renderQueue, playPreview, - toggleStar, dlSongLocal, _applyRadioBtn, _applyShuffleRepeat, _restoreLastPlayed, persistPosition, setPlayIcon, + toggleStar, dlSongLocal, _applyRadioBtn, _applyShuffleRepeat, _restoreLastPlayed, setPlayIcon, npToggleStar, updateMediaSession, updateNpColorMode, reconcileNativeState, initKeyboardShortcuts, } from './player/player.js'; import { @@ -158,9 +158,6 @@ window.addEventListener('load', () => { let _errRetries = 0, _errTimer = null; function bindAudioEl(el) { el.addEventListener('timeupdate', updateProgress); - // Position mitschreiben (gedrosselt), damit ein Neuladen dort weitermacht, wo du warst. - el.addEventListener('timeupdate', () => persistPosition()); - el.addEventListener('pause', () => persistPosition(true)); el.addEventListener('ended', onEnded); el.addEventListener('play', () => { S._trackChanging = false; @@ -318,8 +315,6 @@ window.addEventListener('load', () => { } }); -window.addEventListener('pagehide', () => persistPosition(true)); -document.addEventListener('visibilitychange', () => { if (document.hidden) persistPosition(true); }); window.addEventListener('popstate', (e) => _navOnPopState(e.state)); document.addEventListener('keydown', e => { if(e.key !== 'Backspace') return; diff --git a/src/player/player.js b/src/player/player.js index 8c23718..1bfcf70 100644 --- a/src/player/player.js +++ b/src/player/player.js @@ -1364,25 +1364,6 @@ export function reconcileNativeState() { } catch {} } -// Abspielposition mitschreiben. Der Eintrag entsteht beim Titelwechsel; hier wird nur die Position -// nachgetragen, damit ein Neuladen dort weitermacht, wo du warst — nicht am Anfang des Titels. -// Gedrosselt, weil timeupdate mehrmals pro Sekunde feuert und localStorage synchron schreibt. -let _posLastWrite = 0; -export function persistPosition(force = false) { - if (HAS_BRIDGE) return; // nativ verwaltet Media3 den Zustand - const jetzt = Date.now(); - if (!force && jetzt - _posLastWrite < 5000) return; - _posLastWrite = jetzt; - try { - const roh = localStorage.getItem('ns_last_played'); - if (!roh) return; - const daten = JSON.parse(roh); - daten.position = S.audio?.currentTime || 0; - daten.savedAt = Math.floor(jetzt / 1000); - localStorage.setItem('ns_last_played', JSON.stringify(daten)); - } catch {} -} - export function _restoreLastPlayed() { try { const saved = JSON.parse(localStorage.getItem('ns_last_played') || 'null'); @@ -1407,20 +1388,6 @@ export function _restoreLastPlayed() { // Web path: prime the audio element so play button works immediately. S.audio.src = streamUrl(s.id); S.audio.load(); - // Gespeicherte Position wiederherstellen, sobald die Dauer bekannt ist. Lag sie im letzten - // Rest des Titels, faengt der Titel lieber von vorn an — sonst spielt man beim Fortsetzen - // zwei Sekunden Abspann und springt weiter. - const pos = Number(saved.position) || 0; - if (pos > 1) { - const setzen = () => { - const rest = (S.audio.duration || 0) - pos; - if (S.audio.duration && rest > 5) { - try { S.audio.currentTime = pos; } catch {} - updateProgress(); - } - S.audio.removeEventListener('loadedmetadata', setzen); - }; - S.audio.addEventListener('loadedmetadata', setzen); - } + } catch {} }