From 5d6b5527dcf6353e1377d383c3900520653a284c Mon Sep 17 00:00:00 2001 From: Damontecres Date: Wed, 4 Mar 2026 17:53:06 -0500 Subject: [PATCH] Save/restore focus --- .../ui/detail/music/AlbumDetailsPage.kt | 83 ++++++++++++++----- .../ui/detail/music/ArtistDetailsPage.kt | 47 +++++++++-- 2 files changed, 102 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt index 17a3a8ca..dfc1a9b5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt @@ -67,11 +67,14 @@ import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.QuickDetails import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel +import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.ifElse +import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.letNotEmpty +import com.github.damontecres.wholphin.ui.rememberPosition import com.github.damontecres.wholphin.ui.toBaseItems import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager @@ -159,22 +162,7 @@ class AlbumViewModel loading = LoadingState.Success, ) } - if (album.data.imageTags?.contains(ImageType.BACKDROP) == true) { - backdropService.submit(album) - } else { - val artistIds = - album.data.albumArtists - ?.shuffled() - ?.take(50) - ?.map { it.id } - api.itemsApi - .getItems( - ids = artistIds, - imageTypes = listOf(ImageType.BACKDROP), - ).content.items - .firstOrNull() - ?.let { backdropService.submit(BaseItem(it, false)) } - } + updateBackDrop() if (state.value.similar.isEmpty()) { viewModelScope.launchIO { val similar = @@ -216,6 +204,29 @@ class AlbumViewModel } } + fun updateBackDrop() { + state.value.album?.let { album -> + viewModelScope.launchDefault { + if (album.data.imageTags?.contains(ImageType.BACKDROP) == true) { + backdropService.submit(album) + } else { + val artistIds = + album.data.albumArtists + ?.shuffled() + ?.take(50) + ?.map { it.id } + api.itemsApi + .getItems( + ids = artistIds, + imageTypes = listOf(ImageType.BACKDROP), + ).content.items + .firstOrNull() + ?.let { backdropService.submit(BaseItem(it, false)) } + } + } + } + } + fun setFavorite( itemId: UUID, favorite: Boolean, @@ -252,6 +263,11 @@ data class AlbumState( } } +private const val HEADER_ROW = 0 +private const val SONG_ROW = HEADER_ROW + 1 +private const val MUSIC_VIDEO_ROW = SONG_ROW + 1 +private const val SIMILAR_ROW = MUSIC_VIDEO_ROW + 1 + @Composable fun AlbumDetailsPage( itemId: UUID, @@ -285,7 +301,6 @@ fun AlbumDetailsPage( onClickRemoveFromQueue = {}, ) } - when (val loading = state.loading) { is LoadingState.Error -> { ErrorMessage(loading, modifier) @@ -299,13 +314,24 @@ fun AlbumDetailsPage( LoadingState.Success -> { val album = state.album!! - val focusRequester = remember { FocusRequester() } + var position by rememberPosition(0, 0) + val firstFocusRequester = remember { FocusRequester() } val firstBringIntoViewRequester = remember { BringIntoViewRequester() } - LaunchedEffect(Unit) { focusRequester.requestFocus() } val bringIntoViewRequester = remember { BringIntoViewRequester() } val listState = rememberLazyListState() val itemsBefore = 2 + val focusRequesters = + remember { List(SIMILAR_ROW + 1) { FocusRequester() } } + val songFocusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + if (position.row == SONG_ROW) { + songFocusRequester.tryRequestFocus() + } else { + focusRequesters.getOrNull(position.row)?.tryRequestFocus() + } + viewModel.updateBackDrop() + } val backHandlerActive by remember { derivedStateOf { listState.firstVisibleItemIndex > itemsBefore @@ -371,13 +397,16 @@ fun AlbumDetailsPage( }, favorite = album.favorite, buttonOnFocusChanged = { - if (it.isFocused) scope.launch { bringIntoViewRequester.bringIntoView() } + if (it.isFocused) { + position = RowColumn(HEADER_ROW, 0) + scope.launch { bringIntoViewRequester.bringIntoView() } + } }, modifier = Modifier .onFocusChanged { if (it.hasFocus) scope.launch { bringIntoViewRequester.bringIntoView() } - }.focusRequester(focusRequester), + }.focusRequester(focusRequesters[HEADER_ROW]), ) } } @@ -396,7 +425,10 @@ fun AlbumDetailsPage( ) { SongListItem( song = song, - onClick = { viewModel.play(false, index) }, + onClick = { + position = RowColumn(SONG_ROW, index) + viewModel.play(false, index) + }, onLongClick = { if (song != null) { moreDialog = @@ -424,6 +456,9 @@ fun AlbumDetailsPage( Modifier .focusRequester(firstFocusRequester) .bringIntoViewRequester(firstBringIntoViewRequester), + ).ifElse( + position.row == SONG_ROW && position.column == index, + Modifier.focusRequester(songFocusRequester), ), ) } @@ -434,6 +469,7 @@ fun AlbumDetailsPage( title = stringResource(R.string.music_videos), items = state.musicVideos, onClickItem = { index, item -> + position = RowColumn(MUSIC_VIDEO_ROW, index) viewModel.navigationManager.navigateTo(item.destination()) }, onLongClickItem = { index, item -> @@ -450,6 +486,7 @@ fun AlbumDetailsPage( modifier = mod, ) }, + modifier = Modifier.focusRequester(focusRequesters[MUSIC_VIDEO_ROW]), ) } } @@ -459,6 +496,7 @@ fun AlbumDetailsPage( title = stringResource(R.string.more_like_this), items = state.similar, onClickItem = { index, item -> + position = RowColumn(SIMILAR_ROW, index) viewModel.navigationManager.navigateTo(item.destination()) }, onLongClickItem = { index, item -> @@ -475,6 +513,7 @@ fun AlbumDetailsPage( modifier = mod, ) }, + modifier = Modifier.focusRequester(focusRequesters[SIMILAR_ROW]), ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt index 69f640ae..f2b00089 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt @@ -64,12 +64,16 @@ import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.QuickDetails import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel +import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState +import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.letNotEmpty +import com.github.damontecres.wholphin.ui.rememberPosition import com.github.damontecres.wholphin.ui.toBaseItems +import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler @@ -270,6 +274,12 @@ data class ArtistState( } } +private const val HEADER_ROW = 0 +private const val SONG_ROW = HEADER_ROW + 1 +private const val ALBUM_ROW = SONG_ROW + 1 +private const val MUSIC_VIDEO_ROW = ALBUM_ROW + 1 +private const val SIMILAR_ROW = MUSIC_VIDEO_ROW + 1 + @Composable fun ArtistDetailsPage( itemId: UUID, @@ -316,10 +326,17 @@ fun ArtistDetailsPage( } LoadingState.Success -> { + var position by rememberPosition(0, 0) val artist = state.artist!! - val focusRequester = remember { FocusRequester() } + val focusRequesters = + remember { List(SIMILAR_ROW + 1) { FocusRequester() } } + val songFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { - focusRequester.requestFocus() + if (position.row == SONG_ROW) { + songFocusRequester.tryRequestFocus() + } else { + focusRequesters.getOrNull(position.row)?.tryRequestFocus() + } viewModel.init() } val bringIntoViewRequester = remember { BringIntoViewRequester() } @@ -378,13 +395,16 @@ fun ArtistDetailsPage( }, favorite = artist.favorite, buttonOnFocusChanged = { - if (it.isFocused) scope.launch { bringIntoViewRequester.bringIntoView() } + if (it.isFocused) { + position = RowColumn(HEADER_ROW, 0) + scope.launch { bringIntoViewRequester.bringIntoView() } + } }, modifier = Modifier .onFocusChanged { if (it.hasFocus) scope.launch { bringIntoViewRequester.bringIntoView() } - }.focusRequester(focusRequester), + }.focusRequester(focusRequesters[HEADER_ROW]), ) } } @@ -400,7 +420,10 @@ fun ArtistDetailsPage( itemsIndexed(state.topSongs) { index, song -> SongListItem( song = song, - onClick = { song?.let { viewModel.play(it) } }, + onClick = { + position = RowColumn(SONG_ROW, index) + song?.let { viewModel.play(it) } + }, onLongClick = { if (song != null) { moreDialog = @@ -418,9 +441,15 @@ fun ArtistDetailsPage( ) } }, - modifier = Modifier.fillMaxWidth(.75f), showArtist = false, isPlaying = song != null && currentMusic.currentItemId == song.id, + modifier = + Modifier + .fillMaxWidth(.75f) + .ifElse( + position.row == SONG_ROW && position.column == index, + Modifier.focusRequester(songFocusRequester), + ), ) } } @@ -429,6 +458,7 @@ fun ArtistDetailsPage( title = stringResource(R.string.albums), items = state.albums, onClickItem = { index, album -> + position = RowColumn(ALBUM_ROW, index) viewModel.navigationManager.navigateTo(album.destination()) }, onLongClickItem = { index, album -> @@ -444,6 +474,7 @@ fun ArtistDetailsPage( aspectRatio = AspectRatios.SQUARE, ) }, + modifier = Modifier.focusRequester(focusRequesters[ALBUM_ROW]), ) } if (state.musicVideos.isNotEmpty()) { @@ -452,6 +483,7 @@ fun ArtistDetailsPage( title = stringResource(R.string.music_videos), items = state.musicVideos, onClickItem = { index, item -> + position = RowColumn(MUSIC_VIDEO_ROW, index) viewModel.navigationManager.navigateTo(item.destination()) }, onLongClickItem = { index, item -> @@ -468,6 +500,7 @@ fun ArtistDetailsPage( modifier = mod, ) }, + modifier = Modifier.focusRequester(focusRequesters[MUSIC_VIDEO_ROW]), ) } } @@ -477,6 +510,7 @@ fun ArtistDetailsPage( title = stringResource(R.string.more_like_this), items = state.similar, onClickItem = { index, item -> + position = RowColumn(SIMILAR_ROW, index) viewModel.navigationManager.navigateTo(item.destination()) }, onLongClickItem = { index, item -> @@ -493,6 +527,7 @@ fun ArtistDetailsPage( modifier = mod, ) }, + modifier = Modifier.focusRequester(focusRequesters[SIMILAR_ROW]), ) } }