From 453f8a387b0a6cac37aa7b7bfdb92dc7912834da Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 30 May 2026 22:38:48 -0400 Subject: [PATCH] fix(android): remove duplicate TrackActions snackbar wiring (DRY 3c) ShellScaffold already collects TrackActionsViewModel.transientMessages into its own SnackbarHost. Per-route, the shell's hiltViewModel< TrackActionsViewModel>() and the screen's hiltViewModel() resolve to the same NavBackStackEntry-scoped instance, so any kebab action fires the snackbar twice today (once in the screen's own SnackbarHost, once in the shell's). Strip the per-screen wiring from the four shell-wrapped screens that had it (Library, Search, AlbumDetail, PlaylistDetail): VM param, SnackbarHostState, LaunchedEffect, and the Scaffold's snackbarHost. The shell now owns the snackbar exclusively. NowPlayingScreen is intentionally left untouched -- it's outside the shell (full-screen route), so its own snackbar wiring is the only surface that shows the message. --- .../minstrel/library/ui/AlbumDetailScreen.kt | 12 ------------ .../fabledsword/minstrel/library/ui/LibraryScreen.kt | 12 ------------ .../minstrel/playlists/ui/PlaylistDetailScreen.kt | 11 ----------- .../fabledsword/minstrel/search/ui/SearchScreen.kt | 11 ----------- 4 files changed, 46 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt index f92f124d..2d07aa7b 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt @@ -28,12 +28,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState 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 @@ -62,7 +59,6 @@ import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.ServerImage import com.fabledsword.minstrel.shared.widgets.SkeletonTrackRow import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton -import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel import com.fabledsword.minstrel.theme.FabledSwordFlatTokens @OptIn(ExperimentalMaterial3Api::class) @@ -70,16 +66,9 @@ import com.fabledsword.minstrel.theme.FabledSwordFlatTokens fun AlbumDetailScreen( navController: NavHostController, viewModel: AlbumDetailViewModel = hiltViewModel(), - trackActionsViewModel: TrackActionsViewModel = hiltViewModel(), playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(Unit) { - trackActionsViewModel.transientMessages.collect { msg -> - snackbarHostState.showSnackbar(msg) - } - } Scaffold( modifier = Modifier.fillMaxSize(), topBar = { @@ -92,7 +81,6 @@ fun AlbumDetailScreen( }, ) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { inner -> PullToRefreshScaffold( onRefresh = { viewModel.refresh().join() }, diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt index 13efbb58..d01236eb 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt @@ -18,12 +18,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember @@ -52,7 +49,6 @@ import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.SkeletonAlbumTile import com.fabledsword.minstrel.shared.widgets.SkeletonArtistTile -import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel /** * Library tab. Five-tab TabBar (Artists / Albums / History / Liked / @@ -72,16 +68,9 @@ import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewMode fun LibraryScreen( navController: NavHostController, viewModel: LibraryViewModel = hiltViewModel(), - trackActionsViewModel: TrackActionsViewModel = hiltViewModel(), ) { var selectedTab by remember { mutableIntStateOf(0) } val tabs = LIBRARY_TABS - val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(Unit) { - trackActionsViewModel.transientMessages.collect { msg -> - snackbarHostState.showSnackbar(msg) - } - } Scaffold( modifier = Modifier.fillMaxSize(), @@ -115,7 +104,6 @@ fun LibraryScreen( } } }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { inner -> Box(modifier = Modifier.fillMaxSize().padding(inner)) { when (selectedTab) { diff --git a/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt index 1744ff22..5b262c7f 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt @@ -28,8 +28,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -78,7 +76,6 @@ import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.SkeletonTrackRow import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton -import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel import com.fabledsword.minstrel.theme.FabledSwordFlatTokens import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -241,16 +238,9 @@ class PlaylistDetailViewModel @Inject constructor( fun PlaylistDetailScreen( navController: NavHostController, viewModel: PlaylistDetailViewModel = hiltViewModel(), - trackActionsViewModel: TrackActionsViewModel = hiltViewModel(), playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsState() - val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(Unit) { - trackActionsViewModel.transientMessages.collect { msg -> - snackbarHostState.showSnackbar(msg) - } - } LaunchedEffect(Unit) { viewModel.deleted.collect { navController.popBackStack() } } @@ -276,7 +266,6 @@ fun PlaylistDetailScreen( }, ) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { inner -> PullToRefreshScaffold( onRefresh = { viewModel.refresh().join() }, diff --git a/android/app/src/main/java/com/fabledsword/minstrel/search/ui/SearchScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/search/ui/SearchScreen.kt index 36c84f65..4cb90dbb 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/search/ui/SearchScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/search/ui/SearchScreen.kt @@ -25,8 +25,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults @@ -62,27 +60,19 @@ import com.fabledsword.minstrel.shared.widgets.LoadingCentered import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton -import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchScreen( navController: NavHostController, viewModel: SearchViewModel = hiltViewModel(), - trackActionsViewModel: TrackActionsViewModel = hiltViewModel(), playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel = hiltViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() val playerState by playerViewModel.uiState.collectAsStateWithLifecycle() val playingTrackId = playerState.currentTrack?.id val focusRequester = remember { FocusRequester() } - val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(Unit) { focusRequester.requestFocus() } - LaunchedEffect(Unit) { - trackActionsViewModel.transientMessages.collect { msg -> - snackbarHostState.showSnackbar(msg) - } - } Scaffold( modifier = Modifier.fillMaxSize(), @@ -113,7 +103,6 @@ fun SearchScreen( }, ) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { inner -> Box(modifier = Modifier.fillMaxSize().padding(inner)) { ResultsPane(