mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Initial skip segments implementation
This commit is contained in:
parent
165fd80c0e
commit
405faa388e
11 changed files with 262 additions and 9 deletions
|
|
@ -6,6 +6,7 @@ import androidx.annotation.StringRes
|
|||
import com.github.damontecres.dolphin.R
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.ui.preferences.PreferenceGroup
|
||||
import com.github.damontecres.dolphin.ui.preferences.PreferenceScreenOption
|
||||
import com.github.damontecres.dolphin.ui.preferences.PreferenceValidation
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
|
@ -360,6 +361,77 @@ sealed interface AppPreference<T> {
|
|||
title = R.string.license_info,
|
||||
destination = Destination.License,
|
||||
)
|
||||
|
||||
val AdvancedSettings =
|
||||
AppDestinationPreference(
|
||||
title = R.string.advanced_settings,
|
||||
destination = Destination.Settings(PreferenceScreenOption.ADVANCED),
|
||||
)
|
||||
|
||||
val SkipIntros =
|
||||
AppChoicePreference<SkipSegmentBehavior>(
|
||||
title = R.string.skip_intro_behavior,
|
||||
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
||||
getter = { it.playbackPreferences.skipIntros },
|
||||
setter = { prefs, value ->
|
||||
prefs.updatePlaybackPreferences { skipIntros = value }
|
||||
},
|
||||
displayValues = R.array.skip_behaviors,
|
||||
indexToValue = { SkipSegmentBehavior.forNumber(it) },
|
||||
valueToIndex = { it.number },
|
||||
)
|
||||
|
||||
val SkipOutros =
|
||||
AppChoicePreference<SkipSegmentBehavior>(
|
||||
title = R.string.skip_outro_behavior,
|
||||
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
||||
getter = { it.playbackPreferences.skipOutros },
|
||||
setter = { prefs, value ->
|
||||
prefs.updatePlaybackPreferences { skipOutros = value }
|
||||
},
|
||||
displayValues = R.array.skip_behaviors,
|
||||
indexToValue = { SkipSegmentBehavior.forNumber(it) },
|
||||
valueToIndex = { it.number },
|
||||
)
|
||||
|
||||
val SkipCommercials =
|
||||
AppChoicePreference<SkipSegmentBehavior>(
|
||||
title = R.string.skip_comercials_behavior,
|
||||
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
||||
getter = { it.playbackPreferences.skipCommercials },
|
||||
setter = { prefs, value ->
|
||||
prefs.updatePlaybackPreferences { skipCommercials = value }
|
||||
},
|
||||
displayValues = R.array.skip_behaviors,
|
||||
indexToValue = { SkipSegmentBehavior.forNumber(it) },
|
||||
valueToIndex = { it.number },
|
||||
)
|
||||
|
||||
val SkipPreviews =
|
||||
AppChoicePreference<SkipSegmentBehavior>(
|
||||
title = R.string.skip_previews_behavior,
|
||||
defaultValue = SkipSegmentBehavior.IGNORE,
|
||||
getter = { it.playbackPreferences.skipPreviews },
|
||||
setter = { prefs, value ->
|
||||
prefs.updatePlaybackPreferences { skipPreviews = value }
|
||||
},
|
||||
displayValues = R.array.skip_behaviors,
|
||||
indexToValue = { SkipSegmentBehavior.forNumber(it) },
|
||||
valueToIndex = { it.number },
|
||||
)
|
||||
|
||||
val SkipRecaps =
|
||||
AppChoicePreference<SkipSegmentBehavior>(
|
||||
title = R.string.skip_recap_behavior,
|
||||
defaultValue = SkipSegmentBehavior.IGNORE,
|
||||
getter = { it.playbackPreferences.skipRecaps },
|
||||
setter = { prefs, value ->
|
||||
prefs.updatePlaybackPreferences { skipRecaps = value }
|
||||
},
|
||||
displayValues = R.array.skip_behaviors,
|
||||
indexToValue = { SkipSegmentBehavior.forNumber(it) },
|
||||
valueToIndex = { it.number },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -386,8 +458,6 @@ val basicPreferences =
|
|||
AppPreference.AutoPlayNextUp,
|
||||
AppPreference.AutoPlayNextDelay,
|
||||
AppPreference.SkipBackOnResume,
|
||||
AppPreference.MaxBitrate,
|
||||
AppPreference.PlaybackDebugInfo,
|
||||
),
|
||||
),
|
||||
PreferenceGroup(
|
||||
|
|
@ -398,11 +468,33 @@ val basicPreferences =
|
|||
AppPreference.OssLicenseInfo,
|
||||
),
|
||||
),
|
||||
PreferenceGroup(
|
||||
title = R.string.more,
|
||||
preferences =
|
||||
listOf(
|
||||
AppPreference.AdvancedSettings,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val uiPreferences = listOf<PreferenceGroup>()
|
||||
|
||||
val advancedPreferences = listOf<PreferenceGroup>()
|
||||
val advancedPreferences =
|
||||
listOf(
|
||||
PreferenceGroup(
|
||||
title = R.string.playback,
|
||||
preferences =
|
||||
listOf(
|
||||
AppPreference.SkipIntros,
|
||||
AppPreference.SkipOutros,
|
||||
AppPreference.SkipCommercials,
|
||||
AppPreference.SkipPreviews,
|
||||
AppPreference.SkipRecaps,
|
||||
AppPreference.MaxBitrate,
|
||||
AppPreference.PlaybackDebugInfo,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
data class AppSwitchPreference(
|
||||
@get:StringRes override val title: Int,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ class AppPreferencesSerializer
|
|||
skipBackOnResumeSeconds =
|
||||
AppPreference.SkipBackOnResume.defaultValue.seconds.inWholeMilliseconds
|
||||
maxBitrate = AppPreference.DEFAULT_BITRATE
|
||||
skipIntros = AppPreference.SkipIntros.defaultValue
|
||||
skipOutros = AppPreference.SkipOutros.defaultValue
|
||||
skipCommercials = AppPreference.SkipCommercials.defaultValue
|
||||
skipPreviews = AppPreference.SkipPreviews.defaultValue
|
||||
skipRecaps = AppPreference.SkipRecaps.defaultValue
|
||||
}.build()
|
||||
homePagePreferences =
|
||||
HomePagePreferences
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package com.github.damontecres.dolphin.ui.nav
|
|||
import androidx.navigation3.runtime.NavKey
|
||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.ui.detail.series.SeasonEpisode
|
||||
import com.github.damontecres.dolphin.ui.preferences.PreferenceScreenOption
|
||||
import com.github.damontecres.dolphin.util.UuidSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
|
|
@ -28,7 +29,9 @@ sealed class Destination(
|
|||
) : Destination()
|
||||
|
||||
@Serializable
|
||||
data object Settings : Destination(true)
|
||||
data class Settings(
|
||||
val screen: PreferenceScreenOption,
|
||||
) : Destination(true)
|
||||
|
||||
@Serializable
|
||||
data object Search : Destination()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import com.github.damontecres.dolphin.ui.detail.series.SeriesOverview
|
|||
import com.github.damontecres.dolphin.ui.main.HomePage
|
||||
import com.github.damontecres.dolphin.ui.main.SearchPage
|
||||
import com.github.damontecres.dolphin.ui.playback.PlaybackContent
|
||||
import com.github.damontecres.dolphin.ui.preferences.PreferenceScreenOption
|
||||
import com.github.damontecres.dolphin.ui.preferences.PreferencesPage
|
||||
import com.github.damontecres.dolphin.ui.setup.SwitchServerContent
|
||||
import com.github.damontecres.dolphin.ui.setup.SwitchUserContent
|
||||
|
|
@ -49,10 +48,10 @@ fun DestinationContent(
|
|||
Destination.ServerList -> SwitchServerContent(modifier)
|
||||
Destination.UserList -> SwitchUserContent(modifier)
|
||||
|
||||
Destination.Settings ->
|
||||
is Destination.Settings ->
|
||||
PreferencesPage(
|
||||
preferences.appPreferences,
|
||||
PreferenceScreenOption.BASIC,
|
||||
destination.screen,
|
||||
modifier,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import com.github.damontecres.dolphin.data.model.BaseItem
|
|||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.FontAwesome
|
||||
import com.github.damontecres.dolphin.ui.ifElse
|
||||
import com.github.damontecres.dolphin.ui.preferences.PreferenceScreenOption
|
||||
import com.github.damontecres.dolphin.ui.spacedByWithFooter
|
||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.dolphin.util.ExceptionHandler
|
||||
|
|
@ -218,7 +219,11 @@ fun NavDrawer(
|
|||
icon = Icons.Default.Settings,
|
||||
selected = false,
|
||||
onClick = {
|
||||
viewModel.navigationManager.navigateTo(Destination.Settings)
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.Settings(
|
||||
PreferenceScreenOption.BASIC,
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,7 +54,9 @@ import androidx.media3.ui.compose.state.rememberNextButtonState
|
|||
import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
|
||||
import androidx.media3.ui.compose.state.rememberPresentationState
|
||||
import androidx.media3.ui.compose.state.rememberPreviousButtonState
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.preferences.skipBackOnResume
|
||||
|
|
@ -65,6 +67,7 @@ import com.github.damontecres.dolphin.ui.tryRequestFocus
|
|||
import com.github.damontecres.dolphin.util.seasonEpisode
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
|
@ -94,6 +97,7 @@ fun PlaybackContent(
|
|||
val trickplay by viewModel.trickplay.observeAsState(null)
|
||||
val chapters by viewModel.chapters.observeAsState(listOf())
|
||||
val currentPlayback by viewModel.currentPlayback.observeAsState(null)
|
||||
val currentSegment by viewModel.currentSegment.observeAsState(null)
|
||||
|
||||
var cues by remember { mutableStateOf<List<Cue>>(listOf()) }
|
||||
var showDebugInfo by remember { mutableStateOf(prefs.showDebugInfo) }
|
||||
|
|
@ -315,6 +319,29 @@ fun PlaybackContent(
|
|||
.background(Color.Transparent),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
currentSegment != null && !controllerViewState.controlsVisible && skipIndicatorDuration == 0L,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(40.dp)
|
||||
.align(Alignment.BottomEnd),
|
||||
) {
|
||||
currentSegment?.let { segment ->
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
Button(
|
||||
onClick = {
|
||||
player.seekTo(segment.endTicks.ticks.inWholeMilliseconds)
|
||||
},
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
) {
|
||||
Text(
|
||||
text = "Skip ${segment.type.serialName}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(nextUp != null) {
|
||||
|
|
|
|||
|
|
@ -17,11 +17,13 @@ import androidx.media3.exoplayer.ExoPlayer
|
|||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.data.model.Chapter
|
||||
import com.github.damontecres.dolphin.preferences.AppPreference
|
||||
import com.github.damontecres.dolphin.preferences.SkipSegmentBehavior
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.dolphin.ui.indexOfFirstOrNull
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.util.ApiRequestPager
|
||||
import com.github.damontecres.dolphin.util.EqualityMutableLiveData
|
||||
import com.github.damontecres.dolphin.util.ExceptionHandler
|
||||
import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler
|
||||
import com.github.damontecres.dolphin.util.TrackActivityPlaybackListener
|
||||
|
|
@ -33,28 +35,36 @@ import com.github.damontecres.dolphin.util.subtitleMimeTypes
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
||||
import org.jellyfin.sdk.api.client.extensions.mediaSegmentsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.trickplayApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.api.client.extensions.videosApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentType
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
|
||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||
import org.jellyfin.sdk.model.extensions.inWholeTicks
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
enum class TranscodeType {
|
||||
DIRECT_PLAY,
|
||||
|
|
@ -93,17 +103,20 @@ class PlaybackViewModel
|
|||
val currentPlayback = MutableLiveData<CurrentPlayback?>(null)
|
||||
val trickplay = MutableLiveData<TrickplayInfo?>(null)
|
||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||
val currentSegment = EqualityMutableLiveData<MediaSegmentDto?>(null)
|
||||
|
||||
private lateinit var preferences: UserPreferences
|
||||
private lateinit var deviceProfile: DeviceProfile
|
||||
private lateinit var itemId: UUID
|
||||
private lateinit var dto: BaseItemDto
|
||||
private var activityListener: TrackActivityPlaybackListener? = null
|
||||
|
||||
private val episodes = MutableLiveData<ApiRequestPager<GetEpisodesRequest>>()
|
||||
private var currentEpisodeIndex = Int.MAX_VALUE
|
||||
val nextUpEpisode = MutableLiveData<BaseItem?>()
|
||||
|
||||
init {
|
||||
addCloseable { this@PlaybackViewModel.activityListener?.release() }
|
||||
addCloseable { player.release() }
|
||||
}
|
||||
|
||||
|
|
@ -231,10 +244,15 @@ class PlaybackViewModel
|
|||
withContext(Dispatchers.Main) {
|
||||
this@PlaybackViewModel.audioStreams.value = audioStreams
|
||||
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
|
||||
this@PlaybackViewModel.activityListener?.let {
|
||||
it.release()
|
||||
player.removeListener(it)
|
||||
}
|
||||
val activityListener =
|
||||
TrackActivityPlaybackListener(api, itemId, player)
|
||||
addCloseable { activityListener.release() }
|
||||
player.addListener(activityListener)
|
||||
this@PlaybackViewModel.activityListener = activityListener
|
||||
|
||||
changeStreams(
|
||||
itemId,
|
||||
audioIndex,
|
||||
|
|
@ -249,6 +267,7 @@ class PlaybackViewModel
|
|||
if (base.type == BaseItemKind.EPISODE) {
|
||||
base.seriesId?.let(::getEpisodes)
|
||||
}
|
||||
listenForSegments()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -485,6 +504,59 @@ class PlaybackViewModel
|
|||
}
|
||||
}
|
||||
|
||||
private var segmentJob: Job? = null
|
||||
|
||||
private fun listenForSegments() {
|
||||
segmentJob?.cancel()
|
||||
segmentJob =
|
||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
val prefs = preferences.appPreferences.playbackPreferences
|
||||
val segments by api.mediaSegmentsApi.getItemSegments(itemId)
|
||||
if (segments.items.isNotEmpty()) {
|
||||
while (isActive) {
|
||||
delay(500L)
|
||||
val currentTicks = player.currentPosition.milliseconds.inWholeTicks
|
||||
val currentSegment =
|
||||
segments.items
|
||||
.firstOrNull {
|
||||
it.type != MediaSegmentType.UNKNOWN && currentTicks >= it.startTicks && currentTicks < it.endTicks
|
||||
}
|
||||
if (currentSegment != null) {
|
||||
val behavior =
|
||||
when (currentSegment.type) {
|
||||
MediaSegmentType.COMMERCIAL -> prefs.skipCommercials
|
||||
MediaSegmentType.PREVIEW -> prefs.skipPreviews
|
||||
MediaSegmentType.RECAP -> prefs.skipRecaps
|
||||
MediaSegmentType.OUTRO -> prefs.skipIntros
|
||||
MediaSegmentType.INTRO -> prefs.skipOutros
|
||||
MediaSegmentType.UNKNOWN -> SkipSegmentBehavior.IGNORE
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
when (behavior) {
|
||||
SkipSegmentBehavior.AUTO_SKIP -> {
|
||||
this@PlaybackViewModel.currentSegment.value = null
|
||||
player.seekTo(currentSegment.endTicks.ticks.inWholeMilliseconds + 1)
|
||||
}
|
||||
|
||||
SkipSegmentBehavior.ASK_TO_SKIP -> {
|
||||
this@PlaybackViewModel.currentSegment.value = currentSegment
|
||||
}
|
||||
|
||||
else -> {
|
||||
this@PlaybackViewModel.currentSegment.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
this@PlaybackViewModel.currentSegment.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun playUpNextEpisode() {
|
||||
nextUpEpisode.value?.let {
|
||||
init(Destination.Playback(it.id, 0, it), deviceProfile, preferences)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.github.damontecres.dolphin.util
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
/**
|
||||
* A [MutableLiveData] that only notifies observers if the value does not equal the previous value
|
||||
*/
|
||||
class EqualityMutableLiveData<T> : MutableLiveData<T> {
|
||||
constructor() : super()
|
||||
constructor(value: T) : super(value)
|
||||
|
||||
override fun setValue(value: T?) {
|
||||
if (value != getValue()) {
|
||||
super.setValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun setValueNoCheck(value: T?) {
|
||||
super.setValue(value)
|
||||
}
|
||||
|
||||
override fun postValue(value: T?) {
|
||||
if (value != getValue()) {
|
||||
super.postValue(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,12 @@ syntax = "proto3";
|
|||
option java_package = "com.github.damontecres.dolphin.preferences";
|
||||
option java_multiple_files = true;
|
||||
|
||||
enum SkipSegmentBehavior{
|
||||
IGNORE = 0;
|
||||
AUTO_SKIP = 1;
|
||||
ASK_TO_SKIP = 2;
|
||||
}
|
||||
|
||||
message PlaybackPreferences {
|
||||
int64 skip_forward_ms = 1;
|
||||
int64 skip_back_ms = 2;
|
||||
|
|
@ -13,6 +19,12 @@ message PlaybackPreferences {
|
|||
int64 auto_play_next_delay_seconds = 7;
|
||||
int64 skip_back_on_resume_seconds = 8;
|
||||
int64 max_bitrate = 9;
|
||||
|
||||
SkipSegmentBehavior skip_intros = 10;
|
||||
SkipSegmentBehavior skip_outros = 11;
|
||||
SkipSegmentBehavior skip_commercials = 12;
|
||||
SkipSegmentBehavior skip_recaps = 13;
|
||||
SkipSegmentBehavior skip_previews = 14;
|
||||
}
|
||||
|
||||
message HomePagePreferences{
|
||||
|
|
|
|||
|
|
@ -16,4 +16,10 @@
|
|||
<item>Orange</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="skip_behaviors">
|
||||
<item>Ignore</item>
|
||||
<item>Skip automatically</item>
|
||||
<item>Ask to skip</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -63,5 +63,10 @@
|
|||
<string name="director">Director</string>
|
||||
<string name="max_bitrate">Max bitrate</string>
|
||||
<string name="app_theme">Application Theme</string>
|
||||
<string name="skip_recap_behavior">Skip recap behavior</string>
|
||||
<string name="skip_previews_behavior">Skip previews behavior</string>
|
||||
<string name="skip_comercials_behavior">Skip commercials behavior</string>
|
||||
<string name="skip_outro_behavior">Skip outro behavior</string>
|
||||
<string name="skip_intro_behavior">Skip intro behavior</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue