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 android.content.Context
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement 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.ExpandableFaButton
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
import com.github.damontecres.wholphin.ui.components.FilterByButton 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.LoadingPage
import com.github.damontecres.wholphin.ui.components.MusicContextActions import com.github.damontecres.wholphin.ui.components.MusicContextActions
import com.github.damontecres.wholphin.ui.components.Optional 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.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.roundMinutes 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.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.LocalClock import com.github.damontecres.wholphin.ui.util.LocalClock
@ -367,7 +368,6 @@ fun PlaylistDetails(
), ),
addToPlaylistViewModel: AddPlaylistViewModel = hiltViewModel(), addToPlaylistViewModel: AddPlaylistViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val musicState by viewModel.musicState.collectAsState() val musicState by viewModel.musicState.collectAsState()
@ -405,19 +405,21 @@ fun PlaylistDetails(
} }
} }
val musicContextActions = val musicContextActions =
MusicContextActions( remember {
navigateTo = { viewModel.navigationManager.navigateTo(it) }, MusicContextActions(
onClickPlay = { index, item -> play(index, item, false, MediaType.AUDIO) }, navigateTo = { viewModel.navigationManager.navigateTo(it) },
onClickPlayNext = { index, item -> viewModel.playNext(item) }, onClickPlay = { index, item -> play(index, item, false, MediaType.AUDIO) },
onClickAddToQueue = { item -> viewModel.addToQueue(item, Int.MAX_VALUE) }, onClickPlayNext = { index, item -> viewModel.playNext(item) },
onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) }, onClickAddToQueue = { item -> viewModel.addToQueue(item, Int.MAX_VALUE) },
onClickAddPlaylist = { itemId -> onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) },
addToPlaylistViewModel.loadPlaylists() onClickAddPlaylist = { itemId ->
showPlaylistDialog.makePresent(itemId) addToPlaylistViewModel.loadPlaylists()
}, showPlaylistDialog.makePresent(itemId)
onClickRemoveFromQueue = { _, _ -> }, },
onDeleteItem = viewModel::deleteItem, onClickRemoveFromQueue = { _, _ -> },
) onDeleteItem = viewModel::deleteItem,
)
}
val contextActions = val contextActions =
remember { remember {
ContextMenuActions( ContextMenuActions(
@ -534,7 +536,6 @@ fun PlaylistDetailsContent(
) { ) {
var savedIndex by rememberSaveable { mutableIntStateOf(0) } var savedIndex by rememberSaveable { mutableIntStateOf(0) }
var focusedIndex by remember { mutableIntStateOf(savedIndex) } var focusedIndex by remember { mutableIntStateOf(savedIndex) }
val focus = remember { FocusRequester() }
val focusedItem = items.getOrNull(focusedIndex) val focusedItem = items.getOrNull(focusedIndex)
LaunchedEffect(focusedItem) { LaunchedEffect(focusedItem) {
focusedItem?.let(onChangeBackdrop) focusedItem?.let(onChangeBackdrop)
@ -555,9 +556,15 @@ fun PlaylistDetailsContent(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier =
Modifier Modifier
.padding(top = 16.dp)
.fillMaxSize(), .fillMaxSize(),
) { ) {
val title =
if (loadingState is LoadingState.Success) {
playlist?.name ?: stringResource(R.string.playlist)
} else {
""
}
GridTitle(title)
Row( Row(
horizontalArrangement = Arrangement.spacedBy(24.dp), horizontalArrangement = Arrangement.spacedBy(24.dp),
modifier = modifier =
@ -577,115 +584,28 @@ fun PlaylistDetailsContent(
.padding(top = 80.dp) .padding(top = 80.dp)
.fillMaxWidth(.25f), .fillMaxWidth(.25f),
) )
when (loadingState) { PlaylistItems(
is LoadingState.Error -> { loadingState = loadingState,
ErrorMessage(loadingState, modifier) items = items,
} musicState = musicState,
playButtonFocusRequester = playButtonFocusRequester,
LoadingState.Pending, LoadingState.Loading -> { onFocusItem = { index, item ->
LoadingPage(modifier) focusedIndex = index
} },
onClickItem = { index, item ->
LoadingState.Success -> { savedIndex = index
Column( item?.let { onClickIndex.invoke(index, item) }
modifier = },
Modifier onLongClickItem = { index, item ->
.fillMaxWidth() savedIndex = index
.padding(horizontal = 16.dp), item?.let { onLongClickIndex.invoke(index, item) }
) { },
Text( modifier =
text = playlist?.name ?: stringResource(R.string.playlist), Modifier
color = MaterialTheme.colorScheme.onSurface, .padding(horizontal = 16.dp)
style = MaterialTheme.typography.displayMedium, .weight(1f)
textAlign = TextAlign.Center, .focusRequester(focusRequester),
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(),
)
}
}
}
}
}
} }
} }
} }
@ -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 @Composable
fun PlaylistItem( fun PlaylistItem(
item: BaseItem?, item: BaseItem?,
@ -804,7 +815,23 @@ fun PlaylistItem(
) )
}, },
trailingContent = { 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 now by LocalClock.current.now
val context = LocalContext.current val context = LocalContext.current
val endTimeStr = val endTimeStr =
@ -813,12 +840,13 @@ fun PlaylistItem(
formatTime(context, endTime) formatTime(context, endTime)
} }
val resources = LocalResources.current val resources = LocalResources.current
val durationText = remember(duration) { resources.formatDuration(duration) } val durationText =
remember(resources, duration) { resources.formatDuration(duration) }
Column { Column {
Text( Text(
text = durationText, text = durationText,
) )
if (item.type != BaseItemKind.AUDIO) { if (item?.type != BaseItemKind.AUDIO) {
Text( Text(
text = stringResource(R.string.ends_at, endTimeStr), text = stringResource(R.string.ends_at, endTimeStr),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,