mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
now playing: dismiss transient UI when the notification opens it
Tapping the media notification opened now playing over whatever was on screen, leaving the item context menu, the library options sheet, the keyboard and any open dialog above it. On a now-playing request, clear focus, dismiss the shared menus and close each route's dialog state.
This commit is contained in:
@@ -45,6 +45,7 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.positionInRoot
|
import androidx.compose.ui.layout.positionInRoot
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -299,6 +300,7 @@ private fun MainNavigation() {
|
|||||||
}
|
}
|
||||||
LaunchedEffect(Unit) { if (nowPlayingOpen) nowPlayingProgress = 1f }
|
LaunchedEffect(Unit) { if (nowPlayingOpen) nowPlayingProgress = 1f }
|
||||||
|
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
val playerVisible = playback.item != null
|
val playerVisible = playback.item != null
|
||||||
LaunchedEffect(playerVisible) {
|
LaunchedEffect(playerVisible) {
|
||||||
if (!playerVisible) {
|
if (!playerVisible) {
|
||||||
@@ -308,6 +310,9 @@ private fun MainNavigation() {
|
|||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
nowPlayingLauncher.openNowPlaying.collect {
|
nowPlayingLauncher.openNowPlaying.collect {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
contextMenuHost.dismissTransients()
|
||||||
|
infoSong = null
|
||||||
showingQueue = false
|
showingQueue = false
|
||||||
if (navController.currentDestination?.route in DETAIL_ROUTES) {
|
if (navController.currentDestination?.route in DETAIL_ROUTES) {
|
||||||
navController.popBackStack(Routes.LIBRARY, inclusive = false)
|
navController.popBackStack(Routes.LIBRARY, inclusive = false)
|
||||||
@@ -397,10 +402,14 @@ private fun MainNavigation() {
|
|||||||
onOpenLicenses = { navController.navigate(Routes.LICENSES) },
|
onOpenLicenses = { navController.navigate(Routes.LICENSES) },
|
||||||
onEditServer = { id -> navController.navigate(Routes.editServer(id)) },
|
onEditServer = { id -> navController.navigate(Routes.editServer(id)) },
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
|
dismissRequests = contextMenuHost.dismissRequests,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Routes.DOWNLOADS) {
|
composable(Routes.DOWNLOADS) {
|
||||||
DownloadsRoute(onBack = { navController.popBackStack() })
|
DownloadsRoute(
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
dismissRequests = contextMenuHost.dismissRequests,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable(Routes.SEARCH) {
|
composable(Routes.SEARCH) {
|
||||||
SearchRoute(
|
SearchRoute(
|
||||||
@@ -598,6 +607,7 @@ private fun MainNavigation() {
|
|||||||
AddSourceRoute(
|
AddSourceRoute(
|
||||||
onSaved = { navController.popBackStack() },
|
onSaved = { navController.popBackStack() },
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
|
dismissRequests = contextMenuHost.dismissRequests,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
@@ -608,6 +618,7 @@ private fun MainNavigation() {
|
|||||||
onSaved = { navController.popBackStack() },
|
onSaved = { navController.popBackStack() },
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
sourceId = entry.arguments?.getLong("sourceId"),
|
sourceId = entry.arguments?.getLong("sourceId"),
|
||||||
|
dismissRequests = contextMenuHost.dismissRequests,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ import androidx.compose.material3.ModalBottomSheet
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -147,6 +149,11 @@ class ContextMenuHost {
|
|||||||
var actions by mutableStateOf(ItemActions())
|
var actions by mutableStateOf(ItemActions())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
// Bumped 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
|
||||||
|
|
||||||
fun show(
|
fun show(
|
||||||
target: MenuTarget,
|
target: MenuTarget,
|
||||||
actions: ItemActions,
|
actions: ItemActions,
|
||||||
@@ -159,6 +166,21 @@ class ContextMenuHost {
|
|||||||
target = null
|
target = null
|
||||||
actions = ItemActions()
|
actions = ItemActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun dismissTransients() {
|
||||||
|
dismiss()
|
||||||
|
dismissRequests++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun DismissOnRequest(
|
||||||
|
dismissRequests: Int,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
LaunchedEffect(dismissRequests) {
|
||||||
|
if (dismissRequests > 0) onDismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import com.subtracks.data.model.DownloadList
|
|||||||
import com.subtracks.data.model.DownloadStatus
|
import com.subtracks.data.model.DownloadStatus
|
||||||
import com.subtracks.data.model.DownloadedSong
|
import com.subtracks.data.model.DownloadedSong
|
||||||
import com.subtracks.ui.components.DeleteDownloadsDialog
|
import com.subtracks.ui.components.DeleteDownloadsDialog
|
||||||
|
import com.subtracks.ui.components.DismissOnRequest
|
||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
@@ -70,10 +71,12 @@ sealed interface DownloadDeleteTarget {
|
|||||||
@Composable
|
@Composable
|
||||||
fun DownloadsRoute(
|
fun DownloadsRoute(
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
|
dismissRequests: Int = 0,
|
||||||
viewModel: DownloadsViewModel = koinViewModel(),
|
viewModel: DownloadsViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
val tree by viewModel.tree.collectAsStateWithLifecycle()
|
val tree by viewModel.tree.collectAsStateWithLifecycle()
|
||||||
var pending by remember { mutableStateOf<DownloadDeleteTarget?>(null) }
|
var pending by remember { mutableStateOf<DownloadDeleteTarget?>(null) }
|
||||||
|
DismissOnRequest(dismissRequests) { pending = null }
|
||||||
DownloadsScreen(
|
DownloadsScreen(
|
||||||
tree = tree,
|
tree = tree,
|
||||||
loadEncoding = viewModel::encoding,
|
loadEncoding = viewModel::encoding,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import com.subtracks.playback.PlaybackController
|
|||||||
import com.subtracks.ui.components.CloseWhenDownloadsGone
|
import com.subtracks.ui.components.CloseWhenDownloadsGone
|
||||||
import com.subtracks.ui.components.ContextMenuHost
|
import com.subtracks.ui.components.ContextMenuHost
|
||||||
import com.subtracks.ui.components.DeleteDownloadsDialog
|
import com.subtracks.ui.components.DeleteDownloadsDialog
|
||||||
|
import com.subtracks.ui.components.DismissOnRequest
|
||||||
import com.subtracks.ui.components.HeroDetailScaffold
|
import com.subtracks.ui.components.HeroDetailScaffold
|
||||||
import com.subtracks.ui.components.HeroHeader
|
import com.subtracks.ui.components.HeroHeader
|
||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
@@ -73,6 +74,7 @@ fun AlbumDetailRoute(
|
|||||||
CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack)
|
CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var pendingDelete by remember { mutableStateOf<Long?>(null) }
|
var pendingDelete by remember { mutableStateOf<Long?>(null) }
|
||||||
|
DismissOnRequest(contextMenuHost?.dismissRequests ?: 0) { pendingDelete = null }
|
||||||
val playback by playbackController.state.collectAsStateWithLifecycle()
|
val playback by playbackController.state.collectAsStateWithLifecycle()
|
||||||
val context = playback.context
|
val context = playback.context
|
||||||
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
|
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ import com.subtracks.ui.components.CloseWhenDownloadsGone
|
|||||||
import com.subtracks.ui.components.ContextMenuHost
|
import com.subtracks.ui.components.ContextMenuHost
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.DeleteDownloadsDialog
|
import com.subtracks.ui.components.DeleteDownloadsDialog
|
||||||
|
import com.subtracks.ui.components.DismissOnRequest
|
||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
import com.subtracks.ui.components.ListDownloadIndicator
|
import com.subtracks.ui.components.ListDownloadIndicator
|
||||||
import com.subtracks.ui.components.MenuTarget
|
import com.subtracks.ui.components.MenuTarget
|
||||||
@@ -159,6 +160,7 @@ fun ArtistDetailRoute(
|
|||||||
val albumDownloads by viewModel.albumDownloads.collectAsStateWithLifecycle()
|
val albumDownloads by viewModel.albumDownloads.collectAsStateWithLifecycle()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) }
|
var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) }
|
||||||
|
DismissOnRequest(contextMenuHost?.dismissRequests ?: 0) { pendingDelete = null }
|
||||||
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
|
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
|
||||||
val actions =
|
val actions =
|
||||||
ItemActions(
|
ItemActions(
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ import com.subtracks.data.source.StarType
|
|||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import com.subtracks.ui.components.ContextMenuHost
|
import com.subtracks.ui.components.ContextMenuHost
|
||||||
import com.subtracks.ui.components.DeleteDownloadsDialog
|
import com.subtracks.ui.components.DeleteDownloadsDialog
|
||||||
|
import com.subtracks.ui.components.DismissOnRequest
|
||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
import com.subtracks.ui.components.MenuTarget
|
import com.subtracks.ui.components.MenuTarget
|
||||||
import com.subtracks.ui.components.PendingDownloadDelete
|
import com.subtracks.ui.components.PendingDownloadDelete
|
||||||
@@ -269,6 +270,7 @@ fun LibraryRoute(
|
|||||||
val downloads = downloadsState?.value ?: emptyMap()
|
val downloads = downloadsState?.value ?: emptyMap()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) }
|
var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) }
|
||||||
|
DismissOnRequest(contextMenuHost?.dismissRequests ?: 0) { pendingDelete = null }
|
||||||
val itemActions =
|
val itemActions =
|
||||||
ItemActions(
|
ItemActions(
|
||||||
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
|
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
|
||||||
@@ -339,6 +341,7 @@ fun LibraryRoute(
|
|||||||
albumDownloads = downloads.takeIf { listTab == LibraryListTab.Albums }.orEmpty(),
|
albumDownloads = downloads.takeIf { listTab == LibraryListTab.Albums }.orEmpty(),
|
||||||
artistDownloads = downloads.takeIf { listTab == LibraryListTab.Artists }.orEmpty(),
|
artistDownloads = downloads.takeIf { listTab == LibraryListTab.Artists }.orEmpty(),
|
||||||
playlistDownloads = downloads.takeIf { listTab == LibraryListTab.Playlists }.orEmpty(),
|
playlistDownloads = downloads.takeIf { listTab == LibraryListTab.Playlists }.orEmpty(),
|
||||||
|
dismissRequests = contextMenuHost?.dismissRequests ?: 0,
|
||||||
homeContent = { topInset, bottomInset ->
|
homeContent = { topInset, bottomInset ->
|
||||||
HomeRoute(
|
HomeRoute(
|
||||||
onAlbumClick = onAlbumClick,
|
onAlbumClick = onAlbumClick,
|
||||||
@@ -403,6 +406,7 @@ fun LibraryScreen(
|
|||||||
albumDownloads: Map<String, ListDownloadStatus> = emptyMap(),
|
albumDownloads: Map<String, ListDownloadStatus> = emptyMap(),
|
||||||
artistDownloads: Map<String, ListDownloadStatus> = emptyMap(),
|
artistDownloads: Map<String, ListDownloadStatus> = emptyMap(),
|
||||||
playlistDownloads: Map<String, ListDownloadStatus> = emptyMap(),
|
playlistDownloads: Map<String, ListDownloadStatus> = emptyMap(),
|
||||||
|
dismissRequests: Int = 0,
|
||||||
homeContent: @Composable (topInset: Dp, bottomInset: Dp) -> Unit = { _, _ -> },
|
homeContent: @Composable (topInset: Dp, bottomInset: Dp) -> Unit = { _, _ -> },
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -415,6 +419,8 @@ fun LibraryScreen(
|
|||||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val searchFocus = remember { FocusRequester() }
|
val searchFocus = remember { FocusRequester() }
|
||||||
|
|
||||||
|
DismissOnRequest(dismissRequests) { showOptions = false }
|
||||||
|
|
||||||
LaunchedEffect(pagerState) {
|
LaunchedEffect(pagerState) {
|
||||||
snapshotFlow { pagerState.settledPage }.collect { page ->
|
snapshotFlow { pagerState.settledPage }.collect { page ->
|
||||||
onTabSelected(LibraryTab.entries[page])
|
onTabSelected(LibraryTab.entries[page])
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.subtracks.playback.PlaybackController
|
|||||||
import com.subtracks.ui.components.CloseWhenDownloadsGone
|
import com.subtracks.ui.components.CloseWhenDownloadsGone
|
||||||
import com.subtracks.ui.components.ContextMenuHost
|
import com.subtracks.ui.components.ContextMenuHost
|
||||||
import com.subtracks.ui.components.DeleteDownloadsDialog
|
import com.subtracks.ui.components.DeleteDownloadsDialog
|
||||||
|
import com.subtracks.ui.components.DismissOnRequest
|
||||||
import com.subtracks.ui.components.HeroDetailScaffold
|
import com.subtracks.ui.components.HeroDetailScaffold
|
||||||
import com.subtracks.ui.components.HeroHeader
|
import com.subtracks.ui.components.HeroHeader
|
||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
@@ -60,6 +61,7 @@ fun PlaylistDetailRoute(
|
|||||||
CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack)
|
CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var pendingDelete by remember { mutableStateOf<Long?>(null) }
|
var pendingDelete by remember { mutableStateOf<Long?>(null) }
|
||||||
|
DismissOnRequest(contextMenuHost?.dismissRequests ?: 0) { pendingDelete = null }
|
||||||
val playback by playbackController.state.collectAsStateWithLifecycle()
|
val playback by playbackController.state.collectAsStateWithLifecycle()
|
||||||
val context = playback.context
|
val context = playback.context
|
||||||
val requestDownloadAction: (BulkDownloadAction) -> Unit = { action ->
|
val requestDownloadAction: (BulkDownloadAction) -> Unit = { action ->
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.subtracks.R
|
import com.subtracks.R
|
||||||
|
import com.subtracks.ui.components.DismissOnRequest
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ fun AddSourceRoute(
|
|||||||
onSaved: () -> Unit,
|
onSaved: () -> Unit,
|
||||||
onBack: (() -> Unit)?,
|
onBack: (() -> Unit)?,
|
||||||
sourceId: Long? = null,
|
sourceId: Long? = null,
|
||||||
|
dismissRequests: Int = 0,
|
||||||
viewModel: AddSourceViewModel = koinViewModel(key = sourceId?.toString() ?: "new") { parametersOf(sourceId) },
|
viewModel: AddSourceViewModel = koinViewModel(key = sourceId?.toString() ?: "new") { parametersOf(sourceId) },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
@@ -69,6 +71,7 @@ fun AddSourceRoute(
|
|||||||
onSave = { viewModel.save(onSaved) },
|
onSave = { viewModel.save(onSaved) },
|
||||||
onDelete = { viewModel.delete(onSaved) },
|
onDelete = { viewModel.delete(onSaved) },
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
|
dismissRequests = dismissRequests,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,11 +88,13 @@ fun AddSourceScreen(
|
|||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onBack: (() -> Unit)?,
|
onBack: (() -> Unit)?,
|
||||||
|
dismissRequests: Int = 0,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val nameFocus = remember { FocusRequester() }
|
val nameFocus = remember { FocusRequester() }
|
||||||
val addressFocus = remember { FocusRequester() }
|
val addressFocus = remember { FocusRequester() }
|
||||||
var confirmingDelete by remember { mutableStateOf(false) }
|
var confirmingDelete by remember { mutableStateOf(false) }
|
||||||
|
DismissOnRequest(dismissRequests) { confirmingDelete = false }
|
||||||
LaunchedEffect(state.nameError, state.addressError) {
|
LaunchedEffect(state.nameError, state.addressError) {
|
||||||
when {
|
when {
|
||||||
state.nameError -> nameFocus.requestFocus()
|
state.nameError -> nameFocus.requestFocus()
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import com.subtracks.R
|
import com.subtracks.R
|
||||||
import com.subtracks.data.model.Source
|
import com.subtracks.data.model.Source
|
||||||
import com.subtracks.data.prefs.StreamQuality
|
import com.subtracks.data.prefs.StreamQuality
|
||||||
|
import com.subtracks.ui.components.DismissOnRequest
|
||||||
import com.subtracks.ui.components.rememberViewportFill
|
import com.subtracks.ui.components.rememberViewportFill
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ fun SettingsRoute(
|
|||||||
onOpenLicenses: () -> Unit,
|
onOpenLicenses: () -> Unit,
|
||||||
onEditServer: (Long) -> Unit,
|
onEditServer: (Long) -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
|
dismissRequests: Int = 0,
|
||||||
viewModel: SettingsViewModel = koinViewModel(),
|
viewModel: SettingsViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
val sources by viewModel.sources.collectAsStateWithLifecycle()
|
val sources by viewModel.sources.collectAsStateWithLifecycle()
|
||||||
@@ -105,6 +107,7 @@ fun SettingsRoute(
|
|||||||
onOpenDownloads = onOpenDownloads,
|
onOpenDownloads = onOpenDownloads,
|
||||||
onOpenLicenses = onOpenLicenses,
|
onOpenLicenses = onOpenLicenses,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
|
dismissRequests = dismissRequests,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,9 +136,11 @@ fun SettingsScreen(
|
|||||||
onOpenDownloads: () -> Unit,
|
onOpenDownloads: () -> Unit,
|
||||||
onOpenLicenses: () -> Unit,
|
onOpenLicenses: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
|
dismissRequests: Int = 0,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var dialog by remember { mutableStateOf<SettingsDialog?>(null) }
|
var dialog by remember { mutableStateOf<SettingsDialog?>(null) }
|
||||||
|
DismissOnRequest(dismissRequests) { dialog = null }
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val fill = rememberViewportFill(listState)
|
val fill = rememberViewportFill(listState)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.test.assertCountEquals
|
||||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.onAllNodesWithTag
|
import androidx.compose.ui.test.onAllNodesWithTag
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
@@ -20,6 +21,7 @@ import com.subtracks.data.model.Artist
|
|||||||
import com.subtracks.data.model.Playlist
|
import com.subtracks.data.model.Playlist
|
||||||
import com.subtracks.data.prefs.ListQuery
|
import com.subtracks.data.prefs.ListQuery
|
||||||
import com.subtracks.data.prefs.StarredFilter
|
import com.subtracks.data.prefs.StarredFilter
|
||||||
|
import com.subtracks.ui.components.ContextMenuHost
|
||||||
import com.subtracks.ui.library.ALBUM_COVER_TAG
|
import com.subtracks.ui.library.ALBUM_COVER_TAG
|
||||||
import com.subtracks.ui.library.LibraryScreen
|
import com.subtracks.ui.library.LibraryScreen
|
||||||
import com.subtracks.ui.library.LibraryTab
|
import com.subtracks.ui.library.LibraryTab
|
||||||
@@ -98,6 +100,44 @@ class LibraryOptionsTest {
|
|||||||
assertEquals(StarredFilter.Any, query.starred)
|
assertEquals(StarredFilter.Any, query.starred)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun theOptionsSheetClosesWhenTransientUiIsDismissed() {
|
||||||
|
val host = ContextMenuHost()
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
LibraryScreen(
|
||||||
|
selectedTab = LibraryTab.Albums,
|
||||||
|
onTabSelected = {},
|
||||||
|
albums = remember { flowOf(PagingData.from(albums())) },
|
||||||
|
artists = remember { flowOf(PagingData.empty<Artist>()) },
|
||||||
|
playlists = remember { flowOf(PagingData.empty<Playlist>()) },
|
||||||
|
coverArt = { _, _ -> null },
|
||||||
|
onAlbumClick = {},
|
||||||
|
onArtistClick = {},
|
||||||
|
onPlaylistClick = {},
|
||||||
|
onSync = {},
|
||||||
|
onOpenSettings = {},
|
||||||
|
sortOptions = sortOptionsFor(LibraryTab.Albums),
|
||||||
|
starredSupported = true,
|
||||||
|
dismissRequests = host.dismissRequests,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composeRule.waitUntil(timeoutMillis = 5_000) {
|
||||||
|
composeRule.onAllNodesWithTag(ALBUM_COVER_TAG).fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
composeRule.onNodeWithContentDescription("List options").performClick()
|
||||||
|
composeRule.mainClock.advanceTimeBy(1_000)
|
||||||
|
composeRule.waitForIdle()
|
||||||
|
composeRule.onNodeWithText("Added").assertExists()
|
||||||
|
|
||||||
|
composeRule.runOnIdle { host.dismissTransients() }
|
||||||
|
composeRule.waitForIdle()
|
||||||
|
|
||||||
|
composeRule.onAllNodesWithText("Added").assertCountEquals(0)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun playSortsAreHiddenUntilTheServerReturnsPlayData() {
|
fun playSortsAreHiddenUntilTheServerReturnsPlayData() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
|||||||
Reference in New Issue
Block a user