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, modifier: Modifier = Modifier,
) = Text( ) = Text(
text = title, text = title,
style = MaterialTheme.typography.displayMedium, style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1, maxLines = 1,

View file

@ -653,6 +653,7 @@ fun CollectionFolderView(
}, },
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
var position by rememberInt(viewModel.position)
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) } var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
@ -692,9 +693,15 @@ fun CollectionFolderView(
val gridActions = val gridActions =
remember(actions) { remember(actions) {
GridClickActions( GridClickActions(
onClickItem = actions.onClickItem, onClickItem = { index, item ->
onLongClickItem = position = index
actions.onLongClickItem ?: { position, item -> actions.onClickItem.invoke(index, item)
},
onLongClickItem = { index, item ->
position = index
if (actions.onLongClickItem != null) {
actions.onLongClickItem.invoke(index, item)
} else {
showContextMenu = showContextMenu =
ContextMenu.ForBaseItem( ContextMenu.ForBaseItem(
fromLongClick = true, fromLongClick = true,
@ -707,7 +714,8 @@ fun CollectionFolderView(
canRemoveNextUp = false, canRemoveNextUp = false,
actions = contextActions, actions = contextActions,
) )
}, }
},
onClickPlayAll = onClickPlayAll =
actions.onClickPlayAll ?: { shuffle -> actions.onClickPlayAll ?: { shuffle ->
itemId.toUUIDOrNull()?.let { itemId.toUUIDOrNull()?.let {
@ -785,7 +793,6 @@ fun CollectionFolderView(
var showHeader by rememberSaveable { mutableStateOf(true) } var showHeader by rememberSaveable { mutableStateOf(true) }
val gridFocusRequester = remember { FocusRequester() } val gridFocusRequester = remember { FocusRequester() }
val pager = remember(state.items) { state.items.successValue } val pager = remember(state.items) { state.items.successValue }
var position by rememberInt(viewModel.position)
val focusedItem = pager?.getOrNull(position) val focusedItem = pager?.getOrNull(position)
if (state.viewOptions.showBackdrop) { 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.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -10,13 +9,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.data.model.BaseItem
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.cards.GridCard import com.github.damontecres.wholphin.ui.cards.GridCard
@ -120,13 +116,8 @@ fun ItemGrid(
focusRequester.tryRequestFocus() focusRequester.tryRequestFocus()
} }
Column(modifier = modifier) { Column(modifier = modifier) {
Text( GridTitle(destination.title.getString())
text = destination.title.getString(),
style = MaterialTheme.typography.displayMedium,
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
CardGrid( CardGrid(
pager = st.data, pager = st.data,
onClickItem = { index: Int, item: BaseItem -> 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.PlaybackAction
import com.github.damontecres.wholphin.ui.playback.overlay.PlaybackButton 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.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.playback.overlay.buttonSpacing
import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
@ -74,7 +75,7 @@ fun NowPlayingButtons(
modifier = Modifier.align(Alignment.CenterStart), modifier = Modifier.align(Alignment.CenterStart),
) { ) {
PlaybackButton( PlaybackButton(
iconRes = R.drawable.baseline_more_vert_96, iconRes = R.drawable.vector_settings,
onClick = { onClick = {
onControllerInteraction.invoke() onControllerInteraction.invoke()
onClickMore.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.background
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.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column 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.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.DenseListItem
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.ListItemDefaults import androidx.tv.material3.ListItemDefaults
@ -118,38 +120,27 @@ fun SongListItem(
) )
} }
if (showArtist) { DenseListItem(
// TODO use dense? selected = isPlaying,
ListItem( onClick = onClick,
selected = isPlaying, onLongClick = onLongClick,
onClick = onClick, interactionSource = interactionSource,
onLongClick = onLongClick, leadingContent = leadingContent,
interactionSource = interactionSource, headlineContent = headlineContent,
leadingContent = leadingContent, supportingContent =
headlineContent = headlineContent, if (showArtist && artist != null) {
supportingContent = { {
Text( Text(
text = artist ?: "", text = artist,
) )
}
} else {
null
}, },
trailingContent = trailingContent, trailingContent = trailingContent,
scale = ListItemDefaults.scale(1f, 1f, .95f), scale = ListItemDefaults.scale(1f, 1f, .95f),
modifier = Modifier.weight(1f), 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),
)
}
if (showMoreButton) { if (showMoreButton) {
Button( Button(
onClick = onClickMore, onClick = onClickMore,
@ -198,7 +189,7 @@ fun MusicQueueMarker(
@Composable @Composable
fun SongListItemPreview() { fun SongListItemPreview() {
WholphinTheme { WholphinTheme {
Column { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SongListItem( SongListItem(
title = "Song title", title = "Song title",
artist = "Artists", artist = "Artists",
@ -232,6 +223,17 @@ fun SongListItemPreview() {
showArtist = true, showArtist = true,
isPlaying = 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.input.key.onKeyEvent
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.IntSize 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.SkipIndicator
import com.github.damontecres.wholphin.ui.playback.overlay.SkipSegmentButton import com.github.damontecres.wholphin.ui.playback.overlay.SkipSegmentButton
import com.github.damontecres.wholphin.ui.playback.overlay.rememberSeekBarState 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.calculateEdgeSize
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle
import com.github.damontecres.wholphin.ui.seasonEpisode 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.Media3SubtitleOverride
import com.github.damontecres.wholphin.util.mpv.MpvPlayer import com.github.damontecres.wholphin.util.mpv.MpvPlayer
import io.github.peerless2012.ass.media.widget.AssSubtitleView import io.github.peerless2012.ass.media.widget.AssSubtitleView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@ -163,24 +160,12 @@ fun PlaybackPageContent(
val prefs = preferences.appPreferences.playbackPreferences val prefs = preferences.appPreferences.playbackPreferences
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val configuration = LocalConfiguration.current
val density = LocalDensity.current val density = LocalDensity.current
val userDto by viewModel.currentUserDto.collectAsState() val userDto by viewModel.currentUserDto.collectAsState()
var showDebugInfo by remember { mutableStateOf(prefs.showDebugInfo) } var showDebugInfo by remember { mutableStateOf(prefs.showDebugInfo) }
var playbackDialog by remember { mutableStateOf<PlaybackDialogType?>(null) } 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) { var contentScale by remember(playerBackend) {
mutableStateOf( mutableStateOf(

View file

@ -5,6 +5,7 @@ import android.media.MediaCodecList
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.ui.unit.Density
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.onMain 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.seekBack
import com.github.damontecres.wholphin.ui.seekForward import com.github.damontecres.wholphin.ui.seekForward
import com.github.damontecres.wholphin.ui.showToast 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.PlaybackItemState
import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener
import com.github.damontecres.wholphin.util.checkForSupport 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.mpv.mpvDeviceProfile
import com.github.damontecres.wholphin.util.profile.Codec import com.github.damontecres.wholphin.util.profile.Codec
import com.github.damontecres.wholphin.util.subtitleMimeTypes import com.github.damontecres.wholphin.util.subtitleMimeTypes
@ -1165,10 +1168,10 @@ class PlaybackViewModel
private fun listenForTranscodeReason(): Job = private fun listenForTranscodeReason(): Job =
viewModelScope.launchIO { viewModelScope.launchIO {
state.map { it.currentPlayback }.collectLatest { state.map { it.currentPlayback }.collectLatest {
if (it != null) { if (it != null && it.playMethod == PlayMethod.TRANSCODE && it.transcodeInfo == null) {
try { try {
var transcodeInfo = it.transcodeInfo var transcodeInfo = it.transcodeInfo
while (isActive && it.playMethod == PlayMethod.TRANSCODE && transcodeInfo == null) { while (isActive && transcodeInfo == null) {
delay(2.seconds) delay(2.seconds)
transcodeInfo = transcodeInfo =
api.sessionApi api.sessionApi
@ -1526,6 +1529,22 @@ class PlaybackViewModel
screensaverService.keepScreenOn(isPlaying) 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( override fun onBandwidthEstimate(
eventTime: AnalyticsListener.EventTime, eventTime: AnalyticsListener.EventTime,
totalLoadTimeMs: Int, totalLoadTimeMs: Int,

View file

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