Fixes the skip segment button appearance behavior (#884)

## Description
Fixes the segment (intro, credits, etc) skip button appearance behavior

For the button shown on the video: if the button for a given segment is
clicked, dismissed by the back button, or times out after 10 seconds, it
will never appear again even if you rewind the video.

However, the current non-ignored segment will always show a button on
the playback controls though so it's possible to still use it.

### Related issues
Fixes #875

### Testing
Tested a bunch of scenarios on the emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-02-11 20:06:15 -05:00 committed by GitHub
parent 028553dc91
commit 4161ea418c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 73 additions and 26 deletions

View file

@ -391,6 +391,15 @@ fun CoroutineScope.launchIO(
block: suspend CoroutineScope.() -> Unit, block: suspend CoroutineScope.() -> Unit,
): Job = launch(context = Dispatchers.IO + context, start = start, block = block) ): Job = launch(context = Dispatchers.IO + context, start = start, block = block)
/**
* Launches a coroutine with [Dispatchers.Default] plus the provided [CoroutineContext] defaulting to using [ExceptionHandler]
*/
fun CoroutineScope.launchDefault(
context: CoroutineContext = ExceptionHandler(),
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit,
): Job = launch(context = Dispatchers.Default + context, start = start, block = block)
/** /**
* Converts a UUID to the format used server-side (ie without hyphens). * Converts a UUID to the format used server-side (ie without hyphens).
* *

View file

@ -86,7 +86,6 @@ import com.github.damontecres.wholphin.util.mpv.MpvPlayer
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.extensions.ticks
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import kotlin.time.Duration import kotlin.time.Duration
@ -163,8 +162,7 @@ fun PlaybackPageContent(
itemId = UUID.randomUUID(), itemId = UUID.randomUUID(),
), ),
) )
val currentSegment by viewModel.currentSegment.observeAsState(null) val currentSegment by viewModel.currentSegment.collectAsState()
var segmentCancelled by remember(currentSegment?.id) { mutableStateOf(false) }
val cues by viewModel.subtitleCues.observeAsState(listOf()) val cues by viewModel.subtitleCues.observeAsState(listOf())
var showDebugInfo by remember { mutableStateOf(prefs.showDebugInfo) } var showDebugInfo by remember { mutableStateOf(prefs.showDebugInfo) }
@ -302,10 +300,10 @@ fun PlaybackPageContent(
} }
val showSegment = val showSegment =
!segmentCancelled && currentSegment != null && currentSegment?.interacted == false &&
nextUp == null && !controllerViewState.controlsVisible && skipIndicatorDuration == 0L nextUp == null && !controllerViewState.controlsVisible && skipIndicatorDuration == 0L
BackHandler(showSegment) { BackHandler(showSegment) {
segmentCancelled = true viewModel.updateSegment(currentSegment?.segment?.id, true)
} }
Box( Box(
@ -400,7 +398,7 @@ fun PlaybackPageContent(
onClickPlaylist = { onClickPlaylist = {
viewModel.playItemInPlaylist(it) viewModel.playItemInPlaylist(it)
}, },
currentSegment = currentSegment, currentSegment = currentSegment?.segment,
showClock = preferences.appPreferences.interfacePreferences.showClock, showClock = preferences.appPreferences.interfacePreferences.showClock,
) )
} }
@ -462,13 +460,12 @@ fun PlaybackPageContent(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
focusRequester.tryRequestFocus() focusRequester.tryRequestFocus()
delay(10.seconds) delay(10.seconds)
segmentCancelled = true viewModel.updateSegment(segment.segment.id, true)
} }
TextButton( TextButton(
stringRes = segment.type.skipStringRes, stringRes = segment.segment.type.skipStringRes,
onClick = { onClick = {
segmentCancelled = true viewModel.updateSegment(segment.segment.id, false)
player.seekTo(segment.endTicks.ticks.inWholeMilliseconds)
}, },
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
) )

View file

@ -49,6 +49,7 @@ import com.github.damontecres.wholphin.services.RefreshRateService
import com.github.damontecres.wholphin.services.StreamChoiceService import com.github.damontecres.wholphin.services.StreamChoiceService
import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
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
@ -57,7 +58,6 @@ import com.github.damontecres.wholphin.ui.seekForward
import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.util.EqualityMutableLiveData
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener
@ -164,7 +164,7 @@ class PlaybackViewModel
val currentMediaInfo = MutableLiveData<CurrentMediaInfo>(CurrentMediaInfo.EMPTY) val currentMediaInfo = MutableLiveData<CurrentMediaInfo>(CurrentMediaInfo.EMPTY)
val currentPlayback = MutableStateFlow<CurrentPlayback?>(null) val currentPlayback = MutableStateFlow<CurrentPlayback?>(null)
val currentItemPlayback = MutableLiveData<ItemPlayback>() val currentItemPlayback = MutableLiveData<ItemPlayback>()
val currentSegment = EqualityMutableLiveData<MediaSegmentDto?>(null) val currentSegment = MutableStateFlow<MediaSegmentState?>(null)
private val autoSkippedSegments = mutableSetOf<UUID>() private val autoSkippedSegments = mutableSetOf<UUID>()
val subtitleCues = MutableLiveData<List<Cue>>(listOf()) val subtitleCues = MutableLiveData<List<Cue>>(listOf())
@ -962,10 +962,10 @@ class PlaybackViewModel
/** /**
* Cancels listening for segments and clears current segment state * Cancels listening for segments and clears current segment state
*/ */
private suspend fun resetSegmentState() { private fun resetSegmentState() {
segmentJob?.cancel() segmentJob?.cancel()
autoSkippedSegments.clear() autoSkippedSegments.clear()
currentSegment.setValueOnMain(null) currentSegment.value = null
} }
/** /**
@ -988,8 +988,13 @@ class PlaybackViewModel
it.type != MediaSegmentType.UNKNOWN && currentTicks >= it.startTicks && currentTicks < it.endTicks it.type != MediaSegmentType.UNKNOWN && currentTicks >= it.startTicks && currentTicks < it.endTicks
} }
if (currentSegment != null && if (currentSegment != null &&
currentSegment.itemId == this@PlaybackViewModel.itemId && currentSegment.itemId == this@PlaybackViewModel.itemId
autoSkippedSegments.add(currentSegment.id) ) {
if (currentSegment.id !=
this@PlaybackViewModel
.currentSegment.value
?.segment
?.id
) { ) {
Timber.d( Timber.d(
"Found media segment for %s: %s, %s", "Found media segment for %s: %s, %s",
@ -997,6 +1002,7 @@ class PlaybackViewModel
currentSegment.id, currentSegment.id,
currentSegment.type, currentSegment.type,
) )
}
val playlist = this@PlaybackViewModel.playlist.value val playlist = this@PlaybackViewModel.playlist.value
if (currentSegment.type == MediaSegmentType.OUTRO && if (currentSegment.type == MediaSegmentType.OUTRO &&
@ -1021,13 +1027,21 @@ class PlaybackViewModel
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
when (behavior) { when (behavior) {
SkipSegmentBehavior.AUTO_SKIP -> { SkipSegmentBehavior.AUTO_SKIP -> {
this@PlaybackViewModel.currentSegment.value = null if (autoSkippedSegments.add(currentSegment.id)) {
player.seekTo(currentSegment.endTicks.ticks.inWholeMilliseconds + 1) onMain { player.seekTo(currentSegment.endTicks.ticks.inWholeMilliseconds + 1) }
}
this@PlaybackViewModel.currentSegment.update {
MediaSegmentState(currentSegment, true)
}
} }
SkipSegmentBehavior.ASK_TO_SKIP -> { SkipSegmentBehavior.ASK_TO_SKIP -> {
this@PlaybackViewModel.currentSegment.value = this@PlaybackViewModel.currentSegment.update {
currentSegment MediaSegmentState(
currentSegment,
autoSkippedSegments.contains(currentSegment.id),
)
}
} }
else -> { else -> {
@ -1046,6 +1060,28 @@ class PlaybackViewModel
} }
} }
fun updateSegment(
segmentId: UUID?,
dismissed: Boolean,
) {
viewModelScope.launchDefault {
val segment = currentSegment.value?.segment
if (segment != null && segment.id == segmentId) {
autoSkippedSegments.add(segment.id)
if (dismissed) {
currentSegment.update {
it?.copy(interacted = true)
}
} else {
currentSegment.update {
null
}
onMain { player.seekTo(segment.endTicks.ticks.inWholeMilliseconds + 1) }
}
}
}
}
private fun listenForTranscodeReason(): Job = private fun listenForTranscodeReason(): Job =
viewModelScope.launchIO { viewModelScope.launchIO {
currentPlayback.collectLatest { currentPlayback.collectLatest {
@ -1379,3 +1415,8 @@ data class PlayerState(
val player: Player, val player: Player,
val backend: PlayerBackend, val backend: PlayerBackend,
) )
data class MediaSegmentState(
val segment: MediaSegmentDto,
val interacted: Boolean,
)