Fix some issues with restoring focus between pages (#613)

## Description
Yet more focus issue fixes:
- Restore focus when going back to home page from a later row
- Don't jump to episode row when going back to series overview (such as
from a person page)

This PR also changes how backgrounding the app during playback works.
Now the only goes back to the previous page when the app comes back to
the foreground. This helps with focusing back on the page properly.
There's also a bit more clean up performed.
This commit is contained in:
Ray 2026-01-01 14:37:32 -05:00 committed by GitHub
parent 26a913b05e
commit 3c11d4ba12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 35 additions and 31 deletions

View file

@ -20,6 +20,10 @@ class PlaybackLifecycleObserver
private var wasPlaying: Boolean? = null private var wasPlaying: Boolean? = null
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
val lastDest = navigationManager.backStack.lastOrNull()
if (lastDest is Destination.Playback || lastDest is Destination.PlaybackList) {
navigationManager.goBack()
}
wasPlaying = null wasPlaying = null
} }
@ -40,9 +44,6 @@ class PlaybackLifecycleObserver
} }
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
if (navigationManager.backStack.lastOrNull() is Destination.Playback) {
navigationManager.goBack()
}
themeSongPlayer.stop() themeSongPlayer.stop()
} }
} }

View file

@ -28,9 +28,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.media3.common.Player import androidx.media3.common.Player
import coil3.request.ErrorResult import coil3.request.ErrorResult
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
@ -182,10 +180,10 @@ fun RequestOrRestoreFocus(
debugKey?.let { Timber.v("RequestOrRestoreFocus: %s", it) } debugKey?.let { Timber.v("RequestOrRestoreFocus: %s", it) }
focusRequester.tryRequestFocus() focusRequester.tryRequestFocus()
} }
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { // LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
debugKey?.let { Timber.v("RequestOrRestoreFocus onResume: %s", it) } // debugKey?.let { Timber.v("RequestOrRestoreFocus onResume: %s", it) }
focusRequester.tryRequestFocus() // focusRequester.tryRequestFocus()
} // }
} }
} }

View file

@ -21,6 +21,7 @@ import androidx.lifecycle.map
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
@ -37,7 +38,6 @@ import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.seasonEpisode import com.github.damontecres.wholphin.ui.seasonEpisode
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -158,13 +158,15 @@ fun SeriesOverview(
LoadingState.Success -> { LoadingState.Success -> {
series?.let { series -> series?.let { series ->
LaunchedEffect(Unit) { RequestOrRestoreFocus(
when (rowFocused) { when (rowFocused) {
EPISODE_ROW -> episodeRowFocusRequester.tryRequestFocus() EPISODE_ROW -> episodeRowFocusRequester
CAST_AND_CREW_ROW -> castCrewRowFocusRequester.tryRequestFocus() CAST_AND_CREW_ROW -> castCrewRowFocusRequester
GUEST_STAR_ROW -> guestStarRowFocusRequester.tryRequestFocus() GUEST_STAR_ROW -> guestStarRowFocusRequester
} else -> episodeRowFocusRequester
} },
"series_overview",
)
LifecycleStartEffect(destination.itemId) { LifecycleStartEffect(destination.itemId) {
viewModel.maybePlayThemeSong( viewModel.maybePlayThemeSong(
destination.itemId, destination.itemId,

View file

@ -49,7 +49,6 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.BannerCard
import com.github.damontecres.wholphin.ui.cards.PersonRow import com.github.damontecres.wholphin.ui.cards.PersonRow
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
@ -182,7 +181,6 @@ fun SeriesOverviewContent(
is EpisodeList.Success -> { is EpisodeList.Success -> {
val state = rememberLazyListState(position.episodeRowIndex) val state = rememberLazyListState(position.episodeRowIndex)
RequestOrRestoreFocus(firstItemFocusRequester)
LazyRow( LazyRow(
state = state, state = state,

View file

@ -234,7 +234,7 @@ fun HomePageContent(
val listState = rememberLazyListState() val listState = rememberLazyListState()
val rowFocusRequesters = remember(homeRows.size) { List(homeRows.size) { FocusRequester() } } val rowFocusRequesters = remember(homeRows.size) { List(homeRows.size) { FocusRequester() } }
var focused by remember { mutableStateOf(false) } var focused by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(homeRows) { LaunchedEffect(homeRows) {
if (!focused) { if (!focused) {
homeRows homeRows
@ -246,6 +246,8 @@ fun HomePageContent(
listState.animateScrollToItem(position.row) listState.animateScrollToItem(position.row)
focused = true focused = true
} }
} else {
rowFocusRequesters.getOrNull(position.row)?.tryRequestFocus()
} }
} }
LaunchedEffect(position) { LaunchedEffect(position) {

View file

@ -157,6 +157,7 @@ class PlaybackViewModel
internal lateinit var item: BaseItem internal lateinit var item: BaseItem
internal var forceTranscoding: Boolean = false internal var forceTranscoding: Boolean = false
private var activityListener: TrackActivityPlaybackListener? = null private var activityListener: TrackActivityPlaybackListener? = null
private val jobs = mutableListOf<Job>()
val nextUp = MutableLiveData<BaseItem?>() val nextUp = MutableLiveData<BaseItem?>()
private var isPlaylist = false private var isPlaylist = false
@ -166,20 +167,22 @@ class PlaybackViewModel
val subtitleSearchLanguage = MutableLiveData<String>(Locale.current.language) val subtitleSearchLanguage = MutableLiveData<String>(Locale.current.language)
init { init {
viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() }
player.addListener(this)
(player as? ExoPlayer)?.addAnalyticsListener(this)
addCloseable { player.removeListener(this@PlaybackViewModel) }
addCloseable { (player as? ExoPlayer)?.removeAnalyticsListener(this@PlaybackViewModel) }
addCloseable { addCloseable {
player.removeListener(this@PlaybackViewModel)
(player as? ExoPlayer)?.removeAnalyticsListener(this@PlaybackViewModel)
this@PlaybackViewModel.activityListener?.let { this@PlaybackViewModel.activityListener?.let {
it.release() it.release()
player.removeListener(it) player.removeListener(it)
} }
jobs.forEach { it.cancel() }
player.release()
} }
addCloseable { player.release() } viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() }
subscribe() player.addListener(this)
listenForTranscodeReason() (player as? ExoPlayer)?.addAnalyticsListener(this)
jobs.add(subscribe())
jobs.add(listenForTranscodeReason())
} }
/** /**
@ -874,7 +877,7 @@ class PlaybackViewModel
} }
} }
private fun listenForTranscodeReason() { private fun listenForTranscodeReason(): Job =
viewModelScope.launchIO { viewModelScope.launchIO {
currentPlayback.collectLatest { currentPlayback.collectLatest {
if (it != null) { if (it != null) {
@ -905,7 +908,6 @@ class PlaybackViewModel
} }
} }
} }
}
private var lastInteractionDate: Date = Date() private var lastInteractionDate: Date = Date()
@ -1026,11 +1028,13 @@ class PlaybackViewModel
} }
fun release() { fun release() {
Timber.v("release")
activityListener?.release() activityListener?.release()
player.release() player.release()
activityListener = null
} }
fun subscribe() { fun subscribe(): Job =
api.webSocket api.webSocket
.subscribe<PlaystateMessage>() .subscribe<PlaystateMessage>()
.onEach { message -> .onEach { message ->
@ -1085,7 +1089,6 @@ class PlaybackViewModel
} }
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
}
/** /**
* Atomically update [currentMediaInfo] * Atomically update [currentMediaInfo]