From 61a2472ea96dd196c0d0493f43f879d5be64c11d Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:24:39 +0000 Subject: [PATCH] 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. --- .../ui/components/ItemContextMenu.kt | 138 ++++++++++-------- .../components/ItemContextMenuArtistTest.kt | 26 ++++ 2 files changed, 101 insertions(+), 63 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt b/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt index 958f3129..d4d09719 100644 --- a/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt +++ b/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt @@ -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 diff --git a/app/src/test/kotlin/com/subtracks/ui/components/ItemContextMenuArtistTest.kt b/app/src/test/kotlin/com/subtracks/ui/components/ItemContextMenuArtistTest.kt index dcbbe245..8e878b40 100644 --- a/app/src/test/kotlin/com/subtracks/ui/components/ItemContextMenuArtistTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/components/ItemContextMenuArtistTest.kt @@ -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) + } }