Don't show full playback overlay when pausing (#1144)

## Description
Instead of displaying the full playback overlay when pausing via a
button press, just show a short pause image in the center of the screen.
Any button press that will pause triggers this including a remote Pause,
Pause/Play, or one-click pause Enter.

I decided to make this the standard behavior instead of a toggle because
I think if the user intends to do something with the overlay (change
subtitles, audio, etc), it's still straightforward to open the overlay,
click the pause button (which is focused first), and then move to the
desired option versus assuming click-to-pause will open the overlay so
the user can move to the desired option.

### Related issues
Closes #990

### Testing
Emulator

## Screenshots

[pause_osd.webm](https://github.com/user-attachments/assets/8f908164-2a50-43f9-9e13-a463d2914e42)


## AI or LLM usage
None
This commit is contained in:
Ray 2026-03-25 13:59:48 -04:00 committed by GitHub
parent 8f9b9813b7
commit 2d863e9c32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 113 additions and 8 deletions

View file

@ -1,23 +1,23 @@
package com.github.damontecres.wholphin.ui.playback package com.github.damontecres.wholphin.ui.playback
import androidx.annotation.OptIn
import androidx.media3.common.ForwardingSimpleBasePlayer import androidx.media3.common.ForwardingSimpleBasePlayer
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import com.github.damontecres.wholphin.preferences.PlaybackPreferences import com.github.damontecres.wholphin.preferences.PlaybackPreferences
import com.github.damontecres.wholphin.preferences.skipBackOnResume import com.github.damontecres.wholphin.preferences.skipBackOnResume
import com.github.damontecres.wholphin.ui.seekBack import com.github.damontecres.wholphin.ui.seekBack
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import timber.log.Timber import timber.log.Timber
@OptIn(UnstableApi::class)
class MediaSessionPlayer( class MediaSessionPlayer(
player: Player, player: Player,
private val controllerViewState: ControllerViewState,
private val playbackPreferences: PlaybackPreferences, private val playbackPreferences: PlaybackPreferences,
) : ForwardingSimpleBasePlayer(player) { ) : ForwardingSimpleBasePlayer(player) {
override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> { override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
Timber.v("handleSetPlayWhenReady: playWhenReady=$playWhenReady") Timber.v("handleSetPlayWhenReady: playWhenReady=$playWhenReady")
if (!playWhenReady && player.isPlaying) { if (playWhenReady) {
controllerViewState.showControls()
} else if (playWhenReady) {
playbackPreferences.skipBackOnResume?.let { playbackPreferences.skipBackOnResume?.let {
player.seekBack(it) player.seekBack(it)
} }

View file

@ -0,0 +1,99 @@
package com.github.damontecres.wholphin.ui.playback
import androidx.annotation.OptIn
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.state.observeState
import com.github.damontecres.wholphin.R
import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
/**
* Show an animated "pause" image whenever the player is paused
*/
@Composable
fun PauseIndicator(
player: Player,
modifier: Modifier = Modifier,
duration: Duration = 300.milliseconds,
) {
val state = rememberPauseState(player)
var visible by remember { mutableStateOf(false) }
LaunchedEffect(state.isPaused) {
if (state.isPaused) visible = true
}
AnimatedVisibility(
visible = visible,
enter =
scaleIn(
animationSpec =
tween(
durationMillis = duration.inWholeMilliseconds.toInt(),
),
),
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)),
modifier = modifier,
) {
LaunchedEffect(Unit) {
delay(duration)
delay(50)
visible = false
}
Image(
modifier = Modifier.size(64.dp, 64.dp),
painter = painterResource(id = R.drawable.baseline_pause_24),
contentDescription = null,
)
}
}
/**
* Remember when a player is paused
*/
@Composable
fun rememberPauseState(player: Player): PauseState {
val state = remember(player) { PauseState(player) }
LaunchedEffect(player) { state.observe() }
return state
}
class PauseState(
private val player: Player,
) {
var isPaused by mutableStateOf(false)
private set
@OptIn(UnstableApi::class)
internal suspend fun observe() {
player
.observeState(
Player.EVENT_PLAYBACK_STATE_CHANGED,
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
) {
// Timber.v("isPaused=$isPaused, playWhenReady=${player.playWhenReady}, playbackState=${player.playbackState}")
isPaused = !isPaused && // Not already paused, don't want to trigger more than once
!player.playWhenReady && // Player is actually paused
// Player could play if it was not paused, ie it is not stopped
player.playbackState.let { it == Player.STATE_READY || it == Player.STATE_BUFFERING }
}.observe()
}
}

View file

@ -55,9 +55,7 @@ class PlaybackKeyHandler(
} else if (oneClickPause && isEnterKey(it)) { } else if (oneClickPause && isEnterKey(it)) {
val wasPlaying = player.isPlaying val wasPlaying = player.isPlaying
Util.handlePlayPauseButtonAction(player) Util.handlePlayPauseButtonAction(player)
if (wasPlaying) { if (!wasPlaying) {
controllerViewState.showControls()
} else {
skipBackOnResume?.let { skipBackOnResume?.let {
player.seekBack(it) player.seekBack(it)
} }

View file

@ -361,6 +361,15 @@ fun PlaybackPageContent(
} }
} }
if (!controllerViewState.controlsVisible && skipIndicatorDuration == 0L) {
PauseIndicator(
player = player,
modifier =
Modifier
.align(Alignment.Center),
)
}
// The playback controls // The playback controls
PlaybackOverlay( PlaybackOverlay(

View file

@ -260,7 +260,6 @@ class PlaybackViewModel
val sessionPlayer = val sessionPlayer =
MediaSessionPlayer( MediaSessionPlayer(
player, player,
controllerViewState,
preferences.appPreferences.playbackPreferences, preferences.appPreferences.playbackPreferences,
) )
mediaSession = mediaSession =