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 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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue