menus: scroll the context sheet when it outgrows the screen

A multi-artist album or song now adds one row per artist, so the sheet can
exceed the screen and clip the remaining actions. Cap it at 560dp and scroll
the content inside.
This commit is contained in:
austinried
2026-10-18 02:24:39 +00:00
parent 9971128078
commit 61a2472ea9
2 changed files with 101 additions and 63 deletions
@@ -6,11 +6,14 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.QueueMusic
import androidx.compose.material.icons.rounded.Album
@@ -226,76 +229,84 @@ fun ItemContextMenu(
dragHandle = null,
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
MenuHeader(target)
when (target) {
is MenuTarget.Song -> {
queueItems()
DownloadItem(target.download, target.song, actions, ::dismiss)
StarItem(
current = target.song.starred,
onSet = { starring -> actions.setStar(StarType.Song, target.song.id, starring) },
onDismiss = { dismiss() },
)
target.song.albumId?.let { albumId ->
actions.viewAlbum?.let { view ->
MenuItem(
Icons.Rounded.Album,
target.song.album?.takeIf { it.isNotBlank() } ?: stringResource(R.string.resources_filter_album),
) {
dismiss { view(albumId) }
Column(
modifier =
Modifier
.fillMaxWidth()
.heightIn(max = MENU_MAX_HEIGHT)
.verticalScroll(rememberScrollState()),
) {
MenuHeader(target)
when (target) {
is MenuTarget.Song -> {
queueItems()
DownloadItem(target.download, target.song, actions, ::dismiss)
StarItem(
current = target.song.starred,
onSet = { starring -> actions.setStar(StarType.Song, target.song.id, starring) },
onDismiss = { dismiss() },
)
target.song.albumId?.let { albumId ->
actions.viewAlbum?.let { view ->
MenuItem(
Icons.Rounded.Album,
target.song.album?.takeIf { it.isNotBlank() } ?: stringResource(R.string.resources_filter_album),
) {
dismiss { view(albumId) }
}
}
}
target.song.artistRefs.forEach { artist ->
actions.viewArtist?.let { view ->
MenuItem(
Icons.Rounded.Person,
artist.name.takeIf { it.isNotBlank() } ?: stringResource(R.string.resources_filter_artist),
) {
dismiss { view(artist.id) }
}
}
}
onInfo?.let { info ->
MenuItem(Icons.Rounded.Info, stringResource(R.string.info)) { dismiss { info(target.song) } }
}
}
is MenuTarget.Album -> {
queueItems()
BulkDownloadItem(target, actions, ::dismiss)
StarItem(
current = target.album.starred,
onSet = { starring -> actions.setStar(StarType.Album, target.album.id, starring) },
onDismiss = { dismiss() },
)
target.album.artistRefs.forEach { artist ->
actions.viewArtist?.let { view ->
MenuItem(
Icons.Rounded.Person,
artist.name.takeIf { it.isNotBlank() } ?: stringResource(R.string.resources_filter_artist),
) {
dismiss { view(artist.id) }
}
}
}
}
target.song.artistRefs.forEach { artist ->
actions.viewArtist?.let { view ->
MenuItem(
Icons.Rounded.Person,
artist.name.takeIf { it.isNotBlank() } ?: stringResource(R.string.resources_filter_artist),
) {
dismiss { view(artist.id) }
}
}
is MenuTarget.Artist -> {
BulkDownloadItem(target, actions, ::dismiss)
StarItem(
current = target.artist.starred,
onSet = { starring -> actions.setStar(StarType.Artist, target.artist.id, starring) },
onDismiss = { dismiss() },
)
}
onInfo?.let { info ->
MenuItem(Icons.Rounded.Info, stringResource(R.string.info)) { dismiss { info(target.song) } }
is MenuTarget.Playlist -> {
queueItems()
BulkDownloadItem(target, actions, ::dismiss)
}
}
is MenuTarget.Album -> {
queueItems()
BulkDownloadItem(target, actions, ::dismiss)
StarItem(
current = target.album.starred,
onSet = { starring -> actions.setStar(StarType.Album, target.album.id, starring) },
onDismiss = { dismiss() },
)
target.album.artistRefs.forEach { artist ->
actions.viewArtist?.let { view ->
MenuItem(
Icons.Rounded.Person,
artist.name.takeIf { it.isNotBlank() } ?: stringResource(R.string.resources_filter_artist),
) {
dismiss { view(artist.id) }
}
}
}
}
is MenuTarget.Artist -> {
BulkDownloadItem(target, actions, ::dismiss)
StarItem(
current = target.artist.starred,
onSet = { starring -> actions.setStar(StarType.Artist, target.artist.id, starring) },
onDismiss = { dismiss() },
)
}
is MenuTarget.Playlist -> {
queueItems()
BulkDownloadItem(target, actions, ::dismiss)
}
Spacer(Modifier.height(16.dp))
}
Spacer(Modifier.height(16.dp))
}
}
@@ -445,3 +456,4 @@ private fun starLabel(starred: Long?): String =
if (starred == null) stringResource(R.string.actions_star) else stringResource(R.string.actions_unstar)
private const val STAR_DISMISS_DELAY_MS = 200L
private val MENU_MAX_HEIGHT = 560.dp
@@ -9,6 +9,7 @@ import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.model.ArtistRef
import com.subtracks.data.model.Song
@@ -67,4 +68,29 @@ class ItemContextMenuArtistTest {
assertEquals("ar2", opened)
}
@Test
fun longMenusScrollToTheLastArtist() {
var opened: String? = null
composeRule.setContent {
SubtracksTheme {
ItemContextMenu(
target =
MenuTarget.Song(
song.copy(artists = (1..20).map { ArtistRef("ar$it", "Artist $it") }),
),
actions = ItemActions(viewArtist = { opened = it }),
onDismiss = {},
)
}
}
composeRule.onNodeWithText("Artist 20").performScrollTo().assertIsDisplayed()
composeRule.mainClock.autoAdvance = false
composeRule.onNodeWithText("Artist 20").performClick()
composeRule.mainClock.advanceTimeBy(1_000)
assertEquals("ar20", opened)
}
}