Several small fixes (#1612)

## Description
- Fixes showing "null" in the subtitle for some tv episodes (usually
daily shows like Jeopardy)
- Partially fix holding back to return to the previous grid position
(still has focus issues)
- Show/hide tabs on the discover page similar to other pages in the app
- Prevent a warning server-side if a negative playback timestamp is sent

### Related issues
N/A

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-06-22 18:38:19 -04:00 • committed by GitHub
parent 77d7571276
commit c945e1a472
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 44 additions and 18 deletions

View file

@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.dot import com.github.damontecres.wholphin.ui.dot
import com.github.damontecres.wholphin.ui.formatDateTime import com.github.damontecres.wholphin.ui.formatDateTime
import com.github.damontecres.wholphin.ui.getDateFormatter import com.github.damontecres.wholphin.ui.getDateFormatter
import com.github.damontecres.wholphin.ui.joinNotBlank
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.playback.playable import com.github.damontecres.wholphin.ui.playback.playable
import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.roundMinutes
@ -57,14 +58,14 @@ data class BaseItem(
val title get() = if (type == BaseItemKind.EPISODE) data.seriesName else name val title get() = if (type == BaseItemKind.EPISODE) data.seriesName else name
val subtitle val subtitle: String? by lazy {
get() = when (type) {
when (type) { BaseItemKind.EPISODE -> listOf(data.seasonEpisode, name).joinNotBlank(" - ")
BaseItemKind.EPISODE -> data.seasonEpisode + " - " + name BaseItemKind.SERIES -> data.seriesProductionYears
BaseItemKind.SERIES -> data.seriesProductionYears BaseItemKind.AUDIO -> listOf(data.album, artistsString).joinNotBlank(" - ")
BaseItemKind.AUDIO -> listOfNotNull(data.album, artistsString).joinToString(" - ") else -> data.productionYear?.toString()
else -> data.productionYear?.toString() }
} }
val subtitleLong: String? by lazy { val subtitleLong: String? by lazy {
if (type == BaseItemKind.EPISODE) { if (type == BaseItemKind.EPISODE) {
@ -72,7 +73,7 @@ data class BaseItem(
add(data.seasonEpisodePadded) add(data.seasonEpisodePadded)
add(data.name) add(data.name)
add(data.premiereDate?.let { formatDateTime(it) }) add(data.premiereDate?.let { formatDateTime(it) })
}.filterNotNull().joinToString(" - ") }.joinNotBlank(" - ")
} else { } else {
data.productionYear?.toString() data.productionYear?.toString()
} }

View file

@ -74,6 +74,8 @@ inline fun <T> List<T>.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? {
return if (index >= 0) index else null return if (index >= 0) index else null
} }
fun Iterable<CharSequence?>.joinNotBlank(sep: String): String? = filter { it.isNotNullOrBlank() }.letNotEmpty { it.joinToString(sep) }
/** /**
* Try to call [FocusRequester.requestFocus], but catch & log the exception if something is not configured properly * Try to call [FocusRequester.requestFocus], but catch & log the exception if something is not configured properly
*/ */

View file

@ -141,7 +141,7 @@ fun <T : CardGridItem> CardGrid(
{ index: Int -> { index: Int ->
if (DEBUG) Timber.v("focusOn: focusedIndex=$currentFocusedIndex, index=$index") if (DEBUG) Timber.v("focusOn: focusedIndex=$currentFocusedIndex, index=$index")
if (index != currentFocusedIndex) { if (index != currentFocusedIndex) {
previouslyFocusedIndex = focusedIndex previouslyFocusedIndex = currentFocusedIndex
} }
focusedIndex = index focusedIndex = index
} }
@ -257,10 +257,11 @@ fun <T : CardGridItem> CardGrid(
val newPosition = previouslyFocusedIndex val newPosition = previouslyFocusedIndex
if (DEBUG) Timber.d("Back long pressed: newPosition=$newPosition") if (DEBUG) Timber.d("Back long pressed: newPosition=$newPosition")
if (newPosition > 0) { if (newPosition > 0) {
focusOn(newPosition)
scope.launch(ExceptionHandler()) { scope.launch(ExceptionHandler()) {
gridState.scrollToItem(newPosition, -columns) pager.getOrNull(newPosition)
firstFocus.tryRequestFocus() gridState.scrollToItem(newPosition)
focusOn(newPosition)
alphabetFocus = true
} }
} }
return@onKeyEvent true return@onKeyEvent true

View file

@ -42,6 +42,7 @@ fun DiscoverPage(
0 -> { 0 -> {
SeerrDiscoverPage( SeerrDiscoverPage(
preferences = preferences, preferences = preferences,
positionCallback = { showHeader = it.row < 1 },
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
@ -53,6 +54,7 @@ fun DiscoverPage(
1 -> { 1 -> {
SeerrRequestsPage( SeerrRequestsPage(
focusRequesterOnEmpty = tabDetails.tabFocusRequester, focusRequesterOnEmpty = tabDetails.tabFocusRequester,
positionCallback = { columns, index -> showHeader = index < columns },
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
@ -64,6 +66,7 @@ fun DiscoverPage(
2 -> { 2 -> {
DiscoverSearchPage( DiscoverSearchPage(
preferences = preferences, preferences = preferences,
positionCallback = { columns, index -> showHeader = index < columns },
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()

View file

@ -102,6 +102,7 @@ class DiscoverSearchViewModel
@Composable @Composable
fun DiscoverSearchPage( fun DiscoverSearchPage(
preferences: UserPreferences, preferences: UserPreferences,
positionCallback: (columns: Int, position: Int) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: DiscoverSearchViewModel = hiltViewModel(), viewModel: DiscoverSearchViewModel = hiltViewModel(),
) { ) {
@ -241,7 +242,10 @@ fun DiscoverSearchPage(
onPlayItem = { _, _ -> }, onPlayItem = { _, _ -> },
onClickPosition = { position = it.column }, onClickPosition = { position = it.column },
onClickDiscover = onClickDiscover, onClickDiscover = onClickDiscover,
positionCallback = { columns, index -> position = index }, positionCallback = { columns, index ->
position = index
positionCallback.invoke(columns, index)
},
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }

View file

@ -243,6 +243,7 @@ data class DiscoverState(
@Composable @Composable
fun SeerrDiscoverPage( fun SeerrDiscoverPage(
preferences: UserPreferences, preferences: UserPreferences,
positionCallback: (RowColumn) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SeerrDiscoverViewModel = hiltViewModel(), viewModel: SeerrDiscoverViewModel = hiltViewModel(),
) { ) {
@ -335,12 +336,16 @@ fun SeerrDiscoverPage(
viewModel.navigationManager.navigateTo(item.destination) viewModel.navigationManager.navigateTo(item.destination)
}, },
onLongClickItem = { index, item -> }, onLongClickItem = { index, item -> },
onCardFocus = { index -> position = RowColumn(rowIndex, index) }, onCardFocus = { index ->
position = RowColumn(rowIndex, index)
positionCallback.invoke(position)
},
focusRequester = focusRequesters[rowIndex], focusRequester = focusRequesters[rowIndex],
enableViewMore = row.type != DiscoverRequestType.UNKNOWN, enableViewMore = row.type != DiscoverRequestType.UNKNOWN,
onClickViewMore = { onClickViewMore = {
(row.items as? DataLoadingState.Success<List<DiscoverItem>>)?.data?.size?.let { (row.items as? DataLoadingState.Success<List<DiscoverItem>>)?.data?.size?.let {
position = RowColumn(rowIndex, it) position = RowColumn(rowIndex, it)
positionCallback.invoke(position)
} }
viewModel.navigationManager.navigateTo( viewModel.navigationManager.navigateTo(
Destination.DiscoverMoreResult(row.type), Destination.DiscoverMoreResult(row.type),

View file

@ -148,10 +148,12 @@ data class RequestGridItem(
@Composable @Composable
fun SeerrRequestsPage( fun SeerrRequestsPage(
focusRequesterOnEmpty: FocusRequester?, focusRequesterOnEmpty: FocusRequester?,
positionCallback: (columns: Int, position: Int) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SeerrRequestsViewModel = hiltViewModel(), viewModel: SeerrRequestsViewModel = hiltViewModel(),
) { ) {
val state by viewModel.state.collectAsState(SeerrRequestsState.EMPTY) val state by viewModel.state.collectAsState(SeerrRequestsState.EMPTY)
val columns = 6
when (val state = state.requests) { when (val state = state.requests) {
is DataLoadingState.Error -> { is DataLoadingState.Error -> {
@ -213,7 +215,8 @@ fun SeerrRequestsPage(
modifier = mod, modifier = mod,
) )
}, },
columns = 6, columns = columns,
positionCallback = positionCallback,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }

View file

@ -338,12 +338,15 @@ class PlayExternalViewModel
} }
} }
Timber.v("Result position: %s", position?.milliseconds) Timber.v("Result position: %s", position?.milliseconds)
if (position != null && position < 0L) {
Timber.w("Unknown stop position for external playback")
}
if (position != null || result.data?.action != null) { if (position != null || result.data?.action != null) {
api.playStateApi.reportPlaybackStopped( api.playStateApi.reportPlaybackStopped(
PlaybackStopInfo( PlaybackStopInfo(
itemId = itemId, itemId = itemId,
mediaSourceId = mediaSourceId, mediaSourceId = mediaSourceId,
positionTicks = position?.milliseconds?.inWholeTicks, positionTicks = position?.milliseconds?.inWholeTicks?.takeIf { it >= 0 },
failed = false, failed = false,
), ),
) )

View file

@ -21,6 +21,7 @@ import timber.log.Timber
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
import java.util.UUID import java.util.UUID
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -84,10 +85,13 @@ class TrackActivityPlaybackListener(
launch("reportPlaybackStopped") { launch("reportPlaybackStopped") {
getState.invoke()?.let { state -> getState.invoke()?.let { state ->
Timber.v("reportPlaybackStopped for ${state.itemId} at $position") Timber.v("reportPlaybackStopped for ${state.itemId} at $position")
if (position < Duration.ZERO) {
Timber.w("Negative position when reporting playback stopped: %s", position)
}
api.playStateApi.reportPlaybackStopped( api.playStateApi.reportPlaybackStopped(
PlaybackStopInfo( PlaybackStopInfo(
itemId = state.itemId, itemId = state.itemId,
positionTicks = position.inWholeTicks, positionTicks = position.inWholeTicks.takeIf { it >= 0 },
failed = false, failed = false,
playSessionId = state.playSessionId, playSessionId = state.playSessionId,
liveStreamId = state.liveStreamId, liveStreamId = state.liveStreamId,