Several bug fixes & minor UI changes (#1466)

## Description
### Bug fixes
- Fixes a race condition crash when configuring subtitle styling before
libmpv is initialized
- Save the clicked item position on grid pages (#1428 & #1462)
- Fix querying the server every 3 seconds for transcode reason if the
video is direct playing


### UI changes
- Makes the title on grid pages slightly smaller
- Makes adjustments to the song list item on now playing so that no
space is used if there's no artist
- Use settings icon instead of triple dots on now playing page

### Related issues
See above

### Testing
Emulator, nvidia shield & google streamer

## Screenshots
### Song list item
Before & after
<img width="199" height="119" alt="image"
src="https://github.com/user-attachments/assets/7fd3636a-d1e4-4b1f-8379-8599068d6309"
/>


## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-05-26 18:36:56 -04:00 committed by GitHub
parent 8ea6bbbfca
commit 33161d4d6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 77 additions and 67 deletions

View file

@ -239,7 +239,7 @@ fun GridTitle(
modifier: Modifier = Modifier,
) = Text(
text = title,
style = MaterialTheme.typography.displayMedium,
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center,
maxLines = 1,

View file

@ -653,6 +653,7 @@ fun CollectionFolderView(
},
) {
val state by viewModel.state.collectAsState()
var position by rememberInt(viewModel.position)
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
@ -692,9 +693,15 @@ fun CollectionFolderView(
val gridActions =
remember(actions) {
GridClickActions(
onClickItem = actions.onClickItem,
onLongClickItem =
actions.onLongClickItem ?: { position, item ->
onClickItem = { index, item ->
position = index
actions.onClickItem.invoke(index, item)
},
onLongClickItem = { index, item ->
position = index
if (actions.onLongClickItem != null) {
actions.onLongClickItem.invoke(index, item)
} else {
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = true,
@ -707,7 +714,8 @@ fun CollectionFolderView(
canRemoveNextUp = false,
actions = contextActions,
)
},
}
},
onClickPlayAll =
actions.onClickPlayAll ?: { shuffle ->
itemId.toUUIDOrNull()?.let {
@ -785,7 +793,6 @@ fun CollectionFolderView(
var showHeader by rememberSaveable { mutableStateOf(true) }
val gridFocusRequester = remember { FocusRequester() }
val pager = remember(state.items) { state.items.successValue }
var position by rememberInt(viewModel.position)
val focusedItem = pager?.getOrNull(position)
if (state.viewOptions.showBackdrop) {

View file

@ -2,7 +2,6 @@ package com.github.damontecres.wholphin.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -10,13 +9,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.cards.GridCard
@ -120,13 +116,8 @@ fun ItemGrid(
focusRequester.tryRequestFocus()
}
Column(modifier = modifier) {
Text(
text = destination.title.getString(),
style = MaterialTheme.typography.displayMedium,
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
GridTitle(destination.title.getString())
CardGrid(
pager = st.data,
onClickItem = { index: Int, item: BaseItem ->

View file

@ -44,6 +44,7 @@ import com.github.damontecres.wholphin.ui.playback.ControllerViewState
import com.github.damontecres.wholphin.ui.playback.overlay.PlaybackAction
import com.github.damontecres.wholphin.ui.playback.overlay.PlaybackButton
import com.github.damontecres.wholphin.ui.playback.overlay.PlaybackButtons
import com.github.damontecres.wholphin.ui.playback.overlay.PlaybackFaButton
import com.github.damontecres.wholphin.ui.playback.overlay.buttonSpacing
import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
@ -74,7 +75,7 @@ fun NowPlayingButtons(
modifier = Modifier.align(Alignment.CenterStart),
) {
PlaybackButton(
iconRes = R.drawable.baseline_more_vert_96,
iconRes = R.drawable.vector_settings,
onClick = {
onControllerInteraction.invoke()
onClickMore.invoke()

View file

@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.detail.music
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
@ -21,6 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.tv.material3.DenseListItem
import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem
import androidx.tv.material3.ListItemDefaults
@ -118,38 +120,27 @@ fun SongListItem(
)
}
if (showArtist) {
// TODO use dense?
ListItem(
selected = isPlaying,
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
leadingContent = leadingContent,
headlineContent = headlineContent,
supportingContent = {
Text(
text = artist ?: "",
)
DenseListItem(
selected = isPlaying,
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
leadingContent = leadingContent,
headlineContent = headlineContent,
supportingContent =
if (showArtist && artist != null) {
{
Text(
text = artist,
)
}
} else {
null
},
trailingContent = trailingContent,
scale = ListItemDefaults.scale(1f, 1f, .95f),
modifier = Modifier.weight(1f),
)
} else {
ListItem(
selected = isPlaying,
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
leadingContent = leadingContent,
headlineContent = headlineContent,
supportingContent = null,
trailingContent = trailingContent,
scale = ListItemDefaults.scale(1f, 1f, .95f),
modifier = Modifier.weight(1f),
)
}
trailingContent = trailingContent,
scale = ListItemDefaults.scale(1f, 1f, .95f),
modifier = Modifier.weight(1f),
)
if (showMoreButton) {
Button(
onClick = onClickMore,
@ -198,7 +189,7 @@ fun MusicQueueMarker(
@Composable
fun SongListItemPreview() {
WholphinTheme {
Column {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SongListItem(
title = "Song title",
artist = "Artists",
@ -232,6 +223,17 @@ fun SongListItemPreview() {
showArtist = true,
isPlaying = true,
)
SongListItem(
title = "Song title",
artist = null,
indexNumber = 2,
runtime = 2.minutes + 30.seconds,
onClick = {},
onLongClick = { },
modifier = Modifier,
showArtist = true,
isPlaying = true,
)
}
}
}

View file

@ -48,7 +48,6 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.IntSize
@ -85,7 +84,6 @@ import com.github.damontecres.wholphin.ui.playback.overlay.PlaybackOverlay
import com.github.damontecres.wholphin.ui.playback.overlay.SkipIndicator
import com.github.damontecres.wholphin.ui.playback.overlay.SkipSegmentButton
import com.github.damontecres.wholphin.ui.playback.overlay.rememberSeekBarState
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.applyToMpv
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.calculateEdgeSize
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle
import com.github.damontecres.wholphin.ui.seasonEpisode
@ -95,7 +93,6 @@ import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.Media3SubtitleOverride
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
import io.github.peerless2012.ass.media.widget.AssSubtitleView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
@ -163,24 +160,12 @@ fun PlaybackPageContent(
val prefs = preferences.appPreferences.playbackPreferences
val scope = rememberCoroutineScope()
val configuration = LocalConfiguration.current
val density = LocalDensity.current
val userDto by viewModel.currentUserDto.collectAsState()
var showDebugInfo by remember { mutableStateOf(prefs.showDebugInfo) }
var playbackDialog by remember { mutableStateOf<PlaybackDialogType?>(null) }
LaunchedEffect(player) {
if (playerBackend == PlayerBackend.MPV) {
scope.launch(Dispatchers.IO + ExceptionHandler()) {
// MPV can't play HDR, so always use regular settings
preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv(
configuration,
density,
)
}
}
}
var contentScale by remember(playerBackend) {
mutableStateOf(

View file

@ -5,6 +5,7 @@ import android.media.MediaCodecList
import android.os.Build
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.compose.ui.unit.Density
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -57,6 +58,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.onMain
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.applyToMpv
import com.github.damontecres.wholphin.ui.seekBack
import com.github.damontecres.wholphin.ui.seekForward
import com.github.damontecres.wholphin.ui.showToast
@ -66,6 +68,7 @@ import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.PlaybackItemState
import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener
import com.github.damontecres.wholphin.util.checkForSupport
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile
import com.github.damontecres.wholphin.util.profile.Codec
import com.github.damontecres.wholphin.util.subtitleMimeTypes
@ -1165,10 +1168,10 @@ class PlaybackViewModel
private fun listenForTranscodeReason(): Job =
viewModelScope.launchIO {
state.map { it.currentPlayback }.collectLatest {
if (it != null) {
if (it != null && it.playMethod == PlayMethod.TRANSCODE && it.transcodeInfo == null) {
try {
var transcodeInfo = it.transcodeInfo
while (isActive && it.playMethod == PlayMethod.TRANSCODE && transcodeInfo == null) {
while (isActive && transcodeInfo == null) {
delay(2.seconds)
transcodeInfo =
api.sessionApi
@ -1526,6 +1529,22 @@ class PlaybackViewModel
screensaverService.keepScreenOn(isPlaying)
}
override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {
val player = this@PlaybackViewModel.player
if (availableCommands.contains(Player.COMMAND_PREPARE) && player is MpvPlayer) {
// MpvPlayer is initialized, so configure subtitles
Timber.i("Applying subtitle config to MPV")
viewModelScope.launchDefault {
val configuration = context.resources.configuration
val density = Density(context.resources.displayMetrics.density)
preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv(
configuration,
density,
)
}
}
}
override fun onBandwidthEstimate(
eventTime: AnalyticsListener.EventTime,
totalLoadTimeMs: Int,

View file

@ -167,6 +167,11 @@ class MpvPlayer(
// COMMAND_GET_TEXT,
COMMAND_RELEASE,
).build()
notifyListeners(EVENT_AVAILABLE_COMMANDS_CHANGED) {
onAvailableCommandsChanged(
availableCommands,
)
}
trackSelector.init(this, DefaultBandwidthMeter.getSingletonInstance(context))
}