From 4171d0b861db5ae468d7902f42bce043059c34d3 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sat, 25 Jul 2026 16:13:31 -0400 Subject: [PATCH] Minor UI changes to playlist details page (#1749) ## Description Makes some minor changes to the playlist details page - Center playlist name - Show duration with seconds for songs - Adjust some padding Also fixes an issue where focus could be lost when leaving the page while focused on a playlist item Dev note: bulk of the change in thie PR is refactoring the `LazyColumn` of playlist items into its own composable ### Related issues N/A ### Testing Emulator ## Screenshots N/A, the changes are very minor ## AI or LLM usage None --- .../wholphin/ui/detail/PlaylistDetails.kt | 286 ++++++++++-------- 1 file changed, 157 insertions(+), 129 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index 5859df56..1e570c61 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -3,7 +3,6 @@ package com.github.damontecres.wholphin.ui.detail import android.content.Context import androidx.compose.foundation.background import androidx.compose.foundation.focusGroup -import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement @@ -81,6 +80,7 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ExpandableFaButton import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton import com.github.damontecres.wholphin.ui.components.FilterByButton +import com.github.damontecres.wholphin.ui.components.GridTitle import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.MusicContextActions import com.github.damontecres.wholphin.ui.components.Optional @@ -102,6 +102,7 @@ import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.roundMinutes +import com.github.damontecres.wholphin.ui.roundSeconds import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.util.LocalClock @@ -367,7 +368,6 @@ fun PlaylistDetails( ), addToPlaylistViewModel: AddPlaylistViewModel = hiltViewModel(), ) { - val context = LocalContext.current val state by viewModel.state.collectAsState() val musicState by viewModel.musicState.collectAsState() @@ -405,19 +405,21 @@ fun PlaylistDetails( } } val musicContextActions = - MusicContextActions( - navigateTo = { viewModel.navigationManager.navigateTo(it) }, - onClickPlay = { index, item -> play(index, item, false, MediaType.AUDIO) }, - onClickPlayNext = { index, item -> viewModel.playNext(item) }, - onClickAddToQueue = { item -> viewModel.addToQueue(item, Int.MAX_VALUE) }, - onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) }, - onClickAddPlaylist = { itemId -> - addToPlaylistViewModel.loadPlaylists() - showPlaylistDialog.makePresent(itemId) - }, - onClickRemoveFromQueue = { _, _ -> }, - onDeleteItem = viewModel::deleteItem, - ) + remember { + MusicContextActions( + navigateTo = { viewModel.navigationManager.navigateTo(it) }, + onClickPlay = { index, item -> play(index, item, false, MediaType.AUDIO) }, + onClickPlayNext = { index, item -> viewModel.playNext(item) }, + onClickAddToQueue = { item -> viewModel.addToQueue(item, Int.MAX_VALUE) }, + onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) }, + onClickAddPlaylist = { itemId -> + addToPlaylistViewModel.loadPlaylists() + showPlaylistDialog.makePresent(itemId) + }, + onClickRemoveFromQueue = { _, _ -> }, + onDeleteItem = viewModel::deleteItem, + ) + } val contextActions = remember { ContextMenuActions( @@ -534,7 +536,6 @@ fun PlaylistDetailsContent( ) { var savedIndex by rememberSaveable { mutableIntStateOf(0) } var focusedIndex by remember { mutableIntStateOf(savedIndex) } - val focus = remember { FocusRequester() } val focusedItem = items.getOrNull(focusedIndex) LaunchedEffect(focusedItem) { focusedItem?.let(onChangeBackdrop) @@ -555,9 +556,15 @@ fun PlaylistDetailsContent( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier - .padding(top = 16.dp) .fillMaxSize(), ) { + val title = + if (loadingState is LoadingState.Success) { + playlist?.name ?: stringResource(R.string.playlist) + } else { + "" + } + GridTitle(title) Row( horizontalArrangement = Arrangement.spacedBy(24.dp), modifier = @@ -577,115 +584,28 @@ fun PlaylistDetailsContent( .padding(top = 80.dp) .fillMaxWidth(.25f), ) - when (loadingState) { - is LoadingState.Error -> { - ErrorMessage(loadingState, modifier) - } - - LoadingState.Pending, LoadingState.Loading -> { - LoadingPage(modifier) - } - - LoadingState.Success -> { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) { - Text( - text = playlist?.name ?: stringResource(R.string.playlist), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.displayMedium, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) - if (items.isNotEmpty()) { - LazyColumn( - contentPadding = PaddingValues(8.dp), - modifier = - Modifier - .padding(bottom = 32.dp) - .fillMaxHeight() -// .fillMaxWidth(.8f) - .weight(1f) - .background( - MaterialTheme.colorScheme - .surfaceColorAtElevation(1.dp) - .copy(alpha = .75f), - shape = RoundedCornerShape(16.dp), - ).focusProperties { - onExit = { - playButtonFocusRequester.tryRequestFocus() - } - }.focusRequester(focusRequester) - .focusGroup() - .focusRestorer(focus), - ) { - itemsIndexed(items) { index, item -> - PlaylistItem( - item = item, - index = index, - onClick = { - savedIndex = index - item?.let { - onClickIndex.invoke(index, item) - } - }, - onLongClick = { - savedIndex = index - item?.let { - onLongClickIndex.invoke(index, item) - } - }, - isPlaying = - equalsNotNull( - musicState.currentItemId, - item?.id, - ), - isQueued = item?.id in musicState.queuedIds, - modifier = - Modifier - .ifElse( - item?.type != BaseItemKind.AUDIO, - Modifier.height(80.dp), - ).ifElse( - index == savedIndex, - Modifier.focusRequester(focus), - ).onFocusChanged { - if (it.isFocused) { - focusedIndex = index - } - }.focusProperties { - left = playButtonFocusRequester - previous = playButtonFocusRequester - }, - ) - } - } - } else { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = stringResource(R.string.no_results), - style = MaterialTheme.typography.titleLarge, - textAlign = TextAlign.Center, - modifier = - Modifier - .focusProperties { - onExit = { - playButtonFocusRequester.tryRequestFocus() - } - }.focusRequester(focusRequester) - .focusable(), - ) - } - } - } - } - } + PlaylistItems( + loadingState = loadingState, + items = items, + musicState = musicState, + playButtonFocusRequester = playButtonFocusRequester, + onFocusItem = { index, item -> + focusedIndex = index + }, + onClickItem = { index, item -> + savedIndex = index + item?.let { onClickIndex.invoke(index, item) } + }, + onLongClickItem = { index, item -> + savedIndex = index + item?.let { onLongClickIndex.invoke(index, item) } + }, + modifier = + Modifier + .padding(horizontal = 16.dp) + .weight(1f) + .focusRequester(focusRequester), + ) } } } @@ -771,6 +691,97 @@ fun PlaylistDetailsHeader( } } +@Composable +fun PlaylistItems( + loadingState: LoadingState, + items: List, + musicState: MusicServiceState, + playButtonFocusRequester: FocusRequester, + onFocusItem: (Int, BaseItem?) -> Unit, + onClickItem: (Int, BaseItem?) -> Unit, + onLongClickItem: (Int, BaseItem?) -> Unit, + modifier: Modifier = Modifier, +) { + when (loadingState) { + is LoadingState.Error -> { + ErrorMessage(loadingState, modifier) + } + + LoadingState.Pending, LoadingState.Loading -> { + LoadingPage(modifier) + } + + LoadingState.Success -> { + if (items.isNotEmpty()) { + LazyColumn( + contentPadding = PaddingValues(8.dp), + modifier = + modifier + .padding(bottom = 32.dp) + .fillMaxHeight() + .background( + MaterialTheme.colorScheme + .surfaceColorAtElevation(1.dp) + .copy(alpha = .75f), + shape = RoundedCornerShape(16.dp), + ).focusProperties { + left = playButtonFocusRequester + previous = playButtonFocusRequester + }.focusGroup() + .focusRestorer(), + ) { + itemsIndexed(items) { index, item -> + PlaylistItem( + item = item, + index = index, + onClick = { + onClickItem.invoke(index, item) + }, + onLongClick = { + onLongClickItem.invoke(index, item) + }, + isPlaying = + equalsNotNull( + musicState.currentItemId, + item?.id, + ), + isQueued = item?.id in musicState.queuedIds, + modifier = + Modifier + .ifElse( + item?.type != BaseItemKind.AUDIO, + Modifier.height(80.dp), + ).onFocusChanged { + if (it.isFocused) { + onFocusItem(index, item) + } + }.focusProperties { + left = playButtonFocusRequester + previous = playButtonFocusRequester + }, + ) + } + } + } else { + LaunchedEffect(Unit) { + playButtonFocusRequester.tryRequestFocus() + } + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(R.string.no_results), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = Modifier, + ) + } + } + } + } +} + @Composable fun PlaylistItem( item: BaseItem?, @@ -804,7 +815,23 @@ fun PlaylistItem( ) }, trailingContent = { - item?.data?.runTimeTicks?.ticks?.roundMinutes?.let { duration -> + val duration = + when (item?.type) { + BaseItemKind.AUDIO -> { + item.data.runTimeTicks + ?.ticks + ?.roundSeconds + } + + else -> { + item + ?.data + ?.runTimeTicks + ?.ticks + ?.roundMinutes + } + } + duration?.let { duration -> val now by LocalClock.current.now val context = LocalContext.current val endTimeStr = @@ -813,12 +840,13 @@ fun PlaylistItem( formatTime(context, endTime) } val resources = LocalResources.current - val durationText = remember(duration) { resources.formatDuration(duration) } + val durationText = + remember(resources, duration) { resources.formatDuration(duration) } Column { Text( text = durationText, ) - if (item.type != BaseItemKind.AUDIO) { + if (item?.type != BaseItemKind.AUDIO) { Text( text = stringResource(R.string.ends_at, endTimeStr), style = MaterialTheme.typography.bodySmall,