feat(android): TrackActions hidden-state reacts to SSE (audit v3 Bug-9/§4.12)

TrackActionsViewModel fetched the hidden-track snapshot only at init
and after local flag/unflag, so a hide/unhide from another device
left the sheet's Hide/Unhide label stale until the VM was recreated.
Now it subscribes to EventsStream and refreshes on any quarantine.*
event (flagged / unflagged / resolved / file_deleted /
deleted_via_lidarr).

Also routed the radio / append-to-playlist / hide / unhide failure
snackbars through ErrorCopy.fromThrowable instead of raw it.message,
keeping the action-context prefix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 00:56:26 -04:00
parent 1d1a27540a
commit ca0fb70370
@@ -2,6 +2,8 @@ package com.fabledsword.minstrel.shared.widgets.trackactions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fabledsword.minstrel.api.ErrorCopy
import com.fabledsword.minstrel.events.EventsStream
import com.fabledsword.minstrel.likes.data.LikesRepository
import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.player.PlayerController
@@ -15,6 +17,7 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -31,9 +34,10 @@ import javax.inject.Inject
* Hidden-state observation: [QuarantineRepository] doesn't yet expose
* a Flow for the caller's quarantine list (no Room cache; see the
* repository's class comment), so this VM holds its own snapshot of
* the hidden track-id set. Refreshed on init and after every flag /
* unflag action so the sheet reflects the current state next time
* it opens for the same track.
* the hidden track-id set. Refreshed on init, after every flag /
* unflag action, AND on any `quarantine.*` SSE event so a hide/unhide
* from another device updates the sheet's Hide/Unhide label without
* a manual reload.
*/
@HiltViewModel
class TrackActionsViewModel @Inject constructor(
@@ -41,6 +45,7 @@ class TrackActionsViewModel @Inject constructor(
private val quarantine: QuarantineRepository,
private val player: PlayerController,
private val playlists: PlaylistsRepository,
private val eventsStream: EventsStream,
) : ViewModel() {
private val messages = MutableSharedFlow<String>(extraBufferCapacity = 4)
@@ -51,6 +56,11 @@ class TrackActionsViewModel @Inject constructor(
init {
refreshHidden()
viewModelScope.launch {
eventsStream.events
.filter { it.kind.startsWith("quarantine.") }
.collect { refreshHidden() }
}
}
fun isLikedFlow(trackId: String): Flow<Boolean> =
@@ -84,7 +94,9 @@ class TrackActionsViewModel @Inject constructor(
fun startRadio(trackId: String) {
viewModelScope.launch {
runCatching { player.startRadio(trackId) }
.onFailure { messages.tryEmit("Couldn't start radio: ${it.message ?: "unknown error"}") }
.onFailure {
messages.tryEmit("Couldn't start radio: ${ErrorCopy.fromThrowable(it)}")
}
}
}
@@ -92,7 +104,9 @@ class TrackActionsViewModel @Inject constructor(
viewModelScope.launch {
runCatching { playlists.appendTrack(playlistId, trackId) }
.onSuccess { messages.tryEmit("Added to playlist") }
.onFailure { messages.tryEmit("Couldn't add to playlist: ${it.message ?: "unknown error"}") }
.onFailure {
messages.tryEmit("Couldn't add to playlist: ${ErrorCopy.fromThrowable(it)}")
}
}
}
@@ -100,7 +114,7 @@ class TrackActionsViewModel @Inject constructor(
viewModelScope.launch {
runCatching { quarantine.flag(track.id, reason, notes) }
.onSuccess { refreshHidden() }
.onFailure { messages.tryEmit("Couldn't hide: ${it.message ?: "unknown error"}") }
.onFailure { messages.tryEmit("Couldn't hide: ${ErrorCopy.fromThrowable(it)}") }
}
}
@@ -108,7 +122,7 @@ class TrackActionsViewModel @Inject constructor(
viewModelScope.launch {
runCatching { quarantine.unflag(trackId) }
.onSuccess { refreshHidden() }
.onFailure { messages.tryEmit("Couldn't unhide: ${it.message ?: "unknown error"}") }
.onFailure { messages.tryEmit("Couldn't unhide: ${ErrorCopy.fromThrowable(it)}") }
}
}