Initial support for AniSkip (#772)

Co-authored-by: jmir1 <jhmiramon@gmail.com>
This commit is contained in:
Diego Peña Y Lillo 2022-10-31 14:46:22 -03:00 committed by GitHub
parent 51ae6f8b54
commit 5f8150c735
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 393 additions and 2 deletions

View file

@ -122,12 +122,14 @@ fun AnimeScreen(
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
changeAnimeSkipIntro: (() -> Unit)?,
// For bottom action menu
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
onMultiDeleteClicked: (List<Episode>) -> Unit,
) {
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
AnimeScreenSmallImpl(
@ -149,6 +151,7 @@ fun AnimeScreen(
onDownloadActionClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked,
onMigrateClicked = onMigrateClicked,
changeAnimeSkipIntro = changeAnimeSkipIntro,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
@ -174,6 +177,7 @@ fun AnimeScreen(
onShareClicked = onShareClicked,
onDownloadActionClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked,
changeAnimeSkipIntro = changeAnimeSkipIntro,
onMigrateClicked = onMigrateClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
@ -207,12 +211,14 @@ private fun AnimeScreenSmallImpl(
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
changeAnimeSkipIntro: (() -> Unit)?,
// For bottom action menu
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
onMultiMarkAsSeenClicked: (List<Episode>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
onMultiDeleteClicked: (List<Episode>) -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
@ -305,6 +311,7 @@ private fun AnimeScreenSmallImpl(
onDownloadClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked,
onMigrateClicked = onMigrateClicked,
changeAnimeSkipIntro = changeAnimeSkipIntro,
doGlobalSearch = onSearch,
scrollBehavior = scrollBehavior,
actionModeCounter = selected.size,
@ -409,12 +416,14 @@ fun AnimeScreenLargeImpl(
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
changeAnimeSkipIntro: (() -> Unit)?,
// For bottom action menu
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
onMultiMarkAsSeenClicked: (List<Episode>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
onMultiDeleteClicked: (List<Episode>) -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.current
@ -466,6 +475,7 @@ fun AnimeScreenLargeImpl(
onDownloadClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked,
onMigrateClicked = onMigrateClicked,
changeAnimeSkipIntro = changeAnimeSkipIntro,
actionModeCounter = selected.size,
onSelectAll = {
selected.clear()

View file

@ -53,6 +53,7 @@ fun AnimeSmallAppBar(
onShareClicked: (() -> Unit)?,
onDownloadClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?,
changeAnimeSkipIntro: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
// For action mode
actionModeCounter: Int,
@ -197,6 +198,13 @@ fun AnimeSmallAppBar(
onDismissRequest()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_change_intro_length)) },
onClick = {
changeAnimeSkipIntro?.invoke()
onDismissRequest()
},
)
}
}
}

View file

@ -46,6 +46,7 @@ fun AnimeTopAppBar(
onDownloadClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
changeAnimeSkipIntro: (() -> Unit)?,
doGlobalSearch: (query: String, global: Boolean) -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
// For action mode
@ -53,6 +54,7 @@ fun AnimeTopAppBar(
onSelectAll: () -> Unit,
onInvertSelection: () -> Unit,
onSmallAppBarHeightChanged: (Int) -> Unit,
) {
val scrollPercentageProvider = { scrollBehavior?.scrollFraction?.coerceIn(0f, 1f) ?: 0f }
val inverseScrollPercentageProvider = { 1f - scrollPercentageProvider() }
@ -108,9 +110,11 @@ fun AnimeTopAppBar(
onDownloadClicked = onDownloadClicked,
onEditCategoryClicked = onEditCategoryClicked,
onMigrateClicked = onMigrateClicked,
changeAnimeSkipIntro = changeAnimeSkipIntro,
actionModeCounter = actionModeCounter,
onSelectAll = onSelectAll,
onInvertSelection = onInvertSelection,
)
},
) { measurables, constraints ->

View file

@ -136,6 +136,11 @@ object PreferenceKeys {
const val autoClearChapterCache = "auto_clear_chapter_cache"
const val enableAniSkip = "pref_enable_ani_skip"
const val enableAutoSkip_AniSkip = "pref_enable_auto_skip_ani_skip"
const val waitingTimeAniSkip = "pref_waiting_time_aniskip"
const val enableNetflixStyleAniSkip = "pref_enable_netflixStyle_aniskip"
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"

View file

@ -461,4 +461,9 @@ class PreferencesHelper(val context: Context) {
)
}
}
fun aniSkipEnabled() = prefs.getBoolean(Keys.enableAniSkip, false)
fun autoSkipAniSkip() = prefs.getBoolean(Keys.enableAutoSkip_AniSkip, false)
fun waitingTimeAniSkip() = prefs.getString(Keys.waitingTimeAniSkip, "5")
fun enableNetflixStyleAniSkip() = prefs.getBoolean(Keys.enableNetflixStyleAniSkip, false)
}

View file

@ -39,6 +39,7 @@ import eu.kanade.tachiyomi.data.download.AnimeDownloadService
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
import eu.kanade.tachiyomi.databinding.PrefSkipIntroLengthBinding
import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.ui.anime.episode.DownloadCustomEpisodesDialog
import eu.kanade.tachiyomi.ui.anime.episode.EpisodesSettingsSheet
@ -153,6 +154,7 @@ class AnimeController :
onMultiMarkAsSeenClicked = presenter::markEpisodesSeen,
onMarkPreviousAsSeenClicked = presenter::markPreviousEpisodeSeen,
onMultiDeleteClicked = this::deleteEpisodesWithConfirmation,
changeAnimeSkipIntro = this::changeAnimeSkipIntro.takeIf { successState.anime.favorite },
)
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@ -198,6 +200,31 @@ class AnimeController :
startActivity(intent)
}
private fun changeAnimeSkipIntro() {
val anime = presenter.anime ?: return
val playerActivity = PlayerActivity()
var newSkipIntroLength = playerActivity.presenter.getAnimeSkipIntroLength()
val binding = PrefSkipIntroLengthBinding.inflate(LayoutInflater.from(activity))
playerActivity.presenter.anime = anime.toDbAnime()
with(binding.skipIntroColumn) {
value = playerActivity.presenter.getAnimeSkipIntroLength()
setOnValueChangedListener { _, _, newValue ->
newSkipIntroLength = newValue
}
}
activity?.let {
MaterialAlertDialogBuilder(it)
.setTitle(R.string.action_change_intro_length)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
playerActivity.presenter.setAnimeSkipIntroLength(newSkipIntroLength)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
fun shareAnime() {
val context = view?.context ?: return
val anime = presenter.anime ?: return

View file

@ -49,6 +49,9 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.databinding.PlayerActivityBinding
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.util.AniSkipApi
import eu.kanade.tachiyomi.util.SkipType
import eu.kanade.tachiyomi.util.Stamp
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -58,6 +61,7 @@ import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import `is`.xyz.mpv.MPVLib
import `is`.xyz.mpv.Utils
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import nucleus.factory.RequiresPresenter
import java.io.File
@ -1047,7 +1051,15 @@ class PlayerActivity :
@Suppress("UNUSED_PARAMETER")
fun skipIntro(view: View) {
if (playerControls.binding.controlsSkipIntroBtn.text != "") {
if (skipType != null) {
// this stop the counter
if (waitingAniSkip > 0) {
waitingAniSkip = -1
return
}
skipType.let { MPVLib.command(arrayOf("seek", "${aniSkipInterval!!.first{it.skipType == skipType}.interval.endTime}", "absolute")) }
AniSkipApi.PlayerUtils(binding, aniSkipInterval!!).skipAnimation(skipType!!)
} else if (playerControls.binding.controlsSkipIntroBtn.text != "") {
doubleTapSeek(presenter.getAnimeSkipIntroLength(), isDoubleTap = false)
playerControls.resetControlsFade()
}
@ -1437,6 +1449,7 @@ class PlayerActivity :
.coerceAtLeast(0)
}
}
launchUI {
showLoadingIndicator(false)
if (preferences.adjustOrientationVideoDimensions()) {
@ -1449,6 +1462,54 @@ class PlayerActivity :
}
}
}
// aniSkip stuff
waitingAniSkip = preferences.waitingTimeAniSkip()!!.toInt()
runBlocking {
aniSkipInterval = presenter.aniSkipResponse()
}
}
private val aniSkipEnable = preferences.aniSkipEnabled()
private val autoSkipAniSkip = preferences.autoSkipAniSkip()
private val netflixStyle = preferences.enableNetflixStyleAniSkip()
private var aniSkipInterval: List<Stamp>? = null
private var waitingAniSkip = preferences.waitingTimeAniSkip()!!.toInt()
var skipType: SkipType? = null
@SuppressLint("SetTextI18n")
private fun aniSkipStuff(value: Long) {
if (aniSkipEnable) {
// if it doesn't find the opening it will show the +85 button
val showNormalSkipButton = aniSkipInterval?.firstOrNull { it.skipType == SkipType.op || it.skipType == SkipType.mixedOp } == null
if (showNormalSkipButton) return
skipType = aniSkipInterval?.firstOrNull { it.interval.startTime <= value && it.interval.endTime > value }?.skipType
skipType?.let { skipType ->
val aniSkipPlayerUtils = AniSkipApi.PlayerUtils(binding, aniSkipInterval!!)
if (netflixStyle) {
// show a toast with the seconds before the skip
if (waitingAniSkip == preferences.waitingTimeAniSkip()!!.toInt()) {
Toast.makeText(
this,
"AniSkip: ${getString(R.string.player_aniskip_dontskip_toast,waitingAniSkip)}",
Toast.LENGTH_SHORT,
).show()
}
aniSkipPlayerUtils.showSkipButton(skipType, waitingAniSkip)
waitingAniSkip--
} else if (autoSkipAniSkip) {
skipType.let { MPVLib.command(arrayOf("seek", "${aniSkipInterval!!.first{it.skipType == skipType}.interval.endTime}", "absolute")) }
} else {
aniSkipPlayerUtils.showSkipButton(skipType)
}
} ?: run {
launchUI {
playerControls.binding.controlsSkipIntroBtn.isVisible = false
}
}
}
}
// mpv events
@ -1456,7 +1517,10 @@ class PlayerActivity :
private fun eventPropertyUi(property: String, value: Long) {
when (property) {
"demuxer-cache-time" -> playerControls.updateBufferPosition(value.toInt())
"time-pos" -> playerControls.updatePlaybackPos(value.toInt())
"time-pos" -> {
playerControls.updatePlaybackPos(value.toInt())
aniSkipStuff(value)
}
"duration" -> playerControls.updatePlaybackDuration(value.toInt())
}
}

View file

@ -32,10 +32,14 @@ import eu.kanade.tachiyomi.data.saver.Image
import eu.kanade.tachiyomi.data.saver.ImageSaver
import eu.kanade.tachiyomi.data.saver.Location
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingUpdateJob
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.reader.SaveImageNotifier
import eu.kanade.tachiyomi.util.AniSkipApi
import eu.kanade.tachiyomi.util.Stamp
import eu.kanade.tachiyomi.util.editCover
import eu.kanade.tachiyomi.util.episode.getEpisodeSort
import eu.kanade.tachiyomi.util.lang.byteSize
@ -585,6 +589,35 @@ class PlayerPresenter(
"${anime.title} - ${episode.name}".takeBytes(MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()),
) + filenameSuffix
}
/**
* Returns the response of the AniSkipApi for this episode.
* just works if tracking is enabled.
*/
suspend fun aniSkipResponse(): List<Stamp>? {
val trackManager = Injekt.get<TrackManager>()
var malId: Long?
val episodeNumber = getCurrentEpisodeIndex() + 1
if (getTracks.await(animeId).isEmpty()) {
logcat { "AniSkip: No tracks found for anime $animeId" }
return null
}
getTracks.await(animeId).map { track ->
val service = trackManager.getService(track.syncId)
malId = when (service) {
is MyAnimeList -> track.remoteId
is Anilist -> AniSkipApi().getMalIdFromAL(track.remoteId)
else -> null
}
val duration = view?.player?.duration ?: return null
return malId?.let {
AniSkipApi().getResult(it.toInt(), episodeNumber, duration.toLong())
}
}
return null
}
}
private const val MAX_FILE_NAME_BYTES = 250

View file

@ -144,6 +144,53 @@ class SettingsPlayerController : SettingsController() {
}
}
preferenceCategory {
titleRes = R.string.pref_category_player_aniskip
switchPreference {
key = Keys.enableAniSkip
titleRes = R.string.pref_enable_aniskip
defaultValue = false
}
switchPreference {
key = Keys.enableAutoSkip_AniSkip
titleRes = R.string.pref_enable_auto_skip_ani_skip
defaultValue = false
}
switchPreference {
key = Keys.enableNetflixStyleAniSkip
titleRes = R.string.pref_enable_netflixStyle_aniskip
defaultValue = true
}
listPreference {
key = Keys.waitingTimeAniSkip
titleRes = R.string.pref_waiting_time_aniskip
entriesRes = arrayOf(
R.string.pref_waiting_time_aniskip_5,
R.string.pref_waiting_time_aniskip_6,
R.string.pref_waiting_time_aniskip_7,
R.string.pref_waiting_time_aniskip_8,
R.string.pref_waiting_time_aniskip_9,
R.string.pref_waiting_time_aniskip_10,
)
entryValues = arrayOf(
"5",
"6",
"7",
"8",
"9",
"10",
)
defaultValue = "5"
summary = "%s"
}
}
preferenceCategory {
titleRes = R.string.pref_category_internal_player

View file

@ -0,0 +1,169 @@
package eu.kanade.tachiyomi.util
import android.annotation.SuppressLint
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.PlayerActivityBinding
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.ui.player.PlayerActivity
import eu.kanade.tachiyomi.util.lang.launchUI
import `is`.xyz.mpv.MPVLib
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.*
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import uy.kohesive.injekt.injectLazy
import java.util.*
class AniSkipApi {
private val client = OkHttpClient()
private val json: Json by injectLazy()
// credits: https://github.com/saikou-app/saikou/blob/main/app/src/main/java/ani/saikou/others/AniSkip.kt
fun getResult(malId: Int, episodeNumber: Int, episodeLength: Long): List<Stamp>? {
val url =
"https://api.aniskip.com/v2/skip-times/$malId/$episodeNumber?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=$episodeLength"
return try {
val a = client.newCall(GET(url)).execute().body!!.string()
val res = json.decodeFromString<AniSkipResponse>(a)
if (res.found) res.results else null
} catch (e: Exception) {
null
}
}
fun getMalIdFromAL(id: Long): Long {
val query = """
query{
Media(id:$id){idMal}
}
""".trimMargin()
val response = client.newCall(
POST(
"https://graphql.anilist.co",
body = buildJsonObject { put("query", query) }.toString().toRequestBody(jsonMime),
),
).execute()
return response.body!!.string().substringAfter("idMal\":").substringBefore("}")
.toLongOrNull() ?: 0
}
class PlayerUtils(
private val binding: PlayerActivityBinding,
private val aniSkipResponse: List<Stamp>,
) {
private val playerControls get() = binding.playerControls
private val activity: PlayerActivity get() = binding.root.context as PlayerActivity
fun showSkipButton(skipType: SkipType) {
val skipButtonString = when (skipType) {
SkipType.ed -> R.string.player_aniskip_ed
SkipType.op -> R.string.player_aniskip_op
SkipType.recap -> R.string.player_aniskip_recap
SkipType.mixedOp -> R.string.player_aniskip_mixedOp
}
launchUI {
playerControls.binding.controlsSkipIntroBtn.isVisible = true
playerControls.binding.controlsSkipIntroBtn.text = activity.getString(skipButtonString)
}
}
// this is used when netflixStyle is enabled
@SuppressLint("SetTextI18n")
fun showSkipButton(skipType: SkipType, waitingTime: Int) {
val skipTime = when (skipType) {
SkipType.ed -> aniSkipResponse.first { it.skipType == SkipType.ed }.interval
SkipType.op -> aniSkipResponse.first { it.skipType == SkipType.op }.interval
SkipType.recap -> aniSkipResponse.first { it.skipType == SkipType.recap }.interval
SkipType.mixedOp -> aniSkipResponse.first { it.skipType == SkipType.mixedOp }.interval
}
if (waitingTime > -1) {
if (waitingTime > 0) {
launchUI {
playerControls.binding.controlsSkipIntroBtn.isVisible = true
playerControls.binding.controlsSkipIntroBtn.text = activity.getString(R.string.player_aniskip_dontskip)
}
} else {
seekTo(skipTime.endTime)
skipAnimation(skipType)
}
} else {
// when waitingTime is -1, it means that the user cancelled the skip
showSkipButton(skipType)
}
}
fun skipAnimation(skipType: SkipType) {
binding.secondsView.binding.doubleTapSeconds.text = activity.getString(R.string.player_aniskip_skip, skipType.getString())
binding.secondsView.updateLayoutParams<ConstraintLayout.LayoutParams> {
rightToRight = ConstraintLayout.LayoutParams.PARENT_ID
leftToLeft = ConstraintLayout.LayoutParams.UNSET
}
binding.secondsView.isVisible = true
binding.secondsView.isForward = true
binding.ffwdBg.visibility = View.VISIBLE
binding.ffwdBg.animate().alpha(0.15f).setDuration(100).withEndAction {
binding.secondsView.animate().alpha(1f).setDuration(500).withEndAction {
binding.secondsView.animate().alpha(0f).setDuration(500).withEndAction {
binding.ffwdBg.animate().alpha(0f).setDuration(100).withEndAction {
binding.ffwdBg.visibility = View.GONE
binding.secondsView.isVisible = false
binding.secondsView.alpha = 1f
}
}
}
}.start()
}
private fun seekTo(time: Double) {
MPVLib.command(arrayOf("seek", time.toString(), "absolute"))
}
}
}
@Serializable
data class AniSkipResponse(
val found: Boolean,
val results: List<Stamp>?,
val message: String?,
val statusCode: Int,
)
@Serializable
data class Stamp(
val interval: AniSkipInterval,
val skipType: SkipType,
val skipId: String,
val episodeLength: Double,
)
@Suppress("EnumEntryName")
@Serializable
enum class SkipType {
op, ed, recap, @SerialName("mixed-op")
mixedOp;
fun getString(): String {
return when (this) {
op -> "Opening"
ed -> "Ending"
recap -> "Recap"
mixedOp -> "Mixed-op"
}
}
}
@Serializable
data class AniSkipInterval(
val startTime: Double,
val endTime: Double,
)

View file

@ -1068,6 +1068,25 @@
<string name="playback_options_speed">Playback speed</string>
<string name="playback_options_quality">Video quality</string>
<string name="playback_options_title">Playback options</string>
<string name="action_change_intro_length">Change intro length</string>
<string name="player_aniskip_op">Skip Opening</string>
<string name="player_aniskip_ed">Skip Ending</string>
<string name="player_aniskip_mixedOp">Skip MixedOp</string>
<string name="player_aniskip_recap">Skip Recap</string>
<string name="pref_category_player_aniskip">AniSkip Settings</string>
<string name="pref_enable_aniskip">Enable AniSkip</string>
<string name="pref_enable_auto_skip_ani_skip">Enable auto skip</string>
<string name="pref_waiting_time_aniskip">Button timeout</string>
<string name="pref_waiting_time_aniskip_5">5 seconds</string>
<string name="pref_waiting_time_aniskip_6">6 seconds</string>
<string name="pref_waiting_time_aniskip_7">7 seconds</string>
<string name="pref_waiting_time_aniskip_8">8 seconds</string>
<string name="pref_waiting_time_aniskip_9">9 seconds</string>
<string name="pref_waiting_time_aniskip_10">10 seconds</string>
<string name="pref_enable_netflixStyle_aniskip">Enable Netflix style</string>
<string name="player_aniskip_dontskip">Don\'t skip</string>
<string name="player_aniskip_dontskip_toast">Skip in %d seconds</string>
<string name="player_aniskip_skip">%s Skipped</string>
<string name="pref_navigate_pan">Navigate to pan</string>
<string name="pref_landscape_zoom">Zoom landscape image</string>