address review: signal transient dismissal with an event flow, not an int

An Int counter was already rejected for the now-playing launch signal; use
a broadcast Flow on the shared ContextMenuHost and collect it, so the
dismissal is an event rather than an ever-increasing number.
This commit is contained in:
austinried
2026-10-04 00:56:53 +00:00
parent 1b83a5ff93
commit af6979459b
10 changed files with 43 additions and 30 deletions
@@ -402,13 +402,13 @@ private fun MainNavigation() {
onOpenLicenses = { navController.navigate(Routes.LICENSES) },
onEditServer = { id -> navController.navigate(Routes.editServer(id)) },
onBack = { navController.popBackStack() },
dismissRequests = contextMenuHost.dismissRequests,
dismissals = contextMenuHost.dismissals,
)
}
composable(Routes.DOWNLOADS) {
DownloadsRoute(
onBack = { navController.popBackStack() },
dismissRequests = contextMenuHost.dismissRequests,
dismissals = contextMenuHost.dismissals,
)
}
composable(Routes.SEARCH) {
@@ -607,7 +607,7 @@ private fun MainNavigation() {
AddSourceRoute(
onSaved = { navController.popBackStack() },
onBack = { navController.popBackStack() },
dismissRequests = contextMenuHost.dismissRequests,
dismissals = contextMenuHost.dismissals,
)
}
composable(
@@ -618,7 +618,7 @@ private fun MainNavigation() {
onSaved = { navController.popBackStack() },
onBack = { navController.popBackStack() },
sourceId = entry.arguments?.getLong("sourceId"),
dismissRequests = contextMenuHost.dismissRequests,
dismissals = contextMenuHost.dismissals,
)
}
}
@@ -34,7 +34,6 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -57,6 +56,9 @@ import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.SongDownload
import com.subtracks.data.source.StarType
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import com.subtracks.data.model.Album as AlbumModel
import com.subtracks.data.model.Artist as ArtistModel
@@ -149,10 +151,11 @@ class ContextMenuHost {
var actions by mutableStateOf(ItemActions())
private set
// Bumped when an external event (opening now playing) should close menu-like UI the host does
private val transientDismissals = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
// Fires when an external event (opening now playing) should close menu-like UI the host does
// not own, such as the library's list options sheet.
var dismissRequests by mutableIntStateOf(0)
private set
val dismissals: Flow<Unit> = transientDismissals.asSharedFlow()
fun show(
target: MenuTarget,
@@ -169,17 +172,17 @@ class ContextMenuHost {
fun dismissTransients() {
dismiss()
dismissRequests++
transientDismissals.tryEmit(Unit)
}
}
@Composable
internal fun DismissOnRequest(
dismissRequests: Int,
dismissals: Flow<Unit>,
onDismiss: () -> Unit,
) {
LaunchedEffect(dismissRequests) {
if (dismissRequests > 0) onDismiss()
LaunchedEffect(dismissals) {
dismissals.collect { onDismiss() }
}
}
@@ -51,6 +51,8 @@ import com.subtracks.data.model.DownloadedSong
import com.subtracks.ui.components.DeleteDownloadsDialog
import com.subtracks.ui.components.DismissOnRequest
import com.subtracks.ui.components.EmptyState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import org.koin.compose.viewmodel.koinViewModel
sealed interface DownloadDeleteTarget {
@@ -71,12 +73,12 @@ sealed interface DownloadDeleteTarget {
@Composable
fun DownloadsRoute(
onBack: () -> Unit,
dismissRequests: Int = 0,
dismissals: Flow<Unit> = emptyFlow(),
viewModel: DownloadsViewModel = koinViewModel(),
) {
val tree by viewModel.tree.collectAsStateWithLifecycle()
var pending by remember { mutableStateOf<DownloadDeleteTarget?>(null) }
DismissOnRequest(dismissRequests) { pending = null }
DismissOnRequest(dismissals) { pending = null }
DownloadsScreen(
tree = tree,
loadEncoding = viewModel::encoding,
@@ -46,6 +46,7 @@ import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.rememberArtworkColors
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@@ -74,7 +75,7 @@ fun AlbumDetailRoute(
CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack)
val scope = rememberCoroutineScope()
var pendingDelete by remember { mutableStateOf<Long?>(null) }
DismissOnRequest(contextMenuHost?.dismissRequests ?: 0) { pendingDelete = null }
DismissOnRequest(contextMenuHost?.dismissals ?: emptyFlow()) { pendingDelete = null }
val playback by playbackController.state.collectAsStateWithLifecycle()
val context = playback.context
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
@@ -105,6 +105,7 @@ import com.subtracks.ui.theme.baseArtworkColors
import com.subtracks.ui.theme.heroBarColor
import com.subtracks.ui.theme.rememberArtworkColors
import com.subtracks.ui.theme.rememberOverlaidNameBusy
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@@ -160,7 +161,7 @@ fun ArtistDetailRoute(
val albumDownloads by viewModel.albumDownloads.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) }
DismissOnRequest(contextMenuHost?.dismissRequests ?: 0) { pendingDelete = null }
DismissOnRequest(contextMenuHost?.dismissals ?: emptyFlow()) { pendingDelete = null }
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
val actions =
ItemActions(
@@ -134,6 +134,7 @@ import com.subtracks.ui.theme.librarySurfaceColor
import com.subtracks.ui.theme.playerSurfaceColor
import com.subtracks.ui.theme.rememberArtworkColors
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@@ -270,7 +271,7 @@ fun LibraryRoute(
val downloads = downloadsState?.value ?: emptyMap()
val scope = rememberCoroutineScope()
var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) }
DismissOnRequest(contextMenuHost?.dismissRequests ?: 0) { pendingDelete = null }
DismissOnRequest(contextMenuHost?.dismissals ?: emptyFlow()) { pendingDelete = null }
val itemActions =
ItemActions(
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
@@ -341,7 +342,7 @@ fun LibraryRoute(
albumDownloads = downloads.takeIf { listTab == LibraryListTab.Albums }.orEmpty(),
artistDownloads = downloads.takeIf { listTab == LibraryListTab.Artists }.orEmpty(),
playlistDownloads = downloads.takeIf { listTab == LibraryListTab.Playlists }.orEmpty(),
dismissRequests = contextMenuHost?.dismissRequests ?: 0,
dismissals = contextMenuHost?.dismissals ?: emptyFlow(),
homeContent = { topInset, bottomInset ->
HomeRoute(
onAlbumClick = onAlbumClick,
@@ -406,7 +407,7 @@ fun LibraryScreen(
albumDownloads: Map<String, ListDownloadStatus> = emptyMap(),
artistDownloads: Map<String, ListDownloadStatus> = emptyMap(),
playlistDownloads: Map<String, ListDownloadStatus> = emptyMap(),
dismissRequests: Int = 0,
dismissals: Flow<Unit> = emptyFlow(),
homeContent: @Composable (topInset: Dp, bottomInset: Dp) -> Unit = { _, _ -> },
modifier: Modifier = Modifier,
) {
@@ -419,7 +420,7 @@ fun LibraryScreen(
var searchActive by rememberSaveable { mutableStateOf(false) }
val searchFocus = remember { FocusRequester() }
DismissOnRequest(dismissRequests) { showOptions = false }
DismissOnRequest(dismissals) { showOptions = false }
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.settledPage }.collect { page ->
@@ -35,6 +35,7 @@ import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.rememberArtworkColors
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@@ -61,7 +62,7 @@ fun PlaylistDetailRoute(
CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack)
val scope = rememberCoroutineScope()
var pendingDelete by remember { mutableStateOf<Long?>(null) }
DismissOnRequest(contextMenuHost?.dismissRequests ?: 0) { pendingDelete = null }
DismissOnRequest(contextMenuHost?.dismissals ?: emptyFlow()) { pendingDelete = null }
val playback by playbackController.state.collectAsStateWithLifecycle()
val context = playback.context
val requestDownloadAction: (BulkDownloadAction) -> Unit = { action ->
@@ -48,6 +48,8 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.subtracks.R
import com.subtracks.ui.components.DismissOnRequest
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf
@@ -56,7 +58,7 @@ fun AddSourceRoute(
onSaved: () -> Unit,
onBack: (() -> Unit)?,
sourceId: Long? = null,
dismissRequests: Int = 0,
dismissals: Flow<Unit> = emptyFlow(),
viewModel: AddSourceViewModel = koinViewModel(key = sourceId?.toString() ?: "new") { parametersOf(sourceId) },
) {
val state by viewModel.state.collectAsStateWithLifecycle()
@@ -71,7 +73,7 @@ fun AddSourceRoute(
onSave = { viewModel.save(onSaved) },
onDelete = { viewModel.delete(onSaved) },
onBack = onBack,
dismissRequests = dismissRequests,
dismissals = dismissals,
)
}
@@ -88,13 +90,13 @@ fun AddSourceScreen(
onSave: () -> Unit,
onDelete: () -> Unit,
onBack: (() -> Unit)?,
dismissRequests: Int = 0,
dismissals: Flow<Unit> = emptyFlow(),
modifier: Modifier = Modifier,
) {
val nameFocus = remember { FocusRequester() }
val addressFocus = remember { FocusRequester() }
var confirmingDelete by remember { mutableStateOf(false) }
DismissOnRequest(dismissRequests) { confirmingDelete = false }
DismissOnRequest(dismissals) { confirmingDelete = false }
LaunchedEffect(state.nameError, state.addressError) {
when {
state.nameError -> nameFocus.requestFocus()
@@ -58,6 +58,8 @@ import com.subtracks.data.model.Source
import com.subtracks.data.prefs.StreamQuality
import com.subtracks.ui.components.DismissOnRequest
import com.subtracks.ui.components.rememberViewportFill
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import org.koin.compose.viewmodel.koinViewModel
private val bitrateOptions = listOf(0, 24, 32, 64, 96, 128, 192, 256, 320)
@@ -72,7 +74,7 @@ fun SettingsRoute(
onOpenLicenses: () -> Unit,
onEditServer: (Long) -> Unit,
onBack: () -> Unit,
dismissRequests: Int = 0,
dismissals: Flow<Unit> = emptyFlow(),
viewModel: SettingsViewModel = koinViewModel(),
) {
val sources by viewModel.sources.collectAsStateWithLifecycle()
@@ -107,7 +109,7 @@ fun SettingsRoute(
onOpenDownloads = onOpenDownloads,
onOpenLicenses = onOpenLicenses,
onBack = onBack,
dismissRequests = dismissRequests,
dismissals = dismissals,
)
}
@@ -136,11 +138,11 @@ fun SettingsScreen(
onOpenDownloads: () -> Unit,
onOpenLicenses: () -> Unit,
onBack: () -> Unit,
dismissRequests: Int = 0,
dismissals: Flow<Unit> = emptyFlow(),
modifier: Modifier = Modifier,
) {
var dialog by remember { mutableStateOf<SettingsDialog?>(null) }
DismissOnRequest(dismissRequests) { dialog = null }
DismissOnRequest(dismissals) { dialog = null }
val listState = rememberLazyListState()
val fill = rememberViewportFill(listState)
@@ -119,7 +119,7 @@ class LibraryOptionsTest {
onOpenSettings = {},
sortOptions = sortOptionsFor(LibraryTab.Albums),
starredSupported = true,
dismissRequests = host.dismissRequests,
dismissals = host.dismissals,
)
}
}