diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 09a80342..c7e2f14c 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -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(R.id.video_name) - viewModel.currentItemTitle.observe(this) { title -> - videoNameTextView.text = title - } - val audioButton = binding.playerView.findViewById(R.id.btn_audio_track) val subtitleButton = binding.playerView.findViewById(R.id.btn_subtitle) val speedButton = binding.playerView.findViewById(R.id.btn_speed) @@ -90,6 +92,51 @@ class PlayerActivity : BasePlayerActivity() { val lockButton = binding.playerView.findViewById(R.id.btn_lockview) val unlockButton = binding.playerView.findViewById(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(R.id.image_preview) val timeBar = binding.playerView.findViewById(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) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PreviewScrubListener.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PreviewScrubListener.kt index 166df358..bfe26e63 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PreviewScrubListener.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PreviewScrubListener.kt @@ -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, ) : 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 diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 0c76d7c3..ba93c550 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -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() - val navigateBack: LiveData = _navigateBack + private val _uiState = MutableStateFlow( + UiState( + currentItemTitle = "", + currentIntro = null, + currentTrickPlay = null, + fileLoaded = false, + ), + ) + val uiState = _uiState.asStateFlow() - private val _currentItemTitle = MutableLiveData() - val currentItemTitle: LiveData = _currentItemTitle + private val _navigateBack = MutableSharedFlow() + val navigateBack = _navigateBack.asSharedFlow() private val intros: MutableMap = mutableMapOf() - private val _currentIntro = MutableLiveData(null) - val currentIntro: LiveData = _currentIntro private val trickPlays: MutableMap = mutableMapOf() - private val _currentTrickPlay = MutableStateFlow(null) - val currentTrickPlay = _currentTrickPlay.asStateFlow() var currentAudioTracks: MutableList = mutableListOf() var currentSubtitleTracks: MutableList = mutableListOf() - private val _fileLoaded = MutableLiveData(false) - val fileLoaded: LiveData = _fileLoaded + data class UiState( + val currentItemTitle: String, + val currentIntro: Intro?, + val currentTrickPlay: BifData?, + val fileLoaded: Boolean, + ) private var items: Array = 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]) } } } }