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
This commit is contained in:
Damontecres 2026-07-25 16:13:31 -04:00 • committed by GitHub
parent 26a23d673b
commit 4171d0b861
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<BaseItem?>,
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,