refactor(PlayerActivity): replace livedata with UiState StateFlow

This commit is contained in:
Jarne Demeulemeester 2023-07-31 23:21:41 +02:00
parent a4dc94b310
commit 270f7decaa
No known key found for this signature in database
GPG key ID: 1E5C6AFBD622E9F5
3 changed files with 86 additions and 63 deletions

View file

@ -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)

View file

@ -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

View file

@ -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]) }
}
}
}