ananas/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt
Jarne Demeulemeester 751ee75c3e
Provide better error messages with stacktrace (#119)
* Provide better error messages with stacktrace

+ clean up
+ fix error details popup in MediaInfoFragment

* Simplify exception passing by sending complete exception to the dialog

* Use viewLifecycleOwner with repeatOnLifecycle
2022-06-06 14:41:37 +02:00

249 lines
No EOL
9.7 KiB
Kotlin

package dev.jdtech.jellyfin.fragments
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.bindBaseItemImage
import dev.jdtech.jellyfin.databinding.EpisodeBottomSheetBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.setTintColor
import dev.jdtech.jellyfin.utils.setTintColorAttribute
import dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.LocationType
import timber.log.Timber
import java.util.*
@AndroidEntryPoint
class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
private val args: EpisodeBottomSheetFragmentArgs by navArgs()
private lateinit var binding: EpisodeBottomSheetBinding
private val viewModel: EpisodeBottomSheetViewModel by viewModels()
private val playerViewModel: PlayerViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = EpisodeBottomSheetBinding.inflate(inflater, container, false)
binding.playButton.setOnClickListener {
binding.playButton.setImageResource(android.R.color.transparent)
binding.progressCircular.isVisible = true
viewModel.item?.let {
if (!args.isOffline) {
playerViewModel.loadPlayerItems(it)
} else {
playerViewModel.loadOfflinePlayerItems(viewModel.playerItems[0])
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
Timber.d("$uiState")
when (uiState) {
is EpisodeBottomSheetViewModel.UiState.Normal -> bindUiStateNormal(uiState)
is EpisodeBottomSheetViewModel.UiState.Loading -> bindUiStateLoading()
is EpisodeBottomSheetViewModel.UiState.Error -> bindUiStateError(uiState)
}
}
}
}
playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems ->
when (playerItems) {
is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems)
is PlayerViewModel.PlayerItems -> bindPlayerItems(playerItems)
}
}
if (!args.isOffline) {
val episodeId: UUID = args.episodeId
binding.checkButton.setOnClickListener {
when (viewModel.played) {
true -> {
viewModel.markAsUnplayed(episodeId)
binding.checkButton.setTintColorAttribute(R.attr.colorOnSecondaryContainer, requireActivity().theme)
}
false -> {
viewModel.markAsPlayed(episodeId)
binding.checkButton.setTintColor(R.color.red, requireActivity().theme)
}
}
}
binding.favoriteButton.setOnClickListener {
when (viewModel.favorite) {
true -> {
viewModel.unmarkAsFavorite(episodeId)
binding.favoriteButton.setImageResource(R.drawable.ic_heart)
binding.favoriteButton.setTintColorAttribute(R.attr.colorOnSecondaryContainer, requireActivity().theme)
}
false -> {
viewModel.markAsFavorite(episodeId)
binding.favoriteButton.setImageResource(R.drawable.ic_heart_filled)
binding.favoriteButton.setTintColor(R.color.red, requireActivity().theme)
}
}
}
binding.downloadButton.setOnClickListener {
binding.downloadButton.isEnabled = false
viewModel.loadDownloadRequestItem(episodeId)
binding.downloadButton.setTintColor(R.color.red, requireActivity().theme)
}
viewModel.loadEpisode(episodeId)
} else {
val playerItem = args.playerItem!!
viewModel.loadEpisode(playerItem)
binding.deleteButton.isVisible = true
binding.deleteButton.setOnClickListener {
viewModel.deleteEpisode()
dismiss()
findNavController().navigate(R.id.downloadFragment)
}
binding.checkButton.isVisible = false
binding.favoriteButton.isVisible = false
binding.downloadButtonWrapper.isVisible = false
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
dialog?.let {
val sheet = it as BottomSheetDialog
sheet.behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
private fun bindUiStateNormal(uiState: EpisodeBottomSheetViewModel.UiState.Normal) {
uiState.apply {
if (episode.userData?.playedPercentage != null) {
binding.progressBar.layoutParams.width = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
(episode.userData?.playedPercentage?.times(1.26))!!.toFloat(),
context?.resources?.displayMetrics
).toInt()
binding.progressBar.isVisible = true
}
binding.playButton.isEnabled = available
binding.playButton.alpha = if (!available) 0.5F else 1.0F
// Check icon
when (played) {
true -> binding.checkButton.setTintColor(R.color.red, requireActivity().theme)
false -> binding.checkButton.setTintColorAttribute(R.attr.colorOnSecondaryContainer, requireActivity().theme)
}
// Favorite icon
val favoriteDrawable = when (favorite) {
true -> R.drawable.ic_heart_filled
false -> R.drawable.ic_heart
}
binding.favoriteButton.setImageResource(favoriteDrawable)
if (favorite) binding.favoriteButton.setTintColor(R.color.red, requireActivity().theme)
when (canDownload) {
true -> {
binding.downloadButtonWrapper.isVisible = true
binding.downloadButton.isEnabled = !downloaded
if (downloaded) binding.downloadButton.setTintColor(R.color.red, requireActivity().theme)
}
false -> {
binding.downloadButtonWrapper.isVisible = false
}
}
binding.episodeName.text = String.format(
getString(R.string.episode_name_extended),
episode.parentIndexNumber,
episode.indexNumber,
episode.name
)
binding.overview.text = episode.overview
binding.year.text = dateString
binding.playtime.text = runTime
binding.communityRating.isVisible = episode.communityRating != null
binding.communityRating.text = episode.communityRating.toString()
binding.missingIcon.isVisible = episode.locationType == LocationType.VIRTUAL
bindBaseItemImage(binding.episodeImage, episode)
}
binding.loadingIndicator.isVisible = false
}
private fun bindUiStateLoading() {
binding.loadingIndicator.isVisible = true
}
private fun bindUiStateError(uiState: EpisodeBottomSheetViewModel.UiState.Error) {
binding.loadingIndicator.isVisible = false
binding.overview.text = uiState.error.message
}
private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) {
navigateToPlayerActivity(items.items.toTypedArray())
binding.playButton.setImageDrawable(
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_play
)
)
binding.progressCircular.visibility = View.INVISIBLE
}
private fun bindPlayerItemsError(error: PlayerViewModel.PlayerItemError) {
Timber.e(error.error.message)
binding.playerItemsError.isVisible = true
binding.playButton.setImageDrawable(
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_play
)
)
binding.progressCircular.visibility = View.INVISIBLE
binding.playerItemsErrorDetails.setOnClickListener {
ErrorDialogFragment(error.error).show(parentFragmentManager, "errordialog")
}
}
private fun navigateToPlayerActivity(
playerItems: Array<PlayerItem>,
) {
findNavController().navigate(
EpisodeBottomSheetFragmentDirections.actionEpisodeBottomSheetFragmentToPlayerActivity(
playerItems,
)
)
}
}