mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-24 13:48:55 +03:00
implement: compose player controls with a working timer
This commit is contained in:
parent
3485cf33e5
commit
ba330d7db5
11 changed files with 821 additions and 965 deletions
|
@ -51,6 +51,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
|||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.databinding.PlayerActivityBinding
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.player.controls.PlayerControls
|
||||
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
||||
import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.player.settings.dialogs.EpisodeListDialog
|
||||
|
@ -158,9 +159,7 @@ class PlayerActivity : BaseActivity() {
|
|||
viewModel.saveCurrentEpisodeWatchingProgress()
|
||||
|
||||
lifecycleScope.launchNonCancellable {
|
||||
viewModel.mutableState.update {
|
||||
it.copy(isLoadingEpisode = true)
|
||||
}
|
||||
viewModel.mutableState.update { it.copy(isLoadingEpisode = true) }
|
||||
|
||||
val initResult = viewModel.init(animeId, episodeId, vidList, vidIndex)
|
||||
if (!initResult.second.getOrDefault(false)) {
|
||||
|
@ -281,14 +280,9 @@ class PlayerActivity : BaseActivity() {
|
|||
|
||||
private var hadPreviousAudio = false
|
||||
|
||||
private var videoChapters: List<VideoChapter> = emptyList()
|
||||
set(value) {
|
||||
field = value
|
||||
runOnUiThread {
|
||||
playerControls.seekbar.updateSeekbar(chapters = value)
|
||||
playerControls.chapterText.updateCurrentChapterText(chapters = value)
|
||||
}
|
||||
}
|
||||
private var videoChapters
|
||||
get() = viewModel.state.value.videoChapters
|
||||
set(value) { viewModel.mutableState.update { it.copy(videoChapters = value) } }
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
playerControls.resetControlsFade()
|
||||
|
@ -519,6 +513,12 @@ class PlayerActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
binding.controlsRoot.setComposeContent {
|
||||
val state by viewModel.state.collectAsState()
|
||||
viewModel.onSecondReached(state.timeData.position, state.timeData.duration)
|
||||
PlayerControls(activity = this)
|
||||
}
|
||||
|
||||
playerIsDestroyed = false
|
||||
|
||||
registerReceiver(
|
||||
|
@ -563,7 +563,7 @@ class PlayerActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
if (playerPreferences.defaultIntroLength().get() == 0) {
|
||||
playerControls.binding.controlsSkipIntroBtn.visibility = View.GONE
|
||||
// TODO: A
|
||||
}
|
||||
|
||||
refreshUi()
|
||||
|
@ -573,7 +573,6 @@ class PlayerActivity : BaseActivity() {
|
|||
} else {
|
||||
playerControls.showAndFadeControls()
|
||||
}
|
||||
playerControls.toggleAutoplay(playerPreferences.autoplayEnabled().get())
|
||||
}
|
||||
|
||||
private fun setupPlayerMPV() {
|
||||
|
@ -886,8 +885,6 @@ class PlayerActivity : BaseActivity() {
|
|||
} else {
|
||||
playerPreferences.aspectState().get()
|
||||
}
|
||||
|
||||
playerControls.setViewMode(showText = false)
|
||||
}
|
||||
|
||||
private fun pauseForDialogSheet(fadeControls: Boolean = false): () -> Unit {
|
||||
|
@ -1015,36 +1012,10 @@ class PlayerActivity : BaseActivity() {
|
|||
if (deviceWidth <= deviceHeight) {
|
||||
deviceWidth = deviceHeight.also { deviceHeight = deviceWidth }
|
||||
}
|
||||
|
||||
playerControls.binding.episodeListBtn.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
rightToLeft = playerControls.binding.toggleAutoplay.id
|
||||
rightToRight = ConstraintLayout.LayoutParams.UNSET
|
||||
}
|
||||
playerControls.binding.settingsBtn.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
topToBottom = ConstraintLayout.LayoutParams.UNSET
|
||||
}
|
||||
playerControls.binding.toggleAutoplay.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
leftToLeft = ConstraintLayout.LayoutParams.UNSET
|
||||
leftToRight = playerControls.binding.episodeListBtn.id
|
||||
}
|
||||
} else {
|
||||
if (deviceWidth >= deviceHeight) {
|
||||
deviceWidth = deviceHeight.also { deviceHeight = deviceWidth }
|
||||
}
|
||||
|
||||
playerControls.binding.episodeListBtn.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
rightToLeft = ConstraintLayout.LayoutParams.UNSET
|
||||
rightToRight = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
}
|
||||
playerControls.binding.settingsBtn.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
topToTop = ConstraintLayout.LayoutParams.UNSET
|
||||
topToBottom = playerControls.binding.episodeListBtn.id
|
||||
}
|
||||
playerControls.binding.toggleAutoplay.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
leftToRight = ConstraintLayout.LayoutParams.UNSET
|
||||
}
|
||||
}
|
||||
setupGestures()
|
||||
viewModel.closeDialogSheet()
|
||||
|
@ -1114,7 +1085,6 @@ class PlayerActivity : BaseActivity() {
|
|||
internal fun showLoadingIndicator(visible: Boolean) {
|
||||
viewModel.viewModelScope.launchUI {
|
||||
binding.loadingIndicator.isVisible = visible
|
||||
playerControls.binding.playBtn.isVisible = !visible
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1124,9 +1094,8 @@ class PlayerActivity : BaseActivity() {
|
|||
showLoadingIndicator(position >= cachePosition && seeking)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
fun rotatePlayer(view: View) {
|
||||
fun rotatePlayer() {
|
||||
if (this.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
this.requestedOrientation = playerPreferences.defaultPlayerOrientationLandscape().get()
|
||||
} else {
|
||||
|
@ -1144,27 +1113,6 @@ class PlayerActivity : BaseActivity() {
|
|||
internal fun doubleTapPlayPause() {
|
||||
animationHandler.removeCallbacks(doubleTapPlayPauseRunnable)
|
||||
playerControls.playPause()
|
||||
|
||||
if (!playerControls.binding.unlockedView.isVisible) {
|
||||
when {
|
||||
player.paused!! -> {
|
||||
binding.playPauseView.setImageResource(R.drawable.ic_pause_64dp)
|
||||
}
|
||||
|
||||
!player.paused!! -> {
|
||||
binding.playPauseView.setImageResource(R.drawable.ic_play_arrow_64dp)
|
||||
}
|
||||
}
|
||||
|
||||
AnimationUtils.loadAnimation(this, R.anim.player_fade_in).also { fadeAnimation ->
|
||||
binding.playPauseView.startAnimation(fadeAnimation)
|
||||
binding.playPauseView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
animationHandler.postDelayed(doubleTapPlayPauseRunnable, 500L)
|
||||
} else {
|
||||
binding.playPauseView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var doubleTapBg: ImageView
|
||||
|
@ -1425,14 +1373,12 @@ class PlayerActivity : BaseActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun cycleSpeed(view: View) {
|
||||
fun cycleSpeed() {
|
||||
player.cycleSpeed()
|
||||
refreshUi()
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun skipIntro(view: View) {
|
||||
fun skipIntro() {
|
||||
if (skipType != null) {
|
||||
// this stops the counter
|
||||
if (waitingAniSkip > 0 && netflixStyle) {
|
||||
|
@ -1449,7 +1395,7 @@ class PlayerActivity : BaseActivity() {
|
|||
)
|
||||
}
|
||||
AniSkipApi.PlayerUtils(binding, aniSkipInterval!!).skipAnimation(skipType!!)
|
||||
} else if (playerControls.binding.controlsSkipIntroBtn.text != "") {
|
||||
} else {
|
||||
doubleTapSeek(viewModel.getAnimeSkipIntroLength(), isDoubleTap = false)
|
||||
playerControls.resetControlsFade()
|
||||
}
|
||||
|
@ -1461,14 +1407,16 @@ class PlayerActivity : BaseActivity() {
|
|||
internal fun refreshUi() {
|
||||
viewModel.viewModelScope.launchUI {
|
||||
setVisibilities()
|
||||
player.timePos?.let { playerControls.updatePlaybackPos(it) }
|
||||
player.duration?.let { playerControls.updatePlaybackDuration(it) }
|
||||
updatePlaybackStatus(player.paused ?: return@launchUI)
|
||||
updatePip(start = false)
|
||||
playerControls.updateEpisodeText()
|
||||
playerControls.updatePlaylistButtons()
|
||||
playerControls.updateSpeedButton()
|
||||
withIOContext { player.loadTracks() }
|
||||
player.playbackSpeed?.let { playerPreferences.playerSpeed().set(it.toFloat()) }
|
||||
viewModel.updateSkipIntroText(
|
||||
getString(
|
||||
R.string.player_controls_skip_intro_text,
|
||||
viewModel.getAnimeSkipIntroLength(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1490,9 +1438,6 @@ class PlayerActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
private fun updatePlaybackStatus(paused: Boolean) {
|
||||
val r = if (paused) R.drawable.ic_play_arrow_64dp else R.drawable.ic_pause_64dp
|
||||
playerControls.binding.playBtn.setImageResource(r)
|
||||
|
||||
if (paused) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
|
@ -1610,7 +1555,6 @@ class PlayerActivity : BaseActivity() {
|
|||
episode.last_second_seen
|
||||
}
|
||||
MPVLib.command(arrayOf("set", "start", "${resumePosition / 1000F}"))
|
||||
playerControls.updatePlaybackDuration(resumePosition.toInt() / 1000)
|
||||
}
|
||||
} else {
|
||||
player.timePos?.let {
|
||||
|
@ -1868,7 +1812,7 @@ class PlayerActivity : BaseActivity() {
|
|||
|
||||
private var skipType: SkipType? = null
|
||||
|
||||
private suspend fun aniSkipStuff(position: Long) {
|
||||
private fun aniSkipStuff(position: Long) {
|
||||
if (!aniSkipEnable) return
|
||||
// if it doesn't find any interval it will show the +85 button
|
||||
if (aniSkipInterval == null) return
|
||||
|
@ -1914,14 +1858,14 @@ class PlayerActivity : BaseActivity() {
|
|||
|
||||
internal fun eventPropertyUi(property: String, value: Long) {
|
||||
when (property) {
|
||||
"demuxer-cache-time" -> playerControls.updateBufferPosition(value.toInt())
|
||||
"demuxer-cache-time" -> viewModel.updatePlayerTime(readAhead = value)
|
||||
"time-pos" -> {
|
||||
playerControls.updatePlaybackPos(value.toInt())
|
||||
viewModel.updatePlayerTime(position = value)
|
||||
viewModel.viewModelScope.launchUI { aniSkipStuff(value) }
|
||||
updatePlaybackState()
|
||||
}
|
||||
"duration" -> {
|
||||
playerControls.updatePlaybackDuration(value.toInt())
|
||||
viewModel.updatePlayerTime(duration = value)
|
||||
mediaSession.isActive = true
|
||||
updatePlaybackState()
|
||||
}
|
||||
|
@ -1941,6 +1885,7 @@ class PlayerActivity : BaseActivity() {
|
|||
updatePlaybackStatus(value)
|
||||
updatePlaybackState(pause = true)
|
||||
refreshUi()
|
||||
viewModel.updatePlayerTime(paused = value)
|
||||
}
|
||||
}
|
||||
"eof-reached" -> endFile(value)
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
|
||||
import eu.kanade.domain.items.episode.model.toDbEpisode
|
||||
|
@ -30,6 +31,7 @@ import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
|||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.ui.player.loader.EpisodeLoader
|
||||
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
||||
import eu.kanade.tachiyomi.ui.player.viewer.SeekState
|
||||
import eu.kanade.tachiyomi.ui.player.viewer.SetAsCover
|
||||
import eu.kanade.tachiyomi.ui.reader.SaveImageNotifier
|
||||
import eu.kanade.tachiyomi.util.AniSkipApi
|
||||
|
@ -40,6 +42,7 @@ import eu.kanade.tachiyomi.util.lang.byteSize
|
|||
import eu.kanade.tachiyomi.util.lang.takeBytes
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||
import `is`.xyz.mpv.MPVView
|
||||
import `is`.xyz.mpv.Utils
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -69,6 +72,7 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import java.io.InputStream
|
||||
import java.util.Date
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
class PlayerViewModel @JvmOverloads constructor(
|
||||
private val savedState: SavedStateHandle,
|
||||
|
@ -354,7 +358,7 @@ class PlayerViewModel @JvmOverloads constructor(
|
|||
* Called every time a second is reached in the player. Used to mark the flag of episode being
|
||||
* seen, update tracking services, enqueue downloaded episode deletion and download next episode.
|
||||
*/
|
||||
fun onSecondReached(position: Int, duration: Int) {
|
||||
fun onSecondReached(position: Long, duration: Long) {
|
||||
if (state.value.isLoadingEpisode) return
|
||||
val currentEp = currentEpisode ?: return
|
||||
if (episodeId == -1L) return
|
||||
|
@ -729,8 +733,31 @@ class PlayerViewModel @JvmOverloads constructor(
|
|||
mutableState.update { it.copy(dialog = null, sheet = null) }
|
||||
}
|
||||
|
||||
fun updateSkipIntroText(text: String) {
|
||||
mutableState.update { it.copy(skipIntroText = text) }
|
||||
}
|
||||
|
||||
fun updatePlayerInformation(stringResource: StringResource) {
|
||||
mutableState.update { it.copy(playerInformation = stringResource) }
|
||||
}
|
||||
|
||||
fun updateSeekState(seekState: SeekState) {
|
||||
mutableState.update { it.copy(seekState = seekState) }
|
||||
}
|
||||
|
||||
fun updatePlayerTime(paused: Boolean? = null, position: Long? = null, duration: Long? = null, readAhead: Long? = null) {
|
||||
with(state.value.timeData) {
|
||||
val pause = paused ?: this.paused
|
||||
val pos = position ?: this.position
|
||||
val dur = duration ?: this.duration
|
||||
val rea = readAhead ?: this.readAhead
|
||||
mutableState.update { it.copy(timeData = TimeData(pause, pos, dur, rea)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val timeData: TimeData = TimeData(),
|
||||
val episodeList: List<Episode> = emptyList(),
|
||||
val episode: Episode? = null,
|
||||
val anime: Anime? = null,
|
||||
|
@ -739,25 +766,33 @@ class PlayerViewModel @JvmOverloads constructor(
|
|||
val isLoadingEpisode: Boolean = false,
|
||||
val dialog: Dialog? = null,
|
||||
val sheet: Sheet? = null,
|
||||
val videoChapters: List<MPVView.Chapter> = emptyList(),
|
||||
val skipIntroText: String = "",
|
||||
val playerInformation: StringResource = MR.strings.enable_auto_play,
|
||||
val seekState: SeekState = SeekState.NONE,
|
||||
)
|
||||
|
||||
class TimeData(val paused: Boolean, val position: Long, val duration: Long, val readAhead: Long) {
|
||||
constructor() : this(false,0L, 0L, 0L)
|
||||
}
|
||||
|
||||
class VideoStreams(val quality: Stream, val subtitle: Stream, val audio: Stream) {
|
||||
constructor() : this(Stream(), Stream(), Stream())
|
||||
class Stream(var index: Int = 0, var tracks: Array<Track> = emptyArray())
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
object EpisodeList : Dialog()
|
||||
object SpeedPicker : Dialog()
|
||||
object SkipIntroLength : Dialog()
|
||||
data object EpisodeList : Dialog()
|
||||
data object SpeedPicker : Dialog()
|
||||
data object SkipIntroLength : Dialog()
|
||||
}
|
||||
|
||||
sealed class Sheet {
|
||||
object SubtitleSettings : Sheet()
|
||||
object ScreenshotOptions : Sheet()
|
||||
object PlayerSettings : Sheet()
|
||||
object VideoChapters : Sheet()
|
||||
object StreamsCatalog : Sheet()
|
||||
data object SubtitleSettings : Sheet()
|
||||
data object ScreenshotOptions : Sheet()
|
||||
data object PlayerSettings : Sheet()
|
||||
data object VideoChapters : Sheet()
|
||||
data object StreamsCatalog : Sheet()
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
|
|
|
@ -0,0 +1,285 @@
|
|||
package eu.kanade.tachiyomi.ui.player.controls
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AutoStories
|
||||
import androidx.compose.material.icons.outlined.Fullscreen
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.material.icons.outlined.PictureInPictureAlt
|
||||
import androidx.compose.material.icons.outlined.ScreenRotation
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.player.PlayerActivity
|
||||
import eu.kanade.tachiyomi.ui.player.viewer.AspectState
|
||||
import eu.kanade.tachiyomi.ui.player.viewer.InvertedPlayback
|
||||
import eu.kanade.tachiyomi.ui.player.viewer.SeekState
|
||||
import eu.kanade.tachiyomi.ui.player.viewer.components.Seekbar
|
||||
import `is`.xyz.mpv.MPVLib
|
||||
import `is`.xyz.mpv.MPVView
|
||||
import `is`.xyz.mpv.Utils
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
fun BottomPlayerControls(
|
||||
activity: PlayerActivity,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = activity.viewModel
|
||||
val state by viewModel.state.collectAsState()
|
||||
val preferences = activity.playerPreferences
|
||||
|
||||
val includePip = !preferences.pipOnExit().get() && activity.supportedAndEnabled
|
||||
|
||||
fun onPositionChange(value: Float, wasSeeking: Boolean) {
|
||||
if (!wasSeeking) {
|
||||
SeekState.mode = SeekState.SEEKBAR
|
||||
activity.initSeek()
|
||||
}
|
||||
|
||||
MPVLib.command(arrayOf("seek", value.toInt().toString(), "absolute+keyframes"))
|
||||
|
||||
val duration = activity.player.duration ?: 0
|
||||
if (duration == 0 || activity.initialSeek < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val difference = value.toInt() - activity.initialSeek
|
||||
|
||||
activity.playerControls.showSeekText(value.toInt(), difference)
|
||||
}
|
||||
|
||||
fun onPositionChangeFinished(value: Float) {
|
||||
if (SeekState.mode == SeekState.SEEKBAR) {
|
||||
if (preferences.playerSmoothSeek().get()) {
|
||||
activity.player.timePos = value.toInt()
|
||||
} else {
|
||||
MPVLib.command(
|
||||
arrayOf("seek", value.toInt().toString(), "absolute+keyframes"),
|
||||
)
|
||||
}
|
||||
SeekState.mode = SeekState.NONE
|
||||
} else {
|
||||
MPVLib.command(arrayOf("seek", value.toInt().toString(), "absolute+keyframes"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun setViewMode() {
|
||||
viewModel.updatePlayerInformation(AspectState.mode.stringRes)
|
||||
var aspect = "-1"
|
||||
var pan = "1.0"
|
||||
when (AspectState.mode) {
|
||||
AspectState.CROP -> {
|
||||
pan = "1.0"
|
||||
}
|
||||
AspectState.FIT -> {
|
||||
pan = "0.0"
|
||||
}
|
||||
AspectState.STRETCH -> {
|
||||
aspect = "${activity.deviceWidth}/${activity.deviceHeight}"
|
||||
pan = "0.0"
|
||||
}
|
||||
AspectState.CUSTOM -> {
|
||||
aspect = MPVLib.getPropertyString("video-aspect-override")
|
||||
}
|
||||
}
|
||||
|
||||
MPVLib.setPropertyString("video-aspect-override", aspect)
|
||||
MPVLib.setPropertyString("panscan", pan)
|
||||
preferences.aspectState().set(AspectState.mode)
|
||||
}
|
||||
|
||||
fun cycleViewMode() {
|
||||
AspectState.mode = when (AspectState.mode) {
|
||||
AspectState.FIT -> AspectState.CROP
|
||||
AspectState.CROP -> AspectState.STRETCH
|
||||
else -> AspectState.FIT
|
||||
}
|
||||
setViewMode()
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
contentAlignment = Alignment.BottomStart,
|
||||
modifier = modifier.padding(all = 10.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.Bottom) {
|
||||
PlayerRow(modifier = Modifier.fillMaxWidth()) {
|
||||
|
||||
// Bottom - Left Controls
|
||||
PlayerRow {
|
||||
PlayerIcon(icon = Icons.Outlined.Lock) { viewModel.updateSeekState(SeekState.LOCKED) }
|
||||
PlayerIcon(icon = Icons.Outlined.ScreenRotation) { activity.rotatePlayer() }
|
||||
PlayerTextButton(
|
||||
text = stringResource(
|
||||
id = R.string.ui_speed,
|
||||
preferences.playerSpeed().collectAsState().value,
|
||||
),
|
||||
onClick = activity::cycleSpeed,
|
||||
onLongClick = activity.viewModel::showSpeedPicker,
|
||||
)
|
||||
|
||||
if (state.videoChapters.isNotEmpty()) {
|
||||
val currentChapter = state.videoChapters.last { it.time <= state.timeData.position }
|
||||
ChapterButton(chapter = currentChapter)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Bottom - Right Controls
|
||||
PlayerRow(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
PlayerTextButton(
|
||||
text = state.skipIntroText,
|
||||
onClick = activity::skipIntro,
|
||||
onLongClick = activity.viewModel::showSkipIntroLength,
|
||||
)
|
||||
|
||||
PlayerIcon(icon = Icons.Outlined.Fullscreen) { cycleViewMode() }
|
||||
|
||||
if (includePip) PlayerIcon(icon = Icons.Outlined.PictureInPictureAlt) { activity.updatePip(start = true) }
|
||||
}
|
||||
}
|
||||
|
||||
PlayerRow(modifier = Modifier.fillMaxWidth()) {
|
||||
fun getTimeText(time: Long) = Utils.prettyTime(time.toInt(), true).replace("+", "")
|
||||
val invertedPlayback = preferences.invertedPlayback().collectAsState().value
|
||||
|
||||
val position = when (invertedPlayback) {
|
||||
InvertedPlayback.NONE, InvertedPlayback.DURATION -> state.timeData.position
|
||||
InvertedPlayback.POSITION -> state.timeData.position - state.timeData.duration
|
||||
}
|
||||
val onPositionCLicked = {
|
||||
preferences.invertedPlayback().set(
|
||||
when (invertedPlayback) {
|
||||
InvertedPlayback.NONE, InvertedPlayback.DURATION -> InvertedPlayback.POSITION
|
||||
InvertedPlayback.POSITION -> InvertedPlayback.NONE
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val duration = when (invertedPlayback) {
|
||||
InvertedPlayback.NONE, InvertedPlayback.POSITION -> state.timeData.duration
|
||||
InvertedPlayback.DURATION -> state.timeData.position - state.timeData.duration
|
||||
}
|
||||
val onDurationCLicked = {
|
||||
preferences.invertedPlayback().set(
|
||||
when (invertedPlayback) {
|
||||
InvertedPlayback.NONE, InvertedPlayback.POSITION -> InvertedPlayback.DURATION
|
||||
InvertedPlayback.DURATION -> InvertedPlayback.NONE
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
PlayerTextButton(text = getTimeText(position), onClick = onPositionCLicked)
|
||||
|
||||
Seekbar(
|
||||
position = state.timeData.position.toFloat(),
|
||||
duration = state.timeData.duration.toFloat(),
|
||||
readAhead = state.timeData.readAhead.toFloat(),
|
||||
chapters = state.videoChapters,
|
||||
onPositionChange = ::onPositionChange,
|
||||
onPositionChangeFinished = ::onPositionChangeFinished,
|
||||
modifier = Modifier.widthIn(max = this@BoxWithConstraints.maxWidth - (textButtonWidth * 2))
|
||||
)
|
||||
|
||||
PlayerTextButton(text = getTimeText(duration), onClick = onDurationCLicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChapterButton(
|
||||
chapter: MPVView.Chapter,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(25))
|
||||
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.6F))
|
||||
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = chapter,
|
||||
transitionSpec = {
|
||||
if (targetState.time > initialState.time) {
|
||||
(slideInVertically { height -> height } + fadeIn())
|
||||
.togetherWith(slideOutVertically { height -> -height } + fadeOut())
|
||||
} else {
|
||||
(slideInVertically { height -> -height } + fadeIn())
|
||||
.togetherWith(slideOutVertically { height -> height } + fadeOut())
|
||||
}.using(
|
||||
SizeTransform(clip = false),
|
||||
)
|
||||
},
|
||||
label = "Chapter",
|
||||
) { currentChapter ->
|
||||
Row {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.AutoStories,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = MaterialTheme.padding.small)
|
||||
.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = Utils.prettyTime(currentChapter.time.toInt()),
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
)
|
||||
currentChapter.title?.let {
|
||||
Text(
|
||||
text = " • ",
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package eu.kanade.tachiyomi.ui.player.controls
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.SkipNext
|
||||
import androidx.compose.material.icons.filled.SkipPrevious
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.tachiyomi.ui.player.PlayerActivity
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun MiddlePlayerControls(
|
||||
activity: PlayerActivity,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = activity.viewModel
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
val playPauseIcon = if(state.timeData.paused) Icons.Filled.PlayArrow else Icons.Filled.Pause
|
||||
|
||||
fun switchEpisode(previous: Boolean) {
|
||||
return activity.changeEpisode(viewModel.getAdjacentEpisodeId(previous = previous))
|
||||
}
|
||||
|
||||
val plCount = viewModel.currentPlaylist.size
|
||||
val plPos = viewModel.getCurrentEpisodeIndex()
|
||||
|
||||
Box(contentAlignment = Alignment.Center, modifier = modifier) {
|
||||
PlayerRow(horizontalArrangement = Arrangement.spacedBy(iconSize * 2)) {
|
||||
PlayerIcon(
|
||||
icon = Icons.Filled.SkipPrevious,
|
||||
multiplier = 2,
|
||||
enabled = plPos != 0
|
||||
) { switchEpisode(previous = true) }
|
||||
|
||||
PlayerIcon(icon = playPauseIcon, multiplier = 3) { activity.playerControls.playPause() }
|
||||
|
||||
PlayerIcon(
|
||||
icon = Icons.Filled.SkipNext,
|
||||
multiplier = 2,
|
||||
enabled = plPos != plCount - 1
|
||||
) { switchEpisode(previous = false) }
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(resource = state.playerInformation),
|
||||
modifier = Modifier.padding(top = iconSize * 8),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package eu.kanade.tachiyomi.ui.player.controls
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.LockOpen
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.commonClickable
|
||||
import eu.kanade.tachiyomi.ui.player.PlayerActivity
|
||||
import eu.kanade.tachiyomi.ui.player.viewer.SeekState
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun PlayerControls(
|
||||
activity: PlayerActivity,
|
||||
modifier: Modifier = Modifier,
|
||||
timerState: TimerState = rememberTimerState(),
|
||||
) {
|
||||
val state by activity.viewModel.state.collectAsState()
|
||||
|
||||
val onPlayerPressed = { timerState.controls = if (timerState.controls > 0L) 0L else 3500L }
|
||||
val playerModifier = modifier.pointerInput(Unit) { detectTapGestures(onPress = { onPlayerPressed() }) }
|
||||
|
||||
Surface(modifier = playerModifier, color = Color.Transparent) {
|
||||
if (state.seekState == SeekState.LOCKED) {
|
||||
LockedPlayerControls { activity.viewModel.updateSeekState(SeekState.NONE) }
|
||||
} else {
|
||||
if (timerState.controls > 0L) UnlockedPlayerControls(activity)
|
||||
}
|
||||
|
||||
Text(timerState.controls.toString())
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = timerState.controls, key2 = state.timeData.paused) {
|
||||
if(timerState.controls > 0L && !state.timeData.paused) {
|
||||
delay(500L)
|
||||
timerState.controls -= 500L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockedPlayerControls(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.TopStart,
|
||||
modifier = modifier.padding(all = 10.dp)
|
||||
) {
|
||||
PlayerIcon(icon = Icons.Outlined.LockOpen, onClick = onClick)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ComposeModifierReused")
|
||||
@Composable
|
||||
private fun UnlockedPlayerControls(
|
||||
activity: PlayerActivity,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(color = Color(color = 0x70000000)) {
|
||||
TopPlayerControls(activity)
|
||||
MiddlePlayerControls(activity)
|
||||
BottomPlayerControls(activity)
|
||||
}
|
||||
}
|
||||
|
||||
val iconSize = 20.dp
|
||||
val buttonSize = iconSize + 30.dp
|
||||
val textButtonWidth = 80.dp
|
||||
|
||||
@Composable
|
||||
fun PlayerIcon(
|
||||
icon: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
multiplier: Int = 1,
|
||||
enabled: Boolean = true,
|
||||
timerState: TimerState = rememberTimerState(),
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
val iconSize = iconSize * multiplier
|
||||
val buttonSize = iconSize + 30.dp
|
||||
val onPlayerPressed = { timerState.controls = if (timerState.controls > 0L) 0L else 3500L }
|
||||
val buttonModifier = modifier.size(buttonSize).pointerInput(Unit) { detectTapGestures { onClick() } }
|
||||
|
||||
IconButton(onClick = { onClick(); onPlayerPressed() }, modifier = modifier.size(buttonSize), enabled = enabled){
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(iconSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlayerRow(
|
||||
modifier: Modifier = Modifier,
|
||||
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) = Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = horizontalArrangement,
|
||||
verticalAlignment = verticalAlignment,
|
||||
content = content
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun PlayerTextButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
){
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.size(width = textButtonWidth, height = buttonSize)
|
||||
.commonClickable(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class TimerState (
|
||||
controls: Long = 3500L,
|
||||
) {
|
||||
var controls by mutableStateOf(controls)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberTimerState(
|
||||
controls: Long = 3500L,
|
||||
): TimerState {
|
||||
return remember {
|
||||
TimerState(
|
||||
controls = controls,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package eu.kanade.tachiyomi.ui.player.controls
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.outlined.AutoStories
|
||||
import androidx.compose.material.icons.outlined.ChevronRight
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.outlined.VideoSettings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.ui.player.PlayerActivity
|
||||
import eu.kanade.tachiyomi.ui.player.PlayerViewModel
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
fun TopPlayerControls(
|
||||
activity: PlayerActivity,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = activity.viewModel
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
|
||||
fun toggleAutoplay(autoplay: Boolean) {
|
||||
with(activity.playerPreferences.autoplayEnabled()){
|
||||
this.set(autoplay)
|
||||
val playerInformation = if (this.get()) MR.strings.enable_auto_play else MR.strings.disable_auto_play
|
||||
viewModel.updatePlayerInformation(playerInformation)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.TopStart,
|
||||
modifier = modifier.padding(all = 10.dp)
|
||||
) {
|
||||
SecondaryTopControlsLayout(isPortrait) {
|
||||
|
||||
PlayerRow {
|
||||
PlayerIcon(icon = Icons.AutoMirrored.Outlined.ArrowBack, onClick = activity::onBackPressed)
|
||||
|
||||
PlayerRow(modifier = Modifier.clickable { viewModel.showEpisodeList() }) {
|
||||
Column(Modifier.padding(horizontal = 10.dp)) {
|
||||
Text(
|
||||
text = state.anime?.title ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = state.episode?.name ?: "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontStyle = FontStyle.Italic,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(0.5f),
|
||||
)
|
||||
}
|
||||
|
||||
val expanded = state.dialog == PlayerViewModel.Dialog.EpisodeList
|
||||
val rotation by animateFloatAsState(if (expanded) 90f else 0f)
|
||||
PlayerIcon(
|
||||
icon = Icons.Outlined.ChevronRight,
|
||||
modifier = Modifier.rotate(rotation),
|
||||
) { viewModel.showEpisodeList() }
|
||||
}
|
||||
}
|
||||
|
||||
PlayerRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = if (isPortrait) Arrangement.SpaceEvenly else Arrangement.End,
|
||||
) {
|
||||
AutoplaySwitch(
|
||||
checked = activity.playerPreferences.autoplayEnabled().collectAsState(),
|
||||
onClick = ::toggleAutoplay,
|
||||
)
|
||||
|
||||
|
||||
|
||||
PlayerIcon(icon = Icons.Outlined.VideoSettings) { viewModel.showStreamsCatalog() }
|
||||
PlayerIcon(icon = Icons.Outlined.MoreVert) { viewModel.showPlayerSettings() }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecondaryTopControlsLayout(
|
||||
isPortrait: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
if (isPortrait) {
|
||||
Column { content() }
|
||||
} else {
|
||||
PlayerRow { content() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AutoplaySwitch(
|
||||
checked: State<Boolean>,
|
||||
onClick: (Boolean) -> Unit,
|
||||
) {
|
||||
Switch(
|
||||
checked = checked.value,
|
||||
onCheckedChange = onClick,
|
||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.small),
|
||||
thumbContent = if (checked.value) {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Pause,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -39,203 +39,23 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
|
||||
private val player get() = activity.player
|
||||
|
||||
val seekbar: Seekbar = Seekbar(
|
||||
view = binding.playbackSeekbar,
|
||||
onValueChange = ::onValueChange,
|
||||
onValueChangeFinished = ::onValueChangeFinished,
|
||||
)
|
||||
|
||||
val chapterText: CurrentChapter = CurrentChapter(
|
||||
view = binding.currentChapter,
|
||||
onClick = { activity.viewModel.showVideoChapters() },
|
||||
)
|
||||
|
||||
private fun onValueChange(value: Float, wasSeeking: Boolean) {
|
||||
if (!wasSeeking) {
|
||||
SeekState.mode = SeekState.SEEKBAR
|
||||
activity.initSeek()
|
||||
}
|
||||
|
||||
MPVLib.command(arrayOf("seek", value.toInt().toString(), "absolute+keyframes"))
|
||||
|
||||
val duration = player.duration ?: 0
|
||||
if (duration == 0 || activity.initialSeek < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val difference = value.toInt() - activity.initialSeek
|
||||
|
||||
showSeekText(value.toInt(), difference)
|
||||
}
|
||||
|
||||
private fun onValueChangeFinished(value: Float) {
|
||||
if (SeekState.mode == SeekState.SEEKBAR) {
|
||||
if (playerPreferences.playerSmoothSeek().get()) {
|
||||
player.timePos = value.toInt()
|
||||
} else {
|
||||
MPVLib.command(
|
||||
arrayOf("seek", value.toInt().toString(), "absolute+keyframes"),
|
||||
)
|
||||
}
|
||||
SeekState.mode = SeekState.NONE
|
||||
animationHandler.removeCallbacks(hideUiForSeekRunnable)
|
||||
animationHandler.removeCallbacks(fadeOutControlsRunnable)
|
||||
animationHandler.postDelayed(hideUiForSeekRunnable, 500L)
|
||||
animationHandler.postDelayed(fadeOutControlsRunnable, 3500L)
|
||||
} else {
|
||||
MPVLib.command(arrayOf("seek", value.toInt().toString(), "absolute+keyframes"))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
addView(binding.root)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onViewAdded(child: View?) {
|
||||
binding.backArrowBtn.setOnClickListener { activity.onBackPressed() }
|
||||
|
||||
// Lock and Unlock controls
|
||||
binding.lockBtn.setOnClickListener { lockControls(true) }
|
||||
binding.unlockBtn.setOnClickListener { lockControls(false) }
|
||||
|
||||
// Long click controls
|
||||
binding.cycleSpeedBtn.setOnLongClickListener {
|
||||
activity.viewModel.showSpeedPicker()
|
||||
true
|
||||
}
|
||||
|
||||
binding.prevBtn.setOnClickListener { switchEpisode(previous = true) }
|
||||
binding.playBtn.setOnClickListener { playPause() }
|
||||
binding.nextBtn.setOnClickListener { switchEpisode(previous = false) }
|
||||
|
||||
binding.pipBtn.setOnClickListener { activity.updatePip(start = true) }
|
||||
|
||||
binding.pipBtn.isVisible = !playerPreferences.pipOnExit().get() && activity.supportedAndEnabled
|
||||
|
||||
binding.controlsSkipIntroBtn.setOnLongClickListener {
|
||||
activity.viewModel.showSkipIntroLength()
|
||||
true
|
||||
}
|
||||
|
||||
binding.playbackPositionBtn.setOnClickListener {
|
||||
if (player.timePos != null && player.duration != null) {
|
||||
with(playerPreferences.invertedPlayback()) {
|
||||
this.set(
|
||||
if (this.get() == InvertedPlayback.POSITION) {
|
||||
InvertedPlayback.NONE
|
||||
} else {
|
||||
InvertedPlayback.POSITION
|
||||
},
|
||||
)
|
||||
}
|
||||
updatePlaybackPos(player.timePos!!)
|
||||
updatePlaybackDuration(player.duration!!)
|
||||
}
|
||||
}
|
||||
|
||||
binding.playbackDurationBtn.setOnClickListener {
|
||||
if (player.timePos != null && player.duration != null) {
|
||||
with(playerPreferences.invertedPlayback()) {
|
||||
this.set(
|
||||
if (this.get() == InvertedPlayback.DURATION) {
|
||||
InvertedPlayback.NONE
|
||||
} else {
|
||||
InvertedPlayback.DURATION
|
||||
},
|
||||
)
|
||||
}
|
||||
updatePlaybackPos(player.timePos!!)
|
||||
updatePlaybackDuration(player.duration!!)
|
||||
}
|
||||
}
|
||||
|
||||
binding.toggleAutoplay.setOnCheckedChangeListener { _, isChecked ->
|
||||
toggleAutoplay(
|
||||
isChecked,
|
||||
)
|
||||
}
|
||||
|
||||
binding.cycleViewModeBtn.setOnClickListener { cycleViewMode() }
|
||||
|
||||
binding.settingsBtn.setOnClickListener { activity.viewModel.showPlayerSettings() }
|
||||
|
||||
binding.streamsBtn.setOnClickListener { activity.viewModel.showStreamsCatalog() }
|
||||
|
||||
binding.titleMainTxt.setOnClickListener { activity.viewModel.showEpisodeList() }
|
||||
|
||||
binding.titleSecondaryTxt.setOnClickListener { activity.viewModel.showEpisodeList() }
|
||||
|
||||
binding.episodeListBtn.setOnClickListener { activity.viewModel.showEpisodeList() }
|
||||
}
|
||||
|
||||
private fun switchEpisode(previous: Boolean) {
|
||||
return activity.changeEpisode(activity.viewModel.getAdjacentEpisodeId(previous = previous))
|
||||
}
|
||||
|
||||
internal suspend fun updateEpisodeText() {
|
||||
val viewModel = activity.viewModel
|
||||
val skipIntroText = activity.getString(
|
||||
R.string.player_controls_skip_intro_text,
|
||||
viewModel.getAnimeSkipIntroLength(),
|
||||
)
|
||||
withUIContext {
|
||||
binding.titleMainTxt.text = viewModel.currentAnime?.title
|
||||
binding.titleSecondaryTxt.text = viewModel.currentEpisode?.name
|
||||
binding.controlsSkipIntroBtn.text = skipIntroText
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun updatePlaylistButtons() {
|
||||
val viewModel = activity.viewModel
|
||||
val plCount = viewModel.currentPlaylist.size
|
||||
val plPos = viewModel.getCurrentEpisodeIndex()
|
||||
|
||||
val grey = ContextCompat.getColor(context, R.color.tint_disabled)
|
||||
val white = ContextCompat.getColor(context, R.color.tint_normal)
|
||||
withUIContext {
|
||||
with(binding.prevBtn) {
|
||||
this.imageTintList = ColorStateList.valueOf(if (plPos == 0) grey else white)
|
||||
this.isClickable = plPos != 0
|
||||
}
|
||||
with(binding.nextBtn) {
|
||||
this.imageTintList =
|
||||
ColorStateList.valueOf(if (plPos == plCount - 1) grey else white)
|
||||
this.isClickable = plPos != plCount - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun updateSpeedButton() {
|
||||
withUIContext {
|
||||
binding.cycleSpeedBtn.text = context.getString(R.string.ui_speed, player.playbackSpeed)
|
||||
player.playbackSpeed?.let { playerPreferences.playerSpeed().set(it.toFloat()) }
|
||||
}
|
||||
}
|
||||
|
||||
private var showControls = false
|
||||
private var wasPausedBeforeSeeking = false
|
||||
|
||||
private val nonSeekViewRunnable = Runnable {
|
||||
binding.topControlsGroup.visibility = View.VISIBLE
|
||||
binding.middleControlsGroup.visibility = View.VISIBLE
|
||||
binding.bottomControlsGroup.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private val hideUiForSeekRunnable = Runnable {
|
||||
SeekState.mode = SeekState.NONE
|
||||
player.paused = wasPausedBeforeSeeking
|
||||
if (showControls) {
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_fade_in).also { fadeAnimation ->
|
||||
binding.topControlsGroup.startAnimation(fadeAnimation)
|
||||
binding.topControlsGroup.visibility = View.VISIBLE
|
||||
|
||||
binding.middleControlsGroup.startAnimation(fadeAnimation)
|
||||
binding.middleControlsGroup.visibility = View.VISIBLE
|
||||
|
||||
binding.bottomControlsGroup.startAnimation(fadeAnimation)
|
||||
binding.bottomControlsGroup.visibility = View.VISIBLE
|
||||
}
|
||||
showControls = false
|
||||
} else {
|
||||
showControls = true
|
||||
|
@ -254,17 +74,8 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
animationHandler.removeCallbacks(fadeOutControlsRunnable)
|
||||
animationHandler.removeCallbacks(hideUiForSeekRunnable)
|
||||
|
||||
if (!(
|
||||
binding.topControlsGroup.visibility == View.INVISIBLE &&
|
||||
binding.middleControlsGroup.visibility == INVISIBLE &&
|
||||
binding.bottomControlsGroup.visibility == INVISIBLE
|
||||
)
|
||||
) {
|
||||
|
||||
wasPausedBeforeSeeking = player.paused!!
|
||||
showControls = binding.unlockedView.isVisible
|
||||
binding.topControlsGroup.visibility = View.INVISIBLE
|
||||
binding.middleControlsGroup.visibility = View.INVISIBLE
|
||||
binding.bottomControlsGroup.visibility = View.INVISIBLE
|
||||
player.paused = true
|
||||
animationHandler.removeCallbacks(volumeViewRunnable)
|
||||
animationHandler.removeCallbacks(brightnessViewRunnable)
|
||||
|
@ -272,10 +83,7 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
binding.volumeView.visibility = View.GONE
|
||||
binding.brightnessView.visibility = View.GONE
|
||||
activity.binding.seekView.visibility = View.GONE
|
||||
binding.seekBarGroup.visibility = View.VISIBLE
|
||||
binding.unlockedView.visibility = View.VISIBLE
|
||||
SeekState.mode = SeekState.SCROLL
|
||||
}
|
||||
|
||||
val delay = if (SeekState.mode == SeekState.DOUBLE_TAP) 1000L else 500L
|
||||
|
||||
|
@ -288,84 +96,20 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
internal val fadeOutControlsRunnable = Runnable { fadeOutControls() }
|
||||
|
||||
internal fun lockControls(locked: Boolean) {
|
||||
SeekState.mode = if (locked) SeekState.LOCKED else SeekState.NONE
|
||||
val itemView = if (locked) binding.unlockedView else binding.lockedView
|
||||
itemView.visibility = View.GONE
|
||||
showAndFadeControls()
|
||||
}
|
||||
|
||||
internal fun toggleControls(isTapped: Boolean = false) {
|
||||
val isControlsVisible = binding.lockedView.isVisible || binding.unlockedView.isVisible
|
||||
if (!isControlsVisible && !player.paused!!) {
|
||||
showAndFadeControls()
|
||||
} else if (!isControlsVisible && player.paused!!) {
|
||||
fadeInControls()
|
||||
} else if (isTapped) {
|
||||
fadeOutControls()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun hideControls(hide: Boolean) {
|
||||
animationHandler.removeCallbacks(fadeOutControlsRunnable)
|
||||
if (hide) {
|
||||
binding.unlockedView.visibility = View.GONE
|
||||
binding.lockedView.visibility = View.GONE
|
||||
} else {
|
||||
showAndFadeControls()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
internal fun updatePlaybackPos(position: Int) {
|
||||
val duration = player.duration
|
||||
val invertedPlayback = playerPreferences.invertedPlayback().get()
|
||||
|
||||
if (duration != null) {
|
||||
binding.playbackPositionBtn.text = when (invertedPlayback) {
|
||||
InvertedPlayback.POSITION -> "-${Utils.prettyTime(duration - position)}"
|
||||
InvertedPlayback.DURATION -> Utils.prettyTime(position)
|
||||
InvertedPlayback.NONE -> Utils.prettyTime(position)
|
||||
}
|
||||
binding.playbackDurationBtn.text = when (invertedPlayback) {
|
||||
InvertedPlayback.POSITION -> Utils.prettyTime(duration)
|
||||
InvertedPlayback.DURATION -> "-${Utils.prettyTime(duration - position)}"
|
||||
InvertedPlayback.NONE -> Utils.prettyTime(duration)
|
||||
}
|
||||
activity.viewModel.onSecondReached(position, duration)
|
||||
}
|
||||
seekbar.updateSeekbar(value = position.toFloat())
|
||||
chapterText.updateCurrentChapterText(value = position.toFloat())
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
internal fun updatePlaybackDuration(duration: Int) {
|
||||
val position = player.timePos
|
||||
val invertedPlayback = playerPreferences.invertedPlayback().get()
|
||||
if (position != null) {
|
||||
binding.playbackDurationBtn.text = when (invertedPlayback) {
|
||||
InvertedPlayback.POSITION -> Utils.prettyTime(duration)
|
||||
InvertedPlayback.DURATION -> "-${Utils.prettyTime(duration - position)}"
|
||||
InvertedPlayback.NONE -> Utils.prettyTime(duration)
|
||||
}
|
||||
}
|
||||
|
||||
seekbar.updateSeekbar(duration = duration.toFloat())
|
||||
}
|
||||
|
||||
internal fun updateBufferPosition(bufferPosition: Int) {
|
||||
seekbar.updateSeekbar(readAheadValue = bufferPosition.toFloat())
|
||||
}
|
||||
|
||||
internal fun showAndFadeControls() {
|
||||
val itemView = if (SeekState.mode == SeekState.LOCKED) binding.lockedView else binding.unlockedView
|
||||
if (!itemView.isVisible) fadeInControls()
|
||||
itemView.visibility = View.VISIBLE
|
||||
resetControlsFade()
|
||||
}
|
||||
|
||||
internal fun resetControlsFade() {
|
||||
val itemView = if (SeekState.mode == SeekState.LOCKED) binding.lockedView else binding.unlockedView
|
||||
if (!itemView.isVisible) return
|
||||
animationHandler.removeCallbacks(fadeOutControlsRunnable)
|
||||
if (SeekState.mode == SeekState.SEEKBAR) return
|
||||
animationHandler.postDelayed(fadeOutControlsRunnable, 3500L)
|
||||
|
@ -373,147 +117,20 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
|
||||
private fun fadeOutControls() {
|
||||
animationHandler.removeCallbacks(fadeOutControlsRunnable)
|
||||
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_fade_out).also { fadeAnimation ->
|
||||
val itemView = if (SeekState.mode == SeekState.LOCKED) binding.lockedView else binding.unlockedView
|
||||
itemView.startAnimation(fadeAnimation)
|
||||
itemView.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.seekBarGroup.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_exit_bottom),
|
||||
)
|
||||
if (!showControls) {
|
||||
binding.topControlsGroup.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_exit_top),
|
||||
)
|
||||
binding.bottomRightControlsGroup.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_exit_right),
|
||||
)
|
||||
binding.bottomLeftControlsGroup.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_exit_left),
|
||||
)
|
||||
binding.currentChapter.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_exit_left),
|
||||
)
|
||||
binding.middleControlsGroup.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_fade_out),
|
||||
)
|
||||
}
|
||||
showControls = false
|
||||
}
|
||||
|
||||
private fun fadeInControls() {
|
||||
animationHandler.removeCallbacks(fadeOutControlsRunnable)
|
||||
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_fade_in).also { fadeAnimation ->
|
||||
val itemView = if (SeekState.mode == SeekState.LOCKED) binding.lockedView else binding.unlockedView
|
||||
itemView.startAnimation(fadeAnimation)
|
||||
itemView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
binding.seekBarGroup.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_enter_bottom),
|
||||
)
|
||||
binding.topControlsGroup.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_enter_top),
|
||||
)
|
||||
binding.bottomRightControlsGroup.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_enter_right),
|
||||
)
|
||||
binding.bottomLeftControlsGroup.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_enter_left),
|
||||
)
|
||||
binding.currentChapter.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_enter_left),
|
||||
)
|
||||
binding.middleControlsGroup.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_fade_in),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun playPause() {
|
||||
player.cyclePause()
|
||||
when {
|
||||
player.paused!! -> animationHandler.removeCallbacks(fadeOutControlsRunnable)
|
||||
binding.unlockedView.isVisible -> showAndFadeControls()
|
||||
}
|
||||
}
|
||||
|
||||
// Fade out Player information text
|
||||
private val playerInformationRunnable = Runnable {
|
||||
AnimationUtils.loadAnimation(context, R.anim.player_fade_out).also { fadeAnimation ->
|
||||
binding.playerInformation.startAnimation(fadeAnimation)
|
||||
binding.playerInformation.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun cycleViewMode() {
|
||||
AspectState.mode = when (AspectState.mode) {
|
||||
AspectState.FIT -> AspectState.CROP
|
||||
AspectState.CROP -> AspectState.STRETCH
|
||||
else -> AspectState.FIT
|
||||
}
|
||||
setViewMode(showText = true)
|
||||
}
|
||||
|
||||
internal fun setViewMode(showText: Boolean) {
|
||||
binding.playerInformation.text = activity.stringResource(AspectState.mode.stringRes)
|
||||
var aspect = "-1"
|
||||
var pan = "1.0"
|
||||
when (AspectState.mode) {
|
||||
AspectState.CROP -> {
|
||||
pan = "1.0"
|
||||
}
|
||||
AspectState.FIT -> {
|
||||
pan = "0.0"
|
||||
}
|
||||
AspectState.STRETCH -> {
|
||||
aspect = "${activity.deviceWidth}/${activity.deviceHeight}"
|
||||
pan = "0.0"
|
||||
}
|
||||
AspectState.CUSTOM -> {
|
||||
aspect = MPVLib.getPropertyString("video-aspect-override")
|
||||
}
|
||||
}
|
||||
|
||||
mpvUpdateAspect(aspect = aspect, pan = pan)
|
||||
playerPreferences.aspectState().set(AspectState.mode)
|
||||
|
||||
if (showText) {
|
||||
animationHandler.removeCallbacks(playerInformationRunnable)
|
||||
binding.playerInformation.visibility = View.VISIBLE
|
||||
animationHandler.postDelayed(playerInformationRunnable, 1000L)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mpvUpdateAspect(aspect: String, pan: String) {
|
||||
MPVLib.setPropertyString("video-aspect-override", aspect)
|
||||
MPVLib.setPropertyString("panscan", pan)
|
||||
}
|
||||
|
||||
internal fun toggleAutoplay(isAutoplay: Boolean) {
|
||||
binding.toggleAutoplay.isChecked = isAutoplay
|
||||
binding.toggleAutoplay.thumbDrawable = if (isAutoplay) {
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_play_circle_filled_24)
|
||||
} else {
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_pause_circle_filled_24)
|
||||
}
|
||||
|
||||
if (isAutoplay) {
|
||||
binding.playerInformation.text = activity.getString(R.string.enable_auto_play)
|
||||
} else {
|
||||
binding.playerInformation.text = activity.getString(R.string.disable_auto_play)
|
||||
}
|
||||
|
||||
if (!playerPreferences.autoplayEnabled().get() == isAutoplay) {
|
||||
animationHandler.removeCallbacks(playerInformationRunnable)
|
||||
binding.playerInformation.visibility = View.VISIBLE
|
||||
animationHandler.postDelayed(playerInformationRunnable, 1000L)
|
||||
}
|
||||
playerPreferences.autoplayEnabled().set(isAutoplay)
|
||||
}
|
||||
|
||||
// Fade out seek text
|
||||
private val seekTextRunnable = Runnable {
|
||||
activity.binding.seekView.visibility = View.GONE
|
||||
|
@ -583,7 +200,6 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
|
||||
internal fun showSeekText(position: Int, difference: Int) {
|
||||
hideUiForSeek()
|
||||
updatePlaybackPos(position)
|
||||
|
||||
val diffText = Utils.prettyTime(difference, true)
|
||||
activity.binding.seekText.text = activity.getString(
|
||||
|
|
|
@ -7,8 +7,10 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -18,100 +20,64 @@ import dev.vivvvek.seeker.Segment
|
|||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||
import `is`.xyz.mpv.MPVView.Chapter
|
||||
|
||||
class Seekbar(
|
||||
private val view: ComposeView,
|
||||
private val onValueChange: (Float, Boolean) -> Unit,
|
||||
private val onValueChangeFinished: (Float) -> Unit,
|
||||
@Composable
|
||||
fun Seekbar(
|
||||
position: Float,
|
||||
duration: Float,
|
||||
readAhead: Float,
|
||||
chapters: List<Chapter>,
|
||||
onPositionChange: (Float, Boolean) -> Unit,
|
||||
onPositionChangeFinished: (Float) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private var duration: Float = 1F
|
||||
private var value: Float = 0F
|
||||
private var readAheadValue: Float = 0F
|
||||
private var chapters: List<Chapter> = listOf()
|
||||
private var isDragging: Boolean = false
|
||||
val range = 0F..duration
|
||||
val validSegments = chapters.toSegments().filter { it.start in range }
|
||||
var mutablePosition by remember { mutableFloatStateOf(position) }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isDragging by interactionSource.collectIsDraggedAsState()
|
||||
val gap by animateDpAsState(if (isDragging) 5.dp else 2.dp, label = "gap")
|
||||
val thumbRadius by animateDpAsState(if (isDragging) 10.dp else 8.dp, label = "thumbRadius")
|
||||
val trackHeight by animateDpAsState(
|
||||
targetValue = if (isDragging) 6.dp else 4.dp,
|
||||
label = "trackHeight",
|
||||
)
|
||||
var isSeeked by remember { mutableStateOf(false) }
|
||||
|
||||
fun updateSeekbar(
|
||||
duration: Float? = null,
|
||||
value: Float? = null,
|
||||
readAheadValue: Float? = null,
|
||||
chapters: List<Chapter>? = null,
|
||||
) {
|
||||
if (duration != null) {
|
||||
this.duration = duration
|
||||
}
|
||||
if (value != null) {
|
||||
this.value = value
|
||||
}
|
||||
if (readAheadValue != null) {
|
||||
this.readAheadValue = readAheadValue
|
||||
}
|
||||
if (chapters != null) {
|
||||
this.chapters = chapters
|
||||
}
|
||||
|
||||
view.setComposeContent {
|
||||
SeekbarComposable(
|
||||
duration ?: this.duration,
|
||||
value ?: this.value,
|
||||
readAheadValue ?: this.readAheadValue,
|
||||
chapters?.toSegments()
|
||||
?: this.chapters.toSegments(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeekbarComposable(
|
||||
duration: Float,
|
||||
value: Float,
|
||||
readAheadValue: Float,
|
||||
segments: List<Segment>,
|
||||
) {
|
||||
val range = 0F..duration
|
||||
val validSegments = segments.filter { it.start in range }
|
||||
var mutableValue by remember { mutableFloatStateOf(value) }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isDragging by interactionSource.collectIsDraggedAsState()
|
||||
val gap by animateDpAsState(if (isDragging) 5.dp else 2.dp, label = "gap")
|
||||
val thumbRadius by animateDpAsState(if (isDragging) 10.dp else 8.dp, label = "thumbRadius")
|
||||
val trackHeight by animateDpAsState(
|
||||
targetValue = if (isDragging) 6.dp else 4.dp,
|
||||
label = "trackHeight",
|
||||
)
|
||||
return Seeker(
|
||||
value = value,
|
||||
readAheadValue = readAheadValue,
|
||||
range = range,
|
||||
onValueChangeFinished = {
|
||||
if (this.isDragging) {
|
||||
onValueChangeFinished(mutableValue)
|
||||
this.isDragging = false
|
||||
}
|
||||
},
|
||||
onValueChange = {
|
||||
mutableValue = it
|
||||
if (isDragging) {
|
||||
val wasDragging = this.isDragging
|
||||
this.isDragging = true
|
||||
onValueChange(mutableValue, wasDragging)
|
||||
} else {
|
||||
onValueChangeFinished(mutableValue)
|
||||
}
|
||||
},
|
||||
segments = validSegments,
|
||||
colors = SeekerDefaults.seekerColors(
|
||||
progressColor = MaterialTheme.colorScheme.primary,
|
||||
readAheadColor = MaterialTheme.colorScheme.onSurface,
|
||||
trackColor = MaterialTheme.colorScheme.surface,
|
||||
thumbColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
dimensions = SeekerDefaults.seekerDimensions(
|
||||
trackHeight = trackHeight,
|
||||
gap = gap,
|
||||
thumbRadius = thumbRadius,
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
return Seeker(
|
||||
value = position,
|
||||
readAheadValue = readAhead,
|
||||
range = range,
|
||||
onValueChangeFinished = {
|
||||
if (isSeeked) {
|
||||
onPositionChangeFinished(mutablePosition)
|
||||
isSeeked = false
|
||||
}
|
||||
},
|
||||
onValueChange = {
|
||||
mutablePosition = it
|
||||
if (isDragging) {
|
||||
val wasDragging = isSeeked
|
||||
isSeeked = true
|
||||
onPositionChange(mutablePosition, wasDragging)
|
||||
} else {
|
||||
onPositionChangeFinished(mutablePosition)
|
||||
}
|
||||
},
|
||||
segments = validSegments,
|
||||
colors = SeekerDefaults.seekerColors(
|
||||
progressColor = MaterialTheme.colorScheme.primary,
|
||||
readAheadColor = MaterialTheme.colorScheme.onSurface,
|
||||
trackColor = MaterialTheme.colorScheme.surface,
|
||||
thumbColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
dimensions = SeekerDefaults.seekerDimensions(
|
||||
trackHeight = trackHeight,
|
||||
gap = gap,
|
||||
thumbRadius = thumbRadius,
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -26,7 +26,7 @@ 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
|
||||
// credits: Saikou
|
||||
fun getResult(malId: Int, episodeNumber: Int, episodeLength: Long): List<Stamp>? {
|
||||
val url =
|
||||
"https://api.aniskip.com/v2/skip-times/$malId/$episodeNumber?types[]=ed" +
|
||||
|
@ -65,27 +65,21 @@ class AniSkipApi {
|
|||
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
|
||||
|
||||
internal suspend fun showSkipButton(skipType: SkipType) {
|
||||
internal fun showSkipButton(skipType: SkipType) {
|
||||
val skipButtonString = when (skipType) {
|
||||
SkipType.ED -> MR.strings.player_aniskip_ed
|
||||
SkipType.OP -> MR.strings.player_aniskip_op
|
||||
SkipType.RECAP -> MR.strings.player_aniskip_recap
|
||||
SkipType.MIXED_OP -> MR.strings.player_aniskip_mixedOp
|
||||
}
|
||||
withUIContext {
|
||||
playerControls.binding.controlsSkipIntroBtn.visibility = View.VISIBLE
|
||||
playerControls.binding.controlsSkipIntroBtn.text = activity.stringResource(
|
||||
skipButtonString,
|
||||
)
|
||||
}
|
||||
activity.viewModel.updateSkipIntroText(activity.stringResource(skipButtonString))
|
||||
}
|
||||
|
||||
// this is used when netflixStyle is enabled
|
||||
@SuppressLint("SetTextI18n")
|
||||
suspend fun showSkipButton(skipType: SkipType, waitingTime: Int) {
|
||||
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
|
||||
|
@ -94,12 +88,7 @@ class AniSkipApi {
|
|||
}
|
||||
if (waitingTime > -1) {
|
||||
if (waitingTime > 0) {
|
||||
withUIContext {
|
||||
playerControls.binding.controlsSkipIntroBtn.visibility = View.VISIBLE
|
||||
playerControls.binding.controlsSkipIntroBtn.text = activity.stringResource(
|
||||
MR.strings.player_aniskip_dontskip,
|
||||
)
|
||||
}
|
||||
activity.viewModel.updateSkipIntroText(activity.stringResource(MR.strings.player_aniskip_dontskip))
|
||||
} else {
|
||||
seekTo(skipTime.endTime)
|
||||
skipAnimation(skipType)
|
||||
|
|
|
@ -26,6 +26,11 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/controls_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
|
@ -7,386 +7,6 @@
|
|||
android:layout_height="match_parent"
|
||||
tools:ignore="RtlHardcoded,HardcodedText">
|
||||
|
||||
<!-- Locked player check -->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/lockedView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/unlockBtn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:contentDescription="Unlock player"
|
||||
android:src="@drawable/ic_unlock_20dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/unlocked_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#70000000">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/topControlsGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="visible">
|
||||
|
||||
|
||||
<!-- Top Controls (Right)-->
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/backArrowBtn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Go back"
|
||||
android:src="@drawable/ic_arrow_back_20dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/titleMainTxt"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleMainTxt"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textColor="?attr/colorOnPrimarySurface"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:layout_constraintLeft_toRightOf="@id/backArrowBtn"
|
||||
app:layout_constraintRight_toLeftOf="@id/episodeListBtn"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Anime Name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleSecondaryTxt"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:alpha="0.5"
|
||||
android:textColor="?attr/colorOnPrimarySurface"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="italic"
|
||||
android:maxLines="1"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:layout_constraintLeft_toRightOf="@id/backArrowBtn"
|
||||
app:layout_constraintRight_toLeftOf="@id/episodeListBtn"
|
||||
app:layout_constraintTop_toBottomOf="@id/titleMainTxt"
|
||||
tools:text="Episode Name" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/episodeListBtn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="Episode list"
|
||||
android:src="@drawable/ic_navigate_next_20dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@id/titleMainTxt"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<!-- Top Controls (Left)-->
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/settingsBtn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="Settings"
|
||||
android:src="@drawable/ic_overflow_20dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/streamsBtn"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/episodeListBtn"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/streamsBtn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="Tracks"
|
||||
android:src="@drawable/ic_video_settings_20dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/toggleAutoplay"
|
||||
app:layout_constraintRight_toLeftOf="@id/settingsBtn"
|
||||
app:layout_constraintTop_toTopOf="@id/settingsBtn"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/toggleAutoplay"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
tools:checked="true"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/streamsBtn"
|
||||
app:layout_constraintTop_toTopOf="@id/streamsBtn" />
|
||||
|
||||
<!-- Audio -->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="-"
|
||||
android:textColor="@color/tint_normal"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@id/minorTitleTextView"
|
||||
app:layout_constraintLeft_toLeftOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/minorTitleTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="-"
|
||||
android:textColor="@color/tint_normal"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="96dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/middleControlsGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="visible">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/prevBtn"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginRight="256dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_previous_episode"
|
||||
android:padding="@dimen/screen_edge_margin"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_skip_previous_40dp"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/play_btn"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Play/Pause"
|
||||
android:textColor="#FFF"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?attr/colorOnPrimarySurface"
|
||||
tools:src="@drawable/ic_play_arrow_64dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/nextBtn"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginLeft="256dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_next_episode"
|
||||
android:padding="@dimen/screen_edge_margin"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_skip_next_40dp"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playerInformation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="left"
|
||||
android:text="Information"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/play_btn"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/bottomControlsGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="50dp"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layoutDirection="ltr"
|
||||
android:visibility="visible">
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/currentChapter"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="80"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/bottomRightControlsGroup"
|
||||
app:layout_constraintStart_toEndOf="@+id/bottomLeftControlsGroup" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottomLeftControlsGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="50dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/lockBtn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="Lock player"
|
||||
android:src="@drawable/ic_lock_20dp"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/rotateBtn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="Rotate player"
|
||||
android:onClick="rotatePlayer"
|
||||
android:src="@drawable/ic_screen_rotation_20dp"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/cycleSpeedBtn"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:onClick="cycleSpeed"
|
||||
android:gravity="center"
|
||||
android:text="1.00x"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnPrimarySurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottomRightControlsGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="50dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/controls_skip_intro_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:backgroundTint="?attr/colorPrimary"
|
||||
android:onClick="skipIntro"
|
||||
android:text=""
|
||||
android:textSize="12sp"
|
||||
tools:text="+85s"
|
||||
android:textColor="?attr/colorOnPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/cycleViewModeBtn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="Cycle view modes"
|
||||
android:src="@drawable/ic_fullscreen_black_20dp"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/pipBtn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:contentDescription="@string/action_player_pip"
|
||||
android:src="@drawable/ic_picture_in_picture_20dp"
|
||||
android:visibility="visible"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/seekBarGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playbackPositionBtn"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="50dp"
|
||||
android:gravity="center"
|
||||
android:text="0:00"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#FFF"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent" />
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/playbackSeekbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="80"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@id/playbackPositionBtn"
|
||||
app:layout_constraintRight_toLeftOf="@id/playbackDurationBtn" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playbackDurationBtn"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="50dp"
|
||||
android:gravity="center"
|
||||
android:text="0:00"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#FFF"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
<!-- Extra Controls -->
|
||||
|
||||
<LinearLayout
|
||||
|
|
Loading…
Reference in a new issue