refactor(PlayerActivity): replace livedata with UiState StateFlow
This commit is contained in:
parent
a4dc94b310
commit
270f7decaa
3 changed files with 86 additions and 63 deletions
|
@ -13,6 +13,9 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
|
@ -27,6 +30,8 @@ import dev.jdtech.jellyfin.mpv.TrackType
|
|||
import dev.jdtech.jellyfin.utils.PlayerGestureHelper
|
||||
import dev.jdtech.jellyfin.utils.PreviewScrubListener
|
||||
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import dev.jdtech.jellyfin.player.video.R as PlayerVideoR
|
||||
|
||||
|
@ -42,6 +47,7 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
private var playerGestureHelper: PlayerGestureHelper? = null
|
||||
override val viewModel: PlayerActivityViewModel by viewModels()
|
||||
private val args: PlayerActivityArgs by navArgs()
|
||||
private var previewScrubListener: PreviewScrubListener? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -79,10 +85,6 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
|
||||
val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name)
|
||||
|
||||
viewModel.currentItemTitle.observe(this) { title ->
|
||||
videoNameTextView.text = title
|
||||
}
|
||||
|
||||
val audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
|
||||
val subtitleButton = binding.playerView.findViewById<ImageButton>(R.id.btn_subtitle)
|
||||
val speedButton = binding.playerView.findViewById<ImageButton>(R.id.btn_speed)
|
||||
|
@ -90,6 +92,51 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
val lockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_lockview)
|
||||
val unlockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_unlock)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
viewModel.uiState.collect { uiState ->
|
||||
Timber.d("$uiState")
|
||||
uiState.apply {
|
||||
// Title
|
||||
videoNameTextView.text = currentItemTitle
|
||||
|
||||
// Skip Intro button
|
||||
skipIntroButton.isVisible = currentIntro != null
|
||||
skipIntroButton.setOnClickListener {
|
||||
currentIntro?.let {
|
||||
binding.playerView.player?.seekTo((it.introEnd * 1000).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
// Trick Play
|
||||
previewScrubListener?.let {
|
||||
it.currentTrickPlay = currentTrickPlay
|
||||
}
|
||||
|
||||
// File Loaded
|
||||
if (fileLoaded) {
|
||||
audioButton.isEnabled = true
|
||||
audioButton.imageAlpha = 255
|
||||
lockButton.isEnabled = true
|
||||
lockButton.imageAlpha = 255
|
||||
subtitleButton.isEnabled = true
|
||||
subtitleButton.imageAlpha = 255
|
||||
speedButton.isEnabled = true
|
||||
speedButton.imageAlpha = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.navigateBack.collect {
|
||||
if (it) finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioButton.isEnabled = false
|
||||
audioButton.imageAlpha = 75
|
||||
|
||||
|
@ -194,46 +241,16 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
viewModel.currentIntro.observe(this) {
|
||||
skipIntroButton.isVisible = it != null
|
||||
}
|
||||
|
||||
skipIntroButton.setOnClickListener {
|
||||
viewModel.currentIntro.value?.let {
|
||||
binding.playerView.player?.seekTo((it.introEnd * 1000).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
if (appPreferences.playerTrickPlay) {
|
||||
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
||||
val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
||||
val previewScrubListener = PreviewScrubListener(
|
||||
previewScrubListener = PreviewScrubListener(
|
||||
imagePreview,
|
||||
timeBar,
|
||||
viewModel.player,
|
||||
viewModel.currentTrickPlay,
|
||||
)
|
||||
|
||||
timeBar.addListener(previewScrubListener)
|
||||
}
|
||||
|
||||
viewModel.fileLoaded.observe(this) {
|
||||
if (it) {
|
||||
audioButton.isEnabled = true
|
||||
audioButton.imageAlpha = 255
|
||||
lockButton.isEnabled = true
|
||||
lockButton.imageAlpha = 255
|
||||
subtitleButton.isEnabled = true
|
||||
subtitleButton.imageAlpha = 255
|
||||
speedButton.isEnabled = true
|
||||
speedButton.imageAlpha = 255
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.navigateBack.observe(this) {
|
||||
if (it) {
|
||||
finish()
|
||||
}
|
||||
timeBar.addListener(previewScrubListener!!)
|
||||
}
|
||||
|
||||
viewModel.initializePlayer(args.items)
|
||||
|
|
|
@ -10,23 +10,21 @@ import coil.load
|
|||
import coil.transform.RoundedCornersTransformation
|
||||
import dev.jdtech.jellyfin.utils.bif.BifData
|
||||
import dev.jdtech.jellyfin.utils.bif.BifUtil
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import timber.log.Timber
|
||||
|
||||
class PreviewScrubListener(
|
||||
private val scrubbingPreview: ImageView,
|
||||
private val timeBarView: View,
|
||||
private val player: Player,
|
||||
private val currentTrickPlay: StateFlow<BifData?>,
|
||||
) : TimeBar.OnScrubListener {
|
||||
|
||||
var currentTrickPlay: BifData? = null
|
||||
private val roundedCorners = RoundedCornersTransformation(10f)
|
||||
private var currentBitMap: Bitmap? = null
|
||||
|
||||
override fun onScrubStart(timeBar: TimeBar, position: Long) {
|
||||
Timber.d("Scrubbing started at $position")
|
||||
|
||||
if (currentTrickPlay.value == null) {
|
||||
if (currentTrickPlay == null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -37,7 +35,7 @@ class PreviewScrubListener(
|
|||
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
||||
Timber.d("Scrubbing to $position")
|
||||
|
||||
val currentBifData = currentTrickPlay.value ?: return
|
||||
val currentBifData = currentTrickPlay ?: return
|
||||
val image = BifUtil.getTrickPlayFrame(position.toInt(), currentBifData) ?: return
|
||||
|
||||
val parent = scrubbingPreview.parent as ViewGroup
|
||||
|
|
|
@ -3,8 +3,6 @@ package dev.jdtech.jellyfin.viewmodels
|
|||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
@ -30,8 +28,11 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
@ -49,25 +50,32 @@ constructor(
|
|||
) : ViewModel(), Player.Listener {
|
||||
val player: Player
|
||||
|
||||
private val _navigateBack = MutableLiveData<Boolean>()
|
||||
val navigateBack: LiveData<Boolean> = _navigateBack
|
||||
private val _uiState = MutableStateFlow(
|
||||
UiState(
|
||||
currentItemTitle = "",
|
||||
currentIntro = null,
|
||||
currentTrickPlay = null,
|
||||
fileLoaded = false,
|
||||
),
|
||||
)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private val _currentItemTitle = MutableLiveData<String>()
|
||||
val currentItemTitle: LiveData<String> = _currentItemTitle
|
||||
private val _navigateBack = MutableSharedFlow<Boolean>()
|
||||
val navigateBack = _navigateBack.asSharedFlow()
|
||||
|
||||
private val intros: MutableMap<UUID, Intro> = mutableMapOf()
|
||||
private val _currentIntro = MutableLiveData<Intro?>(null)
|
||||
val currentIntro: LiveData<Intro?> = _currentIntro
|
||||
|
||||
private val trickPlays: MutableMap<UUID, BifData> = mutableMapOf()
|
||||
private val _currentTrickPlay = MutableStateFlow<BifData?>(null)
|
||||
val currentTrickPlay = _currentTrickPlay.asStateFlow()
|
||||
|
||||
var currentAudioTracks: MutableList<MPVPlayer.Companion.Track> = mutableListOf()
|
||||
var currentSubtitleTracks: MutableList<MPVPlayer.Companion.Track> = mutableListOf()
|
||||
|
||||
private val _fileLoaded = MutableLiveData(false)
|
||||
val fileLoaded: LiveData<Boolean> = _fileLoaded
|
||||
data class UiState(
|
||||
val currentItemTitle: String,
|
||||
val currentIntro: Intro?,
|
||||
val currentTrickPlay: BifData?,
|
||||
val fileLoaded: Boolean,
|
||||
)
|
||||
|
||||
private var items: Array<PlayerItem> = arrayOf()
|
||||
|
||||
|
@ -202,7 +210,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
_currentTrickPlay.value = null
|
||||
_uiState.update { it.copy(currentTrickPlay = null) }
|
||||
playWhenReady = false
|
||||
playbackPosition = 0L
|
||||
currentMediaItemIndex = 0
|
||||
|
@ -238,10 +246,10 @@ constructor(
|
|||
intros[itemId]?.let { intro ->
|
||||
val seconds = player.currentPosition / 1000.0
|
||||
if (seconds > intro.showSkipPromptAt && seconds < intro.hideSkipPromptAt) {
|
||||
_currentIntro.value = intro
|
||||
_uiState.update { it.copy(currentIntro = intro) }
|
||||
return@let
|
||||
}
|
||||
_currentIntro.value = null
|
||||
_uiState.update { it.copy(currentIntro = null) }
|
||||
}
|
||||
}
|
||||
handler.postDelayed(this, 1000L)
|
||||
|
@ -258,16 +266,16 @@ constructor(
|
|||
try {
|
||||
items.first { it.itemId.toString() == player.currentMediaItem?.mediaId }
|
||||
.let { item ->
|
||||
if (item.parentIndexNumber != null && item.indexNumber != null
|
||||
) {
|
||||
_currentItemTitle.value = if (item.indexNumberEnd == null) {
|
||||
val itemTitle = if (item.parentIndexNumber != null && item.indexNumber != null) {
|
||||
if (item.indexNumberEnd == null) {
|
||||
"S${item.parentIndexNumber}:E${item.indexNumber} - ${item.name}"
|
||||
} else {
|
||||
"S${item.parentIndexNumber}:E${item.indexNumber}-${item.indexNumberEnd} - ${item.name}"
|
||||
}
|
||||
} else {
|
||||
_currentItemTitle.value = item.name
|
||||
item.name
|
||||
}
|
||||
_uiState.update { it.copy(currentItemTitle = itemTitle) }
|
||||
|
||||
jellyfinRepository.postPlaybackStart(item.itemId)
|
||||
|
||||
|
@ -309,11 +317,11 @@ constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
_fileLoaded.value = true
|
||||
_uiState.update { it.copy(fileLoaded = true) }
|
||||
}
|
||||
ExoPlayer.STATE_ENDED -> {
|
||||
stateString = "ExoPlayer.STATE_ENDED -"
|
||||
_navigateBack.value = true
|
||||
_navigateBack.tryEmit(true)
|
||||
}
|
||||
}
|
||||
Timber.d("Changed player state to $stateString")
|
||||
|
@ -356,7 +364,7 @@ constructor(
|
|||
trickPlayData?.let { bifData ->
|
||||
Timber.d("Trickplay Images: ${bifData.imageCount}")
|
||||
trickPlays[itemId] = bifData
|
||||
_currentTrickPlay.value = trickPlays[itemId]
|
||||
_uiState.update { it.copy(currentTrickPlay = trickPlays[itemId]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue