feat(player): track skippable segment type to gate intro and outro UI

Expose the active skippable segment type from the player state so the UI
can show the skip intro button only for intro segments and trigger the
next episode overlay when an outro segment is active.
This commit is contained in:
2026-06-19 12:00:16 +00:00
parent a4267d85ce
commit 54c895ff0f
4 changed files with 14 additions and 5 deletions

View File

@@ -62,6 +62,7 @@ import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView import androidx.media3.ui.SubtitleView
import hu.bbara.purefin.core.player.model.TimedMarker import hu.bbara.purefin.core.player.model.TimedMarker
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
import hu.bbara.purefin.model.SegmentType
import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility
import hu.bbara.purefin.ui.screen.player.components.PlayerSeekBarTrack import hu.bbara.purefin.ui.screen.player.components.PlayerSeekBarTrack
import hu.bbara.purefin.ui.screen.player.components.TvIconButton import hu.bbara.purefin.ui.screen.player.components.TvIconButton
@@ -179,11 +180,12 @@ fun TvPlayerScreen(
val showSkipIntroButton = !controlsVisible val showSkipIntroButton = !controlsVisible
&& uiState.activeSkippableSegmentEndMs != null && uiState.activeSkippableSegmentEndMs != null
&& uiState.activeSkippableSegmentType == SegmentType.INTRO
&& !uiState.isEnded && !uiState.isEnded
val showNextEpisodeOverlay = !controlsVisible val showNextEpisodeOverlay = !controlsVisible
&& uiState.nextEpisode != null && uiState.nextEpisode != null
&& uiState.durationMs > 0L && uiState.durationMs > 0L
&& (uiState.durationMs - uiState.positionMs) <= 60_000L && ((uiState.durationMs - uiState.positionMs) <= 30_000L || uiState.activeSkippableSegmentType == SegmentType.OUTRO)
&& !uiState.isEnded && !uiState.isEnded
LaunchedEffect(showSkipIntroButton, showNextEpisodeOverlay) { LaunchedEffect(showSkipIntroButton, showNextEpisodeOverlay) {
rootFocusRequester = when { rootFocusRequester = when {

View File

@@ -43,6 +43,7 @@ import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView import androidx.media3.ui.SubtitleView
import hu.bbara.purefin.core.player.model.TimedMarker import hu.bbara.purefin.core.player.model.TimedMarker
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
import hu.bbara.purefin.model.SegmentType
import hu.bbara.purefin.ui.common.visual.EmptyValueTimedVisibility import hu.bbara.purefin.ui.common.visual.EmptyValueTimedVisibility
import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility
import hu.bbara.purefin.ui.screen.player.components.NextEpisodeOverlay import hu.bbara.purefin.ui.screen.player.components.NextEpisodeOverlay
@@ -100,11 +101,11 @@ fun PlayerScreen(
} }
val showSkipIntroButton = !controlsVisible && uiState.activeSkippableSegmentEndMs != null val showSkipIntroButton = !controlsVisible && uiState.activeSkippableSegmentEndMs != null && uiState.activeSkippableSegmentType == SegmentType.INTRO
val showNextEpisodeOverlay = !controlsVisible val showNextEpisodeOverlay = !controlsVisible
&& uiState.nextEpisode != null && uiState.nextEpisode != null
&& uiState.durationMs > 0L && uiState.durationMs > 0L
&& (uiState.durationMs - uiState.positionMs) <= 60_000L && ((uiState.durationMs - uiState.positionMs) <= 60_000L || uiState.activeSkippableSegmentType == SegmentType.OUTRO)
&& !uiState.isEnded && !uiState.isEnded
@@ -301,7 +302,7 @@ fun PlayerScreen(
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.padding( .padding(
end = 24.dp, end = 24.dp,
bottom = 120.dp bottom = 24.dp
) )
) { ) {
uiState.nextEpisode?.let { nextEpisode -> uiState.nextEpisode?.let { nextEpisode ->

View File

@@ -1,5 +1,7 @@
package hu.bbara.purefin.core.player.model package hu.bbara.purefin.core.player.model
import hu.bbara.purefin.model.SegmentType
data class PlayerUiState( data class PlayerUiState(
val isPlaying: Boolean = false, val isPlaying: Boolean = false,
val isBuffering: Boolean = false, val isBuffering: Boolean = false,
@@ -11,6 +13,7 @@ data class PlayerUiState(
val positionMs: Long = 0L, val positionMs: Long = 0L,
val bufferedMs: Long = 0L, val bufferedMs: Long = 0L,
val activeSkippableSegmentEndMs: Long? = null, val activeSkippableSegmentEndMs: Long? = null,
val activeSkippableSegmentType: SegmentType? = null,
val error: String? = null, val error: String? = null,
val playbackSpeed: Float = 1f, val playbackSpeed: Float = 1f,
val chapters: List<TimedMarker> = emptyList(), val chapters: List<TimedMarker> = emptyList(),

View File

@@ -117,7 +117,10 @@ class PlayerViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
playerManager.activeSkippableSegment.collect { mediaSegment -> playerManager.activeSkippableSegment.collect { mediaSegment ->
_uiState.update { _uiState.update {
it.copy(activeSkippableSegmentEndMs = mediaSegment?.endMs) it.copy(
activeSkippableSegmentEndMs = mediaSegment?.endMs,
activeSkippableSegmentType = mediaSegment?.type
)
} }
} }
} }