diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailScreen.kt index bd8a23a1..7de9db13 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailScreen.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -28,7 +30,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -59,6 +63,12 @@ fun ArtistDetailScreen( viewModel: ArtistDetailViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(Unit) { + viewModel.transientMessages.collect { msg -> + snackbarHostState.showSnackbar(msg) + } + } Scaffold( modifier = Modifier.fillMaxSize(), topBar = { @@ -71,6 +81,7 @@ fun ArtistDetailScreen( }, ) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, ) { inner -> PullToRefreshScaffold( onRefresh = { viewModel.refresh().join() }, diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailViewModel.kt index ef1a41aa..22e73e4c 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailViewModel.kt @@ -13,10 +13,13 @@ import com.fabledsword.minstrel.nav.DetailSeedCache import com.fabledsword.minstrel.player.PlayerController import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -45,6 +48,10 @@ class ArtistDetailViewModel @Inject constructor( MutableStateFlow(ArtistDetailUiState.Loading()) val uiState: StateFlow = internal.asStateFlow() + /** Transient one-shot messages for the screen-level snackbar (e.g. Play-button failure). */ + private val transientMessagesChannel = Channel(Channel.BUFFERED) + val transientMessages: Flow = transientMessagesChannel.receiveAsFlow() + val artistLiked: StateFlow = likes.observeIsLiked(LikesRepository.ENTITY_ARTIST, artistId) .stateIn( @@ -92,7 +99,9 @@ class ArtistDetailViewModel @Inject constructor( viewModelScope.launch { try { val tracks = repository.fetchArtistTracks(artistId).shuffled() - if (tracks.isNotEmpty()) { + if (tracks.isEmpty()) { + transientMessagesChannel.trySend("No tracks to play for this artist.") + } else { player.setQueue( tracks = tracks, initialIndex = 0, @@ -100,11 +109,11 @@ class ArtistDetailViewModel @Inject constructor( ) } } catch ( - @Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable, + @Suppress("TooGenericExceptionCaught") e: Throwable, ) { - // Silent failure — the play button has no inline error - // affordance; user can retry by re-tapping. A future - // refinement surfaces this via a snackbar. + transientMessagesChannel.trySend( + "Couldn't start playback: ${e.message ?: "unknown error"}", + ) } } }