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 android.widget.TextView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.view.isVisible 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.common.C
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.DefaultTimeBar 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.PlayerGestureHelper
import dev.jdtech.jellyfin.utils.PreviewScrubListener import dev.jdtech.jellyfin.utils.PreviewScrubListener
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import dev.jdtech.jellyfin.player.video.R as PlayerVideoR import dev.jdtech.jellyfin.player.video.R as PlayerVideoR
@ -42,6 +47,7 @@ class PlayerActivity : BasePlayerActivity() {
private var playerGestureHelper: PlayerGestureHelper? = null private var playerGestureHelper: PlayerGestureHelper? = null
override val viewModel: PlayerActivityViewModel by viewModels() override val viewModel: PlayerActivityViewModel by viewModels()
private val args: PlayerActivityArgs by navArgs() private val args: PlayerActivityArgs by navArgs()
private var previewScrubListener: PreviewScrubListener? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -79,10 +85,6 @@ class PlayerActivity : BasePlayerActivity() {
val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name) 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 audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
val subtitleButton = binding.playerView.findViewById<ImageButton>(R.id.btn_subtitle) val subtitleButton = binding.playerView.findViewById<ImageButton>(R.id.btn_subtitle)
val speedButton = binding.playerView.findViewById<ImageButton>(R.id.btn_speed) 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 lockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_lockview)
val unlockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_unlock) 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.isEnabled = false
audioButton.imageAlpha = 75 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) { if (appPreferences.playerTrickPlay) {
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview) val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress) val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
val previewScrubListener = PreviewScrubListener( previewScrubListener = PreviewScrubListener(
imagePreview, imagePreview,
timeBar, timeBar,
viewModel.player, viewModel.player,
viewModel.currentTrickPlay,
) )
timeBar.addListener(previewScrubListener) 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()
}
} }
viewModel.initializePlayer(args.items) viewModel.initializePlayer(args.items)

View file

