From ca0fb70370feb5cdf5877840f259c7ee940662db Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 00:56:26 -0400 Subject: [PATCH] =?UTF-8?q?feat(android):=20TrackActions=20hidden-state=20?= =?UTF-8?q?reacts=20to=20SSE=20(audit=20v3=20Bug-9/=C2=A74.12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../trackactions/TrackActionsViewModel.kt | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/trackactions/TrackActionsViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/trackactions/TrackActionsViewModel.kt index 9d885dbd..7e4748ea 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/trackactions/TrackActionsViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/trackactions/TrackActionsViewModel.kt @@ -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(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 = @@ -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)}") } } }