mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-29 01:29:02 +03:00
Initial support for AniSkip (#772)
Co-authored-by: jmir1 <jhmiramon@gmail.com>
This commit is contained in:
parent
51ae6f8b54
commit
5f8150c735
11 changed files with 393 additions and 2 deletions
|
@ -122,12 +122,14 @@ fun AnimeScreen(
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
changeAnimeSkipIntro: (() -> Unit)?,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
||||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
|
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
|
||||||
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
||||||
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
||||||
AnimeScreenSmallImpl(
|
AnimeScreenSmallImpl(
|
||||||
|
@ -149,6 +151,7 @@ fun AnimeScreen(
|
||||||
onDownloadActionClicked = onDownloadActionClicked,
|
onDownloadActionClicked = onDownloadActionClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||||
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
|
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
|
||||||
|
@ -174,6 +177,7 @@ fun AnimeScreen(
|
||||||
onShareClicked = onShareClicked,
|
onShareClicked = onShareClicked,
|
||||||
onDownloadActionClicked = onDownloadActionClicked,
|
onDownloadActionClicked = onDownloadActionClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
|
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||||
|
@ -207,12 +211,14 @@ private fun AnimeScreenSmallImpl(
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
changeAnimeSkipIntro: (() -> Unit)?,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
||||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsRead: Boolean) -> Unit,
|
onMultiMarkAsSeenClicked: (List<Episode>, markAsRead: Boolean) -> Unit,
|
||||||
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
||||||
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
|
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
|
||||||
|
@ -305,6 +311,7 @@ private fun AnimeScreenSmallImpl(
|
||||||
onDownloadClicked = onDownloadActionClicked,
|
onDownloadClicked = onDownloadActionClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||||
doGlobalSearch = onSearch,
|
doGlobalSearch = onSearch,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
actionModeCounter = selected.size,
|
actionModeCounter = selected.size,
|
||||||
|
@ -409,12 +416,14 @@ fun AnimeScreenLargeImpl(
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
changeAnimeSkipIntro: (() -> Unit)?,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
||||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsRead: Boolean) -> Unit,
|
onMultiMarkAsSeenClicked: (List<Episode>, markAsRead: Boolean) -> Unit,
|
||||||
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
||||||
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
@ -466,6 +475,7 @@ fun AnimeScreenLargeImpl(
|
||||||
onDownloadClicked = onDownloadActionClicked,
|
onDownloadClicked = onDownloadActionClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||||
actionModeCounter = selected.size,
|
actionModeCounter = selected.size,
|
||||||
onSelectAll = {
|
onSelectAll = {
|
||||||
selected.clear()
|
selected.clear()
|
||||||
|
|
|
@ -53,6 +53,7 @@ fun AnimeSmallAppBar(
|
||||||
onShareClicked: (() -> Unit)?,
|
onShareClicked: (() -> Unit)?,
|
||||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
|
changeAnimeSkipIntro: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
// For action mode
|
// For action mode
|
||||||
actionModeCounter: Int,
|
actionModeCounter: Int,
|
||||||
|
@ -197,6 +198,13 @@ fun AnimeSmallAppBar(
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string.action_change_intro_length)) },
|
||||||
|
onClick = {
|
||||||
|
changeAnimeSkipIntro?.invoke()
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ fun AnimeTopAppBar(
|
||||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
changeAnimeSkipIntro: (() -> Unit)?,
|
||||||
doGlobalSearch: (query: String, global: Boolean) -> Unit,
|
doGlobalSearch: (query: String, global: Boolean) -> Unit,
|
||||||
scrollBehavior: TopAppBarScrollBehavior?,
|
scrollBehavior: TopAppBarScrollBehavior?,
|
||||||
// For action mode
|
// For action mode
|
||||||
|
@ -53,6 +54,7 @@ fun AnimeTopAppBar(
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
onSmallAppBarHeightChanged: (Int) -> Unit,
|
onSmallAppBarHeightChanged: (Int) -> Unit,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
val scrollPercentageProvider = { scrollBehavior?.scrollFraction?.coerceIn(0f, 1f) ?: 0f }
|
val scrollPercentageProvider = { scrollBehavior?.scrollFraction?.coerceIn(0f, 1f) ?: 0f }
|
||||||
val inverseScrollPercentageProvider = { 1f - scrollPercentageProvider() }
|
val inverseScrollPercentageProvider = { 1f - scrollPercentageProvider() }
|
||||||
|
@ -108,9 +110,11 @@ fun AnimeTopAppBar(
|
||||||
onDownloadClicked = onDownloadClicked,
|
onDownloadClicked = onDownloadClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||||
actionModeCounter = actionModeCounter,
|
actionModeCounter = actionModeCounter,
|
||||||
onSelectAll = onSelectAll,
|
onSelectAll = onSelectAll,
|
||||||
onInvertSelection = onInvertSelection,
|
onInvertSelection = onInvertSelection,
|
||||||
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { measurables, constraints ->
|
) { measurables, constraints ->
|
||||||
|
|
|
@ -136,6 +136,11 @@ object PreferenceKeys {
|
||||||
|
|
||||||
const val autoClearChapterCache = "auto_clear_chapter_cache"
|
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 trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
|
fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ import eu.kanade.tachiyomi.data.download.AnimeDownloadService
|
||||||
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
|
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
|
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
|
||||||
|
import eu.kanade.tachiyomi.databinding.PrefSkipIntroLengthBinding
|
||||||
import eu.kanade.tachiyomi.network.HttpException
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
import eu.kanade.tachiyomi.ui.anime.episode.DownloadCustomEpisodesDialog
|
import eu.kanade.tachiyomi.ui.anime.episode.DownloadCustomEpisodesDialog
|
||||||
import eu.kanade.tachiyomi.ui.anime.episode.EpisodesSettingsSheet
|
import eu.kanade.tachiyomi.ui.anime.episode.EpisodesSettingsSheet
|
||||||
|
@ -153,6 +154,7 @@ class AnimeController :
|
||||||
onMultiMarkAsSeenClicked = presenter::markEpisodesSeen,
|
onMultiMarkAsSeenClicked = presenter::markEpisodesSeen,
|
||||||
onMarkPreviousAsSeenClicked = presenter::markPreviousEpisodeSeen,
|
onMarkPreviousAsSeenClicked = presenter::markPreviousEpisodeSeen,
|
||||||
onMultiDeleteClicked = this::deleteEpisodesWithConfirmation,
|
onMultiDeleteClicked = this::deleteEpisodesWithConfirmation,
|
||||||
|
changeAnimeSkipIntro = this::changeAnimeSkipIntro.takeIf { successState.anime.favorite },
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
@ -198,6 +200,31 @@ class AnimeController :
|
||||||
startActivity(intent)
|
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() {
|
fun shareAnime() {
|
||||||
val context = view?.context ?: return
|
val context = view?.context ?: return
|
||||||
val anime = presenter.anime ?: return
|
val anime = presenter.anime ?: return
|
||||||
|
|
|
@ -49,6 +49,9 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.databinding.PlayerActivityBinding
|
import eu.kanade.tachiyomi.databinding.PlayerActivityBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
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.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
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 eu.kanade.tachiyomi.util.system.toast
|
||||||
import `is`.xyz.mpv.MPVLib
|
import `is`.xyz.mpv.MPVLib
|
||||||
import `is`.xyz.mpv.Utils
|
import `is`.xyz.mpv.Utils
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -1047,7 +1051,15 @@ class PlayerActivity :
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun skipIntro(view: View) {
|
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)
|
doubleTapSeek(presenter.getAnimeSkipIntroLength(), isDoubleTap = false)
|
||||||
playerControls.resetControlsFade()
|
playerControls.resetControlsFade()
|
||||||
}
|
}
|
||||||
|
@ -1437,6 +1449,7 @@ class PlayerActivity :
|
||||||
.coerceAtLeast(0)
|
.coerceAtLeast(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
launchUI {
|
launchUI {
|
||||||
showLoadingIndicator(false)
|
showLoadingIndicator(false)
|
||||||
if (preferences.adjustOrientationVideoDimensions()) {
|
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
|
// mpv events
|
||||||
|
@ -1456,7 +1517,10 @@ class PlayerActivity :
|
||||||
private fun eventPropertyUi(property: String, value: Long) {
|
private fun eventPropertyUi(property: String, value: Long) {
|
||||||
when (property) {
|
when (property) {
|
||||||
"demuxer-cache-time" -> playerControls.updateBufferPosition(value.toInt())
|
"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())
|
"duration" -> playerControls.updatePlaybackDuration(value.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,10 +32,14 @@ import eu.kanade.tachiyomi.data.saver.Image
|
||||||
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||||
import eu.kanade.tachiyomi.data.saver.Location
|
import eu.kanade.tachiyomi.data.saver.Location
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
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.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingUpdateJob
|
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.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.ui.reader.SaveImageNotifier
|
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.editCover
|
||||||
import eu.kanade.tachiyomi.util.episode.getEpisodeSort
|
import eu.kanade.tachiyomi.util.episode.getEpisodeSort
|
||||||
import eu.kanade.tachiyomi.util.lang.byteSize
|
import eu.kanade.tachiyomi.util.lang.byteSize
|
||||||
|
@ -585,6 +589,35 @@ class PlayerPresenter(
|
||||||
"${anime.title} - ${episode.name}".takeBytes(MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()),
|
"${anime.title} - ${episode.name}".takeBytes(MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()),
|
||||||
) + filenameSuffix
|
) + 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
|
private const val MAX_FILE_NAME_BYTES = 250
|
||||||
|
|
|
@ -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 {
|
preferenceCategory {
|
||||||
titleRes = R.string.pref_category_internal_player
|
titleRes = R.string.pref_category_internal_player
|
||||||
|
|
||||||
|
|
169
app/src/main/java/eu/kanade/tachiyomi/util/AniSkipApi.kt
Normal file
169
app/src/main/java/eu/kanade/tachiyomi/util/AniSkipApi.kt
Normal 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,
|
||||||
|
)
|
|
@ -1068,6 +1068,25 @@
|
||||||
<string name="playback_options_speed">Playback speed</string>
|
<string name="playback_options_speed">Playback speed</string>
|
||||||
<string name="playback_options_quality">Video quality</string>
|
<string name="playback_options_quality">Video quality</string>
|
||||||
<string name="playback_options_title">Playback options</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_navigate_pan">Navigate to pan</string>
|
||||||
<string name="pref_landscape_zoom">Zoom landscape image</string>
|
<string name="pref_landscape_zoom">Zoom landscape image</string>
|
||||||
|
|
Loading…
Reference in a new issue