@ -10,23 +10,21 @@ import coil.load
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import dev.jdtech.jellyfin.utils.bif.BifData import dev.jdtech.jellyfin.utils.bif.BifData
import dev.jdtech.jellyfin.utils.bif.BifUtil import dev.jdtech.jellyfin.utils.bif.BifUtil
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber import timber.log.Timber
class PreviewScrubListener( class PreviewScrubListener(
private val scrubbingPreview: ImageView, private val scrubbingPreview: ImageView,
private val timeBarView: View, private val timeBarView: View,
private val player: Player, private val player: Player,
private val currentTrickPlay: StateFlow<BifData?>,
) : TimeBar.OnScrubListener { ) : TimeBar.OnScrubListener {
var currentTrickPlay: BifData? = null
private val roundedCorners = RoundedCornersTransformation(10f) private val roundedCorners = RoundedCornersTransformation(10f)
private var currentBitMap: Bitmap? = null private var currentBitMap: Bitmap? = null
override fun onScrubStart(timeBar: TimeBar, position: Long) { override fun onScrubStart(timeBar: TimeBar, position: Long) {
Timber.d("Scrubbing started at $position") Timber.d("Scrubbing started at $position")
if (currentTrickPlay.value == null) { if (currentTrickPlay == null) {
return return
} }
@ -37,7 +35,7 @@ class PreviewScrubListener(
override fun onScrubMove(timeBar: TimeBar, position: Long) { override fun onScrubMove(timeBar: TimeBar, position: Long) {
Timber.d("Scrubbing to $position") Timber.d("Scrubbing to $position")
val currentBifData = currentTrickPlay.value ?: return val currentBifData = currentTrickPlay ?: return
val image = BifUtil.getTrickPlayFrame(position.toInt(), currentBifData) ?: return val image = BifUtil.getTrickPlayFrame(position.toInt(), currentBifData) ?: return
val parent = scrubbingPreview.parent as ViewGroup val parent = scrubbingPreview.parent as ViewGroup

View file

@ -3,8 +3,6 @@ package dev.jdtech.jellyfin.viewmodels
import android.app.Application import android.app.Application
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -30,8 +28,11 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@ -49,25 +50,32 @@ constructor(
) : ViewModel(), Player.Listener { ) : ViewModel(), Player.Listener {
val player: Player val player: Player
private val _navigateBack = MutableLiveData<Boolean>() private val _uiState = MutableStateFlow(
val navigateBack: LiveData<Boolean> = _navigateBack UiState(
currentItemTitle = "",
currentIntro = null,
currentTrickPlay = null,
fileLoaded = false,
),
)
val uiState = _uiState.asStateFlow()
private val _currentItemTitle = MutableLiveData<String>() private val _navigateBack = MutableSharedFlow<Boolean>()
val currentItemTitle: LiveData<String> = _currentItemTitle val navigateBack = _navigateBack.asSharedFlow()
private val intros: MutableMap<UUID, Intro> = mutableMapOf() 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 trickPlays: MutableMap<UUID, BifData> = mutableMapOf()
private val _currentTrickPlay = MutableStateFlow<BifData?>(null)
val currentTrickPlay = _currentTrickPlay.asStateFlow()
var currentAudioTracks: MutableList<MPVPlayer.Companion.Track> = mutableListOf() var currentAudioTracks: MutableList<MPVPlayer.Companion.Track> = mutableListOf()
var currentSubtitleTracks: MutableList<MPVPlayer.Companion.Track> = mutableListOf() var currentSubtitleTracks: MutableList<MPVPlayer.Companion.Track> = mutableListOf()
private val _fileLoaded = MutableLiveData(false) data class UiState(
val fileLoaded: LiveData<Boolean> = _fileLoaded val currentItemTitle: String,
val currentIntro: Intro?,
val currentTrickPlay: BifData?,
val fileLoaded: Boolean,
)
private var items: Array<PlayerItem> = arrayOf() private var items: Array<PlayerItem> = arrayOf()
@ -202,7 +210,7 @@ constructor(
} }
} }
_currentTrickPlay.value = null _uiState.update { it.copy(currentTrickPlay = null) }
playWhenReady = false playWhenReady = false
playbackPosition = 0L playbackPosition = 0L
currentMediaItemIndex = 0 currentMediaItemIndex = 0
@ -238,10 +246,10 @@ constructor(
intros[itemId]?.let { intro -> intros[itemId]?.let { intro ->
val seconds = player.currentPosition / 1000.0 val seconds = player.currentPosition / 1000.0
if (seconds > intro.showSkipPromptAt && seconds < intro.hideSkipPromptAt) { if (seconds > intro.showSkipPromptAt && seconds < intro.hideSkipPromptAt) {
_currentIntro.value = intro _uiState.update { it.copy(currentIntro = intro) }
return@let return@let
} }
_currentIntro.value = null _uiState.update { it.copy(currentIntro = null) }
} }
} }
handler.postDelayed(this, 1000L) handler.postDelayed(this, 1000L)
@ -258,16 +266,16 @@ constructor(
try { try {
items.first { it.itemId.toString() == player.currentMediaItem?.mediaId } items.first { it.itemId.toString() == player.currentMediaItem?.mediaId }
.let { item -> .let { item ->
if (item.parentIndexNumber != null && item.indexNumber != null val itemTitle = if (item.parentIndexNumber != null && item.indexNumber != null) {
) { if (item.indexNumberEnd == null) {
_currentItemTitle.value = if (item.indexNumberEnd == null) {
"S${item.parentIndexNumber}:E${item.indexNumber} - ${item.name}" "S${item.parentIndexNumber}:E${item.indexNumber} - ${item.name}"
} else { } else {
"S${item.parentIndexNumber}:E${item.indexNumber}-${item.indexNumberEnd} - ${item.name}" "S${item.parentIndexNumber}:E${item.indexNumber}-${item.indexNumberEnd} - ${item.name}"
} }
} else { } else {
_currentItemTitle.value = item.name item.name
} }
_uiState.update { it.copy(currentItemTitle = itemTitle) }
jellyfinRepository.postPlaybackStart(item.itemId) jellyfinRepository.postPlaybackStart(item.itemId)
@ -309,11 +317,11 @@ constructor(
} }
} }
} }
_fileLoaded.value = true _uiState.update { it.copy(fileLoaded = true) }
} }
ExoPlayer.STATE_ENDED -> { ExoPlayer.STATE_ENDED -> {
stateString = "ExoPlayer.STATE_ENDED -" stateString = "ExoPlayer.STATE_ENDED -"
_navigateBack.value = true _navigateBack.tryEmit(true)
} }
} }
Timber.d("Changed player state to $stateString") Timber.d("Changed player state to $stateString")
@ -356,7 +364,7 @@ constructor(
trickPlayData?.let { bifData -> trickPlayData?.let { bifData ->
Timber.d("Trickplay Images: ${bifData.imageCount}") Timber.d("Trickplay Images: ${bifData.imageCount}")
trickPlays[itemId] = bifData trickPlays[itemId] = bifData
_currentTrickPlay.value = trickPlays[itemId] _uiState.update { it.copy(currentTrickPlay = trickPlays[itemId]) }
} }
} }
} }